From a99f13da0f1da1a87c4e956ed69a178dfba59baa Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Mon, 4 May 2026 15:02:36 +0200 Subject: [PATCH] fix: tidy up scopes --- server/api/atproto/login.get.ts | 2 +- server/utils/atproto-oauth.ts | 28 ++++++++++++++++++++++++++-- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/server/api/atproto/login.get.ts b/server/api/atproto/login.get.ts index 3cad7b7..aa4d1e1 100644 --- a/server/api/atproto/login.get.ts +++ b/server/api/atproto/login.get.ts @@ -20,7 +20,7 @@ export default defineEventHandler(async event => { // so this is safe to use as an opaque link key. const url = await client.authorize(handle, { state: installationId, - scope: 'atproto transition:generic', + scope: SYNCHUB_OAUTH_SCOPE, }) await sendRedirect(event, url.toString(), 302) diff --git a/server/utils/atproto-oauth.ts b/server/utils/atproto-oauth.ts index 7efcbbf..f1a05f0 100644 --- a/server/utils/atproto-oauth.ts +++ b/server/utils/atproto-oauth.ts @@ -12,6 +12,30 @@ import { atprotoSession, atprotoState } from '../db/schema' import { useDb } from './db' import { decrypt, encrypt } from './encryption' +/** + * Granular permissions per https://atproto.com/specs/permission. + * + * - `atproto`: required by the OAuth profile. + * - `repo:sh.tangled.publicKey`: write the user's tangled SSH public key + * records (publish on connect, rotate from the dashboard). + * - `repo:sh.tangled.repo`: write `sh.tangled.repo` records (initial repo + * enrolment, plus future description / topic updates). + * - `rpc:sh.tangled.repo.create?aud=*`: call the `sh.tangled.repo.create` + * procedure on any knot, and by extension mint the matching service-auth + * JWT via the PDS. We use `aud=*` rather than pinning a specific knot + * because (a) the granular-scope spec requires `aud` to be either a + * fragmented `did:web:host#service` or `*`, but tangled knots accept + * plain `did:web:host` audiences on issued JWTs, and (b) it's + * forward-compatible with per-user knots (PLAN.md open question 1). + * Still narrowly scoped — only one specific procedure NSID. + */ +export const SYNCHUB_OAUTH_SCOPE = [ + 'atproto', + 'repo:sh.tangled.publicKey', + 'repo:sh.tangled.repo', + 'rpc:sh.tangled.repo.create?aud=*', +].join(' ') + let cachedClient: NodeOAuthClient | undefined /** @@ -42,7 +66,7 @@ export async function useOAuthClient(): Promise { const isLoopback = publicURL.startsWith('http://127.0.0.1') || publicURL.startsWith('http://localhost') const clientId = isLoopback // Loopback dev: spec-defined synthetic client_id; no metadata fetched by PDS. - ? `http://localhost?redirect_uri=${encodeURIComponent(`${publicURL}/api/atproto/callback`)}&scope=${encodeURIComponent('atproto transition:generic')}` + ? `http://localhost?redirect_uri=${encodeURIComponent(`${publicURL}/api/atproto/callback`)}&scope=${encodeURIComponent(SYNCHUB_OAUTH_SCOPE)}` : `${publicURL}/.well-known/atproto-client-metadata.json` const options: NodeOAuthClientOptions = { @@ -53,7 +77,7 @@ export async function useOAuthClient(): Promise { redirect_uris: [`${publicURL}/api/atproto/callback`], grant_types: ['authorization_code', 'refresh_token'], response_types: ['code'], - scope: 'atproto transition:generic', + scope: SYNCHUB_OAUTH_SCOPE, application_type: 'web', token_endpoint_auth_method: 'private_key_jwt', token_endpoint_auth_signing_alg: 'ES256', -- 2.51.2