// @pdsjs/core/ports - Port interfaces for hexagonal architecture /** * @typedef {Object} RecordResult * @property {string} uri * @property {string} cid * @property {Uint8Array|ArrayBuffer} value */ /** * @typedef {Object} EventResult * @property {number} seq * @property {string} commit_cid * @property {Uint8Array} evt */ /** * @typedef {Object} ActorRecord * @property {string} did * @property {string|null} handle * @property {number} createdAt */ /** * OAuth request data stored during authorization flow. * Shape varies depending on flow stage (PAR, authorization, code exchange). * @typedef {Object} OAuthRequestData * @property {string} [clientId] - OAuth client ID * @property {string|null} [redirectUri] - Redirect URI * @property {string|null} [scope] - OAuth scope * @property {string|null} [codeChallenge] - PKCE code challenge * @property {string|null} [codeChallengeMethod] - PKCE method (S256) * @property {string|null} [dpopJkt] - DPoP key thumbprint * @property {string|null} [state] - OAuth state parameter * @property {string|null} [responseMode] - Response mode (query/fragment) * @property {string|null} [loginHint] - Login hint * @property {string} [code] - Authorization code (after approval) * @property {string} [did] - User DID (after approval) * @property {string} [requestUri] - For code mapping lookup * @property {Object} [clientMetadata] - Client metadata */ /** * OAuth token data for refresh tokens. * @typedef {Object} OAuthTokenData * @property {string} did - User DID * @property {string} [clientId] - OAuth client ID * @property {string} [scope] - OAuth scope * @property {string} [dpopJkt] - DPoP key thumbprint * @property {number} [createdAt] - Token creation timestamp * @property {number} [updatedAt] - Last time the session was touched: its * authorization or its most recent refresh. Absent on tokens written before * this field existed, where createdAt stands in. * @property {string|null} [userAgent] - Device that opened the session * @property {string} [tokenId] - Set by listOAuthTokensByDid only: the storage * key, so a listed session can be revoked without a second lookup. This is * the refresh token itself — never put it in a response. * @property {string} [rotatedTo] - On a refresh token just used: the one that * replaced it, which it answers with until that one is used in turn. A * record with this is spent, not a session of its own. * @property {string} [replaced] - The spent token this one replaced, dropped * at the next refresh so a session keeps one of them at a time. */ /** * The account's email address and its verification state. A pending change is * held here until the code sent to the new address comes back, so an address * only becomes the account's once it has been proven reachable. * @typedef {Object} EmailState * @property {string|null} address - The confirmed address, if any * @property {boolean} verified - Whether `address` completed a code exchange * @property {EmailPending|null} [pending] - An address awaiting its code */ /** * @typedef {Object} EmailPending * @property {string} address - The address a code was sent to * @property {string} codeHash - HMAC of the code; the code itself is not stored * @property {number} expiresAt - Epoch ms after which the code is dead * @property {number} [sentAt] - Epoch ms the code was last sent, for resend cooldown */ /** * An account password the user set through the account pages, stored as a * hash. The seed hash names the configured password (PDS_PASSWORD) this * change was made under: a configured password that no longer matches the * seed is an operator reset and wins over the stored hash. * @typedef {Object} AccountPasswordState * @property {string} hash - PBKDF2 hash of the password, keyed on the DID * @property {string|null} seedHash - Hash of the configured password at change time, null when none was set * @property {string} changedAt - ISO 8601 datetime */ /** * A confirmation code mailed before a PLC operation is signed. Only its HMAC * is kept, so the stored value cannot authorize anything by itself. * @typedef {Object} PlcToken * @property {string} codeHash - HMAC of the code * @property {number} expiresAt - Epoch ms after which the code is dead */ /** * Sends mail on the server's behalf. Optional: with no emailer configured a * PDS still stores an address, it just cannot prove anyone can read it. * * @typedef {Object} EmailPort * @property {(message: {to: string, subject: string, text: string, html?: string}) => Promise} send */ /** * A stored app password. The password itself is never returned — only its * hash is kept, so listing them cannot leak credentials. * @typedef {Object} AppPasswordRecord * @property {string} name - User-supplied label, unique per account * @property {boolean} privileged - Granted access to sensitive account state * @property {string} createdAt - ISO 8601 datetime */ /** * @typedef {Object} SharedStoragePort * * // Actor/handle registry * @property {(did: string) => Promise} getActor * @property {(handle: string) => Promise} resolveHandle - Returns DID or null * @property {(did: string, handle: string|null, createdAt: number) => Promise} putActor * @property {(did: string) => Promise} deleteActor * * // OAuth requests * @property {(requestUri: string) => Promise} getOAuthRequest * @property {(requestUri: string, data: OAuthRequestData, expiresAt: number) => Promise} putOAuthRequest * @property {(requestUri: string) => Promise} deleteOAuthRequest * * // OAuth tokens * @property {(tokenId: string) => Promise} getOAuthToken * @property {(tokenId: string, data: OAuthTokenData) => Promise} putOAuthToken * @property {(tokenId: string) => Promise} deleteOAuthToken * @property {(did: string) => Promise} listOAuthTokensByDid * * // DPoP jti tracking * @property {(jti: string, expiresAt: number) => Promise} checkAndStoreDpopJti * @property {() => Promise} cleanupExpiredDpopJtis * * // App passwords * @property {(did: string, name: string, hash: string, privileged: boolean, createdAt: string) => Promise} putAppPassword - False if the name is already taken * @property {(did: string, hash: string) => Promise} getAppPasswordByHash * @property {(did: string) => Promise} listAppPasswords * @property {(did: string, name: string) => Promise} deleteAppPassword - False if no such name * * // Sessions opened by password login, keyed by the refresh token's jti * @property {(session: SessionRecord) => Promise} putSession * @property {(jti: string) => Promise} getSession * @property {(did: string) => Promise} listSessions * @property {(jti: string) => Promise} deleteSession - False if no such session * @property {(jti: string, expiresAt: number, nextJti: string) => Promise} [spendSession] - * Mark a session's token spent by a rotation, shortening its expiry and * naming its successor — but only if no other rotation has already named a * different one. False means this caller lost that race and must defer to * the successor already recorded. A store that cannot do this atomically may * leave it out, at the cost of two simultaneous refreshes forking a session. * * // Passkeys * @property {(record: PasskeyRecord) => Promise} putPasskey - False if the credential is already registered * @property {(did: string) => Promise} listPasskeys * @property {(credentialId: string) => Promise} getPasskey - Looked up without a DID: a sign-in knows only the credential * @property {(did: string, credentialId: string) => Promise} deletePasskey - False if no such credential * @property {(credentialId: string, signCount: number, lastUsedAt: string) => Promise} touchPasskey */ /** * One login opened with the account password or an app password. The tokens * themselves are stateless JWTs; this row is what makes a session listable * and revocable. * @typedef {Object} SessionRecord * @property {string} jti - The current refresh token's id * @property {string} did * @property {string} label - What opened it: the account password, or an app password's name * @property {string} scope - The session's access level * @property {string|null} userAgent - Client's User-Agent at sign-in, truncated * @property {string} createdAt - ISO 8601 datetime * @property {string|null} refreshedAt - ISO 8601 datetime of the last rotation * @property {number} [expiresAt] - The refresh token's expiry (epoch seconds). * A store that can expire rows should honor it; absent on legacy rows. * @property {string|null} [nextJti] - Set once this row's token has been spent * by a rotation: the id its replacement was minted under. The row is kept, * with its expiry shortened to the grace period, so that a client replaying * the spent token is handed the same replacement rather than signed out. A * row carrying this is no longer a login in its own right. */ /** * A registered passkey. The public key is stored as base64url SPKI, the form * WebCrypto imports directly. * @typedef {Object} PasskeyRecord * @property {string} credentialId - base64url of the credential's raw id * @property {string} did - The account the credential signs in as * @property {string} name - User-supplied label * @property {string} publicKey - base64url SPKI * @property {number} algorithm - COSE algorithm identifier * @property {number} signCount - Last counter the authenticator reported * @property {string} createdAt - ISO 8601 datetime * @property {string|null} lastUsedAt - ISO 8601 datetime, or null if unused */ /** * Preferences the account app keeps server-side, so they follow the account * across browsers and devices. * @typedef {Object} AccountUiState * @property {Record} appAliases - Apps rows folded together: * each key domain's records display under the value domain's row * @property {string[]} dismissedAppPairs - Combine offers turned down, each * as "from>to", so the Apps screen stops suggesting that pair * @property {'system'|'light'|'dark'} theme - The console's color palette: * follow the device's preference, or pin light or dark * @property {'stable'|'canary'} releaseChannel - Which published releases the * update check follows: tagged releases, or canary builds of the main branch */ /** * @typedef {Object} BlobDetails * @property {string} cid * @property {string} mimeType * @property {number} size * @property {number} createdAt - Epoch milliseconds the blob was stored * @property {number} time - Epoch ms the content entered the account: the * earliest referencing record's stored `createdAt`, else that record's TID * time, else `createdAt`. On a migrated repo this is the original post * time, not the migration date. * @property {boolean} timeFromRecord - Whether `time` came from a referencing * record; false means it is only the blob's arrival time here. * @property {string[]} records - AT-URIs of the records that reference it */ /** * The broad media kind a mime type falls under: its type prefix, or `other` * for anything outside the named four. * @typedef {'image'|'video'|'audio'|'text'|'other'} BlobKind */ /** * How a blob listing is narrowed and ordered. Filters compose (AND). * @typedef {Object} BlobListOptions * @property {BlobKind} [kind] - Only blobs of this media kind * @property {string} [appAuthority] - Only blobs referenced by a record whose * collection NSID starts with this authority (e.g. `app.bsky`) * @property {boolean} [unused] - Only blobs no record references * @property {'newest'|'largest'} [sort] - Order; `newest` when omitted */ /** * @typedef {Object} BlobBucket * @property {number} count * @property {number} bytes */ /** * Aggregate counts over all blobs, for filter chips and the storage bar. * @typedef {Object} BlobFacets * @property {BlobBucket} total * @property {BlobBucket} unused - Blobs no record references * @property {(BlobBucket & {kind: BlobKind})[]} kinds - Only kinds present * @property {(BlobBucket & {authority: string})[]} apps - Per collection-NSID * authority (e.g. `app.bsky`); a blob used by two apps counts once in each */ /** * @typedef {Object} ActorStoragePort * @property {(uri: string) => Promise<{cid: string, value: Uint8Array}|null>} getRecord * @property {(collection: string, cursor: string|null, limit: number, reverse?: boolean) => Promise<{records: RecordResult[], cursor: string|null}>} listRecords - `reverse` lists newest rkey first * @property {() => Promise>} listAllRecords - List all records for MST building * @property {(uri: string, cid: string, collection: string, rkey: string, value: Uint8Array) => Promise} putRecord * @property {(uri: string) => Promise} deleteRecord * @property {(cid: string) => Promise} getBlock * @property {(cid: string, data: Uint8Array) => Promise} putBlock * @property {(cids: string[]) => Promise} [deleteBlocks] * Optional, with the three below: a store that has them drops the blocks * each commit leaves unreachable, and getRepo reads its block table rather * than walking the tree. Absent, every block is kept. * @property {(cursor: string|null, limit: number) => Promise<{blocks: Array<{cid: string, data: Uint8Array}>, cursor: string|null}>} [listBlocks] * Every stored block, in CID order, `limit` at a time after `cursor`. * @property {(cids: string[]) => Promise} [recordCidsAmong] * Those of `cids` some record still has as its CID. * @property {() => Promise} [getBlocksCollected] * Whether the block table holds only what the current commit reaches: * false until the first sweep, and again after the tree is rebuilt or a * repo imported. * @property {(collected: boolean) => Promise} [setBlocksCollected] * @property {() => Promise<{seq: number, cid: string, rev: string}|null>} getLatestCommit * @property {(seq: number, cid: string, rev: string, prev: string|null) => Promise} putCommit * @property {(cursor: number|null, limit: number) => Promise<{events: EventResult[], cursor: number|null}>} getEvents * @property {(before: number, limit: number) => Promise<{seq: number, evt: Uint8Array}[]>} [getEventsBefore] * @property {(seq: number, did: string, commitCid: string, evt: Uint8Array) => Promise} putEvent * @property {() => Promise} getLatestSeq * @property {(cid: string) => Promise<{mimeType: string, size: number, createdAt: number}|null>} getBlob * @property {(cid: string, mimeType: string, size: number) => Promise} putBlob * @property {(cid: string) => Promise} deleteBlob * @property {(cursor: string|null, limit: number) => Promise<{cids: string[], cursor: string|null}>} listBlobs * @property {(cursor: string|null, limit: number, options?: BlobListOptions) => Promise<{blobs: BlobDetails[], cursor: string|null}>} [listBlobDetails] * Optional: blobs with their metadata and referencing records, filtered and * ordered per the options. Absent, the account console has no Files page. * @property {() => Promise} [getBlobFacets] * Optional: aggregate blob counts by kind, app, and usage. * @property {(olderThan: number) => Promise} getOrphanedBlobs * @property {(blobCid: string, recordUri: string, recordTime?: number|null) => Promise} linkBlobToRecord * `recordTime` is the referencing record's own timestamp (epoch ms) when it * carries one; it dates the blob by the record's clock rather than arrival. * @property {(recordUri: string) => Promise} unlinkBlobsFromRecord * @property {(blobCid: string) => Promise} listBlobRecordUris * Every record URI the link table holds for a blob. The public blob * endpoints read these to refuse a blob only space records reference. * @property {() => Promise} getDid * @property {(did: string) => Promise} setDid * @property {() => Promise} getPrivateKey * @property {(key: Uint8Array) => Promise} setPrivateKey * @property {() => Promise} getKemKey - The account's `#atproto_kem` key, a 32-byte P-256 scalar * @property {(key: Uint8Array) => Promise} setKemKey * @property {() => Promise} getHandle * @property {() => Promise} getEmail * @property {(state: EmailState|null) => Promise} setEmail * @property {() => Promise} [getAccountPassword] - The stored account password. A store without these two methods pins the password to the deployment's configuration. * @property {(state: AccountPasswordState|null) => Promise} [setAccountPassword] * @property {() => Promise} getPlcToken * @property {(token: PlcToken|null) => Promise} setPlcToken * @property {() => Promise} getAccountStatus * @property {(status: string) => Promise} setAccountStatus * @property {(handle: string) => Promise} setHandle * @property {() => Promise} getPreferences * @property {(preferences: Object[]) => Promise} setPreferences * @property {() => Promise} [getBackupState] * Optional: backup settings and run history, one JSON document. Absent, the * backup endpoints report backups as unavailable. * @property {(state: import('./backup.js').BackupState) => Promise} [setBackupState] * @property {() => Promise} [getAccountUiState] * Optional: the account app's own preferences, one JSON document. Absent, * the Apps screen never offers to combine rows. * @property {(state: AccountUiState) => Promise} [setAccountUiState] */ /** * One streamed blob write. The CID arrives only at commit, after the last * chunk: content addressing needs the whole hash. An adapter therefore stages * bytes somewhere temporary and files them under the CID at commit. After * commit or abort the writer takes no further calls. * * @typedef {Object} BlobWriter * @property {(chunk: Uint8Array) => Promise} write * @property {(cid: string, mimeType: string) => Promise} commit * @property {() => Promise} abort */ /** * @typedef {Object} BlobPort * @property {(did: string, cid: string) => Promise<{data: Uint8Array, mimeType: string}|null>} get * @property {(did: string, cid: string, data: Uint8Array, mimeType: string) => Promise} put * @property {(did: string, cid: string) => Promise} delete * @property {(did: string) => Promise} [createWriter] * Optional: write one blob without holding it in memory. Absent, core * buffers the upload and calls `put`, bounded by the upload limit. * @property {(did: string, cid: string, range?: {start: number, end: number}) => Promise<{body: ReadableStream, mimeType: string}|null>} [getStream] * Optional: read one blob, or an inclusive byte range of it, without * holding it in memory. The caller clamps the range to the blob's size. * Absent, core buffers via `get` and slices. */ /** * An incremental hash over one byte stream. `digest` is final: further * updates throw. * * @typedef {Object} Hasher * @property {(chunk: Uint8Array) => void|Promise} update * @property {() => Uint8Array|Promise} digest */ /** * Makes one SHA-256 Hasher per call. Platform packages inject a native * implementation (node:crypto, the Workers DigestStream); the core default is * plain JavaScript and slower. * * @typedef {() => Hasher} HasherFactory */ /** * Where backups land: any flat key/value object store — a directory, an S3 * bucket, an R2 bucket. Keys are `/`-separated paths chosen by core * (`snapshots//repo.car`, `blobs/`); adapters treat them as * opaque strings. `list` returns every key under a prefix — a single-account * PDS holds at most a few thousand, so no cursor. * * @typedef {Object} BackupTargetPort * @property {(key: string, data: Uint8Array, contentType?: string) => Promise} put * @property {(key: string, body: ReadableStream, contentType?: string) => Promise} [putStream] * Optional: write one object without holding it in memory, and without a * size limit the target sets on a single request. Absent, core reads the * blob whole and calls `put`. * @property {(key: string) => Promise} get * @property {(prefix: string) => Promise} list * @property {(key: string) => Promise} delete */ /** * @typedef {Object} WebSocketSender * @property {(data: Uint8Array) => void} send - Send binary data to client * @property {() => void} close - Close the connection */ /** * @typedef {Object} WebSocketPort * @property {(request: Request) => boolean} isUpgrade - Check if request is WebSocket upgrade * @property {(request: Request, onConnect: (sender: WebSocketSender) => Promise) => Response} upgrade - Upgrade connection and call onConnect with sender * @property {(data: Uint8Array) => void} [broadcast] - Broadcast data to all connected clients */ /** * @typedef {'valid' | 'unknown'} ValidationStatus */ /** * Lexicon resolver port for validating records against schemas. * Implementations may resolve schemas dynamically (DNS + fetch) or use static schemas. * * @typedef {Object} LexiconResolverPort * @property {(collection: string, record: object, opts?: {strict?: boolean}) => Promise} assertValid * Resolves schema if needed, validates record. * Returns 'valid' on success, 'unknown' if the record could not be held to a * schema this PDS knows. Throws Error with descriptive message if the record * is invalid against a known schema. * `strict` reflects the caller's `validate: true`: require validation to * happen, and enforce schemas resolved from the network too. * @property {(nsid: string) => Promise} [isPublished] * Optional. Whether the NSID's lexicon is published (resolvable) via the protocol. * @property {(nsid: string) => Promise} [getLexicon] * Optional. The resolved lexicon document for an NSID, or null if unresolvable. */ /** * Verifies signatures against did:key identifiers. P-256 is always supported; * secp256k1 support depends on an injected implementation. * * @typedef {Object} SignatureVerifierPort * @property {(didKey: string, data: Uint8Array, sig: Uint8Array) => Promise} verify */ /** * Signs with secp256k1 account keys — the curve the reference PDS gives every * account, and the only one some third-party services (Go feed generators, * labelers) can verify. WebCrypto cannot sign secp256k1, so the * implementation is injected like the verifier above; without one, this PDS * signs with P-256 only. `sign` must return a low-S compact 64-byte (r || s) * signature over the SHA-256 of `data`. * * @typedef {Object} Secp256k1SignerPort * @property {() => Uint8Array} generatePrivateKey - New random 32-byte scalar * @property {(privateKey: Uint8Array, compressed?: boolean) => Uint8Array} getPublicKey * Public key for a scalar: 33-byte compressed (default) or 65-byte uncompressed. * @property {(privateKey: Uint8Array, data: Uint8Array) => Promise} sign */ /** * Resolves a DID to its DID document, or null when it cannot. Injected rather * than imported so that @pdsjs/spaces stays dependency-free. * * `forceRefresh` asks an implementation that holds its answers to read past * the one it holds. A caller verifying a signature passes it after the * signature fails: a held document can name a key the issuer rotated away * from. An implementation that never holds an answer ignores it. * * @typedef {(did: string, opts?: {forceRefresh?: boolean}) => Promise} DidResolverPort */ /** * Read-only view of this account's spaces, for the account page. * * Injected the same way the space route table is: core never imports * @pdsjs/spaces, so a deployment with permissioned data off has no spaces * section and core has no idea the package exists. * * @typedef {Object} SpaceBrowserPort * @property {() => Promise} listSpaces * @property {(uri: string) => Promise>} countRecords * @property {(uri: string) => Promise} countMembers * @property {(uri: string) => Promise} listMembers * @property {(uri: string, collection: string, limit: number) => Promise<{records: Array<{rkey: string, cid: string, indexedAt: string, value: unknown}>, truncated: boolean}>} listRecords * @property {() => Promise>} listAllRecords */ /** * A veto over record writes in the named collections, consulted before the * write lands. Built by a feature package (branch protection comes from * @pdsjs/git) and injected by the platform; core knows only the shape. A * check that throws refuses the write with its message, and the error's * `code` names the XRPC error. * @typedef {Object} RecordWriteGuardPort * @property {string[]} collections * @property {(write: {space: string|null, collection: string, rkey: string, prevValue: unknown|null, nextValue: unknown|null}) => Promise} check */ /** * Space management for the account page, built by a platform package from * @pdsjs/spaces; core never imports that package. The account page runs * behind the owner's session, so these carry no scope checks of their own. * @typedef {Object} SpaceAdminPort * @property {(opts: {type: string, skey?: string}) => Promise<{uri: string}>} createSpace * @property {(space: string, did: string, access: SpaceMemberAccess) => Promise} putMember * @property {(space: string, did: string) => Promise} removeMember * @property {(space: string, collection: string, rkey: string, record: Object) => Promise} createRecord * Write a record into an owned space, refusing an rkey that exists. The * account page uses this to create a repository before its first push. * @property {(space: string, collection: string, rkey: string, record: Object) => Promise} putRecord * Write a record into an owned space, creating or replacing. The account * page uses this for repository settings. * @property {(space: string, collection: string, rkey: string) => Promise} getRecord * A record from an owned space, decoded raw rather than display-formatted, * so a caller can rewrite it without corrupting blob references. * @property {(space: string, collection: string, rkey: string) => Promise} deleteRecord * Remove a record from an owned space, refusing an rkey that is not there. * The blobs it held are freed by the orphan sweep, which reads the space * records for what they reference. */ /** * One applied commit, as handed to the commit tap. * * @typedef {Object} CommitTapEvent * @property {string} did * @property {number} seq * @property {string} rev * @property {Array<{action: string, uri: string, cid: string|null}>} ops */ /** * Called after each successful commit. Feature packages maintain derived * state from this tap (materialized queries; eventually rules). Core logs * and discards any error, so a subscriber can never fail a write. * * @typedef {(evt: CommitTapEvent) => Promise|void} CommitTapPort */ /** * What core hands an extension factory. Every function late-binds into the * server, so a factory may run before the server's handlers exist and an * extension may call back into the server at request time. * * @typedef {Object} ExtensionHost * @property {ActorStoragePort} actorStorage - The account's storage behind the extension's collection scope: a record read or write naming a collection outside `PdsExtension.collections` is refused. Blocks, blobs, the event log and the account's own fields pass through. * @property {BlobPort} blobs - Blob storage, unscoped: a blob belongs to the account, not to a collection * @property {SpaceStoragePort} [spaceStorage] - Permissioned space storage behind the same scope, present when spaces are on. An extension whose records may be held in a space reads them here. * @property {SpaceBrowserPort} [spaceBrowser] - Read-only view of the account's spaces behind the same scope, built by core over spaceStorage when spaces are on * @property {DidResolverPort} [didResolver] - Resolves a DID to its document, when the platform injected one * @property {SignatureVerifierPort} [verifier] - Verifies a signature against a did:key, when the platform injected one * @property {() => Promise<{curve: 'p256'|'secp256k1', sign: (bytes: Uint8Array) => Promise}>} getSigner - Signs with the account's own signing key, for an extension that signs commits of its own. Throws when the server has no key. * @property {() => Promise<{privateKey: Uint8Array, didKey: string}|null>} getKemKey - The account's `#atproto_kem` key: its 32-byte P-256 scalar and did:key. Null while the account has none. * @property {RecordWriteGuardPort} writeGuard - The server's composed write guard, the platform's and every extension's, read at write time. An extension that applies record writes of its own consults it. * @property {string} [hostname] * @property {string} plcUrl - The PLC directory this server resolves DIDs against * @property {number} blobUploadLimit - Largest blob this server accepts, in bytes. An extension that splits a file across blobs sizes its parts to it. * @property {boolean} readOnly * @property {HasherFactory} [hasher] - The platform's SHA-256 factory; absent, an extension hashes in plain JavaScript * @property {() => Promise} getDid * @property {(request: Request) => Promise} xrpc - The server's own fetch, for self-addressed XRPC calls. The call is anonymous: any Authorization, Cookie or DPoP header on the request is removed first. * @property {(did: string, olderThan: number) => Promise} reapOrphanedBlobs - Delete blobs no record references, created before `olderThan` (epoch ms). An extension that deletes a record calls it to free the bytes at once rather than at the hourly sweep. * @property {(did: string, ops: Array<{action: string, uri: string, cid?: string}>) => Promise<{cid: string, rev: string}>} createCommit - Commits record ops to the public repo. Every op must name a collection in `PdsExtension.collections`; one that does not rejects the whole commit before it starts. * @property {(request: Request, url: URL, handler: (did: string, body: Record) => Promise, options?: {write?: boolean, raw?: boolean}) => Promise} accountApi * Answers a JSON request behind the owner's account session, the way the * /account/api routes do: 401 without a session, 403 for a write on a * read-only server, a thrown error as a 400. * @property {(request: Request) => Promise} readAccountSession - The DID behind the request's account-page session cookie, or null. For a route that answers in its own shape and takes the owner's session alongside a bearer token. * @property {(request: Request) => Promise} readJsonBounded - The request's JSON body, or a 413 Response when the body exceeds the server's JSON limit. * @property {(at: number) => Promise} scheduleWakeup - Asks the platform to run the extensions' `tick` no later than `at` (epoch ms). On a platform that runs ticks on a short interval this does nothing. * @property {(work: Promise) => void} background - Runs `work` after the response is sent. The platform keeps the request alive until `work` settles. An error from `work` is logged. */ /** * @typedef {Object} ExtensionRoute * @property {string} [method] - HTTP method (defaults to GET) * @property {'required'|'optional'} [auth] - Authentication requirement * @property {boolean} [serviceAuth] - Also accepts a service auth token the account minted for this server, bound to the route's lexicon method * @property {(request: Request, url: URL, auth: {did: string, scope: string}|null) => Promise|Response} handler * Called unbound: an extension reaches the server through its host, not * through `this`. */ /** * A feature package mounted on the server. Core never imports a feature * package; a platform package passes its factory in and core builds the * extension against an ExtensionHost. * * @typedef {Object} PdsExtension * @property {string} name - Names the extension in errors * @property {string[]} [collections] - The record collections the extension reads and writes through its host, `['*']` for every collection. A call naming any other collection is refused. Absent, the extension may read and write no records at all. * @property {Record} [routes] - Route table fragment, merged into the server's table. A path core already serves is refused at construction. * @property {string[]} [prefixes] - Path prefixes `handle` answers under, `/npm` say. Required with `handle`. A prefix core serves under (`/xrpc`, `/oauth`, `/account`, `/.well-known`, or any route in its table) or that overlaps another extension's is refused at construction. * @property {(request: Request, url: URL) => Promise} [handle] - Answers requests under `prefixes` before core dispatch, for surfaces outside XRPC (a registry, say). The extension owns the whole response: no CORS decoration, no DPoP nonce. A null return passes the request on. * @property {(request: Request, url: URL) => Promise} [handleHost] - Answers requests addressed to a hostname other than the server's (a static site, say), after prefix handlers and before core dispatch. The extension owns the whole response. A null return passes the request on. * @property {RecordWriteGuardPort} [writeGuard] - A veto over writes into the public repo, consulted alongside the platform's guard. * @property {CommitTapPort} [onCommit] - Called after each successful commit. An error is logged and never fails the write. * @property {Record} [features] - Merged into the account overview's `features`, which the account application reads to show or hide its sections. * @property {(count: (collection: string) => number) => Record} [stats] - Merged into the account overview's `stats`, the counts the account application shows beside its sections. `count` answers how many records a collection holds across the public repo and every live space. * @property {() => Promise} [tick] - Background work, run by the platform on a short interval or at the times passed to `scheduleWakeup`. Answers the next time (epoch ms) it has work due, or null. */ /** * @typedef {(host: ExtensionHost) => PdsExtension} ExtensionFactory */ /** * @typedef {Object} SpaceRow * @property {string} uri - space AT-URI * @property {string} spaceDid - the space authority's DID, as carried in `uri` * @property {string} spaceType - the space type NSID, as carried in `uri` * @property {boolean} isOwner - whether this actor is the space authority * @property {SpacePolicy|null} policy - the policy governing the space, with * its defaults filled in. Null when this actor is not the authority. * @property {string} createdAt - ISO timestamp * @property {string|null} deletedAt - ISO timestamp, set by soft delete */ /** * A space policy, as `com.atproto.space.createSpace` takes it: an open union * whose `$type` names the policy (`com.atproto.personalspace.policy`, * `com.atproto.memberspace.policy` or `com.atproto.managedspace.policy`) and * whose other fields configure it. * * @typedef {{$type: string} & Record} SpacePolicy */ /** * What a member of a space may do. Both flags are host-internal state the * authority consults when the memberspace policy governs the space. * * @typedef {Object} SpaceMemberAccess * @property {boolean} read - whether the member may hold a space credential * @property {boolean} write - whether the authority tracks the member's writes */ /** * @typedef {SpaceMemberAccess & {did: string}} SpaceMemberRow */ /** * @typedef {Object} SpaceRecordRow * @property {string} collection * @property {string} rkey * @property {string} cid * @property {Uint8Array} value - DAG-CBOR encoded record * @property {string} repoRev - rev of the commit that wrote it * @property {string} indexedAt - ISO timestamp */ /** * A listed record. `value` is absent when the caller asked for metadata only. * @typedef {Object} SpaceRecordListRow * @property {string} collection * @property {string} rkey * @property {string} cid * @property {Uint8Array} [value] - DAG-CBOR encoded record * @property {string} repoRev * @property {string} indexedAt */ /** * @typedef {Object} SpaceOpRow * @property {string} rev * @property {number} idx - position within the rev, from 0 * @property {'create'|'update'|'delete'} action * @property {string} collection * @property {string} rkey * @property {string|null} cid - null for deletes * @property {string|null} prev - null for creates */ /** * @typedef {Object} SpaceWriterRow * @property {string} did * @property {string} repoRev - the writer's repo rev, as last reported * @property {Uint8Array} data - the writer's commit data, as last reported * @property {string} spaceRev - where the authority sequenced this update */ /** * A write notice a repo host has not yet delivered to the space's authority. * @typedef {Object} SpaceNotifyRow * @property {string} space * @property {string} repoRev * @property {Uint8Array} data * @property {number} attempts - failed deliveries of this repoRev so far * @property {string} nextAttemptAt - ISO timestamp * @property {string} deadlineAt - ISO timestamp after which delivery stops */ /** * A notice an authority has not yet delivered to a syncer registered for a * space it deleted. * @typedef {Object} SpaceDeletedNotifyRow * @property {string} space * @property {string} service - the syncer's service identifier * @property {string} serviceEndpoint * @property {number} attempts - failed deliveries so far * @property {string} nextAttemptAt - ISO timestamp * @property {string} deadlineAt - ISO timestamp: the registration's expiry */ /** * @typedef {Object} SpaceRecipientRow * @property {string} serviceDid * @property {string} serviceEndpoint * @property {string} expiresAt */ /** * A single write within a commit. * @typedef {Object} SpaceWrite * @property {'create'|'update'|'delete'} action * @property {string} collection * @property {string} rkey * @property {string|null} cid - null for deletes * @property {Uint8Array|null} value - null for deletes * @property {string|null} prev - null for creates */ /** * Per-actor storage for permissioned spaces (proposal 0016). * * Separate from ActorStoragePort so that a deployment without spaces carries * no space schema. Writes go through the single commit-shaped * `commitSpaceWrite` rather than per-table methods, so transaction control * stays inside the adapter instead of leaking through the port. * * @typedef {Object} SpaceStoragePort * @property {(uri: string) => Promise} getSpace * @property {(space: SpaceRow) => Promise} putSpace * @property {(uri: string, deletedAt: string) => Promise} deleteSpace * @property {(opts: {type?: string|null, did?: string|null, cursor?: string|null, limit?: number, excludeDeleted?: boolean}) => Promise<{spaces: SpaceRow[], cursor: string|null}>} listSpaces * @property {(space: string, did: string, access: SpaceMemberAccess) => Promise} putMember * @property {(space: string, did: string) => Promise} removeMember * @property {(space: string, cursor: string|null, limit: number) => Promise<{members: SpaceMemberRow[], cursor: string|null}>} listMembers * @property {(space: string, did: string) => Promise} getMember * @property {(space: string) => Promise} removeAllMembers * @property {(space: string, collection: string, rkey: string) => Promise} getSpaceRecord * @property {(space: string, collection: string, rkey: string) => Promise} getSpaceRecordCid * @property {(space: string, opts: {collection?: string|null, cursor?: string|null, limit: number, reverse?: boolean, excludeValues?: boolean}) => Promise<{records: SpaceRecordListRow[], cursor: string|null}>} listSpaceRecords * @property {(space: string) => Promise>} listAllSpaceRecords * @property {(space: string, commit: {writes: SpaceWrite[], setHash: Uint8Array, rev: string, indexedAt: string}) => Promise} commitSpaceWrite * @property {(space: string) => Promise<{setHash: Uint8Array|null, rev: string|null}|null>} getSpaceRepo * @property {(space: string, since: string|null, limit: number) => Promise} listSpaceOps * @property {(space: string, did: string) => Promise} getSpaceWriter * @property {(space: string) => Promise} getSpaceRevHead - the latest spaceRev allocated in the space * @property {(space: string, did: string, repoRev: string, data: Uint8Array, spaceRev: string) => Promise} putSpaceWriter * @property {(space: string, did: string) => Promise} removeSpaceWriter * @property {(space: string, cursor: string|null, limit: number) => Promise<{writers: SpaceWriterRow[], cursor: string|null}>} listSpaceWriters - ascending spaceRev after `cursor`, exclusive; `cursor` is the last entry's spaceRev, null on an empty page * @property {(space: string, jtis: string[], expiresAt: string) => Promise} putCredentialRevocations - idempotent; also drops expired revocations * @property {(space: string, jti: string) => Promise} isCredentialRevoked * @property {(row: Omit) => Promise} enqueueSpaceNotify - replaces any row for the space, with attempts reset * @property {(now: string, limit: number) => Promise} listDueSpaceNotify * @property {(space: string, repoRev: string, attempts: number, nextAttemptAt: string) => Promise} rescheduleSpaceNotify - only while the row still holds repoRev * @property {(space: string, repoRev: string) => Promise} deleteSpaceNotify - only while the row still holds repoRev * @property {(row: SpaceDeletedNotifyRow) => Promise} enqueueSpaceDeletedNotify - replaces any row for the space and service * @property {(now: string, limit: number) => Promise} listDueSpaceDeletedNotify * @property {(space: string, service: string, attempts: number, nextAttemptAt: string) => Promise} rescheduleSpaceDeletedNotify * @property {(space: string, service: string) => Promise} deleteSpaceDeletedNotify * @property {() => Promise} nextSpaceNotifyAt - the earliest nextAttemptAt of either kind of notice, or null * @property {(space: string, serviceDid: string, serviceEndpoint: string, expiresAt: string) => Promise} putCredentialRecipient * @property {(space: string, serviceDid: string) => Promise} deleteCredentialRecipient * @property {(space: string) => Promise} listCredentialRecipients */ // Export empty object - types are defined via JSDoc export {};