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.
- package/dist/cli.js +694 -145
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +174 -10
- package/dist/index.js +589 -276
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts +3805 -0
- package/dist/server.js +6371 -0
- package/dist/server.js.map +1 -0
- package/dist/worker.d.ts +152 -16
- package/dist/worker.js +246 -29
- package/dist/worker.js.map +1 -1
- package/package.json +10 -3
package/dist/server.d.ts
ADDED
|
@@ -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 };
|