import { DurableObject } from "cloudflare:workers"; import { loadControlPlaneSecrets } from "../configuration/control-plane.ts"; import type { Env } from "./config.ts"; import { decrypt, encrypt } from "./crypto.ts"; export interface Transaction { bindingHash: string; verifier: string; previousSession: string | null; returnTo: "/connect"; bridgeContinuation?: string; providerInstallationId?: string; domainInstallationId?: string; expiresAt: number; } export interface Principal { subject: string; grantRef: string; selectedAccountId: string | null; expiresAt: number; } export interface Grant { subject: string; accessToken: string; scopes: string[]; expiresAt: number; } export interface BridgeRequest { installationId: string; state: string; challenge: string; audience?: string; expiresAt: number; } export interface BridgeContinuation extends BridgeRequest { bindingHash: string; } export interface BridgeCode extends BridgeRequest { subject: string; audience: string; } export interface OperationBinding { subject: string; accountId: string; installationId: string; operationId: string; } export interface ProtectedOperation extends OperationBinding { grantRef: string; expiresAt: number; bootstrapSecret: string | null; } const matchesOperation = ( value: OperationBinding, expected: OperationBinding, ) => value.subject === expected.subject && value.accountId === expected.accountId && value.installationId === expected.installationId && value.operationId === expected.operationId; type RecordValue = | { kind: "transaction"; value: Transaction } | { kind: "session"; value: Principal } | { kind: "grant"; value: Grant } | { kind: "continuation"; value: BridgeContinuation } | { kind: "code"; value: BridgeCode } | { kind: "operation"; value: ProtectedOperation }; interface Stored { kind: RecordValue["kind"]; expiresAt: number; iv: string; ciphertext: string; } // No public fetch router, listing RPC, metadata database, or token-returning HTTP // endpoint. Only this control-plane Worker's trusted binding can invoke RPC. // Each random transaction/session/grant gets its own strongly consistent object. export class AuthVault extends DurableObject { #encryptionKey() { return loadControlPlaneSecrets(this.env).credentialEncryptionKey.reveal(); } async #open(record: Stored): Promise { const secret = this.#encryptionKey(); try { return await decrypt( secret, record, `${this.ctx.id}:${record.kind}:${record.expiresAt}`, ); } catch (error) { // A rotated key or failed ciphertext authentication invalidates this // credential. Configuration/storage failures are not authentication results. if ( error instanceof DOMException && ["OperationError", "DataError", "InvalidCharacterError"].includes( error.name, ) ) return null; throw error; } } async #put(record: RecordValue) { const expiresAt = record.value.expiresAt; const sealed = await encrypt( this.#encryptionKey(), record.value, `${this.ctx.id}:${record.kind}:${expiresAt}`, ); await this.ctx.storage.transaction(async (tx) => { if (await tx.get("record")) throw new Error("Vault record already exists"); await tx.put("record", { kind: record.kind, expiresAt, ...sealed }); await tx.setAlarm(expiresAt); }); } async createContinuation(value: BridgeContinuation) { await this.#put({ kind: "continuation", value }); } async createCode(value: BridgeCode) { await this.#put({ kind: "code", value }); } async claimContinuation( bindingHash: string, ): Promise { return this.ctx.storage.transaction(async (tx) => { const stored = await tx.get("record"); if ( !stored || stored.kind !== "continuation" || stored.expiresAt <= Date.now() ) return null; const value = await this.#open(stored); if (!value || value.bindingHash !== bindingHash) return null; await tx.delete("record"); return value; }); } async claimCode(expected: { installationId: string; audience: string; state: string; challenge: string; }): Promise { return this.ctx.storage.transaction(async (tx) => { const stored = await tx.get("record"); if (!stored || stored.kind !== "code" || stored.expiresAt <= Date.now()) return null; const value = await this.#open(stored); if ( !value || Object.entries(expected).some( ([key, item]) => value[key as keyof BridgeCode] !== item, ) ) return null; await tx.delete("record"); return value; }); } async createOperation( value: ProtectedOperation, ): Promise { if ( !value.subject || value.subject.length > 512 || !/^[a-f0-9]{32}$/.test(value.accountId) || !/^[a-f0-9]{32}$/.test(value.installationId) || !/^[a-f0-9]{32}$/.test(value.operationId) || !/^[A-Za-z0-9_-]{43}$/.test(value.grantRef) || !Number.isSafeInteger(value.expiresAt) || value.expiresAt <= Date.now() || value.expiresAt > Date.now() + 8 * 60 * 60_000 || (value.bootstrapSecret !== null && !/^[A-Za-z0-9_-]{43}$/.test(value.bootstrapSecret)) ) throw new Error("Invalid protected operation"); const grant = await this.env.AUTH_VAULT.get( this.env.AUTH_VAULT.idFromName(`grant:${value.grantRef}`), ).grant(value.subject); if (!grant || value.expiresAt > grant.expiresAt) throw new Error("Protected operation authorization unavailable"); const sealed = await encrypt( this.#encryptionKey(), value, `${this.ctx.id}:operation:${value.expiresAt}`, ); return this.ctx.storage.transaction(async (tx) => { const existing = await tx.get("record"); if (existing) { if (existing.kind !== "operation" || existing.expiresAt <= Date.now()) throw new Error("Protected operation unavailable"); const previous = await this.#open(existing); if ( !previous || !matchesOperation(previous, value) || previous.grantRef !== value.grantRef || previous.expiresAt !== value.expiresAt ) throw new Error("Protected operation conflict"); return previous; } await tx.put("record", { kind: "operation", expiresAt: value.expiresAt, ...sealed, }); await tx.setAlarm(value.expiresAt); return value; }); } async operation( expected: OperationBinding, ): Promise { const stored = await this.ctx.storage.get("record"); if ( !stored || stored.kind !== "operation" || stored.expiresAt <= Date.now() ) return null; const value = await this.#open(stored); return value && matchesOperation(value, expected) ? value : null; } async retireBootstrap(expected: OperationBinding): Promise { return this.ctx.storage.transaction(async (tx) => { const stored = await tx.get("record"); if ( !stored || stored.kind !== "operation" || stored.expiresAt <= Date.now() ) return false; const value = await this.#open(stored); if (!value || !matchesOperation(value, expected)) return false; value.bootstrapSecret = null; const sealed = await encrypt( this.#encryptionKey(), value, `${this.ctx.id}:operation:${value.expiresAt}`, ); await tx.put("record", { kind: "operation", expiresAt: value.expiresAt, ...sealed, }); return true; }); } async createTransaction(value: Transaction) { await this.#put({ kind: "transaction", value }); } async createSession(value: Principal) { await this.#put({ kind: "session", value }); } async createGrant(value: Grant) { await this.#put({ kind: "grant", value }); } async claimTransaction( bindingHash: string, previousSession: string | null, ): Promise { // Read/compare/delete are one native storage transaction. The code exchange // happens only after this commit; failures cannot reopen a claimed state. return this.ctx.storage.transaction(async (tx) => { const stored = await tx.get("record"); if ( !stored || stored.kind !== "transaction" || stored.expiresAt <= Date.now() ) return null; const value = await this.#open(stored); if ( !value || value.bindingHash !== bindingHash || value.previousSession !== previousSession ) return null; await tx.delete("record"); return value; }); } async session(): Promise { const stored = await this.ctx.storage.get("record"); if (!stored || stored.kind !== "session" || stored.expiresAt <= Date.now()) return null; return this.#open(stored); } async grant(subject: string): Promise { const stored = await this.ctx.storage.get("record"); if (!stored || stored.kind !== "grant" || stored.expiresAt <= Date.now()) return null; const grant = await this.#open(stored); return grant?.subject === subject ? grant : null; } async selectAccount( subject: string, grantRef: string, accountId: string, ): Promise { return this.ctx.storage.transaction(async (tx) => { const stored = await tx.get("record"); if ( !stored || stored.kind !== "session" || stored.expiresAt <= Date.now() ) return false; const value = await this.#open(stored); if (!value || value.subject !== subject || value.grantRef !== grantRef) return false; value.selectedAccountId = accountId; const sealed = await encrypt( this.#encryptionKey(), value, `${this.ctx.id}:session:${stored.expiresAt}`, ); await tx.put("record", { kind: "session", expiresAt: stored.expiresAt, ...sealed, }); return true; }); } async retireSession(): Promise { return this.ctx.storage.transaction(async (tx) => { const stored = await tx.get("record"); if (!stored || stored.kind !== "session") return null; const principal = await this.#open(stored); await tx.delete("record"); return principal; }); } async destroy() { await this.ctx.storage.deleteAll(); } async alarm() { await this.ctx.storage.deleteAll(); } }