From 72c256e70e4d07f409e44466555597a47918ac4d Mon Sep 17 00:00:00 2001 From: Bretton <36870434+BrettM86@users.noreply.github.com> Date: Thu, 23 Jul 2026 22:02:12 -0700 Subject: [PATCH] chore(lexicon): cap remaining unbounded strings per goat lint; add publish runbook goat lex lint flagged 77 string fields with no maxLength across params, outputs, and knownValues tokens. Cap them all (cursors 500, knownValues 64, plus per-field caps for email/password/JWTs/api-keys/search queries). Remaining large-string warnings are intentional (base64 image fields, long-form content at the 10:1 grapheme ratio). Add docs/LEXICON_PUBLISHING.md (publish/hold-back sets, decisions log, workflow) and scripts/publish-lexicon-dns.sh (idempotent Cloudflare API upsert of the 9 _lexicon TXT delegations; moderation deferred via flag). Co-Authored-By: Claude Fable 5 --- docs/LEXICON_PUBLISHING.md | 105 ++++++++++++++++++ .../social/coves/actor/getComments.json | 14 ++- .../lexicon/social/coves/actor/getPosts.json | 23 +++- .../lexicon/social/coves/actor/signup.json | 28 +++-- .../social/coves/aggregator/createApiKey.json | 13 ++- .../lexicon/social/coves/aggregator/defs.json | 43 +++++-- .../coves/aggregator/getAuthorizations.json | 14 ++- .../coves/aggregator/listForCommunity.json | 14 ++- .../social/coves/aggregator/register.json | 16 ++- .../social/coves/community/comment/defs.json | 64 +++++++++-- .../coves/community/comment/getComments.json | 36 ++++-- .../lexicon/social/coves/community/defs.json | 9 +- .../social/coves/community/getMembers.json | 6 +- .../coves/community/getSubscribers.json | 6 +- .../lexicon/social/coves/community/list.json | 31 ++++-- .../social/coves/community/post/defs.json | 6 +- .../social/coves/community/post/search.json | 55 ++++++--- .../social/coves/community/search.json | 22 ++-- .../lexicon/social/coves/embed/external.json | 6 +- .../lexicon/social/coves/feed/getAll.json | 53 +++++++-- .../social/coves/feed/getCommunity.json | 37 ++++-- .../social/coves/feed/getDiscover.json | 31 ++++-- .../social/coves/feed/getTimeline.json | 33 ++++-- .../lexicon/social/coves/feed/vote.json | 16 ++- .../social/coves/feed/vote/create.json | 13 ++- .../lexicon/social/coves/moderation/ban.json | 24 +++- .../social/coves/moderation/listBans.json | 26 +++-- scripts/publish-lexicon-dns.sh | 70 ++++++++++++ 28 files changed, 661 insertions(+), 153 deletions(-) create mode 100644 docs/LEXICON_PUBLISHING.md create mode 100755 scripts/publish-lexicon-dns.sh diff --git a/docs/LEXICON_PUBLISHING.md b/docs/LEXICON_PUBLISHING.md new file mode 100644 index 0000000..2060f5f --- /dev/null +++ b/docs/LEXICON_PUBLISHING.md @@ -0,0 +1,105 @@ +# Publishing the social.coves.* Lexicons + +The lexicons under `internal/atproto/lexicon/social/coves/` are published to the +AT Protocol network as `com.atproto.lexicon.schema` records, resolvable via +`_lexicon` DNS TXT records on `coves.social`. Once published, atproto schema +evolution rules apply: **additive changes only** (new optional fields, new +knownValues). Anything breaking-shaped needs a new NSID. + +## Design decisions (do not revisit casually) + +- **Nested NSIDs are deliberate.** Operations live under their record noun + (`community.post.create` under the `community.post` record) rather than + Bluesky-style flat verbs (`createPost`). This is an API-organization choice; + the extra DNS delegations it requires are handled by the script below. +- **Embed `alt` text is optional, permanently** (decided 2026-07-23) — matches + live bridge content and clients. +- **`updateProfile` input takes `bio`; the record stores `description`.** Wire + truth on both sides. + +## What is published vs held back + +**Published:** `richtext.*`, `embed.*`, `actor.*`, `feed.*`, `community.*` +(including `post.*` and `comment.*` — but NOT `rules` or `moderator`), +`aggregator.*`. + +**Held back until tribunal/governance design lands:** the entire +`moderation.*` namespace, `community.rules`, `community.moderator`. Publish +them later with `--include-moderation` on the DNS script plus a `goat lex +publish` of those files. + +## One-time setup + +1. **Publishing account.** The schema records live in a normal atproto repo. + Create a dedicated account so the publishing identity is project-owned: + ```sh + goat account create \ + --pds-host https://pds.coves.me \ + --handle lexicons.coves.social \ + --password '' \ + --email \ + [--invite-code ] + ``` + Note the DID it prints (`goat resolve lexicons.coves.social` shows it). + An existing account works too — the DNS records just point at whichever DID + holds the schema records. + +2. **DNS delegation.** One TXT record per unique NSID authority: + + | Record name | Content | + |---|---| + | `_lexicon.actor.coves.social` | `did=` | + | `_lexicon.aggregator.coves.social` | `did=` | + | `_lexicon.community.coves.social` | `did=` | + | `_lexicon.comment.community.coves.social` | `did=` | + | `_lexicon.post.community.coves.social` | `did=` | + | `_lexicon.embed.coves.social` | `did=` | + | `_lexicon.feed.coves.social` | `did=` | + | `_lexicon.vote.feed.coves.social` | `did=` | + | `_lexicon.richtext.coves.social` | `did=` | + | `_lexicon.moderation.coves.social` | *(deferred with moderation)* | + + Via the Cloudflare API (token needs Zone:DNS:Edit on `coves.social`): + ```sh + CF_API_TOKEN=... LEXICON_DID=did:plc:... ./scripts/publish-lexicon-dns.sh + ``` + +## Publish / update workflow + +```sh +# 0. Gates — all must pass +go run ./cmd/validate-lexicon # schemas + fixtures +go run ./cmd/validate-live -pds https://pds.coves.me # no live record invalidated +goat lex lint internal/atproto/lexicon/social/coves # style (large-string warns are intentional) +goat lex breaking internal/atproto/lexicon/social/coves # evolution rules vs published state + +# 1. Log in as the publishing account (session cached by goat) +goat account login -u lexicons.coves.social -p '' --pds-host https://pds.coves.me + +# 2. Publish. Safety properties of `goat lex publish`: +# - refuses NSIDs whose _lexicon DNS doesn't point at the logged-in account, +# so the whole moderation/* namespace is auto-excluded until its DNS +# record exists (never pass --skip-dns-check) +# - skips schemas identical to what's already live; --update required to +# overwrite changed ones +export LEXICONS_DIR=internal/atproto/lexicon +goat lex publish \ + internal/atproto/lexicon/social/coves/richtext \ + internal/atproto/lexicon/social/coves/embed \ + internal/atproto/lexicon/social/coves/actor \ + internal/atproto/lexicon/social/coves/feed \ + internal/atproto/lexicon/social/coves/aggregator \ + internal/atproto/lexicon/social/coves/community +# community/rules.json and community/moderator.json share the community DNS +# authority, so exclude them by unpublishing if swept in: +goat lex unpublish social.coves.community.rules social.coves.community.moderator + +# 3. Verify resolution end-to-end +goat lex check-dns internal/atproto/lexicon/social/coves +goat lex resolve social.coves.community.post +goat lex status internal/atproto/lexicon/social/coves +``` + +Record updates use the same `goat lex publish --update` — records are keyed by +NSID and overwritten in place. Run `goat lex breaking` + `goat lex diff` first, +always. diff --git a/internal/atproto/lexicon/social/coves/actor/getComments.json b/internal/atproto/lexicon/social/coves/actor/getComments.json index 4be1bfe..14cbde1 100644 --- a/internal/atproto/lexicon/social/coves/actor/getComments.json +++ b/internal/atproto/lexicon/social/coves/actor/getComments.json @@ -7,7 +7,9 @@ "description": "Get a user's comments for their profile page.", "parameters": { "type": "params", - "required": ["actor"], + "required": [ + "actor" + ], "properties": { "actor": { "type": "string", @@ -26,7 +28,8 @@ "default": 50 }, "cursor": { - "type": "string" + "type": "string", + "maxLength": 500 } } }, @@ -34,7 +37,9 @@ "encoding": "application/json", "schema": { "type": "object", - "required": ["comments"], + "required": [ + "comments" + ], "properties": { "comments": { "type": "array", @@ -44,7 +49,8 @@ } }, "cursor": { - "type": "string" + "type": "string", + "maxLength": 500 } } } diff --git a/internal/atproto/lexicon/social/coves/actor/getPosts.json b/internal/atproto/lexicon/social/coves/actor/getPosts.json index c000587..69a253b 100644 --- a/internal/atproto/lexicon/social/coves/actor/getPosts.json +++ b/internal/atproto/lexicon/social/coves/actor/getPosts.json @@ -7,7 +7,9 @@ "description": "Get a user's posts for their profile page.", "parameters": { "type": "params", - "required": ["actor"], + "required": [ + "actor" + ], "properties": { "actor": { "type": "string", @@ -16,9 +18,14 @@ }, "filter": { "type": "string", - "knownValues": ["posts-with-replies", "posts-no-replies", "posts-with-media"], + "knownValues": [ + "posts-with-replies", + "posts-no-replies", + "posts-with-media" + ], "default": "posts-with-replies", - "description": "Filter for post types" + "description": "Filter for post types", + "maxLength": 64 }, "community": { "type": "string", @@ -32,7 +39,8 @@ "default": 50 }, "cursor": { - "type": "string" + "type": "string", + "maxLength": 500 } } }, @@ -40,7 +48,9 @@ "encoding": "application/json", "schema": { "type": "object", - "required": ["feed"], + "required": [ + "feed" + ], "properties": { "feed": { "type": "array", @@ -50,7 +60,8 @@ } }, "cursor": { - "type": "string" + "type": "string", + "maxLength": 500 } } } diff --git a/internal/atproto/lexicon/social/coves/actor/signup.json b/internal/atproto/lexicon/social/coves/actor/signup.json index 733c673..c3f7982 100644 --- a/internal/atproto/lexicon/social/coves/actor/signup.json +++ b/internal/atproto/lexicon/social/coves/actor/signup.json @@ -9,7 +9,11 @@ "encoding": "application/json", "schema": { "type": "object", - "required": ["handle", "email", "password"], + "required": [ + "handle", + "email", + "password" + ], "properties": { "handle": { "type": "string", @@ -18,15 +22,18 @@ }, "email": { "type": "string", - "description": "Email address for account recovery and notifications" + "description": "Email address for account recovery and notifications", + "maxLength": 320 }, "password": { "type": "string", - "description": "Account password (must meet strength requirements)" + "description": "Account password (must meet strength requirements)", + "maxLength": 512 }, "inviteCode": { "type": "string", - "description": "Invite code (required if instance has invite-only registration)" + "description": "Invite code (required if instance has invite-only registration)", + "maxLength": 128 } } } @@ -35,7 +42,12 @@ "encoding": "application/json", "schema": { "type": "object", - "required": ["did", "handle", "accessJwt", "refreshJwt"], + "required": [ + "did", + "handle", + "accessJwt", + "refreshJwt" + ], "properties": { "did": { "type": "string", @@ -49,11 +61,13 @@ }, "accessJwt": { "type": "string", - "description": "Access token for authenticated requests" + "description": "Access token for authenticated requests", + "maxLength": 8192 }, "refreshJwt": { "type": "string", - "description": "Refresh token for obtaining new access tokens" + "description": "Refresh token for obtaining new access tokens", + "maxLength": 8192 } } } diff --git a/internal/atproto/lexicon/social/coves/aggregator/createApiKey.json b/internal/atproto/lexicon/social/coves/aggregator/createApiKey.json index 3255286..e58296e 100644 --- a/internal/atproto/lexicon/social/coves/aggregator/createApiKey.json +++ b/internal/atproto/lexicon/social/coves/aggregator/createApiKey.json @@ -17,15 +17,22 @@ "encoding": "application/json", "schema": { "type": "object", - "required": ["key", "keyPrefix", "did", "createdAt"], + "required": [ + "key", + "keyPrefix", + "did", + "createdAt" + ], "properties": { "key": { "type": "string", - "description": "The plain-text API key. This is shown ONCE and cannot be retrieved again. Format: ckapi_<64-hex-chars> (32 bytes hex-encoded)" + "description": "The plain-text API key. This is shown ONCE and cannot be retrieved again. Format: ckapi_<64-hex-chars> (32 bytes hex-encoded)", + "maxLength": 256 }, "keyPrefix": { "type": "string", - "description": "First 12 characters of the key (e.g., 'ckapi_ab12cd') for identification in logs and UI" + "description": "First 12 characters of the key (e.g., 'ckapi_ab12cd') for identification in logs and UI", + "maxLength": 64 }, "did": { "type": "string", diff --git a/internal/atproto/lexicon/social/coves/aggregator/defs.json b/internal/atproto/lexicon/social/coves/aggregator/defs.json index 0911fbc..5ea7484 100644 --- a/internal/atproto/lexicon/social/coves/aggregator/defs.json +++ b/internal/atproto/lexicon/social/coves/aggregator/defs.json @@ -5,7 +5,11 @@ "aggregatorView": { "type": "object", "description": "Detailed view of an aggregator service", - "required": ["did", "displayName", "createdAt"], + "required": [ + "did", + "displayName", + "createdAt" + ], "properties": { "did": { "type": "string", @@ -57,7 +61,12 @@ "aggregatorViewDetailed": { "type": "object", "description": "Detailed view of an aggregator with stats", - "required": ["did", "displayName", "createdAt", "stats"], + "required": [ + "did", + "displayName", + "createdAt", + "stats" + ], "properties": { "did": { "type": "string", @@ -105,7 +114,10 @@ "aggregatorStats": { "type": "object", "description": "Statistics about an aggregator's usage", - "required": ["communitiesUsing", "postsCreated"], + "required": [ + "communitiesUsing", + "postsCreated" + ], "properties": { "communitiesUsing": { "type": "integer", @@ -122,7 +134,12 @@ "authorizationView": { "type": "object", "description": "View of an aggregator authorization for a community", - "required": ["aggregatorDid", "communityDid", "enabled", "createdAt"], + "required": [ + "aggregatorDid", + "communityDid", + "enabled", + "createdAt" + ], "properties": { "aggregatorDid": { "type": "string", @@ -141,7 +158,8 @@ }, "communityName": { "type": "string", - "description": "Display name of the community" + "description": "Display name of the community", + "maxLength": 1280 }, "enabled": { "type": "boolean", @@ -180,7 +198,11 @@ "communityAuthView": { "type": "object", "description": "Aggregator's view of authorization for a community (used by aggregators querying their authorizations)", - "required": ["aggregator", "enabled", "createdAt"], + "required": [ + "aggregator", + "enabled", + "createdAt" + ], "properties": { "aggregator": { "type": "ref", @@ -208,11 +230,16 @@ "apiKeyView": { "type": "object", "description": "View of an API key's metadata. The actual key value is never returned after initial creation.", - "required": ["prefix", "createdAt", "isRevoked"], + "required": [ + "prefix", + "createdAt", + "isRevoked" + ], "properties": { "prefix": { "type": "string", - "description": "First 12 characters of the key (e.g., 'ckapi_ab12cd') for identification in logs and UI" + "description": "First 12 characters of the key (e.g., 'ckapi_ab12cd') for identification in logs and UI", + "maxLength": 64 }, "createdAt": { "type": "string", diff --git a/internal/atproto/lexicon/social/coves/aggregator/getAuthorizations.json b/internal/atproto/lexicon/social/coves/aggregator/getAuthorizations.json index ee6d494..4d0d7b4 100644 --- a/internal/atproto/lexicon/social/coves/aggregator/getAuthorizations.json +++ b/internal/atproto/lexicon/social/coves/aggregator/getAuthorizations.json @@ -7,7 +7,9 @@ "description": "Get list of communities that have authorized a specific aggregator. Used by aggregators to query which communities they can post to. Authentication optional.", "parameters": { "type": "params", - "required": ["aggregatorDid"], + "required": [ + "aggregatorDid" + ], "properties": { "aggregatorDid": { "type": "string", @@ -28,7 +30,8 @@ }, "cursor": { "type": "string", - "description": "Pagination cursor" + "description": "Pagination cursor", + "maxLength": 500 } } }, @@ -36,7 +39,9 @@ "encoding": "application/json", "schema": { "type": "object", - "required": ["authorizations"], + "required": [ + "authorizations" + ], "properties": { "authorizations": { "type": "array", @@ -48,7 +53,8 @@ }, "cursor": { "type": "string", - "description": "Pagination cursor for next page" + "description": "Pagination cursor for next page", + "maxLength": 500 } } } diff --git a/internal/atproto/lexicon/social/coves/aggregator/listForCommunity.json b/internal/atproto/lexicon/social/coves/aggregator/listForCommunity.json index c434b32..897b812 100644 --- a/internal/atproto/lexicon/social/coves/aggregator/listForCommunity.json +++ b/internal/atproto/lexicon/social/coves/aggregator/listForCommunity.json @@ -7,7 +7,9 @@ "description": "List all aggregators authorized for a specific community. Used by community settings UI to show enabled/disabled aggregators. Authentication optional.", "parameters": { "type": "params", - "required": ["community"], + "required": [ + "community" + ], "properties": { "community": { "type": "string", @@ -28,7 +30,8 @@ }, "cursor": { "type": "string", - "description": "Pagination cursor" + "description": "Pagination cursor", + "maxLength": 500 } } }, @@ -36,7 +39,9 @@ "encoding": "application/json", "schema": { "type": "object", - "required": ["aggregators"], + "required": [ + "aggregators" + ], "properties": { "aggregators": { "type": "array", @@ -48,7 +53,8 @@ }, "cursor": { "type": "string", - "description": "Pagination cursor for next page" + "description": "Pagination cursor for next page", + "maxLength": 500 } } } diff --git a/internal/atproto/lexicon/social/coves/aggregator/register.json b/internal/atproto/lexicon/social/coves/aggregator/register.json index e40aa7e..7aa4a37 100644 --- a/internal/atproto/lexicon/social/coves/aggregator/register.json +++ b/internal/atproto/lexicon/social/coves/aggregator/register.json @@ -9,7 +9,10 @@ "encoding": "application/json", "schema": { "type": "object", - "required": ["did", "domain"], + "required": [ + "did", + "domain" + ], "properties": { "did": { "type": "string", @@ -28,7 +31,10 @@ "encoding": "application/json", "schema": { "type": "object", - "required": ["did", "handle"], + "required": [ + "did", + "handle" + ], "properties": { "did": { "type": "string", @@ -37,11 +43,13 @@ }, "handle": { "type": "string", - "description": "Handle extracted from DID document" + "description": "Handle extracted from DID document", + "maxLength": 512 }, "message": { "type": "string", - "description": "Success message with next steps" + "description": "Success message with next steps", + "maxLength": 1000 } } } diff --git a/internal/atproto/lexicon/social/coves/community/comment/defs.json b/internal/atproto/lexicon/social/coves/community/comment/defs.json index b8cccc5..b4c3f49 100644 --- a/internal/atproto/lexicon/social/coves/community/comment/defs.json +++ b/internal/atproto/lexicon/social/coves/community/comment/defs.json @@ -5,7 +5,16 @@ "commentView": { "type": "object", "description": "Base view for a single comment with voting, stats, and viewer state", - "required": ["uri", "cid", "author", "record", "post", "createdAt", "indexedAt", "stats"], + "required": [ + "uri", + "cid", + "author", + "record", + "post", + "createdAt", + "indexedAt", + "stats" + ], "properties": { "uri": { "type": "string", @@ -69,7 +78,9 @@ "threadViewComment": { "type": "object", "description": "Wrapper for threaded comment structure, similar to Bluesky's threadViewPost pattern", - "required": ["comment"], + "required": [ + "comment" + ], "properties": { "comment": { "type": "ref", @@ -81,7 +92,11 @@ "description": "Nested replies to this comment", "items": { "type": "union", - "refs": ["#threadViewComment", "#notFoundComment", "#blockedComment"] + "refs": [ + "#threadViewComment", + "#notFoundComment", + "#blockedComment" + ] } }, "hasMore": { @@ -93,7 +108,10 @@ "commentRef": { "type": "object", "description": "Reference to a comment record", - "required": ["uri", "cid"], + "required": [ + "uri", + "cid" + ], "properties": { "uri": { "type": "string", @@ -110,7 +128,10 @@ "postRef": { "type": "object", "description": "Reference to a post record", - "required": ["uri", "cid"], + "required": [ + "uri", + "cid" + ], "properties": { "uri": { "type": "string", @@ -127,7 +148,10 @@ "notFoundComment": { "type": "object", "description": "Comment was not found (deleted, never indexed, or invalid URI)", - "required": ["uri", "notFound"], + "required": [ + "uri", + "notFound" + ], "properties": { "uri": { "type": "string", @@ -144,7 +168,10 @@ "blockedComment": { "type": "object", "description": "Comment is blocked due to viewer blocking author or moderation action", - "required": ["uri", "blocked"], + "required": [ + "uri", + "blocked" + ], "properties": { "uri": { "type": "string", @@ -158,15 +185,24 @@ }, "blockedBy": { "type": "string", - "knownValues": ["author", "moderator"], - "description": "What caused the block: viewer blocked author, or comment was removed by moderators" + "knownValues": [ + "author", + "moderator" + ], + "description": "What caused the block: viewer blocked author, or comment was removed by moderators", + "maxLength": 64 } } }, "commentStats": { "type": "object", "description": "Statistics for a comment", - "required": ["upvotes", "downvotes", "score", "replyCount"], + "required": [ + "upvotes", + "downvotes", + "score", + "replyCount" + ], "properties": { "upvotes": { "type": "integer", @@ -195,8 +231,12 @@ "properties": { "vote": { "type": "string", - "knownValues": ["up", "down"], - "description": "Viewer's vote on this comment" + "knownValues": [ + "up", + "down" + ], + "description": "Viewer's vote on this comment", + "maxLength": 64 }, "voteUri": { "type": "string", diff --git a/internal/atproto/lexicon/social/coves/community/comment/getComments.json b/internal/atproto/lexicon/social/coves/community/comment/getComments.json index bbbe499..9ff863f 100644 --- a/internal/atproto/lexicon/social/coves/community/comment/getComments.json +++ b/internal/atproto/lexicon/social/coves/community/comment/getComments.json @@ -7,7 +7,9 @@ "description": "Get comments for a post with threading and sorting support. Supports hot/top/new sorting, configurable nesting depth, and pagination.", "parameters": { "type": "params", - "required": ["post"], + "required": [ + "post" + ], "properties": { "post": { "type": "string", @@ -22,13 +24,26 @@ "sort": { "type": "string", "default": "hot", - "knownValues": ["hot", "top", "new"], - "description": "Sort order: hot (trending), top (highest score), new (most recent)" + "knownValues": [ + "hot", + "top", + "new" + ], + "description": "Sort order: hot (trending), top (highest score), new (most recent)", + "maxLength": 64 }, "timeframe": { "type": "string", - "knownValues": ["hour", "day", "week", "month", "year", "all"], - "description": "Timeframe for 'top' sort. Ignored for other sort types." + "knownValues": [ + "hour", + "day", + "week", + "month", + "year", + "all" + ], + "description": "Timeframe for 'top' sort. Ignored for other sort types.", + "maxLength": 64 }, "depth": { "type": "integer", @@ -46,7 +61,8 @@ }, "cursor": { "type": "string", - "description": "Pagination cursor from previous response" + "description": "Pagination cursor from previous response", + "maxLength": 500 } } }, @@ -54,7 +70,10 @@ "encoding": "application/json", "schema": { "type": "object", - "required": ["comments", "post"], + "required": [ + "comments", + "post" + ], "properties": { "comments": { "type": "array", @@ -71,7 +90,8 @@ }, "cursor": { "type": "string", - "description": "Pagination cursor for fetching next page of top-level comments" + "description": "Pagination cursor for fetching next page of top-level comments", + "maxLength": 500 } } } diff --git a/internal/atproto/lexicon/social/coves/community/defs.json b/internal/atproto/lexicon/social/coves/community/defs.json index 92a3de7..feb804f 100644 --- a/internal/atproto/lexicon/social/coves/community/defs.json +++ b/internal/atproto/lexicon/social/coves/community/defs.json @@ -43,7 +43,8 @@ "unlisted", "private" ], - "description": "Community visibility level" + "description": "Community visibility level", + "maxLength": 64 }, "subscriberCount": { "type": "integer", @@ -151,7 +152,8 @@ "unlisted", "private" ], - "description": "Community visibility level" + "description": "Community visibility level", + "maxLength": 64 }, "moderationType": { "type": "string", @@ -159,7 +161,8 @@ "moderator", "sortition" ], - "description": "Type of moderation system" + "description": "Type of moderation system", + "maxLength": 64 }, "contentWarnings": { "type": "array", diff --git a/internal/atproto/lexicon/social/coves/community/getMembers.json b/internal/atproto/lexicon/social/coves/community/getMembers.json index 2eed433..21cd87a 100644 --- a/internal/atproto/lexicon/social/coves/community/getMembers.json +++ b/internal/atproto/lexicon/social/coves/community/getMembers.json @@ -23,7 +23,8 @@ "default": 50 }, "cursor": { - "type": "string" + "type": "string", + "maxLength": 500 }, "sort": { "type": "string", @@ -53,7 +54,8 @@ } }, "cursor": { - "type": "string" + "type": "string", + "maxLength": 500 } } } diff --git a/internal/atproto/lexicon/social/coves/community/getSubscribers.json b/internal/atproto/lexicon/social/coves/community/getSubscribers.json index 0123b24..bbc9b91 100644 --- a/internal/atproto/lexicon/social/coves/community/getSubscribers.json +++ b/internal/atproto/lexicon/social/coves/community/getSubscribers.json @@ -23,7 +23,8 @@ "default": 50 }, "cursor": { - "type": "string" + "type": "string", + "maxLength": 500 } } }, @@ -43,7 +44,8 @@ } }, "cursor": { - "type": "string" + "type": "string", + "maxLength": 500 } } } diff --git a/internal/atproto/lexicon/social/coves/community/list.json b/internal/atproto/lexicon/social/coves/community/list.json index 33463e4..056426b 100644 --- a/internal/atproto/lexicon/social/coves/community/list.json +++ b/internal/atproto/lexicon/social/coves/community/list.json @@ -16,23 +16,35 @@ }, "cursor": { "type": "string", - "description": "Pagination cursor" + "description": "Pagination cursor", + "maxLength": 500 }, "visibility": { "type": "string", - "knownValues": ["public", "unlisted", "private"], - "description": "Filter communities by visibility level" + "knownValues": [ + "public", + "unlisted", + "private" + ], + "description": "Filter communities by visibility level", + "maxLength": 64 }, "sort": { "type": "string", - "knownValues": ["popular", "active", "new", "alphabetical"], + "knownValues": [ + "popular", + "active", + "new", + "alphabetical" + ], "default": "popular", "maxLength": 64, "description": "Sorting method" }, "category": { "type": "string", - "description": "Filter by category" + "description": "Filter by category", + "maxLength": 50 }, "language": { "type": "string", @@ -49,7 +61,9 @@ "encoding": "application/json", "schema": { "type": "object", - "required": ["communities"], + "required": [ + "communities" + ], "properties": { "communities": { "type": "array", @@ -59,11 +73,12 @@ } }, "cursor": { - "type": "string" + "type": "string", + "maxLength": 500 } } } } } } -} \ No newline at end of file +} diff --git a/internal/atproto/lexicon/social/coves/community/post/defs.json b/internal/atproto/lexicon/social/coves/community/post/defs.json index 2083af6..e81e069 100644 --- a/internal/atproto/lexicon/social/coves/community/post/defs.json +++ b/internal/atproto/lexicon/social/coves/community/post/defs.json @@ -170,7 +170,8 @@ "community", "moderator" ], - "description": "What caused the block: viewer blocked author, viewer blocked community, or post was removed by moderators" + "description": "What caused the block: viewer blocked author, viewer blocked community, or post was removed by moderators", + "maxLength": 64 }, "author": { "type": "ref", @@ -256,7 +257,8 @@ "up", "down" ], - "description": "Viewer's vote on this post" + "description": "Viewer's vote on this post", + "maxLength": 64 }, "voteUri": { "type": "string", diff --git a/internal/atproto/lexicon/social/coves/community/post/search.json b/internal/atproto/lexicon/social/coves/community/post/search.json index 2b45cec..e85563e 100644 --- a/internal/atproto/lexicon/social/coves/community/post/search.json +++ b/internal/atproto/lexicon/social/coves/community/post/search.json @@ -7,11 +7,14 @@ "description": "Search for posts", "parameters": { "type": "params", - "required": ["q"], + "required": [ + "q" + ], "properties": { "q": { "type": "string", - "description": "Search query" + "description": "Search query", + "maxLength": 500 }, "community": { "type": "string", @@ -25,25 +28,47 @@ }, "type": { "type": "string", - "knownValues": ["text", "image", "video", "article", "microblog"], - "description": "Filter by post type" + "knownValues": [ + "text", + "image", + "video", + "article", + "microblog" + ], + "description": "Filter by post type", + "maxLength": 64 }, "tags": { "type": "array", "items": { - "type": "string" + "type": "string", + "maxGraphemes": 64, + "maxLength": 640 }, "description": "Filter by tags" }, "sort": { "type": "string", - "knownValues": ["relevance", "new", "top"], - "default": "relevance" + "knownValues": [ + "relevance", + "new", + "top" + ], + "default": "relevance", + "maxLength": 64 }, "timeframe": { "type": "string", - "knownValues": ["hour", "day", "week", "month", "year", "all"], - "default": "all" + "knownValues": [ + "hour", + "day", + "week", + "month", + "year", + "all" + ], + "default": "all", + "maxLength": 64 }, "limit": { "type": "integer", @@ -52,7 +77,8 @@ "default": 50 }, "cursor": { - "type": "string" + "type": "string", + "maxLength": 500 } } }, @@ -60,7 +86,9 @@ "encoding": "application/json", "schema": { "type": "object", - "required": ["posts"], + "required": [ + "posts" + ], "properties": { "posts": { "type": "array", @@ -70,11 +98,12 @@ } }, "cursor": { - "type": "string" + "type": "string", + "maxLength": 500 } } } } } } -} \ No newline at end of file +} diff --git a/internal/atproto/lexicon/social/coves/community/search.json b/internal/atproto/lexicon/social/coves/community/search.json index 7b9862e..75e6c47 100644 --- a/internal/atproto/lexicon/social/coves/community/search.json +++ b/internal/atproto/lexicon/social/coves/community/search.json @@ -7,11 +7,14 @@ "description": "Search for communities by name or description", "parameters": { "type": "params", - "required": ["q"], + "required": [ + "q" + ], "properties": { "q": { "type": "string", - "description": "Search query" + "description": "Search query", + "maxLength": 500 }, "limit": { "type": "integer", @@ -20,11 +23,13 @@ "default": 50 }, "cursor": { - "type": "string" + "type": "string", + "maxLength": 500 }, "category": { "type": "string", - "description": "Filter by category" + "description": "Filter by category", + "maxLength": 50 }, "language": { "type": "string", @@ -37,7 +42,9 @@ "encoding": "application/json", "schema": { "type": "object", - "required": ["communities"], + "required": [ + "communities" + ], "properties": { "communities": { "type": "array", @@ -47,11 +54,12 @@ } }, "cursor": { - "type": "string" + "type": "string", + "maxLength": 500 } } } } } } -} \ No newline at end of file +} diff --git a/internal/atproto/lexicon/social/coves/embed/external.json b/internal/atproto/lexicon/social/coves/embed/external.json index 0eab0aa..0019c0e 100644 --- a/internal/atproto/lexicon/social/coves/embed/external.json +++ b/internal/atproto/lexicon/social/coves/embed/external.json @@ -62,7 +62,8 @@ "video", "website" ], - "description": "Type hint for content rendering and filtering" + "description": "Type hint for content rendering and filtering", + "maxLength": 64 }, "provider": { "type": "string", @@ -179,7 +180,8 @@ "video", "website" ], - "description": "Type hint for content rendering and filtering" + "description": "Type hint for content rendering and filtering", + "maxLength": 64 }, "provider": { "type": "string", diff --git a/internal/atproto/lexicon/social/coves/feed/getAll.json b/internal/atproto/lexicon/social/coves/feed/getAll.json index 32dc6bf..4b72857 100644 --- a/internal/atproto/lexicon/social/coves/feed/getAll.json +++ b/internal/atproto/lexicon/social/coves/feed/getAll.json @@ -10,28 +10,55 @@ "properties": { "sort": { "type": "string", - "knownValues": ["hot", "top", "new"], + "knownValues": [ + "hot", + "top", + "new" + ], "default": "hot", - "description": "Sort order for global feed" + "description": "Sort order for global feed", + "maxLength": 64 }, "postType": { "type": "string", - "knownValues": ["text", "article", "image", "video", "microblog"], - "description": "Filter by a single post type (computed from embed structure)" + "knownValues": [ + "text", + "article", + "image", + "video", + "microblog" + ], + "description": "Filter by a single post type (computed from embed structure)", + "maxLength": 64 }, "postTypes": { "type": "array", "items": { "type": "string", - "knownValues": ["text", "article", "image", "video", "microblog"] + "knownValues": [ + "text", + "article", + "image", + "video", + "microblog" + ], + "maxLength": 64 }, "description": "Filter by multiple post types (computed from embed structure)" }, "timeframe": { "type": "string", - "knownValues": ["hour", "day", "week", "month", "year", "all"], + "knownValues": [ + "hour", + "day", + "week", + "month", + "year", + "all" + ], "default": "day", - "description": "Timeframe for top sorting (only applies when sort=top)" + "description": "Timeframe for top sorting (only applies when sort=top)", + "maxLength": 64 }, "limit": { "type": "integer", @@ -40,7 +67,8 @@ "default": 15 }, "cursor": { - "type": "string" + "type": "string", + "maxLength": 500 } } }, @@ -48,7 +76,9 @@ "encoding": "application/json", "schema": { "type": "object", - "required": ["feed"], + "required": [ + "feed" + ], "properties": { "feed": { "type": "array", @@ -58,11 +88,12 @@ } }, "cursor": { - "type": "string" + "type": "string", + "maxLength": 500 } } } } } } -} \ No newline at end of file +} diff --git a/internal/atproto/lexicon/social/coves/feed/getCommunity.json b/internal/atproto/lexicon/social/coves/feed/getCommunity.json index 6620403..989da10 100644 --- a/internal/atproto/lexicon/social/coves/feed/getCommunity.json +++ b/internal/atproto/lexicon/social/coves/feed/getCommunity.json @@ -7,7 +7,9 @@ "description": "Get a feed of posts from a specific community", "parameters": { "type": "params", - "required": ["community"], + "required": [ + "community" + ], "properties": { "community": { "type": "string", @@ -16,15 +18,28 @@ }, "sort": { "type": "string", - "knownValues": ["hot", "top", "new"], + "knownValues": [ + "hot", + "top", + "new" + ], "default": "hot", - "description": "Sort order for community feed" + "description": "Sort order for community feed", + "maxLength": 64 }, "timeframe": { "type": "string", - "knownValues": ["hour", "day", "week", "month", "year", "all"], + "knownValues": [ + "hour", + "day", + "week", + "month", + "year", + "all" + ], "default": "day", - "description": "Timeframe for top sorting (only applies when sort=top)" + "description": "Timeframe for top sorting (only applies when sort=top)", + "maxLength": 64 }, "limit": { "type": "integer", @@ -33,7 +48,8 @@ "default": 15 }, "cursor": { - "type": "string" + "type": "string", + "maxLength": 500 } } }, @@ -41,7 +57,9 @@ "encoding": "application/json", "schema": { "type": "object", - "required": ["feed"], + "required": [ + "feed" + ], "properties": { "feed": { "type": "array", @@ -51,11 +69,12 @@ } }, "cursor": { - "type": "string" + "type": "string", + "maxLength": 500 } } } } } } -} \ No newline at end of file +} diff --git a/internal/atproto/lexicon/social/coves/feed/getDiscover.json b/internal/atproto/lexicon/social/coves/feed/getDiscover.json index 898cc6d..4d2f764 100644 --- a/internal/atproto/lexicon/social/coves/feed/getDiscover.json +++ b/internal/atproto/lexicon/social/coves/feed/getDiscover.json @@ -10,15 +10,28 @@ "properties": { "sort": { "type": "string", - "knownValues": ["hot", "top", "new"], + "knownValues": [ + "hot", + "top", + "new" + ], "default": "hot", - "description": "Sort order for discover feed" + "description": "Sort order for discover feed", + "maxLength": 64 }, "timeframe": { "type": "string", - "knownValues": ["hour", "day", "week", "month", "year", "all"], + "knownValues": [ + "hour", + "day", + "week", + "month", + "year", + "all" + ], "default": "day", - "description": "Timeframe for top sorting (only applies when sort=top)" + "description": "Timeframe for top sorting (only applies when sort=top)", + "maxLength": 64 }, "limit": { "type": "integer", @@ -27,7 +40,8 @@ "default": 15 }, "cursor": { - "type": "string" + "type": "string", + "maxLength": 500 } } }, @@ -35,7 +49,9 @@ "encoding": "application/json", "schema": { "type": "object", - "required": ["feed"], + "required": [ + "feed" + ], "properties": { "feed": { "type": "array", @@ -45,7 +61,8 @@ } }, "cursor": { - "type": "string" + "type": "string", + "maxLength": 500 } } } diff --git a/internal/atproto/lexicon/social/coves/feed/getTimeline.json b/internal/atproto/lexicon/social/coves/feed/getTimeline.json index 74b77f2..10746a5 100644 --- a/internal/atproto/lexicon/social/coves/feed/getTimeline.json +++ b/internal/atproto/lexicon/social/coves/feed/getTimeline.json @@ -10,15 +10,28 @@ "properties": { "sort": { "type": "string", - "knownValues": ["hot", "top", "new"], + "knownValues": [ + "hot", + "top", + "new" + ], "default": "hot", - "description": "Sort order for timeline feed" + "description": "Sort order for timeline feed", + "maxLength": 64 }, "timeframe": { "type": "string", - "knownValues": ["hour", "day", "week", "month", "year", "all"], + "knownValues": [ + "hour", + "day", + "week", + "month", + "year", + "all" + ], "default": "day", - "description": "Timeframe for top sorting (only applies when sort=top)" + "description": "Timeframe for top sorting (only applies when sort=top)", + "maxLength": 64 }, "limit": { "type": "integer", @@ -27,7 +40,8 @@ "default": 15 }, "cursor": { - "type": "string" + "type": "string", + "maxLength": 500 } } }, @@ -35,7 +49,9 @@ "encoding": "application/json", "schema": { "type": "object", - "required": ["feed"], + "required": [ + "feed" + ], "properties": { "feed": { "type": "array", @@ -45,11 +61,12 @@ } }, "cursor": { - "type": "string" + "type": "string", + "maxLength": 500 } } } } } } -} \ No newline at end of file +} diff --git a/internal/atproto/lexicon/social/coves/feed/vote.json b/internal/atproto/lexicon/social/coves/feed/vote.json index d683942..31e4872 100644 --- a/internal/atproto/lexicon/social/coves/feed/vote.json +++ b/internal/atproto/lexicon/social/coves/feed/vote.json @@ -8,7 +8,11 @@ "key": "tid", "record": { "type": "object", - "required": ["subject", "direction", "createdAt"], + "required": [ + "subject", + "direction", + "createdAt" + ], "properties": { "subject": { "type": "ref", @@ -17,8 +21,12 @@ }, "direction": { "type": "string", - "knownValues": ["up", "down"], - "description": "Vote direction: up for upvote, down for downvote" + "knownValues": [ + "up", + "down" + ], + "description": "Vote direction: up for upvote, down for downvote", + "maxLength": 64 }, "createdAt": { "type": "string", @@ -29,4 +37,4 @@ } } } -} \ No newline at end of file +} diff --git a/internal/atproto/lexicon/social/coves/feed/vote/create.json b/internal/atproto/lexicon/social/coves/feed/vote/create.json index 42cfa05..6f39d86 100644 --- a/internal/atproto/lexicon/social/coves/feed/vote/create.json +++ b/internal/atproto/lexicon/social/coves/feed/vote/create.json @@ -9,7 +9,10 @@ "encoding": "application/json", "schema": { "type": "object", - "required": ["subject", "direction"], + "required": [ + "subject", + "direction" + ], "properties": { "subject": { "type": "ref", @@ -18,8 +21,12 @@ }, "direction": { "type": "string", - "knownValues": ["up", "down"], - "description": "Vote direction: up for upvote, down for downvote" + "knownValues": [ + "up", + "down" + ], + "description": "Vote direction: up for upvote, down for downvote", + "maxLength": 64 } } } diff --git a/internal/atproto/lexicon/social/coves/moderation/ban.json b/internal/atproto/lexicon/social/coves/moderation/ban.json index dcda5cd..07b5c69 100644 --- a/internal/atproto/lexicon/social/coves/moderation/ban.json +++ b/internal/atproto/lexicon/social/coves/moderation/ban.json @@ -8,7 +8,12 @@ "key": "tid", "record": { "type": "object", - "required": ["community", "subject", "banType", "createdAt"], + "required": [ + "community", + "subject", + "banType", + "createdAt" + ], "properties": { "community": { "type": "string", @@ -22,8 +27,12 @@ }, "banType": { "type": "string", - "knownValues": ["moderator", "tribunal"], - "description": "How the ban was imposed" + "knownValues": [ + "moderator", + "tribunal" + ], + "description": "How the ban was imposed", + "maxLength": 64 }, "reason": { "type": "string", @@ -48,7 +57,12 @@ }, "status": { "type": "string", - "knownValues": ["active", "expired", "revoked"] + "knownValues": [ + "active", + "expired", + "revoked" + ], + "maxLength": 64 }, "expiresAt": { "type": "string", @@ -73,4 +87,4 @@ } } } -} \ No newline at end of file +} diff --git a/internal/atproto/lexicon/social/coves/moderation/listBans.json b/internal/atproto/lexicon/social/coves/moderation/listBans.json index a245dbb..186e3e1 100644 --- a/internal/atproto/lexicon/social/coves/moderation/listBans.json +++ b/internal/atproto/lexicon/social/coves/moderation/listBans.json @@ -7,7 +7,9 @@ "description": "List bans for a community (moderator only)", "parameters": { "type": "params", - "required": ["community"], + "required": [ + "community" + ], "properties": { "community": { "type": "string", @@ -16,9 +18,15 @@ }, "status": { "type": "string", - "knownValues": ["active", "expired", "revoked", "all"], + "knownValues": [ + "active", + "expired", + "revoked", + "all" + ], "default": "active", - "description": "Filter by ban status" + "description": "Filter by ban status", + "maxLength": 64 }, "limit": { "type": "integer", @@ -29,7 +37,8 @@ }, "cursor": { "type": "string", - "description": "Pagination cursor" + "description": "Pagination cursor", + "maxLength": 500 } } }, @@ -37,7 +46,9 @@ "encoding": "application/json", "schema": { "type": "object", - "required": ["bans"], + "required": [ + "bans" + ], "properties": { "bans": { "type": "array", @@ -48,7 +59,8 @@ }, "cursor": { "type": "string", - "description": "Pagination cursor for next page" + "description": "Pagination cursor for next page", + "maxLength": 500 } } } @@ -65,4 +77,4 @@ ] } } -} \ No newline at end of file +} diff --git a/scripts/publish-lexicon-dns.sh b/scripts/publish-lexicon-dns.sh new file mode 100755 index 0000000..b3fafd4 --- /dev/null +++ b/scripts/publish-lexicon-dns.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env bash +# Creates/updates the _lexicon TXT records on Cloudflare that delegate the +# social.coves.* NSID namespaces to the lexicon publishing account's DID. +# +# Usage: +# CF_API_TOKEN= \ +# LEXICON_DID=did:plc:xxxxxxxxxxxx \ +# ./scripts/publish-lexicon-dns.sh [--include-moderation] +# +# Idempotent: existing records are updated in place, missing ones created. +set -euo pipefail + +ZONE_NAME="coves.social" + +if [[ -z "${CF_API_TOKEN:-}" || -z "${LEXICON_DID:-}" ]]; then + echo "error: CF_API_TOKEN and LEXICON_DID must be set" >&2 + exit 1 +fi + +# One record per unique NSID authority (NSID minus its last segment, reversed). +# social.coves.community.post.get -> authority social.coves.community.post +# -> DNS name _lexicon.post.community.coves.social +AUTHORITIES=( + "_lexicon.actor.${ZONE_NAME}" + "_lexicon.aggregator.${ZONE_NAME}" + "_lexicon.community.${ZONE_NAME}" + "_lexicon.comment.community.${ZONE_NAME}" + "_lexicon.post.community.${ZONE_NAME}" + "_lexicon.embed.${ZONE_NAME}" + "_lexicon.feed.${ZONE_NAME}" + "_lexicon.vote.feed.${ZONE_NAME}" + "_lexicon.richtext.${ZONE_NAME}" +) +if [[ "${1:-}" == "--include-moderation" ]]; then + AUTHORITIES+=("_lexicon.moderation.${ZONE_NAME}") +fi + +API="https://api.cloudflare.com/client/v4" +AUTH=(-H "Authorization: Bearer ${CF_API_TOKEN}" -H "Content-Type: application/json") + +ZONE_ID=$(curl -sf "${AUTH[@]}" "${API}/zones?name=${ZONE_NAME}" | python3 -c \ + "import json,sys; r=json.load(sys.stdin)['result']; print(r[0]['id'] if r else '')") +if [[ -z "$ZONE_ID" ]]; then + echo "error: zone ${ZONE_NAME} not found (token missing zone read access?)" >&2 + exit 1 +fi +echo "zone ${ZONE_NAME}: ${ZONE_ID}" + +CONTENT="did=${LEXICON_DID}" +for NAME in "${AUTHORITIES[@]}"; do + EXISTING=$(curl -sf "${AUTH[@]}" \ + "${API}/zones/${ZONE_ID}/dns_records?type=TXT&name=${NAME}" | python3 -c \ + "import json,sys; r=json.load(sys.stdin)['result']; print(r[0]['id'] if r else '')") + BODY=$(python3 -c "import json; print(json.dumps({ + 'type': 'TXT', 'name': '${NAME}', 'content': '\"${CONTENT}\"', 'ttl': 3600}))") + if [[ -n "$EXISTING" ]]; then + curl -sf "${AUTH[@]}" -X PUT "${API}/zones/${ZONE_ID}/dns_records/${EXISTING}" \ + -d "$BODY" > /dev/null + echo "updated ${NAME} -> ${CONTENT}" + else + curl -sf "${AUTH[@]}" -X POST "${API}/zones/${ZONE_ID}/dns_records" \ + -d "$BODY" > /dev/null + echo "created ${NAME} -> ${CONTENT}" + fi +done + +echo +echo "done — verify propagation with:" +echo " dig +short TXT _lexicon.actor.${ZONE_NAME}" +echo " goat lex check-dns internal/atproto/lexicon/social/coves" -- 2.51.2