Ga naar inhoud

tapedb/worker

Gegenereerd uit de TypeScript-declaraties van het publieke tapedb/worker-entrypoint. Signatures en comments zijn het API-contract voor deze preview.

Extracts and verifies the claim attached to an engine request.

declare function authenticateEngineRequest(request: Request, env: ClaimVerificationEnv, workspaceId: string): Promise<RoleClaim>;
declare function defineTapeDB(options: DefineTapeDBOptions): TapeDBWorker;

Serialises the verified claim for the Durable Object. Only the fields the engine needs are forwarded; signature material is never re-sent.

declare function encodeTrustedRoleClaim(claim: RoleClaim): string;

Reads a claim token from the request.

  • Authorization: Bearer <jwt> is the preferred transport.
  • ?claim=<jwt> is a temporary fallback for WebSocket upgrades, where browser clients cannot set an Authorization header. The mint endpoint should prefer a header wherever the client supports it.

A present-but-non-Bearer Authorization header returns null instead of falling back to the query string, so the two transports cannot be confused.

declare function extractClaimToken(request: Request): string | null;

POST /_tapedb/claim: session to workspace role to a short-lived claim.

Rate limiting is the deployment’s call. If you expose this on the public internet, back allowRequest with a Cloudflare rate-limit binding.

declare function mintClaim(request: Request, deps: MintClaimDependencies): Promise<Response>;

Development-only issuer. Mints an OWNER claim for any workspace without a session so tapedb dev works with zero configuration.

The caller is responsible for refusing this in production; defineTapeDB enforces that before this function is reachable.

declare function mintDevClaim(workspaceId: string, secret: string): Promise<MintedClaim>;

Signs the compact HS256 claim the engine verifier accepts.

declare function signRoleClaim(secret: string, input: MintClaimInput, now?: number): Promise<MintedClaim>;

Verifies a compact JWS against the frozen RoleClaim contract.

Throws ClaimError for malformed tokens, bad signatures, expiry, workspace mismatches and invalid claim values. A missing server secret throws a plain Error: that is a deployment problem, not a client failure.

declare function verifyRoleClaim(token: string, env: ClaimVerificationEnv, expectedWorkspaceId: string): Promise<RoleClaim>;

Typed verification failure; code is safe to return to the caller.

declare class ClaimError extends Error {
readonly code: ClaimErrorCode;
constructor(code: ClaimErrorCode, message: string);
}

One workspace engine, addressed by idFromName(workspaceId).

Owns its SQLite schema, applies bundled Atlas migrations on boot, commits transactions, and flushes/snapshots to R2 on a 30-second alarm.

declare class WorkspaceRoom extends DurableObject<Env> {
/**
* Last emitted result per socket client id and subscription id. This is a
* cache, not a source of truth: hibernation clears it and the next pass
* falls back to a full `query:data` payload.
*/
private readonly subscriptionResults;
/** Namespaces touched since the current debounce window opened. */
private pendingInvalidationNamespaces;
private invalidationTimer;
constructor(ctx: DurableObjectState, env: Env);
private get workspaceId();
/**
* Applies pending migrations before the object serves any request. Each
* migration and its `_migrations` row commit in the same transaction, so a
* failed boot cannot leave a half-migrated schema, and each runs with
* foreign keys off and checked before commit (see
* `applyMigrationWithoutCascades`).
*/
private applyMigrations;
private applyMigration;
fetch(request: Request): Promise<Response>;
private route;
private transact;
/**
* Runs a query document against this workspace. The claim is read from the
* Worker-forwarded header; `executeQuery` requires `view` on every requested
* and traversed namespace before touching SQL.
*/
private query;
private handleWebSocket;
webSocketMessage(ws: WebSocket, message: string | ArrayBuffer): Promise<void>;
webSocketClose(ws: WebSocket, code: number, reason: string): void;
webSocketError(ws: WebSocket): void;
private handlePing;
private topicError;
private handleTopicJoin;
private handleTopicLeave;
/**
* Delivers to every other socket joined to the topic. The sender need not
* be joined; it never receives its own message.
*/
private handleTopicSend;
private handlePresenceSet;
private handlePresenceClear;
private handleCursorSet;
private handleDragSet;
private handleDragClear;
private broadcastPresence;
private broadcastToTarget;
private presencePeers;
private socketsForTarget;
private removePresence;
private handleSubscribe;
private handleUnsubscribe;
private send;
private closeSocket;
private resultsFor;
private dropSubscriptionResults;
private scheduleInvalidation;
private flushInvalidations;
/** One subscription's re-run for this flush: its result, or why it failed. */
private runSubscription;
/**
* Runs the durability pass (flush then snapshot) and schedules the next
* pass only while there is work left. The 30s delay lets transactions
* accumulate into fewer, larger log segments.
*/
alarm(): Promise<void>;
/**
* Schedules an alarm for the next due durability pass. An existing alarm is
* never pushed later: a write must not delay a flush that is already due.
*/
private scheduleNextDurabilityAlarm;
private scheduleDurabilityAlarm;
/**
* File metadata rows. Bytes live in R2 and never enter the transaction log;
* this table is the queryable index over them.
*/
private listFiles;
private recordFile;
private forgetFile;
/** File-index entries are log rows, so they ship to R2 like any write. */
private scheduleFileFlush;
/** Registry snapshot for tooling: API names, attribute types and links. */
private schemaDocument;
private status;
}

The two methods a deployment implements. Everything else about identity (passwords, magic codes, OAuth, cookies) stays in the host application.

interface ClaimIdentity {
/** Resolve the caller's subject from the request. Null means "not signed in". */
getSubject(headers: Headers): Promise<{
sub: string;
} | null>;
/** Resolve the caller's role in a workspace. Null means "not a member". */
getRole(workspaceId: string, sub: string): Promise<WorkspaceRole | null>;
}
interface DefineTapeDBOptions {
schema: TapeDBSchema;
/**
* The app's committed migrations, from `tapedb/migrations.gen.ts`. Omit
* only before the first `tapedb migrate dev`: the schema's generated SQL
* is used, which cannot evolve.
*/
migrations?: readonly AppMigration[];
/**
* Identity adapter. Implement `getSubject` and `getRole` to serve
* `POST /_tapedb/claim`. Without it that route returns 501 and the consumer
* must supply tokens to the client itself.
*/
identity?: ClaimIdentity;
/**
* Development-only issuer: mints an OWNER claim for any workspace with no
* session. Always refused when `ENVIRONMENT` is `production`.
*/
devClaims?: boolean;
/**
* Broadcast topics. `authorize` decides whether a caller may join a topic
* or send to it; without it any member of the workspace may, as with
* presence.
*/
topics?: {
authorize?: TopicAuthorizer;
};
}

Worker bindings and vars. tapedb init writes exactly these.

Three resources: one Durable Object namespace, two R2 buckets. There is no D1 and no identity database: the engine consumes a verified claim and never reads a session.

interface Env {
/** The database room. One instance per workspace. */
WORKSPACE: DurableObjectNamespace<WorkspaceRoom>;
/** Transaction log segments and snapshots. */
DATA: R2Bucket;
/** Uploaded files. Optional unless `db.$files` is used. */
FILES?: R2Bucket;
ENVIRONMENT?: string;
SERVICE_VERSION?: string;
ENABLE_SMOKE?: string;
ALLOWED_ORIGINS?: string;
/** Shared HS256 secret used to verify role claims. */
TAPEDB_CLAIM_SECRET: string;
/** Optional expected `iss` claim; checked only when set. */
TAPEDB_CLAIM_ISSUER?: string;
/** Optional expected `aud` claim; checked only when set. */
TAPEDB_CLAIM_AUDIENCE?: string;
/**
* Enables the development claim issuer. Set to `"true"` by `tapedb dev` on
* the command line and never written to `wrangler.jsonc`, so a plain
* `wrangler deploy` has no dev claim route.
*/
TAPEDB_DEV_CLAIMS?: string;
}
interface MintClaimDependencies {
identity: ClaimIdentity;
secret: string;
issuer?: string;
audience?: string;
ttlSeconds?: number;
now?: () => number;
/** Optional per-caller limiter; return false to reject with 429. */
allowRequest?: (key: string) => Promise<boolean>;
}
interface TapeDBWorker {
/** The database room class. Export it from the Worker entry. */
WorkspaceRoom: typeof WorkspaceRoom;
worker: {
fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response>;
};
}

One app migration, as tapedb migrate bundles it.

type AppMigration = BundledWorkspaceMigration;

Broadcast topics: fire-and-forget messages between the sockets of one workspace, on the socket they already hold.

A socket joins topics; a message sent to a topic reaches every other socket joined to it, with the sender’s client id and subject. Nothing is stored or replayed: a socket that is not joined when a message is sent never sees it. Joined topics live in the socket attachment, so they survive hibernation, and the client rejoins them after a reconnect.

Who may join or send is the deployment’s call (defineTapeDB({ topics })); by default any member of the workspace may, as with presence.

type TopicAction = "join" | "send";

Decides whether a caller may join a topic or send to it. Return false (or throw) to refuse; the socket gets a topic:error and nothing else happens.

type TopicAuthorizer = (context: {
auth: {
sub: string;
role: number;
workspaceId: string;
};
topic: string;
action: TopicAction;
}) => boolean;

Worker entry.

defineTapeDB() installs the deployment schema and returns the Durable Object class plus the fetch handler. The consumer’s Worker is then two exports and nothing else:

const tapedb = defineTapeDB({ schema }); export const WorkspaceRoom = tapedb.WorkspaceRoom; export default tapedb.worker;

declare const MAX_UPLOAD_BYTES: number;

Trusted header forwarded to the Durable Object after verification.

declare const TRUSTED_CLAIM_HEADER = "x-tapedb-claims";

Shared role-claim contract between the Worker (JWT verification) and the workspace engine (permission checks). consumes this shape.

declare const WORKSPACE_ROLE_LEVELS: {
readonly GUEST: 0;
readonly USER: 1;
readonly MEMBER: 2;
readonly EDITOR: 3;
readonly MODERATOR: 4;
readonly ADMIN: 5;
readonly OWNER: 6;
readonly FOUNDER: 7;
readonly DEVELOPER: 8;
};