OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-proto-peer / lib / sessions.js

51 lines · javascript ref:
// 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
    }
  }
}