Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
14 kB · 432 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433import { DurableObject } from "cloudflare:workers";import * as Effect from "effect/Effect";import { loadControlPlaneSecrets } from "../configuration/control-plane.ts";import type { Env } from "./config.ts";import { decrypt, encrypt } from "./crypto.ts";import { transaction } from "./storage.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<Env> { #encryptionKey() { return loadControlPlaneSecrets(this.env).credentialEncryptionKey.reveal(); } #open<T>(record: Stored) { return Effect.gen({ self: this }, function* () { const secret = this.#encryptionKey(); return yield* decrypt<T>( secret, record, `${this.ctx.id}:${record.kind}:${record.expiresAt}`, ).pipe( Effect.catchDefect((error) => { if ( error instanceof DOMException && ["OperationError", "DataError", "InvalidCharacterError"].includes( error.name, ) ) return Effect.succeed(null); return Effect.die(error); }), ); }); } #put(record: RecordValue) { return Effect.gen({ self: this }, function* () { const expiresAt = record.value.expiresAt; const sealed = yield* encrypt( this.#encryptionKey(), record.value, `${this.ctx.id}:${record.kind}:${expiresAt}`, ); yield* transaction(this.ctx.storage, (tx) => Effect.gen({ self: this }, function* () { if (yield* Effect.promise(() => tx.get("record"))) return yield* Effect.die(new Error("Vault record already exists")); yield* Effect.promise(() => tx.put("record", { kind: record.kind, expiresAt, ...sealed }), ); yield* Effect.promise(() => tx.setAlarm(expiresAt)); }), ); }); } createContinuation(value: BridgeContinuation) { return Effect.runPromise(this.#put({ kind: "continuation", value })); } createCode(value: BridgeCode) { return Effect.runPromise(this.#put({ kind: "code", value })); } claimContinuation(bindingHash: string): Promise<BridgeContinuation | null> { return Effect.runPromise( transaction(this.ctx.storage, (tx) => Effect.gen({ self: this }, function* () { const stored = yield* Effect.promise(() => tx.get<Stored>("record")); if ( !stored || stored.kind !== "continuation" || stored.expiresAt <= Date.now() ) return null; const value = yield* this.#open<BridgeContinuation>(stored); if (!value || value.bindingHash !== bindingHash) return null; yield* Effect.promise(() => tx.delete("record")); return value; }), ), ); } claimCode(expected: { installationId: string; audience: string; state: string; challenge: string; }): Promise<BridgeCode | null> { return Effect.runPromise( transaction(this.ctx.storage, (tx) => Effect.gen({ self: this }, function* () { const stored = yield* Effect.promise(() => tx.get<Stored>("record")); if ( !stored || stored.kind !== "code" || stored.expiresAt <= Date.now() ) return null; const value = yield* this.#open<BridgeCode>(stored); if ( !value || Object.entries(expected).some( ([key, item]) => value[key as keyof BridgeCode] !== item, ) ) return null; yield* Effect.promise(() => tx.delete("record")); return value; }), ), ); } createOperation(value: ProtectedOperation): Promise<ProtectedOperation> { return Effect.runPromise( Effect.gen({ self: this }, function* () { 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)) ) return yield* Effect.die(new Error("Invalid protected operation")); const grant = yield* Effect.promise(() => this.env.AUTH_VAULT.get( this.env.AUTH_VAULT.idFromName(`grant:${value.grantRef}`), ).grant(value.subject), ); if (!grant || value.expiresAt > grant.expiresAt) return yield* Effect.die( new Error("Protected operation authorization unavailable"), ); const sealed = yield* encrypt( this.#encryptionKey(), value, `${this.ctx.id}:operation:${value.expiresAt}`, ); return yield* transaction(this.ctx.storage, (tx) => Effect.gen({ self: this }, function* () { const existing = yield* Effect.promise(() => tx.get<Stored>("record"), ); if (existing) { if ( existing.kind !== "operation" || existing.expiresAt <= Date.now() ) return yield* Effect.die( new Error("Protected operation unavailable"), ); const previous = yield* this.#open<ProtectedOperation>(existing); if ( !previous || !matchesOperation(previous, value) || previous.grantRef !== value.grantRef || previous.expiresAt !== value.expiresAt ) return yield* Effect.die( new Error("Protected operation conflict"), ); return previous; } yield* Effect.promise(() => tx.put("record", { kind: "operation", expiresAt: value.expiresAt, ...sealed, }), ); yield* Effect.promise(() => tx.setAlarm(value.expiresAt)); return value; }), ); }), ); } operation(expected: OperationBinding): Promise<ProtectedOperation | null> { return Effect.runPromise( Effect.gen({ self: this }, function* () { const stored = yield* Effect.promise(() => this.ctx.storage.get<Stored>("record"), ); if ( !stored || stored.kind !== "operation" || stored.expiresAt <= Date.now() ) return null; const value = yield* this.#open<ProtectedOperation>(stored); return value && matchesOperation(value, expected) ? value : null; }), ); } retireBootstrap(expected: OperationBinding): Promise<boolean> { return Effect.runPromise( transaction(this.ctx.storage, (tx) => Effect.gen({ self: this }, function* () { const stored = yield* Effect.promise(() => tx.get<Stored>("record")); if ( !stored || stored.kind !== "operation" || stored.expiresAt <= Date.now() ) return false; const value = yield* this.#open<ProtectedOperation>(stored); if (!value || !matchesOperation(value, expected)) return false; value.bootstrapSecret = null; const sealed = yield* encrypt( this.#encryptionKey(), value, `${this.ctx.id}:operation:${value.expiresAt}`, ); yield* Effect.promise(() => tx.put("record", { kind: "operation", expiresAt: value.expiresAt, ...sealed, }), ); return true; }), ), ); } createTransaction(value: Transaction) { return Effect.runPromise(this.#put({ kind: "transaction", value })); } createSession(value: Principal) { return Effect.runPromise(this.#put({ kind: "session", value })); } createGrant(value: Grant) { return Effect.runPromise(this.#put({ kind: "grant", value })); } claimTransaction( bindingHash: string, previousSession: string | null, ): Promise<Transaction | null> { return Effect.runPromise( // Read/compare/delete are one native storage transaction. The code exchange // happens only after this commit; failures cannot reopen a claimed state. transaction(this.ctx.storage, (tx) => Effect.gen({ self: this }, function* () { const stored = yield* Effect.promise(() => tx.get<Stored>("record")); if ( !stored || stored.kind !== "transaction" || stored.expiresAt <= Date.now() ) return null; const value = yield* this.#open<Transaction>(stored); if ( !value || value.bindingHash !== bindingHash || value.previousSession !== previousSession ) return null; yield* Effect.promise(() => tx.delete("record")); return value; }), ), ); } session(): Promise<Principal | null> { return Effect.runPromise( Effect.gen({ self: this }, function* () { const stored = yield* Effect.promise(() => this.ctx.storage.get<Stored>("record"), ); if ( !stored || stored.kind !== "session" || stored.expiresAt <= Date.now() ) return null; return yield* this.#open<Principal>(stored); }), ); } grant(subject: string): Promise<Grant | null> { return Effect.runPromise( Effect.gen({ self: this }, function* () { const stored = yield* Effect.promise(() => this.ctx.storage.get<Stored>("record"), ); if ( !stored || stored.kind !== "grant" || stored.expiresAt <= Date.now() ) return null; const grant = yield* this.#open<Grant>(stored); return grant?.subject === subject ? grant : null; }), ); } selectAccount( subject: string, grantRef: string, accountId: string, ): Promise<boolean> { return Effect.runPromise( transaction(this.ctx.storage, (tx) => Effect.gen({ self: this }, function* () { const stored = yield* Effect.promise(() => tx.get<Stored>("record")); if ( !stored || stored.kind !== "session" || stored.expiresAt <= Date.now() ) return false; const value = yield* this.#open<Principal>(stored); if ( !value || value.subject !== subject || value.grantRef !== grantRef ) return false; value.selectedAccountId = accountId; const sealed = yield* encrypt( this.#encryptionKey(), value, `${this.ctx.id}:session:${stored.expiresAt}`, ); yield* Effect.promise(() => tx.put("record", { kind: "session", expiresAt: stored.expiresAt, ...sealed, }), ); return true; }), ), ); } retireSession(): Promise<Principal | null> { return Effect.runPromise( transaction(this.ctx.storage, (tx) => Effect.gen({ self: this }, function* () { const stored = yield* Effect.promise(() => tx.get<Stored>("record")); if (!stored || stored.kind !== "session") return null; const principal = yield* this.#open<Principal>(stored); yield* Effect.promise(() => tx.delete("record")); return principal; }), ), ); } destroy() { return Effect.runPromise( Effect.promise(() => this.ctx.storage.deleteAll()), ); } alarm() { return Effect.runPromise( Effect.promise(() => this.ctx.storage.deleteAll()), ); }}