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/CHANGELOG.md +31 -0
- package/dist/cli.js +446 -92
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +126 -1
- package/dist/index.js +148 -0
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts +53 -1
- package/dist/server.js +86 -1
- package/dist/server.js.map +1 -1
- package/dist/worker.d.ts +199 -2
- package/dist/worker.js +191 -1
- package/dist/worker.js.map +1 -1
- package/features/ai-chat/src/ChatPanel.tsx +70 -30
- package/package.json +1 -1
- package/dist/testing.d.ts +0 -115
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-
|
|
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
|
-
|
|
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,
|