···11+-- D1 account directory schema for rookery
22+-- Apply with: wrangler d1 execute rookery-directory --file schema/directory.sql
33+44+CREATE TABLE IF NOT EXISTS accounts (
55+ did TEXT PRIMARY KEY,
66+ handle TEXT NOT NULL UNIQUE,
77+ do_id TEXT NOT NULL,
88+ active INTEGER NOT NULL DEFAULT 1,
99+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
1010+);
1111+1212+CREATE INDEX IF NOT EXISTS idx_accounts_handle ON accounts(handle);
+55
src/directory.ts
···11+import type { Env } from "./types";
22+33+const SCHEMA = `
44+CREATE TABLE IF NOT EXISTS accounts (
55+ did TEXT PRIMARY KEY,
66+ handle TEXT NOT NULL UNIQUE,
77+ do_id TEXT NOT NULL,
88+ active INTEGER NOT NULL DEFAULT 1,
99+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
1010+);
1111+`;
1212+1313+const INDEX = "CREATE INDEX IF NOT EXISTS idx_accounts_handle ON accounts(handle);";
1414+1515+export async function initDirectory(db: D1Database): Promise<void> {
1616+ await db.batch([
1717+ db.prepare(SCHEMA),
1818+ db.prepare(INDEX),
1919+ ]);
2020+}
2121+2222+export class RepoNotFoundError extends Error {
2323+ constructor(message: string) {
2424+ super(message);
2525+ this.name = "RepoNotFoundError";
2626+ }
2727+}
2828+2929+export async function resolveRepo(
3030+ repo: string,
3131+ env: Env,
3232+): Promise<{ did: string; doId: string }> {
3333+ const row = repo.includes(":")
3434+ ? await env.DIRECTORY.prepare(
3535+ "SELECT did, do_id FROM accounts WHERE did = ? AND active = 1",
3636+ ).bind(repo).first<{ did: string; do_id: string }>()
3737+ : await env.DIRECTORY.prepare(
3838+ "SELECT did, do_id FROM accounts WHERE handle = ? AND active = 1",
3939+ ).bind(repo).first<{ did: string; do_id: string }>();
4040+4141+ if (!row) {
4242+ throw new RepoNotFoundError(`Repository not found: ${repo}`);
4343+ }
4444+4545+ return { did: row.did, doId: row.do_id };
4646+}
4747+4848+export async function insertAccount(
4949+ db: D1Database,
5050+ account: { did: string; handle: string; doId: string },
5151+): Promise<void> {
5252+ await db.prepare(
5353+ "INSERT INTO accounts (did, handle, do_id) VALUES (?, ?, ?)",
5454+ ).bind(account.did, account.handle, account.doId).run();
5555+}
+2
src/types.ts
···77export interface Env {
88 /** Durable Object namespace for account storage */
99 ACCOUNT: DurableObjectNamespace<AccountDurableObject>;
1010+ /** D1 account directory for cross-account queries */
1111+ DIRECTORY: D1Database;
1012 /** Public hostname of the PDS */
1113 ROOKERY_HOSTNAME: string;
1214 /** Handle domain suffix (e.g. ".pds.example.com") */