···2020 // so this is safe to use as an opaque link key.
2121 const url = await client.authorize(handle, {
2222 state: installationId,
2323- scope: 'atproto transition:generic',
2323+ scope: SYNCHUB_OAUTH_SCOPE,
2424 })
25252626 await sendRedirect(event, url.toString(), 302)
+26-2
server/utils/atproto-oauth.ts
···1212import { useDb } from './db'
1313import { decrypt, encrypt } from './encryption'
14141515+/**
1616+ * Granular permissions per https://atproto.com/specs/permission.
1717+ *
1818+ * - `atproto`: required by the OAuth profile.
1919+ * - `repo:sh.tangled.publicKey`: write the user's tangled SSH public key
2020+ * records (publish on connect, rotate from the dashboard).
2121+ * - `repo:sh.tangled.repo`: write `sh.tangled.repo` records (initial repo
2222+ * enrolment, plus future description / topic updates).
2323+ * - `rpc:sh.tangled.repo.create?aud=*`: call the `sh.tangled.repo.create`
2424+ * procedure on any knot, and by extension mint the matching service-auth
2525+ * JWT via the PDS. We use `aud=*` rather than pinning a specific knot
2626+ * because (a) the granular-scope spec requires `aud` to be either a
2727+ * fragmented `did:web:host#service` or `*`, but tangled knots accept
2828+ * plain `did:web:host` audiences on issued JWTs, and (b) it's
2929+ * forward-compatible with per-user knots (PLAN.md open question 1).
3030+ * Still narrowly scoped — only one specific procedure NSID.
3131+ */
3232+export const SYNCHUB_OAUTH_SCOPE = [
3333+ 'atproto',
3434+ 'repo:sh.tangled.publicKey',
3535+ 'repo:sh.tangled.repo',
3636+ 'rpc:sh.tangled.repo.create?aud=*',
3737+].join(' ')
3838+1539let cachedClient: NodeOAuthClient | undefined
16401741/**
···4266 const isLoopback = publicURL.startsWith('http://127.0.0.1') || publicURL.startsWith('http://localhost')
4367 const clientId = isLoopback
4468 // Loopback dev: spec-defined synthetic client_id; no metadata fetched by PDS.
4545- ? `http://localhost?redirect_uri=${encodeURIComponent(`${publicURL}/api/atproto/callback`)}&scope=${encodeURIComponent('atproto transition:generic')}`
6969+ ? `http://localhost?redirect_uri=${encodeURIComponent(`${publicURL}/api/atproto/callback`)}&scope=${encodeURIComponent(SYNCHUB_OAUTH_SCOPE)}`
4670 : `${publicURL}/.well-known/atproto-client-metadata.json`
47714872 const options: NodeOAuthClientOptions = {
···5377 redirect_uris: [`${publicURL}/api/atproto/callback`],
5478 grant_types: ['authorization_code', 'refresh_token'],
5579 response_types: ['code'],
5656- scope: 'atproto transition:generic',
8080+ scope: SYNCHUB_OAUTH_SCOPE,
5781 application_type: 'web',
5882 token_endpoint_auth_method: 'private_key_jwt',
5983 token_endpoint_auth_signing_alg: 'ES256',