this repo has no description
0
fork

Configure Feed

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

feat(oauth): request minimum-permission scope via permission-set lexicon

Replaces the broad `transition:generic` scope with an `include:` reference
to a new `com.atmosphereaccount.registry.fullPermissions` permission-set
lexicon, which grants only the writes this app actually needs:

- repo writes to com.atmosphereaccount.registry.profile
- blob:image/* uploads (avatars)

The permission-set's title/detail render in the user's consent dialog,
so reducing scope also makes the prompt clearer.

Made-with: Cursor

+62 -3
+23
lexicons/com/atmosphereaccount/registry/fullPermissions.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "com.atmosphereaccount.registry.fullPermissions", 4 + "defs": { 5 + "main": { 6 + "type": "permission-set", 7 + "title": "Atmosphere Account", 8 + "detail": "Manage your Atmosphere Explore profile and avatar.", 9 + "permissions": [ 10 + { 11 + "type": "permission", 12 + "resource": "repo", 13 + "collection": ["com.atmosphereaccount.registry.profile"] 14 + }, 15 + { 16 + "type": "permission", 17 + "resource": "blob", 18 + "accept": ["image/*"] 19 + } 20 + ] 21 + } 22 + } 23 + }
+13 -1
lib/lexicons.ts
··· 9 9 10 10 export const PROFILE_NSID = "com.atmosphereaccount.registry.profile"; 11 11 export const FEATURED_NSID = "com.atmosphereaccount.registry.featured"; 12 + /** 13 + * Permission-set lexicon NSID requested via the OAuth `include:` scope. 14 + * The set itself only references the profile collection + image blobs; 15 + * see `lexicons/com/atmosphereaccount/registry/fullPermissions.json`. 16 + */ 17 + export const PERMISSION_SET_NSID = 18 + "com.atmosphereaccount.registry.fullPermissions"; 12 19 13 20 export const REGISTRY_NSIDS = [ 14 21 PROFILE_NSID, 15 22 FEATURED_NSID, 23 + PERMISSION_SET_NSID, 16 24 ] as const; 17 25 18 26 export const CATEGORIES = [ ··· 211 219 .includes(kind); 212 220 if (e.url !== undefined && e.url !== null && e.url !== "") { 213 221 if (!isUrl(e.url)) { 214 - return { ok: false, error: `links[].url (${kind}): must be http(s) URL` }; 222 + return { 223 + ok: false, 224 + error: `links[].url (${kind}): must be http(s) URL`, 225 + }; 215 226 } 216 227 entry.url = (e.url as string).trim(); 217 228 } else if (!isAtmosphere) { ··· 407 418 const fileMap: Record<string, string> = { 408 419 [PROFILE_NSID]: "profile.json", 409 420 [FEATURED_NSID]: "featured.json", 421 + [PERMISSION_SET_NSID]: "fullPermissions.json", 410 422 }; 411 423 const filename = fileMap[nsid]; 412 424 const url = new URL(
+14 -1
lib/oauth.ts
··· 35 35 redirectUri, 36 36 } from "./env.ts"; 37 37 38 - const DEFAULT_SCOPE = "atproto transition:generic"; 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) 43 + * 44 + * This MUST stay in sync with `routes/oauth/client-metadata.json.ts`, 45 + * which is the document atproto authorization servers actually fetch. 46 + * 47 + * Inline-form equivalent (for reference): 48 + * atproto repo:com.atmosphereaccount.registry.profile blob:image/* 49 + */ 50 + const DEFAULT_SCOPE = 51 + "atproto include:com.atmosphereaccount.registry.fullPermissions"; 39 52 const STATE_TTL_MS = 10 * 60 * 1000; 40 53 const ACCESS_TOKEN_REFRESH_THRESHOLD_MS = 60 * 1000; 41 54
+12 -1
routes/oauth/client-metadata.json.ts
··· 23 23 grant_types: ["authorization_code", "refresh_token"], 24 24 response_types: ["code"], 25 25 redirect_uris: [redirectUri()], 26 - scope: "atproto transition:generic", 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. 32 + * 33 + * Inline-form equivalent (kept as a reference for maintainers): 34 + * atproto repo:com.atmosphereaccount.registry.profile blob:image/* 35 + */ 36 + scope: 37 + "atproto include:com.atmosphereaccount.registry.fullPermissions", 27 38 dpop_bound_access_tokens: true, 28 39 token_endpoint_auth_method: "private_key_jwt", 29 40 token_endpoint_auth_signing_alg: "ES256",