/** * Keeps the instance's Ed25519 key, which signs what it tells other instances * in federated sign-in: the identity tokens of `identity-provider-service.ts` * and the events of its outbox. The key is made the first time it's needed * and stored in `federation_keys`, so it survives restarts; other instances * fetch its public half from `GET /.well-known/jwks.json`. * * @module */ import type { JSONWebKeySet, JWK } from "@panva/jose"; import { db } from "../db/connection.ts"; import { generateInstanceKey, importInstanceKey, type InstanceSigningKey, } from "../lib/jwt.ts"; /** * Holds the key once loaded. It is a promise so that two first requests in a * row don't each make a key. */ let current: Promise | null = null; /** * Returns the key the instance signs with now: the newest stored, or a new * one, stored, when there is none. */ export function getSigningKey(): Promise { current ??= loadOrCreate().catch((err) => { current = null; throw err; }); return current; } /** Reads the newest stored key, or makes and stores the first one. */ async function loadOrCreate(): Promise { const row = db.prepare( `SELECT kid, private_jwk FROM federation_keys ORDER BY created_at DESC LIMIT 1;`, ).get() as { kid: string; private_jwk: string } | undefined; if (row) { return await importInstanceKey(row.kid, JSON.parse(row.private_jwk)); } const { kid, privateJwk, publicJwk } = await generateInstanceKey(); db.prepare( `INSERT INTO federation_keys (kid, private_jwk, public_jwk, created_at) VALUES (?, ?, ?, ?);`, ).run( kid, JSON.stringify(privateJwk), JSON.stringify(publicJwk), new Date().toISOString(), ); return await importInstanceKey(kid, privateJwk); } /** * Returns the public halves of every stored key, for * `GET /.well-known/jwks.json`. It makes the first key when there is none, so * that the set is never empty. */ export async function getPublicKeys(): Promise { await getSigningKey(); const rows = db.prepare( `SELECT public_jwk FROM federation_keys ORDER BY created_at DESC;`, ).all() as { public_jwk: string }[]; return { keys: rows.map((r) => JSON.parse(r.public_jwk) as JWK) }; } /** Forgets the loaded key, for the tests, which wipe the table between cases. */ export function resetSigningKey(): void { current = null; }