/** * Prepares federated sign-in, by which a user signs in here with their * account on another gerbeur instance, and the other way round. It adds the * instance's signing key, the link from a local account to the one it stands * for elsewhere, the instances an admin trusts or blocks, the state of the * sign-ins in progress on both sides, the grants users gave other instances, * and the events owed to them. * * `/authorize` and `/federation/callback` become pages, so their first * segments are reserved. * * An account from another instance has no password here: its `password_hash` * is `''`, which no password verifies. Emails stay unique among local accounts * only, since the same person may also have a local account, or come from two * instances, with one address. * * @module */ import type { DatabaseSync } from "node:sqlite"; import { renameReservedCategories } from "../../lib/reserved-slugs.ts"; /** * Run once by `runMigrations`. Nothing exists to convert, since every account * so far is local, but `authorize` and `federation` become the pages of a * sign-in, so a category that had one as its slug is renamed. */ export function up(db: DatabaseSync): void { db.exec(` CREATE TABLE IF NOT EXISTS federation_keys ( kid TEXT PRIMARY KEY, private_jwk TEXT NOT NULL, public_jwk TEXT NOT NULL, created_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS user_identities ( user_id TEXT PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE, issuer TEXT NOT NULL, subject TEXT NOT NULL, picture TEXT, synced_at TEXT NOT NULL ); CREATE UNIQUE INDEX IF NOT EXISTS idx_user_identities_subject ON user_identities(issuer, subject); CREATE TABLE IF NOT EXISTS federation_peers ( issuer TEXT PRIMARY KEY, status TEXT NOT NULL, name TEXT, created_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS federation_logins ( state TEXT PRIMARY KEY, issuer TEXT NOT NULL, code_verifier TEXT NOT NULL, nonce TEXT NOT NULL, invite_token TEXT, created_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS oauth_codes ( code_hash TEXT PRIMARY KEY, user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, client_id TEXT NOT NULL, redirect_uri TEXT NOT NULL, code_challenge TEXT NOT NULL, nonce TEXT NOT NULL, expires_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS federation_grants ( user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, client_id TEXT NOT NULL, client_name TEXT, granted_at TEXT NOT NULL, last_used_at TEXT NOT NULL, PRIMARY KEY (user_id, client_id) ); CREATE TABLE IF NOT EXISTS federation_outbox ( id TEXT PRIMARY KEY, client_id TEXT NOT NULL, event TEXT NOT NULL, attempts INTEGER NOT NULL DEFAULT 0, next_attempt_at TEXT NOT NULL, created_at TEXT NOT NULL ); CREATE INDEX IF NOT EXISTS idx_federation_outbox_next ON federation_outbox(next_attempt_at); `); const emailIndex = db.prepare( `SELECT sql FROM sqlite_master WHERE name = 'idx_users_email_lower';`, ).get() as { sql: string } | undefined; if (!emailIndex?.sql.includes("WHERE")) { db.exec(` DROP INDEX IF EXISTS idx_users_email_lower; CREATE UNIQUE INDEX idx_users_email_lower ON users(lower(email)) WHERE password_hash != ''; `); } renameReservedCategories(db); }