this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix(oauth): make permission-set lexicon resolvable by auth servers

Per the atproto Lexicon spec, NSID resolution is DNS-based (TXT record at
_lexicon.<reversed-authority> pointing at a DID, plus a schema record on
that DID's PDS) — not HTTP. Without that infrastructure, PDSes returned
`invalid_scope` for include:com.atmosphereaccount.registry.fullPermissions
and login was blocked.

- Drop the blob permission from fullPermissions.json: the permission spec
forbids blob permissions inside permission sets, so goat lex publish
would have rejected the schema. Add blob:image/* as a top-level scope
alongside the include: in lib/oauth.ts and client-metadata.json.ts —
same effective access, valid spec shape.
- Add deno tasks (lex:lint, lex:check-dns, lex:status, lex:publish,
lex:publish:update) wrapping goat for one-shot DNS verification and
publication.
- Document the full one-time setup (DNS TXT at Porkbun + app password +
goat lex publish) and day-to-day workflow in docs/PUBLISHING_LEXICONS.md.

Made-with: Cursor

+211 -23
+5
deno.json
··· 10 10 "extract:lottie-assets": "deno run -A scripts/extract-lottie-assets.ts", 11 11 "indexer": "deno run -A worker/indexer.ts", 12 12 "publish:featured": "deno run -A scripts/publish-featured.ts", 13 + "lex:lint": "goat lex lint lexicons/", 14 + "lex:check-dns": "goat lex check-dns --example-did did:plc:ab7uvkn4kyf7l7prl26pz4r2 lexicons/", 15 + "lex:status": "goat lex status lexicons/", 16 + "lex:publish": "goat lex publish lexicons/", 17 + "lex:publish:update": "goat lex publish --update lexicons/", 13 18 "start": "deno serve -A _fresh/server.js", 14 19 "update": "deno run -A -r jsr:@fresh/update ." 15 20 },
+168
docs/PUBLISHING_LEXICONS.md
··· 1 + # Publishing Atmosphere Account lexicons 2 + 3 + This project owns the `com.atmosphereaccount.registry.*` Lexicon namespace. The 4 + schemas live in [`lexicons/`](../lexicons) and are the source of truth for both 5 + our records and our OAuth permission set. 6 + 7 + For the OAuth login flow to work, atproto authorization servers (the user's PDS) 8 + must be able to resolve the permission-set lexicon 9 + `com.atmosphereaccount.registry.fullPermissions` at runtime. Resolution is 10 + DNS-based per the [atproto Lexicon spec](https://atproto.com/specs/lexicon), not 11 + HTTP — serving the JSON at `/.well-known/atproto-lexicon/...` is **not** 12 + sufficient. 13 + 14 + This document explains the one-time setup and the day-to-day publish flow. 15 + 16 + --- 17 + 18 + ## Authority DID 19 + 20 + All `com.atmosphereaccount.registry.*` lexicons are published by: 21 + 22 + | | | 23 + | ---------- | ---------------------------------------------- | 24 + | **DID** | `did:plc:ab7uvkn4kyf7l7prl26pz4r2` | 25 + | **Handle** | `atmosphereaccount.com` | 26 + | **PDS** | `https://stropharia.us-west.host.bsky.network` | 27 + 28 + This is the Bluesky account registered for `atmosphereaccount.com`. It is the 29 + **only** account that may publish or update lexicons under this namespace — DNS 30 + authority for `_lexicon.registry.atmosphereaccount.com` points exclusively at 31 + this DID. 32 + 33 + > **Do not change which DID owns the namespace casually.** Rotating the 34 + > authority DID invalidates every existing OAuth consent and breaks every 35 + > resolver that has cached the old DID. If a rotation is ever truly needed, the 36 + > [Lexicon spec § "Authority crisis"](https://atproto.com/specs/lexicon) 37 + > describes the recovery path. 38 + 39 + --- 40 + 41 + ## One-time setup 42 + 43 + ### 1. DNS TXT record 44 + 45 + Add the following record at Porkbun (the DNS provider for 46 + `atmosphereaccount.com`): 47 + 48 + | Type | Host | Answer | 49 + | ----- | ------------------- | -------------------------------------- | 50 + | `TXT` | `_lexicon.registry` | `did=did:plc:ab7uvkn4kyf7l7prl26pz4r2` | 51 + 52 + > Porkbun's DNS UI takes the **sub-domain part only** in the "Host" field, so 53 + > enter `_lexicon.registry` (not the full 54 + > `_lexicon.registry.atmosphereaccount.com`). The `did=` prefix in the value is 55 + > required by the spec — do not omit it. 56 + 57 + Verify propagation with: 58 + 59 + ```bash 60 + dig +short TXT _lexicon.registry.atmosphereaccount.com @1.1.1.1 61 + # expected: "did=did:plc:ab7uvkn4kyf7l7prl26pz4r2" 62 + ``` 63 + 64 + Or run our preview task, which performs the same lookup using `goat`: 65 + 66 + ```bash 67 + deno task lex:check-dns 68 + ``` 69 + 70 + A clean run prints nothing about missing entries. 71 + 72 + ### 2. App password for `goat` 73 + 74 + `goat lex publish` writes records to the authority account's PDS. It needs 75 + credentials. **Always use an app password**, not the main account password: 76 + 77 + 1. Sign in to https://bsky.app as `atmosphereaccount.com`. 78 + 2. Settings → Privacy & Security → App Passwords → "Add app password". 79 + 3. Name it something obvious like `goat-lex-publish`. 80 + 4. Save it to your password manager — it's shown only once. 81 + 82 + Export it for `goat`: 83 + 84 + ```bash 85 + export GOAT_USERNAME=atmosphereaccount.com 86 + export GOAT_PASSWORD='xxxx-xxxx-xxxx-xxxx' 87 + ``` 88 + 89 + (Or pass `--username` / `--app-password` to each invocation.) 90 + 91 + ### 3. First-time publish 92 + 93 + ```bash 94 + deno task lex:lint # style + best-practice check (warnings OK) 95 + deno task lex:check-dns # confirm DNS is in place 96 + deno task lex:publish # create the schema records 97 + ``` 98 + 99 + `goat lex publish` only creates records that don't already exist. To update an 100 + existing schema record, use: 101 + 102 + ```bash 103 + deno task lex:publish:update 104 + ``` 105 + 106 + Updates are constrained by the same backwards-compatibility rules as any atproto 107 + lexicon — see [Lexicon § "Versioning"](https://atproto.com/specs/lexicon). 108 + 109 + --- 110 + 111 + ## Day-to-day workflow 112 + 113 + When you add or modify a lexicon in `lexicons/`: 114 + 115 + 1. `deno task lex:lint` — fix any new warnings you can. 116 + 2. `deno task lex:status` — show what's drifted between local and live. 117 + 3. `deno task lex:publish:update` — push changes to the PDS. 118 + 4. Wait a few seconds, then verify resolution end-to-end: 119 + 120 + ```bash 121 + goat lex resolve com.atmosphereaccount.registry.fullPermissions 122 + ``` 123 + 124 + You should see the schema record JSON. If you get an error, the most common 125 + causes are: 126 + 127 + - DNS TXT record missing or wrong (`deno task lex:check-dns`) 128 + - Schema record not yet replicated to the relay used by `goat resolve` (wait 129 + 30-60s and retry) 130 + - Authentication failed (wrong `GOAT_PASSWORD` or expired app password) 131 + 132 + --- 133 + 134 + ## OAuth integration notes 135 + 136 + The login flow requests this scope (see `lib/oauth.ts`): 137 + 138 + ``` 139 + atproto include:com.atmosphereaccount.registry.fullPermissions blob:image/* 140 + ``` 141 + 142 + - **`include:...`** — the authorization server resolves this NSID via DNS and 143 + reads the schema record's `title` / `detail` to render the consent dialog. It 144 + also expands the `permissions[]` array into the actual granted scope. If 145 + resolution fails the PDS returns `invalid_scope`, which is exactly what 146 + blocked logins until DNS was set up. 147 + - **`blob:image/*`** is a top-level scope on purpose. The atproto permission 148 + spec 149 + [explicitly disallows `blob` permissions inside 150 + permission sets](https://atproto.com/specs/permission#permission-sets) — they 151 + must always be requested separately. 152 + 153 + If you change the permission set's `title`, `detail`, or `permissions[]`, 154 + remember the consent dialog won't reflect it until you `lex:publish:update` 155 + **and** the cache on the user's auth server expires. 156 + 157 + --- 158 + 159 + ## Useful references 160 + 161 + - [Lexicon spec](https://atproto.com/specs/lexicon) — the resolution algorithm, 162 + in detail. 163 + - [Permissions spec](https://atproto.com/specs/permission) — what can and can't 164 + go into a permission set, and how scope strings are constructed. 165 + - [Permission Sets guide](https://atproto.com/guides/permission-sets) — the 166 + friendly walk-through with examples. 167 + - [Lexicon Garden — Adding Lexicons](https://lexicon.garden/help/adding-lexicons) 168 + — third-party guide that mirrors the steps above.
+1 -6
lexicons/com/atmosphereaccount/registry/fullPermissions.json
··· 5 5 "main": { 6 6 "type": "permission-set", 7 7 "title": "Atmosphere Account", 8 - "detail": "Manage your Atmosphere Explore profile and avatar.", 8 + "detail": "Manage your Atmosphere Explore profile (create, update, and remove your project listing).", 9 9 "permissions": [ 10 10 { 11 11 "type": "permission", 12 12 "resource": "repo", 13 13 "collection": ["com.atmosphereaccount.registry.profile"] 14 - }, 15 - { 16 - "type": "permission", 17 - "resource": "blob", 18 - "accept": ["image/*"] 19 14 } 20 15 ] 21 16 }
+27 -9
lib/oauth.ts
··· 36 36 } from "./env.ts"; 37 37 38 38 /** 39 - * Minimum-permission scope. The permission-set lexicon 40 - * (`com.atmosphereaccount.registry.fullPermissions`) grants only: 41 - * - repo writes (create/update/delete) to com.atmosphereaccount.registry.profile 42 - * - image/* blob uploads (for avatars) 39 + * Minimum-permission scope. 43 40 * 44 - * This MUST stay in sync with `routes/oauth/client-metadata.json.ts`, 45 - * which is the document atproto authorization servers actually fetch. 41 + * Composed of three parts: 42 + * - `atproto` - identity 43 + * - `include:com.atmosphereaccount.registry.fullPermissions` - repo writes 44 + * to our 45 + * profile 46 + * collection 47 + * (resolved 48 + * dynamically 49 + * from the 50 + * published 51 + * permission-set 52 + * lexicon) 53 + * - `blob:image/*` - avatar + 54 + * SVG icon 55 + * uploads 46 56 * 47 - * Inline-form equivalent (for reference): 48 - * atproto repo:com.atmosphereaccount.registry.profile blob:image/* 57 + * The `blob` scope is intentionally NOT bundled into the permission set 58 + * because the atproto permission spec explicitly disallows blob permissions 59 + * inside permission sets — they must always be requested separately. 60 + * See https://atproto.com/specs/permission ("Permission Sets"). 61 + * 62 + * The permission-set lexicon must be published to the DID that holds DNS 63 + * authority for `_lexicon.registry.atmosphereaccount.com` before this scope 64 + * will resolve. See `docs/PUBLISHING_LEXICONS.md` for setup steps. 65 + * 66 + * MUST stay in sync with `routes/oauth/client-metadata.json.ts`. 49 67 */ 50 68 const DEFAULT_SCOPE = 51 - "atproto include:com.atmosphereaccount.registry.fullPermissions"; 69 + "atproto include:com.atmosphereaccount.registry.fullPermissions blob:image/*"; 52 70 const STATE_TTL_MS = 10 * 60 * 1000; 53 71 const ACCESS_TOKEN_REFRESH_THRESHOLD_MS = 60 * 1000; 54 72
+10 -8
routes/oauth/client-metadata.json.ts
··· 24 24 response_types: ["code"], 25 25 redirect_uris: [redirectUri()], 26 26 /** 27 - * Minimum-permission scope: only writes to our own profile 28 - * collection plus image-blob uploads (for avatars). The 29 - * permission-set lexicon is published at 30 - * /.well-known/atproto-lexicon/com.atmosphereaccount.registry.fullPermissions 31 - * and rendered in the consent dialog via its title/detail. 27 + * Minimum-permission scope. The `include:` half is resolved by the 28 + * authorization server via DNS-based atproto lexicon resolution 29 + * (TXT record at `_lexicon.registry.atmosphereaccount.com`) and 30 + * rendered in the consent dialog via its `title` / `detail`. 32 31 * 33 - * Inline-form equivalent (kept as a reference for maintainers): 34 - * atproto repo:com.atmosphereaccount.registry.profile blob:image/* 32 + * `blob:image/*` is requested as a top-level scope because the 33 + * atproto permission spec explicitly disallows blob permissions 34 + * inside permission sets. 35 + * 36 + * MUST stay in sync with `DEFAULT_SCOPE` in `lib/oauth.ts`. 35 37 */ 36 38 scope: 37 - "atproto include:com.atmosphereaccount.registry.fullPermissions", 39 + "atproto include:com.atmosphereaccount.registry.fullPermissions blob:image/*", 38 40 dpop_bound_access_tokens: true, 39 41 token_endpoint_auth_method: "private_key_jwt", 40 42 token_endpoint_auth_signing_alg: "ES256",