From bd2c916e33434b767fa89fa1305ff37e1ff8ffc6 Mon Sep 17 00:00:00 2001 From: Alexandre Plennevaux Date: Wed, 24 Jun 2026 17:28:49 +0200 Subject: [PATCH] Add community.lexicon.app lexicons (#76) --- community/lexicon/app/README.md | 118 +++++++ community/lexicon/app/defs.json | 295 ++++++++++++++++++ community/lexicon/app/entry.json | 110 +++++++ community/lexicon/app/profile.json | 96 ++++++ .../lexicon/app/profileLocalization.json | 74 +++++ community/lexicon/preference/ai.json | 3 +- 6 files changed, 695 insertions(+), 1 deletion(-) create mode 100644 community/lexicon/app/README.md create mode 100644 community/lexicon/app/defs.json create mode 100644 community/lexicon/app/entry.json create mode 100644 community/lexicon/app/profile.json create mode 100644 community/lexicon/app/profileLocalization.json diff --git a/community/lexicon/app/README.md b/community/lexicon/app/README.md new file mode 100644 index 0000000..4a46c38 --- /dev/null +++ b/community/lexicon/app/README.md @@ -0,0 +1,118 @@ +# community.lexicon.app + +This set of Lexicon schemas describes apps built on or for the AT Protocol. + +## Records + +* `profile`: A canonical app profile published by the app's DID at rkey + `self`. + +* `profileLocalization`: Locale-specific metadata for a canonical app profile. + These records should be published by the same DID as the profile. Publishers + should use normalized BCP 47 language tags as rkeys when possible, such as + `fr` or `pt-BR`; consumers should trust the record's `locale` field over the + rkey. + +* `entry`: A community or third-party app listing. Any account can publish an + entry, including entries for apps they do not own. + +## Canonical profiles + +Entries can name a `profileDid`. Consumers can derive the canonical profile URI +from that DID: + +```text +at:///community.lexicon.app.profile/self +``` + +An entry without `profileDid` should be treated as a standalone third-party +listing. A `profileDid` is a pointer to where a canonical profile may exist, not +proof of ownership or verification by itself. + +## Links + +Records use `links` for all external and protocol destinations. The first link +should be the primary app destination. A Web App Manifest is represented as a +normal link with a known role: + +```json +{ + "role": "community.lexicon.app.defs#linkRoleWebManifest", + "label": "Web App Manifest", + "uri": "https://example.com/manifest.webmanifest" +} +``` + +Distribution URLs, such as App Store, Play Store, and F-Droid listings, also +belong in `links` with their corresponding roles. + +## Discovery signals + +Use `platforms` for platform filtering, `lexicons` for AT Protocol +interoperability, and `tags` for freeform discovery or category labels. Tags +should not duplicate platforms, lexicon interop, or language fields. + +The `lexicons` field is self-declared and may be incomplete. Directories can use +it to answer questions like "which apps consume or produce this lexicon?", but +should not treat it as verified/audited truth. + +The `accountIndicators` field lists records whose presence in an account can +suggest that the account probably uses the app. For example: + +```json +{ + "collection": "org.passingreads.actor.profile", + "rkey": "self" +} +``` + +This is only a heuristic usage signal. It does not prove active use, consent, or +endorsement. + +## Internationalization + +Entries can include `langs` to describe the human language of the entry text, +matching the convention used by `app.bsky.feed.post`. This describes the +listing, not every language supported by the app UI. + +App publishers can add `profileLocalization` records for authoritative localized +names, descriptions, links, images, and discovery tags. + +Publishers SHOULD use a lowercase BCP 47 language tag as the localization record +rkey (e.g. `fr`, `pt-br`, `zh-hant`) so the rkey acts as a natural uniqueness +key per locale. Publishers SHOULD publish at most one `profileLocalization` +record per locale. If duplicate records exist for the same locale, consumers +SHOULD prefer the one with the most recent `updatedAt` or `createdAt` timestamp. + +## Rich app metadata + +Records can include an `images` array (up to 24 items) for icons, logos, hero +images, screenshots, banners, social cards, app store visuals, ads, or other +directory media. Each item MUST include `alt` text and exactly one of `image` +(an ATProto blob) or `uri` (a remote URL). Consumers MUST ignore items that +carry neither or both. `purpose` and `aspectRatio` are optional per item; +multiple items may share the same `purpose` (e.g. several screenshots or hero +variants are all valid). If no known image purpose fits, omit `purpose`. + +Web App Manifests can still prefill or augment install behavior, icons, +screenshots, language, and platform or form-factor metadata, but they are linked +through `links` rather than modeled as a top-level field. + +## Trust and verification + +`profileDid` in an `entry` record is an unverified claim made by whoever +published the entry -- it is not proof that the named DID endorses the listing. +Directories SHOULD treat it purely as a hint. Before labeling an entry +"official" or "verified", directories SHOULD: + +1. Fetch the record at `at:///community.lexicon.app.profile/self` + and prefer its fields over any data copied into the entry. +2. Verify ownership out of band using methods such as `rel=me`, `.well-known` + resources, or other trust policies. + +Verification is otherwise intentionally out of scope for these records. + +## Deferred ideas + +License metadata and backend deployment/self-hosting signals are useful follow-up +ideas, but are intentionally left out of this v1 proposal. diff --git a/community/lexicon/app/defs.json b/community/lexicon/app/defs.json new file mode 100644 index 0000000..34ea759 --- /dev/null +++ b/community/lexicon/app/defs.json @@ -0,0 +1,295 @@ +{ + "lexicon": 1, + "id": "community.lexicon.app.defs", + "defs": { + "link": { + "type": "object", + "description": "A labeled URI associated with an app.", + "required": ["uri"], + "properties": { + "uri": { + "type": "string", + "format": "uri", + "description": "Destination URI." + }, + "label": { + "type": "string", + "maxLength": 100, + "maxGraphemes": 50, + "description": "Human-readable label for the URI." + }, + "role": { + "type": "ref", + "ref": "#linkRole", + "description": "Known role of this link, if any." + } + } + }, + "linkRole": { + "type": "string", + "description": "Known role of a link associated with an app.", + "knownValues": [ + "community.lexicon.app.defs#linkRoleWebsite", + "community.lexicon.app.defs#linkRoleWebManifest", + "community.lexicon.app.defs#linkRolePrivacyPolicy", + "community.lexicon.app.defs#linkRoleTermsOfService", + "community.lexicon.app.defs#linkRoleSupport", + "community.lexicon.app.defs#linkRoleSourceCode", + "community.lexicon.app.defs#linkRoleDocs", + "community.lexicon.app.defs#linkRoleChangelog", + "community.lexicon.app.defs#linkRoleStatus", + "community.lexicon.app.defs#linkRoleAppStore", + "community.lexicon.app.defs#linkRolePlayStore", + "community.lexicon.app.defs#linkRoleFDroid" + ] + }, + "linkRoleWebsite": { + "type": "token", + "description": "Primary website or landing page." + }, + "linkRoleWebManifest": { + "type": "token", + "description": "Web App Manifest." + }, + "linkRolePrivacyPolicy": { + "type": "token", + "description": "Privacy policy." + }, + "linkRoleTermsOfService": { + "type": "token", + "description": "Terms of service." + }, + "linkRoleSupport": { + "type": "token", + "description": "Support, help, or contact page." + }, + "linkRoleSourceCode": { + "type": "token", + "description": "Source code repository or source distribution." + }, + "linkRoleDocs": { + "type": "token", + "description": "Documentation." + }, + "linkRoleChangelog": { + "type": "token", + "description": "Changelog or release notes." + }, + "linkRoleStatus": { + "type": "token", + "description": "Service status page." + }, + "linkRoleAppStore": { + "type": "token", + "description": "Apple App Store listing." + }, + "linkRolePlayStore": { + "type": "token", + "description": "Google Play Store listing." + }, + "linkRoleFDroid": { + "type": "token", + "description": "F-Droid listing." + }, + "image": { + "type": "object", + "description": "An image associated with an app, including accessibility and display metadata. Exactly one of `image` (ATProto blob) or `uri` (remote URL) MUST be present. Consumers SHOULD ignore image items that have neither or both.", + "required": ["alt"], + "properties": { + "purpose": { + "type": "string", + "knownValues": [ + "community.lexicon.app.defs#purposeIcon", + "community.lexicon.app.defs#purposeLogo", + "community.lexicon.app.defs#purposeHero", + "community.lexicon.app.defs#purposeScreenshot", + "community.lexicon.app.defs#purposeBanner", + "community.lexicon.app.defs#purposeSocialCard", + "community.lexicon.app.defs#purposeAppStore", + "community.lexicon.app.defs#purposeAd" + ], + "description": "How directories and stores should use the image. Omit this field when no known purpose fits." + }, + "image": { + "type": "blob", + "description": "The raw image file.", + "accept": ["image/*"], + "maxSize": 2000000 + }, + "uri": { + "type": "string", + "format": "uri", + "description": "Remote image URI." + }, + "alt": { + "type": "string", + "maxLength": 1000, + "maxGraphemes": 300, + "description": "Alt text description of the image, for accessibility." + }, + "aspectRatio": { + "type": "ref", + "ref": "#aspectRatio" + } + } + }, + "aspectRatio": { + "type": "object", + "description": "width:height represents an aspect ratio. It may be approximate.", + "required": ["width", "height"], + "properties": { + "width": { + "type": "integer", + "minimum": 1 + }, + "height": { + "type": "integer", + "minimum": 1 + } + } + }, + "purposeIcon": { + "type": "token", + "description": "A small square icon representing the app, typically used in launchers, lists, and tab bars." + }, + "purposeLogo": { + "type": "token", + "description": "A logotype or wordmark for the app." + }, + "purposeHero": { + "type": "token", + "description": "A large promotional or feature image, typically used at the top of a directory listing." + }, + "purposeScreenshot": { + "type": "token", + "description": "A screenshot of the app's UI, used in directory and store listings." + }, + "purposeBanner": { + "type": "token", + "description": "A wide banner image, such as a header image for a profile or listing page." + }, + "purposeSocialCard": { + "type": "token", + "description": "An image sized and formatted for social media sharing previews (Open Graph, Twitter Card, etc.)." + }, + "purposeAppStore": { + "type": "token", + "description": "A promotional image formatted for a native app store listing." + }, + "purposeAd": { + "type": "token", + "description": "A promotional or advertising image." + }, + "status": { + "type": "string", + "description": "Current release or maintenance status of an app.", + "knownValues": [ + "community.lexicon.app.defs#unreleased", + "community.lexicon.app.defs#preview", + "community.lexicon.app.defs#released", + "community.lexicon.app.defs#unmaintained", + "community.lexicon.app.defs#discontinued" + ] + }, + "unreleased": { + "type": "token", + "description": "The app has been announced but is not yet available." + }, + "preview": { + "type": "token", + "description": "The app is available as an alpha, beta, early access, or preview release." + }, + "released": { + "type": "token", + "description": "The app is generally available." + }, + "unmaintained": { + "type": "token", + "description": "The app may still be available, but is no longer actively maintained." + }, + "discontinued": { + "type": "token", + "description": "The app is no longer available or supported." + }, + "platform": { + "type": "string", + "description": "Platform where an app is available.", + "knownValues": [ + "community.lexicon.app.defs#platformWeb", + "community.lexicon.app.defs#platformIOS", + "community.lexicon.app.defs#platformAndroid", + "community.lexicon.app.defs#platformMacOS", + "community.lexicon.app.defs#platformWindows", + "community.lexicon.app.defs#platformLinux", + "community.lexicon.app.defs#platformCLI" + ] + }, + "platformWeb": { + "type": "token", + "description": "Web app or website." + }, + "platformIOS": { + "type": "token", + "description": "iOS app." + }, + "platformAndroid": { + "type": "token", + "description": "Android app." + }, + "platformMacOS": { + "type": "token", + "description": "macOS app." + }, + "platformWindows": { + "type": "token", + "description": "Windows app." + }, + "platformLinux": { + "type": "token", + "description": "Linux app." + }, + "platformCLI": { + "type": "token", + "description": "Command line interface." + }, + "lexiconInterop": { + "type": "object", + "description": "Self-declared AT Protocol lexicon interoperability signals.", + "properties": { + "produces": { + "type": "array", + "description": "Lexicon collections this app creates or publishes records for.", + "items": { + "type": "string", + "format": "nsid" + } + }, + "consumes": { + "type": "array", + "description": "Lexicon collections this app reads, displays, imports, or otherwise consumes.", + "items": { + "type": "string", + "format": "nsid" + } + } + } + }, + "accountIndicator": { + "type": "object", + "description": "A record whose presence in an account can indicate that the account probably uses this app.", + "required": ["collection"], + "properties": { + "collection": { + "type": "string", + "format": "nsid", + "description": "Record collection to look for in an account repository." + }, + "rkey": { + "type": "string", + "format": "record-key", + "description": "Optional record key to look for within the collection." + } + } + } + } +} diff --git a/community/lexicon/app/entry.json b/community/lexicon/app/entry.json new file mode 100644 index 0000000..d854b61 --- /dev/null +++ b/community/lexicon/app/entry.json @@ -0,0 +1,110 @@ +{ + "lexicon": 1, + "id": "community.lexicon.app.entry", + "defs": { + "main": { + "type": "record", + "description": "A third-party or community listing for an app built on or for the AT Protocol.", + "key": "tid", + "record": { + "type": "object", + "required": ["name", "links", "createdAt"], + "properties": { + "name": { + "type": "string", + "maxLength": 200, + "maxGraphemes": 100, + "description": "The display name of the app." + }, + "description": { + "type": "string", + "maxLength": 3000, + "maxGraphemes": 300, + "description": "A short description of what the app does." + }, + "images": { + "type": "array", + "maxLength": 24, + "description": "Visual assets for directories and stores, such as icons, hero images, screenshots, banners, and social cards.", + "items": { + "type": "ref", + "ref": "community.lexicon.app.defs#image" + } + }, + "tags": { + "type": "array", + "maxLength": 10, + "items": { + "type": "string", + "maxLength": 64, + "maxGraphemes": 32 + }, + "description": "Open discovery tags for filtering and search, preferably lowercase. Do not use tags for platforms, lexicon interoperability, or listing language." + }, + "status": { + "type": "ref", + "ref": "community.lexicon.app.defs#status", + "description": "Current release or maintenance status of the app." + }, + "profileDid": { + "type": "string", + "format": "did", + "description": "DID expected to publish the canonical app profile at community.lexicon.app.profile/self." + }, + "langs": { + "type": "array", + "description": "Human language of this entry's text. This describes the listing, not the languages supported by the app.", + "maxLength": 3, + "items": { + "type": "string", + "format": "language" + } + }, + "links": { + "type": "array", + "minLength": 1, + "maxLength": 12, + "description": "Relevant destinations for the app. The first link should be the primary destination.", + "items": { + "type": "ref", + "ref": "community.lexicon.app.defs#link" + } + }, + "platforms": { + "type": "array", + "maxLength": 12, + "description": "Platforms where this app is available.", + "items": { + "type": "ref", + "ref": "community.lexicon.app.defs#platform" + } + }, + "lexicons": { + "type": "ref", + "ref": "community.lexicon.app.defs#lexiconInterop", + "description": "Self-declared AT Protocol lexicon interoperability signals." + }, + "accountIndicators": { + "type": "array", + "maxLength": 12, + "description": "Records whose presence in an account can indicate that the account probably uses this app.", + "items": { + "type": "ref", + "ref": "community.lexicon.app.defs#accountIndicator" + } + }, + "createdAt": { + "type": "string", + "format": "datetime", + "description": "Client-declared timestamp when this entry was created." + }, + "updatedAt": { + "type": "string", + "format": "datetime", + "description": "Client-declared timestamp when this entry was last updated." + } + } + } + } + } +} diff --git a/community/lexicon/app/profile.json b/community/lexicon/app/profile.json new file mode 100644 index 0000000..aa4d18f --- /dev/null +++ b/community/lexicon/app/profile.json @@ -0,0 +1,96 @@ +{ + "lexicon": 1, + "id": "community.lexicon.app.profile", + "defs": { + "main": { + "type": "record", + "description": "The canonical self-published profile for an app built on or for the AT Protocol.", + "key": "literal:self", + "record": { + "type": "object", + "required": ["name", "links", "createdAt"], + "properties": { + "name": { + "type": "string", + "maxLength": 200, + "maxGraphemes": 100, + "description": "The display name of the app." + }, + "description": { + "type": "string", + "maxLength": 3000, + "maxGraphemes": 300, + "description": "A short description of what the app does." + }, + "images": { + "type": "array", + "maxLength": 24, + "description": "Visual assets for directories and stores, such as icons, hero images, screenshots, banners, and social cards.", + "items": { + "type": "ref", + "ref": "community.lexicon.app.defs#image" + } + }, + "tags": { + "type": "array", + "maxLength": 10, + "items": { + "type": "string", + "maxLength": 64, + "maxGraphemes": 32 + }, + "description": "Open discovery tags for filtering and search, preferably lowercase. Do not use tags for platforms or lexicon interoperability." + }, + "status": { + "type": "ref", + "ref": "community.lexicon.app.defs#status", + "description": "Current release or maintenance status of the app." + }, + "links": { + "type": "array", + "minLength": 1, + "maxLength": 12, + "description": "Relevant destinations for the app. The first link should be the primary destination.", + "items": { + "type": "ref", + "ref": "community.lexicon.app.defs#link" + } + }, + "platforms": { + "type": "array", + "maxLength": 12, + "description": "Platforms where this app is available.", + "items": { + "type": "ref", + "ref": "community.lexicon.app.defs#platform" + } + }, + "lexicons": { + "type": "ref", + "ref": "community.lexicon.app.defs#lexiconInterop", + "description": "Self-declared AT Protocol lexicon interoperability signals." + }, + "accountIndicators": { + "type": "array", + "maxLength": 12, + "description": "Records whose presence in an account can indicate that the account probably uses this app.", + "items": { + "type": "ref", + "ref": "community.lexicon.app.defs#accountIndicator" + } + }, + "createdAt": { + "type": "string", + "format": "datetime", + "description": "Client-declared timestamp when this profile was created." + }, + "updatedAt": { + "type": "string", + "format": "datetime", + "description": "Client-declared timestamp when this profile was last updated." + } + } + } + } + } +} diff --git a/community/lexicon/app/profileLocalization.json b/community/lexicon/app/profileLocalization.json new file mode 100644 index 0000000..60edc2e --- /dev/null +++ b/community/lexicon/app/profileLocalization.json @@ -0,0 +1,74 @@ +{ + "lexicon": 1, + "id": "community.lexicon.app.profileLocalization", + "description": "Localized metadata for a community.lexicon.app.profile record.", + "defs": { + "main": { + "type": "record", + "description": "A locale-specific override for an app profile. This record should be published by the same DID as the canonical app profile.", + "key": "any", + "record": { + "type": "object", + "required": ["locale", "createdAt"], + "properties": { + "locale": { + "type": "string", + "format": "language", + "description": "BCP 47 language tag for this localized metadata." + }, + "name": { + "type": "string", + "maxLength": 200, + "maxGraphemes": 100, + "description": "Localized display name of the app." + }, + "description": { + "type": "string", + "maxLength": 3000, + "maxGraphemes": 300, + "description": "Localized description of what the app does." + }, + "images": { + "type": "array", + "maxLength": 24, + "description": "Localized visual assets for directories and stores, such as icons, hero images, screenshots, banners, and social cards.", + "items": { + "type": "ref", + "ref": "community.lexicon.app.defs#image" + } + }, + "tags": { + "type": "array", + "maxLength": 10, + "items": { + "type": "string", + "maxLength": 64, + "maxGraphemes": 32 + }, + "description": "Localized discovery tags for filtering and search." + }, + "links": { + "type": "array", + "minLength": 1, + "maxLength": 12, + "description": "Localized destinations for the app. The first link should be the primary destination for this locale.", + "items": { + "type": "ref", + "ref": "community.lexicon.app.defs#link" + } + }, + "createdAt": { + "type": "string", + "format": "datetime", + "description": "Client-declared timestamp when this localization was created." + }, + "updatedAt": { + "type": "string", + "format": "datetime", + "description": "Client-declared timestamp when this localization was last updated." + } + } + } + } + } +} diff --git a/community/lexicon/preference/ai.json b/community/lexicon/preference/ai.json index bb87ee6..18d8bb5 100644 --- a/community/lexicon/preference/ai.json +++ b/community/lexicon/preference/ai.json @@ -70,7 +70,8 @@ }, "globalScope": { "type": "object", - "description": "Account-wide default. The record at key 'self' should carry this scope." + "description": "Account-wide default. The record at key 'self' should carry this scope.", + "properties": {} }, "entityScope": { "type": "object", -- 2.51.2