deepspace 0.6.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/worker.d.ts CHANGED
@@ -5,6 +5,7 @@ import * as zod from 'zod';
5
5
  import * as better_auth from 'better-auth';
6
6
  import { betterAuth } from 'better-auth';
7
7
  import * as better_auth_plugins from 'better-auth/plugins';
8
+ import { Hono } from 'hono';
8
9
 
9
10
  /**
10
11
  * Wire protocol constants.
@@ -898,6 +899,8 @@ declare function encode<M extends BaseMessage<string, unknown>>(message: M): str
898
899
  * - Raw SQLite access via this.sql
899
900
  * - Broadcast helpers: broadcast(), sendTo()
900
901
  * - HTTP fetch handler with WebSocket upgrade detection
902
+ * - Internal control-plane endpoint: POST /internal/disconnect-sockets
903
+ * (force every client to reconnect and resync after out-of-band writes)
901
904
  *
902
905
  * Subclasses implement lifecycle hooks:
903
906
  * onConnect, onMessage, onBinaryMessage, onDisconnect, onRequest, onAlarm
@@ -917,6 +920,21 @@ declare abstract class BaseRoom<E = Record<string, unknown>> {
917
920
  protected sql: SqlStorage;
918
921
  constructor(state: DurableObjectState, env: unknown);
919
922
  fetch(request: Request): Promise<Response>;
923
+ /**
924
+ * Handle built-in `/internal/*` control-plane routes shared by every room
925
+ * type. Returns a `Response` if the request was an internal route, or `null`
926
+ * to let the caller continue normal dispatch.
927
+ *
928
+ * Currently:
929
+ * - `POST /internal/disconnect-sockets` — close every live WebSocket so
930
+ * clients reconnect and resync. Optional JSON body `{ code?, reason? }`
931
+ * overrides the defaults (1012 / 'state-refresh'). Responds with
932
+ * `{ success: true, closed: <n> }`.
933
+ *
934
+ * See the security note on `fetch()`: this path is only reachable via DO
935
+ * stub fetch from the app worker, never from the public internet.
936
+ */
937
+ protected handleInternalRequest(request: Request, url: URL): Promise<Response | null>;
920
938
  private handleWebSocketUpgrade;
921
939
  webSocketMessage(ws: WebSocket, message: ArrayBuffer | string): Promise<void>;
922
940
  webSocketClose(ws: WebSocket, _code: number, _reason: string): Promise<void>;
@@ -955,6 +973,33 @@ declare abstract class BaseRoom<E = Record<string, unknown>> {
955
973
  * Get all connected WebSockets.
956
974
  */
957
975
  protected getWebSockets(): WebSocket[];
976
+ /**
977
+ * Force every connected client to reconnect and resync by closing all live
978
+ * WebSockets. Returns the number of sockets closed.
979
+ *
980
+ * Use this after an **out-of-band, server-side write** to the room's records
981
+ * — an admin import route, a migration script, a cron job, or a server
982
+ * action — that connected clients have no way to learn about. Without it, a
983
+ * browser tab holding stale in-memory state keeps operating on (and may
984
+ * autosave over) data that changed underneath it. Closing the socket makes
985
+ * the client SDK reconnect and pull fresh query results.
986
+ *
987
+ * The default close code is 1012 ("service restart") with reason
988
+ * 'state-refresh'. The DeepSpace client treats *any* close as a reconnect
989
+ * trigger (it does not special-case clean/1000 closes), so on reconnect it
990
+ * re-subscribes every active query and receives fresh `QUERY_RESULT`s —
991
+ * `useQuery` consumers see the new data without re-subscribing.
992
+ *
993
+ * Each close is guarded so one already-closing socket can't abort the sweep.
994
+ *
995
+ * @param options.code WebSocket close code (default 1012).
996
+ * @param options.reason WebSocket close reason (default 'state-refresh').
997
+ * @returns the number of sockets that were closed.
998
+ */
999
+ disconnectAllSockets(options?: {
1000
+ code?: number;
1001
+ reason?: string;
1002
+ }): number;
958
1003
  /**
959
1004
  * Get the user attachment for a WebSocket.
960
1005
  */
@@ -1401,6 +1446,14 @@ declare class RecordRoom<E = Record<string, unknown>> extends BaseRoom<E> {
1401
1446
  * truthy value on the worker to opt in). Off by default.
1402
1447
  */
1403
1448
  private get perfLogEnabled();
1449
+ /**
1450
+ * The HTTP debug API (`/api/debug/*`) runs arbitrary SQL and role changes
1451
+ * with no auth of its own, so it is gated here at the DO's single ingress.
1452
+ * Off unless a deployment opts in with `ALLOW_DEBUG_ROUTES=true`
1453
+ * (`deepspace dev`/`test` set it automatically). Deployments holding shared
1454
+ * data override this to always return false.
1455
+ */
1456
+ protected get debugRoutesEnabled(): boolean;
1404
1457
  constructor(state: DurableObjectState, env: unknown, schemas?: CollectionSchema[], config?: RecordRoomConfig);
1405
1458
  private getPermissionContext;
1406
1459
  fetch(request: Request): Promise<Response>;
@@ -2955,7 +3008,7 @@ declare function createDeepSpaceAI(env: DeepSpaceAIEnv, provider: Provider, opti
2955
3008
  * const jwt = c.req.header('Authorization')!.slice(7)
2956
3009
  * const tools = await composioTools(c.env, { toolkit: 'gmail', authToken: jwt })
2957
3010
  * const ai = createDeepSpaceAI(c.env, 'anthropic', { authToken: jwt })
2958
- * const result = streamText({ model: ai('claude-sonnet-4-6'), prompt, tools })
3011
+ * const result = streamText({ model: ai('claude-sonnet-5'), prompt, tools })
2959
3012
  *
2960
3013
  * Merge with your own tools: `tools: { ...buildTools(executor), ...composio }`.
2961
3014
  */
@@ -4446,4 +4499,148 @@ type DeepSpaceAuth = ReturnType<typeof createDeepSpaceAuth>;
4446
4499
  /** Collections that bypass schema checks (permissive access) */
4447
4500
  declare const SYSTEM_COLLECTIONS: Set<string>;
4448
4501
 
4449
- export { AI_CHATS_SCHEMA, AI_MESSAGES_SCHEMA, ALLOWED_BINDING_TYPES, APP_NAME_RULES, AUTO_PROVISIONABLE_TYPES, AUTO_PROVISION_SENTINEL, type ActionContext, type ActionHandler, type ActionResult, type ActionTools, type ApiWorkerEnv, type AppNameResolution, type AppNameValidation, type AuthWorkerEnv, Awareness, type AwarenessState, type AwarenessStates, BASE_USERS_SCHEMA, BUILT_IN_TOOLS, type BaseMessage, BaseRoom, CHANNELS_SCHEMA, CHANNEL_INVITATIONS_SCHEMA, CHANNEL_MEMBERS_SCHEMA, CONVERSATION_SCHEMAS, COST_RATES, CanvasRoom, type CanvasShape, type ChatContextConfig, type ChatMessageRow, type ChatRow, type ChatTurn, type ClientMessage, type CollectionPermissionSummary, type CollectionSchema, type ColumnDefinition, type ColumnInterpretation, type ComposioToolsOptions, type ConnectionAttachment, type ConvMemberData, type ConvMessageData, type ConvReactionData, type ConvReadCursorData, type ConvVoteData, type ConversationStateData, type CronContext, type CronExecution, CronRoom, type CronRoomConfig, type CronTask, type CustomBinding, type CustomBindingManifest, DEFAULT_CONTEXT_CONFIG, DEFAULT_DO_MANIFEST, DEFAULT_QUERY_LIMIT, DIRECTORY_SCHEMAS, type DOBindings, type DOManifest, type DOManifestEntry, type DeepSpaceAIEnv, type DeepSpaceAIOptions, type DeepSpaceAuth, type DeepSpaceAuthConfig, type DirectoryCommunityData, type DirectoryConversationData, type DirectoryMembershipData, type DirectoryPostData, type DoMigrationDirective, type DoMigrationPlan, type ExistingDOBinding, GLOBAL_DO_TYPES, GLOBAL_DO_TYPE_NAMES, type GameInput, GameRoom, type GameRoomConfig, type GetActionData, type GlobalDOType, type HandlerContext, type Job, type JobContext, JobRoom, type JobRoomConfig, type JobStatus, type JwtClaims, type JwtVerifierConfig, MESSAGES_SCHEMA, MSG, MSG_AWARENESS, MSG_SYNC, MSG_SYNC_STEP1, MSG_SYNC_STEP2, MSG_SYNC_UPDATE, MSG_YJS_AWARENESS, MSG_YJS_SYNC, type MessageHandlers, type MsgType, type MutateActionData, type PermissionAnalysis, type PermissionContext, type PermissionLevel, type PermissionSource, type PlatformWorkerEnv, type Player, type PrefixResult, type PresencePeer, PresenceRoom, type ProtocolMessage, type QueryActionData, REACTIONS_SCHEMA, READ_RECEIPTS_SCHEMA, RESERVED_BINDING_NAMES, RESERVED_COLLECTION_NAMES, ROLE_ADMIN, ROLE_ANONYMOUS, ROLE_DEFAULT, RecordRoom, type RecordRoomConfig, type ResolvedColumn, type ResolvedPermission, type RolePermissions, type RunMigrationsResult, SYSTEM_COLLECTIONS, SYSTEM_MANAGED_COLUMNS, SchemaRegistry, type ScopeContext, type ScopedR2Auth, type ScopedR2Config, type ScopedR2Handler, type ScreenshotEnv, type ScreenshotOptions, type ScreenshotResult, type ServerMessage, type SharedConnection, type StoredRecord, type Summarizer, type SyncResult, TEST_ACCOUNT_EMAIL_SUFFIX, type TokenDebugInfo, type ToolResult, type ToolSchema, USERS_COLUMNS, type User, type UserAttachment, VOTING_SCHEMAS, type ValidationError, type VerifiedAuth, type VerifyOutcome, type VerifyResult, type Viewport, WORKSPACE_SCHEMAS, YjsRoom, analyzePermissions, apiWorkerFetch, appendMessage, applyAiToolDefaults, applySlidingWindow, authWorkerFetch, bindingManifestFromOutputConfig, buildCronContext, buildTableSelect, buildUiParts, canCreate, canDelete, canRead, canUpdate, capToolResultSize, captureScreenshot, checkFieldPermissions, clientBuild, coerceValue, collectionTableName, columnId, composioTools, computeDoMigration, computeHmacHex, createChat, createDecoder, createDeepSpaceAI, createDeepSpaceAuth, createEncoder, createScopedR2Handler, dataToColumnValues, decodeJwtPayload, deleteChatCascade, dispatch, encode, encodeAwarenessMessage, encodeSyncStep1, encodeSyncStep2, encodeUpdate, enqueueJob, getChat, getGlobalDOSchemas, getGlobalDOType, getMessageType, getRolePermissions, handleAwarenessMessage, handleSyncMessage, hasContent, isAutoProvision, isOwner, isTestAccountClaims, isTestAccountEmail, isTestAccountTier, lintSchema, loadMessages, makeDefaultSummarizer, meterAi, meterUsage, meterVectorize, noopPermissionContext, platformWorkerFetch, prepareMessagesWithCompaction, priceBindingUsageEvent, readVarUint, readVarUint8Array, resolveAppName, resolveColumn, rowToData, runMigrations, serverBuild, timingSafeEqualHex, timingSafeEqualStrings, toUint8Array, totalChars, truncateOldToolResults, turnsToCoreMessages, unwrapToolOutput, updateChat, validateAppName, validateBindingManifest, validateDoManifest, verifyJwt, workspaceAccountsSchema, workspaceContentSharesSchema, workspaceEmailHandlesSchema, workspaceFormResponsesSchema, workspacePeopleSchema, workspaceProjectsSchema, workspaceTagsSchema, workspaceTasksSchema, workspaceTeamMembersSchema, workspaceTeamsSchema, workspaceTransactionsSchema, writeVarUint, writeVarUint8Array };
4502
+ /**
4503
+ * Client-error ingestion route (opt-in) — the Worker half of client-side
4504
+ * error reporting.
4505
+ *
4506
+ * Mount it in your app Worker with a single line:
4507
+ *
4508
+ * import { registerClientErrorRoute } from 'deepspace/worker'
4509
+ * registerClientErrorRoute(app) // BEFORE the app.all('/_deepspace/*') proxy
4510
+ *
4511
+ * Then, in the browser, `installClientErrorReporter()` (from 'deepspace')
4512
+ * forwards uncaught errors here. The handler runs in the app's OWN Worker, so
4513
+ * the `console.error` it emits is tagged by Cloudflare with this app's id —
4514
+ * meaning a browser can only ever write to its own app's log stream, never
4515
+ * another tenant's or the platform's. Reports then surface in `deepspace logs`
4516
+ * (and the dashboard) tagged `CLIENT`.
4517
+ *
4518
+ * Anonymous by design (logged-out visitors hit client errors too), so the
4519
+ * defenses are size caps (normalizeClientErrorReport) + a best-effort
4520
+ * per-isolate throttle. It never blocks the page: it returns 204 and swallows
4521
+ * work quickly.
4522
+ */
4523
+
4524
+ /**
4525
+ * Handle one client-error report. Framework-agnostic — pass a raw `Request`.
4526
+ * Returns 204 on success, 400 on a malformed/empty body, 429 when throttled.
4527
+ */
4528
+ declare function handleClientErrorReport(request: Request): Promise<Response>;
4529
+ /**
4530
+ * Register `POST /_deepspace/client-errors` on your Hono app. Mount it BEFORE
4531
+ * the `app.all('/_deepspace/*')` proxy so the specific route wins the match.
4532
+ */
4533
+ declare function registerClientErrorRoute(app: Hono<any, any, any>): void;
4534
+
4535
+ /**
4536
+ * Client-side error reporting — shared contract.
4537
+ *
4538
+ * Browser JS errors never invoke the app's Worker, so they never reach
4539
+ * Workers Logs and are invisible to `deepspace logs`. The opt-in reporter
4540
+ * (`installClientErrorReporter`, client entry) forwards them to the app's own
4541
+ * Worker via `registerClientErrorRoute` (worker entry), which `console.error`s
4542
+ * a marker-prefixed envelope. The platform log reader recognises the marker
4543
+ * and tags the event `source: 'client'`.
4544
+ *
4545
+ * This module is isomorphic — no DOM, no Hono — so both halves import the
4546
+ * exact same marker, caps, and normaliser and can never drift.
4547
+ */
4548
+ /**
4549
+ * Prefix the Worker route prepends before JSON-stringifying a report into a
4550
+ * single `console.error` line. The platform mapper
4551
+ * (platform/deploy-worker/src/lib/workers-logs.ts) detects this prefix to set
4552
+ * `source: 'client'`. Deliberately distinctive so an ordinary app log is
4553
+ * vanishingly unlikely to collide. The marker is added SERVER-SIDE by the app
4554
+ * Worker — a browser cannot forge it onto a genuine server log.
4555
+ *
4556
+ * The platform copy is kept in sync by a drift test; if you change this
4557
+ * string, that test fails until the platform const matches.
4558
+ */
4559
+ declare const CLIENT_LOG_MARKER = "[deepspace:client-error]";
4560
+ type ClientErrorKind = 'error' | 'unhandledrejection' | 'react';
4561
+ /** A sanitized, capped browser-error report — the wire + log-line payload. */
4562
+ interface ClientErrorReport {
4563
+ kind: ClientErrorKind;
4564
+ message: string;
4565
+ name?: string;
4566
+ stack?: string;
4567
+ /** Page URL where it fired (href or the script filename). */
4568
+ url?: string;
4569
+ line?: number;
4570
+ col?: number;
4571
+ }
4572
+
4573
+ /**
4574
+ * `deepspace logs` wire DTO — the single source of truth for the whitelisted
4575
+ * shape the platform telemetry reader emits and every consumer reads.
4576
+ *
4577
+ * The producer is `platform/deploy-worker/src/lib/workers-logs.ts` (which
4578
+ * rebuilds each event field-by-field from a raw Cloudflare row — see the
4579
+ * security invariant there). The consumers are the CLI tail
4580
+ * (`cli/commands/logs.ts`), the dashboard Logs panel
4581
+ * (`apps/dashboard/src/lib/types.ts`), and the e2e whitelist assertion
4582
+ * (`tests/e2e/tests/logs.spec.ts`). This module used to be hand-mirrored in
4583
+ * all four places; `source` was added to three of them and missed in the
4584
+ * e2e whitelist, so it now lives here once and they import it.
4585
+ *
4586
+ * Isomorphic — no DOM, no Hono, no platform types — so it re-exports cleanly
4587
+ * from both `deepspace` (client) and `deepspace/worker` and is safe to import
4588
+ * from the private deploy-worker.
4589
+ */
4590
+ declare const LOG_LEVELS: readonly ["debug", "log", "info", "warn", "error"];
4591
+ type LogLevel = (typeof LOG_LEVELS)[number];
4592
+ /** One log / request / exception event, rebuilt field-by-field from a CF row. */
4593
+ interface AppLogEvent {
4594
+ /** `$metadata.id` — unique per event; the client's dedupe key. */
4595
+ id: string;
4596
+ /** Unix ms. */
4597
+ timestamp: number;
4598
+ level: LogLevel;
4599
+ eventType: 'log' | 'request' | 'exception' | 'other';
4600
+ message: string;
4601
+ /**
4602
+ * Present (= `'client'`) only for a browser-forwarded error the app's own
4603
+ * Worker re-logged via `registerClientErrorRoute`. Absent means the log was
4604
+ * emitted server-side — there is no explicit `'server'` value, so consumers
4605
+ * branch on `source === 'client'`, never on a truthy/other check.
4606
+ */
4607
+ source?: 'client';
4608
+ /** Invocation outcome (`ok`, `exception`, `exceededCpu`, …) when present. */
4609
+ outcome?: string;
4610
+ request?: {
4611
+ method: string;
4612
+ path: string;
4613
+ status?: number;
4614
+ };
4615
+ exception?: {
4616
+ name: string;
4617
+ message: string;
4618
+ stack?: string;
4619
+ };
4620
+ }
4621
+ interface AppLogsResponse {
4622
+ /** Ascending by timestamp (CF returns newest-first; the reader flips). */
4623
+ events: AppLogEvent[];
4624
+ /** Max event timestamp in ms — pass back as `since` to poll; null when empty. */
4625
+ cursor: number | null;
4626
+ /** True when the window returned `limit` events (older events were cut). */
4627
+ truncated: boolean;
4628
+ pollIntervalMs: number;
4629
+ }
4630
+ /**
4631
+ * Runtime tuple of every key a whitelisted `AppLogEvent` may carry — the
4632
+ * canonical whitelist the e2e suite mirrors when asserting no event leaks a key
4633
+ * outside this set. The two type-level assertions below fail compilation if
4634
+ * this tuple and the `AppLogEvent` interface disagree in either direction, so
4635
+ * adding an interface field forces a matching key here.
4636
+ *
4637
+ * NOTE: the e2e spec keeps its OWN copy of this set (it can't import the SDK —
4638
+ * `@deepspace/e2e` has no `deepspace` dependency), kept in step by a comment,
4639
+ * not by these compile-time guards. So this tuple hardens the code copies
4640
+ * (deploy-worker/CLI/dashboard, which import it), while the e2e mirror remains a
4641
+ * knowingly-accepted duplicate.
4642
+ */
4643
+ declare const APP_LOG_EVENT_KEYS: readonly ["id", "timestamp", "level", "eventType", "message", "source", "outcome", "request", "exception"];
4644
+ type AppLogEventKey = (typeof APP_LOG_EVENT_KEYS)[number];
4645
+
4646
+ export { AI_CHATS_SCHEMA, AI_MESSAGES_SCHEMA, ALLOWED_BINDING_TYPES, APP_LOG_EVENT_KEYS, APP_NAME_RULES, AUTO_PROVISIONABLE_TYPES, AUTO_PROVISION_SENTINEL, type ActionContext, type ActionHandler, type ActionResult, type ActionTools, type ApiWorkerEnv, type AppLogEvent, type AppLogEventKey, type AppLogsResponse, type AppNameResolution, type AppNameValidation, type AuthWorkerEnv, Awareness, type AwarenessState, type AwarenessStates, BASE_USERS_SCHEMA, BUILT_IN_TOOLS, type BaseMessage, BaseRoom, CHANNELS_SCHEMA, CHANNEL_INVITATIONS_SCHEMA, CHANNEL_MEMBERS_SCHEMA, CLIENT_LOG_MARKER, CONVERSATION_SCHEMAS, COST_RATES, CanvasRoom, type CanvasShape, type ChatContextConfig, type ChatMessageRow, type ChatRow, type ChatTurn, type ClientErrorKind, type ClientErrorReport, type ClientMessage, type CollectionPermissionSummary, type CollectionSchema, type ColumnDefinition, type ColumnInterpretation, type ComposioToolsOptions, type ConnectionAttachment, type ConvMemberData, type ConvMessageData, type ConvReactionData, type ConvReadCursorData, type ConvVoteData, type ConversationStateData, type CronContext, type CronExecution, CronRoom, type CronRoomConfig, type CronTask, type CustomBinding, type CustomBindingManifest, DEFAULT_CONTEXT_CONFIG, DEFAULT_DO_MANIFEST, DEFAULT_QUERY_LIMIT, DIRECTORY_SCHEMAS, type DOBindings, type DOManifest, type DOManifestEntry, type DeepSpaceAIEnv, type DeepSpaceAIOptions, type DeepSpaceAuth, type DeepSpaceAuthConfig, type DirectoryCommunityData, type DirectoryConversationData, type DirectoryMembershipData, type DirectoryPostData, type DoMigrationDirective, type DoMigrationPlan, type ExistingDOBinding, GLOBAL_DO_TYPES, GLOBAL_DO_TYPE_NAMES, type GameInput, GameRoom, type GameRoomConfig, type GetActionData, type GlobalDOType, type HandlerContext, type Job, type JobContext, JobRoom, type JobRoomConfig, type JobStatus, type JwtClaims, type JwtVerifierConfig, LOG_LEVELS, type LogLevel, MESSAGES_SCHEMA, MSG, MSG_AWARENESS, MSG_SYNC, MSG_SYNC_STEP1, MSG_SYNC_STEP2, MSG_SYNC_UPDATE, MSG_YJS_AWARENESS, MSG_YJS_SYNC, type MessageHandlers, type MsgType, type MutateActionData, type PermissionAnalysis, type PermissionContext, type PermissionLevel, type PermissionSource, type PlatformWorkerEnv, type Player, type PrefixResult, type PresencePeer, PresenceRoom, type ProtocolMessage, type QueryActionData, REACTIONS_SCHEMA, READ_RECEIPTS_SCHEMA, RESERVED_BINDING_NAMES, RESERVED_COLLECTION_NAMES, ROLE_ADMIN, ROLE_ANONYMOUS, ROLE_DEFAULT, RecordRoom, type RecordRoomConfig, type ResolvedColumn, type ResolvedPermission, type RolePermissions, type RunMigrationsResult, SYSTEM_COLLECTIONS, SYSTEM_MANAGED_COLUMNS, SchemaRegistry, type ScopeContext, type ScopedR2Auth, type ScopedR2Config, type ScopedR2Handler, type ScreenshotEnv, type ScreenshotOptions, type ScreenshotResult, type ServerMessage, type SharedConnection, type StoredRecord, type Summarizer, type SyncResult, TEST_ACCOUNT_EMAIL_SUFFIX, type TokenDebugInfo, type ToolResult, type ToolSchema, USERS_COLUMNS, type User, type UserAttachment, VOTING_SCHEMAS, type ValidationError, type VerifiedAuth, type VerifyOutcome, type VerifyResult, type Viewport, WORKSPACE_SCHEMAS, YjsRoom, analyzePermissions, apiWorkerFetch, appendMessage, applyAiToolDefaults, applySlidingWindow, authWorkerFetch, bindingManifestFromOutputConfig, buildCronContext, buildTableSelect, buildUiParts, canCreate, canDelete, canRead, canUpdate, capToolResultSize, captureScreenshot, checkFieldPermissions, clientBuild, coerceValue, collectionTableName, columnId, composioTools, computeDoMigration, computeHmacHex, createChat, createDecoder, createDeepSpaceAI, createDeepSpaceAuth, createEncoder, createScopedR2Handler, dataToColumnValues, decodeJwtPayload, deleteChatCascade, dispatch, encode, encodeAwarenessMessage, encodeSyncStep1, encodeSyncStep2, encodeUpdate, enqueueJob, getChat, getGlobalDOSchemas, getGlobalDOType, getMessageType, getRolePermissions, handleAwarenessMessage, handleClientErrorReport, handleSyncMessage, hasContent, isAutoProvision, isOwner, isTestAccountClaims, isTestAccountEmail, isTestAccountTier, lintSchema, loadMessages, makeDefaultSummarizer, meterAi, meterUsage, meterVectorize, noopPermissionContext, platformWorkerFetch, prepareMessagesWithCompaction, priceBindingUsageEvent, readVarUint, readVarUint8Array, registerClientErrorRoute, resolveAppName, resolveColumn, rowToData, runMigrations, serverBuild, timingSafeEqualHex, timingSafeEqualStrings, toUint8Array, totalChars, truncateOldToolResults, turnsToCoreMessages, unwrapToolOutput, updateChat, validateAppName, validateBindingManifest, validateDoManifest, verifyJwt, workspaceAccountsSchema, workspaceContentSharesSchema, workspaceEmailHandlesSchema, workspaceFormResponsesSchema, workspacePeopleSchema, workspaceProjectsSchema, workspaceTagsSchema, workspaceTasksSchema, workspaceTeamMembersSchema, workspaceTeamsSchema, workspaceTransactionsSchema, writeVarUint, writeVarUint8Array };
package/dist/worker.js CHANGED
@@ -16,6 +16,8 @@ var BaseRoom = class {
16
16
  // ==========================================================================
17
17
  async fetch(request) {
18
18
  const url = new URL(request.url);
19
+ const internal = await this.handleInternalRequest(request, url);
20
+ if (internal) return internal;
19
21
  if (request.headers.get("Upgrade") === "websocket") {
20
22
  return this.handleWebSocketUpgrade(request, url);
21
23
  }
@@ -25,6 +27,36 @@ var BaseRoom = class {
25
27
  return new Response("Not Found", { status: 404 });
26
28
  }
27
29
  // ==========================================================================
30
+ // Internal Control-Plane Endpoints
31
+ // ==========================================================================
32
+ /**
33
+ * Handle built-in `/internal/*` control-plane routes shared by every room
34
+ * type. Returns a `Response` if the request was an internal route, or `null`
35
+ * to let the caller continue normal dispatch.
36
+ *
37
+ * Currently:
38
+ * - `POST /internal/disconnect-sockets` — close every live WebSocket so
39
+ * clients reconnect and resync. Optional JSON body `{ code?, reason? }`
40
+ * overrides the defaults (1012 / 'state-refresh'). Responds with
41
+ * `{ success: true, closed: <n> }`.
42
+ *
43
+ * See the security note on `fetch()`: this path is only reachable via DO
44
+ * stub fetch from the app worker, never from the public internet.
45
+ */
46
+ async handleInternalRequest(request, url) {
47
+ if (request.method === "POST" && url.pathname === "/internal/disconnect-sockets") {
48
+ let options;
49
+ try {
50
+ const text = await request.text();
51
+ if (text) options = JSON.parse(text);
52
+ } catch {
53
+ }
54
+ const closed = this.disconnectAllSockets(options);
55
+ return Response.json({ success: true, closed });
56
+ }
57
+ return null;
58
+ }
59
+ // ==========================================================================
28
60
  // WebSocket Connection
29
61
  // ==========================================================================
30
62
  async handleWebSocketUpgrade(request, url) {
@@ -111,6 +143,42 @@ var BaseRoom = class {
111
143
  getWebSockets() {
112
144
  return this.state.getWebSockets();
113
145
  }
146
+ /**
147
+ * Force every connected client to reconnect and resync by closing all live
148
+ * WebSockets. Returns the number of sockets closed.
149
+ *
150
+ * Use this after an **out-of-band, server-side write** to the room's records
151
+ * — an admin import route, a migration script, a cron job, or a server
152
+ * action — that connected clients have no way to learn about. Without it, a
153
+ * browser tab holding stale in-memory state keeps operating on (and may
154
+ * autosave over) data that changed underneath it. Closing the socket makes
155
+ * the client SDK reconnect and pull fresh query results.
156
+ *
157
+ * The default close code is 1012 ("service restart") with reason
158
+ * 'state-refresh'. The DeepSpace client treats *any* close as a reconnect
159
+ * trigger (it does not special-case clean/1000 closes), so on reconnect it
160
+ * re-subscribes every active query and receives fresh `QUERY_RESULT`s —
161
+ * `useQuery` consumers see the new data without re-subscribing.
162
+ *
163
+ * Each close is guarded so one already-closing socket can't abort the sweep.
164
+ *
165
+ * @param options.code WebSocket close code (default 1012).
166
+ * @param options.reason WebSocket close reason (default 'state-refresh').
167
+ * @returns the number of sockets that were closed.
168
+ */
169
+ disconnectAllSockets(options) {
170
+ const code = options?.code ?? 1012;
171
+ const reason = options?.reason ?? "state-refresh";
172
+ let closed = 0;
173
+ for (const ws of this.state.getWebSockets()) {
174
+ try {
175
+ ws.close(code, reason);
176
+ closed++;
177
+ } catch {
178
+ }
179
+ }
180
+ return closed;
181
+ }
114
182
  /**
115
183
  * Get the user attachment for a WebSocket.
116
184
  */
@@ -2670,6 +2738,16 @@ var RecordRoom = class extends BaseRoom {
2670
2738
  get perfLogEnabled() {
2671
2739
  return !!this.env?.DEEPSPACE_DO_PERF;
2672
2740
  }
2741
+ /**
2742
+ * The HTTP debug API (`/api/debug/*`) runs arbitrary SQL and role changes
2743
+ * with no auth of its own, so it is gated here at the DO's single ingress.
2744
+ * Off unless a deployment opts in with `ALLOW_DEBUG_ROUTES=true`
2745
+ * (`deepspace dev`/`test` set it automatically). Deployments holding shared
2746
+ * data override this to always return false.
2747
+ */
2748
+ get debugRoutesEnabled() {
2749
+ return this.env?.ALLOW_DEBUG_ROUTES === "true";
2750
+ }
2673
2751
  constructor(state, env, schemas = [], config = {}) {
2674
2752
  super(state, env);
2675
2753
  this.schemaRegistry = new SchemaRegistry([
@@ -2705,14 +2783,21 @@ var RecordRoom = class extends BaseRoom {
2705
2783
  // HTTP Entry Point (overrides BaseRoom.fetch)
2706
2784
  // ============================================================================
2707
2785
  async fetch(request) {
2786
+ const url = new URL(request.url);
2787
+ if (url.pathname.startsWith("/internal/")) {
2788
+ const internal = await this.handleInternalRequest(request, url);
2789
+ if (internal) return internal;
2790
+ }
2708
2791
  const fetchStart = Date.now();
2709
2792
  const isColdStart = this.freshConstruct;
2710
2793
  const needsInit = !this.initPromise;
2711
2794
  this.freshConstruct = false;
2712
2795
  await this.ensureInitialized();
2713
2796
  const initMs = Date.now() - fetchStart;
2714
- const url = new URL(request.url);
2715
2797
  if (url.pathname.startsWith("/api/")) {
2798
+ if (url.pathname.startsWith("/api/debug/") && !this.debugRoutesEnabled) {
2799
+ return new Response("Not Found", { status: 404 });
2800
+ }
2716
2801
  return handleApiRequest(this.createHandlerContext(), request, url);
2717
2802
  }
2718
2803
  if (request.headers.get("Upgrade") === "websocket") {
@@ -7232,10 +7317,111 @@ async function generateAppleClientSecret(apple) {
7232
7317
  cachedAppleSecret = { keyId: apple.keyId, token, expiresAt };
7233
7318
  return token;
7234
7319
  }
7320
+
7321
+ // src/shared/client-errors.ts
7322
+ var CLIENT_LOG_MARKER = "[deepspace:client-error]";
7323
+ var CLIENT_ERROR_PATH = "/_deepspace/client-errors";
7324
+ var CLIENT_ERROR_CAPS = {
7325
+ message: 1024,
7326
+ stack: 4096,
7327
+ name: 256,
7328
+ url: 512
7329
+ };
7330
+ function capText(value, max) {
7331
+ if (typeof value !== "string") return void 0;
7332
+ return value.length > max ? value.slice(0, max) : value;
7333
+ }
7334
+ function finiteNumber(value) {
7335
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
7336
+ }
7337
+ function normalizeClientErrorReport(raw) {
7338
+ if (typeof raw !== "object" || raw === null) return null;
7339
+ const o = raw;
7340
+ const kind = o.kind === "unhandledrejection" || o.kind === "react" ? o.kind : "error";
7341
+ const message = capText(o.message, CLIENT_ERROR_CAPS.message) ?? "";
7342
+ const name = capText(o.name, CLIENT_ERROR_CAPS.name);
7343
+ const stack = capText(o.stack, CLIENT_ERROR_CAPS.stack);
7344
+ const url = capText(o.url, CLIENT_ERROR_CAPS.url);
7345
+ const line = finiteNumber(o.line);
7346
+ const col = finiteNumber(o.col);
7347
+ if (!message && !stack) return null;
7348
+ const report = { kind, message: message || (name ?? "Error") };
7349
+ if (name) report.name = name;
7350
+ if (stack) report.stack = stack;
7351
+ if (url) report.url = url;
7352
+ if (line !== void 0) report.line = line;
7353
+ if (col !== void 0) report.col = col;
7354
+ return report;
7355
+ }
7356
+ function clientErrorToLogLine(report) {
7357
+ return `${CLIENT_LOG_MARKER} ${JSON.stringify(report)}`;
7358
+ }
7359
+
7360
+ // src/server/client-errors.ts
7361
+ var THROTTLE_WINDOW_MS = 1e3;
7362
+ var THROTTLE_MAX = 20;
7363
+ var windowStart = 0;
7364
+ var windowCount = 0;
7365
+ var MAX_BODY_BYTES = 64 * 1024;
7366
+ function shouldThrottleClientError(now = Date.now()) {
7367
+ if (now - windowStart >= THROTTLE_WINDOW_MS) {
7368
+ windowStart = now;
7369
+ windowCount = 0;
7370
+ }
7371
+ windowCount += 1;
7372
+ return windowCount > THROTTLE_MAX;
7373
+ }
7374
+ function jsonResponse(body, status) {
7375
+ return new Response(JSON.stringify(body), {
7376
+ status,
7377
+ headers: { "Content-Type": "application/json" }
7378
+ });
7379
+ }
7380
+ async function handleClientErrorReport(request) {
7381
+ const site = request.headers.get("sec-fetch-site");
7382
+ if (site && site !== "same-origin" && site !== "none") {
7383
+ return new Response(null, { status: 403 });
7384
+ }
7385
+ if (shouldThrottleClientError()) {
7386
+ return new Response(null, { status: 429, headers: { "Retry-After": "1" } });
7387
+ }
7388
+ const declared = Number(request.headers.get("content-length"));
7389
+ if (Number.isFinite(declared) && declared > MAX_BODY_BYTES) {
7390
+ return jsonResponse({ error: "payload_too_large" }, 413);
7391
+ }
7392
+ let body;
7393
+ try {
7394
+ body = await request.json();
7395
+ } catch {
7396
+ return jsonResponse({ error: "invalid_json" }, 400);
7397
+ }
7398
+ const report = normalizeClientErrorReport(body);
7399
+ if (!report) return jsonResponse({ error: "invalid_report" }, 400);
7400
+ console.error(clientErrorToLogLine(report));
7401
+ return new Response(null, { status: 204 });
7402
+ }
7403
+ function registerClientErrorRoute(app) {
7404
+ app.post(CLIENT_ERROR_PATH, (c) => handleClientErrorReport(c.req.raw));
7405
+ }
7406
+
7407
+ // src/shared/log-events.ts
7408
+ var LOG_LEVELS = ["debug", "log", "info", "warn", "error"];
7409
+ var APP_LOG_EVENT_KEYS = [
7410
+ "id",
7411
+ "timestamp",
7412
+ "level",
7413
+ "eventType",
7414
+ "message",
7415
+ "source",
7416
+ "outcome",
7417
+ "request",
7418
+ "exception"
7419
+ ];
7235
7420
  export {
7236
7421
  AI_CHATS_SCHEMA,
7237
7422
  AI_MESSAGES_SCHEMA,
7238
7423
  ALLOWED_BINDING_TYPES,
7424
+ APP_LOG_EVENT_KEYS,
7239
7425
  APP_NAME_RULES,
7240
7426
  AUTO_PROVISIONABLE_TYPES,
7241
7427
  AUTO_PROVISION_SENTINEL,
@@ -7246,6 +7432,7 @@ export {
7246
7432
  CHANNELS_SCHEMA,
7247
7433
  CHANNEL_INVITATIONS_SCHEMA,
7248
7434
  CHANNEL_MEMBERS_SCHEMA,
7435
+ CLIENT_LOG_MARKER,
7249
7436
  CONVERSATION_SCHEMAS,
7250
7437
  COST_RATES,
7251
7438
  CanvasRoom,
@@ -7258,6 +7445,7 @@ export {
7258
7445
  GLOBAL_DO_TYPE_NAMES,
7259
7446
  GameRoom,
7260
7447
  JobRoom,
7448
+ LOG_LEVELS,
7261
7449
  MESSAGES_SCHEMA,
7262
7450
  MSG,
7263
7451
  MSG_AWARENESS,
@@ -7330,6 +7518,7 @@ export {
7330
7518
  getMessageType,
7331
7519
  getRolePermissions,
7332
7520
  handleAwarenessMessage,
7521
+ handleClientErrorReport,
7333
7522
  handleSyncMessage,
7334
7523
  hasContent,
7335
7524
  isAutoProvision,
@@ -7349,6 +7538,7 @@ export {
7349
7538
  priceBindingUsageEvent,
7350
7539
  readVarUint,
7351
7540
  readVarUint8Array,
7541
+ registerClientErrorRoute,
7352
7542
  resolveAppName,
7353
7543
  resolveColumn,
7354
7544
  rowToData,