~ knotless Search/
↑↓ move↵ open⇧↵ all results

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
103 lines · 2.8 KB Raw History
/**
 * 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<string, string|undefined>} 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<string, string|undefined>} 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<string, string|undefined>} 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<string, string|undefined>} 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<string, string|undefined>} env
 * @param {string} identifier
 */
export function deleteSession(env, identifier) {
  rmSync(join(sessionDir(env), fileName(identifier)), { force: true });
}