// Session auth for the swarm's bsky-login path. The PEER is the sole auth authority: it verifies the
// app-password against `appPasswordHash` and mints/verifies JWTs with the peer-held `jwtSecret`. The
// keyless bridge relays create-session/refresh-session over RPC but never sees credentials or the
// secret. Mirrors lib/peer.js's session logic exactly, so a bsky client logging in against the swarm
// behaves identically to the single-tenant peer. Secrets are carried in the swarm config (both
// devices), since the swarm boots from the config bundle, not the single-tenant vault.
import { verifyPassword, signJwt, verifyJwt } from 'atproto-identity/lib/auth.js'
const ACCESS_TTL_SEC = 2 * 60 * 60 // 2h — standard atproto access-token lifetime
const REFRESH_TTL_SEC = 90 * 24 * 60 * 60 // 90d — standard atproto refresh-token lifetime
export function makeSessions ({ did, handle, appPasswordHash, jwtSecret } = {}) {
const canLogin = !!(jwtSecret && appPasswordHash)
const nowSec = () => Math.floor(Date.now() / 1000)
const mint = () => {
const n = nowSec(); const base = { sub: did }
return {
did,
handle,
accessJwt: signJwt({ ...base, scope: 'com.atproto.access', exp: n + ACCESS_TTL_SEC }, jwtSecret),
refreshJwt: signJwt({ ...base, scope: 'com.atproto.refresh', exp: n + REFRESH_TTL_SEC }, jwtSecret)
}
}
return {
canLogin,
// com.atproto.server.createSession — verify the app-password, mint tokens.
createSession ({ identifier, password } = {}) {
if (!canLogin) throw new Error('createSession: sessions not configured on this peer')
if (identifier && identifier !== handle && identifier !== did) throw new Error('createSession: unknown identifier')
if (!password || !verifyPassword(password, appPasswordHash)) throw new Error('createSession: invalid credentials')
return mint()
},
// com.atproto.server.refreshSession — re-mint from a valid refresh token.
refreshSession (refreshJwt) {
if (!canLogin) throw new Error('refreshSession: sessions not configured on this peer')
const payload = verifyJwt(refreshJwt, jwtSecret, { nowSec: nowSec() })
if (payload.scope !== 'com.atproto.refresh') throw new Error('refreshSession: not a refresh token')
return mint()
},
// Verify an access token (for the write path): throws unless a valid, in-scope access JWT for this DID.
verifyAccess (accessJwt) {
if (!canLogin) throw new Error('verifyAccess: sessions not configured on this peer')
const payload = verifyJwt(accessJwt, jwtSecret, { nowSec: nowSec() })
if (payload.scope !== 'com.atproto.access') throw new Error('verifyAccess: not an access token')
if (payload.sub && payload.sub !== did) throw new Error('verifyAccess: token subject mismatch')
return payload
}
}
}