import { DurableObject } from "cloudflare:workers"; import { BlockMap, CommitData, ReadableBlockstore, Repo, RepoStorage } from "@atproto/repo"; import { Secp256k1Keypair } from "@atproto/crypto"; import { CID } from "@atproto/lex-data"; import * as _getcirrus_oauth_provider0 from "@getcirrus/oauth-provider"; import { AuthCodeData, ClientMetadata, LexiconPermissionSet, OAuthStorage, PARData, TokenData } from "@getcirrus/oauth-provider"; import { Hono } from "hono"; import * as hono_types0 from "hono/types"; //#region src/oauth-storage.d.ts /** * A cached permission-set lookup. `stale` means the entry has passed its * 24h soft-expiry: it is still safe to use but should be refreshed * opportunistically. */ interface CachedPermissionSet { set: LexiconPermissionSet; fetchedAt: number; stale: boolean; } /** * SQLite-backed OAuth storage for Cloudflare Durable Objects. * * Implements the OAuthStorage interface from @getcirrus/oauth-provider, * storing OAuth data in SQLite tables within a Durable Object. */ declare class SqliteOAuthStorage implements OAuthStorage { private sql; constructor(sql: SqlStorage); /** * Initialize the OAuth database schema. Should be called once on DO startup. */ initSchema(): void; private migrateClientTable; private migrateTokensTable; private ensureTokenIndexes; /** * Clean up expired entries. Should be called periodically. */ cleanup(): void; saveAuthCode(code: string, data: AuthCodeData): Promise; getAuthCode(code: string): Promise; deleteAuthCode(code: string): Promise; saveTokens(data: TokenData): Promise; getTokenByAccess(accessToken: string): Promise; getTokenByRefresh(refreshToken: string): Promise; revokeToken(accessToken: string): Promise; revokeAllTokens(sub: string): Promise; saveClient(clientId: string, metadata: ClientMetadata): Promise; getClient(clientId: string): Promise; savePAR(requestUri: string, data: PARData): Promise; getPAR(requestUri: string): Promise; deletePAR(requestUri: string): Promise; checkAndSaveNonce(nonce: string): Promise; /** * Clear all OAuth data (for testing). */ destroy(): void; /** Soft-expiry: serve stale, but try to refresh. */ static readonly PERMISSION_SET_STALE_MS: number; /** Hard-expiry: drop entirely. */ static readonly PERMISSION_SET_EXPIRY_MS: number; savePermissionSet(nsid: string, set: LexiconPermissionSet, now?: number): void; getPermissionSet(nsid: string): CachedPermissionSet | null; /** * Save a WebAuthn challenge for later verification */ saveWebAuthnChallenge(challenge: string): void; /** * Consume a WebAuthn challenge (single-use, deleted after retrieval) * @returns true if challenge was valid and consumed, false if not found or expired */ consumeWebAuthnChallenge(challenge: string): boolean; } //#endregion //#region src/storage.d.ts /** * SQLite-backed repository storage for Cloudflare Durable Objects. * * Implements the RepoStorage interface from @atproto/repo, storing blocks * in a SQLite database within a Durable Object. */ declare class SqliteRepoStorage extends ReadableBlockstore implements RepoStorage { private sql; constructor(sql: SqlStorage); /** * Initialize the database schema. Should be called once on DO startup. * @param initialActive - Whether the account should start in active state (default true) */ initSchema(initialActive?: boolean): void; /** * Get the current root CID of the repository. */ getRoot(): Promise; /** * Get the current revision string. */ getRev(): Promise; /** * Get the raw bytes for a block by CID. */ getBytes(cid: CID): Promise; /** * Check if a block exists. */ has(cid: CID): Promise; /** * Get multiple blocks at once. */ getBlocks(cids: CID[]): Promise<{ blocks: BlockMap; missing: CID[]; }>; /** * Store a single block. */ putBlock(cid: CID, block: Uint8Array, rev: string): Promise; /** * Store multiple blocks at once. */ putMany(blocks: BlockMap, rev: string): Promise; /** * Update the repository root. */ updateRoot(cid: CID, rev: string): Promise; /** * Apply a commit atomically: add new blocks, remove old blocks, update root. */ applyCommit(commit: CommitData): Promise; /** * Get total storage size in bytes. */ sizeInBytes(): Promise; /** * Clear all data (for testing). */ destroy(): Promise; /** * Count the number of blocks stored. */ countBlocks(): Promise; /** * Get user preferences. */ getPreferences(): Promise; /** * Update user preferences. */ putPreferences(preferences: unknown[]): Promise; /** * Get the activation state of the account. */ getActive(): Promise; /** * Set the activation state of the account. */ setActive(active: boolean): Promise; /** * Get the stored email address. */ getEmail(): string | null; /** * Set the email address. */ setEmail(email: string): void; /** * Get all cached collection names. */ getCollections(): string[]; /** * Add a collection name to the cache (no-op if already present). */ addCollection(collection: string): void; /** * Remove a collection name from the cache (no-op if not present). */ removeCollection(collection: string): void; /** * Check if the collections cache has been populated. */ hasCollections(): boolean; /** * Add a blob reference from a record. */ addRecordBlob(recordUri: string, blobCid: string): void; /** * Add multiple blob references from a record. */ addRecordBlobs(recordUri: string, blobCids: string[]): void; /** * Remove all blob references for a record. */ removeRecordBlobs(recordUri: string): void; /** * Track an imported blob. */ trackImportedBlob(cid: string, size: number, mimeType: string): void; /** * Check if a blob has been imported. */ isBlobImported(cid: string): boolean; /** * Count expected blobs (distinct blobs referenced by records). */ countExpectedBlobs(): number; /** * Count imported blobs. */ countImportedBlobs(): number; /** * List blobs that are referenced but not yet imported. */ listMissingBlobs(limit?: number, cursor?: string): { blobs: Array<{ cid: string; recordUri: string; }>; cursor?: string; }; /** * Clear all blob tracking data (for testing). */ clearBlobTracking(): void; /** * Save a passkey credential. */ savePasskey(credentialId: string, publicKey: Uint8Array, counter: number, name?: string): void; /** * Get a passkey by credential ID. */ getPasskey(credentialId: string): { credentialId: string; publicKey: Uint8Array; counter: number; name: string | null; createdAt: string; lastUsedAt: string | null; } | null; /** * List all passkeys. */ listPasskeys(): Array<{ credentialId: string; name: string | null; createdAt: string; lastUsedAt: string | null; }>; /** * Delete a passkey. */ deletePasskey(credentialId: string): boolean; /** * Update passkey counter after successful authentication. */ updatePasskeyCounter(credentialId: string, counter: number): void; /** * Check if any passkeys exist (for conditional UI). */ hasPasskeys(): boolean; /** * Save a registration token with challenge and optional name. */ savePasskeyToken(token: string, challenge: string, expiresAt: number, name?: string): void; /** * Get and consume a registration token. */ consumePasskeyToken(token: string): { challenge: string; name: string | null; } | null; /** * Clean up expired tokens. */ cleanupPasskeyTokens(): void; /** * Save an app password (store bcrypt hash, not plaintext). */ saveAppPassword(name: string, passwordHash: string): void; /** * List all app passwords (names and creation dates only — never return hashes). */ listAppPasswords(): Array<{ name: string; createdAt: string; }>; /** * Delete an app password by name. */ deleteAppPassword(name: string): boolean; /** * Get all app password hashes for verification during login. */ getAppPasswordHashes(): Array<{ name: string; passwordHash: string; }>; } //#endregion //#region src/types.d.ts /** * Data location options for Durable Object placement. * * - "auto": No location constraint (default, recommended) * - "eu": European Union - hard guarantee data never leaves EU * - Location hints (best-effort, not guaranteed): * - "wnam": Western North America * - "enam": Eastern North America * - "sam": South America * - "weur": Western Europe * - "eeur": Eastern Europe * - "apac": Asia-Pacific * - "oc": Oceania * - "afr": Africa * - "me": Middle East * * IMPORTANT: This setting only affects newly-created Durable Objects. * Changing this after initial deployment will NOT migrate existing data. * To relocate data, you must export and re-import to a new PDS. */ type DataLocation = "auto" | "eu" | "wnam" | "enam" | "sam" | "weur" | "eeur" | "apac" | "oc" | "afr" | "me"; /** * Environment bindings required by the PDS worker. * Consumers must provide these bindings in their wrangler config. */ interface PDSEnv { /** The account's DID (e.g., did:web:example.com) */ DID: string; /** The account's handle (e.g., alice.example.com) */ HANDLE: string; /** Public hostname of the PDS */ PDS_HOSTNAME: string; /** Bearer token for write operations */ AUTH_TOKEN: string; /** Private signing key (hex-encoded) */ SIGNING_KEY: string; /** Public signing key (multibase-encoded) */ SIGNING_KEY_PUBLIC: string; /** Secret for signing session JWTs */ JWT_SECRET: string; /** Bcrypt hash of account password */ PASSWORD_HASH: string; /** Durable Object namespace for account storage */ ACCOUNT: DurableObjectNamespace; /** R2 bucket for blob storage (optional) */ BLOBS?: R2Bucket; /** Account email address (optional, used by some clients) */ EMAIL?: string; /** Initial activation state for new accounts (default: true) */ INITIAL_ACTIVE?: string; /** * Data location for Durable Object placement. * * WARNING: DO NOT CHANGE THIS AFTER INITIAL DEPLOYMENT. * This setting only affects newly-created DOs. Changing it will NOT * migrate existing data and may cause issues. * * Options: * - "auto" or unset: No location constraint (default, recommended) * - "eu": European Union - hard guarantee data never leaves EU * - Location hints (best-effort, not guaranteed): * "wnam", "enam", "sam", "weur", "eeur", "apac", "oc", "afr", "me" */ DATA_LOCATION?: DataLocation; } //#endregion //#region src/validation.d.ts type ValidationStatus = "valid" | "unknown"; //#endregion //#region src/account-do.d.ts /** * Account Durable Object - manages a single user's AT Protocol repository. * * This DO provides: * - SQLite-backed block storage for the repository * - AT Protocol Repo instance for repository operations * - Firehose WebSocket connections * - Sequence number management */ declare class AccountDurableObject extends DurableObject { private storage; private oauthStorage; private repo; private keypair; private sequencer; private blobStore; private storageInitialized; private repoInitialized; constructor(ctx: DurableObjectState, env: PDSEnv); /** * Initialize the storage adapter. Called lazily on first storage access. */ private ensureStorageInitialized; /** * Run cleanup on storage to remove expired entries */ private runCleanup; /** * Alarm handler for periodic cleanup * Called by Cloudflare Workers when the alarm fires */ alarm(): Promise; /** * Initialize the Repo instance. Called lazily on first repo access. */ private ensureRepoInitialized; /** * Get the storage adapter for direct access (used by tests and internal operations). */ getStorage(): Promise; /** * Get the OAuth storage adapter for OAuth operations. */ getOAuthStorage(): Promise; /** * Get the Repo instance for repository operations. */ getRepo(): Promise; /** * Ensure the account is active. Throws error if deactivated. */ ensureActive(): Promise; /** * Get the signing keypair for repository operations. */ getKeypair(): Promise; /** * Update the Repo instance after mutations. */ setRepo(repo: Repo): Promise; /** * Drop the in-memory repo so the next access reloads from storage. * Used after a write fails post-applyWrites: Cloudflare rolls back the * SQLite writes, but JS state isn't rolled back, so the cached Repo can * end up ahead of storage. That mismatch produces firehose events whose * `since` rev the relay never saw, causing it to mark us desynced. */ private invalidateRepoCache; /** * RPC method: Get repo metadata for describeRepo */ rpcDescribeRepo(): Promise<{ did: string; collections: string[]; cid: string; }>; /** * RPC method: Get a single record */ rpcGetRecord(collection: string, rkey: string): Promise<{ cid: string; record: Rpc.Serializable; } | null>; /** * RPC method: List records in a collection */ rpcListRecords(collection: string, opts: { limit: number; cursor?: string; reverse?: boolean; }): Promise<{ records: Array<{ uri: string; cid: string; value: unknown; }>; cursor?: string; }>; /** * RPC method: Create a record */ rpcCreateRecord(collection: string, rkey: string | undefined, record: unknown, validationStatus?: ValidationStatus): Promise<{ uri: string; cid: string; commit: { cid: string; rev: string; }; validationStatus?: ValidationStatus; }>; /** * RPC method: Delete a record */ rpcDeleteRecord(collection: string, rkey: string): Promise<{ commit: { cid: string; rev: string; }; } | null>; /** * RPC method: Put a record (create or update) */ rpcPutRecord(collection: string, rkey: string, record: unknown, validationStatus?: ValidationStatus): Promise<{ uri: string; cid: string; commit: { cid: string; rev: string; }; validationStatus?: ValidationStatus; }>; /** * RPC method: Apply multiple writes (batch create/update/delete) */ rpcApplyWrites(writes: Array<{ $type: string; collection: string; rkey?: string; value?: unknown; validationStatus?: ValidationStatus; }>): Promise<{ commit: { cid: string; rev: string; }; results: Array<{ $type: string; uri?: string; cid?: string; validationStatus?: ValidationStatus; }>; }>; /** * RPC method: Get repo status */ rpcGetRepoStatus(): Promise<{ did: string; head: string; rev: string; }>; /** * Handle streaming getRepo via fetch (not RPC, to enable streaming response). */ private handleGetRepo; /** * RPC method: Get specific blocks by CID as CAR file * Used for partial sync and migration. */ rpcGetBlocks(cids: string[]): Promise; /** * RPC method: Get record with proof as CAR file. * Returns the commit block and all MST blocks needed to verify * the existence (or non-existence) of a record. * Used by com.atproto.sync.getRecord for record verification. */ rpcGetRecordProof(collection: string, rkey: string): Promise; /** * RPC method: Import repo from CAR file * This is used for account migration - importing an existing repository * from another PDS. */ rpcImportRepo(carBytes: Uint8Array): Promise<{ did: string; rev: string; cid: string; }>; /** * RPC method: Record an already-stored blob's metadata. * * The blob bytes are written to R2 by the stateless Worker, not here. * This DO is single-threaded and also holds the relay's firehose * WebSocket; awaiting an R2 put inside it (R2 latency is independent of * object size — even a small image can stall) pins the input gate, and * Cloudflare resets the object when a storage op can't complete in time, * dropping the firehose and desyncing the relay. Only the tiny tracking * row needs the DO's SQLite. */ rpcTrackBlob(cid: string, size: number, mimeType: string): Promise; /** * RPC method: Get a blob from R2 */ rpcGetBlob(cidStr: string): Promise; /** * Encode a firehose frame (header + body CBOR). */ private encodeFrame; /** * Encode any event frame based on its type. */ private encodeEventFrame; /** * Encode an error frame. */ private encodeErrorFrame; /** * Encode an #info message (op:1, t:'#info'). Used for non-fatal * conditions like OutdatedCursor where the stream continues. */ private encodeInfoFrame; /** * Backfill firehose events from a cursor. */ private backfillFirehose; /** * Broadcast a sequenced event to all connected firehose clients. */ private broadcastEvent; /** * Handle WebSocket upgrade for firehose (subscribeRepos). */ handleFirehoseUpgrade(request: Request): Promise; /** * WebSocket message handler (hibernation API). */ webSocketMessage(_ws: WebSocket, _message: string | ArrayBuffer): void; /** * WebSocket close handler (hibernation API). */ webSocketClose(_ws: WebSocket, _code: number, _reason: string, _wasClean: boolean): void; /** * WebSocket error handler (hibernation API). */ webSocketError(_ws: WebSocket, error: Error): void; /** * RPC method: Get user preferences */ rpcGetPreferences(): Promise<{ preferences: unknown[]; }>; /** * RPC method: Put user preferences */ rpcPutPreferences(preferences: unknown[]): Promise; /** * RPC method: Get stored email */ rpcGetEmail(): Promise<{ email: string | null; }>; /** * RPC method: Update stored email */ rpcUpdateEmail(email: string): Promise; /** * RPC method: Get account activation state */ rpcGetActive(): Promise; /** * RPC method: Activate account. * Emits #account + #identity + #sync per sync 1.1, so relays pick up * the new state without polling. #sync is only emitted when a repo * root exists (i.e. after migration import or initial commit). */ rpcActivateAccount(): Promise; /** * RPC method: Deactivate account. * Emits #account(active=false, status='deactivated') per sync 1.1. */ rpcDeactivateAccount(): Promise; /** * RPC method: Count blocks in storage */ rpcCountBlocks(): Promise; /** * RPC method: Count records in repository */ rpcCountRecords(): Promise; /** * RPC method: Count expected blobs (referenced in records) */ rpcCountExpectedBlobs(): Promise; /** * RPC method: Count imported blobs */ rpcCountImportedBlobs(): Promise; /** * RPC method: List missing blobs (referenced but not imported) */ rpcListMissingBlobs(limit?: number, cursor?: string): Promise<{ blobs: Array<{ cid: string; recordUri: string; }>; cursor?: string; }>; /** * RPC method: Reset migration state. * Clears imported repo and blob tracking to allow re-import. * Only works when account is deactivated. */ rpcResetMigration(): Promise<{ blocksDeleted: number; blobsCleared: number; }>; /** * Emit an identity event to notify downstream services to refresh identity cache. * `handle` is optional per sync 1.1. */ rpcEmitIdentityEvent(handle?: string): Promise<{ seq: number; }>; /** * RPC method: Health check - verifies storage is accessible */ rpcHealthCheck(): Promise<{ ok: true; }>; /** * RPC method: Firehose status - returns subscriber count and latest sequence */ rpcGetFirehoseStatus(): Promise<{ subscribers: Array<{ connectedAt: number; cursor: number; ip: string | null; }>; latestSeq: number | null; }>; /** Save an authorization code */ rpcSaveAuthCode(code: string, data: _getcirrus_oauth_provider0.AuthCodeData): Promise; /** Get authorization code data */ rpcGetAuthCode(code: string): Promise<_getcirrus_oauth_provider0.AuthCodeData | null>; /** Delete an authorization code */ rpcDeleteAuthCode(code: string): Promise; /** Save token data */ rpcSaveTokens(data: _getcirrus_oauth_provider0.TokenData): Promise; /** Get token data by access token */ rpcGetTokenByAccess(accessToken: string): Promise<_getcirrus_oauth_provider0.TokenData | null>; /** Get token data by refresh token */ rpcGetTokenByRefresh(refreshToken: string): Promise<_getcirrus_oauth_provider0.TokenData | null>; /** Revoke a token */ rpcRevokeToken(accessToken: string): Promise; /** Revoke all tokens for a user */ rpcRevokeAllTokens(sub: string): Promise; /** Save client metadata */ rpcSaveClient(clientId: string, metadata: _getcirrus_oauth_provider0.ClientMetadata): Promise; /** Get client metadata */ rpcGetClient(clientId: string): Promise<_getcirrus_oauth_provider0.ClientMetadata | null>; /** Save PAR data */ rpcSavePAR(requestUri: string, data: _getcirrus_oauth_provider0.PARData): Promise; /** Get PAR data */ rpcGetPAR(requestUri: string): Promise<_getcirrus_oauth_provider0.PARData | null>; /** Delete PAR data */ rpcDeletePAR(requestUri: string): Promise; /** Check and save DPoP nonce */ rpcCheckAndSaveNonce(nonce: string): Promise; /** * Look up a cached permission-set lexicon by NSID. Returns the cached * value (with `stale: true` when past its 24h soft-expiry) or null when * not cached or hard-expired. */ rpcGetPermissionSet(nsid: string): Promise; /** Cache a fetched permission-set lexicon. */ rpcSavePermissionSet(nsid: string, set: _getcirrus_oauth_provider0.LexiconPermissionSet): Promise; /** Save a passkey credential */ rpcSavePasskey(credentialId: string, publicKey: Uint8Array, counter: number, name?: string): Promise; /** Get a passkey by credential ID */ rpcGetPasskey(credentialId: string): Promise<{ credentialId: string; publicKey: Uint8Array; counter: number; name: string | null; createdAt: string; lastUsedAt: string | null; } | null>; /** List all passkeys */ rpcListPasskeys(): Promise>; /** Delete a passkey */ rpcDeletePasskey(credentialId: string): Promise; /** Update passkey counter after authentication */ rpcUpdatePasskeyCounter(credentialId: string, counter: number): Promise; /** Check if passkeys exist */ rpcHasPasskeys(): Promise; /** Save a registration token */ rpcSavePasskeyToken(token: string, challenge: string, expiresAt: number, name?: string): Promise; /** Consume a registration token */ rpcConsumePasskeyToken(token: string): Promise<{ challenge: string; name: string | null; } | null>; /** Save a WebAuthn challenge for passkey authentication */ rpcSaveWebAuthnChallenge(challenge: string): Promise; /** Consume a WebAuthn challenge (single-use) */ rpcConsumeWebAuthnChallenge(challenge: string): Promise; /** Save an app password (bcrypt hash) */ rpcSaveAppPassword(name: string, passwordHash: string): Promise; /** List all app passwords (names and dates only) */ rpcListAppPasswords(): Promise>; /** Delete an app password by name */ rpcDeleteAppPassword(name: string): Promise; /** Get all app password hashes for login verification */ rpcGetAppPasswordHashes(): Promise>; /** * HTTP fetch handler for WebSocket upgrades and streaming responses. * Used instead of RPC when the response can't be serialized (WebSocket) * or when streaming is needed to avoid buffering large payloads (getRepo). */ fetch(request: Request): Promise; } //#endregion //#region src/index.d.ts declare const app: Hono<{ Bindings: PDSEnv; }, hono_types0.BlankSchema, "/">; //#endregion export { AccountDurableObject, type DataLocation, type PDSEnv, app as default }; //# sourceMappingURL=index.d.ts.map