deepspace 0.3.8 → 0.3.9

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.
@@ -0,0 +1,3805 @@
1
+ import { LanguageModel, ModelMessage } from 'ai';
2
+ import * as Y from 'yjs';
3
+ import * as better_auth from 'better-auth';
4
+ import * as better_auth_plugins from 'better-auth/plugins';
5
+ import { Context } from 'hono';
6
+
7
+ /**
8
+ * Tools API Types and Definitions
9
+ *
10
+ * MCP-like interface for agent tool calls.
11
+ * Built-in tools for storage and backup operations.
12
+ */
13
+ interface ToolSchema {
14
+ name: string;
15
+ description: string;
16
+ params: Record<string, {
17
+ type: 'string' | 'number' | 'boolean' | 'object' | 'array';
18
+ description: string;
19
+ required?: boolean;
20
+ default?: unknown;
21
+ }>;
22
+ }
23
+ /**
24
+ * Discriminated union so callers don't have to guard on `error` being
25
+ * defined when `success` is false — TS enforces the invariant.
26
+ */
27
+ type ToolResult = {
28
+ success: true;
29
+ data?: unknown;
30
+ } | {
31
+ success: false;
32
+ error: string;
33
+ };
34
+ /**
35
+ * Built-in tool definitions for storage, record, schema, user, and backup operations
36
+ */
37
+ declare const BUILT_IN_TOOLS: ToolSchema[];
38
+
39
+ /**
40
+ * Shared Scoped R2 Files Handler
41
+ *
42
+ * Provides a secure, prefix-scoped R2 files API that enforces:
43
+ * 1. All R2 keys are validated against the resolved prefix (no bypass)
44
+ * 2. Path traversal (`..`, `.`) is rejected
45
+ * 3. Mutations (upload/delete) require authentication by default
46
+ *
47
+ * Each worker provides a `resolvePrefix` callback for its scoping rules.
48
+ * The security invariants are enforced here once — not per-worker.
49
+ *
50
+ * Routes:
51
+ * POST /api/files/upload → upload (prefix + generated key)
52
+ * GET /api/files → list (prefix + optional user prefix)
53
+ * GET /api/files/:key → download (validated against prefix)
54
+ * DELETE /api/files/:key → delete (validated against prefix)
55
+ */
56
+ interface ScopeContext {
57
+ userId: string | null;
58
+ url: URL;
59
+ }
60
+ type PrefixResult = {
61
+ prefix: string;
62
+ error?: undefined;
63
+ } | {
64
+ prefix?: undefined;
65
+ error: string;
66
+ };
67
+ interface ScopedR2Config {
68
+ /**
69
+ * Resolve the R2 key prefix for the given scope.
70
+ * Called with the `?scope=` query param value (default: 'self').
71
+ */
72
+ resolvePrefix: (scope: string, ctx: ScopeContext) => PrefixResult;
73
+ /**
74
+ * Require a non-null userId for upload and delete.
75
+ * @default true
76
+ */
77
+ requireAuthForMutations?: boolean;
78
+ }
79
+ interface ScopedR2Auth {
80
+ userId: string | null;
81
+ }
82
+ type ScopedR2Handler = (request: Request, url: URL, bucket: R2Bucket, auth: ScopedR2Auth) => Promise<Response>;
83
+ /**
84
+ * Create a scoped R2 files handler.
85
+ *
86
+ * Security guarantees:
87
+ * - Download/delete keys are validated to start with the resolved prefix
88
+ * - Path traversal (`..`) is rejected at the entry point
89
+ * - Mutations require a non-null userId by default
90
+ *
91
+ * @returns A handler function: `(request, url, bucket, auth) => Promise<Response>`
92
+ */
93
+ declare function createScopedR2Handler(config: ScopedR2Config): ScopedR2Handler;
94
+
95
+ interface Query {
96
+ collection: string;
97
+ where?: Record<string, unknown>;
98
+ orderBy?: string;
99
+ orderDir?: 'asc' | 'desc';
100
+ limit?: number;
101
+ }
102
+ interface Subscription {
103
+ id: string;
104
+ query: Query;
105
+ }
106
+ /** Key for Yjs doc: collection:recordId:fieldName */
107
+ type YjsDocKey = string;
108
+ interface YjsSubscription {
109
+ collection: string;
110
+ recordId: string;
111
+ fieldName: string;
112
+ }
113
+ interface RecordResult {
114
+ recordId: string;
115
+ data: Record<string, unknown>;
116
+ createdBy: string;
117
+ createdAt: string;
118
+ updatedAt: string;
119
+ }
120
+ interface SubscribePayload {
121
+ subscriptionId: string;
122
+ query: Query;
123
+ }
124
+ interface UnsubscribePayload {
125
+ subscriptionId: string;
126
+ }
127
+ interface PutPayload {
128
+ collection: string;
129
+ recordId: string;
130
+ data: Record<string, unknown>;
131
+ requestId?: string;
132
+ }
133
+ interface DeletePayload {
134
+ collection: string;
135
+ recordId: string;
136
+ requestId?: string;
137
+ }
138
+ interface SetRolePayload {
139
+ userId: string;
140
+ role: string;
141
+ }
142
+ interface YjsJoinPayload {
143
+ collection: string;
144
+ recordId: string;
145
+ fieldName: string;
146
+ }
147
+ interface YjsLeavePayload {
148
+ collection: string;
149
+ recordId: string;
150
+ fieldName: string;
151
+ }
152
+ interface DirectoryConversationData {
153
+ Name: string;
154
+ Description: string;
155
+ Type: string;
156
+ Visibility: string;
157
+ CreatedBy: string;
158
+ ParticipantHash: string;
159
+ ParticipantIds: string;
160
+ Status: string;
161
+ AssigneeId: string;
162
+ LinkedRef: string;
163
+ LastMessageAt: string;
164
+ LastMessagePreview: string;
165
+ LastMessageAuthor: string;
166
+ MessageCount: number;
167
+ }
168
+ interface ConversationStateData {
169
+ ConversationId: string;
170
+ UserId: string;
171
+ LastReadAt: string;
172
+ LastReadMessageCount: number;
173
+ Starred: number;
174
+ Archived: number;
175
+ Trashed: number;
176
+ Labels: string;
177
+ Folder: string;
178
+ }
179
+ interface DirectoryCommunityData {
180
+ Name: string;
181
+ Description: string;
182
+ CreatedBy: string;
183
+ Type: string;
184
+ Visibility: string;
185
+ MemberCount: number;
186
+ Rules: string;
187
+ IconUrl: string;
188
+ CoverUrl: string;
189
+ }
190
+ interface DirectoryMembershipData {
191
+ CommunityId: string;
192
+ UserId: string;
193
+ UserName: string;
194
+ Role: string;
195
+ JoinedAt: string;
196
+ }
197
+ interface DirectoryPostData {
198
+ Title: string;
199
+ Content: string;
200
+ AuthorId: string;
201
+ Type: string;
202
+ CommunityId: string;
203
+ ParentId: string;
204
+ ConversationId: string;
205
+ Status: string;
206
+ Tags: string;
207
+ LinkUrl: string;
208
+ }
209
+ interface ConvMessageData {
210
+ Content: string;
211
+ AuthorId: string;
212
+ ParentId: string;
213
+ Edited: number;
214
+ MessageType: string;
215
+ Metadata: string;
216
+ }
217
+ interface ConvReactionData {
218
+ MessageId: string;
219
+ Emoji: string;
220
+ UserId: string;
221
+ }
222
+ interface ConvMemberData {
223
+ UserId: string;
224
+ UserName: string;
225
+ Role: string;
226
+ }
227
+ interface ConvReadCursorData {
228
+ UserId: string;
229
+ LastReadAt: string;
230
+ }
231
+ interface ConvVoteData {
232
+ TargetId: string;
233
+ UserId: string;
234
+ Direction: number;
235
+ }
236
+
237
+ /**
238
+ * Server Action Types
239
+ *
240
+ * Types for app-defined server actions that run in the site worker.
241
+ * Actions bypass user RBAC via the X-App-Action header — the app's
242
+ * server-side code IS the trust boundary.
243
+ */
244
+
245
+ /**
246
+ * Discriminated result wrapper. Narrowing on `.success` lets TS know
247
+ * `.data` is present in the success branch and `.error` in the failure
248
+ * branch, so callers can't read the wrong field by accident.
249
+ *
250
+ * `TData` is the per-operation data shape — `tools.query` returns
251
+ * `{ records, count }`, `tools.get` returns `{ record }`, etc. Apps
252
+ * that compose their own server actions can specialize further.
253
+ */
254
+ type ActionResult<TData = unknown> = {
255
+ success: true;
256
+ data: TData;
257
+ error?: never;
258
+ } | {
259
+ success: false;
260
+ data?: never;
261
+ error: string;
262
+ };
263
+ /** Shape of the data field for `tools.query`. */
264
+ interface QueryActionData<T = Record<string, unknown>> {
265
+ records: Array<RecordResult & {
266
+ data: T;
267
+ }>;
268
+ count: number;
269
+ }
270
+ /** Shape of the data field for `tools.get`. */
271
+ interface GetActionData<T = Record<string, unknown>> {
272
+ record: RecordResult & {
273
+ data: T;
274
+ };
275
+ }
276
+ /** Shape of the data field for `tools.create`/`update`/`remove`. */
277
+ interface MutateActionData {
278
+ recordId: string;
279
+ }
280
+ interface ActionTools {
281
+ /**
282
+ * Insert a new record. When `recordId` is omitted the DO generates one
283
+ * (typical). Pass `recordId` to upsert against a known key — useful
284
+ * for `users` where the row id must equal the auth user's id so
285
+ * `tools.get('users', userId)` resolves.
286
+ */
287
+ create<T extends Record<string, unknown> = Record<string, unknown>>(collection: string, data: T, recordId?: string): Promise<ActionResult<MutateActionData>>;
288
+ update<T extends Record<string, unknown> = Record<string, unknown>>(collection: string, recordId: string, data: Partial<T>): Promise<ActionResult<MutateActionData>>;
289
+ remove(collection: string, recordId: string): Promise<ActionResult<MutateActionData>>;
290
+ get<T extends Record<string, unknown> = Record<string, unknown>>(collection: string, recordId: string): Promise<ActionResult<GetActionData<T>>>;
291
+ query<T extends Record<string, unknown> = Record<string, unknown>>(collection: string, options?: {
292
+ where?: Record<string, unknown>;
293
+ orderBy?: string;
294
+ orderDir?: 'asc' | 'desc';
295
+ limit?: number;
296
+ }): Promise<ActionResult<QueryActionData<T>>>;
297
+ /**
298
+ * Call an integration endpoint (e.g. 'openai/chat-completion') via the
299
+ * api-worker. On success, `result.data` is the integration's response
300
+ * body directly — there is no `.response` wrapper. So an OpenAI call
301
+ * yields `result.data.choices`, a Freepik image call yields
302
+ * `result.data.images`, etc.
303
+ */
304
+ integration<T = unknown>(endpoint: string, data?: unknown): Promise<ActionResult<T>>;
305
+ /**
306
+ * Insert or refresh the `users` row for an authenticated caller.
307
+ * Mirrors the WS-connect registerUser flow — useful for CLI-only
308
+ * actions (e.g. publishing via `deepspace foo publish`) where the
309
+ * caller may never have opened the web app and has no `users` row
310
+ * yet. Bypasses SYSTEM_MANAGED column stripping so name/email/
311
+ * imageUrl are actually written.
312
+ *
313
+ * Defaults `userId` to the action's caller. Pass `isAdmin: true` only
314
+ * if the caller's platform-tier role is admin (worker.ts should
315
+ * derive this from the verified JWT — never trust client input).
316
+ */
317
+ registerUser(opts: {
318
+ userId?: string;
319
+ name?: string;
320
+ email?: string;
321
+ imageUrl?: string;
322
+ isAdmin?: boolean;
323
+ }): Promise<ActionResult<{
324
+ user: {
325
+ id: string;
326
+ name: string;
327
+ email: string;
328
+ imageUrl?: string;
329
+ role: string;
330
+ };
331
+ }>>;
332
+ }
333
+ /**
334
+ * `TEnv` lets apps type the worker-scoped env object passed to the
335
+ * action handler. Defaults to a loose `Record<string, unknown>` so
336
+ * unparameterized handlers still compile; apps that want strict typing
337
+ * can do `ActionHandler<Env>` where Env is their own worker's bindings
338
+ * interface.
339
+ */
340
+ interface ActionContext<TEnv = Record<string, unknown>> {
341
+ userId: string;
342
+ params: Record<string, unknown>;
343
+ tools: ActionTools;
344
+ /**
345
+ * The worker's env bindings. Used by actions that need access to
346
+ * secrets, bindings, or platform-injected values like `OWNER_USER_ID`
347
+ * (e.g. for owner-only action gating).
348
+ */
349
+ env: TEnv;
350
+ /**
351
+ * The caller's raw JWT. Forward this on outbound requests that need to
352
+ * impersonate the user (e.g. checking `/api/apps` ownership on the
353
+ * deploy worker, where the user — not the app owner — should be billed
354
+ * / authorized).
355
+ */
356
+ callerJwt: string;
357
+ }
358
+ type ActionHandler<TEnv = Record<string, unknown>> = (ctx: ActionContext<TEnv>) => Promise<ActionResult>;
359
+
360
+ /**
361
+ * Cron System — Server-Side Scheduled Tasks
362
+ *
363
+ * Provides CronContext for miniapp cron handlers and buildCronContext
364
+ * to construct it from worker environment bindings.
365
+ *
366
+ * CronContext gives handlers access to:
367
+ * - records: Query/create/update/delete via RecordRoom tools API
368
+ * - integrations: Call platform integration endpoints (billed to owner)
369
+ * - ownerUserId: The app owner's user ID
370
+ */
371
+ /** Context passed to cron handler functions */
372
+ interface CronContext {
373
+ /** RecordRoom data access (queries the DO directly via tools API) */
374
+ records: {
375
+ query(collection: string, opts?: {
376
+ where?: Record<string, unknown>;
377
+ limit?: number;
378
+ }): Promise<any[]>;
379
+ create(collection: string, data: Record<string, unknown>): Promise<any>;
380
+ update(collection: string, recordId: string, data: Record<string, unknown>): Promise<any>;
381
+ delete(collection: string, recordId: string): Promise<any>;
382
+ };
383
+ /** Call platform integration endpoints, billed to owner */
384
+ integrations: {
385
+ call(endpoint: string, params: Record<string, unknown>): Promise<any>;
386
+ };
387
+ /** App owner's user ID */
388
+ ownerUserId: string;
389
+ }
390
+ /** Environment bindings needed by buildCronContext */
391
+ interface CronEnv {
392
+ RECORD_ROOMS: DurableObjectNamespace;
393
+ INTERNAL_STORAGE_HMAC_SECRET?: string;
394
+ /** Override API base URL for local dev or custom platform deployments. */
395
+ API_BASE_URL?: string;
396
+ }
397
+ /**
398
+ * Build a CronContext from worker environment bindings.
399
+ *
400
+ * @param env - Worker environment with RECORD_ROOMS DO namespace and HMAC secret
401
+ * @param ownerUserId - App owner's user ID (for RBAC and billing)
402
+ * @param roomId - RecordRoom ID (defaults to 'default')
403
+ */
404
+ declare function buildCronContext(env: CronEnv, ownerUserId: string, roomId?: string): CronContext;
405
+
406
+ /**
407
+ * Upstream worker proxy helpers.
408
+ *
409
+ * App workers reach the platform's other workers (api, platform, auth) through
410
+ * one of two transports:
411
+ *
412
+ * 1. **Service binding** (`env.API_WORKER` / `env.PLATFORM_WORKER`) — the
413
+ * preferred path in production. Configured via wrangler `[[services]]`.
414
+ * Cross-worker calls over plain `*.workers.dev` URLs return Cloudflare
415
+ * error 1042 in production, so the binding is the only working path
416
+ * in deployed apps.
417
+ *
418
+ * 2. **HTTPS URL** (`env.API_WORKER_URL` / `env.PLATFORM_WORKER_URL`) — the
419
+ * fallback used in local development. `deepspace dev` writes these
420
+ * into `.dev.vars`, which `wrangler dev` exposes as env vars. Service
421
+ * bindings don't work cross-process under `wrangler dev` for SDK apps,
422
+ * so the URL is the only working path in dev.
423
+ *
424
+ * The auth-worker has no service binding even in production — its responses
425
+ * carry `Set-Cookie` headers we want preserved verbatim, which we get for
426
+ * free over plain HTTPS. So `authWorkerFetch` is URL-only; the helper exists
427
+ * for surface consistency, not to switch transports.
428
+ *
429
+ * Each helper:
430
+ * - Prefers the binding if present, falls back to the URL otherwise.
431
+ * - Throws an actionable Error if neither is configured. No silent 502s.
432
+ * - Forwards `init` (method/headers/body) verbatim to the upstream worker.
433
+ *
434
+ * History: a previous in-tree helper folded the binding/URL fallback into
435
+ * the AI module's `resolveTransport`. Inline call sites in the starter
436
+ * template (integrations, files, debug) used `c.env.X.fetch(...)` directly,
437
+ * which broke `npx deepspace dev` for any app calling those routes — the
438
+ * binding is undefined locally, so the fetch threw. These helpers
439
+ * standardize on the same shape `resolveTransport` had, so every upstream
440
+ * call works in both dev and prod.
441
+ */
442
+ /**
443
+ * Env shape required by `apiWorkerFetch`. App workers should extend this
444
+ * (the starter template does) so the helper can be called with `c.env`.
445
+ */
446
+ interface ApiWorkerEnv {
447
+ /** Cloudflare service binding for the api-worker. Preferred. */
448
+ API_WORKER?: Fetcher;
449
+ /** HTTPS URL for the api-worker. Used when the binding is absent (dev). */
450
+ API_WORKER_URL?: string;
451
+ }
452
+ /** Env shape required by `platformWorkerFetch`. */
453
+ interface PlatformWorkerEnv {
454
+ /** Cloudflare service binding for the platform-worker. Preferred. */
455
+ PLATFORM_WORKER?: Fetcher;
456
+ /** HTTPS URL for the platform-worker. Used when the binding is absent. */
457
+ PLATFORM_WORKER_URL?: string;
458
+ }
459
+ /** Env shape required by `authWorkerFetch`. URL-only. */
460
+ interface AuthWorkerEnv {
461
+ /** HTTPS URL for the auth-worker. Always required. */
462
+ AUTH_WORKER_URL?: string;
463
+ }
464
+ /**
465
+ * Fetch the api-worker. Prefers the `API_WORKER` service binding, falls
466
+ * back to `API_WORKER_URL` over HTTPS.
467
+ *
468
+ * `path` is treated as path-only — any host in a passed-in URL is
469
+ * stripped and replaced. This matches how `c.env.API_WORKER.fetch(...)`
470
+ * already worked in the starter template (the host was always a
471
+ * placeholder like `api-worker`).
472
+ */
473
+ declare function apiWorkerFetch(env: ApiWorkerEnv, path: string, init?: RequestInit): Promise<Response>;
474
+ /**
475
+ * Fetch the platform-worker. Prefers the `PLATFORM_WORKER` service binding,
476
+ * falls back to `PLATFORM_WORKER_URL` over HTTPS.
477
+ *
478
+ * Accepts a `Request` instance directly so callers can hand off
479
+ * `c.req.raw`-derived requests with their original method/headers/body
480
+ * intact. (`/api/files/*` does this — it forwards the caller's body
481
+ * stream verbatim.)
482
+ */
483
+ declare function platformWorkerFetch(env: PlatformWorkerEnv, pathOrRequest: string | Request, init?: RequestInit): Promise<Response>;
484
+ /**
485
+ * Fetch the auth-worker over HTTPS. URL-only — there is no auth-worker
486
+ * service binding, by design (we want plain-HTTP cookie semantics).
487
+ *
488
+ * Kept as a helper for surface symmetry with `apiWorkerFetch` /
489
+ * `platformWorkerFetch`. Throws if `AUTH_WORKER_URL` is unset.
490
+ */
491
+ declare function authWorkerFetch(env: AuthWorkerEnv, path: string, init?: RequestInit): Promise<Response>;
492
+
493
+ /**
494
+ * AI provider helpers — create Vercel AI SDK providers that route through
495
+ * the DeepSpace API worker proxy for per-user billing.
496
+ *
497
+ * Supported providers: anthropic, openai, cerebras.
498
+ *
499
+ * The API worker can be reached in two ways:
500
+ * - Service binding `env.API_WORKER` (Cloudflare Fetcher) — preferred in
501
+ * production if the app has declared the binding in wrangler.toml.
502
+ * - HTTPS URL `env.API_WORKER_URL` — used in local dev and in production
503
+ * for apps that don't declare the binding. `deepspace dev` writes this
504
+ * into `.dev.vars` automatically.
505
+ *
506
+ * Auth is automatic by default:
507
+ * - For server-side autonomous calls (cron, DO alarms, background agents),
508
+ * the helper reads the long-lived `env.APP_OWNER_JWT` minted at deploy
509
+ * time (or by `deepspace dev` in local development) and uses it for the
510
+ * proxy auth header. The owner is billed automatically via the JWT sub.
511
+ * - For user-initiated calls (e.g. an `/api/ai/chat` route handling a
512
+ * browser request), pass `options.authToken` explicitly with the user's
513
+ * own JWT so the call is billed to the user.
514
+ *
515
+ * Usage:
516
+ *
517
+ * // Server-side autonomous — no auth config needed
518
+ * import { createDeepSpaceAI } from 'deepspace/worker'
519
+ * const cerebras = createDeepSpaceAI(env, 'cerebras')
520
+ * const result = await generateText({ model: cerebras('llama-3.3-70b'), ... })
521
+ *
522
+ * // User-initiated (inside a request handler)
523
+ * const jwt = c.req.header('Authorization')!.slice(7)
524
+ * const anthropic = createDeepSpaceAI(c.env, 'anthropic', { authToken: jwt })
525
+ */
526
+
527
+ /**
528
+ * Model factory: `(modelId) => LanguageModel`. The explicit return type
529
+ * keeps tsup's DTS build from leaking unportable `.pnpm/@ai-sdk+provider/...`
530
+ * paths into the published `dist/index.d.ts`.
531
+ */
532
+ type DeepSpaceModelFactory = (modelId: string) => LanguageModel;
533
+ type Provider = 'anthropic' | 'openai' | 'cerebras';
534
+ interface DeepSpaceAIEnv extends ApiWorkerEnv {
535
+ /**
536
+ * Long-lived owner-scoped JWT minted at deploy time (or by `deepspace dev`).
537
+ * Used as the default proxy auth token when `options.authToken` is absent.
538
+ * Bills the app owner.
539
+ */
540
+ APP_OWNER_JWT?: string;
541
+ }
542
+ interface DeepSpaceAIOptions {
543
+ /**
544
+ * Explicit auth token for this call. Use this for user-initiated flows
545
+ * where the caller's own JWT should be billed. If omitted, the helper
546
+ * falls back to `env.APP_OWNER_JWT` (bills the app owner).
547
+ *
548
+ * Billing is always against the JWT subject — to bill a different user,
549
+ * pass a JWT whose subject is that user. The proxy does not accept any
550
+ * client-supplied billing override.
551
+ */
552
+ authToken?: string;
553
+ }
554
+ /**
555
+ * Build an AI SDK provider that routes through the DeepSpace API worker.
556
+ *
557
+ * Resolves the transport (service binding or URL) and the auth token
558
+ * (explicit or `env.APP_OWNER_JWT`) automatically. Throws a clear error if
559
+ * either is unconfigured.
560
+ */
561
+ declare function createDeepSpaceAI(env: DeepSpaceAIEnv, provider: Provider, options?: DeepSpaceAIOptions): DeepSpaceModelFactory;
562
+
563
+ /**
564
+ * captureScreenshot — call platform-worker /internal/screenshot.
565
+ *
566
+ * Apps don't ship CF Browser Rendering bindings or puppeteer in their
567
+ * own bundle. The platform holds the binding; consumers call this
568
+ * helper to get PNG bytes for a URL.
569
+ *
570
+ * The platform enforces: a host allowlist (*.app.space / *.deep.space),
571
+ * a per-app sliding rate limit, and viewport/timeout clamping. Returns
572
+ * `null` on any non-2xx — callers should treat as "no preview available"
573
+ * and surface their own fallback UX.
574
+ *
575
+ * Auth is the same HMAC-of-appName pattern `/internal/files` uses:
576
+ * x-app-identity-token = hmac(PLATFORM_IDENTITY_SECRET, APP_NAME)
577
+ * x-app-name = APP_NAME
578
+ *
579
+ * Apps already have both as bindings (APP_IDENTITY_TOKEN + APP_NAME),
580
+ * so this helper is a thin wrapper — no extra secrets to manage.
581
+ */
582
+
583
+ interface ScreenshotOptions {
584
+ url: string;
585
+ viewport?: {
586
+ width: number;
587
+ height: number;
588
+ };
589
+ waitUntil?: 'load' | 'domcontentloaded' | 'networkidle0' | 'networkidle2';
590
+ timeoutMs?: number;
591
+ fullPage?: boolean;
592
+ }
593
+ interface ScreenshotEnv extends PlatformWorkerEnv {
594
+ APP_NAME: string;
595
+ APP_IDENTITY_TOKEN: string;
596
+ }
597
+ interface ScreenshotResult {
598
+ /** PNG bytes. */
599
+ body: ArrayBuffer;
600
+ /** `image/png`. */
601
+ contentType: string;
602
+ }
603
+ /**
604
+ * Capture a screenshot of `opts.url` and return the PNG bytes.
605
+ *
606
+ * Returns null on capture failure (target unreachable, timeout, BR
607
+ * binding misconfigured platform-side). The platform endpoint logs the
608
+ * underlying error; callers should treat null as "no preview available"
609
+ * and surface their own UX fallback.
610
+ */
611
+ declare function captureScreenshot(env: ScreenshotEnv, opts: ScreenshotOptions): Promise<ScreenshotResult | null>;
612
+
613
+ /**
614
+ * Chat context pipeline — keeps the per-request payload to the LLM bounded.
615
+ *
616
+ * `prepareMessagesWithCompaction` runs before `streamText`: truncate old tool
617
+ * results, apply a cached summary if available, otherwise summarize the older
618
+ * half of history when over budget. Falls back to a sliding window if
619
+ * summarization fails. `capToolResultSize` caps individual tool calls.
620
+ */
621
+
622
+ interface ChatTurn {
623
+ id?: string;
624
+ role: 'user' | 'assistant' | 'system';
625
+ content: string;
626
+ parts?: unknown[];
627
+ }
628
+ type Summarizer = (messages: ChatTurn[]) => Promise<string>;
629
+ interface ChatContextConfig {
630
+ contextBudget: number;
631
+ toolResultCap: number;
632
+ keepRecentToolResults: number;
633
+ minKept: number;
634
+ }
635
+ declare const DEFAULT_CONTEXT_CONFIG: ChatContextConfig;
636
+ declare function totalChars(messages: ChatTurn[]): number;
637
+ /**
638
+ * Replace older tool-result payloads with a small marker. Keeps the last
639
+ * `keepRecent` tool results intact. Errors (`success: false`) are preserved —
640
+ * they're small and the agent needs them for reasoning.
641
+ */
642
+ declare function truncateOldToolResults(messages: ChatTurn[], keepRecent: number): ChatTurn[];
643
+ /**
644
+ * Drop oldest messages until total character count is under `charCap`,
645
+ * never going below `minKept` messages. System messages (e.g. compaction
646
+ * summaries) are pinned — dropping them would discard the most condensed
647
+ * context first.
648
+ */
649
+ declare function applySlidingWindow(messages: ChatTurn[], charCap: number, minKept: number): ChatTurn[];
650
+ /**
651
+ * Replace oversized tool results with an error telling the agent to narrow
652
+ * its query. Keeps a small preview so the agent can see what it got.
653
+ */
654
+ declare function capToolResultSize(result: unknown, byteCap: number): unknown;
655
+ /**
656
+ * Convert persisted ChatTurns into AI SDK ModelMessages.
657
+ *
658
+ * Persisted assistant rows store `parts` in UI shape (text + tool-invocation,
659
+ * each invocation carrying its own `result`). When fed back to the LLM, the
660
+ * shape MUST match the original multi-step flow: an assistant message
661
+ * containing a `tool_use` block must end with that block, the IMMEDIATELY
662
+ * NEXT message must be a tool/user message containing the matching
663
+ * `tool_result`, and any text the model produced AFTER seeing the tool
664
+ * result belongs in a SEPARATE assistant message after the tool message.
665
+ *
666
+ * Anthropic specifically rejects an assistant message of the form
667
+ * `[text, tool_use, text]` — the trailing text breaks its `tool_use` →
668
+ * `tool_result` pairing check. So we walk the parts in order and split at
669
+ * each tool-invocation boundary, emitting a fresh assistant + tool pair per
670
+ * tool call, and a final trailing assistant message for any post-tool text.
671
+ *
672
+ * Tool-invocation entries with `state: 'call'` (no result — typically an
673
+ * interrupted stream) are dropped on both sides.
674
+ */
675
+ declare function turnsToCoreMessages(turns: ChatTurn[]): ModelMessage[];
676
+ /**
677
+ * Convert AI SDK response messages into our persisted UI shape (text +
678
+ * tool-invocation parts), pairing each assistant tool-call with its tool-
679
+ * result from the following tool message. Order is chronological.
680
+ *
681
+ * Inverse of `turnsToCoreMessages`: takes the v5 `ModelMessage[]` returned
682
+ * from `streamText`'s `onFinish` and produces the flat `parts` array we
683
+ * persist on `ai-messages` rows. Reads `c.input` / `c.output` (v5 wire
684
+ * names) and unwraps `output`'s tagged-union via `unwrapToolOutput`.
685
+ */
686
+ declare function buildUiParts(responseMessages: ModelMessage[]): unknown[];
687
+ /**
688
+ * Unwrap v5's tagged tool-result `output` to the flat shape we persist.
689
+ * Errors get remapped to `{ success: false, error }` because
690
+ * `truncateOldToolResults` preserves entries with that shape across turns —
691
+ * without the remap, error context would get truncated like a normal result.
692
+ */
693
+ declare function unwrapToolOutput(output: unknown): unknown;
694
+ /**
695
+ * Pre-stream pipeline with compaction.
696
+ *
697
+ * 1. Truncate old tool results.
698
+ * 2. If a cached summary covers a known message id, replace prior turns with it.
699
+ * 3. If still over budget, summarize the older half of `working` and return a
700
+ * `newSummary` for persistence — runs even after cached-summary application
701
+ * so a long-running chat can re-summarize on subsequent turns.
702
+ * 4. On summarizer error or missing ids, fall back to a sliding window
703
+ * (which preserves system messages — see `applySlidingWindow`).
704
+ */
705
+ declare function prepareMessagesWithCompaction(messages: ChatTurn[], config: ChatContextConfig, options: {
706
+ summarizer: Summarizer;
707
+ cachedSummary?: {
708
+ text: string;
709
+ throughId: string;
710
+ };
711
+ }): Promise<{
712
+ messages: ChatTurn[];
713
+ newSummary?: {
714
+ text: string;
715
+ throughId: string;
716
+ };
717
+ }>;
718
+ /**
719
+ * Build a default summarizer backed by Claude Haiku.
720
+ *
721
+ * Billing: defaults to the app owner via `APP_OWNER_JWT` — summarization is
722
+ * usually infrastructure, not user work. Pass `{ authToken }` to bill a
723
+ * specific user (e.g. the caller's JWT) instead.
724
+ */
725
+ declare function makeDefaultSummarizer(env: DeepSpaceAIEnv, options?: {
726
+ authToken?: string;
727
+ }): Summarizer;
728
+
729
+ /**
730
+ * Chat history helpers — wrap RecordRoom's tools API for ai-chats / ai-messages.
731
+ *
732
+ * Trust model: every helper sends `X-App-Action: 'true'`, which bypasses
733
+ * RecordRoom's per-record RBAC. The worker is the trust boundary, not
734
+ * RecordRoom. Callers MUST verify ownership before invoking write helpers
735
+ * (`updateChat`, `appendMessage`, `deleteChatCascade`); the worker's
736
+ * `/api/ai/chat`, `PATCH /api/ai/chats/:id`, and `DELETE /api/ai/chats/:id`
737
+ * routes do this via a `getChat()` precheck that 404s when the row is
738
+ * missing or owned by another user. Read helpers (`getChat`, `loadMessages`)
739
+ * filter by `chatId` against userBound rows, so cross-user reads return
740
+ * empty — but new consumers should still consider an explicit ownership
741
+ * check before exposing data.
742
+ *
743
+ * The tools API returns records as `{ recordId, data, createdAt, updatedAt }`
744
+ * envelopes; helpers below flatten them into ChatRow / ChatMessageRow.
745
+ */
746
+ /**
747
+ * Canonical chat row.
748
+ *
749
+ * `recordId` is the primary identifier — same envelope shape as every
750
+ * other DeepSpace data type (records.* tools, useQuery results, etc.).
751
+ *
752
+ * `id` is kept as a deprecated alias so existing callers don't break,
753
+ * but every new caller should prefer `recordId`. Without this rename
754
+ * an integrator who reads `chat.recordId` (the obvious thing given the
755
+ * rest of the SDK) silently gets `undefined`, then ships code that
756
+ * sends `{"chatId": undefined}` to `/api/ai/chat` and gets back a 400
757
+ * with a misleading error.
758
+ */
759
+ type ChatRow = {
760
+ recordId: string;
761
+ /** @deprecated Use `recordId`. Retained for backward compatibility. */
762
+ id: string;
763
+ userId: string;
764
+ title: string;
765
+ model?: string;
766
+ compactedSummary?: string;
767
+ compactedThroughId?: string;
768
+ createdAt: string;
769
+ updatedAt: string;
770
+ };
771
+ type ChatMessageRow = {
772
+ recordId: string;
773
+ /** @deprecated Use `recordId`. Retained for backward compatibility. */
774
+ id: string;
775
+ chatId: string;
776
+ userId: string;
777
+ role: 'user' | 'assistant' | 'system';
778
+ content: string;
779
+ parts?: unknown[];
780
+ createdAt: string;
781
+ };
782
+ declare function getChat(stub: DurableObjectStub, chatId: string, userId: string): Promise<ChatRow | null>;
783
+ declare function createChat(stub: DurableObjectStub, userId: string, opts?: {
784
+ title?: string;
785
+ model?: string;
786
+ }): Promise<ChatRow>;
787
+ declare function updateChat(stub: DurableObjectStub, chatId: string, userId: string, patch: Partial<Pick<ChatRow, 'title' | 'model' | 'compactedSummary' | 'compactedThroughId'>>): Promise<void>;
788
+ declare function deleteChatCascade(stub: DurableObjectStub, chatId: string, userId: string): Promise<void>;
789
+ declare function loadMessages(stub: DurableObjectStub, chatId: string, userId: string): Promise<ChatMessageRow[]>;
790
+ declare function appendMessage(stub: DurableObjectStub, msg: {
791
+ id: string;
792
+ chatId: string;
793
+ userId: string;
794
+ role: 'user' | 'assistant' | 'system';
795
+ content: string;
796
+ parts?: unknown[];
797
+ }): Promise<void>;
798
+
799
+ /**
800
+ * Per-binding usage metering — record Vectorize / Workers AI / etc. costs
801
+ * to the auto-attached `USAGE_EVENTS` Analytics Engine dataset.
802
+ *
803
+ * Why: the platform's tail-worker captures per-invocation compute (CPU + wall
804
+ * time + script name) but it can't see which model an AI call hit, how many
805
+ * tokens it embedded, or how many vectors a Vectorize query scanned. Without
806
+ * those signals there's no way to surface per-tenant binding cost on the
807
+ * billing dashboard.
808
+ *
809
+ * The deploy-worker auto-attaches a `USAGE_EVENTS` AE binding to every app
810
+ * (dataset: `deepspace_binding_usage`). Apps don't need to declare it. They
811
+ * just call `meterAi(...)` / `meterVectorize(...)` / `meterUsage(...)` after
812
+ * each call and the dashboard rolls it up by `ownerUserId`.
813
+ *
814
+ * Schema written:
815
+ * indexes: [ownerUserId]
816
+ * blobs: [appName, kind, model_or_index, op]
817
+ * doubles: [units, count]
818
+ *
819
+ * Use:
820
+ * await meterAi(env, '@cf/qwen/qwen3-embedding-0.6b', { inputChars: 5000 })
821
+ * await meterVectorize(env, 'unison-candidates', 'query', { vectors: 1000 })
822
+ * await meterUsage(env, 'custom-thing', { units: 1 })
823
+ */
824
+ interface MeteringEnv {
825
+ USAGE_EVENTS?: AnalyticsEngineDataset;
826
+ OWNER_USER_ID?: string;
827
+ APP_NAME?: string;
828
+ }
829
+ /**
830
+ * Generic event recorder. Returns `false` if the binding isn't present
831
+ * (dev / not yet deployed) or if AnalyticsEngine throws — metering must
832
+ * never break the calling code path.
833
+ */
834
+ declare function meterUsage(env: MeteringEnv, kind: string, fields?: {
835
+ id?: string;
836
+ op?: string;
837
+ units?: number;
838
+ count?: number;
839
+ }): boolean;
840
+ /**
841
+ * Record a Workers AI call.
842
+ *
843
+ * Cloudflare prices input and output tokens at different rates for LLMs
844
+ * (output is typically more expensive); embedding models bill input only.
845
+ * Emits up to two events per call so the dashboard rollup can group by
846
+ * `op` and apply the right per-token rate:
847
+ *
848
+ * op='input' units=inputChars
849
+ * op='output' units=outputChars
850
+ *
851
+ * For a pure embedding call (outputChars=0), only the input event fires.
852
+ * Pass `inputChars` and `outputChars` raw — the rough chars-to-token
853
+ * conversion happens at price time using `COST_RATES.ai.embedInputPerChar`.
854
+ *
855
+ * Note: only embedding-input has an authoritative rate today. LLM-output
856
+ * pricing varies wildly per model so `priceBindingUsageEvent` returns 0
857
+ * for `op='output'` until per-model rates are wired. The events are still
858
+ * recorded so the dashboard can show that the calls happened.
859
+ */
860
+ declare function meterAi(env: MeteringEnv, model: string, fields?: {
861
+ inputChars?: number;
862
+ outputChars?: number;
863
+ calls?: number;
864
+ }): boolean;
865
+ /**
866
+ * Record a Vectorize operation.
867
+ *
868
+ * Cloudflare's published model (https://developers.cloudflare.com/vectorize/platform/pricing/):
869
+ *
870
+ * "If you have 10,000 vectors with 384-dimensions in an index, and make
871
+ * 100 queries against that index, your total queried vector dimensions
872
+ * would sum to 3.878 million ((10000 + 100) * 384)."
873
+ *
874
+ * So query billing is *additive* — `(stored + queries) * dims` summed
875
+ * across the call, not per-query-multiplied-by-stored. Translating to a
876
+ * per-call meter:
877
+ *
878
+ * op='query': units = (vectors + storedCount) * dims
879
+ * Without `storedCount` we significantly undercount: a single
880
+ * query against a 100K-vector index produces ~100K queried
881
+ * dims, not just `dims`.
882
+ * op='upsert': CF doesn't bill upserts directly; the chargeable delta is
883
+ * the change to stored-vector-month. `units = vectors * dims`
884
+ * approximates the per-call storage delta.
885
+ * op='delete' / 'getByIds': recorded for observability; no direct cost.
886
+ *
887
+ * Edge case: querying an empty index gives `(1 + 0) * dims = dims`, which
888
+ * matches CF's formula (the `+ queries` term is always added, even at 0
889
+ * stored). If CF later changes that and an empty-index query bills 0,
890
+ * adjust here — `metering` is the single place to update the math.
891
+ */
892
+ declare function meterVectorize(env: MeteringEnv, indexName: string, op: 'query' | 'upsert' | 'delete' | 'getByIds', fields?: {
893
+ vectors?: number;
894
+ dims?: number;
895
+ storedCount?: number;
896
+ }): boolean;
897
+ /**
898
+ * Per-`units` USD multipliers, matched to the (`kind`, `op`) the meter
899
+ * helpers above record. Dashboard rollup can multiply
900
+ *
901
+ * SUM(_sample_interval * doubles[1]) -- units
902
+ *
903
+ * by these to surface $-figures without re-querying CF's billing API.
904
+ */
905
+ declare const COST_RATES: {
906
+ readonly ai: {
907
+ /**
908
+ * USD per character of *embedding input* (bge-m3 / qwen3-embedding tier).
909
+ * Renamed from `perChar` to make explicit that this rate does NOT
910
+ * apply to LLM-generation output — see `priceBindingUsageEvent`.
911
+ */
912
+ readonly embedInputPerChar: number;
913
+ };
914
+ readonly vectorize: {
915
+ /** USD per queried dimension (per query, per stored vector compared). */
916
+ readonly queriedPerDim: number;
917
+ /** USD per stored dimension per month. */
918
+ readonly storedPerDimPerMonth: number;
919
+ };
920
+ };
921
+ /**
922
+ * Price a single rolled-up `deepspace_binding_usage` row. Lives next to
923
+ * `COST_RATES` so the (kind, op) → rate mapping stays paired with the schema
924
+ * the meter helpers write.
925
+ *
926
+ * Returns 0 for combinations without an authoritative per-unit rate; the row
927
+ * still surfaces in dashboards for observability. Notable zeros:
928
+ * - `ai/output`: LLM-output prices vary per model and `meterAi` doesn't
929
+ * carry a model-family signal. Pricing it at the embedding rate would
930
+ * silently under-bill chat-LLM use.
931
+ * - `vectorize.storedPerDimPerMonth`: events are per-call deltas, not
932
+ * monthly snapshots, so a windowed SUM isn't meaningful here.
933
+ */
934
+ declare function priceBindingUsageEvent(kind: string, op: string, units: number): number;
935
+
936
+ /**
937
+ * Lightweight D1 schema bootstrapping for apps that use auto-provisioned
938
+ * `[[d1_databases]]` bindings.
939
+ *
940
+ * The auto-provisioner gives apps an empty D1; the app needs to create its
941
+ * own tables before using them. This helper runs ordered SQL fragments and
942
+ * tracks which have applied via a `_dpc_migrations` meta-table so re-running
943
+ * is a no-op:
944
+ *
945
+ * ```ts
946
+ * import { runMigrations } from 'deepspace/worker'
947
+ *
948
+ * await runMigrations(env.CARDS_DB, [
949
+ * `CREATE TABLE cards (id INTEGER PRIMARY KEY, json TEXT NOT NULL);`,
950
+ * `CREATE INDEX idx_cards_updated ON cards(updated_at);`,
951
+ * ])
952
+ * ```
953
+ *
954
+ * **SQL formatting:** statements may span multiple lines freely. The runner
955
+ * splits each migration string on `;` and runs each fragment via
956
+ * `prepare().run()`. That sidesteps `db.exec()`'s newline-or-semicolon quirks
957
+ * (it's optimized for migration files, not inline strings) and gets you
958
+ * predictable single-statement semantics. Trailing `;` after the last
959
+ * statement is fine; semicolons inside string literals are not (we use a
960
+ * naive split, since DDL almost never has them).
961
+ *
962
+ * Each entry in the array is one migration. The runner records the index of
963
+ * each successfully-applied migration in `_dpc_migrations`; subsequent calls
964
+ * skip rows already recorded. Adding a new migration means appending to the
965
+ * array; never reorder or delete entries.
966
+ *
967
+ * Why a meta-table instead of `PRAGMA user_version`: D1's SQLite authorizer
968
+ * rejects PRAGMA writes with `SQLITE_AUTH`, even though the same statements
969
+ * work in raw SQLite. A real table works on any D1 database and stays a
970
+ * trivial bootstrap (one CREATE TABLE IF NOT EXISTS).
971
+ *
972
+ * Concurrency: D1 serializes statements per database, but two simultaneous
973
+ * `runMigrations` callers could race the same migration index. The duplicate
974
+ * INSERT collides on the primary key and the second caller sees the failure
975
+ * — but the migration itself uses `IF NOT EXISTS` so the schema is correct
976
+ * either way. Apps invoke this at startup which is single-threaded per
977
+ * worker isolate; the cross-isolate race is rare and self-healing.
978
+ *
979
+ * This is the simplest possible migration story (option 1 in
980
+ * docs/proposals/binding-auto-provisioning.md). Apps that outgrow it can
981
+ * adopt CF's `wrangler d1 migrations apply` directly without breaking the
982
+ * helper.
983
+ */
984
+ interface RunMigrationsResult {
985
+ /** Version before this run started. Equals the count of migrations already applied. */
986
+ fromVersion: number;
987
+ /** Version after migrations applied. Equals fromVersion if nothing ran. */
988
+ toVersion: number;
989
+ /** Number of migrations applied this call. */
990
+ applied: number;
991
+ }
992
+ /**
993
+ * Apply ordered SQL migrations to a D1 database. Idempotent: the next call
994
+ * with the same array is a no-op until the array grows.
995
+ *
996
+ * Throws on any individual migration failure. The migrations meta-row is
997
+ * only inserted after a migration succeeds, so a partial failure leaves a
998
+ * recoverable state — fix the SQL, redeploy, and the failed migration runs
999
+ * on next startup.
1000
+ */
1001
+ declare function runMigrations(db: D1Database, migrations: readonly string[]): Promise<RunMigrationsResult>;
1002
+
1003
+ /**
1004
+ * Wire protocol constants.
1005
+ *
1006
+ * The JSON WebSocket protocol uses dotted string identifiers (e.g.
1007
+ * `"game.input"`) as the `type` discriminator. All message types are
1008
+ * grouped under a single `MSG` object so imports stay tidy:
1009
+ *
1010
+ * import { MSG, dispatch, clientBuild } from 'deepspace'
1011
+ *
1012
+ * dispatch<ServerMessage>(raw, {
1013
+ * [MSG.GAME_STATE]: (p) => { ... },
1014
+ * [MSG.GAME_TICK]: (p) => { ... },
1015
+ * })
1016
+ *
1017
+ * Each key's value is the on-wire string — grep-friendly, self-
1018
+ * documenting, and infinite per namespace. Adding a new message is one
1019
+ * line here plus one arm in the discriminated union in `./messages.ts`.
1020
+ *
1021
+ * Yjs binary protocol constants (`MSG_YJS_SYNC`, `MSG_YJS_AWARENESS`) are
1022
+ * intentionally kept numeric and separate from `MSG` — they ride a
1023
+ * binary WebSocket frame format and aren't part of the JSON dispatcher.
1024
+ */
1025
+ declare const MSG: {
1026
+ readonly SUBSCRIBE: "core.subscribe";
1027
+ readonly UNSUBSCRIBE: "core.unsubscribe";
1028
+ readonly QUERY_RESULT: "core.query_result";
1029
+ readonly RECORD_CHANGE: "core.record_change";
1030
+ readonly PUT: "core.put";
1031
+ readonly DELETE: "core.delete";
1032
+ readonly ERROR: "core.error";
1033
+ readonly USER_INFO: "user.info";
1034
+ readonly USER_LIST: "user.list";
1035
+ readonly SET_ROLE: "user.set_role";
1036
+ readonly USER_UPDATE: "user.update";
1037
+ readonly YJS_JOIN: "yjs.join";
1038
+ readonly YJS_LEAVE: "yjs.leave";
1039
+ readonly ACK: "records.ack";
1040
+ readonly LIST_SCHEMAS: "records.list_schemas";
1041
+ readonly RESUBSCRIBE: "records.resubscribe";
1042
+ readonly GAME_STATE: "game.state";
1043
+ readonly GAME_INPUT: "game.input";
1044
+ readonly GAME_PLAYER_JOIN: "game.player_join";
1045
+ readonly GAME_PLAYER_LEAVE: "game.player_leave";
1046
+ readonly GAME_PLAYER_READY: "game.player_ready";
1047
+ readonly GAME_START: "game.start";
1048
+ readonly GAME_END: "game.end";
1049
+ readonly GAME_TICK: "game.tick";
1050
+ readonly CANVAS_SHAPES: "canvas.shapes";
1051
+ readonly CANVAS_ADD: "canvas.add";
1052
+ readonly CANVAS_MOVE: "canvas.move";
1053
+ readonly CANVAS_RESIZE: "canvas.resize";
1054
+ readonly CANVAS_DELETE: "canvas.delete";
1055
+ readonly CANVAS_UPDATE: "canvas.update";
1056
+ readonly CANVAS_VIEWPORT: "canvas.viewport";
1057
+ readonly CANVAS_UNDO: "canvas.undo";
1058
+ readonly CANVAS_REDO: "canvas.redo";
1059
+ readonly CRON_TASKS: "cron.tasks";
1060
+ readonly CRON_HISTORY: "cron.history";
1061
+ readonly CRON_TRIGGER: "cron.trigger";
1062
+ readonly CRON_PAUSE: "cron.pause";
1063
+ readonly CRON_RESUME: "cron.resume";
1064
+ readonly CRON_STATUS: "cron.status";
1065
+ readonly PRESENCE_SYNC: "presence.sync";
1066
+ readonly PRESENCE_JOIN: "presence.join";
1067
+ readonly PRESENCE_LEAVE: "presence.leave";
1068
+ readonly PRESENCE_UPDATE: "presence.update";
1069
+ readonly GW_SCOPE_CONNECT: "gateway.scope_connect";
1070
+ readonly GW_SCOPE_DISCONNECT: "gateway.scope_disconnect";
1071
+ readonly GW_SCOPE_ERROR: "gateway.scope_error";
1072
+ readonly GW_TOKEN_REFRESH: "gateway.token_refresh";
1073
+ readonly GW_USER_UPDATE: "gateway.user_update";
1074
+ };
1075
+
1076
+ /**
1077
+ * Typed wire-protocol layer — discriminated unions, typed builders, and a
1078
+ * type-safe dispatcher for every `MSG.*` the SDK understands.
1079
+ *
1080
+ * Why this exists
1081
+ * ---------------
1082
+ *
1083
+ * The string `MSG.*` constants in `./constants.ts` are the authoritative
1084
+ * wire protocol, but using them directly is error-prone: a typo picks the
1085
+ * wrong message with the wrong payload shape and fails silently at
1086
+ * runtime. This module pairs every constant with its payload type, so
1087
+ * that:
1088
+ *
1089
+ * 1. Building a message with `clientBuild.gameInput(...)` is payload-
1090
+ * checked at the call site — the compiler refuses to ship a wrong
1091
+ * shape.
1092
+ *
1093
+ * 2. Parsing an inbound message via `dispatch(raw, handlers)` narrows the
1094
+ * payload type inside each handler automatically, replacing the
1095
+ * unsafe `switch (msg.type) { case MSG.X: (payload as any).foo }`
1096
+ * pattern.
1097
+ *
1098
+ * 3. Tightening `BaseRoom.sendTo` / `BaseRoom.broadcast` /
1099
+ * `HandlerContext.send` / `SubscriptionContext.send` to accept
1100
+ * `ServerMessage` turns the type layer into enforcement: any room
1101
+ * that ships a payload inconsistent with its declared arm fails to
1102
+ * compile. Without that, the discriminated union is documentation,
1103
+ * not contract.
1104
+ *
1105
+ * 4. Adding a new `MSG.*` is localized: one entry in the discriminated
1106
+ * union, one builder function, one handler key in every dispatcher
1107
+ * that cares. No grep-and-fix across the codebase.
1108
+ *
1109
+ * 5. Apps can extend the SDK protocol without forking: `dispatch<M>` is
1110
+ * generic over any `M extends ProtocolMessage`, and builders are
1111
+ * plain objects so apps compose via spread (`{ ...clientBuild,
1112
+ * myMessage: ... }`).
1113
+ *
1114
+ * Direction split
1115
+ * ---------------
1116
+ *
1117
+ * Some message types carry different payloads depending on who's sending.
1118
+ * `MSG.GAME_START`, for example, is `{}` when the client requests a start
1119
+ * but `{ state, tick }` when the server broadcasts the start event.
1120
+ * `MSG.CANVAS_ADD` is a flat shape dict on the way in and a `{ shape }`
1121
+ * wrapper on the way out. Modelling these with one union would force
1122
+ * handlers to juggle a union payload — clunky and error-prone. Instead we
1123
+ * split by direction:
1124
+ *
1125
+ * - `ClientMessage` — what the client sends to the server
1126
+ * - `ServerMessage` — what the server sends to the client
1127
+ * - `ProtocolMessage = ClientMessage | ServerMessage` (for code that
1128
+ * really doesn't care — avoid when possible)
1129
+ *
1130
+ * Each side gets its own builder (`clientBuild` / `serverBuild`) and each
1131
+ * side's dispatcher is parameterised with the union it expects.
1132
+ *
1133
+ * Payload strictness
1134
+ * ------------------
1135
+ *
1136
+ * Where payload shapes are stable + narrow (ids, flags), we type them
1137
+ * precisely. Where they're opaque or escape the protocol layer (record
1138
+ * data blobs, Yjs binary frames, game-engine state), we use `unknown` and
1139
+ * defer narrowing to the caller. This is intentional: over-typing opaque
1140
+ * payloads would require the protocol layer to import application types
1141
+ * and defeat the "thin wire contract" goal.
1142
+ */
1143
+
1144
+ /**
1145
+ * The outer shape of every wire message. `T` is the string discriminator
1146
+ * (e.g. `"game.input"`) — keeping it as a generic literal type lets the
1147
+ * discriminated-union narrowing in `dispatch()` pick the right payload.
1148
+ *
1149
+ * Callers extending the protocol should pass a string-literal type for
1150
+ * `T`, not the widened `string`. `BaseMessage<string, P>` collapses the
1151
+ * discriminated union and handler-map key inference falls back to a
1152
+ * single untyped `string` key, losing all narrowing.
1153
+ */
1154
+ interface BaseMessage<T extends string, P> {
1155
+ type: T;
1156
+ payload: P;
1157
+ }
1158
+ /** Matches when a payload is intentionally empty — `{}` on the wire. */
1159
+ type EmptyPayload = Record<string, never>;
1160
+ /**
1161
+ * Every message the server can send. Room and handler `send` / `broadcast`
1162
+ * signatures are tightened to this union so outbound payloads are
1163
+ * compile-checked against the wire contract. As with `ClientMessage`,
1164
+ * extend via a string-literal union arm in app code when adding new
1165
+ * server-side broadcasts.
1166
+ */
1167
+ type ServerMessage = BaseMessage<typeof MSG.QUERY_RESULT, {
1168
+ subscriptionId: string;
1169
+ records: unknown[];
1170
+ }> | BaseMessage<typeof MSG.RECORD_CHANGE, {
1171
+ collection: string;
1172
+ record: unknown;
1173
+ changeType: 'create' | 'update' | 'delete';
1174
+ }> | BaseMessage<typeof MSG.ERROR, {
1175
+ error: string;
1176
+ subscriptionId?: string;
1177
+ }> | BaseMessage<typeof MSG.ACK, {
1178
+ requestId: string;
1179
+ success: true;
1180
+ recordId?: string;
1181
+ } | {
1182
+ requestId: string;
1183
+ success: false;
1184
+ error: string;
1185
+ }> | BaseMessage<typeof MSG.RESUBSCRIBE, EmptyPayload> | BaseMessage<typeof MSG.LIST_SCHEMAS, {
1186
+ schemas: unknown;
1187
+ }> | BaseMessage<typeof MSG.USER_INFO, unknown> | BaseMessage<typeof MSG.USER_LIST, {
1188
+ users: unknown[];
1189
+ }> | BaseMessage<typeof MSG.YJS_JOIN, {
1190
+ collection: string;
1191
+ recordId: string;
1192
+ fieldName: string;
1193
+ canWrite: boolean;
1194
+ }> | BaseMessage<typeof MSG.GAME_STATE, {
1195
+ state: unknown;
1196
+ tick: number;
1197
+ players: unknown[];
1198
+ running: boolean;
1199
+ }> | BaseMessage<typeof MSG.GAME_TICK, {
1200
+ state: unknown;
1201
+ tick: number;
1202
+ }> | BaseMessage<typeof MSG.GAME_START, {
1203
+ state: unknown;
1204
+ tick: number;
1205
+ }> | BaseMessage<typeof MSG.GAME_END, {
1206
+ state: unknown;
1207
+ tick: number;
1208
+ }> | BaseMessage<typeof MSG.GAME_PLAYER_JOIN, {
1209
+ player: unknown;
1210
+ }> | BaseMessage<typeof MSG.GAME_PLAYER_LEAVE, {
1211
+ userId: string;
1212
+ }> | BaseMessage<typeof MSG.GAME_PLAYER_READY, {
1213
+ userId: string;
1214
+ }> | BaseMessage<typeof MSG.CANVAS_SHAPES, {
1215
+ shapes: unknown[];
1216
+ viewports: unknown[];
1217
+ }> | BaseMessage<typeof MSG.CANVAS_ADD, {
1218
+ shape: unknown;
1219
+ }> | BaseMessage<typeof MSG.CANVAS_MOVE, {
1220
+ shapeId: string;
1221
+ x: number;
1222
+ y: number;
1223
+ }> | BaseMessage<typeof MSG.CANVAS_RESIZE, {
1224
+ shapeId: string;
1225
+ width: number;
1226
+ height: number;
1227
+ x?: number;
1228
+ y?: number;
1229
+ }> | BaseMessage<typeof MSG.CANVAS_DELETE, {
1230
+ shapeId: string;
1231
+ }> | BaseMessage<typeof MSG.CANVAS_UPDATE, {
1232
+ shapeId: string;
1233
+ props: Record<string, unknown>;
1234
+ }> | BaseMessage<typeof MSG.CANVAS_VIEWPORT, {
1235
+ viewport: unknown;
1236
+ } | {
1237
+ userId: string;
1238
+ removed: true;
1239
+ }> | BaseMessage<typeof MSG.CRON_TASKS, {
1240
+ tasks: unknown;
1241
+ }> | BaseMessage<typeof MSG.CRON_HISTORY, {
1242
+ history: unknown;
1243
+ }> | BaseMessage<typeof MSG.CRON_STATUS, {
1244
+ tasks: unknown;
1245
+ recentHistory: unknown;
1246
+ }> | BaseMessage<typeof MSG.PRESENCE_SYNC, {
1247
+ peers: unknown[];
1248
+ }> | BaseMessage<typeof MSG.PRESENCE_JOIN, {
1249
+ peer: unknown;
1250
+ }> | BaseMessage<typeof MSG.PRESENCE_LEAVE, {
1251
+ userId: string;
1252
+ }> | BaseMessage<typeof MSG.PRESENCE_UPDATE, {
1253
+ userId: string;
1254
+ state: Record<string, unknown>;
1255
+ }> | BaseMessage<typeof MSG.GW_SCOPE_ERROR, {
1256
+ scopeType: string;
1257
+ scopeId: string;
1258
+ error: string;
1259
+ }> | BaseMessage<typeof MSG.GW_USER_UPDATE, unknown>;
1260
+
1261
+ /**
1262
+ * BaseRoom — Abstract base class for all DeepSpace Durable Objects.
1263
+ *
1264
+ * Provides:
1265
+ * - WebSocket upgrade with Cloudflare hibernation API
1266
+ * - Connection tracking (WebSocket -> UserAttachment)
1267
+ * - Auth: parse JWT-verified user info from URL search params
1268
+ * - Presence: connected users list, awareness on connect/disconnect
1269
+ * - Message routing: JSON parse -> dispatch by `type` field, binary hook
1270
+ * - Raw SQLite access via this.sql
1271
+ * - Broadcast helpers: broadcast(), sendTo()
1272
+ * - HTTP fetch handler with WebSocket upgrade detection
1273
+ *
1274
+ * Subclasses implement lifecycle hooks:
1275
+ * onConnect, onMessage, onBinaryMessage, onDisconnect, onRequest, onAlarm
1276
+ */
1277
+
1278
+ interface UserAttachment {
1279
+ userId: string;
1280
+ userName: string;
1281
+ userEmail: string;
1282
+ userImageUrl?: string;
1283
+ /** Subclass-specific data serialized alongside user info */
1284
+ [key: string]: unknown;
1285
+ }
1286
+ declare abstract class BaseRoom<E = Record<string, unknown>> {
1287
+ protected state: DurableObjectState;
1288
+ protected env: E;
1289
+ protected sql: SqlStorage;
1290
+ constructor(state: DurableObjectState, env: unknown);
1291
+ fetch(request: Request): Promise<Response>;
1292
+ private handleWebSocketUpgrade;
1293
+ webSocketMessage(ws: WebSocket, message: ArrayBuffer | string): Promise<void>;
1294
+ webSocketClose(ws: WebSocket, code: number, reason: string): Promise<void>;
1295
+ webSocketError(ws: WebSocket, error: unknown): Promise<void>;
1296
+ alarm(): Promise<void>;
1297
+ /**
1298
+ * Called when a new WebSocket connects (after auth parsing).
1299
+ * Return an augmented attachment to serialize on the WebSocket,
1300
+ * or void to use the default attachment.
1301
+ */
1302
+ protected onConnect(ws: WebSocket, user: UserAttachment): UserAttachment | void | Promise<UserAttachment | void>;
1303
+ /**
1304
+ * Called for each parsed JSON message.
1305
+ */
1306
+ protected abstract onMessage(ws: WebSocket, user: UserAttachment, message: {
1307
+ type: string;
1308
+ [key: string]: unknown;
1309
+ }): void | Promise<void>;
1310
+ /**
1311
+ * Called for binary messages (Yjs, custom protocols).
1312
+ */
1313
+ protected onBinaryMessage?(ws: WebSocket, user: UserAttachment, data: ArrayBuffer): void | Promise<void>;
1314
+ /**
1315
+ * Called when a WebSocket disconnects.
1316
+ */
1317
+ protected onDisconnect(ws: WebSocket, user: UserAttachment): void | Promise<void>;
1318
+ /**
1319
+ * Called for HTTP requests that are NOT WebSocket upgrades.
1320
+ */
1321
+ protected onRequest?(request: Request): Response | Promise<Response>;
1322
+ /**
1323
+ * Called on DO alarm.
1324
+ */
1325
+ protected onAlarm?(): void | Promise<void>;
1326
+ /**
1327
+ * Get all connected WebSockets.
1328
+ */
1329
+ protected getWebSockets(): WebSocket[];
1330
+ /**
1331
+ * Get the user attachment for a WebSocket.
1332
+ */
1333
+ protected getAttachment(ws: WebSocket): UserAttachment | null;
1334
+ /**
1335
+ * Get all currently connected users.
1336
+ */
1337
+ protected getConnectedUsers(): UserAttachment[];
1338
+ /**
1339
+ * Send a JSON message to a specific WebSocket.
1340
+ *
1341
+ * Typed as `ServerMessage` so every room's outbound traffic is
1342
+ * compile-checked against the wire protocol contract. Passing
1343
+ * `{ type: 'whatever', payload: {...} }` with a non-matching arm fails
1344
+ * to compile — that's the whole point of the typed layer. Apps that
1345
+ * need to send an app-specific message should override `sendTo` in
1346
+ * their subclass with a widened union (`ServerMessage | MyAppMessage`).
1347
+ */
1348
+ protected sendTo(ws: WebSocket, message: ServerMessage): void;
1349
+ /**
1350
+ * Send binary data to a specific WebSocket.
1351
+ */
1352
+ protected sendBinaryTo(ws: WebSocket, data: Uint8Array | ArrayBuffer): void;
1353
+ /**
1354
+ * Broadcast a JSON message to all connected WebSockets.
1355
+ * Optionally exclude a specific WebSocket (e.g. the sender).
1356
+ *
1357
+ * See `sendTo` for the reasoning behind typing as `ServerMessage`.
1358
+ */
1359
+ protected broadcast(message: ServerMessage, exclude?: WebSocket): void;
1360
+ /**
1361
+ * Broadcast binary data to all connected WebSockets.
1362
+ */
1363
+ protected broadcastBinary(data: Uint8Array | ArrayBuffer, exclude?: WebSocket): void;
1364
+ }
1365
+
1366
+ /**
1367
+ * Server-specific protocol types
1368
+ *
1369
+ * These depend on Cloudflare Workers / Yjs imports and can't live in shared/types.
1370
+ * All other protocol types (Query, payloads, etc.) live in shared/types/index.ts.
1371
+ */
1372
+
1373
+ /** Stored on WebSocket attachment (survives hibernation) */
1374
+ interface ConnectionAttachment extends UserAttachment {
1375
+ role: string;
1376
+ subscriptions: Subscription[];
1377
+ /** Yjs docs this connection is editing */
1378
+ yjsSubscriptions: YjsSubscription[];
1379
+ /** Yjs client ID for awareness */
1380
+ yjsClientId?: number;
1381
+ /** Client-side Yjs awareness clientId (extracted from first awareness message) */
1382
+ awarenessClientId?: number;
1383
+ }
1384
+
1385
+ /**
1386
+ * Collection Schema Definitions & Validation
1387
+ *
1388
+ * All collections use typed SQL columns. No document-mode / fields-based storage.
1389
+ */
1390
+ type ColumnInterpretation = {
1391
+ kind: 'plain';
1392
+ } | {
1393
+ kind: 'currency';
1394
+ symbol: string;
1395
+ decimals: number;
1396
+ } | {
1397
+ kind: 'date';
1398
+ format?: string;
1399
+ } | {
1400
+ kind: 'datetime';
1401
+ format?: string;
1402
+ } | {
1403
+ kind: 'boolean';
1404
+ trueLabel?: string;
1405
+ falseLabel?: string;
1406
+ } | {
1407
+ kind: 'percent';
1408
+ decimals?: number;
1409
+ } | {
1410
+ kind: 'select';
1411
+ options: string[];
1412
+ } | {
1413
+ kind: 'multiselect';
1414
+ options: string[];
1415
+ } | {
1416
+ kind: 'url';
1417
+ } | {
1418
+ kind: 'email';
1419
+ } | {
1420
+ kind: 'json';
1421
+ } | {
1422
+ kind: 'reference';
1423
+ targetTable: string;
1424
+ displayColumn: string;
1425
+ };
1426
+ interface ColumnDefinition {
1427
+ /** Stable ID override (survives renames). Falls back to `col_{name}`. */
1428
+ id?: string;
1429
+ name: string;
1430
+ storage: 'number' | 'text';
1431
+ interpretation: ColumnInterpretation | string;
1432
+ expression?: string;
1433
+ /** Auto-populate with current user ID on create. */
1434
+ userBound?: boolean;
1435
+ /** Cannot be changed after initial creation. */
1436
+ immutable?: boolean;
1437
+ /** Must be provided on create (non-null). */
1438
+ required?: boolean;
1439
+ /** Default value if not provided on create. */
1440
+ default?: unknown;
1441
+ /** Auto-set ISO timestamp when the named field changes (optionally to a specific value). */
1442
+ timestampTrigger?: {
1443
+ field: string;
1444
+ value?: unknown;
1445
+ };
1446
+ }
1447
+ interface ResolvedColumn {
1448
+ id: string;
1449
+ name: string;
1450
+ storage: 'number' | 'text';
1451
+ interpretation: ColumnInterpretation;
1452
+ expression?: string;
1453
+ readonly: boolean;
1454
+ userBound?: boolean;
1455
+ immutable?: boolean;
1456
+ required?: boolean;
1457
+ default?: unknown;
1458
+ timestampTrigger?: {
1459
+ field: string;
1460
+ value?: unknown;
1461
+ };
1462
+ }
1463
+ declare function collectionTableName(name: string): string;
1464
+ declare function columnId(name: string): string;
1465
+ declare function resolveColumn(col: ColumnDefinition): ResolvedColumn;
1466
+ declare function rowToData(row: Record<string, unknown>, columns: ResolvedColumn[]): Record<string, unknown>;
1467
+ declare function dataToColumnValues(data: Record<string, unknown>, columns: ResolvedColumn[]): Record<string, unknown>;
1468
+ declare function coerceValue(value: unknown, storage: 'number' | 'text', interpretation: ColumnInterpretation): unknown;
1469
+ declare function buildTableSelect(collectionName: string, columns: ResolvedColumn[]): string;
1470
+ type PermissionLevel = boolean | 'own' | 'unclaimed-or-own' | 'collaborator' | 'team' | 'access' | 'published' | 'shared';
1471
+ interface RolePermissions {
1472
+ read: PermissionLevel;
1473
+ create: boolean;
1474
+ update: PermissionLevel;
1475
+ delete: PermissionLevel;
1476
+ /** If set, only these columns can be updated by this role. */
1477
+ writableFields?: string[];
1478
+ }
1479
+ interface CollectionSchema {
1480
+ name: string;
1481
+ /** Column definitions — every collection is stored in a typed SQL table. */
1482
+ columns: ColumnDefinition[];
1483
+ /** Composite uniqueness constraint (e.g., ['userId', 'taskId']). */
1484
+ uniqueOn?: string[];
1485
+ /** Column name used for ownership checks (default: `_created_by`). */
1486
+ ownerField?: string;
1487
+ /** Column containing JSON array of collaborator user IDs. */
1488
+ collaboratorsField?: string;
1489
+ /** Column containing team ID for team-based access. */
1490
+ teamField?: string;
1491
+ /**
1492
+ * Column controlling per-record read visibility.
1493
+ * String: visible when `data[field] === 'public'`.
1494
+ * Object: visible when `data[field] === value`.
1495
+ */
1496
+ visibilityField?: string | {
1497
+ field: string;
1498
+ value: unknown;
1499
+ };
1500
+ /** Permissions per role. Use '*' for a catch-all fallback. */
1501
+ permissions: Record<string, RolePermissions>;
1502
+ /** Default role for new users (only on 'users' collection). */
1503
+ defaultRole?: string;
1504
+ }
1505
+ interface User {
1506
+ id: string;
1507
+ email: string;
1508
+ name: string;
1509
+ imageUrl?: string;
1510
+ role: string;
1511
+ createdAt: string;
1512
+ lastSeenAt: string;
1513
+ }
1514
+ interface StoredRecord {
1515
+ collection: string;
1516
+ recordId: string;
1517
+ data: Record<string, unknown>;
1518
+ createdBy: string;
1519
+ createdAt: string;
1520
+ updatedAt: string;
1521
+ }
1522
+ interface PermissionContext {
1523
+ isTeamMember: (teamId: string, userId: string) => boolean;
1524
+ }
1525
+ declare const noopPermissionContext: PermissionContext;
1526
+ declare function getRolePermissions(schema: CollectionSchema, role: string): RolePermissions;
1527
+ declare function isOwner(schema: CollectionSchema, record: {
1528
+ data: Record<string, unknown>;
1529
+ createdBy: string;
1530
+ }, userId: string): boolean;
1531
+ declare function canRead(schema: CollectionSchema, role: string, record: {
1532
+ data: Record<string, unknown>;
1533
+ createdBy: string;
1534
+ recordId?: string;
1535
+ }, userId: string, ctx?: PermissionContext): boolean;
1536
+ declare function canCreate(schema: CollectionSchema, role: string): boolean;
1537
+ declare function canUpdate(schema: CollectionSchema, role: string, record: {
1538
+ data: Record<string, unknown>;
1539
+ createdBy: string;
1540
+ recordId?: string;
1541
+ }, userId: string, ctx?: PermissionContext): boolean;
1542
+ declare function canDelete(schema: CollectionSchema, role: string, record: {
1543
+ data: Record<string, unknown>;
1544
+ createdBy: string;
1545
+ recordId?: string;
1546
+ }, userId: string, ctx?: PermissionContext): boolean;
1547
+ /** Check if a field update violates writableFields restrictions. */
1548
+ declare function checkFieldPermissions(schema: CollectionSchema, role: string, newData: Record<string, unknown>, existingData?: Record<string, unknown>): string | null;
1549
+ /** Names of columns managed by the system (registerUser), not client mutations. */
1550
+ declare const SYSTEM_MANAGED_COLUMNS: Set<string>;
1551
+ /** Standard user columns. Apps spread these into their users schema. */
1552
+ declare const USERS_COLUMNS: ColumnDefinition[];
1553
+ declare const BASE_USERS_SCHEMA: CollectionSchema;
1554
+ /**
1555
+ * Lint a CollectionSchema for declarations that look like they should
1556
+ * enforce something but don't, due to interactions between top-level
1557
+ * fields (visibilityField, ownerField) and per-role permission levels.
1558
+ *
1559
+ * The SDK has historically silently accepted schemas that imply more
1560
+ * enforcement than they actually deliver — e.g., `visibilityField` set
1561
+ * but every role's `read: true` means "anyone can read everything"
1562
+ * regardless of `visibility`. Warn loudly at registration so app
1563
+ * authors notice before shipping a privacy bug.
1564
+ *
1565
+ * Returns an array of warning messages. Empty = clean.
1566
+ */
1567
+ declare function lintSchema(schema: CollectionSchema): string[];
1568
+ declare class SchemaRegistry {
1569
+ private trusted;
1570
+ constructor(schemas?: CollectionSchema[]);
1571
+ registerTrusted(schema: CollectionSchema): void;
1572
+ get(name: string): CollectionSchema | undefined;
1573
+ has(name: string): boolean;
1574
+ hasTrusted(name: string): boolean;
1575
+ all(): CollectionSchema[];
1576
+ names(): string[];
1577
+ }
1578
+ type PermissionSource = 'explicit' | 'wildcard' | 'default-deny';
1579
+ interface ResolvedPermission {
1580
+ level: PermissionLevel | boolean;
1581
+ source: PermissionSource;
1582
+ }
1583
+ interface CollectionPermissionSummary {
1584
+ collection: string;
1585
+ ownerField?: string;
1586
+ collaboratorsField?: string;
1587
+ teamField?: string;
1588
+ columns: ColumnDefinition[];
1589
+ permissions: Record<string, {
1590
+ read: ResolvedPermission;
1591
+ create: ResolvedPermission;
1592
+ update: ResolvedPermission;
1593
+ delete: ResolvedPermission;
1594
+ writableFields?: string[];
1595
+ }>;
1596
+ }
1597
+ interface PermissionAnalysis {
1598
+ roles: string[];
1599
+ collections: CollectionPermissionSummary[];
1600
+ }
1601
+ declare function analyzePermissions(schemas: CollectionSchema[]): PermissionAnalysis;
1602
+
1603
+ /**
1604
+ * RecordRoom Durable Object
1605
+ *
1606
+ * SQLite-based storage with query-based real-time subscriptions.
1607
+ * Extends BaseRoom for WebSocket/connection infrastructure.
1608
+ *
1609
+ * Architecture:
1610
+ * - Data stored in SQLite (single `records` table)
1611
+ * - Clients subscribe to QUERIES, not collections
1612
+ * - On record change, server evaluates which subscriptions match
1613
+ * - Only matching subscribers receive updates
1614
+ *
1615
+ * Protocol:
1616
+ * - SUBSCRIBE { subscriptionId, query } → QUERY_RESULT { subscriptionId, records }
1617
+ * - UNSUBSCRIBE { subscriptionId }
1618
+ * - PUT { collection, recordId, data } → broadcasts RECORD_CHANGE to matching
1619
+ * - DELETE { collection, recordId } → broadcasts RECORD_CHANGE to matching
1620
+ */
1621
+
1622
+ /**
1623
+ * RecordRoom configuration options
1624
+ */
1625
+ interface RecordRoomConfig {
1626
+ /**
1627
+ * User ID of the app owner.
1628
+ * This user automatically gets 'admin' role on connect.
1629
+ */
1630
+ ownerUserId?: string;
1631
+ }
1632
+ /**
1633
+ * RecordRoom Durable Object
1634
+ */
1635
+ declare class RecordRoom<E = Record<string, unknown>> extends BaseRoom<E> {
1636
+ private schemaRegistry;
1637
+ private initPromise;
1638
+ /** Yjs docs loaded in memory (key: collection:recordId:fieldName) */
1639
+ private yjsDocs;
1640
+ /** Next Yjs client ID counter */
1641
+ private nextYjsClientId;
1642
+ /** Owner user ID — gets admin role automatically */
1643
+ private ownerUserId;
1644
+ /** True until the first fetch() completes — detects hibernation wake-up */
1645
+ private freshConstruct;
1646
+ constructor(state: DurableObjectState, env: unknown, schemas?: CollectionSchema[], config?: RecordRoomConfig);
1647
+ private getPermissionContext;
1648
+ fetch(request: Request): Promise<Response>;
1649
+ /** Timing info from the current fetch(), used by onConnect for logging */
1650
+ private _fetchTiming;
1651
+ private ensureInitialized;
1652
+ private initializeDatabase;
1653
+ private migrateUsersTableIfExists;
1654
+ private migrateRecordsTable;
1655
+ private ensureCollectionTable;
1656
+ private ensureAllCollectionTables;
1657
+ protected onConnect(ws: WebSocket, user: UserAttachment): Promise<ConnectionAttachment>;
1658
+ protected onMessage(ws: WebSocket, user: UserAttachment, msg: {
1659
+ type: string;
1660
+ [key: string]: unknown;
1661
+ }): Promise<void>;
1662
+ protected onBinaryMessage(ws: WebSocket, user: UserAttachment, data: ArrayBuffer): Promise<void>;
1663
+ protected onDisconnect(ws: WebSocket, user: UserAttachment): void;
1664
+ webSocketMessage(ws: WebSocket, message: ArrayBuffer | string): Promise<void>;
1665
+ webSocketClose(ws: WebSocket, code: number, reason: string): Promise<void>;
1666
+ webSocketError(ws: WebSocket, error: unknown): Promise<void>;
1667
+ private handleRecordMessage;
1668
+ private handleListSchemas;
1669
+ private createHandlerContext;
1670
+ private createRecordContext;
1671
+ private createUserContext;
1672
+ private createYjsContext;
1673
+ private send;
1674
+ private sendBinaryHelper;
1675
+ }
1676
+
1677
+ /**
1678
+ * YjsRoom — Lightweight Durable Object for collaborative Yjs documents.
1679
+ * Extends BaseRoom for WebSocket/connection infrastructure.
1680
+ *
1681
+ * Unlike RecordRoom (schemas, RBAC, queries, user state), YjsRoom is
1682
+ * purpose-built for Yjs: sync, relay, persist. One DO per document.
1683
+ *
1684
+ * Architecture (SOTA for Yjs + Cloudflare DOs):
1685
+ * - Auth verified at the worker edge, role passed to DO via URL params
1686
+ * - DO is a thin Yjs sync relay: receive → apply → persist → broadcast
1687
+ * - Viewers can observe but not write; members/admins can write
1688
+ * - State persisted as a single binary blob in SQLite
1689
+ *
1690
+ * Uses the shared yjs-protocol.ts encoding utilities — no duplication.
1691
+ */
1692
+
1693
+ interface YjsAttachment extends UserAttachment {
1694
+ role: string;
1695
+ canWrite: boolean;
1696
+ awarenessClientId: number | null;
1697
+ }
1698
+ declare class YjsRoom<E = Record<string, unknown>> extends BaseRoom<E> {
1699
+ private doc;
1700
+ private initialized;
1701
+ private awarenessStates;
1702
+ constructor(state: DurableObjectState, env: unknown);
1703
+ private ensureInitialized;
1704
+ private getDoc;
1705
+ private persistDoc;
1706
+ protected onConnect(ws: WebSocket, user: UserAttachment): YjsAttachment;
1707
+ protected onMessage(ws: WebSocket, user: UserAttachment, message: {
1708
+ type: string;
1709
+ [key: string]: unknown;
1710
+ }): void;
1711
+ protected onBinaryMessage(ws: WebSocket, user: UserAttachment, data: ArrayBuffer): void;
1712
+ protected onDisconnect(ws: WebSocket, user: UserAttachment): void;
1713
+ private handleSync;
1714
+ private handleAwareness;
1715
+ private readAwarenessUpdates;
1716
+ private encodeAwarenessMessage;
1717
+ private sendAwarenessSnapshot;
1718
+ private broadcastRaw;
1719
+ }
1720
+
1721
+ /**
1722
+ * GameRoom — Authoritative game loop Durable Object.
1723
+ *
1724
+ * Extends BaseRoom with:
1725
+ * - Alarm-based tick loop with configurable interval
1726
+ * - Player management (join, leave, ready state)
1727
+ * - Input collection per tick, authoritative state computation
1728
+ * - State broadcast to all connected players
1729
+ *
1730
+ * Subclasses implement game logic via lifecycle hooks:
1731
+ * onTick, onPlayerJoin, onPlayerLeave, onGameStart, onGameEnd
1732
+ *
1733
+ * Message types: game.*
1734
+ */
1735
+
1736
+ interface GameRoomConfig {
1737
+ /** Ticks per second (default: 20) */
1738
+ tickRate?: number;
1739
+ /** Minimum players to start (default: 1) */
1740
+ minPlayers?: number;
1741
+ /** Maximum players (default: unlimited) */
1742
+ maxPlayers?: number;
1743
+ }
1744
+ interface Player {
1745
+ userId: string;
1746
+ userName: string;
1747
+ ready: boolean;
1748
+ connectedAt: string;
1749
+ data: Record<string, unknown>;
1750
+ }
1751
+ interface GameInput {
1752
+ userId: string;
1753
+ action: string;
1754
+ data: Record<string, unknown>;
1755
+ tick: number;
1756
+ }
1757
+ interface GameAttachment extends UserAttachment {
1758
+ joinedAt: string;
1759
+ }
1760
+ declare abstract class GameRoom<E = Record<string, unknown>> extends BaseRoom<E> {
1761
+ private config;
1762
+ private players;
1763
+ private inputBuffer;
1764
+ private currentTick;
1765
+ private gameState;
1766
+ private running;
1767
+ private initialized;
1768
+ constructor(state: DurableObjectState, env: unknown, config?: GameRoomConfig);
1769
+ private ensureInitialized;
1770
+ private persistState;
1771
+ protected onConnect(ws: WebSocket, user: UserAttachment): GameAttachment;
1772
+ protected onMessage(ws: WebSocket, user: UserAttachment, message: {
1773
+ type: string;
1774
+ [key: string]: unknown;
1775
+ }): Promise<void>;
1776
+ protected onDisconnect(ws: WebSocket, user: UserAttachment): void;
1777
+ protected onAlarm(): Promise<void>;
1778
+ private checkAutoStart;
1779
+ private startGame;
1780
+ private stopGame;
1781
+ protected getGameState(): Record<string, unknown>;
1782
+ protected setGameState(state: Record<string, unknown>): void;
1783
+ protected getPlayers(): Player[];
1784
+ protected isRunning(): boolean;
1785
+ protected getCurrentTick(): number;
1786
+ /**
1787
+ * Called each tick with current state and collected inputs.
1788
+ * Return the new game state, or undefined to keep current state.
1789
+ */
1790
+ protected abstract onTick(state: Record<string, unknown>, inputs: GameInput[], tick: number): Record<string, unknown> | undefined | Promise<Record<string, unknown> | undefined>;
1791
+ /** Called when a player connects */
1792
+ protected onPlayerJoin(player: Player): void;
1793
+ /** Called when a player disconnects */
1794
+ protected onPlayerLeave(player: Player): void;
1795
+ /** Called when the game starts */
1796
+ protected onGameStart(): void;
1797
+ /** Called when the game ends */
1798
+ protected onGameEnd(finalState: Record<string, unknown>): void;
1799
+ /**
1800
+ * Called once when the DO first hydrates persisted state from storage.
1801
+ * Receives the parsed state blob as it was written by a previous build.
1802
+ * Return the state object to install as `gameState`.
1803
+ *
1804
+ * Subclasses with evolving schemas should override this hook to:
1805
+ * - merge new fields onto a default template,
1806
+ * - upgrade shapes across versioned states,
1807
+ * - or discard stale blobs entirely by returning a fresh object.
1808
+ *
1809
+ * The default implementation is a pass-through, preserving the legacy
1810
+ * "stored blob is gospel" behavior for subclasses that don't care.
1811
+ *
1812
+ * If JSON parsing of the stored blob fails this hook is NOT called — the
1813
+ * DO starts with an empty state and the subclass's `onGameStart` (or
1814
+ * first `onTick`) is responsible for initializing.
1815
+ */
1816
+ protected onHydrateState(stored: Record<string, unknown>): Record<string, unknown>;
1817
+ }
1818
+
1819
+ /**
1820
+ * CanvasRoom — Spatial canvas Durable Object (tldraw-style).
1821
+ *
1822
+ * Extends BaseRoom with Yjs-backed spatial operations.
1823
+ * Each shape is a Y.Map entry, enabling multi-user concurrent editing.
1824
+ *
1825
+ * Features:
1826
+ * - Shape CRUD (add, move, resize, delete, update properties)
1827
+ * - Viewport awareness (each user's visible region)
1828
+ * - Per-user undo/redo stacks
1829
+ *
1830
+ * Message types: canvas.*
1831
+ */
1832
+
1833
+ interface CanvasShape {
1834
+ id: string;
1835
+ type: string;
1836
+ x: number;
1837
+ y: number;
1838
+ width: number;
1839
+ height: number;
1840
+ rotation?: number;
1841
+ props: Record<string, unknown>;
1842
+ createdBy: string;
1843
+ createdAt: string;
1844
+ updatedAt: string;
1845
+ }
1846
+ interface Viewport {
1847
+ userId: string;
1848
+ x: number;
1849
+ y: number;
1850
+ width: number;
1851
+ height: number;
1852
+ zoom: number;
1853
+ }
1854
+ interface CanvasAttachment extends UserAttachment {
1855
+ viewport: Viewport | null;
1856
+ }
1857
+ declare class CanvasRoom<E = Record<string, unknown>> extends BaseRoom<E> {
1858
+ private doc;
1859
+ private initialized;
1860
+ private viewports;
1861
+ private undoStacks;
1862
+ private redoStacks;
1863
+ constructor(state: DurableObjectState, env: unknown);
1864
+ private ensureInitialized;
1865
+ private getDoc;
1866
+ private persistDoc;
1867
+ private getShapesMap;
1868
+ fetch(request: Request): Promise<Response>;
1869
+ protected onConnect(ws: WebSocket, user: UserAttachment): CanvasAttachment;
1870
+ protected onMessage(ws: WebSocket, user: UserAttachment, message: {
1871
+ type: string;
1872
+ [key: string]: unknown;
1873
+ }): Promise<void>;
1874
+ protected onDisconnect(ws: WebSocket, user: UserAttachment): void;
1875
+ private pushUndo;
1876
+ private clearRedo;
1877
+ private handleUndo;
1878
+ private handleRedo;
1879
+ private getAllShapes;
1880
+ }
1881
+
1882
+ /**
1883
+ * PresenceRoom — Ephemeral presence-tracking Durable Object.
1884
+ *
1885
+ * Extends BaseRoom. No SQLite — purely in-memory presence state.
1886
+ * Tracks who is present in a given scope (canvas, doc, thread, etc.)
1887
+ * and broadcasts join/leave/state-update events to all connected peers.
1888
+ *
1889
+ * Each scope ID maps to its own DO instance. Clients connect via
1890
+ * /ws/presence/:scopeId and receive real-time presence for that scope.
1891
+ *
1892
+ * Peers can attach arbitrary state (cursor position, typing indicator,
1893
+ * viewport, selection, etc.) via MSG.PRESENCE_UPDATE.
1894
+ *
1895
+ * Message types: presence.*
1896
+ */
1897
+
1898
+ interface PresencePeer {
1899
+ userId: string;
1900
+ userName: string;
1901
+ userEmail: string;
1902
+ userImageUrl?: string;
1903
+ joinedAt: string;
1904
+ /** Arbitrary per-user state (cursor, typing, viewport, etc.) */
1905
+ state: Record<string, unknown>;
1906
+ }
1907
+ interface PresenceAttachment extends UserAttachment {
1908
+ joinedAt: string;
1909
+ }
1910
+ declare class PresenceRoom<E = Record<string, unknown>> extends BaseRoom<E> {
1911
+ private peers;
1912
+ private peerSockets;
1913
+ constructor(state: DurableObjectState, env: unknown);
1914
+ /**
1915
+ * Durable Objects can hibernate and clear heap while Cloudflare keeps
1916
+ * WebSocket connections. Deserialize attachments from already-connected
1917
+ * sockets so `peers` matches reality before we send PRESENCE_SYNC.
1918
+ */
1919
+ private hydratePeersFromLiveSockets;
1920
+ protected onConnect(ws: WebSocket, user: UserAttachment): PresenceAttachment;
1921
+ protected onMessage(ws: WebSocket, user: UserAttachment, message: {
1922
+ type: string;
1923
+ [key: string]: unknown;
1924
+ }): Promise<void>;
1925
+ protected onDisconnect(ws: WebSocket, user: UserAttachment): void;
1926
+ }
1927
+
1928
+ /**
1929
+ * CronRoom — Per-app scheduled task execution Durable Object.
1930
+ *
1931
+ * Extends BaseRoom. One DO per app shards cron work and avoids the
1932
+ * dispatch-worker's global KV-poll bottleneck. The DO alarm triggers
1933
+ * `onTask(name)` on the configured cadence; each execution is recorded
1934
+ * to a per-app `cron_history` table. Subscribers (admin clients via the
1935
+ * `useCronMonitor` hook) get pushes over the WebSocket.
1936
+ *
1937
+ * Tasks declare *either* `intervalMinutes` (run every N minutes) *or*
1938
+ * `schedule` + `timezone` (5-field cron expression evaluated against an
1939
+ * IANA timezone via `Intl.DateTimeFormat`). Cron mode is DST-aware
1940
+ * because the wall-clock comparison happens after the timezone shift,
1941
+ * not before.
1942
+ *
1943
+ * Message types: cron.*
1944
+ */
1945
+
1946
+ interface CronTask {
1947
+ name: string;
1948
+ /** Interval in minutes (interval mode) — mutually exclusive with `schedule`. */
1949
+ intervalMinutes?: number;
1950
+ /** 5-field cron expression (cron mode) — requires `timezone`. */
1951
+ schedule?: string;
1952
+ /** IANA timezone string (e.g. "America/New_York"). Required with `schedule`. */
1953
+ timezone?: string;
1954
+ /** Whether the task starts paused. */
1955
+ paused?: boolean;
1956
+ }
1957
+ interface CronRoomConfig {
1958
+ tasks: CronTask[];
1959
+ }
1960
+ interface CronExecution {
1961
+ taskName: string;
1962
+ startedAt: string;
1963
+ completedAt: string | null;
1964
+ success: boolean;
1965
+ durationMs: number;
1966
+ error?: string;
1967
+ }
1968
+ declare abstract class CronRoom<E = Record<string, unknown>> extends BaseRoom<E> {
1969
+ private tasks;
1970
+ private initialized;
1971
+ constructor(state: DurableObjectState, env: unknown, config: CronRoomConfig);
1972
+ private ensureInitialized;
1973
+ fetch(request: Request): Promise<Response>;
1974
+ protected onConnect(ws: WebSocket, user: UserAttachment): UserAttachment;
1975
+ protected onMessage(ws: WebSocket, user: UserAttachment, message: {
1976
+ type: string;
1977
+ [key: string]: unknown;
1978
+ }): Promise<void>;
1979
+ protected onAlarm(): Promise<void>;
1980
+ private executeTask;
1981
+ private scheduleNextAlarm;
1982
+ private getTaskStates;
1983
+ private getRecentHistory;
1984
+ private broadcastStatus;
1985
+ /**
1986
+ * Execute a scheduled task by name.
1987
+ * Called both by the alarm scheduler and manual trigger.
1988
+ */
1989
+ protected abstract onTask(taskName: string): void | Promise<void>;
1990
+ }
1991
+
1992
+ /**
1993
+ * DO Manifest — Dynamic Durable Object binding declarations.
1994
+ *
1995
+ * Apps export a `__DO_MANIFEST__` array in their worker.ts.
1996
+ * The CLI extracts it and sends it to the deploy worker,
1997
+ * which uses it to generate dynamic CF API bindings and migrations.
1998
+ */
1999
+ interface DOManifestEntry {
2000
+ /** CF binding name, e.g. 'RECORD_ROOMS' */
2001
+ binding: string;
2002
+ /** Exported class name, e.g. 'AppRecordRoom' */
2003
+ className: string;
2004
+ /** Whether this DO uses SQLite storage */
2005
+ sqlite: boolean;
2006
+ }
2007
+ type DOManifest = DOManifestEntry[];
2008
+ /**
2009
+ * Utility type: auto-generates Env bindings from a manifest.
2010
+ *
2011
+ * @example
2012
+ * const manifest = [
2013
+ * { binding: 'RECORD_ROOMS', className: 'AppRecordRoom', sqlite: true },
2014
+ * { binding: 'GAME_ROOMS', className: 'AppGameRoom', sqlite: true },
2015
+ * ] as const satisfies DOManifest
2016
+ *
2017
+ * type Env = BaseEnv & DOBindings<typeof manifest>
2018
+ * // => { RECORD_ROOMS: DurableObjectNamespace; GAME_ROOMS: DurableObjectNamespace }
2019
+ */
2020
+ type DOBindings<T extends readonly DOManifestEntry[]> = {
2021
+ [K in T[number]['binding']]: DurableObjectNamespace;
2022
+ };
2023
+ /** Default manifest for apps that don't declare one */
2024
+ declare const DEFAULT_DO_MANIFEST: DOManifest;
2025
+ /**
2026
+ * Shape-validate a DO manifest received over the wire (e.g. from the CLI's
2027
+ * deploy form-field). Without this, malformed input gets passed straight to
2028
+ * `deployToWfP`'s `.filter(...).map(...)` chain and crashes the route mid-deploy.
2029
+ *
2030
+ * Mirrors the contract of `validateBindingManifest` for non-DO bindings.
2031
+ */
2032
+ declare function validateDoManifest(manifest: unknown): {
2033
+ valid: true;
2034
+ manifest: DOManifest;
2035
+ } | {
2036
+ valid: false;
2037
+ reason: string;
2038
+ };
2039
+
2040
+ /**
2041
+ * Binding Manifest — non-DO bindings declared by an app's wrangler.toml that
2042
+ * the deploy-worker should pass through to Cloudflare's WfP upload API.
2043
+ *
2044
+ * Apps don't export a `__BINDING_MANIFEST__`; the CLI extracts these from the
2045
+ * normalized vite/wrangler output config at deploy time. This file just owns
2046
+ * the types + validation so both sides (CLI client and deploy-worker server)
2047
+ * agree on the shape.
2048
+ */
2049
+ /**
2050
+ * A single non-DO binding the app declares. Mirrors CF's WfP binding API.
2051
+ *
2052
+ * Provisionable resources (d1, kv_namespace, vectorize, r2_bucket, queue) accept
2053
+ * the literal string `"auto"` in their ID field to request platform-side
2054
+ * provisioning at deploy time. When `"auto"` is used, the deploy-worker creates
2055
+ * the resource on the platform CF account, persists the resulting CF ID in the
2056
+ * app registry, and substitutes the real ID before forwarding to WfP. The
2057
+ * sentinel sticks around in this type because:
2058
+ * 1. `wrangler` parsing requires a non-empty string in the id field
2059
+ * 2. The CLI passes the unresolved manifest through to the deploy-worker
2060
+ * 3. The deploy-worker is the only side with CF API credentials
2061
+ *
2062
+ * Companion fields (`database_name`, `title`, `dimensions`, `metric`) are only
2063
+ * used when `"auto"` is set — they tell the provisioner how to create the
2064
+ * resource. After provisioning these fields are still present on the wire but
2065
+ * ignored by WfP.
2066
+ */
2067
+ type CustomBinding = {
2068
+ type: 'vectorize';
2069
+ name: string;
2070
+ /** Either a pre-existing index name or the literal `"auto"`. */
2071
+ index_name: string;
2072
+ /** Required when `index_name === "auto"`. */
2073
+ dimensions?: number;
2074
+ /** Required when `index_name === "auto"`. */
2075
+ metric?: 'cosine' | 'euclidean' | 'dot-product';
2076
+ } | {
2077
+ type: 'ai';
2078
+ name: string;
2079
+ } | {
2080
+ type: 'r2_bucket';
2081
+ name: string;
2082
+ /** Either a pre-existing bucket name or the literal `"auto"`. */
2083
+ bucket_name: string;
2084
+ } | {
2085
+ type: 'kv_namespace';
2086
+ name: string;
2087
+ /** Either a pre-existing KV namespace ID or the literal `"auto"`. */
2088
+ namespace_id: string;
2089
+ /** Required when `namespace_id === "auto"`. Human-readable namespace title. */
2090
+ title?: string;
2091
+ } | {
2092
+ type: 'd1';
2093
+ name: string;
2094
+ /** Either a pre-existing D1 database UUID or the literal `"auto"`. */
2095
+ id: string;
2096
+ /** Required when `id === "auto"`. Human-readable database name. */
2097
+ database_name?: string;
2098
+ } | {
2099
+ type: 'queue';
2100
+ name: string;
2101
+ /** Either a pre-existing queue name or the literal `"auto"`. */
2102
+ queue_name: string;
2103
+ } | {
2104
+ type: 'browser_rendering';
2105
+ name: string;
2106
+ } | {
2107
+ type: 'analytics_engine';
2108
+ name: string;
2109
+ dataset?: string;
2110
+ } | {
2111
+ type: 'hyperdrive';
2112
+ name: string;
2113
+ id: string;
2114
+ };
2115
+ type CustomBindingManifest = CustomBinding[];
2116
+ /** Sentinel string in an ID field that requests platform-side provisioning. */
2117
+ declare const AUTO_PROVISION_SENTINEL = "auto";
2118
+ /** Binding types whose ID field accepts the `"auto"` sentinel for provisioning. */
2119
+ declare const AUTO_PROVISIONABLE_TYPES: Set<string>;
2120
+ /**
2121
+ * True if a binding has the `"auto"` sentinel in its primary ID field. Used by
2122
+ * the deploy-worker to decide which entries need provisioning and by the
2123
+ * validator to enforce companion-field requirements.
2124
+ */
2125
+ declare function isAutoProvision(b: CustomBinding): boolean;
2126
+ /** Binding `type` values an app is allowed to declare. */
2127
+ declare const ALLOWED_BINDING_TYPES: Set<string>;
2128
+ /**
2129
+ * Binding NAMES the SDK reserves on every app — apps may not redeclare them.
2130
+ *
2131
+ * Includes:
2132
+ * - Static-asset + service bindings the platform sets up automatically.
2133
+ * - SDK-managed env (auth, identity, owner JWT, HMAC secret).
2134
+ * - The auto-attached cost-tracking AE dataset (`USAGE_EVENTS`).
2135
+ *
2136
+ * DO binding names (RECORD_ROOMS, YJS_ROOMS, etc.) are NOT in this set
2137
+ * because they live in a separate manifest (`__DO_MANIFEST__`).
2138
+ */
2139
+ declare const RESERVED_BINDING_NAMES: Set<string>;
2140
+ /**
2141
+ * Per-binding validation error. `binding` is undefined for top-level
2142
+ * shape failures (e.g. manifest is not an array).
2143
+ */
2144
+ interface ValidationError {
2145
+ binding?: CustomBinding;
2146
+ reason: string;
2147
+ }
2148
+ /**
2149
+ * Validate a binding manifest. Returns errors; an empty array means valid.
2150
+ *
2151
+ * Used both client-side (CLI) for friendly fail-fast and server-side
2152
+ * (deploy-worker) as a security boundary — apps can't sneak in reserved
2153
+ * binding names by editing the wire format.
2154
+ */
2155
+ declare function validateBindingManifest(manifest: unknown): {
2156
+ valid: true;
2157
+ bindings: CustomBindingManifest;
2158
+ } | {
2159
+ valid: false;
2160
+ errors: ValidationError[];
2161
+ };
2162
+ /**
2163
+ * Convert vite/wrangler's normalized config (from `.wrangler/deploy/config.json`)
2164
+ * into a CustomBindingManifest.
2165
+ *
2166
+ * Vite normalizes wrangler.toml into object/array structures with shapes like
2167
+ * `{ ai: { binding: 'AI' } }`, `{ vectorize: [{ binding, index_name }] }`,
2168
+ * etc. We extract each known shape with explicit field plucks (no broad
2169
+ * `as` casts) and return a flat array.
2170
+ */
2171
+ declare function bindingManifestFromOutputConfig(outputConfig: Record<string, unknown>): CustomBindingManifest;
2172
+
2173
+ /**
2174
+ * Pure helpers for computing Cloudflare Durable Object migrations from a
2175
+ * declared manifest + the bindings already registered on a deployed script.
2176
+ *
2177
+ * Lives in the SDK (not deploy-worker) so the logic is testable with vitest
2178
+ * and reusable from other CF deploy paths if we ever add them.
2179
+ */
2180
+
2181
+ /** Subset of CF's `bindings` API response we read from. */
2182
+ interface ExistingDOBinding {
2183
+ /** Binding name in `env`, e.g. 'RECORD_ROOMS' */
2184
+ name: string;
2185
+ /** Always `'durable_object_namespace'` for DO bindings. */
2186
+ type: string;
2187
+ /** SDK class name, e.g. 'AppRecordRoom' */
2188
+ class_name?: string;
2189
+ }
2190
+ /** What goes in the CF script-upload `migrations` block. */
2191
+ interface DoMigrationDirective {
2192
+ tag: string;
2193
+ new_sqlite_classes?: string[];
2194
+ deleted_classes?: string[];
2195
+ }
2196
+ interface DoMigrationPlan {
2197
+ /** New SQLite classes to register (present in manifest, absent in existing). */
2198
+ newSqliteClasses: string[];
2199
+ /** Classes to delete (present in existing, absent in manifest). */
2200
+ deletedClasses: string[];
2201
+ /** True when there's actual delta — only then should the migrations block be sent. */
2202
+ needsMigration: boolean;
2203
+ /** The full directive to splat into the CF script-upload metadata. Null if no migration is needed. */
2204
+ directive: DoMigrationDirective | null;
2205
+ }
2206
+ interface ComputeDoMigrationOptions {
2207
+ /**
2208
+ * Override for the timestamp baked into the migration tag. Tests pass a
2209
+ * fixed value to assert determinism; production omits this and gets
2210
+ * `Date.now()`, which guarantees lifetime tag uniqueness even across
2211
+ * cycles like `[A]→[A,B]→[A]→[A,B]`.
2212
+ */
2213
+ now?: number;
2214
+ }
2215
+ /**
2216
+ * Compute the migration plan for a deploy.
2217
+ *
2218
+ * manifest: what the app declares now
2219
+ * existing: what CF currently has registered for this script
2220
+ *
2221
+ * Behavior:
2222
+ * - new_sqlite_classes ← in manifest, not in existing, sqlite=true
2223
+ * - deleted_classes ← in existing, not in manifest
2224
+ * - needsMigration ← either of the above is non-empty
2225
+ * - tag ← content-addressed by (add, remove) so:
2226
+ * - identical re-deploy → unchanged tag → no-op (and
2227
+ * `needsMigration` is false anyway)
2228
+ * - any class change → unique tag → CF processes
2229
+ *
2230
+ * Bug history: an earlier version computed `tag = v${count}`. Removing a class
2231
+ * dropped the count, the migration block was skipped (no NEW classes), and CF
2232
+ * retained the orphaned class registration with its SQLite storage. The
2233
+ * `deleted_classes` path closes that gap; the content-addressed tag prevents
2234
+ * tag collisions when class sets are added and removed in different orders.
2235
+ */
2236
+ declare function computeDoMigration(manifest: readonly DOManifestEntry[], existing: readonly ExistingDOBinding[], options?: ComputeDoMigrationOptions): DoMigrationPlan;
2237
+
2238
+ /**
2239
+ * App-name validation + sanitization helpers.
2240
+ *
2241
+ * Strategy: validate strictly so we have a precise definition of "valid",
2242
+ * but DON'T reject non-conforming names — sanitize them, warn the user, and
2243
+ * proceed. Hard rejection would break apps whose `wrangler.toml name` was
2244
+ * something like `My_App` (previously deployed as `my-app` via silent
2245
+ * server-side sanitization). The new behavior preserves "still deploys,"
2246
+ * but the CLI now surfaces a warning so the user can fix the name when
2247
+ * convenient instead of being silently surprised by their hostname.
2248
+ *
2249
+ * Rules track Cloudflare's WfP script-name constraints (RFC 1035 host label,
2250
+ * no consecutive dashes) plus our 2-char minimum so subdomains read sensibly.
2251
+ */
2252
+ declare const APP_NAME_RULES: {
2253
+ /** ^[a-z0-9](-?[a-z0-9])+$ — RFC 1035 host label, no consecutive dashes. */
2254
+ readonly pattern: RegExp;
2255
+ readonly minLength: 2;
2256
+ readonly maxLength: 63;
2257
+ };
2258
+ type AppNameValidation = {
2259
+ valid: true;
2260
+ name: string;
2261
+ } | {
2262
+ valid: false;
2263
+ reason: string;
2264
+ };
2265
+ /**
2266
+ * Strict validation: returns valid only if the name already conforms.
2267
+ * Useful as a precondition test or for CI lints.
2268
+ */
2269
+ declare function validateAppName(raw: unknown): AppNameValidation;
2270
+ type AppNameResolution = {
2271
+ ok: true;
2272
+ name: string;
2273
+ warning?: string;
2274
+ } | {
2275
+ ok: false;
2276
+ reason: string;
2277
+ };
2278
+ /**
2279
+ * Resolve an app name for deploy: prefer the input as-is if valid, otherwise
2280
+ * sanitize and warn. Hard-fail only if even sanitization can't produce a
2281
+ * valid name (empty, all-non-alphanumeric, too short, too long).
2282
+ *
2283
+ * The intent is "what previously worked still works, with a friendly warning
2284
+ * about non-conforming names."
2285
+ */
2286
+ declare function resolveAppName(raw: unknown): AppNameResolution;
2287
+
2288
+ /**
2289
+ * AI Chat Schemas
2290
+ *
2291
+ * Pre-built collection schemas for DO-backed AI chat history.
2292
+ * The worker is the only writer; the client reads via `useQuery`.
2293
+ */
2294
+
2295
+ declare const AI_CHATS_SCHEMA: CollectionSchema;
2296
+ declare const AI_MESSAGES_SCHEMA: CollectionSchema;
2297
+
2298
+ /**
2299
+ * Messaging Schemas
2300
+ *
2301
+ * Pre-built collection schemas for messaging functionality.
2302
+ * Any app can import these to add channels, messages, reactions, etc.
2303
+ *
2304
+ * @example
2305
+ * ```typescript
2306
+ * import { CHANNELS_SCHEMA, MESSAGES_SCHEMA, REACTIONS_SCHEMA } from 'deepspace/worker'
2307
+ * export const schemas = [usersSchema, CHANNELS_SCHEMA, MESSAGES_SCHEMA, REACTIONS_SCHEMA]
2308
+ * ```
2309
+ */
2310
+
2311
+ declare const CHANNELS_SCHEMA: CollectionSchema;
2312
+ declare const MESSAGES_SCHEMA: CollectionSchema;
2313
+ declare const REACTIONS_SCHEMA: CollectionSchema;
2314
+ declare const CHANNEL_MEMBERS_SCHEMA: CollectionSchema;
2315
+ declare const CHANNEL_INVITATIONS_SCHEMA: CollectionSchema;
2316
+ declare const READ_RECEIPTS_SCHEMA: CollectionSchema;
2317
+
2318
+ /**
2319
+ * Shared conversation schemas for RecordRoom-based conversations.
2320
+ *
2321
+ * These define the collections inside a per-conversation RecordRoom DO.
2322
+ * Used by apps that have messaging/conversation features (slack-clone,
2323
+ * helpdesk, mail, reddit-clone, etc.).
2324
+ *
2325
+ * Each conversation gets its own RecordRoom DO keyed by `conv:{id}`.
2326
+ */
2327
+
2328
+ declare const CONVERSATION_SCHEMAS: CollectionSchema[];
2329
+ /**
2330
+ * Voting schemas for Reddit-style apps.
2331
+ * Add these alongside CONVERSATION_SCHEMAS for apps that need voting.
2332
+ */
2333
+ declare const VOTING_SCHEMAS: CollectionSchema[];
2334
+
2335
+ /**
2336
+ * Directory DO Schemas
2337
+ *
2338
+ * Purpose-built collections for the `dir:{appName}` global DO type.
2339
+ * This is the cross-app-visible directory layer — any app can subscribe
2340
+ * to another app's directory in real-time via SHARED_CONNECTIONS.
2341
+ *
2342
+ * Five collections covering all standard communication/social patterns:
2343
+ * - conversations: channels, DMs, email threads, support chats
2344
+ * - conversation_state: per-user metadata (read cursor, stars, labels, folders)
2345
+ * - communities: groups, forums, boards, projects
2346
+ * - memberships: user membership in communities
2347
+ * - posts: feed items, tweets, Q&A questions, announcements
2348
+ *
2349
+ * Domain-specific data (tickets, CRM, procurement) stays in app DOs.
2350
+ * Message content stays in conv:{id} DOs (CONVERSATION_SCHEMAS).
2351
+ */
2352
+
2353
+ /** All directory schemas for the `dir:{appName}` global DO type. */
2354
+ declare const DIRECTORY_SCHEMAS: CollectionSchema[];
2355
+
2356
+ /**
2357
+ * Workspace DO Schemas
2358
+ *
2359
+ * All collections for the workspace:default Durable Object.
2360
+ * This DO consolidates shared, cross-app business data:
2361
+ *
2362
+ * - teams / team_members — workspace-wide team management (replaces built-in teams)
2363
+ * - tasks / projects / tags — team-scoped task management
2364
+ * - people — shared contacts directory
2365
+ * - transactions / accounts — shared financial ledger
2366
+ *
2367
+ * Team-scoped collections use teamField RBAC: members see only
2368
+ * their team's records; admins see everything.
2369
+ */
2370
+
2371
+ declare const workspaceTeamsSchema: CollectionSchema;
2372
+ declare const workspaceTeamMembersSchema: CollectionSchema;
2373
+ declare const workspaceTasksSchema: CollectionSchema;
2374
+ declare const workspaceProjectsSchema: CollectionSchema;
2375
+ declare const workspaceTagsSchema: CollectionSchema;
2376
+ declare const workspacePeopleSchema: CollectionSchema;
2377
+ declare const workspaceTransactionsSchema: CollectionSchema;
2378
+ declare const workspaceAccountsSchema: CollectionSchema;
2379
+ /**
2380
+ * Universal sharing index. Any app can create share records for any content type.
2381
+ *
2382
+ * ContentType: 'document' | 'slide' | 'spreadsheet' | ... (extensible)
2383
+ * ShareType: 'channel' | 'team' | 'direct' | 'link' | 'org' (extensible)
2384
+ * ShareTarget: the ID of the target (channelId, teamId, userId, linkId, orgId)
2385
+ * Permission: 'view' | 'edit' — what the share grants
2386
+ *
2387
+ * Content itself lives in the owner's app DO.
2388
+ * This table is the discovery layer: "what has been shared, with whom, and how".
2389
+ */
2390
+ declare const workspaceContentSharesSchema: CollectionSchema;
2391
+ declare const workspaceFormResponsesSchema: CollectionSchema;
2392
+ /**
2393
+ * Maps DeepSpace users to their claimed @app.space email handles.
2394
+ * Shared across all apps so any app can look up a user's email address.
2395
+ * All handles are under the @app.space domain.
2396
+ */
2397
+ declare const workspaceEmailHandlesSchema: CollectionSchema;
2398
+ declare const WORKSPACE_SCHEMAS: CollectionSchema[];
2399
+
2400
+ /**
2401
+ * Shared Durable Object Schemas
2402
+ *
2403
+ * Central registry of global DO types with fixed schemas.
2404
+ * Apps connect to shared DOs via SHARED_CONNECTIONS in constants.ts.
2405
+ * The ScopeRegistry resolves collection names to the correct scope automatically.
2406
+ *
2407
+ * Scope tiers:
2408
+ * - App DO (app:{appHandle}) — private to each app, app defines tables
2409
+ * - Dir DO (dir:{appHandle}) — cross-app directory (conversations, communities, posts)
2410
+ * - Workspace DO (workspace:default) — shared business data (teams, tasks, people, ledger)
2411
+ * - Conv DO (conv:{id}) — single conversation (messages, reactions, members)
2412
+ */
2413
+
2414
+ interface SharedConnection {
2415
+ type: string;
2416
+ instanceId?: string;
2417
+ }
2418
+ interface GlobalDOType {
2419
+ name: string;
2420
+ schemas: CollectionSchema[];
2421
+ description: string;
2422
+ }
2423
+ declare const GLOBAL_DO_TYPES: GlobalDOType[];
2424
+ /** All valid global DO type names. */
2425
+ declare const GLOBAL_DO_TYPE_NAMES: string[];
2426
+ /** Look up a global DO type by name, returns null if not found. */
2427
+ declare function getGlobalDOType(name: string): GlobalDOType | null;
2428
+ /** Get the fixed schemas for a global DO type. Returns empty array if unknown type. */
2429
+ declare function getGlobalDOSchemas(typeName: string): CollectionSchema[];
2430
+ /** All collection names reserved by global DO types. Apps must not reuse these. */
2431
+ declare const RESERVED_COLLECTION_NAMES: Set<string>;
2432
+
2433
+ /**
2434
+ * Subscription handlers for RecordRoom
2435
+ *
2436
+ * All collections use table-mode storage (c_* tables with typed columns).
2437
+ */
2438
+
2439
+ interface SubscriptionContext {
2440
+ sql: SqlStorage;
2441
+ schemaRegistry: SchemaRegistry;
2442
+ state: DurableObjectState;
2443
+ getPermissionContext(): PermissionContext;
2444
+ /** Typed against `ServerMessage` so outbound broadcasts are
2445
+ * compile-checked against the wire contract. */
2446
+ send(ws: WebSocket, message: ServerMessage): void;
2447
+ }
2448
+ /**
2449
+ * Handle a new subscription request.
2450
+ *
2451
+ * We no longer store subscriptions server-side - broadcasts go to all clients
2452
+ * and they filter locally. This avoids hibernation issues.
2453
+ */
2454
+ declare function handleSubscribe(ctx: SubscriptionContext, ws: WebSocket, attachment: ConnectionAttachment, payload: SubscribePayload): void;
2455
+ /**
2456
+ * Handle unsubscribe request.
2457
+ *
2458
+ * Since we no longer store subscriptions server-side, this is a no-op.
2459
+ * The client handles unsubscription locally.
2460
+ */
2461
+ declare function handleUnsubscribe(_ctx: SubscriptionContext, _ws: WebSocket, _attachment: ConnectionAttachment, _payload: UnsubscribePayload): void;
2462
+ /**
2463
+ * Execute a query and return matching records.
2464
+ * All collections use table-mode (c_* tables).
2465
+ *
2466
+ * `skipUserRbac` lets a server-action caller (i.e. the app itself, via
2467
+ * `X-App-Action`) bypass the per-user read filter for parity with the other
2468
+ * `tools.*` operations (`get`, `create`, `update`, `remove`).
2469
+ */
2470
+ declare function executeQuery(ctx: SubscriptionContext, query: Query, userId: string, userRole: string, skipUserRbac?: boolean): RecordResult[];
2471
+ /**
2472
+ * Check if a record matches a subscription's query and permissions
2473
+ */
2474
+ declare function recordMatchesSubscription(record: {
2475
+ recordId: string;
2476
+ data: Record<string, unknown>;
2477
+ createdBy: string;
2478
+ }, collection: string, query: Query, userId: string, userRole: string, schema: CollectionSchema, ctx: PermissionContext): boolean;
2479
+ /**
2480
+ * Broadcast a record change to all connected clients who can read it.
2481
+ *
2482
+ * Instead of matching subscriptions (which are lost on hibernation),
2483
+ * we broadcast to ALL clients and include the collection name.
2484
+ * The client filters based on its local subscriptions.
2485
+ */
2486
+ declare function broadcastChange(ctx: SubscriptionContext, state: DurableObjectState, collection: string, record: RecordResult, changeType: 'create' | 'update' | 'delete'): void;
2487
+
2488
+ /**
2489
+ * Record operation handlers for RecordRoom (PUT/DELETE)
2490
+ *
2491
+ * All collections use table-mode storage (c_* tables with typed columns).
2492
+ */
2493
+
2494
+ interface RecordContext extends SubscriptionContext {
2495
+ state: DurableObjectState;
2496
+ }
2497
+ /**
2498
+ * Get a single record from its c_* table.
2499
+ */
2500
+ declare function getRecord(sql: SqlStorage, collection: string, recordId: string, schema?: CollectionSchema): {
2501
+ data: Record<string, unknown>;
2502
+ createdBy: string;
2503
+ createdAt: string;
2504
+ updatedAt: string;
2505
+ } | null;
2506
+ /**
2507
+ * Handle PUT (create/update) record request via WebSocket.
2508
+ * Thin wrapper around putRecord() — translates ToolResult errors to WS messages.
2509
+ */
2510
+ declare function handlePut(ctx: RecordContext, ws: WebSocket, attachment: ConnectionAttachment, payload: PutPayload): void;
2511
+ /**
2512
+ * Handle DELETE record request via WebSocket.
2513
+ * Thin wrapper around deleteRecord() — translates ToolResult errors to WS messages.
2514
+ */
2515
+ declare function handleDelete(ctx: RecordContext, ws: WebSocket, attachment: ConnectionAttachment, payload: DeletePayload): void;
2516
+ /**
2517
+ * Put (create/update) a record. Returns ToolResult instead of sending WS messages.
2518
+ * Performs schema validation, RBAC checks, and broadcasts changes.
2519
+ *
2520
+ * @param skipUserRbac - When true, skip user role checks. Used by server actions
2521
+ * that have already been authorized at the app level.
2522
+ * @param systemUpdate - When true, also skip system-managed field stripping.
2523
+ * Used for server-initiated updates to system fields (e.g. user profile sync).
2524
+ */
2525
+ declare function putRecord(ctx: RecordContext, collection: string, recordId: string, data: Record<string, unknown>, userId: string, userRole: string, skipUserRbac?: boolean, systemUpdate?: boolean): ToolResult;
2526
+ /**
2527
+ * Delete a record. Returns ToolResult instead of sending WS messages.
2528
+ * Performs RBAC check and broadcasts the deletion.
2529
+ *
2530
+ * @param skipUserRbac - When true, skip user role checks. Used by server actions.
2531
+ */
2532
+ declare function deleteRecord(ctx: RecordContext, collection: string, recordId: string, userId: string, userRole: string, skipUserRbac?: boolean): ToolResult;
2533
+ /**
2534
+ * Read a single record with RBAC check. Returns ToolResult.
2535
+ *
2536
+ * @param skipUserRbac - When true, skip user role checks. Used by server actions.
2537
+ */
2538
+ declare function readRecord(ctx: RecordContext, collection: string, recordId: string, userId: string, userRole: string, skipUserRbac?: boolean): ToolResult;
2539
+
2540
+ /**
2541
+ * User management handlers for RecordRoom
2542
+ *
2543
+ * Users are stored in the c_users table (table-mode).
2544
+ * System-managed fields (email, name, role, etc.) can only be set by registerUser().
2545
+ */
2546
+
2547
+ interface UserContext {
2548
+ sql: SqlStorage;
2549
+ state: DurableObjectState;
2550
+ schemaRegistry: SchemaRegistry;
2551
+ send(ws: WebSocket, message: {
2552
+ type: string;
2553
+ payload: unknown;
2554
+ }): void;
2555
+ }
2556
+ /**
2557
+ * Get a single user by ID from the c_users table.
2558
+ */
2559
+ declare function getUser(sql: SqlStorage, userId: string, schemaRegistry?: SchemaRegistry): User | null;
2560
+ /**
2561
+ * Get all users from the c_users table.
2562
+ */
2563
+ declare function getAllUsers(sql: SqlStorage, schemaRegistry?: SchemaRegistry): User[];
2564
+ /**
2565
+ * Register or update a user in the c_users table.
2566
+ *
2567
+ * This is the ONLY way to set system-managed fields (email, name, role, etc.).
2568
+ * Normal mutations via handlePut will reject changes to system-managed fields.
2569
+ *
2570
+ * Role derivation (in order of priority):
2571
+ * 1. isAdmin=true (global admin, canvas owner, or app owner) → always 'admin'
2572
+ * 2. Existing role in users collection (preserved)
2573
+ * 3. Default role (configurable per-app, defaults to 'member')
2574
+ *
2575
+ * This allows each miniapp to define its own role hierarchy while
2576
+ * ensuring admins and owners always have full access.
2577
+ */
2578
+ declare function registerUser(sql: SqlStorage, userId: string, name: string, email: string, imageUrl: string | undefined, isAdmin: boolean, defaultRole?: string, schemaRegistry?: SchemaRegistry): Promise<User>;
2579
+ /**
2580
+ * Handle user list request.
2581
+ * Returns all users with full data (system + app fields).
2582
+ */
2583
+ declare function handleUserList(ctx: UserContext, ws: WebSocket, _attachment: ConnectionAttachment): void;
2584
+ /**
2585
+ * Handle user profile update.
2586
+ *
2587
+ * Called when the client's profile loads after the initial WS connection.
2588
+ * Updates the user's name/email/imageUrl in c_users and broadcasts the
2589
+ * updated user list to all connected clients so names refresh in real time.
2590
+ */
2591
+ interface UserUpdatePayload {
2592
+ name?: string;
2593
+ email?: string;
2594
+ imageUrl?: string;
2595
+ }
2596
+ declare function handleUserUpdate(ctx: RecordContext, ws: WebSocket, attachment: ConnectionAttachment, payload: UserUpdatePayload): void;
2597
+ /**
2598
+ * Handle set role request (admin only).
2599
+ * Updates the role field in the c_users table.
2600
+ */
2601
+ declare function handleSetRole(ctx: UserContext, ws: WebSocket, attachment: ConnectionAttachment, payload: SetRolePayload): Promise<void>;
2602
+
2603
+ /**
2604
+ * Yjs collaborative editing handlers for RecordRoom
2605
+ */
2606
+
2607
+ /**
2608
+ * Schemas for system collections.
2609
+ * These have empty columns arrays — they only use system columns
2610
+ * (_row_id, _created_by, _created_at, _updated_at).
2611
+ * Yjs data is stored in the yjs_docs table, not in the record itself.
2612
+ */
2613
+ declare const SYSTEM_COLLECTION_SCHEMAS: CollectionSchema[];
2614
+ interface YjsContext {
2615
+ sql: SqlStorage;
2616
+ state: DurableObjectState;
2617
+ yjsDocs: Map<YjsDocKey, Y.Doc>;
2618
+ schemaRegistry: SchemaRegistry;
2619
+ getPermissionContext(): PermissionContext;
2620
+ send(ws: WebSocket, message: {
2621
+ type: string;
2622
+ payload: unknown;
2623
+ }): void;
2624
+ sendBinary(ws: WebSocket, data: Uint8Array): void;
2625
+ }
2626
+ /**
2627
+ * Create a Yjs doc key from collection, recordId, and fieldName
2628
+ */
2629
+ declare function getYjsDocKey(collection: string, recordId: string, fieldName: string): YjsDocKey;
2630
+ /**
2631
+ * Get or create a Y.Doc for a record field.
2632
+ * Loads from database if exists, creates new if not.
2633
+ */
2634
+ declare function getOrCreateYjsDoc(ctx: YjsContext, docKey: YjsDocKey): Promise<Y.Doc>;
2635
+ /**
2636
+ * Save Yjs doc state to database.
2637
+ */
2638
+ declare function saveYjsDoc(sql: SqlStorage, docKey: YjsDocKey, doc: Y.Doc): void;
2639
+ /**
2640
+ * Handle request to join Yjs sync for a record field.
2641
+ * System collections (see SYSTEM_COLLECTIONS) are permissive; others require schema.
2642
+ */
2643
+ declare function handleYjsJoin(ctx: YjsContext, ws: WebSocket, attachment: ConnectionAttachment, payload: YjsJoinPayload): Promise<void>;
2644
+ /**
2645
+ * Handle request to leave Yjs sync for a record field.
2646
+ */
2647
+ declare function handleYjsLeave(ws: WebSocket, attachment: ConnectionAttachment, payload: YjsLeavePayload): void;
2648
+ /**
2649
+ * Handle binary Yjs sync messages from clients.
2650
+ *
2651
+ * Protocol:
2652
+ * - MSG_SYNC_STEP1: Client sends state vector → Server responds with SYNC_STEP2 (diff)
2653
+ * - MSG_SYNC_STEP2/UPDATE: Client sends update → Server applies and broadcasts
2654
+ */
2655
+ declare function handleYjsBinaryMessage(ctx: YjsContext, ws: WebSocket, attachment: ConnectionAttachment, data: Uint8Array): Promise<void>;
2656
+ /**
2657
+ * Broadcast a Yjs update to all subscribers of a doc, except the sender.
2658
+ */
2659
+ declare function broadcastYjsUpdate(ctx: YjsContext, docKey: YjsDocKey, update: Uint8Array, excludeWs: WebSocket | null): void;
2660
+
2661
+ /**
2662
+ * HTTP Debug API handlers for RecordRoom
2663
+ */
2664
+
2665
+ interface DebugApiContext extends SubscriptionContext {
2666
+ state: DurableObjectState;
2667
+ yjsDocs: Map<YjsDocKey, Y.Doc>;
2668
+ sendBinary: (ws: WebSocket, data: Uint8Array) => void;
2669
+ }
2670
+ /**
2671
+ * Handle HTTP API requests (for debugging)
2672
+ */
2673
+ declare function handleApiRequest(ctx: DebugApiContext, request: Request, url: URL): Promise<Response>;
2674
+
2675
+ /**
2676
+ * Tools API HTTP handlers for RecordRoom
2677
+ *
2678
+ * Provides an HTTP interface for agent tool calls (records, schemas, users).
2679
+ *
2680
+ * Caller identity is supplied via HTTP headers:
2681
+ *
2682
+ * X-User-Id: <userId> — identifies the caller (required for any
2683
+ * tool that touches user-bound data).
2684
+ * X-App-Action: 'true' — bypass user RBAC because the app's
2685
+ * server-side code is already the trust
2686
+ * boundary. Used by server actions and
2687
+ * cron jobs; unsafe to pass from clients.
2688
+ *
2689
+ * The userId is looked up in the users collection to derive the caller's
2690
+ * role. All operations go through the same RBAC checks as the WebSocket
2691
+ * path unless flagged as an app action.
2692
+ */
2693
+
2694
+ interface ToolsApiContext extends SubscriptionContext {
2695
+ state: DurableObjectState;
2696
+ yjsDocs: Map<YjsDocKey, Y.Doc>;
2697
+ sendBinary: (ws: WebSocket, data: Uint8Array) => void;
2698
+ ownerUserId?: string;
2699
+ }
2700
+ /**
2701
+ * Handle /tools/ API requests.
2702
+ * Called from handleApiRequest when path starts with 'tools/'.
2703
+ */
2704
+ declare function handleToolsRequest(ctx: ToolsApiContext, request: Request, path: string): Promise<Response>;
2705
+
2706
+ /**
2707
+ * Auth types for the DeepSpace SDK.
2708
+ *
2709
+ * Provider-agnostic shapes for JWT verification (issuer, audience, azp
2710
+ * matching, ES256 public key) and the HMAC-signed internal-request
2711
+ * envelope used for worker-to-worker calls.
2712
+ */
2713
+ interface JwtVerifierConfig {
2714
+ /** PEM-encoded public key (ES256) for JWT verification */
2715
+ publicKey: string;
2716
+ /** Expected issuer (e.g. "https://auth.deep.space") */
2717
+ issuer: string;
2718
+ /** Expected audience (usually the configured platform API URL) */
2719
+ audience?: string | string[];
2720
+ /** Allowed origins / authorized parties (supports wildcards like "https://*.app.space") */
2721
+ authorizedParties?: string[];
2722
+ /** Clock skew tolerance in milliseconds (default: 5000) */
2723
+ clockSkewMs?: number;
2724
+ }
2725
+ interface JwtClaims {
2726
+ sub: string;
2727
+ iss?: string;
2728
+ aud?: string | string[];
2729
+ azp?: string;
2730
+ exp?: number;
2731
+ iat?: number;
2732
+ name?: string;
2733
+ email?: string;
2734
+ image?: string;
2735
+ [key: string]: unknown;
2736
+ }
2737
+ interface VerifiedAuth {
2738
+ userId: string;
2739
+ claims: JwtClaims;
2740
+ }
2741
+ interface VerifyResult extends VerifiedAuth {
2742
+ }
2743
+ interface TokenDebugInfo {
2744
+ iss?: string | null;
2745
+ aud?: string | string[] | null;
2746
+ azp?: string | null;
2747
+ exp?: number | null;
2748
+ iat?: number | null;
2749
+ }
2750
+ interface VerifyOutcome {
2751
+ result: VerifyResult | null;
2752
+ debug?: TokenDebugInfo;
2753
+ error?: unknown;
2754
+ }
2755
+ interface InternalSignature {
2756
+ timestamp: string;
2757
+ signature: string;
2758
+ }
2759
+ interface VerifyInternalSignatureInput {
2760
+ secret: string | undefined;
2761
+ timestamp: string | null | undefined;
2762
+ signature: string | null | undefined;
2763
+ payload: string;
2764
+ maxSkewMs?: number;
2765
+ }
2766
+ interface SignInternalPayloadInput {
2767
+ secret: string;
2768
+ payload: string;
2769
+ timestamp?: string;
2770
+ }
2771
+
2772
+ /**
2773
+ * JWT verification for DeepSpace workers.
2774
+ *
2775
+ * jose-based ES256 verification (jose runs on the Cloudflare Workers
2776
+ * edge runtime). Imported public keys are cached per-PEM to avoid
2777
+ * re-importing on every request, and `azp` is matched against an
2778
+ * optional list of authorized-party patterns supporting `*` wildcards.
2779
+ */
2780
+
2781
+ /**
2782
+ * Verify a DeepSpace JWT token.
2783
+ *
2784
+ * @param config - Verification configuration (public key, issuer, audience)
2785
+ * @param token - The JWT string to verify
2786
+ * @returns VerifyOutcome with either the verified result or error details
2787
+ */
2788
+ declare function verifyJwt(config: JwtVerifierConfig, token: string | null | undefined): Promise<VerifyOutcome>;
2789
+
2790
+ /**
2791
+ * HMAC-based internal authentication for service-to-service calls.
2792
+ *
2793
+ * Ported as-is from Miyagi3 — this is auth-provider-agnostic.
2794
+ */
2795
+
2796
+ declare const DEFAULT_MAX_SKEW_MS: number;
2797
+ declare function computeHmacHex(secret: string, payload: string): Promise<string>;
2798
+ declare function timingSafeEqualHex(a: string, b: string): Promise<boolean>;
2799
+ declare function signInternalPayload({ secret, payload, timestamp, }: SignInternalPayloadInput): Promise<InternalSignature>;
2800
+ declare function verifyInternalSignature({ secret, timestamp, signature, payload, maxSkewMs, }: VerifyInternalSignatureInput): Promise<boolean>;
2801
+ declare function buildInternalPayload(body: unknown): string;
2802
+
2803
+ declare function decodeJwtPayload(token: string | null | undefined): TokenDebugInfo | undefined;
2804
+
2805
+ /**
2806
+ * Better Auth configuration factory for DeepSpace
2807
+ *
2808
+ * Provides pre-configured Better Auth instances for Cloudflare Workers + D1.
2809
+ */
2810
+ interface DeepSpaceAuthConfig {
2811
+ /** D1 database binding */
2812
+ database: D1Database;
2813
+ /** Base URL for the auth worker (e.g. "https://auth.deep.space") */
2814
+ baseURL: string;
2815
+ /** Secret for session signing */
2816
+ secret: string;
2817
+ /** Google OAuth credentials (optional) */
2818
+ google?: {
2819
+ clientId: string;
2820
+ clientSecret: string;
2821
+ };
2822
+ /** GitHub OAuth credentials (optional) */
2823
+ github?: {
2824
+ clientId: string;
2825
+ clientSecret: string;
2826
+ };
2827
+ /** Enable email/password authentication */
2828
+ emailAndPassword?: boolean;
2829
+ /** Trusted origins for CORS */
2830
+ trustedOrigins?: string[];
2831
+ }
2832
+ /**
2833
+ * Create a Better Auth instance configured for DeepSpace.
2834
+ *
2835
+ * This is called per-request in the auth worker since D1 bindings
2836
+ * are request-scoped in Cloudflare Workers.
2837
+ */
2838
+ declare function createDeepSpaceAuth(config: DeepSpaceAuthConfig): better_auth.Auth<{
2839
+ database: D1Database;
2840
+ baseURL: string;
2841
+ secret: string;
2842
+ emailAndPassword: {
2843
+ enabled: boolean;
2844
+ };
2845
+ socialProviders: Record<string, {
2846
+ clientId: string;
2847
+ clientSecret: string;
2848
+ }>;
2849
+ trustedOrigins: string[];
2850
+ plugins: [{
2851
+ id: "organization";
2852
+ endpoints: better_auth_plugins.OrganizationEndpoints<better_auth_plugins.OrganizationOptions & {
2853
+ teams: {
2854
+ enabled: true;
2855
+ };
2856
+ dynamicAccessControl?: {
2857
+ enabled?: false | undefined;
2858
+ } | undefined;
2859
+ }> & better_auth_plugins.TeamEndpoints<better_auth_plugins.OrganizationOptions & {
2860
+ teams: {
2861
+ enabled: true;
2862
+ };
2863
+ dynamicAccessControl?: {
2864
+ enabled?: false | undefined;
2865
+ } | undefined;
2866
+ }>;
2867
+ schema: better_auth_plugins.OrganizationSchema<better_auth_plugins.OrganizationOptions & {
2868
+ teams: {
2869
+ enabled: true;
2870
+ };
2871
+ dynamicAccessControl?: {
2872
+ enabled?: false | undefined;
2873
+ } | undefined;
2874
+ }>;
2875
+ $Infer: {
2876
+ Organization: {
2877
+ id: string;
2878
+ name: string;
2879
+ slug: string;
2880
+ createdAt: Date;
2881
+ logo?: string | null | undefined;
2882
+ metadata?: any;
2883
+ };
2884
+ Invitation: {
2885
+ id: string;
2886
+ organizationId: string;
2887
+ email: string;
2888
+ role: "member" | "admin" | "owner";
2889
+ status: better_auth_plugins.InvitationStatus;
2890
+ inviterId: string;
2891
+ expiresAt: Date;
2892
+ createdAt: Date;
2893
+ teamId?: string | undefined | undefined;
2894
+ };
2895
+ Member: {
2896
+ id: string;
2897
+ organizationId: string;
2898
+ role: "member" | "admin" | "owner";
2899
+ createdAt: Date;
2900
+ userId: string;
2901
+ teamId?: string | undefined | undefined;
2902
+ user: {
2903
+ id: string;
2904
+ email: string;
2905
+ name: string;
2906
+ image?: string | undefined;
2907
+ };
2908
+ };
2909
+ Team: {
2910
+ id: string;
2911
+ name: string;
2912
+ organizationId: string;
2913
+ createdAt: Date;
2914
+ updatedAt?: Date | undefined;
2915
+ };
2916
+ TeamMember: {
2917
+ id: string;
2918
+ teamId: string;
2919
+ userId: string;
2920
+ createdAt: Date;
2921
+ };
2922
+ ActiveOrganization: {
2923
+ members: {
2924
+ id: string;
2925
+ organizationId: string;
2926
+ role: "member" | "admin" | "owner";
2927
+ createdAt: Date;
2928
+ userId: string;
2929
+ teamId?: string | undefined | undefined;
2930
+ user: {
2931
+ id: string;
2932
+ email: string;
2933
+ name: string;
2934
+ image?: string | undefined;
2935
+ };
2936
+ }[];
2937
+ invitations: {
2938
+ id: string;
2939
+ organizationId: string;
2940
+ email: string;
2941
+ role: "member" | "admin" | "owner";
2942
+ status: better_auth_plugins.InvitationStatus;
2943
+ inviterId: string;
2944
+ expiresAt: Date;
2945
+ createdAt: Date;
2946
+ teamId?: string | undefined | undefined;
2947
+ }[];
2948
+ teams: {
2949
+ id: string;
2950
+ name: string;
2951
+ organizationId: string;
2952
+ createdAt: Date;
2953
+ updatedAt?: Date | undefined;
2954
+ }[];
2955
+ } & {
2956
+ id: string;
2957
+ name: string;
2958
+ slug: string;
2959
+ createdAt: Date;
2960
+ logo?: string | null | undefined;
2961
+ metadata?: any;
2962
+ };
2963
+ };
2964
+ $ERROR_CODES: {
2965
+ YOU_ARE_NOT_ALLOWED_TO_CREATE_A_NEW_ORGANIZATION: better_auth.RawError<"YOU_ARE_NOT_ALLOWED_TO_CREATE_A_NEW_ORGANIZATION">;
2966
+ YOU_HAVE_REACHED_THE_MAXIMUM_NUMBER_OF_ORGANIZATIONS: better_auth.RawError<"YOU_HAVE_REACHED_THE_MAXIMUM_NUMBER_OF_ORGANIZATIONS">;
2967
+ ORGANIZATION_ALREADY_EXISTS: better_auth.RawError<"ORGANIZATION_ALREADY_EXISTS">;
2968
+ ORGANIZATION_SLUG_ALREADY_TAKEN: better_auth.RawError<"ORGANIZATION_SLUG_ALREADY_TAKEN">;
2969
+ ORGANIZATION_NOT_FOUND: better_auth.RawError<"ORGANIZATION_NOT_FOUND">;
2970
+ USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION: better_auth.RawError<"USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION">;
2971
+ YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_ORGANIZATION: better_auth.RawError<"YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_ORGANIZATION">;
2972
+ YOU_ARE_NOT_ALLOWED_TO_DELETE_THIS_ORGANIZATION: better_auth.RawError<"YOU_ARE_NOT_ALLOWED_TO_DELETE_THIS_ORGANIZATION">;
2973
+ NO_ACTIVE_ORGANIZATION: better_auth.RawError<"NO_ACTIVE_ORGANIZATION">;
2974
+ USER_IS_ALREADY_A_MEMBER_OF_THIS_ORGANIZATION: better_auth.RawError<"USER_IS_ALREADY_A_MEMBER_OF_THIS_ORGANIZATION">;
2975
+ MEMBER_NOT_FOUND: better_auth.RawError<"MEMBER_NOT_FOUND">;
2976
+ ROLE_NOT_FOUND: better_auth.RawError<"ROLE_NOT_FOUND">;
2977
+ YOU_ARE_NOT_ALLOWED_TO_CREATE_A_NEW_TEAM: better_auth.RawError<"YOU_ARE_NOT_ALLOWED_TO_CREATE_A_NEW_TEAM">;
2978
+ TEAM_ALREADY_EXISTS: better_auth.RawError<"TEAM_ALREADY_EXISTS">;
2979
+ TEAM_NOT_FOUND: better_auth.RawError<"TEAM_NOT_FOUND">;
2980
+ YOU_CANNOT_LEAVE_THE_ORGANIZATION_AS_THE_ONLY_OWNER: better_auth.RawError<"YOU_CANNOT_LEAVE_THE_ORGANIZATION_AS_THE_ONLY_OWNER">;
2981
+ YOU_CANNOT_LEAVE_THE_ORGANIZATION_WITHOUT_AN_OWNER: better_auth.RawError<"YOU_CANNOT_LEAVE_THE_ORGANIZATION_WITHOUT_AN_OWNER">;
2982
+ YOU_ARE_NOT_ALLOWED_TO_DELETE_THIS_MEMBER: better_auth.RawError<"YOU_ARE_NOT_ALLOWED_TO_DELETE_THIS_MEMBER">;
2983
+ YOU_ARE_NOT_ALLOWED_TO_INVITE_USERS_TO_THIS_ORGANIZATION: better_auth.RawError<"YOU_ARE_NOT_ALLOWED_TO_INVITE_USERS_TO_THIS_ORGANIZATION">;
2984
+ USER_IS_ALREADY_INVITED_TO_THIS_ORGANIZATION: better_auth.RawError<"USER_IS_ALREADY_INVITED_TO_THIS_ORGANIZATION">;
2985
+ INVITATION_NOT_FOUND: better_auth.RawError<"INVITATION_NOT_FOUND">;
2986
+ YOU_ARE_NOT_THE_RECIPIENT_OF_THE_INVITATION: better_auth.RawError<"YOU_ARE_NOT_THE_RECIPIENT_OF_THE_INVITATION">;
2987
+ EMAIL_VERIFICATION_REQUIRED_BEFORE_ACCEPTING_OR_REJECTING_INVITATION: better_auth.RawError<"EMAIL_VERIFICATION_REQUIRED_BEFORE_ACCEPTING_OR_REJECTING_INVITATION">;
2988
+ YOU_ARE_NOT_ALLOWED_TO_CANCEL_THIS_INVITATION: better_auth.RawError<"YOU_ARE_NOT_ALLOWED_TO_CANCEL_THIS_INVITATION">;
2989
+ INVITER_IS_NO_LONGER_A_MEMBER_OF_THE_ORGANIZATION: better_auth.RawError<"INVITER_IS_NO_LONGER_A_MEMBER_OF_THE_ORGANIZATION">;
2990
+ YOU_ARE_NOT_ALLOWED_TO_INVITE_USER_WITH_THIS_ROLE: better_auth.RawError<"YOU_ARE_NOT_ALLOWED_TO_INVITE_USER_WITH_THIS_ROLE">;
2991
+ FAILED_TO_RETRIEVE_INVITATION: better_auth.RawError<"FAILED_TO_RETRIEVE_INVITATION">;
2992
+ YOU_HAVE_REACHED_THE_MAXIMUM_NUMBER_OF_TEAMS: better_auth.RawError<"YOU_HAVE_REACHED_THE_MAXIMUM_NUMBER_OF_TEAMS">;
2993
+ UNABLE_TO_REMOVE_LAST_TEAM: better_auth.RawError<"UNABLE_TO_REMOVE_LAST_TEAM">;
2994
+ YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_MEMBER: better_auth.RawError<"YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_MEMBER">;
2995
+ ORGANIZATION_MEMBERSHIP_LIMIT_REACHED: better_auth.RawError<"ORGANIZATION_MEMBERSHIP_LIMIT_REACHED">;
2996
+ YOU_ARE_NOT_ALLOWED_TO_CREATE_TEAMS_IN_THIS_ORGANIZATION: better_auth.RawError<"YOU_ARE_NOT_ALLOWED_TO_CREATE_TEAMS_IN_THIS_ORGANIZATION">;
2997
+ YOU_ARE_NOT_ALLOWED_TO_DELETE_TEAMS_IN_THIS_ORGANIZATION: better_auth.RawError<"YOU_ARE_NOT_ALLOWED_TO_DELETE_TEAMS_IN_THIS_ORGANIZATION">;
2998
+ YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_TEAM: better_auth.RawError<"YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_TEAM">;
2999
+ YOU_ARE_NOT_ALLOWED_TO_DELETE_THIS_TEAM: better_auth.RawError<"YOU_ARE_NOT_ALLOWED_TO_DELETE_THIS_TEAM">;
3000
+ INVITATION_LIMIT_REACHED: better_auth.RawError<"INVITATION_LIMIT_REACHED">;
3001
+ TEAM_MEMBER_LIMIT_REACHED: better_auth.RawError<"TEAM_MEMBER_LIMIT_REACHED">;
3002
+ USER_IS_NOT_A_MEMBER_OF_THE_TEAM: better_auth.RawError<"USER_IS_NOT_A_MEMBER_OF_THE_TEAM">;
3003
+ YOU_CAN_NOT_ACCESS_THE_MEMBERS_OF_THIS_TEAM: better_auth.RawError<"YOU_CAN_NOT_ACCESS_THE_MEMBERS_OF_THIS_TEAM">;
3004
+ YOU_DO_NOT_HAVE_AN_ACTIVE_TEAM: better_auth.RawError<"YOU_DO_NOT_HAVE_AN_ACTIVE_TEAM">;
3005
+ YOU_ARE_NOT_ALLOWED_TO_CREATE_A_NEW_TEAM_MEMBER: better_auth.RawError<"YOU_ARE_NOT_ALLOWED_TO_CREATE_A_NEW_TEAM_MEMBER">;
3006
+ YOU_ARE_NOT_ALLOWED_TO_REMOVE_A_TEAM_MEMBER: better_auth.RawError<"YOU_ARE_NOT_ALLOWED_TO_REMOVE_A_TEAM_MEMBER">;
3007
+ YOU_ARE_NOT_ALLOWED_TO_ACCESS_THIS_ORGANIZATION: better_auth.RawError<"YOU_ARE_NOT_ALLOWED_TO_ACCESS_THIS_ORGANIZATION">;
3008
+ YOU_ARE_NOT_A_MEMBER_OF_THIS_ORGANIZATION: better_auth.RawError<"YOU_ARE_NOT_A_MEMBER_OF_THIS_ORGANIZATION">;
3009
+ MISSING_AC_INSTANCE: better_auth.RawError<"MISSING_AC_INSTANCE">;
3010
+ YOU_MUST_BE_IN_AN_ORGANIZATION_TO_CREATE_A_ROLE: better_auth.RawError<"YOU_MUST_BE_IN_AN_ORGANIZATION_TO_CREATE_A_ROLE">;
3011
+ YOU_ARE_NOT_ALLOWED_TO_CREATE_A_ROLE: better_auth.RawError<"YOU_ARE_NOT_ALLOWED_TO_CREATE_A_ROLE">;
3012
+ YOU_ARE_NOT_ALLOWED_TO_UPDATE_A_ROLE: better_auth.RawError<"YOU_ARE_NOT_ALLOWED_TO_UPDATE_A_ROLE">;
3013
+ YOU_ARE_NOT_ALLOWED_TO_DELETE_A_ROLE: better_auth.RawError<"YOU_ARE_NOT_ALLOWED_TO_DELETE_A_ROLE">;
3014
+ YOU_ARE_NOT_ALLOWED_TO_READ_A_ROLE: better_auth.RawError<"YOU_ARE_NOT_ALLOWED_TO_READ_A_ROLE">;
3015
+ YOU_ARE_NOT_ALLOWED_TO_LIST_A_ROLE: better_auth.RawError<"YOU_ARE_NOT_ALLOWED_TO_LIST_A_ROLE">;
3016
+ YOU_ARE_NOT_ALLOWED_TO_GET_A_ROLE: better_auth.RawError<"YOU_ARE_NOT_ALLOWED_TO_GET_A_ROLE">;
3017
+ TOO_MANY_ROLES: better_auth.RawError<"TOO_MANY_ROLES">;
3018
+ INVALID_RESOURCE: better_auth.RawError<"INVALID_RESOURCE">;
3019
+ ROLE_NAME_IS_ALREADY_TAKEN: better_auth.RawError<"ROLE_NAME_IS_ALREADY_TAKEN">;
3020
+ CANNOT_DELETE_A_PRE_DEFINED_ROLE: better_auth.RawError<"CANNOT_DELETE_A_PRE_DEFINED_ROLE">;
3021
+ ROLE_IS_ASSIGNED_TO_MEMBERS: better_auth.RawError<"ROLE_IS_ASSIGNED_TO_MEMBERS">;
3022
+ };
3023
+ options: NoInfer<better_auth_plugins.OrganizationOptions & {
3024
+ teams: {
3025
+ enabled: true;
3026
+ };
3027
+ dynamicAccessControl?: {
3028
+ enabled?: false | undefined;
3029
+ } | undefined;
3030
+ }>;
3031
+ }, {
3032
+ id: "two-factor";
3033
+ endpoints: {
3034
+ enableTwoFactor: better_auth.StrictEndpoint<"/two-factor/enable", {
3035
+ method: "POST";
3036
+ body: better_auth.ZodObject<{
3037
+ password: better_auth.ZodString;
3038
+ issuer: better_auth.ZodOptional<better_auth.ZodString>;
3039
+ }, better_auth.$strip>;
3040
+ use: ((inputContext: better_auth.MiddlewareInputContext<better_auth.MiddlewareOptions>) => Promise<{
3041
+ session: {
3042
+ session: Record<string, any> & {
3043
+ id: string;
3044
+ createdAt: Date;
3045
+ updatedAt: Date;
3046
+ userId: string;
3047
+ expiresAt: Date;
3048
+ token: string;
3049
+ ipAddress?: string | null | undefined;
3050
+ userAgent?: string | null | undefined;
3051
+ };
3052
+ user: Record<string, any> & {
3053
+ id: string;
3054
+ createdAt: Date;
3055
+ updatedAt: Date;
3056
+ email: string;
3057
+ emailVerified: boolean;
3058
+ name: string;
3059
+ image?: string | null | undefined;
3060
+ };
3061
+ };
3062
+ }>)[];
3063
+ metadata: {
3064
+ openapi: {
3065
+ summary: string;
3066
+ description: string;
3067
+ responses: {
3068
+ 200: {
3069
+ description: string;
3070
+ content: {
3071
+ "application/json": {
3072
+ schema: {
3073
+ type: "object";
3074
+ properties: {
3075
+ totpURI: {
3076
+ type: string;
3077
+ description: string;
3078
+ };
3079
+ backupCodes: {
3080
+ type: string;
3081
+ items: {
3082
+ type: string;
3083
+ };
3084
+ description: string;
3085
+ };
3086
+ };
3087
+ };
3088
+ };
3089
+ };
3090
+ };
3091
+ };
3092
+ };
3093
+ };
3094
+ }, {
3095
+ totpURI: string;
3096
+ backupCodes: string[];
3097
+ }>;
3098
+ disableTwoFactor: better_auth.StrictEndpoint<"/two-factor/disable", {
3099
+ method: "POST";
3100
+ body: better_auth.ZodObject<{
3101
+ password: better_auth.ZodString;
3102
+ }, better_auth.$strip>;
3103
+ use: ((inputContext: better_auth.MiddlewareInputContext<better_auth.MiddlewareOptions>) => Promise<{
3104
+ session: {
3105
+ session: Record<string, any> & {
3106
+ id: string;
3107
+ createdAt: Date;
3108
+ updatedAt: Date;
3109
+ userId: string;
3110
+ expiresAt: Date;
3111
+ token: string;
3112
+ ipAddress?: string | null | undefined;
3113
+ userAgent?: string | null | undefined;
3114
+ };
3115
+ user: Record<string, any> & {
3116
+ id: string;
3117
+ createdAt: Date;
3118
+ updatedAt: Date;
3119
+ email: string;
3120
+ emailVerified: boolean;
3121
+ name: string;
3122
+ image?: string | null | undefined;
3123
+ };
3124
+ };
3125
+ }>)[];
3126
+ metadata: {
3127
+ openapi: {
3128
+ summary: string;
3129
+ description: string;
3130
+ responses: {
3131
+ 200: {
3132
+ description: string;
3133
+ content: {
3134
+ "application/json": {
3135
+ schema: {
3136
+ type: "object";
3137
+ properties: {
3138
+ status: {
3139
+ type: string;
3140
+ };
3141
+ };
3142
+ };
3143
+ };
3144
+ };
3145
+ };
3146
+ };
3147
+ };
3148
+ };
3149
+ }, {
3150
+ status: boolean;
3151
+ }>;
3152
+ verifyBackupCode: better_auth.StrictEndpoint<"/two-factor/verify-backup-code", {
3153
+ method: "POST";
3154
+ body: better_auth.ZodObject<{
3155
+ code: better_auth.ZodString;
3156
+ disableSession: better_auth.ZodOptional<better_auth.ZodBoolean>;
3157
+ trustDevice: better_auth.ZodOptional<better_auth.ZodBoolean>;
3158
+ }, better_auth.$strip>;
3159
+ metadata: {
3160
+ openapi: {
3161
+ description: string;
3162
+ responses: {
3163
+ "200": {
3164
+ description: string;
3165
+ content: {
3166
+ "application/json": {
3167
+ schema: {
3168
+ type: "object";
3169
+ properties: {
3170
+ user: {
3171
+ type: string;
3172
+ properties: {
3173
+ id: {
3174
+ type: string;
3175
+ description: string;
3176
+ };
3177
+ email: {
3178
+ type: string;
3179
+ format: string;
3180
+ nullable: boolean;
3181
+ description: string;
3182
+ };
3183
+ emailVerified: {
3184
+ type: string;
3185
+ nullable: boolean;
3186
+ description: string;
3187
+ };
3188
+ name: {
3189
+ type: string;
3190
+ nullable: boolean;
3191
+ description: string;
3192
+ };
3193
+ image: {
3194
+ type: string;
3195
+ format: string;
3196
+ nullable: boolean;
3197
+ description: string;
3198
+ };
3199
+ twoFactorEnabled: {
3200
+ type: string;
3201
+ description: string;
3202
+ };
3203
+ createdAt: {
3204
+ type: string;
3205
+ format: string;
3206
+ description: string;
3207
+ };
3208
+ updatedAt: {
3209
+ type: string;
3210
+ format: string;
3211
+ description: string;
3212
+ };
3213
+ };
3214
+ required: string[];
3215
+ description: string;
3216
+ };
3217
+ session: {
3218
+ type: string;
3219
+ properties: {
3220
+ token: {
3221
+ type: string;
3222
+ description: string;
3223
+ };
3224
+ userId: {
3225
+ type: string;
3226
+ description: string;
3227
+ };
3228
+ createdAt: {
3229
+ type: string;
3230
+ format: string;
3231
+ description: string;
3232
+ };
3233
+ expiresAt: {
3234
+ type: string;
3235
+ format: string;
3236
+ description: string;
3237
+ };
3238
+ };
3239
+ required: string[];
3240
+ description: string;
3241
+ };
3242
+ };
3243
+ required: string[];
3244
+ };
3245
+ };
3246
+ };
3247
+ };
3248
+ };
3249
+ };
3250
+ };
3251
+ }, {
3252
+ token: string | undefined;
3253
+ user: (Record<string, any> & {
3254
+ id: string;
3255
+ createdAt: Date;
3256
+ updatedAt: Date;
3257
+ email: string;
3258
+ emailVerified: boolean;
3259
+ name: string;
3260
+ image?: string | null | undefined;
3261
+ }) | better_auth_plugins.UserWithTwoFactor;
3262
+ }>;
3263
+ generateBackupCodes: better_auth.StrictEndpoint<"/two-factor/generate-backup-codes", {
3264
+ method: "POST";
3265
+ body: better_auth.ZodObject<{
3266
+ password: better_auth.ZodString;
3267
+ }, better_auth.$strip>;
3268
+ use: ((inputContext: better_auth.MiddlewareInputContext<better_auth.MiddlewareOptions>) => Promise<{
3269
+ session: {
3270
+ session: Record<string, any> & {
3271
+ id: string;
3272
+ createdAt: Date;
3273
+ updatedAt: Date;
3274
+ userId: string;
3275
+ expiresAt: Date;
3276
+ token: string;
3277
+ ipAddress?: string | null | undefined;
3278
+ userAgent?: string | null | undefined;
3279
+ };
3280
+ user: Record<string, any> & {
3281
+ id: string;
3282
+ createdAt: Date;
3283
+ updatedAt: Date;
3284
+ email: string;
3285
+ emailVerified: boolean;
3286
+ name: string;
3287
+ image?: string | null | undefined;
3288
+ };
3289
+ };
3290
+ }>)[];
3291
+ metadata: {
3292
+ openapi: {
3293
+ description: string;
3294
+ responses: {
3295
+ "200": {
3296
+ description: string;
3297
+ content: {
3298
+ "application/json": {
3299
+ schema: {
3300
+ type: "object";
3301
+ properties: {
3302
+ status: {
3303
+ type: string;
3304
+ description: string;
3305
+ enum: boolean[];
3306
+ };
3307
+ backupCodes: {
3308
+ type: string;
3309
+ items: {
3310
+ type: string;
3311
+ };
3312
+ description: string;
3313
+ };
3314
+ };
3315
+ required: string[];
3316
+ };
3317
+ };
3318
+ };
3319
+ };
3320
+ };
3321
+ };
3322
+ };
3323
+ }, {
3324
+ status: boolean;
3325
+ backupCodes: string[];
3326
+ }>;
3327
+ viewBackupCodes: better_auth.StrictEndpoint<string, {
3328
+ method: "POST";
3329
+ body: better_auth.ZodObject<{
3330
+ userId: better_auth.ZodCoercedString<unknown>;
3331
+ }, better_auth.$strip>;
3332
+ }, {
3333
+ status: boolean;
3334
+ backupCodes: string[];
3335
+ }>;
3336
+ sendTwoFactorOTP: better_auth.StrictEndpoint<"/two-factor/send-otp", {
3337
+ method: "POST";
3338
+ body: better_auth.ZodOptional<better_auth.ZodObject<{
3339
+ trustDevice: better_auth.ZodOptional<better_auth.ZodBoolean>;
3340
+ }, better_auth.$strip>>;
3341
+ metadata: {
3342
+ openapi: {
3343
+ summary: string;
3344
+ description: string;
3345
+ responses: {
3346
+ 200: {
3347
+ description: string;
3348
+ content: {
3349
+ "application/json": {
3350
+ schema: {
3351
+ type: "object";
3352
+ properties: {
3353
+ status: {
3354
+ type: string;
3355
+ };
3356
+ };
3357
+ };
3358
+ };
3359
+ };
3360
+ };
3361
+ };
3362
+ };
3363
+ };
3364
+ }, {
3365
+ status: boolean;
3366
+ }>;
3367
+ verifyTwoFactorOTP: better_auth.StrictEndpoint<"/two-factor/verify-otp", {
3368
+ method: "POST";
3369
+ body: better_auth.ZodObject<{
3370
+ code: better_auth.ZodString;
3371
+ trustDevice: better_auth.ZodOptional<better_auth.ZodBoolean>;
3372
+ }, better_auth.$strip>;
3373
+ metadata: {
3374
+ openapi: {
3375
+ summary: string;
3376
+ description: string;
3377
+ responses: {
3378
+ "200": {
3379
+ description: string;
3380
+ content: {
3381
+ "application/json": {
3382
+ schema: {
3383
+ type: "object";
3384
+ properties: {
3385
+ token: {
3386
+ type: string;
3387
+ description: string;
3388
+ };
3389
+ user: {
3390
+ type: string;
3391
+ properties: {
3392
+ id: {
3393
+ type: string;
3394
+ description: string;
3395
+ };
3396
+ email: {
3397
+ type: string;
3398
+ format: string;
3399
+ nullable: boolean;
3400
+ description: string;
3401
+ };
3402
+ emailVerified: {
3403
+ type: string;
3404
+ nullable: boolean;
3405
+ description: string;
3406
+ };
3407
+ name: {
3408
+ type: string;
3409
+ nullable: boolean;
3410
+ description: string;
3411
+ };
3412
+ image: {
3413
+ type: string;
3414
+ format: string;
3415
+ nullable: boolean;
3416
+ description: string;
3417
+ };
3418
+ createdAt: {
3419
+ type: string;
3420
+ format: string;
3421
+ description: string;
3422
+ };
3423
+ updatedAt: {
3424
+ type: string;
3425
+ format: string;
3426
+ description: string;
3427
+ };
3428
+ };
3429
+ required: string[];
3430
+ description: string;
3431
+ };
3432
+ };
3433
+ required: string[];
3434
+ };
3435
+ };
3436
+ };
3437
+ };
3438
+ };
3439
+ };
3440
+ };
3441
+ }, {
3442
+ token: string;
3443
+ user: better_auth_plugins.UserWithTwoFactor;
3444
+ } | {
3445
+ token: string;
3446
+ user: Record<string, any> & {
3447
+ id: string;
3448
+ createdAt: Date;
3449
+ updatedAt: Date;
3450
+ email: string;
3451
+ emailVerified: boolean;
3452
+ name: string;
3453
+ image?: string | null | undefined;
3454
+ };
3455
+ }>;
3456
+ generateTOTP: better_auth.StrictEndpoint<string, {
3457
+ method: "POST";
3458
+ body: better_auth.ZodObject<{
3459
+ secret: better_auth.ZodString;
3460
+ }, better_auth.$strip>;
3461
+ metadata: {
3462
+ openapi: {
3463
+ summary: string;
3464
+ description: string;
3465
+ responses: {
3466
+ 200: {
3467
+ description: string;
3468
+ content: {
3469
+ "application/json": {
3470
+ schema: {
3471
+ type: "object";
3472
+ properties: {
3473
+ code: {
3474
+ type: string;
3475
+ };
3476
+ };
3477
+ };
3478
+ };
3479
+ };
3480
+ };
3481
+ };
3482
+ };
3483
+ };
3484
+ }, {
3485
+ code: string;
3486
+ }>;
3487
+ getTOTPURI: better_auth.StrictEndpoint<"/two-factor/get-totp-uri", {
3488
+ method: "POST";
3489
+ use: ((inputContext: better_auth.MiddlewareInputContext<better_auth.MiddlewareOptions>) => Promise<{
3490
+ session: {
3491
+ session: Record<string, any> & {
3492
+ id: string;
3493
+ createdAt: Date;
3494
+ updatedAt: Date;
3495
+ userId: string;
3496
+ expiresAt: Date;
3497
+ token: string;
3498
+ ipAddress?: string | null | undefined;
3499
+ userAgent?: string | null | undefined;
3500
+ };
3501
+ user: Record<string, any> & {
3502
+ id: string;
3503
+ createdAt: Date;
3504
+ updatedAt: Date;
3505
+ email: string;
3506
+ emailVerified: boolean;
3507
+ name: string;
3508
+ image?: string | null | undefined;
3509
+ };
3510
+ };
3511
+ }>)[];
3512
+ body: better_auth.ZodObject<{
3513
+ password: better_auth.ZodString;
3514
+ }, better_auth.$strip>;
3515
+ metadata: {
3516
+ openapi: {
3517
+ summary: string;
3518
+ description: string;
3519
+ responses: {
3520
+ 200: {
3521
+ description: string;
3522
+ content: {
3523
+ "application/json": {
3524
+ schema: {
3525
+ type: "object";
3526
+ properties: {
3527
+ totpURI: {
3528
+ type: string;
3529
+ };
3530
+ };
3531
+ };
3532
+ };
3533
+ };
3534
+ };
3535
+ };
3536
+ };
3537
+ };
3538
+ }, {
3539
+ totpURI: string;
3540
+ }>;
3541
+ verifyTOTP: better_auth.StrictEndpoint<"/two-factor/verify-totp", {
3542
+ method: "POST";
3543
+ body: better_auth.ZodObject<{
3544
+ code: better_auth.ZodString;
3545
+ trustDevice: better_auth.ZodOptional<better_auth.ZodBoolean>;
3546
+ }, better_auth.$strip>;
3547
+ metadata: {
3548
+ openapi: {
3549
+ summary: string;
3550
+ description: string;
3551
+ responses: {
3552
+ 200: {
3553
+ description: string;
3554
+ content: {
3555
+ "application/json": {
3556
+ schema: {
3557
+ type: "object";
3558
+ properties: {
3559
+ status: {
3560
+ type: string;
3561
+ };
3562
+ };
3563
+ };
3564
+ };
3565
+ };
3566
+ };
3567
+ };
3568
+ };
3569
+ };
3570
+ }, {
3571
+ token: string;
3572
+ user: better_auth_plugins.UserWithTwoFactor;
3573
+ } | {
3574
+ token: string;
3575
+ user: Record<string, any> & {
3576
+ id: string;
3577
+ createdAt: Date;
3578
+ updatedAt: Date;
3579
+ email: string;
3580
+ emailVerified: boolean;
3581
+ name: string;
3582
+ image?: string | null | undefined;
3583
+ };
3584
+ }>;
3585
+ };
3586
+ options: NoInfer<better_auth_plugins.TwoFactorOptions>;
3587
+ hooks: {
3588
+ after: {
3589
+ matcher(context: better_auth.HookEndpointContext): boolean;
3590
+ handler: (inputContext: better_auth.MiddlewareInputContext<better_auth.MiddlewareOptions>) => Promise<{
3591
+ twoFactorRedirect: boolean;
3592
+ } | undefined>;
3593
+ }[];
3594
+ };
3595
+ schema: {
3596
+ user: {
3597
+ fields: {
3598
+ twoFactorEnabled: {
3599
+ type: "boolean";
3600
+ required: false;
3601
+ defaultValue: false;
3602
+ input: false;
3603
+ };
3604
+ };
3605
+ };
3606
+ twoFactor: {
3607
+ fields: {
3608
+ secret: {
3609
+ type: "string";
3610
+ required: true;
3611
+ returned: false;
3612
+ index: true;
3613
+ };
3614
+ backupCodes: {
3615
+ type: "string";
3616
+ required: true;
3617
+ returned: false;
3618
+ };
3619
+ userId: {
3620
+ type: "string";
3621
+ required: true;
3622
+ returned: false;
3623
+ references: {
3624
+ model: string;
3625
+ field: string;
3626
+ };
3627
+ index: true;
3628
+ };
3629
+ };
3630
+ };
3631
+ };
3632
+ rateLimit: {
3633
+ pathMatcher(path: string): boolean;
3634
+ window: number;
3635
+ max: number;
3636
+ }[];
3637
+ $ERROR_CODES: {
3638
+ OTP_NOT_ENABLED: better_auth.RawError<"OTP_NOT_ENABLED">;
3639
+ OTP_HAS_EXPIRED: better_auth.RawError<"OTP_HAS_EXPIRED">;
3640
+ TOTP_NOT_ENABLED: better_auth.RawError<"TOTP_NOT_ENABLED">;
3641
+ TWO_FACTOR_NOT_ENABLED: better_auth.RawError<"TWO_FACTOR_NOT_ENABLED">;
3642
+ BACKUP_CODES_NOT_ENABLED: better_auth.RawError<"BACKUP_CODES_NOT_ENABLED">;
3643
+ INVALID_BACKUP_CODE: better_auth.RawError<"INVALID_BACKUP_CODE">;
3644
+ INVALID_CODE: better_auth.RawError<"INVALID_CODE">;
3645
+ TOO_MANY_ATTEMPTS_REQUEST_NEW_CODE: better_auth.RawError<"TOO_MANY_ATTEMPTS_REQUEST_NEW_CODE">;
3646
+ INVALID_TWO_FACTOR_COOKIE: better_auth.RawError<"INVALID_TWO_FACTOR_COOKIE">;
3647
+ };
3648
+ }];
3649
+ }>;
3650
+ type DeepSpaceAuth = ReturnType<typeof createDeepSpaceAuth>;
3651
+
3652
+ /**
3653
+ * Subscription primitives shared between client (useSubscription hook) and
3654
+ * server (requireSubscription helper). Pulled into a shared module so the
3655
+ * entitlement gate has a single source of truth — if it ever diverges between
3656
+ * client and server, gated features silently disagree about who's allowed in,
3657
+ * which is a security bug, not a UX bug.
3658
+ */
3659
+ type SubscriptionStatus = 'none' | 'trialing' | 'active' | 'past_due' | 'canceled' | 'incomplete' | 'incomplete_expired' | 'unpaid' | 'paused';
3660
+ /** Per-interval price advertised by a plan. */
3661
+ interface PlanPrice {
3662
+ interval: 'month' | 'year';
3663
+ priceCents: number;
3664
+ currency?: string;
3665
+ }
3666
+ /**
3667
+ * Plan shape returned by `/api/subscriptions/me` and consumed directly by
3668
+ * `<PricingTable plans={sub.plans}>`. Fields beyond slug/rank are optional so
3669
+ * a free tier (no prices, no trial) doesn't need to fabricate them.
3670
+ */
3671
+ interface PlanInfo {
3672
+ slug: string;
3673
+ rank: number;
3674
+ name: string;
3675
+ trialDays?: number | null;
3676
+ prices: PlanPrice[];
3677
+ }
3678
+
3679
+ /**
3680
+ * Server-side subscription helpers. Called from the developer's worker
3681
+ * (Hono Context) — they proxy to the api-worker's `/api/subscriptions/me`
3682
+ * using the worker's signed app-identity headers, so the same trust
3683
+ * model the SDK hook uses applies here.
3684
+ */
3685
+
3686
+ interface SubscriptionRead {
3687
+ tier: string;
3688
+ status: SubscriptionStatus;
3689
+ currentPeriodEnd: number | null;
3690
+ cancelAtPeriodEnd: boolean;
3691
+ trialEndsAt: number | null;
3692
+ plans: PlanInfo[];
3693
+ }
3694
+ interface StarterAppEnv$1 extends ApiWorkerEnv {
3695
+ APP_IDENTITY_TOKEN: string;
3696
+ APP_NAME: string;
3697
+ }
3698
+ declare function getSubscription(c: Context<{
3699
+ Bindings: StarterAppEnv$1;
3700
+ }>): Promise<SubscriptionRead>;
3701
+ /**
3702
+ * Enforce a tier gate inside a route. Throws `SubscriptionRequiredError` if
3703
+ * the caller's subscription doesn't meet the requested tier OR isn't currently
3704
+ * entitled (status ∈ {active, trialing}). Callers do
3705
+ * `await requireSubscription(c, { atLeast: 'pro' })` and let the thrown
3706
+ * response propagate.
3707
+ *
3708
+ * Why the status gate: a row with `planSlug='pro'` and `status='past_due'`
3709
+ * means "this user used to be on Pro but their payment is overdue" — paid
3710
+ * features should not unlock. Same for canceled/unpaid/incomplete.
3711
+ */
3712
+ declare function requireSubscription(c: Context<{
3713
+ Bindings: StarterAppEnv$1;
3714
+ }>, opts: {
3715
+ tier?: string;
3716
+ atLeast?: string;
3717
+ }): Promise<SubscriptionRead>;
3718
+ declare class SubscriptionRequiredError extends Error {
3719
+ readonly required: string;
3720
+ readonly current: string;
3721
+ constructor(required: string, current: string);
3722
+ }
3723
+ /**
3724
+ * Thrown by `getSubscription` / `requireSubscription` when the upstream
3725
+ * `/api/subscriptions/me` rejects the caller with 401 or 403 — typically
3726
+ * because the inbound request didn't carry a Bearer token, or carried one
3727
+ * the api-worker can't verify. Distinct from `SubscriptionRequiredError`
3728
+ * (which is about tier/entitlement, not identity) so route handlers can
3729
+ * map identity failures to 401 and tier failures to 402.
3730
+ */
3731
+ declare class SubscriptionAuthError extends Error {
3732
+ readonly status: number;
3733
+ constructor(message: string, status: number);
3734
+ }
3735
+ /**
3736
+ * Cancel one customer's subscription, or every subscription on a given plan.
3737
+ *
3738
+ * Forwards the inbound `Authorization` header — the platform verifies the
3739
+ * actor is the app owner before calling Stripe. As with `refundInvoice`,
3740
+ * gate this in your own admin route too; the platform check is the second
3741
+ * layer, not the first.
3742
+ *
3743
+ * Defaults to `atPeriodEnd: true` so customers aren't cut off mid-cycle.
3744
+ * Pass `atPeriodEnd: false` for an immediate cancel (refund handled
3745
+ * separately if you want one).
3746
+ */
3747
+ interface CancelSubscriptionOpts {
3748
+ /** Cancel one specific customer's subscription. Mutually exclusive with `planSlug`. */
3749
+ userId?: string;
3750
+ /** Cancel every active subscription on this plan. Mutually exclusive with `userId`. */
3751
+ planSlug?: string;
3752
+ /** Default true. Pass false for an immediate cancel. */
3753
+ atPeriodEnd?: boolean;
3754
+ /** Optional free-form audit reason. */
3755
+ reason?: string;
3756
+ }
3757
+ interface CancelSubscriptionResult {
3758
+ success: boolean;
3759
+ canceled: number;
3760
+ failures: Array<{
3761
+ stripeSubscriptionId: string;
3762
+ error: string;
3763
+ }>;
3764
+ atPeriodEnd: boolean;
3765
+ /**
3766
+ * True when the matching subscription set was larger than the server-side
3767
+ * batch limit (currently 50). Loop the call until this returns false to
3768
+ * cancel every remaining row — the `cancel_at_period_end` flag is
3769
+ * idempotent, so re-flagging an already-flagged subscription is a no-op.
3770
+ */
3771
+ hasMore: boolean;
3772
+ }
3773
+ declare function cancelSubscription(c: Context<{
3774
+ Bindings: StarterAppEnv$1;
3775
+ }>, opts: CancelSubscriptionOpts): Promise<CancelSubscriptionResult>;
3776
+ declare class CancelSubscriptionError extends Error {
3777
+ readonly status: number;
3778
+ constructor(message: string, status: number);
3779
+ }
3780
+
3781
+ interface StarterAppEnv extends ApiWorkerEnv {
3782
+ APP_IDENTITY_TOKEN: string;
3783
+ APP_NAME: string;
3784
+ }
3785
+ interface RefundResult {
3786
+ success: boolean;
3787
+ stripeRefundId: string;
3788
+ amountRefunded: number;
3789
+ status: 'pending' | 'succeeded' | 'failed' | 'canceled' | 'requires_action' | null;
3790
+ }
3791
+ interface RefundOpts {
3792
+ invoiceId: string;
3793
+ amount?: number;
3794
+ reason?: 'requested_by_customer' | 'duplicate' | 'fraudulent';
3795
+ requestNonce?: string;
3796
+ }
3797
+ declare function refundInvoice(c: Context<{
3798
+ Bindings: StarterAppEnv;
3799
+ }>, opts: RefundOpts): Promise<RefundResult>;
3800
+ declare class RefundError extends Error {
3801
+ readonly status: number;
3802
+ constructor(message: string, status: number);
3803
+ }
3804
+
3805
+ 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, BASE_USERS_SCHEMA, BUILT_IN_TOOLS, BaseRoom, CHANNELS_SCHEMA, CHANNEL_INVITATIONS_SCHEMA, CHANNEL_MEMBERS_SCHEMA, CONVERSATION_SCHEMAS, COST_RATES, CancelSubscriptionError, type CancelSubscriptionOpts, type CancelSubscriptionResult, CanvasRoom, type CanvasShape, type ChatContextConfig, type ChatMessageRow, type ChatRow, type ChatTurn, type CollectionPermissionSummary, type CollectionSchema, type ColumnDefinition, type ColumnInterpretation, 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_MAX_SKEW_MS, DIRECTORY_SCHEMAS, type DOBindings, type DOManifest, type DOManifestEntry, type DebugApiContext, 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 InternalSignature, type JwtClaims, type JwtVerifierConfig, MESSAGES_SCHEMA, type MutateActionData, type PermissionAnalysis, type PermissionContext, type PermissionLevel, type PermissionSource, type PlanInfo, type PlatformWorkerEnv, type Player, type PrefixResult, type PresencePeer, PresenceRoom, type QueryActionData, REACTIONS_SCHEMA, READ_RECEIPTS_SCHEMA, RESERVED_BINDING_NAMES, RESERVED_COLLECTION_NAMES, type RecordContext, RecordRoom, type RecordRoomConfig, RefundError, type RefundOpts, type RefundResult, type ResolvedColumn, type ResolvedPermission, type RolePermissions, type RunMigrationsResult, SYSTEM_COLLECTION_SCHEMAS, SYSTEM_MANAGED_COLUMNS, SchemaRegistry, type ScopeContext, type ScopedR2Auth, type ScopedR2Config, type ScopedR2Handler, type ScreenshotEnv, type ScreenshotOptions, type ScreenshotResult, type SharedConnection, type SignInternalPayloadInput, type StoredRecord, SubscriptionAuthError, type SubscriptionContext, type SubscriptionRead, SubscriptionRequiredError, type SubscriptionStatus, type Summarizer, type TokenDebugInfo, type ToolResult, type ToolSchema, type ToolsApiContext, USERS_COLUMNS, type User, type UserAttachment, type UserContext, VOTING_SCHEMAS, type ValidationError, type VerifiedAuth, type VerifyInternalSignatureInput, type VerifyOutcome, type VerifyResult, type Viewport, WORKSPACE_SCHEMAS, type YjsContext, YjsRoom, analyzePermissions, apiWorkerFetch, appendMessage, applySlidingWindow, authWorkerFetch, bindingManifestFromOutputConfig, broadcastChange, broadcastYjsUpdate, buildCronContext, buildInternalPayload, buildTableSelect, buildUiParts, canCreate, canDelete, canRead, canUpdate, cancelSubscription, capToolResultSize, captureScreenshot, checkFieldPermissions, coerceValue, collectionTableName, columnId, computeDoMigration, computeHmacHex, createChat, createDeepSpaceAI, createDeepSpaceAuth, createScopedR2Handler, dataToColumnValues, decodeJwtPayload, deleteChatCascade, deleteRecord, executeQuery, getAllUsers, getChat, getGlobalDOSchemas, getGlobalDOType, getOrCreateYjsDoc, getRecord, getRolePermissions, getSubscription, getUser, getYjsDocKey, handleApiRequest, handleDelete, handlePut, handleSetRole, handleSubscribe, handleToolsRequest, handleUnsubscribe, handleUserList, handleUserUpdate, handleYjsBinaryMessage, handleYjsJoin, handleYjsLeave, isAutoProvision, isOwner, lintSchema, loadMessages, makeDefaultSummarizer, meterAi, meterUsage, meterVectorize, noopPermissionContext, platformWorkerFetch, prepareMessagesWithCompaction, priceBindingUsageEvent, putRecord, readRecord, recordMatchesSubscription, refundInvoice, registerUser, requireSubscription, resolveAppName, resolveColumn, rowToData, runMigrations, saveYjsDoc, signInternalPayload, timingSafeEqualHex, totalChars, truncateOldToolResults, turnsToCoreMessages, unwrapToolOutput, updateChat, validateAppName, validateBindingManifest, validateDoManifest, verifyInternalSignature, verifyJwt, workspaceAccountsSchema, workspaceContentSharesSchema, workspaceEmailHandlesSchema, workspaceFormResponsesSchema, workspacePeopleSchema, workspaceProjectsSchema, workspaceTagsSchema, workspaceTasksSchema, workspaceTeamMembersSchema, workspaceTeamsSchema, workspaceTransactionsSchema };