/** * On-disk store for OAuth sessions, one JSON file per identifier under * ~/.config/atproto-git. The file holds the token pair and the private DPoP * key they are bound to, so it is written owner-readable only. * * The git credential store cannot hold these: it stores flat * username/password strings, and a DPoP-bound token is useless without its * key. */ import { mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync, } from 'node:fs'; import { homedir } from 'node:os'; import { join } from 'node:path'; /** * @param {Record} env * @returns {string} */ export function sessionDir(env) { if (env.ATPROTO_GIT_CONFIG_DIR) return env.ATPROTO_GIT_CONFIG_DIR; const base = env.XDG_CONFIG_HOME || join(homedir(), '.config'); return join(base, 'atproto-git'); } /** * A filesystem-safe name for an identifier. Handles and DIDs share a charset * that only needs `:` mapped, but escape defensively. * @param {string} identifier * @returns {string} */ function fileName(identifier) { return `${identifier.replace(/[^A-Za-z0-9._-]/g, '_')}.json`; } /** * @param {Record} env * @param {string} identifier - handle or DID the session was created for * @returns {import('./oauth.js').OauthSession|null} */ export function loadSession(env, identifier) { try { const raw = readFileSync( join(sessionDir(env), fileName(identifier)), 'utf8', ); return JSON.parse(raw); } catch { return null; } } /** * Find a session by the DID it authenticated, for callers that know the * account but not the identifier it was logged in under. * @param {Record} env * @param {string} did * @returns {import('./oauth.js').OauthSession|null} */ export function findSessionByDid(env, did) { try { for (const entry of readdirSync(sessionDir(env))) { if (!entry.endsWith('.json')) continue; try { const session = JSON.parse( readFileSync(join(sessionDir(env), entry), 'utf8'), ); if (session?.did === did) return session; } catch { // A malformed file hides one session, not all of them. } } } catch { // No directory means no sessions. } return null; } /** * @param {Record} env * @param {string} identifier * @param {import('./oauth.js').OauthSession} session */ export function saveSession(env, identifier, session) { const dir = sessionDir(env); mkdirSync(dir, { recursive: true, mode: 0o700 }); writeFileSync(join(dir, fileName(identifier)), JSON.stringify(session), { mode: 0o600, }); } /** * @param {Record} env * @param {string} identifier */ export function deleteSession(env, identifier) { rmSync(join(sessionDir(env), fileName(identifier)), { force: true }); }