import * as Y from 'yjs'; import { LanguageModel, ToolSet, StreamTextResult, streamText, ModelMessage } from 'ai'; import { Hono, Context, ErrorHandler } from 'hono'; import * as zod_v4_core from 'zod/v4/core'; import * as zod from 'zod'; import * as better_auth_plugins from 'better-auth/plugins'; import * as better_auth from 'better-auth'; import { betterAuth } from 'better-auth'; /** * Wire protocol constants. * * The JSON WebSocket protocol uses dotted string identifiers (e.g. * `"records.query"`) as the `type` discriminator. All message types are * grouped under a single `MSG` object so imports stay tidy: * * import { MSG, dispatch, clientBuild } from 'deepspace' * * dispatch(raw, { * }) * * Each key's value is the on-wire string — grep-friendly, self- * documenting, and infinite per namespace. Adding a new message is one * line here plus one arm in the discriminated union in `./messages.ts`. * * Yjs binary protocol constants (`MSG_YJS_SYNC`, `MSG_YJS_AWARENESS`) are * intentionally kept numeric and separate from `MSG` — they ride a * binary WebSocket frame format and aren't part of the JSON dispatcher. */ declare const MSG: { readonly SUBSCRIBE: "core.subscribe"; readonly UNSUBSCRIBE: "core.unsubscribe"; readonly QUERY_RESULT: "core.query_result"; readonly RECORD_CHANGE: "core.record_change"; readonly PUT: "core.put"; readonly DELETE: "core.delete"; readonly ERROR: "core.error"; readonly USER_INFO: "user.info"; readonly USER_LIST: "user.list"; readonly SET_ROLE: "user.set_role"; readonly USER_UPDATE: "user.update"; readonly AUTH: "auth"; readonly YJS_JOIN: "yjs.join"; readonly YJS_LEAVE: "yjs.leave"; readonly ACK: "records.ack"; readonly LIST_SCHEMAS: "records.list_schemas"; readonly RESUBSCRIBE: "records.resubscribe"; readonly CANVAS_SHAPES: "canvas.shapes"; readonly CANVAS_ADD: "canvas.add"; readonly CANVAS_MOVE: "canvas.move"; readonly CANVAS_RESIZE: "canvas.resize"; readonly CANVAS_DELETE: "canvas.delete"; readonly CANVAS_UPDATE: "canvas.update"; readonly CANVAS_VIEWPORT: "canvas.viewport"; readonly CANVAS_UNDO: "canvas.undo"; readonly CANVAS_REDO: "canvas.redo"; readonly CRON_TASKS: "cron.tasks"; readonly CRON_HISTORY: "cron.history"; readonly CRON_TRIGGER: "cron.trigger"; readonly CRON_PAUSE: "cron.pause"; readonly CRON_RESUME: "cron.resume"; readonly CRON_STATUS: "cron.status"; readonly CRON_ACK: "cron.ack"; readonly JOB_ENQUEUE: "job.enqueue"; readonly JOB_CANCEL: "job.cancel"; readonly JOB_RETRY: "job.retry"; readonly JOB_UPDATE: "job.update"; readonly PRESENCE_SYNC: "presence.sync"; readonly PRESENCE_JOIN: "presence.join"; readonly PRESENCE_LEAVE: "presence.leave"; readonly PRESENCE_UPDATE: "presence.update"; }; /** * Type of any message type constant — the union of every string literal * stored in `MSG`. Useful when declaring functions that accept "any known * message type" without enumerating all 54 strings by hand. */ type MsgType = (typeof MSG)[keyof typeof MSG]; /** Outer envelope id for binary-framed yjs sync messages. Varuint-encoded. */ declare const MSG_YJS_SYNC = 22; /** Outer envelope id for binary-framed yjs awareness messages. Varuint-encoded. */ declare const MSG_YJS_AWARENESS = 23; /** Role assigned to unauthenticated WebSocket connections */ declare const ROLE_ANONYMOUS = "viewer"; /** Prefix `base-room` mints anonymous socket ids with. */ declare const ANONYMOUS_USER_ID_PREFIX = "anon-"; /** The one test for an anonymous socket identity — `ROLE_ANONYMOUS` cannot be * it, since authenticated users can legitimately hold the same role name. */ declare function isAnonymousUserId(userId: string): boolean; /** Default role for newly registered authenticated users */ declare const ROLE_DEFAULT = "member"; /** Admin role */ declare const ROLE_ADMIN = "admin"; /** * Reported when a record id resolves to nothing. * * This crosses the DO boundary as a response body and callers branch on it, * so it is a contract rather than prose. `getChat` in particular treats it as * "no such chat" and returns null — the same answer it gives for a chat owned * by someone else, which is what stops chat ids being enumerable. If a room * reworded this independently, a miss would start throwing while a cross-user * hit kept returning null, and that difference is an existence oracle. */ declare const RECORD_NOT_FOUND = "Record not found"; /** * Typed wire-protocol layer — discriminated unions, typed builders, and a * type-safe dispatcher for every `MSG.*` the SDK understands. * * Why this exists * --------------- * * The string `MSG.*` constants in `./constants.ts` are the authoritative * wire protocol, but using them directly is error-prone: a typo picks the * wrong message with the wrong payload shape and fails silently at * runtime. This module pairs every constant with its payload type, so * that: * * 1. Building a message with `clientBuild.canvasAdd(...)` is payload- * checked at the call site — the compiler refuses to ship a wrong * shape. * * 2. Parsing an inbound message via `dispatch(raw, handlers)` narrows the * payload type inside each handler automatically, replacing the * unsafe `switch (msg.type) { case MSG.X: (payload as any).foo }` * pattern. * * 3. Tightening `BaseRoom.sendTo` / `BaseRoom.broadcast` / * `HandlerContext.send` / `SubscriptionContext.send` to accept * `ServerMessage` turns the type layer into enforcement: any room * that ships a payload inconsistent with its declared arm fails to * compile. Without that, the discriminated union is documentation, * not contract. * * 4. Adding a new `MSG.*` is localized: one entry in the discriminated * union, one builder function, one handler key in every dispatcher * that cares. No grep-and-fix across the codebase. * * 5. Apps can extend the SDK protocol without forking: `dispatch` is * generic over any `M extends ProtocolMessage`, and builders are * plain objects so apps compose via spread (`{ ...clientBuild, * myMessage: ... }`). * * Direction split * --------------- * * Some message types carry different payloads depending on who's sending. * `MSG.RECORDS_RESUBSCRIBE`, for example, is `{}` when the client asks to * but `{ state, tick }` when the server broadcasts the start event. * `MSG.CANVAS_ADD` is a flat shape dict on the way in and a `{ shape }` * wrapper on the way out. Modelling these with one union would force * handlers to juggle a union payload — clunky and error-prone. Instead we * split by direction: * * - `ClientMessage` — what the client sends to the server * - `ServerMessage` — what the server sends to the client * - `ProtocolMessage = ClientMessage | ServerMessage` (for code that * really doesn't care — avoid when possible) * * Each side gets its own builder (`clientBuild` / `serverBuild`) and each * side's dispatcher is parameterised with the union it expects. * * Payload strictness * ------------------ * * Where payload shapes are stable + narrow (ids, flags), we type them * precisely. Where they're opaque or escape the protocol layer (record * data blobs, Yjs binary frames, canvas shapes), we use `unknown` and * defer narrowing to the caller. This is intentional: over-typing opaque * payloads would require the protocol layer to import application types * and defeat the "thin wire contract" goal. */ /** * The outer shape of every wire message. `T` is the string discriminator * (e.g. `"canvas.add"`) — keeping it as a generic literal type lets the * discriminated-union narrowing in `dispatch()` pick the right payload. * * Callers extending the protocol should pass a string-literal type for * `T`, not the widened `string`. `BaseMessage` collapses the * discriminated union and handler-map key inference falls back to a * single untyped `string` key, losing all narrowing. */ interface BaseMessage { type: T; payload: P; } /** Matches when a payload is intentionally empty — `{}` on the wire. */ type EmptyPayload = Record; /** * Every message the client can legitimately send. Extend in app code with * a string-literal union arm: * * type MyClientMessage = ClientMessage | BaseMessage<'myapp.foo', { x: number }> */ type ClientMessage = BaseMessage | BaseMessage | BaseMessage; requestId?: string; }> | BaseMessage | BaseMessage | BaseMessage | BaseMessage | BaseMessage | BaseMessage | BaseMessage | BaseMessage | BaseMessage | BaseMessage | BaseMessage | BaseMessage; }> | BaseMessage | BaseMessage | BaseMessage | BaseMessage | BaseMessage | BaseMessage | BaseMessage | BaseMessage | BaseMessage | BaseMessage>; /** * Every message the server can send. Room and handler `send` / `broadcast` * signatures are tightened to this union so outbound payloads are * compile-checked against the wire contract. As with `ClientMessage`, * extend via a string-literal union arm in app code when adding new * server-side broadcasts. */ type ServerMessage = BaseMessage | BaseMessage | BaseMessage | BaseMessage | BaseMessage | BaseMessage | BaseMessage | BaseMessage | BaseMessage | BaseMessage | BaseMessage | BaseMessage | BaseMessage | BaseMessage | BaseMessage | BaseMessage; }> | BaseMessage | BaseMessage | BaseMessage | BaseMessage | BaseMessage | BaseMessage | BaseMessage | BaseMessage | BaseMessage | BaseMessage; }>; /** * Any wire message, regardless of direction. Use this only when the code * path genuinely doesn't care — otherwise pick `ClientMessage` or * `ServerMessage` so the handler's payload type is narrower. */ type ProtocolMessage = ClientMessage | ServerMessage; /** Factories for every client → server message. */ declare const clientBuild: { subscribe: (subscriptionId: string, query: unknown) => { readonly type: "core.subscribe"; readonly payload: { readonly subscriptionId: string; readonly query: unknown; }; }; unsubscribe: (subscriptionId: string) => { readonly type: "core.unsubscribe"; readonly payload: { readonly subscriptionId: string; }; }; put: (collection: string, recordId: string, data: Record, requestId?: string) => { readonly type: "core.put"; readonly payload: { readonly collection: string; readonly recordId: string; readonly data: Record; readonly requestId: string | undefined; }; }; remove: (collection: string, recordId: string, requestId?: string) => { readonly type: "core.delete"; readonly payload: { readonly collection: string; readonly recordId: string; readonly requestId: string | undefined; }; }; userList: () => { readonly type: "user.list"; readonly payload: EmptyPayload; }; setRole: (userId: string, role: string) => { readonly type: "user.set_role"; readonly payload: { readonly userId: string; readonly role: string; }; }; userUpdate: () => { readonly type: "user.update"; readonly payload: EmptyPayload; }; listSchemas: () => { readonly type: "records.list_schemas"; readonly payload: EmptyPayload; }; yjsJoin: (collection: string, recordId: string, fieldName: string) => { readonly type: "yjs.join"; readonly payload: { readonly collection: string; readonly recordId: string; readonly fieldName: string; }; }; yjsLeave: (collection: string, recordId: string, fieldName: string) => { readonly type: "yjs.leave"; readonly payload: { readonly collection: string; readonly recordId: string; readonly fieldName: string; }; }; canvasAdd: (shape: Record) => { readonly type: "canvas.add"; readonly payload: Record; }; canvasMove: (shapeId: string, x: number, y: number) => { readonly type: "canvas.move"; readonly payload: { readonly shapeId: string; readonly x: number; readonly y: number; }; }; canvasResize: (shapeId: string, width: number, height: number, x?: number, y?: number) => { readonly type: "canvas.resize"; readonly payload: { readonly shapeId: string; readonly width: number; readonly height: number; readonly x: number | undefined; readonly y: number | undefined; }; }; canvasDelete: (shapeId: string) => { readonly type: "canvas.delete"; readonly payload: { readonly shapeId: string; }; }; canvasUpdate: (shapeId: string, props: Record) => { readonly type: "canvas.update"; readonly payload: { readonly shapeId: string; readonly props: Record; }; }; canvasViewport: (viewport: Record) => { readonly type: "canvas.viewport"; readonly payload: Record; }; canvasUndo: () => { readonly type: "canvas.undo"; readonly payload: EmptyPayload; }; canvasRedo: () => { readonly type: "canvas.redo"; readonly payload: EmptyPayload; }; cronTrigger: (taskName: string, requestId?: string) => { readonly type: "cron.trigger"; readonly payload: { readonly taskName: string; readonly requestId: string | undefined; }; }; cronPause: (taskName: string, requestId?: string) => { readonly type: "cron.pause"; readonly payload: { readonly taskName: string; readonly requestId: string | undefined; }; }; cronResume: (taskName: string, requestId?: string) => { readonly type: "cron.resume"; readonly payload: { readonly taskName: string; readonly requestId: string | undefined; }; }; jobEnqueue: (requestId: string, type: string, payload?: unknown, maxAttempts?: number) => { readonly type: "job.enqueue"; readonly payload: { readonly requestId: string; readonly type: string; readonly payload: unknown; readonly maxAttempts: number | undefined; }; }; jobCancel: (jobId: string) => { readonly type: "job.cancel"; readonly payload: { readonly jobId: string; }; }; jobRetry: (jobId: string) => { readonly type: "job.retry"; readonly payload: { readonly jobId: string; }; }; presenceUpdate: (state: Record) => { readonly type: "presence.update"; readonly payload: Record; }; }; /** Factories for every server → client message. Used by room DOs and * handler modules so server broadcasts are payload-checked too. Opaque * slots (`record`, `shape`, `peer`, etc.) take `unknown` so concrete * domain types — `RecordResult`, `CanvasShape`, `MediaPeer` — can be * passed without casts. */ declare const serverBuild: { queryResult: (subscriptionId: string, records: unknown[]) => { readonly type: "core.query_result"; readonly payload: { readonly subscriptionId: string; readonly records: unknown[]; }; }; recordChange: (collection: string, record: unknown, changeType: "create" | "update" | "delete") => { readonly type: "core.record_change"; readonly payload: { readonly collection: string; readonly record: unknown; readonly changeType: "create" | "update" | "delete"; }; }; error: (error: string, subscriptionId?: string) => { readonly type: "core.error"; readonly payload: { readonly error: string; readonly subscriptionId: string | undefined; }; }; ackSuccess: (requestId: string, recordId?: string) => { readonly type: "records.ack"; readonly payload: { readonly requestId: string; readonly success: true; readonly recordId: string | undefined; }; }; ackFailure: (requestId: string, error: string) => { readonly type: "records.ack"; readonly payload: { readonly requestId: string; readonly success: false; readonly error: string; }; }; resubscribe: () => { readonly type: "records.resubscribe"; readonly payload: EmptyPayload; }; schemas: (schemas: unknown) => { readonly type: "records.list_schemas"; readonly payload: { readonly schemas: unknown; }; }; userInfo: (user: unknown) => { readonly type: "user.info"; readonly payload: unknown; }; userList: (users: unknown[]) => { readonly type: "user.list"; readonly payload: { readonly users: unknown[]; }; }; yjsJoin: (collection: string, recordId: string, fieldName: string, canWrite: boolean) => { readonly type: "yjs.join"; readonly payload: { readonly collection: string; readonly recordId: string; readonly fieldName: string; readonly canWrite: boolean; }; }; canvasShapes: (shapes: unknown[], viewports: unknown[]) => { readonly type: "canvas.shapes"; readonly payload: { readonly shapes: unknown[]; readonly viewports: unknown[]; }; }; canvasAdd: (shape: unknown) => { readonly type: "canvas.add"; readonly payload: { readonly shape: unknown; }; }; canvasMove: (shapeId: string, x: number, y: number) => { readonly type: "canvas.move"; readonly payload: { readonly shapeId: string; readonly x: number; readonly y: number; }; }; canvasResize: (shapeId: string, width: number, height: number, x?: number, y?: number) => { readonly type: "canvas.resize"; readonly payload: { readonly shapeId: string; readonly width: number; readonly height: number; readonly x: number | undefined; readonly y: number | undefined; }; }; canvasDelete: (shapeId: string) => { readonly type: "canvas.delete"; readonly payload: { readonly shapeId: string; }; }; canvasUpdate: (shapeId: string, props: Record) => { readonly type: "canvas.update"; readonly payload: { readonly shapeId: string; readonly props: Record; }; }; canvasViewport: (viewport: unknown) => { readonly type: "canvas.viewport"; readonly payload: { readonly viewport: unknown; }; }; canvasViewportRemoved: (userId: string) => { readonly type: "canvas.viewport"; readonly payload: { readonly userId: string; readonly removed: true; }; }; cronTasks: (tasks: unknown) => { readonly type: "cron.tasks"; readonly payload: { readonly tasks: unknown; }; }; cronHistory: (history: unknown) => { readonly type: "cron.history"; readonly payload: { readonly history: unknown; }; }; cronStatus: (tasks: unknown, recentHistory: unknown) => { readonly type: "cron.status"; readonly payload: { readonly tasks: unknown; readonly recentHistory: unknown; }; }; cronAckSuccess: (requestId: string, taskName: string) => { readonly type: "cron.ack"; readonly payload: { readonly requestId: string; readonly taskName: string; readonly ok: true; }; }; cronAckFailure: (requestId: string, reason: "read_only" | "unknown_task" | "failed", error?: string, taskName?: string) => { readonly type: "cron.ack"; readonly payload: { readonly requestId: string; readonly taskName: string | undefined; readonly ok: false; readonly reason: "read_only" | "unknown_task" | "failed"; readonly error: string | undefined; }; }; jobSnapshot: (jobs: unknown[]) => { readonly type: "job.update"; readonly payload: { readonly kind: "snapshot"; readonly jobs: unknown[]; }; }; jobEnqueued: (job: unknown, requestId?: string) => { readonly type: "job.update"; readonly payload: { readonly kind: "enqueued"; readonly job: unknown; readonly requestId: string | undefined; }; }; jobProgress: (job: unknown) => { readonly type: "job.update"; readonly payload: { readonly kind: "progress"; readonly job: unknown; }; }; jobSucceeded: (job: unknown) => { readonly type: "job.update"; readonly payload: { readonly kind: "succeeded"; readonly job: unknown; }; }; jobFailed: (job: unknown) => { readonly type: "job.update"; readonly payload: { readonly kind: "failed"; readonly job: unknown; }; }; jobCanceled: (job: unknown) => { readonly type: "job.update"; readonly payload: { readonly kind: "canceled"; readonly job: unknown; }; }; jobRetried: (job: unknown) => { readonly type: "job.update"; readonly payload: { readonly kind: "retried"; readonly job: unknown; }; }; presenceSync: (peers: unknown[]) => { readonly type: "presence.sync"; readonly payload: { readonly peers: unknown[]; }; }; presenceJoin: (peer: unknown) => { readonly type: "presence.join"; readonly payload: { readonly peer: unknown; }; }; presenceLeave: (userId: string) => { readonly type: "presence.leave"; readonly payload: { readonly userId: string; }; }; presenceUpdate: (userId: string, state: Record) => { readonly type: "presence.update"; readonly payload: { readonly userId: string; readonly state: Record; }; }; }; /** * Handler map keyed by a message's string `type`. Every key maps to the * payload type of the matching arm in `M`, so handlers get automatic * payload narrowing with no runtime checks. * * Narrowing requires each `M` arm to use a string-literal `T`; widening * `M` to `BaseMessage` collapses all keys to a single * untyped `string` and the map becomes a plain `Record void>`. */ type MessageHandlers = ProtocolMessage> = { [K in M as K['type']]?: (payload: K['payload']) => void; }; /** * Parse and route an incoming wire message. Accepts either the raw JSON * string (directly from `ws.onmessage`'s `event.data`) or an already- * parsed object. Returns `true` if a handler ran, `false` otherwise * (unrecognised type, parse failure, or malformed envelope). * * Defaults `M` to `ProtocolMessage` so callers who don't care about * direction can just do `dispatch(raw, handlers)`; narrow with * `dispatch` / `dispatch` to restrict which * arms the handler map is allowed to cover. */ declare function dispatch = ProtocolMessage>(raw: unknown, handlers: MessageHandlers): boolean; /** * Serialise a typed-built message for `ws.send`. A tiny wrapper around * `JSON.stringify` kept around so call sites read as * `ws.send(encode(clientBuild.canvasAdd(...)))` rather than paying * attention to stringify arguments. */ declare function encode>(message: M): string; /** * BaseRoom — Abstract base class for all DeepSpace Durable Objects. * * Provides: * - WebSocket upgrade with Cloudflare hibernation API * - Connection tracking (WebSocket -> UserAttachment) * - Auth: parse JWT-verified user info from internal request headers * - Presence: connected users list, awareness on connect/disconnect * - Message routing: JSON parse -> dispatch by `type` field, binary hook * - Raw SQLite access via this.sql * - Broadcast helpers: broadcast(), sendTo() * - HTTP fetch handler with WebSocket upgrade detection * - Internal control-plane endpoint: POST /internal/disconnect-sockets * (force every client to reconnect and resync after out-of-band writes) * * Subclasses implement lifecycle hooks: * onConnect, onMessage, onBinaryMessage, onDisconnect, onRequest, onAlarm */ interface UserAttachment { userId: string; userName: string; userEmail: string; userImageUrl?: string; /** Subclass-specific data serialized alongside user info */ [key: string]: unknown; } declare abstract class BaseRoom> { protected state: DurableObjectState; protected env: E; protected sql: SqlStorage; constructor(state: DurableObjectState, env: unknown); fetch(request: Request): Promise; /** * Handle built-in `/internal/*` control-plane routes shared by every room * type. Returns a `Response` if the request was an internal route, or `null` * to let the caller continue normal dispatch. * * Currently: * - `POST /internal/disconnect-sockets` — close every live WebSocket so * clients reconnect and resync. Optional JSON body `{ code?, reason? }` * overrides the defaults (1012 / 'state-refresh'). Responds with * `{ success: true, closed: }`. * * See the security note on `fetch()`: this path is only reachable via DO * stub fetch from the app worker, never from the public internet. */ protected handleInternalRequest(request: Request, url: URL): Promise; private handleWebSocketUpgrade; webSocketMessage(ws: WebSocket, message: ArrayBuffer | string): Promise; webSocketClose(ws: WebSocket, _code: number, _reason: string): Promise; webSocketError(_ws: WebSocket, error: unknown): Promise; alarm(): Promise; /** * Called when a new WebSocket connects (after auth parsing). * Return an augmented attachment to serialize on the WebSocket, * or void to use the default attachment. */ protected onConnect(_ws: WebSocket, _user: UserAttachment): UserAttachment | void | Promise; /** * Called for each parsed JSON message. */ protected abstract onMessage(ws: WebSocket, user: UserAttachment, message: { type: string; [key: string]: unknown; }): void | Promise; /** * Called for binary messages (Yjs, custom protocols). */ protected onBinaryMessage?(ws: WebSocket, user: UserAttachment, data: ArrayBuffer): void | Promise; /** * Called when a WebSocket disconnects. */ protected onDisconnect(_ws: WebSocket, _user: UserAttachment): void | Promise; /** * Called for HTTP requests that are NOT WebSocket upgrades. */ protected onRequest?(request: Request): Response | Promise; /** * Called on DO alarm. */ protected onAlarm?(): void | Promise; /** * Get all connected WebSockets. */ protected getWebSockets(): WebSocket[]; /** * Force every connected client to reconnect and resync by closing all live * WebSockets. Returns the number of sockets closed. * * Use this after an **out-of-band, server-side write** to the room's records * — an admin import route, a migration script, a cron job, or a server * action — that connected clients have no way to learn about. Without it, a * browser tab holding stale in-memory state keeps operating on (and may * autosave over) data that changed underneath it. Closing the socket makes * the client SDK reconnect and pull fresh query results. * * The default close code is 1012 ("service restart") with reason * 'state-refresh'. The DeepSpace client treats *any* close as a reconnect * trigger (it does not special-case clean/1000 closes), so on reconnect it * re-subscribes every active query and receives fresh `QUERY_RESULT`s — * `useQuery` consumers see the new data without re-subscribing. * * Each close is guarded so one already-closing socket can't abort the sweep. * * @param options.code WebSocket close code (default 1012). * @param options.reason WebSocket close reason (default 'state-refresh'). * @returns the number of sockets that were closed. */ disconnectAllSockets(options?: { code?: number; reason?: string; }): number; /** * Get the user attachment for a WebSocket. */ protected getAttachment(ws: WebSocket): UserAttachment | null; /** * Get all currently connected users. */ protected getConnectedUsers(): UserAttachment[]; /** * Send a JSON message to a specific WebSocket. * * Typed as `ServerMessage` so every room's outbound traffic is * compile-checked against the wire protocol contract. Passing * `{ type: 'whatever', payload: {...} }` with a non-matching arm fails * to compile — that's the whole point of the typed layer. Apps that * need to send an app-specific message should override `sendTo` in * their subclass with a widened union (`ServerMessage | MyAppMessage`). */ protected sendTo(ws: WebSocket, message: ServerMessage): void; /** * Send binary data to a specific WebSocket. */ protected sendBinaryTo(ws: WebSocket, data: Uint8Array | ArrayBuffer): void; /** * Broadcast a JSON message to all connected WebSockets. * Optionally exclude a specific WebSocket (e.g. the sender). * * See `sendTo` for the reasoning behind typing as `ServerMessage`. */ protected broadcast(message: ServerMessage, exclude?: WebSocket): void; /** * Broadcast binary data to all connected WebSockets. */ protected broadcastBinary(data: Uint8Array | ArrayBuffer, exclude?: WebSocket): void; } /** * Shared type definitions for the DeepSpace SDK * * Types used by both client-side SDK and server-side workers. */ type ColumnInterpretation = { kind: 'plain'; } | { kind: 'currency'; symbol: string; decimals: number; } | { kind: 'date'; format?: string; } | { kind: 'datetime'; format?: string; } | { kind: 'boolean'; trueLabel?: string; falseLabel?: string; } | { kind: 'percent'; decimals?: number; } | { kind: 'select'; options: string[]; } | { kind: 'multiselect'; options: string[]; } | { kind: 'url'; } | { kind: 'email'; } | { kind: 'json'; } | { kind: 'reference'; targetTable: string; displayColumn: string; }; interface ColumnDefinition { /** Stable ID override (survives renames). Falls back to `col_{name}`. */ id?: string; name: string; storage: 'number' | 'text'; interpretation: ColumnInterpretation | string; expression?: string; userBound?: boolean; immutable?: boolean; required?: boolean; default?: unknown; timestampTrigger?: { field: string; value?: unknown; }; } type PermissionLevel = boolean | 'own' | 'unclaimed-or-own' | 'collaborator' | 'team' | 'access' | 'published' | 'shared'; interface RolePermissions { read: PermissionLevel; /** Create has no existing record to evaluate, so it is an explicit grant. */ create: boolean; update: PermissionLevel; delete: PermissionLevel; /** If set, only these caller-supplied columns can be created or updated by this role. */ writableFields?: string[]; } interface CollectionSchema { name: string; /** Every collection is stored in typed SQL columns. */ columns: ColumnDefinition[]; /** Composite uniqueness constraint (for example `['userId', 'taskId']`). */ uniqueOn?: string[]; /** Ownership column; defaults to the row creator. */ ownerField?: string; /** Column containing a JSON array of collaborator user ids. */ collaboratorsField?: string; /** Column containing the team id used by team permission rules. */ teamField?: string; /** A string is public when equal to `public`; an object supplies an exact value. */ visibilityField?: string | { field: string; value: unknown; }; /** Permissions per role; `*` is the catch-all role. */ permissions: Record; /** Default role assigned on the `users` collection. */ defaultRole?: string; /** * `users` collection only: what the `user.list` roster shows a non-admin * whose `read` policy is row-scoped (`'own'`, `'team'`, …). Default * `'public-identity'`: every registered user's id/name/imageUrl/role/lastSeenAt * (`lastSeenAt` is what `usePresence` reads) — the * row policy keeps guarding full-row reads on the records/query path. * `'read-policy'`: the roster contains only the rows the caller's read * policy grants (still projected to public identity) — for apps that scope * users to a tenant/team and must not show names across it. */ roster?: 'public-identity' | 'read-policy'; } interface Query { collection: string; where?: Record; orderBy?: string; orderDir?: 'asc' | 'desc'; limit?: number; } interface Subscription { id: string; query: Query; } /** Key for Yjs doc: collection:recordId:fieldName */ type YjsDocKey = string; interface YjsSubscription { collection: string; recordId: string; fieldName: string; } interface RecordResult { recordId: string; data: Record; createdBy: string; createdAt: string; updatedAt: string; } /** * Server-specific protocol types * * These depend on Cloudflare Workers / Yjs imports and can't live in shared/types. * All other protocol types (Query, payloads, etc.) live in shared/types/index.ts. */ /** Stored on WebSocket attachment (survives hibernation) */ interface ConnectionAttachment extends UserAttachment { role: string; subscriptions: Subscription[]; /** Yjs docs this connection is editing */ yjsSubscriptions: YjsSubscription[]; /** Client-side Yjs awareness clientId for each subscribed document. */ awarenessClientIds?: Partial>; } /** * Context passed to handlers for accessing shared resources. * * `send` is typed against `ServerMessage` so every outbound message is * compile-checked against the wire-protocol discriminated union. See * `shared/protocol/messages.ts` for why. */ interface HandlerContext { sql: SqlStorage; state: DurableObjectState; yjsDocs: Map; getWebSockets(): Iterable; send(ws: WebSocket, message: ServerMessage): void; sendBinary(ws: WebSocket, data: Uint8Array): void; } /** * Collection Schema Definitions & Validation * * All collections use typed SQL columns. No document-mode / fields-based storage. */ interface ResolvedColumn { id: string; name: string; storage: 'number' | 'text'; interpretation: ColumnInterpretation; expression?: string; readonly: boolean; userBound?: boolean; immutable?: boolean; required?: boolean; default?: unknown; timestampTrigger?: { field: string; value?: unknown; }; } declare function collectionTableName(name: string): string; declare function columnId(name: string): string; declare function resolveColumn(col: ColumnDefinition): ResolvedColumn; declare function rowToData(row: Record, columns: ResolvedColumn[]): Record; declare function dataToColumnValues(data: Record, columns: ResolvedColumn[]): Record; declare function coerceValue(value: unknown, storage: 'number' | 'text', interpretation: ColumnInterpretation): unknown; /** Canonical numeric representation for date/datetime storage and triggers. */ declare function epochSeconds(date?: Date): number; declare function buildTableSelect(collectionName: string, columns: ResolvedColumn[]): string; interface User { id: string; email: string; name: string; imageUrl?: string; role: string; createdAt: string; lastSeenAt: string; } interface StoredRecord { collection: string; recordId: string; data: Record; createdBy: string; createdAt: string; updatedAt: string; } interface PermissionContext { isTeamMember: (teamId: string, userId: string) => boolean; } declare const noopPermissionContext: PermissionContext; declare function getRolePermissions(schema: CollectionSchema, role: string): RolePermissions; declare function isOwner(schema: CollectionSchema, record: { data: Record; createdBy: string; }, userId: string): boolean; declare function canRead(schema: CollectionSchema, role: string, record: { data: Record; createdBy: string; recordId?: string; }, userId: string, ctx?: PermissionContext): boolean; declare function canCreate(schema: CollectionSchema, role: string): boolean; declare function canUpdate(schema: CollectionSchema, role: string, record: { data: Record; createdBy: string; recordId?: string; }, userId: string, ctx?: PermissionContext): boolean; declare function canDelete(schema: CollectionSchema, role: string, record: { data: Record; createdBy: string; recordId?: string; }, userId: string, ctx?: PermissionContext): boolean; /** Check if a caller-supplied field write violates writableFields restrictions. */ declare function checkFieldPermissions(schema: CollectionSchema, role: string, newData: Record, existingData?: Record): string | null; /** * `unclaimed-or-own` lets a client claim an empty owner field, but never lets * them choose another user's id. Check the post-write record so the invariant * covers both creation and updates while still allowing an owner to unclaim. */ declare function checkUnclaimedOwnerTransition(schema: CollectionSchema, role: string, nextData: Record, userId: string): string | null; /** * Columns the system *assigns* — from the identity provider at registration, * or from an admin action. A caller who sends one of these has chosen a value * and means it, so a record write that would change one is refused loudly * rather than silently dropped. */ declare const SYSTEM_ASSIGNED_COLUMNS: Set; /** * Columns the system *maintains* on its own schedule, without telling anyone. * `registerUser` bumps `lastSeenAt` on every WS connect and `handleUserUpdate` * rewrites it on every 60s presence heartbeat; neither broadcasts, so every * client's copy goes stale silently and there is no heal path short of a * refetch. Read-modify-write echo (`put(id, { ...rec.data, field })`) is the * SDK's own house style, so refusing these would refuse writes over a value the * caller never chose and cannot keep current. They are preserved silently. */ declare const SYSTEM_MAINTAINED_COLUMNS: Set; /** Every `users` column owned by the system rather than by record writes. */ declare const SYSTEM_MANAGED_COLUMNS: Set; /** Standard user columns. Apps spread these into their users schema. */ declare const USERS_COLUMNS: ColumnDefinition[]; declare const BASE_USERS_SCHEMA: CollectionSchema; /** * Lint a CollectionSchema for declarations that look like they should * enforce something but don't, due to interactions between top-level * fields (visibilityField, ownerField) and per-role permission levels. * * The SDK has historically silently accepted schemas that imply more * enforcement than they actually deliver — e.g., `visibilityField` set * but every role's `read: true` means "anyone can read everything" * regardless of `visibility`. Warn loudly at registration so app * authors notice before shipping a privacy bug. * * Returns an array of warning messages. Empty = clean. */ declare function lintSchema(schema: CollectionSchema): string[]; /** * Lint a whole schema set: every per-schema warning, plus the rules that * need to see the set. The `'team'` permission level resolves membership from * a `team_members` collection (`teamId`, `userId`, `status`); without one it * denies everything, silently — a mistake that presents as a sync bug, not * as a schema error, so name it here. */ declare function lintSchemas(schemas: CollectionSchema[]): string[]; /** The rules that need to see the whole schema set (per-schema rules live in * `lintSchema`; `lintSchemas` composes both). */ declare function lintSchemaSet(schemas: CollectionSchema[]): string[]; declare class SchemaRegistry { private trusted; constructor(schemas?: CollectionSchema[]); registerTrusted(schema: CollectionSchema): void; get(name: string): CollectionSchema | undefined; has(name: string): boolean; all(): CollectionSchema[]; names(): string[]; } /** * RecordRoom Durable Object * * SQLite-based storage with query-based real-time subscriptions. * Extends BaseRoom for WebSocket/connection infrastructure. * * Architecture: * - Data stored in SQLite (single `records` table) * - Clients subscribe to QUERIES, not collections * - On record change, server evaluates which subscriptions match * - Only matching subscribers receive updates * * Protocol: * - SUBSCRIBE { subscriptionId, query } → QUERY_RESULT { subscriptionId, records } * - UNSUBSCRIBE { subscriptionId } * - PUT { collection, recordId, data } → broadcasts RECORD_CHANGE to matching * - DELETE { collection, recordId } → broadcasts RECORD_CHANGE to matching */ /** * RecordRoom configuration options */ interface RecordRoomConfig { /** * User ID of the app owner. * This user automatically gets 'admin' role on connect. */ ownerUserId?: string; } /** * RecordRoom Durable Object */ declare class RecordRoom> extends BaseRoom { private schemaRegistry; private initPromise; /** Yjs docs loaded in memory (key: collection:recordId:fieldName) */ private yjsDocs; /** Owner user ID — gets admin role automatically */ private ownerUserId; /** True until the first fetch() completes — detects hibernation wake-up */ private freshConstruct; /** * Per-connection `[DO Perf]` timing logs are noisy on every hot path, so * they're gated behind a `DEEPSPACE_DO_PERF` env binding (set it to any * truthy value on the worker to opt in). Off by default. */ private get perfLogEnabled(); /** * The HTTP debug API (`/api/debug/*`) runs arbitrary SQL and role changes * with no auth of its own, so it is gated here at the DO's single ingress. * Off unless a deployment opts in with `ALLOW_DEBUG_ROUTES=true` * (`deepspace dev start`/`test run` set it automatically). Deployments holding shared * data override this to always return false. */ protected get debugRoutesEnabled(): boolean; constructor(state: DurableObjectState, env: unknown, schemas?: CollectionSchema[], config?: RecordRoomConfig); private getPermissionContext; fetch(request: Request): Promise; /** Timing info from the current fetch(), used by onConnect for logging */ private _fetchTiming; private ensureInitialized; private initializeDatabase; private ensureCollectionTable; private ensureAllCollectionTables; protected onConnect(ws: WebSocket, user: UserAttachment): Promise; /** * Required to satisfy BaseRoom's abstract contract, but never invoked: * RecordRoom overrides `webSocketMessage`/`webSocketClose` directly (below), * so the BaseRoom dispatch path that would call this never runs. All real * message handling lives in `handleRecordMessage`. */ protected onMessage(): void; webSocketMessage(ws: WebSocket, message: ArrayBuffer | string): Promise; webSocketClose(ws: WebSocket, code: number, reason: string): Promise; private handleRecordMessage; private handleListSchemas; private createHandlerContext; private createRecordContext; private createUserContext; private createYjsContext; private send; private sendBinaryHelper; } /** * YjsRoom — Lightweight Durable Object for collaborative Yjs documents. * Extends BaseRoom for WebSocket/connection infrastructure. * * Unlike RecordRoom (schemas, RBAC, queries, user state), YjsRoom is * purpose-built for Yjs: sync, relay, persist. One DO per document. * * Architecture (SOTA for Yjs + Cloudflare DOs): * - Auth verified at the worker edge, role passed to DO via URL params * - DO is a thin Yjs sync relay: receive → apply → persist → broadcast * - Viewers can observe but not write; members/admins can write * - State persisted as a single binary blob in SQLite * * Uses the shared yjs-protocol.ts encoding utilities — no duplication. */ interface YjsAttachment extends UserAttachment { role: string; canWrite: boolean; awarenessClientId: number | null; } declare class YjsRoom> extends BaseRoom { private doc; private initialized; private awarenessStates; constructor(state: DurableObjectState, env: unknown); private ensureInitialized; private getDoc; private persistDoc; protected onConnect(ws: WebSocket, user: UserAttachment): YjsAttachment; protected onMessage(_ws: WebSocket, _user: UserAttachment, _message: { type: string; [key: string]: unknown; }): void; protected onBinaryMessage(ws: WebSocket, _user: UserAttachment, data: ArrayBuffer): void; protected onDisconnect(ws: WebSocket, _user: UserAttachment): void; private handleSync; private handleAwareness; private readAwarenessUpdates; private encodeAwarenessMessage; private sendAwarenessSnapshot; private broadcastRaw; } /** * CanvasRoom — Spatial canvas Durable Object (tldraw-style). * * Extends BaseRoom with Yjs-backed spatial operations. * Each shape is a Y.Map entry, enabling multi-user concurrent editing. * * Features: * - Shape CRUD (add, move, resize, delete, update properties) * - Viewport awareness (each user's visible region) * - Per-user undo/redo stacks * * Message types: canvas.* */ interface CanvasShape { id: string; type: string; x: number; y: number; width: number; height: number; rotation?: number; props: Record; createdBy: string; createdAt: string; updatedAt: string; } interface Viewport { userId: string; x: number; y: number; width: number; height: number; zoom: number; } interface CanvasAttachment extends UserAttachment { viewport: Viewport | null; /** True for member/admin roles; false for viewers and unauthenticated anon. */ canWrite: boolean; } declare class CanvasRoom> extends BaseRoom { private doc; private initialized; private viewports; private undoStacks; private redoStacks; constructor(state: DurableObjectState, env: unknown); private ensureInitialized; private getDoc; private persistDoc; private getShapesMap; fetch(request: Request): Promise; protected onConnect(ws: WebSocket, user: UserAttachment): CanvasAttachment; protected onMessage(ws: WebSocket, user: UserAttachment, message: { type: string; [key: string]: unknown; }): Promise; protected onDisconnect(_ws: WebSocket, user: UserAttachment): void; private pushUndo; private clearRedo; private handleUndo; private handleRedo; private getAllShapes; } /** * PresenceRoom — Ephemeral presence-tracking Durable Object. * * Extends BaseRoom. No SQLite — purely in-memory presence state. * Tracks who is present in a given scope (canvas, doc, thread, etc.) * and broadcasts join/leave/state-update events to all connected peers. * * Each scope ID maps to its own DO instance. Clients connect via * /ws/presence/:scopeId and receive real-time presence for that scope. * * Peers can attach small ephemeral state (cursor position, typing indicator, * viewport, selection, etc.) via MSG.PRESENCE_UPDATE. * * Message types: presence.* */ interface PresencePeer { userId: string; userName: string; joinedAt: string; /** Small per-user state (cursor, typing, viewport, etc.) */ state: Record; } interface PresenceAttachment extends UserAttachment { joinedAt: string; } declare class PresenceRoom> extends BaseRoom { private peers; private peerSockets; constructor(state: DurableObjectState, env: unknown); /** * Durable Objects can hibernate and clear heap while Cloudflare keeps * WebSocket connections. Deserialize attachments from already-connected * sockets so `peers` matches reality before we send PRESENCE_SYNC. */ private hydratePeersFromLiveSockets; protected onConnect(ws: WebSocket, user: UserAttachment): PresenceAttachment; protected onMessage(ws: WebSocket, user: UserAttachment, message: { type: string; [key: string]: unknown; }): Promise; protected onDisconnect(ws: WebSocket, user: UserAttachment): void; } /** Pure validation and evaluation for CronRoom schedules. */ interface CronTask { name: string; /** Interval in minutes (interval mode) — mutually exclusive with `schedule`. */ intervalMinutes?: number; /** 5-field cron expression (cron mode) — requires `timezone`. */ schedule?: string; /** IANA timezone string (e.g. "America/New_York"). Required with `schedule`. */ timezone?: string; /** Whether the task starts paused. */ paused?: boolean; } /** * CronRoom — Per-app scheduled task execution Durable Object. * * Extends BaseRoom. One DO per app shards cron work and avoids the * dispatch-worker's global KV-poll bottleneck. The DO alarm triggers * `onTask(name)` on the configured cadence; each execution is recorded * to a per-app `cron_history` table. Subscribers (admin clients via the * `useCronMonitor` hook) get pushes over the WebSocket. * * Tasks declare *either* `intervalMinutes` (run every N minutes) *or* * `schedule` + `timezone` (5-field cron expression evaluated against an * IANA timezone via `Intl.DateTimeFormat`). Cron mode is DST-aware * because the wall-clock comparison happens after the timezone shift, * not before. * * Message types: cron.* */ interface CronRoomConfig { tasks: CronTask[]; } interface CronExecution { taskName: string; startedAt: string; completedAt: string | null; success: boolean; durationMs: number; error?: string; } interface CronAttachment extends UserAttachment { /** True for member/admin roles; false for viewers and unauthenticated anon. */ canWrite: boolean; } declare abstract class CronRoom> extends BaseRoom { private tasks; private initialized; constructor(state: DurableObjectState, env: unknown, config: CronRoomConfig); private ensureInitialized; fetch(request: Request): Promise; protected handleInternalRequest(request: Request, url: URL): Promise; protected onConnect(ws: WebSocket, user: UserAttachment): CronAttachment; protected onMessage(ws: WebSocket, user: UserAttachment, message: { type: string; [key: string]: unknown; }): Promise; private dispatchMessage; protected onAlarm(): Promise; private executeTask; private scheduleNextAlarm; private getTaskStates; private getRecentHistory; private broadcastStatus; /** * Receipt for a mutation frame, addressed only to its sender. Receipts * are opt-in by correlation id: frames without a `requestId` keep the * fire-and-forget contract, so the (untyped) `requestId` from the wire * is the single gate here rather than a check at every call site. * Frames go through the shared `serverBuild` constructors so the wire * shape is enforced by the protocol types, not re-spelled here. */ private ackMutation; /** * Execute a scheduled task by name. * Called both by the alarm scheduler and manual trigger. */ protected abstract onTask(taskName: string): void | Promise; } /** * Wake the app's CronRoom so its alarm is armed. * * A Durable Object does not exist until something fetches it, and CronRoom * arms its alarm inside that first fetch — so a deployed schedule that no * client has opened yet runs nothing, and nothing reports it. The app worker * calls this from its request path (once per isolate is enough); the first * request the worker handles after a deploy then arms the schedule. A no-op * when the app declares no tasks. The wake runs under `ctx.waitUntil`, so it * never delays the response. */ declare function armCronRoom(ctx: { waitUntil(promise: Promise): void; }, namespace: DurableObjectNamespace, roomId: string, tasks: readonly CronTask[]): void; /** * JobRoom — Per-app durable background-job execution Durable Object. * * Solves the "long job dies when the response goes out" problem on * Cloudflare Workers. `ctx.waitUntil` only gets 30s after a response; * jobs that need minutes-to-hours live here instead: rows in SQLite, * picked up by DO alarms (15-min wall budget per tick), resumable * across ticks via `ctx.continue` for the rare longer cases. * * Lifecycle: queued → running → succeeded | failed | canceled * * Crash recovery: if an isolate is recycled mid-run, the row stays at * `running`. On next init, rows older than ~16 min are either retried * (if attempts left) or marked failed. * * See the abstract `onJob` method below for the subclass contract. * Message types: job.* */ type JobStatus = 'queued' | 'running' | 'succeeded' | 'failed' | 'canceled'; /** * Job record exposed to `onJob` handlers and clients. Payloads are typed * as `unknown` at the SDK layer; subclasses narrow via a generic param. */ interface Job

{ id: string; type: string; status: JobStatus; payload: P; result?: unknown; error?: string; progress?: number; progressMessage?: string; attempts: number; maxAttempts: number; enqueuedAt: string; startedAt?: string | null; completedAt?: string | null; enqueuedBy?: string | null; /** Set when a previous run called `ctx.continue(state)`. */ resumeFrom?: unknown; } interface JobContext { /** Set progress (0..1) with an optional message. Broadcasts. */ progress(value: number, message?: string): void; /** * Save a resumable checkpoint and re-run `onJob` on the next alarm * with `job.resumeFrom = state`. The return value of `onJob` is * ignored once `continue` has been called. */ continue(state: unknown, options?: { afterMs?: number; }): void; /** * Fires when a JOB_CANCEL arrives while this job is running in this * isolate. Cross-isolate cancels still mark the row `canceled` but * can't fire this signal. */ signal: AbortSignal; } interface JobRoomConfig { /** Default for jobs enqueued without explicit `maxAttempts`. Default 1 (no auto-retry). */ defaultMaxAttempts?: number; /** TTL for terminal rows (succeeded/failed/canceled) in ms. Default 24h. */ retentionMs?: number; /** Job count in the initial snapshot to a new subscriber. Default 100. */ snapshotLimit?: number; /** Delay before a failed attempt is retried, in ms. Default 1000. */ retryBackoffMs?: number; /** Re-check whether a connected user may mutate the queue. Defaults to member/admin roles. */ authorizeWrite?: (user: UserAttachment) => boolean | Promise; /** Re-check whether a connected user may observe the queue. Defaults to authorizeWrite. */ authorizeRead?: (user: UserAttachment) => boolean | Promise; } interface JobAttachment extends UserAttachment { canWrite: boolean; } declare abstract class JobRoom, P = unknown, R = unknown> extends BaseRoom { private initialized; private readonly defaultMaxAttempts; private readonly retentionMs; private readonly snapshotLimit; private readonly retryBackoffMs; private readonly authorizeWrite; private readonly authorizeRead; private broadcastQueue; /** In-flight jobs in this isolate; lets same-isolate JOB_CANCEL fire the signal. */ private readonly inFlight; /** Set by `ctx.continue` inside `executeJob`; reset before every run. */ private continueState; constructor(state: DurableObjectState, env: unknown, config?: JobRoomConfig); private ensureInitialized; fetch(request: Request): Promise; protected onConnect(ws: WebSocket, user: UserAttachment): Promise; protected onMessage(ws: WebSocket, user: UserAttachment, message: { type: string; [key: string]: unknown; }): Promise; protected onAlarm(): Promise; /** * One internal endpoint, `POST /enqueue`, used by the `enqueueJob` * helper at the bottom of this file. The room is not meant to be * reachable from the public internet; callers route through the app * worker (or call `this.enqueue(...)` directly from a subclass). * * Subclasses overriding `onRequest` should call `super.onRequest(req)` * for paths they don't handle. */ protected onRequest(request: Request): Promise; /** * Synchronously persist a new job and arm the alarm. Returns the new * job. This is the in-isolate path — useful for a subclass `onJob` * that wants to chain follow-up work without paying a fetch hop: * * protected async onJob(job: Job, ctx: JobContext) { * // ... do work ... * if (needsFollowup) this.enqueue('followup', { parent: job.id }) * } * * Workers code that lives outside the DO isolate (HTTP routes, cron * tasks, server actions) cannot call this directly — they must go * through the `enqueueJob` helper at the bottom of this file, which * routes via the DO's `/enqueue` HTTP endpoint. */ enqueue(type: string, payload?: unknown, options?: { maxAttempts?: number; enqueuedBy?: string; }): Job; private insertJob; private cancelJob; private retryJob; /** * Drain due jobs in FIFO order. All due jobs share the alarm's * 15-min wall budget (a CF property, not a per-job allocation). */ private drainDueJobs; private executeJob; private makeContext; private readStatus; /** Reauthorize every live event so role revocation does not leave a stale reader. */ protected broadcast(message: ServerMessage, exclude?: WebSocket): void; private resolveAccess; private scheduleNextAlarm; private recoverStuckRunning; private pruneExpired; private getRecentJobs; private getJobById; /** * Run a single job. Return a value for success; throw for failure * (retried until attempts >= maxAttempts). Use `ctx.progress` for * progress, `ctx.continue(state)` to span multiple alarms, and * `ctx.signal` to honor client cancellations. */ protected abstract onJob(job: Job

, ctx: JobContext): Promise | R | void; } /** * Enqueue a job from worker code that has the DO namespace. Routed by * `roomId` (typically `app:`; pass different ids for sharded * queues). Returns the new jobId; throws on failure. * * const jobId = await enqueueJob( * env.JOB_ROOMS, * `app:${env.APP_NAME}`, * 'ai-summarize', * { text }, * { maxAttempts: 3 }, * ) */ declare function enqueueJob(namespace: DurableObjectNamespace, roomId: string, type: string, payload?: unknown, options?: { maxAttempts?: number; enqueuedBy?: string; }): Promise; /** * DO Manifest — Dynamic Durable Object binding declarations. * * Apps export a `__DO_MANIFEST__` array in their worker.ts. * The CLI extracts it and sends it to the deploy worker, * which uses it to generate dynamic CF API bindings and migrations. */ interface DOManifestEntry { /** CF binding name, e.g. 'RECORD_ROOMS' */ binding: string; /** Exported class name, e.g. 'AppRecordRoom' */ className: string; /** Whether this DO uses SQLite storage */ sqlite: boolean; } type DOManifest = DOManifestEntry[]; /** * Utility type: auto-generates Env bindings from a manifest. * * @example * const manifest = [ * { binding: 'RECORD_ROOMS', className: 'AppRecordRoom', sqlite: true }, * ] as const satisfies DOManifest * * type Env = BaseEnv & DOBindings * // => { RECORD_ROOMS: DurableObjectNamespace } */ type DOBindings = { [K in T[number]['binding']]: DurableObjectNamespace; }; /** Default manifest for apps that don't declare one */ declare const DEFAULT_DO_MANIFEST: DOManifest; /** * Shape-validate a DO manifest received over the wire (e.g. from the CLI's * deploy form-field). Without this, malformed input gets passed straight to * `deployToWfP`'s `.filter(...).map(...)` chain and crashes the route mid-deploy. * * Mirrors the contract of `validateBindingManifest` for non-DO bindings. */ declare function validateDoManifest(manifest: unknown): { valid: true; manifest: DOManifest; } | { valid: false; reason: string; }; /** * Binding Manifest — non-DO bindings declared by an app's wrangler.toml that * the deploy-worker should pass through to Cloudflare's WfP upload API. * * Apps don't export a `__BINDING_MANIFEST__`; the CLI extracts these from the * normalized vite/wrangler output config at deploy time. This file just owns * the types + validation so both sides (CLI client and deploy-worker server) * agree on the shape. */ /** * A single non-DO binding the app declares. Mirrors CF's WfP binding API. * * Provisionable resources (d1, kv_namespace, vectorize, r2_bucket, queue) accept * the literal string `"auto"` in their ID field to request platform-side * provisioning at deploy time. When `"auto"` is used, the deploy-worker creates * the resource on the platform CF account, persists the resulting CF ID in the * app registry, and substitutes the real ID before forwarding to WfP. The * sentinel sticks around in this type because: * 1. `wrangler` parsing requires a non-empty string in the id field * 2. The CLI passes the unresolved manifest through to the deploy-worker * 3. The deploy-worker is the only side with CF API credentials * * Companion fields (`database_name`, `title`, `dimensions`, `metric`) are only * used when `"auto"` is set — they tell the provisioner how to create the * resource. After provisioning these fields are still present on the wire but * ignored by WfP. */ type CustomBinding = { type: 'plain_text'; name: string; text: string; } | { type: 'json'; name: string; json: BindingJsonValue; } | { /** Managed by DeepSpace; never forwarded into the customer Worker env. */ type: 'ai_search'; name: string; /** Only the untrusted `auto` sentinel is accepted from wrangler.toml. */ instance_name: string; } | { type: 'vectorize'; name: string; /** Either a pre-existing index name or the literal `"auto"`. */ index_name: string; /** Required when `index_name === "auto"`. */ dimensions?: number; /** Required when `index_name === "auto"`. */ metric?: 'cosine' | 'euclidean' | 'dot-product'; } | { type: 'ai'; name: string; } | { type: 'r2_bucket'; name: string; /** Either a pre-existing bucket name or the literal `"auto"`. */ bucket_name: string; } | { type: 'kv_namespace'; name: string; /** Either a pre-existing KV namespace ID or the literal `"auto"`. */ namespace_id: string; /** Required when `namespace_id === "auto"`. Human-readable namespace title. */ title?: string; } | { type: 'd1'; name: string; /** Either a pre-existing D1 database UUID or the literal `"auto"`. */ id: string; /** Required when `id === "auto"`. Human-readable database name. */ database_name?: string; } | { type: 'queue'; name: string; /** Either a pre-existing queue name or the literal `"auto"`. */ queue_name: string; } | { type: 'browser_rendering'; name: string; } | { type: 'analytics_engine'; name: string; dataset?: string; } | { type: 'hyperdrive'; name: string; id: string; } | { type: 'ratelimit'; name: string; namespace_id: string; simple: { limit: number; period: 10 | 60; }; }; type CustomBindingManifest = CustomBinding[]; /** Values Wrangler accepts for JSON environment-variable bindings. */ type BindingJsonValue = null | boolean | number | string | BindingJsonValue[] | { [key: string]: BindingJsonValue; }; /** Cloudflare's maximum UTF-8 payload for one environment variable. */ declare const MAX_ENV_VAR_BYTES: number; /** * Bound on names that become Cloudflare binding names verbatim (declared * bindings and app secrets). Cloudflare itself caps a binding name at 2712 * bytes but only rejects at deploy time — after a too-long secret name was * already accepted into the store, which bricks every later deploy. Refuse at * write time instead, with a bound no sane name exceeds. */ declare const MAX_BINDING_NAME_LENGTH = 256; /** One rule for user secret names, shared by the CLI's fail-fast validation * and the deploy-worker's authoritative store so the layers cannot drift. */ declare const SECRET_NAME_RE: RegExp; /** Sentinel string in an ID field that requests platform-side provisioning. */ declare const AUTO_PROVISION_SENTINEL = "auto"; /** Binding types whose ID field accepts the `"auto"` sentinel for provisioning. */ declare const AUTO_PROVISIONABLE_TYPES: Set; /** * True if a binding has the `"auto"` sentinel in its primary ID field. Used by * the deploy-worker to decide which entries need provisioning and by the * validator to enforce companion-field requirements. */ declare function isAutoProvision(b: CustomBinding): boolean; /** Binding `type` values an app is allowed to declare. */ declare const ALLOWED_BINDING_TYPES: Set; /** * Binding NAMES the SDK reserves on every app — apps may not redeclare them. * * Includes: * - Static-asset + service bindings the platform sets up automatically. * - SDK-managed env (auth, identity, owner JWT). * - The auto-attached cost-tracking AE dataset (`USAGE_EVENTS`). * * DO binding names (RECORD_ROOMS, YJS_ROOMS, etc.) are NOT in this set * because they live in a separate manifest (`__DO_MANIFEST__`). */ declare const RESERVED_BINDING_NAMES: Set; /** * Per-binding validation error. `binding` is undefined for top-level * shape failures (e.g. manifest is not an array). */ interface ValidationError { binding?: CustomBinding; reason: string; } /** * Validate a binding manifest. Returns errors; an empty array means valid. * * Used both client-side (CLI) for friendly fail-fast and server-side * (deploy-worker) as a security boundary — apps can't sneak in reserved * binding names by editing the wire format. */ declare function validateBindingManifest(manifest: unknown): { valid: true; bindings: CustomBindingManifest; } | { valid: false; errors: ValidationError[]; }; /** * Convert vite/wrangler's normalized config (from `.wrangler/deploy/config.json`) * into a CustomBindingManifest. * * Vite normalizes wrangler.toml into object/array structures with shapes like * `{ ai: { binding: 'AI' } }`, `{ vectorize: [{ binding, index_name }] }`, * etc. We extract each known shape with explicit field plucks (no broad * `as` casts) and return a flat array. */ declare function bindingManifestFromOutputConfig(outputConfig: Record): CustomBindingManifest; /** * Pure helpers for computing Cloudflare Durable Object migrations from a * declared manifest + the bindings already registered on a deployed script. * * Lives in the SDK (not deploy-worker) so the logic is testable with vitest * and reusable from other CF deploy paths if we ever add them. */ /** Subset of CF's `bindings` API response we read from. */ interface ExistingDOBinding { /** Binding name in `env`, e.g. 'RECORD_ROOMS' */ name: string; /** Always `'durable_object_namespace'` for DO bindings. */ type: string; /** SDK class name, e.g. 'AppRecordRoom' */ class_name?: string; } /** What goes in the CF script-upload `migrations` block. */ interface DoMigrationDirective { tag: string; new_sqlite_classes?: string[]; deleted_classes?: string[]; } interface DoMigrationPlan { /** New SQLite classes to register (present in manifest, absent in existing). */ newSqliteClasses: string[]; /** Classes to delete (present in existing, absent in manifest). */ deletedClasses: string[]; /** True when there's actual delta — only then should the migrations block be sent. */ needsMigration: boolean; /** The full directive to splat into the CF script-upload metadata. Null if no migration is needed. */ directive: DoMigrationDirective | null; } interface ComputeDoMigrationOptions { /** * Override for the timestamp baked into the migration tag. Tests pass a * fixed value to assert determinism; production omits this and gets * `Date.now()`, which guarantees lifetime tag uniqueness even across * cycles like `[A]→[A,B]→[A]→[A,B]`. */ now?: number; } /** * Compute the migration plan for a deploy. * * manifest: what the app declares now * existing: what CF currently has registered for this script * * Behavior: * - new_sqlite_classes ← in manifest, not in existing, sqlite=true * - deleted_classes ← in existing, not in manifest * - needsMigration ← either of the above is non-empty * - tag ← content-addressed by (add, remove) so: * - identical re-deploy → unchanged tag → no-op (and * `needsMigration` is false anyway) * - any class change → unique tag → CF processes * * Bug history: an earlier version computed `tag = v${count}`. Removing a class * dropped the count, the migration block was skipped (no NEW classes), and CF * retained the orphaned class registration with its SQLite storage. The * `deleted_classes` path closes that gap; the content-addressed tag prevents * tag collisions when class sets are added and removed in different orders. */ declare function computeDoMigration(manifest: readonly DOManifestEntry[], existing: readonly ExistingDOBinding[], options?: ComputeDoMigrationOptions): DoMigrationPlan; /** * App-name validation + sanitization helpers. * * Strategy: validate strictly so we have a precise definition of "valid", * but DON'T reject non-conforming names — sanitize them, warn the user, and * proceed. Hard rejection would break apps whose `wrangler.toml name` was * something like `My_App` (previously deployed as `my-app` via silent * server-side sanitization). The new behavior preserves "still deploys," * but the CLI now surfaces a warning so the user can fix the name when * convenient instead of being silently surprised by their hostname. * * Rules track Cloudflare's WfP script-name constraints (RFC 1035 host label, * no consecutive dashes) plus our 2-char minimum so subdomains read sensibly. */ declare const APP_NAME_RULES: { /** ^[a-z0-9](-?[a-z0-9])+$ — RFC 1035 host label, no consecutive dashes. */ readonly pattern: RegExp; readonly minLength: 2; readonly maxLength: 63; }; type AppNameValidation = { valid: true; name: string; } | { valid: false; reason: string; }; /** * Strict validation: returns valid only if the name already conforms. * Useful as a precondition test or for CI lints. */ declare function validateAppName(raw: unknown): AppNameValidation; type AppNameResolution = { ok: true; name: string; warning?: string; } | { ok: false; reason: string; }; /** * Resolve an app name for deploy: prefer the input as-is if valid, otherwise * sanitize and warn. Hard-fail only if even sanitization can't produce a * valid name (empty, all-non-alphanumeric, too short, too long). * * The intent is "what previously worked still works, with a friendly warning * about non-conforming names." */ declare function resolveAppName(raw: unknown): AppNameResolution; /** * AI Chat Schemas * * Pre-built collection schemas for DO-backed AI chat history. * The worker is the only writer; the client reads via `useQuery`. */ declare const AI_CHATS_SCHEMA: CollectionSchema; declare const AI_MESSAGES_SCHEMA: CollectionSchema; /** * Messaging Schemas * * Pre-built collection schemas for messaging functionality. * These schemas intentionally model public channels only. RecordRoom row * permissions cannot make a channel private by themselves; advertising DMs * or private groups here would expose their records to every room member. * * @example * ```typescript * import { CHANNELS_SCHEMA, MESSAGES_SCHEMA, REACTIONS_SCHEMA } from 'deepspace/worker' * export const schemas = [usersSchema, CHANNELS_SCHEMA, MESSAGES_SCHEMA, REACTIONS_SCHEMA] * ``` */ declare const CHANNELS_SCHEMA: CollectionSchema; declare const MESSAGES_SCHEMA: CollectionSchema; declare const REACTIONS_SCHEMA: CollectionSchema; declare const CHANNEL_MEMBERS_SCHEMA: CollectionSchema; declare const READ_RECEIPTS_SCHEMA: CollectionSchema; /** * Yjs Sync Protocol Implementation * * This implements the y-protocols sync and awareness protocols without * depending on y-websocket internal paths. Makes it easier to bundle * and template. * * Protocol spec: https://github.com/yjs/y-protocols */ declare const MSG_SYNC = 0; declare const MSG_AWARENESS = 1; declare const MSG_SYNC_STEP1 = 0; declare const MSG_SYNC_STEP2 = 1; declare const MSG_SYNC_UPDATE = 2; declare function createEncoder(): { data: number[]; }; declare function toUint8Array(encoder: { data: number[]; }): Uint8Array; declare function writeVarUint(encoder: { data: number[]; }, num: number): void; declare function writeVarUint8Array(encoder: { data: number[]; }, arr: Uint8Array): void; declare function createDecoder(data: Uint8Array): { data: Uint8Array; pos: number; }; declare function readVarUint(decoder: { data: Uint8Array; pos: number; }): number; declare function readVarUint8Array(decoder: { data: Uint8Array; pos: number; }): Uint8Array; declare function hasContent(decoder: { data: Uint8Array; pos: number; }): boolean; /** * Encode sync step 1: our state vector */ declare function encodeSyncStep1(doc: Y.Doc): Uint8Array; /** * Encode sync step 2: diff based on received state vector */ declare function encodeSyncStep2(doc: Y.Doc, stateVector: Uint8Array): Uint8Array; /** * Encode an incremental update */ declare function encodeUpdate(update: Uint8Array): Uint8Array; interface SyncResult { type: 'step1' | 'step2' | 'update'; response?: Uint8Array; stateVector?: Uint8Array; update?: Uint8Array; } /** * Handle an incoming sync message * Returns response to send (if any) and whether this was an update */ declare function handleSyncMessage(doc: Y.Doc, data: Uint8Array): SyncResult; interface AwarenessState { [key: string]: unknown; } interface AwarenessStates { [clientId: number]: AwarenessState; } /** * Simple awareness implementation for presence/cursors */ declare class Awareness { doc: Y.Doc; clientID: number; states: Map; private meta; private updateListeners; private changeListeners; constructor(doc: Y.Doc); getStates(): Map; getLocalState(): AwarenessState | null; setLocalState(state: AwarenessState | null): void; setLocalStateField(field: string, value: unknown): void; on(event: 'update' | 'change', handler: (changes: { added: number[]; updated: number[]; removed: number[]; }) => void): void; off(event: 'update' | 'change', handler: (changes: { added: number[]; updated: number[]; removed: number[]; }) => void): void; private emit; /** * Encode awareness update for specific clients */ encodeUpdate(clientIds?: number[]): Uint8Array; /** * Apply awareness update from another client */ applyUpdate(data: Uint8Array): { added: number[]; updated: number[]; removed: number[]; }; /** * Remove awareness states for disconnected clients */ removeStates(clientIds: number[]): void; /** Release all state and listeners owned by this awareness instance. */ destroy(): void; } /** * Encode a full awareness message */ declare function encodeAwarenessMessage(awareness: Awareness, clientIds?: number[]): Uint8Array; /** * Decode and apply awareness message */ declare function handleAwarenessMessage(awareness: Awareness, data: Uint8Array): { added: number[]; updated: number[]; removed: number[]; }; /** * Get message type from raw data */ declare function getMessageType(data: Uint8Array): number; /** Asset-layer misses became real 404s, and the worker owns the fallback. The * config and the worker's fallback path have to move together. */ declare const WORKER_OWNED_NOT_FOUND_MIGRATION_ID = "2026-08-worker-owned-not-found"; /** Room identity moved to internal headers and privileged room routes gained app-role checks. */ declare const SECURE_ROOM_BOUNDARIES_MIGRATION_ID = "2026-08-secure-room-boundaries"; /** `ActionTools` gained `deleteWhere`, so the app's own tools factory — which * declares that interface as its return type — has to supply it. */ declare const ACTION_TOOLS_DELETE_WHERE_MIGRATION_ID = "2026-08-action-tools-delete-where"; /** The browser's app id moved off a scaffold-time literal onto a build-time * define that `deepspace/build` reads from the wrangler config being built. */ declare const BUILD_INJECTED_APP_ID_MIGRATION_ID = "2026-08-build-injected-app-id"; /** The action route refuses a call without a bearer token as 401 (a cookie * session has none) instead of crashing on a non-null assertion, and the * file states the trust model actions run under. */ declare const ACTION_ROUTES_BEARER_GUARD_MIGRATION_ID = "2026-08-action-routes-bearer-guard"; /** The files proxy identifies a header-less same-origin read (an ``, * `