deepspace 0.7.0 → 0.8.1

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.
Files changed (63) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +22 -8
  3. package/dist/cli.js +9812 -4674
  4. package/dist/cli.js.map +1 -1
  5. package/dist/feature-installer.js +638 -0
  6. package/dist/feature-installer.js.map +1 -0
  7. package/dist/index.d.ts +107 -117
  8. package/dist/index.js +610 -779
  9. package/dist/index.js.map +1 -1
  10. package/dist/server.d.ts +403 -167
  11. package/dist/server.js +498 -367
  12. package/dist/server.js.map +1 -1
  13. package/dist/testing.d.ts +115 -0
  14. package/dist/testing.js +3 -3
  15. package/dist/testing.js.map +1 -1
  16. package/dist/worker.d.ts +397 -160
  17. package/dist/worker.js +506 -367
  18. package/dist/worker.js.map +1 -1
  19. package/features/ai-chat/feature.json +2 -0
  20. package/features/ai-chat/src/AiChatPage.tsx +2 -3
  21. package/features/ai-chat/src/ChatPanel.messages.tsx +344 -0
  22. package/features/ai-chat/src/ChatPanel.stream.ts +397 -0
  23. package/features/ai-chat/src/ChatPanel.tsx +158 -862
  24. package/features/ai-chat/src/__tests__/ChatPanel.stream.test.ts +163 -0
  25. package/features/canvas/src/CanvasListPage.tsx +1 -1
  26. package/features/docs/feature.json +4 -8
  27. package/features/docs/src/DocsEditorPage.tsx +33 -326
  28. package/features/docs/src/DocsListPage.tsx +33 -29
  29. package/features/docs/src/InviteDialog.tsx +11 -40
  30. package/features/docs/src/__tests__/docs-presence-access.test.ts +71 -0
  31. package/features/docs/src/docs-library-types.ts +28 -3
  32. package/features/docs/src/docs-ui.css +1 -4
  33. package/features/docs/src/use-docs-presence-access.ts +330 -0
  34. package/features/file-attachments/feature.json +2 -2
  35. package/features/file-attachments/src/AttachmentPreview.tsx +428 -0
  36. package/features/file-attachments/src/AttachmentsPage.tsx +100 -462
  37. package/features/file-attachments/src/useFilePreview.ts +54 -68
  38. package/features/file-manager/src/FileManagerPage.tsx +1 -1
  39. package/features/integration-test/src/integration.spec.ts +1 -1
  40. package/features/items/feature.json +1 -5
  41. package/features/items/src/ItemsPage.tsx +207 -135
  42. package/features/landing/feature.json +3 -0
  43. package/features/landing/src/LandingPage.tsx +4 -18
  44. package/features/leaderboard/feature.json +1 -5
  45. package/features/leaderboard/src/LeaderboardPage.tsx +333 -198
  46. package/features/messaging/feature.json +0 -4
  47. package/features/messaging/src/ChatMultiPage.tsx +1 -3
  48. package/features/messaging/src/ChatPage.tsx +1 -3
  49. package/features/messaging/src/components/chat/MessageActionSheet.tsx +40 -16
  50. package/features/search-bar/feature.json +8 -3
  51. package/features/search-bar/src/SearchOverlay.tsx +108 -252
  52. package/features/search-bar/src/__tests__/search-model.test.ts +40 -0
  53. package/features/search-bar/src/search-model.ts +223 -0
  54. package/features/tasks/src/TasksPage.tsx +3 -3
  55. package/features/topbar/src/Topbar.tsx +0 -17
  56. package/package.json +2 -3
  57. package/features/docs/src/docs-favorites.ts +0 -18
  58. package/features/docs/src/use-yjs-room-with-awareness.ts +0 -289
  59. package/features/file-attachments/src/attachments-constants.ts +0 -9
  60. package/features/items/src/items-constants.ts +0 -10
  61. package/features/leaderboard/src/leaderboard-constants.ts +0 -21
  62. package/features/messaging/src/components/chat/ReactionPicker.tsx +0 -2
  63. package/scripts/add-feature.cjs +0 -807
package/dist/server.js CHANGED
@@ -1,3 +1,72 @@
1
+ // src/server/utils/cors.ts
2
+ function matchOrigin(origin, patterns) {
3
+ if (!origin) return null;
4
+ const matches = patterns.some((pattern) => {
5
+ const rx = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\\\*/g, "[^./]+");
6
+ return new RegExp(`^${rx}$`).test(origin);
7
+ });
8
+ return matches ? origin : null;
9
+ }
10
+ function platformCorsPatterns(env) {
11
+ const app = env.APP_DOMAIN;
12
+ const platform = env.PLATFORM_DOMAIN;
13
+ return [
14
+ .../* @__PURE__ */ new Set([`https://${platform}`, `https://*.${platform}`, `https://*.${app}`]),
15
+ "http://localhost:*"
16
+ ];
17
+ }
18
+
19
+ // src/server/utils/plan-quotas.ts
20
+ var DEPLOYED_APPS_DEFAULTS = {
21
+ free: 1,
22
+ starter: 4,
23
+ premium: 10,
24
+ admin: 100
25
+ };
26
+ function parseQuotaLimit(raw, fallback) {
27
+ if (!raw) return fallback;
28
+ const n = Number(raw);
29
+ return Number.isInteger(n) && n >= 0 ? n : fallback;
30
+ }
31
+
32
+ // src/server/utils/registry-client.ts
33
+ var REGISTRY_INSTANCE = "v1:global";
34
+ var APP_ID_RE = /^(app_[0-9A-HJKMNP-TV-Z]{26}|[a-z0-9][a-z0-9_-]{0,63})$/;
35
+ var STRICT_APP_ID_RE = /^app_[0-9A-HJKMNP-TV-Z]{26}$/;
36
+ var RegistryClientError = class extends Error {
37
+ constructor(message, code, status) {
38
+ super(message);
39
+ this.code = code;
40
+ this.status = status;
41
+ this.name = "RegistryClientError";
42
+ }
43
+ };
44
+ function registryStub(ns) {
45
+ return ns.get(ns.idFromName(REGISTRY_INSTANCE));
46
+ }
47
+ async function callRegistry(stub, body) {
48
+ const res = await stub.fetch("https://registry/", {
49
+ method: "POST",
50
+ headers: { "Content-Type": "application/json" },
51
+ body: JSON.stringify(body)
52
+ });
53
+ const json = await res.json().catch(() => ({}));
54
+ if (!res.ok) {
55
+ throw new RegistryClientError(
56
+ json.error ?? `Registry error (${res.status})`,
57
+ json.code ?? "error",
58
+ res.status
59
+ );
60
+ }
61
+ return json;
62
+ }
63
+ function registryErrorJson(c, err) {
64
+ if (err instanceof RegistryClientError) {
65
+ return c.json({ error: err.message, code: err.code }, err.status);
66
+ }
67
+ throw err;
68
+ }
69
+
1
70
  // src/server/utils/tools.ts
2
71
  var DEFAULT_QUERY_LIMIT = 50;
3
72
  function applyAiToolDefaults(toolName, params) {
@@ -77,72 +146,6 @@ var BUILT_IN_TOOLS = [
77
146
  description: "List all users in this room",
78
147
  params: {}
79
148
  },
80
- // Storage tools (legacy key-value)
81
- {
82
- name: "storage.list",
83
- description: "List all keys in a storage scope",
84
- params: {
85
- scope: { type: "string", description: "Storage scope: global, files, user, userFiles", required: true },
86
- prefix: { type: "string", description: "Optional key prefix filter", required: false },
87
- userId: { type: "string", description: "User ID (required for user/userFiles scopes)", required: false }
88
- }
89
- },
90
- {
91
- name: "storage.read",
92
- description: "Read one or more keys from storage",
93
- params: {
94
- scope: { type: "string", description: "Storage scope: global, files, user, userFiles", required: true },
95
- keys: { type: "array", description: "Array of keys to read (or single key string)", required: true },
96
- userId: { type: "string", description: "User ID (required for user/userFiles scopes)", required: false }
97
- }
98
- },
99
- {
100
- name: "storage.write",
101
- description: "Write one or more key-value pairs to storage (batch supported)",
102
- params: {
103
- scope: { type: "string", description: "Storage scope: global, files, user, userFiles", required: true },
104
- data: { type: "object", description: "Object with key-value pairs to write", required: true },
105
- userId: { type: "string", description: "User ID (required for user/userFiles scopes)", required: false }
106
- }
107
- },
108
- {
109
- name: "storage.delete",
110
- description: "Delete one or more keys from storage",
111
- params: {
112
- scope: { type: "string", description: "Storage scope: global, files, user, userFiles", required: true },
113
- keys: { type: "array", description: "Array of keys to delete", required: true },
114
- userId: { type: "string", description: "User ID (required for user/userFiles scopes)", required: false }
115
- }
116
- },
117
- // Backup tools
118
- {
119
- name: "backup.create",
120
- description: "Create a backup of the current Yjs document state to R2",
121
- params: {
122
- description: { type: "string", description: "Optional description for the backup", required: false }
123
- }
124
- },
125
- {
126
- name: "backup.list",
127
- description: "List all available backups",
128
- params: {
129
- limit: { type: "number", description: "Maximum number of backups to return (default: 50)", required: false }
130
- }
131
- },
132
- {
133
- name: "backup.restore",
134
- description: "Restore the Yjs document from a backup",
135
- params: {
136
- backupId: { type: "string", description: "The backup ID to restore from", required: true }
137
- }
138
- },
139
- {
140
- name: "backup.delete",
141
- description: "Delete a specific backup",
142
- params: {
143
- backupId: { type: "string", description: "The backup ID to delete", required: true }
144
- }
145
- },
146
149
  // Yjs tools (collaborative document access)
147
150
  {
148
151
  name: "yjs.list",
@@ -173,8 +176,11 @@ var BUILT_IN_TOOLS = [
173
176
  // src/server/utils/scoped-r2-files.ts
174
177
  var CORS_HEADERS = { "Access-Control-Allow-Origin": "*" };
175
178
  function contentDisposition(originalName) {
176
- const safe = originalName.replace(/[\x00-\x1f\x7f"\\]/g, "_") || "download";
177
- const encoded = encodeURIComponent(originalName);
179
+ const safe = originalName.replace(/[^\x20-\x7e]|["\\]/g, "_") || "download";
180
+ const encoded = encodeURIComponent(originalName).replace(
181
+ /[!'()*]/g,
182
+ (char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`
183
+ );
178
184
  return `inline; filename="${safe}"; filename*=UTF-8''${encoded}`;
179
185
  }
180
186
  function sanitizeSubpath(raw) {
@@ -183,6 +189,9 @@ function sanitizeSubpath(raw) {
183
189
  if (segments.some((s) => s === ".." || s === ".")) return null;
184
190
  return raw;
185
191
  }
192
+ function encodeKeyPath(key) {
193
+ return key.split("/").map(encodeURIComponent).join("/");
194
+ }
186
195
  function isKeyWithinPrefix(key, prefix) {
187
196
  return key.startsWith(prefix);
188
197
  }
@@ -203,9 +212,15 @@ function normalizeMimeType(raw) {
203
212
  function createScopedR2Handler(config) {
204
213
  const requireAuthForMutations = config.requireAuthForMutations ?? true;
205
214
  return async (request, url, bucket, auth) => {
206
- const subpathRaw = decodeURIComponent(
207
- url.pathname.replace("/api/files", "").replace(/^\//, "")
208
- );
215
+ let subpathRaw;
216
+ try {
217
+ subpathRaw = decodeURIComponent(url.pathname.replace("/api/files", "").replace(/^\//, ""));
218
+ } catch {
219
+ return Response.json(
220
+ { error: "Invalid path encoding" },
221
+ { status: 400, headers: CORS_HEADERS }
222
+ );
223
+ }
209
224
  const subpath = sanitizeSubpath(subpathRaw);
210
225
  if (subpath === null) {
211
226
  return Response.json(
@@ -213,9 +228,10 @@ function createScopedR2Handler(config) {
213
228
  { status: 400, headers: CORS_HEADERS }
214
229
  );
215
230
  }
216
- const isKeyedGet = subpath !== "" && request.method === "GET";
217
- const defaultScope = isKeyedGet ? "app" : "self";
218
- const scope = url.searchParams.get("scope") || defaultScope;
231
+ const scope = url.searchParams.get("scope") || "self";
232
+ if (scope !== "self" && scope !== "app") {
233
+ return Response.json({ error: "Invalid scope" }, { status: 400, headers: CORS_HEADERS });
234
+ }
219
235
  const result = config.resolvePrefix(scope, { userId: auth.userId, url });
220
236
  if (!result.prefix) {
221
237
  const errorMsg = result.error ?? "Invalid scope";
@@ -230,10 +246,10 @@ function createScopedR2Handler(config) {
230
246
  { status: 401, headers: CORS_HEADERS }
231
247
  );
232
248
  }
233
- return handleUpload(request, url, bucket, prefix, auth.userId);
249
+ return handleUpload(request, url, bucket, prefix, auth.userId, scope);
234
250
  }
235
251
  if (!subpath && request.method === "GET") {
236
- return handleList(bucket, prefix, url);
252
+ return handleList(bucket, prefix, url, scope);
237
253
  }
238
254
  if (subpath && request.method === "GET") {
239
255
  if (!isKeyWithinPrefix(subpath, prefix)) {
@@ -242,7 +258,7 @@ function createScopedR2Handler(config) {
242
258
  { status: 403, headers: CORS_HEADERS }
243
259
  );
244
260
  }
245
- return handleDownload(bucket, subpath);
261
+ return handleDownload(bucket, subpath, scope);
246
262
  }
247
263
  if (subpath && request.method === "DELETE") {
248
264
  if (requireAuthForMutations && !auth.userId) {
@@ -263,7 +279,7 @@ function createScopedR2Handler(config) {
263
279
  return Response.json({ error: "Method not allowed" }, { status: 405, headers: CORS_HEADERS });
264
280
  };
265
281
  }
266
- async function handleUpload(request, url, bucket, prefix, userId) {
282
+ async function handleUpload(request, url, bucket, prefix, userId, scope) {
267
283
  try {
268
284
  const contentType = request.headers.get("content-type") || "";
269
285
  let fileData;
@@ -327,35 +343,37 @@ async function handleUpload(request, url, bucket, prefix, userId) {
327
343
  uploadedAt: (/* @__PURE__ */ new Date()).toISOString()
328
344
  }
329
345
  });
330
- const fileUrl = `${url.origin}/api/files/${key}`;
346
+ const fileUrl = new URL(`/api/files/${encodeKeyPath(key)}`, url.origin);
347
+ fileUrl.searchParams.set("scope", scope);
331
348
  return Response.json(
332
- { success: true, key, url: fileUrl, name: fileName },
349
+ { success: true, key, url: fileUrl.toString(), name: fileName },
333
350
  { headers: CORS_HEADERS }
334
351
  );
335
352
  } catch (err) {
336
353
  const msg = err instanceof Error ? err.message : "Upload failed";
337
354
  console.error("[handleUpload] Error:", msg, err instanceof Error ? err.stack : "");
338
- return Response.json(
339
- { error: msg },
340
- { status: 500, headers: CORS_HEADERS }
341
- );
355
+ return Response.json({ error: msg }, { status: 500, headers: CORS_HEADERS });
342
356
  }
343
357
  }
344
- async function handleList(bucket, prefix, url) {
358
+ async function handleList(bucket, prefix, url, scope) {
345
359
  const userPrefix = url.searchParams.get("prefix") || "";
346
360
  const listPrefix = `${prefix}${userPrefix}`;
347
361
  const limit = parseInt(url.searchParams.get("limit") || "100", 10);
348
362
  const listed = await bucket.list({ prefix: listPrefix, limit });
349
- const files = listed.objects.map((obj) => ({
350
- key: obj.key,
351
- size: obj.size,
352
- uploaded: obj.uploaded.toISOString(),
353
- url: `${url.origin}/api/files/${obj.key}`,
354
- ...obj.customMetadata
355
- }));
363
+ const files = listed.objects.map((obj) => {
364
+ const fileUrl = new URL(`/api/files/${encodeKeyPath(obj.key)}`, url.origin);
365
+ fileUrl.searchParams.set("scope", scope);
366
+ return {
367
+ key: obj.key,
368
+ size: obj.size,
369
+ uploaded: obj.uploaded.toISOString(),
370
+ url: fileUrl.toString(),
371
+ ...obj.customMetadata
372
+ };
373
+ });
356
374
  return Response.json({ files, truncated: listed.truncated }, { headers: CORS_HEADERS });
357
375
  }
358
- async function handleDownload(bucket, key) {
376
+ async function handleDownload(bucket, key, scope) {
359
377
  const object = await bucket.get(key);
360
378
  if (!object) {
361
379
  return Response.json({ error: "File not found" }, { status: 404, headers: CORS_HEADERS });
@@ -363,7 +381,10 @@ async function handleDownload(bucket, key) {
363
381
  const headers = new Headers();
364
382
  headers.set("Content-Type", object.httpMetadata?.contentType || "application/octet-stream");
365
383
  headers.set("X-Content-Type-Options", "nosniff");
366
- headers.set("Cache-Control", "public, max-age=31536000, immutable");
384
+ headers.set(
385
+ "Cache-Control",
386
+ scope === "app" ? "public, max-age=31536000, immutable" : "private, no-store"
387
+ );
367
388
  headers.set("Access-Control-Allow-Origin", "*");
368
389
  if (object.customMetadata?.originalName) {
369
390
  headers.set("Content-Disposition", contentDisposition(object.customMetadata.originalName));
@@ -378,7 +399,7 @@ function resolveApiTransport(env) {
378
399
  return { kind: "url", baseUrl: env.API_WORKER_URL.replace(/\/$/, "") };
379
400
  }
380
401
  throw new Error(
381
- "apiWorkerFetch: neither env.API_WORKER nor env.API_WORKER_URL is set. Add a [[services]] binding in wrangler.toml for production, or let `deepspace dev` write API_WORKER_URL into .dev.vars for local development."
402
+ "apiWorkerFetch: neither env.API_WORKER nor env.API_WORKER_URL is set. Add a [[services]] binding in wrangler.toml for production, or let `deepspace dev start` write API_WORKER_URL into .dev.vars for local development."
382
403
  );
383
404
  }
384
405
  function resolvePlatformTransport(env) {
@@ -387,7 +408,7 @@ function resolvePlatformTransport(env) {
387
408
  return { kind: "url", baseUrl: env.PLATFORM_WORKER_URL.replace(/\/$/, "") };
388
409
  }
389
410
  throw new Error(
390
- "platformWorkerFetch: neither env.PLATFORM_WORKER nor env.PLATFORM_WORKER_URL is set. Add a [[services]] binding in wrangler.toml for production, or let `deepspace dev` write PLATFORM_WORKER_URL into .dev.vars for local development."
411
+ "platformWorkerFetch: neither env.PLATFORM_WORKER nor env.PLATFORM_WORKER_URL is set. Add a [[services]] binding in wrangler.toml for production, or let `deepspace dev start` write PLATFORM_WORKER_URL into .dev.vars for local development."
391
412
  );
392
413
  }
393
414
  function buildTargetUrl(transport, path, internalHost) {
@@ -433,7 +454,7 @@ function platformWorkerFetch(env, pathOrRequest, init) {
433
454
  function authWorkerFetch(env, path, init) {
434
455
  if (!env.AUTH_WORKER_URL) {
435
456
  throw new Error(
436
- "authWorkerFetch: env.AUTH_WORKER_URL is not set. `deepspace dev` should write this into .dev.vars; for production, set it as a wrangler var or secret."
457
+ "authWorkerFetch: env.AUTH_WORKER_URL is not set. `deepspace dev start` should write this into .dev.vars; for production, set it as a wrangler var or secret."
437
458
  );
438
459
  }
439
460
  const baseUrl = env.AUTH_WORKER_URL.replace(/\/$/, "");
@@ -454,15 +475,17 @@ function buildCronContext(env, ownerUserId, roomId = "default") {
454
475
  const roomIdObj = env.RECORD_ROOMS.idFromName(roomId);
455
476
  const room = env.RECORD_ROOMS.get(roomIdObj);
456
477
  async function executeTool3(tool2, params) {
457
- const response = await room.fetch(new Request("https://internal/api/tools/execute", {
458
- method: "POST",
459
- headers: {
460
- "Content-Type": "application/json",
461
- "X-User-Id": ownerUserId,
462
- "X-App-Action": "true"
463
- },
464
- body: JSON.stringify({ tool: tool2, params })
465
- }));
478
+ const response = await room.fetch(
479
+ new Request("https://internal/api/tools/execute", {
480
+ method: "POST",
481
+ headers: {
482
+ "Content-Type": "application/json",
483
+ "X-User-Id": ownerUserId,
484
+ "X-App-Action": "true"
485
+ },
486
+ body: JSON.stringify({ tool: tool2, params })
487
+ })
488
+ );
466
489
  const result = await response.json();
467
490
  if (!result.success) {
468
491
  throw new Error(`RecordRoom tool ${tool2} failed: ${result.error || "Unknown error"}`);
@@ -492,7 +515,7 @@ function buildCronContext(env, ownerUserId, roomId = "default") {
492
515
  async call(endpoint, params = {}) {
493
516
  if (!env.APP_OWNER_JWT) {
494
517
  throw new Error(
495
- "integrations.call requires env.APP_OWNER_JWT. Deployed apps receive this automatically; in dev, `deepspace dev` writes it into .dev.vars."
518
+ "integrations.call requires env.APP_OWNER_JWT. Deployed apps receive this automatically; in dev, `deepspace dev start` writes it into .dev.vars."
496
519
  );
497
520
  }
498
521
  const response = await apiWorkerFetch(env, `/api/integrations/${endpoint}`, {
@@ -543,7 +566,7 @@ function createDeepSpaceAI(env, provider, options = {}) {
543
566
  const authToken = options.authToken ?? env.APP_OWNER_JWT;
544
567
  if (!authToken) {
545
568
  throw new Error(
546
- "createDeepSpaceAI: no auth token available. Either pass `options.authToken` explicitly (for user-initiated calls), or ensure `env.APP_OWNER_JWT` is set (injected at deploy time or by `deepspace dev`)."
569
+ "createDeepSpaceAI: no auth token available. Either pass `options.authToken` explicitly (for user-initiated calls), or ensure `env.APP_OWNER_JWT` is set (injected at deploy time or by `deepspace dev start`)."
547
570
  );
548
571
  }
549
572
  const proxyFetch = (input, init) => {
@@ -679,7 +702,7 @@ function appendAppIdentity(headers, env) {
679
702
  }
680
703
 
681
704
  // src/server/utils/screenshot.ts
682
- async function captureScreenshot(env, opts) {
705
+ async function captureScreenshot(env, url) {
683
706
  const headers = new Headers({ "content-type": "application/json" });
684
707
  appendAppIdentity(headers, env);
685
708
  const res = await platformWorkerFetch(
@@ -687,16 +710,11 @@ async function captureScreenshot(env, opts) {
687
710
  new Request("https://platform-internal/internal/screenshot", {
688
711
  method: "POST",
689
712
  headers,
690
- body: JSON.stringify(opts)
713
+ body: JSON.stringify({ url })
691
714
  })
692
715
  );
693
716
  if (!res.ok) {
694
- let detail = "";
695
- try {
696
- detail = (await res.text()).slice(0, 300);
697
- } catch {
698
- }
699
- console.warn(`[captureScreenshot] platform returned ${res.status}:`, detail);
717
+ console.warn(`[captureScreenshot] platform returned ${res.status}`);
700
718
  return null;
701
719
  }
702
720
  return {
@@ -1062,8 +1080,6 @@ async function executeTool(stub, userId, tool2, params) {
1062
1080
  function toChatRow(env) {
1063
1081
  return {
1064
1082
  recordId: env.recordId,
1065
- // Deprecated alias — populated for back-compat with callers reading `id`.
1066
- id: env.recordId,
1067
1083
  userId: env.data.userId,
1068
1084
  title: env.data.title ?? "",
1069
1085
  model: env.data.model,
@@ -1076,8 +1092,6 @@ function toChatRow(env) {
1076
1092
  function toMessageRow(env) {
1077
1093
  return {
1078
1094
  recordId: env.recordId,
1079
- // Deprecated alias — populated for back-compat with callers reading `id`.
1080
- id: env.recordId,
1081
1095
  chatId: env.data.chatId,
1082
1096
  userId: env.data.userId,
1083
1097
  role: env.data.role,
@@ -1330,7 +1344,7 @@ var BaseRoom = class {
1330
1344
  // ==========================================================================
1331
1345
  // WebSocket Connection
1332
1346
  // ==========================================================================
1333
- async handleWebSocketUpgrade(request, url) {
1347
+ async handleWebSocketUpgrade(_request, url) {
1334
1348
  const userId = url.searchParams.get("userId") || void 0;
1335
1349
  const userName = url.searchParams.get("userName") || "Anonymous";
1336
1350
  const userEmail = url.searchParams.get("userEmail") || "";
@@ -1382,7 +1396,7 @@ var BaseRoom = class {
1382
1396
  }
1383
1397
  }
1384
1398
  }
1385
- async webSocketError(ws, error) {
1399
+ async webSocketError(_ws, error) {
1386
1400
  console.error(`[${this.constructor.name}] webSocketError:`, error);
1387
1401
  }
1388
1402
  async alarm() {
@@ -2382,6 +2396,11 @@ function parseYjsDocKey(docKey) {
2382
2396
  fieldName: docKey.slice(lastColon + 1)
2383
2397
  };
2384
2398
  }
2399
+ function hasYjsSubscription(attachment, target) {
2400
+ return attachment.yjsSubscriptions.some(
2401
+ (subscription) => subscription.collection === target.collection && subscription.recordId === target.recordId && subscription.fieldName === target.fieldName
2402
+ );
2403
+ }
2385
2404
  var SYSTEM_COLLECTIONS = /* @__PURE__ */ new Set([
2386
2405
  "canvas-settings"
2387
2406
  ]);
@@ -2397,7 +2416,7 @@ var SYSTEM_COLLECTION_SCHEMAS = [
2397
2416
  function getYjsDocKey(collection, recordId, fieldName) {
2398
2417
  return `${collection}:${recordId}:${fieldName}`;
2399
2418
  }
2400
- async function getOrCreateYjsDoc(ctx, docKey) {
2419
+ function getOrCreateYjsDoc(ctx, docKey) {
2401
2420
  let doc = ctx.yjsDocs.get(docKey);
2402
2421
  if (doc) return doc;
2403
2422
  doc = new Y2.Doc();
@@ -2419,6 +2438,20 @@ async function getOrCreateYjsDoc(ctx, docKey) {
2419
2438
  ctx.yjsDocs.set(docKey, doc);
2420
2439
  return doc;
2421
2440
  }
2441
+ function unloadYjsDocIfUnused(ctx, docKey, excludeWs = null) {
2442
+ const target = parseYjsDocKey(docKey);
2443
+ if (!target) return false;
2444
+ for (const ws of ctx.state.getWebSockets()) {
2445
+ if (ws === excludeWs) continue;
2446
+ const attachment = ws.deserializeAttachment();
2447
+ if (attachment && hasYjsSubscription(attachment, target)) return false;
2448
+ }
2449
+ const doc = ctx.yjsDocs.get(docKey);
2450
+ if (!doc) return false;
2451
+ ctx.yjsDocs.delete(docKey);
2452
+ doc.destroy();
2453
+ return true;
2454
+ }
2422
2455
  function saveYjsDoc(sql, docKey, doc) {
2423
2456
  const state = Y2.encodeStateAsUpdate(doc);
2424
2457
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -2432,7 +2465,7 @@ function saveYjsDoc(sql, docKey, doc) {
2432
2465
  now
2433
2466
  );
2434
2467
  }
2435
- async function handleYjsJoin(ctx, ws, attachment, payload) {
2468
+ function handleYjsJoin(ctx, ws, attachment, payload) {
2436
2469
  const { collection, recordId, fieldName } = payload;
2437
2470
  const isSystem = SYSTEM_COLLECTIONS.has(collection);
2438
2471
  const schema = ctx.schemaRegistry.get(collection);
@@ -2471,10 +2504,8 @@ async function handleYjsJoin(ctx, ws, attachment, payload) {
2471
2504
  hasWriteAccess = canUpdate(schema, attachment.role, recordWithId, attachment.userId, permCtx);
2472
2505
  }
2473
2506
  const docKey = getYjsDocKey(collection, recordId, fieldName);
2474
- const doc = await getOrCreateYjsDoc(ctx, docKey);
2475
- if (!attachment.yjsSubscriptions.some(
2476
- (s) => s.collection === collection && s.recordId === recordId && s.fieldName === fieldName
2477
- )) {
2507
+ const doc = getOrCreateYjsDoc(ctx, docKey);
2508
+ if (!hasYjsSubscription(attachment, payload)) {
2478
2509
  attachment.yjsSubscriptions.push({ collection, recordId, fieldName });
2479
2510
  ws.serializeAttachment(attachment);
2480
2511
  }
@@ -2496,14 +2527,15 @@ async function handleYjsJoin(ctx, ws, attachment, payload) {
2496
2527
  }
2497
2528
  ctx.send(ws, { type: MSG.YJS_JOIN, payload: { collection, recordId, fieldName, canWrite: hasWriteAccess } });
2498
2529
  }
2499
- function handleYjsLeave(ws, attachment, payload) {
2530
+ function handleYjsLeave(ctx, ws, attachment, payload) {
2500
2531
  const { collection, recordId, fieldName } = payload;
2501
2532
  attachment.yjsSubscriptions = attachment.yjsSubscriptions.filter(
2502
2533
  (s) => !(s.collection === collection && s.recordId === recordId && s.fieldName === fieldName)
2503
2534
  );
2504
2535
  ws.serializeAttachment(attachment);
2536
+ unloadYjsDocIfUnused(ctx, getYjsDocKey(collection, recordId, fieldName), ws);
2505
2537
  }
2506
- async function handleYjsBinaryMessage(ctx, ws, attachment, data) {
2538
+ function handleYjsBinaryMessage(ctx, ws, attachment, data) {
2507
2539
  const decoder = createDecoder(data);
2508
2540
  const messageType = readVarUint(decoder);
2509
2541
  if (messageType === MSG_YJS_AWARENESS) {
@@ -2512,11 +2544,7 @@ async function handleYjsBinaryMessage(ctx, ws, attachment, data) {
2512
2544
  const payload = readVarUint8Array(decoder);
2513
2545
  const parsed2 = parseYjsDocKey(docKey2);
2514
2546
  if (!parsed2) return;
2515
- const { collection: collection2, recordId: recordId2, fieldName: fieldName2 } = parsed2;
2516
- const isSubscribed2 = attachment.yjsSubscriptions.some(
2517
- (s) => s.collection === collection2 && s.recordId === recordId2 && s.fieldName === fieldName2
2518
- );
2519
- if (!isSubscribed2) return;
2547
+ if (!hasYjsSubscription(attachment, parsed2)) return;
2520
2548
  if (attachment.awarenessClientId == null) {
2521
2549
  try {
2522
2550
  const pd = createDecoder(payload);
@@ -2536,12 +2564,9 @@ async function handleYjsBinaryMessage(ctx, ws, attachment, data) {
2536
2564
  const docKey = new TextDecoder().decode(docKeyBytes);
2537
2565
  const parsed = parseYjsDocKey(docKey);
2538
2566
  if (!parsed) return;
2539
- const { collection, recordId, fieldName } = parsed;
2540
- const isSubscribed = attachment.yjsSubscriptions.some(
2541
- (s) => s.collection === collection && s.recordId === recordId && s.fieldName === fieldName
2542
- );
2543
- if (!isSubscribed) return;
2544
- const doc = await getOrCreateYjsDoc(ctx, docKey);
2567
+ const { collection, recordId } = parsed;
2568
+ if (!hasYjsSubscription(attachment, parsed)) return;
2569
+ const doc = getOrCreateYjsDoc(ctx, docKey);
2545
2570
  const syncType = readVarUint(decoder);
2546
2571
  switch (syncType) {
2547
2572
  case MSG_SYNC_STEP1: {
@@ -2566,7 +2591,6 @@ async function handleYjsBinaryMessage(ctx, ws, attachment, data) {
2566
2591
  }
2567
2592
  const update = readVarUint8Array(decoder);
2568
2593
  Y2.applyUpdate(doc, update, "client");
2569
- saveYjsDoc(ctx.sql, docKey, doc);
2570
2594
  broadcastYjsUpdate(ctx, docKey, update, ws);
2571
2595
  break;
2572
2596
  }
@@ -2575,7 +2599,6 @@ async function handleYjsBinaryMessage(ctx, ws, attachment, data) {
2575
2599
  function broadcastYjsUpdate(ctx, docKey, update, excludeWs) {
2576
2600
  const parsed = parseYjsDocKey(docKey);
2577
2601
  if (!parsed) return;
2578
- const { collection, recordId, fieldName } = parsed;
2579
2602
  const encoder = createEncoder();
2580
2603
  writeVarUint(encoder, MSG_YJS_SYNC);
2581
2604
  writeVarUint8Array(encoder, new TextEncoder().encode(docKey));
@@ -2586,10 +2609,7 @@ function broadcastYjsUpdate(ctx, docKey, update, excludeWs) {
2586
2609
  if (ws === excludeWs) continue;
2587
2610
  const attachment = ws.deserializeAttachment();
2588
2611
  if (!attachment) continue;
2589
- const isSubscribed = attachment.yjsSubscriptions.some(
2590
- (s) => s.collection === collection && s.recordId === recordId && s.fieldName === fieldName
2591
- );
2592
- if (isSubscribed) {
2612
+ if (hasYjsSubscription(attachment, parsed)) {
2593
2613
  ctx.sendBinary(ws, message);
2594
2614
  }
2595
2615
  }
@@ -2597,7 +2617,6 @@ function broadcastYjsUpdate(ctx, docKey, update, excludeWs) {
2597
2617
  function broadcastAwareness(ctx, docKey, payload, excludeWs) {
2598
2618
  const parsed = parseYjsDocKey(docKey);
2599
2619
  if (!parsed) return;
2600
- const { collection, recordId, fieldName } = parsed;
2601
2620
  const encoder = createEncoder();
2602
2621
  writeVarUint(encoder, MSG_YJS_AWARENESS);
2603
2622
  writeVarUint8Array(encoder, new TextEncoder().encode(docKey));
@@ -2607,10 +2626,7 @@ function broadcastAwareness(ctx, docKey, payload, excludeWs) {
2607
2626
  if (ws === excludeWs) continue;
2608
2627
  const attachment = ws.deserializeAttachment();
2609
2628
  if (!attachment) continue;
2610
- const isSubscribed = attachment.yjsSubscriptions.some(
2611
- (s) => s.collection === collection && s.recordId === recordId && s.fieldName === fieldName
2612
- );
2613
- if (isSubscribed) {
2629
+ if (hasYjsSubscription(attachment, parsed)) {
2614
2630
  ctx.sendBinary(ws, message);
2615
2631
  }
2616
2632
  }
@@ -2629,6 +2645,17 @@ function broadcastAwarenessRemoval(ctx, attachment) {
2629
2645
  broadcastAwareness(ctx, docKey, removalPayload, null);
2630
2646
  }
2631
2647
  }
2648
+ function handleYjsDisconnect(ctx, ws, attachment) {
2649
+ broadcastAwarenessRemoval(ctx, attachment);
2650
+ const docKeys = new Set(
2651
+ attachment.yjsSubscriptions.map(
2652
+ (subscription) => getYjsDocKey(subscription.collection, subscription.recordId, subscription.fieldName)
2653
+ )
2654
+ );
2655
+ for (const docKey of docKeys) {
2656
+ unloadYjsDocIfUnused(ctx, docKey, ws);
2657
+ }
2658
+ }
2632
2659
 
2633
2660
  // src/server/handlers/subscriptions.ts
2634
2661
  function handleSubscribe(ctx, ws, attachment, payload) {
@@ -2962,7 +2989,7 @@ function handleUserList(ctx, ws, _attachment) {
2962
2989
  }));
2963
2990
  ctx.send(ws, { type: MSG.USER_LIST, payload: { users } });
2964
2991
  }
2965
- function handleUserUpdate(ctx, ws, attachment, payload) {
2992
+ function handleUserUpdate(ctx, _ws, attachment, payload) {
2966
2993
  const existing = getUserRecord(ctx.sql, attachment.userId, ctx.schemaRegistry);
2967
2994
  if (!existing) return;
2968
2995
  const data = { lastSeenAt: (/* @__PURE__ */ new Date()).toISOString() };
@@ -3067,7 +3094,7 @@ async function handleToolExecute(ctx, request) {
3067
3094
  }
3068
3095
  }
3069
3096
  if (tool2.startsWith("yjs.")) {
3070
- const result2 = await executeYjsTool(ctx, tool2, params, userId, userRole);
3097
+ const result2 = executeYjsTool(ctx, tool2, params, userId, userRole);
3071
3098
  const status2 = result2.success ? 200 : result2.error?.includes("not found") ? 404 : 400;
3072
3099
  return Response.json(result2, { status: status2, headers: CORS_HEADERS2 });
3073
3100
  }
@@ -3222,7 +3249,15 @@ function buildYjsCtx(ctx) {
3222
3249
  sendBinary: ctx.sendBinary
3223
3250
  };
3224
3251
  }
3225
- async function executeYjsTool(ctx, toolName, params, userId, userRole) {
3252
+ function withYjsDoc(ctx, docKey, operation) {
3253
+ const doc = getOrCreateYjsDoc(ctx, docKey);
3254
+ try {
3255
+ return operation(doc);
3256
+ } finally {
3257
+ unloadYjsDocIfUnused(ctx, docKey);
3258
+ }
3259
+ }
3260
+ function executeYjsTool(ctx, toolName, params, userId, userRole) {
3226
3261
  switch (toolName) {
3227
3262
  case "yjs.list": {
3228
3263
  const rows = ctx.sql.exec(
@@ -3250,20 +3285,21 @@ async function executeYjsTool(ctx, toolName, params, userId, userRole) {
3250
3285
  if (permError) return permError;
3251
3286
  const docKey = getYjsDocKey(collection, recordId, fieldName);
3252
3287
  const yjsCtx = buildYjsCtx(ctx);
3253
- const doc = await getOrCreateYjsDoc(yjsCtx, docKey);
3254
- const ytext = doc.getText(fieldName);
3255
- const text = ytext.toString();
3256
- const sharedTypes = {};
3257
- for (const [key, type] of doc.share) {
3258
- if (type instanceof Y3.Text) {
3259
- sharedTypes[key || "(default)"] = `Y.Text (${type.length} chars)`;
3260
- } else if (type instanceof Y3.Map) {
3261
- sharedTypes[key] = `Y.Map (${type.size} entries)`;
3262
- } else if (type instanceof Y3.Array) {
3263
- sharedTypes[key] = `Y.Array (${type.length} items)`;
3288
+ return withYjsDoc(yjsCtx, docKey, (doc) => {
3289
+ const ytext = doc.getText(fieldName);
3290
+ const text = ytext.toString();
3291
+ const sharedTypes = {};
3292
+ for (const [key, type] of doc.share) {
3293
+ if (type instanceof Y3.Text) {
3294
+ sharedTypes[key || "(default)"] = `Y.Text (${type.length} chars)`;
3295
+ } else if (type instanceof Y3.Map) {
3296
+ sharedTypes[key] = `Y.Map (${type.size} entries)`;
3297
+ } else if (type instanceof Y3.Array) {
3298
+ sharedTypes[key] = `Y.Array (${type.length} items)`;
3299
+ }
3264
3300
  }
3265
- }
3266
- return { success: true, data: { docKey, text, sharedTypes } };
3301
+ return { success: true, data: { docKey, text, sharedTypes } };
3302
+ });
3267
3303
  }
3268
3304
  case "yjs.setText": {
3269
3305
  const { collection, recordId, fieldName, text } = params;
@@ -3277,22 +3313,26 @@ async function executeYjsTool(ctx, toolName, params, userId, userRole) {
3277
3313
  if (permError) return permError;
3278
3314
  const docKey = getYjsDocKey(collection, recordId, fieldName);
3279
3315
  const yjsCtx = buildYjsCtx(ctx);
3280
- const doc = await getOrCreateYjsDoc(yjsCtx, docKey);
3281
- const ytext = doc.getText(fieldName);
3282
- let capturedUpdate = null;
3283
- const updateHandler = (update) => {
3284
- capturedUpdate = update;
3285
- };
3286
- doc.on("update", updateHandler);
3287
- doc.transact(() => {
3288
- ytext.delete(0, ytext.length);
3289
- ytext.insert(0, text);
3316
+ return withYjsDoc(yjsCtx, docKey, (doc) => {
3317
+ const ytext = doc.getText(fieldName);
3318
+ let capturedUpdate = null;
3319
+ const updateHandler = (update) => {
3320
+ capturedUpdate = update;
3321
+ };
3322
+ doc.on("update", updateHandler);
3323
+ try {
3324
+ doc.transact(() => {
3325
+ ytext.delete(0, ytext.length);
3326
+ ytext.insert(0, text);
3327
+ });
3328
+ } finally {
3329
+ doc.off("update", updateHandler);
3330
+ }
3331
+ if (capturedUpdate) {
3332
+ broadcastYjsUpdate(yjsCtx, docKey, capturedUpdate, null);
3333
+ }
3334
+ return { success: true, data: { docKey, length: text.length } };
3290
3335
  });
3291
- doc.off("update", updateHandler);
3292
- if (capturedUpdate) {
3293
- broadcastYjsUpdate(yjsCtx, docKey, capturedUpdate, null);
3294
- }
3295
- return { success: true, data: { docKey, length: text.length } };
3296
3336
  }
3297
3337
  default:
3298
3338
  return { success: false, error: `Unknown Yjs tool: ${toolName}` };
@@ -3535,8 +3575,6 @@ var RecordRoom = class extends BaseRoom {
3535
3575
  initPromise = null;
3536
3576
  /** Yjs docs loaded in memory (key: collection:recordId:fieldName) */
3537
3577
  yjsDocs = /* @__PURE__ */ new Map();
3538
- /** Next Yjs client ID counter */
3539
- nextYjsClientId = 1;
3540
3578
  /** Owner user ID — gets admin role automatically */
3541
3579
  ownerUserId;
3542
3580
  /** True until the first fetch() completes — detects hibernation wake-up */
@@ -3553,7 +3591,7 @@ var RecordRoom = class extends BaseRoom {
3553
3591
  * The HTTP debug API (`/api/debug/*`) runs arbitrary SQL and role changes
3554
3592
  * with no auth of its own, so it is gated here at the DO's single ingress.
3555
3593
  * Off unless a deployment opts in with `ALLOW_DEBUG_ROUTES=true`
3556
- * (`deepspace dev`/`test` set it automatically). Deployments holding shared
3594
+ * (`deepspace dev start`/`test run` set it automatically). Deployments holding shared
3557
3595
  * data override this to always return false.
3558
3596
  */
3559
3597
  get debugRoutesEnabled() {
@@ -3688,13 +3726,17 @@ var RecordRoom = class extends BaseRoom {
3688
3726
  this.sql.exec(`DROP TABLE IF EXISTS records`);
3689
3727
  return;
3690
3728
  }
3691
- let migrated = 0;
3692
- let skipped = 0;
3729
+ const prepared = [];
3730
+ const blocked = {
3731
+ missingSchema: 0,
3732
+ invalidData: 0,
3733
+ targetConflict: 0
3734
+ };
3693
3735
  for (const row of rows) {
3694
3736
  const r = row;
3695
3737
  const schema = this.schemaRegistry.get(r.collection);
3696
3738
  if (!schema || !schema.columns) {
3697
- skipped++;
3739
+ blocked.missingSchema++;
3698
3740
  continue;
3699
3741
  }
3700
3742
  const tbl = collectionTableName(r.collection);
@@ -3704,14 +3746,18 @@ var RecordRoom = class extends BaseRoom {
3704
3746
  }
3705
3747
  const existing = this.sql.exec(`SELECT 1 FROM "${tbl}" WHERE _row_id = ?`, r.record_id).toArray();
3706
3748
  if (existing.length > 0) {
3707
- skipped++;
3749
+ blocked.targetConflict++;
3708
3750
  continue;
3709
3751
  }
3710
3752
  let data;
3711
3753
  try {
3712
3754
  data = JSON.parse(r.data);
3713
3755
  } catch {
3714
- skipped++;
3756
+ blocked.invalidData++;
3757
+ continue;
3758
+ }
3759
+ if (data === null || typeof data !== "object" || Array.isArray(data)) {
3760
+ blocked.invalidData++;
3715
3761
  continue;
3716
3762
  }
3717
3763
  const columns = schema.columns.map(resolveColumn);
@@ -3732,20 +3778,32 @@ var RecordRoom = class extends BaseRoom {
3732
3778
  r.updated_at,
3733
3779
  ...colIds.map((c) => colValues[c])
3734
3780
  ];
3735
- this.sql.exec(
3736
- `INSERT INTO "${tbl}" (${allCols.join(", ")}) VALUES (${placeholders})`,
3737
- ...params
3738
- );
3739
- migrated++;
3781
+ prepared.push({
3782
+ sql: `INSERT INTO "${tbl}" (${allCols.join(", ")}) VALUES (${placeholders})`,
3783
+ params
3784
+ });
3740
3785
  }
3741
- if (migrated > 0 || skipped > 0) {
3742
- console.log(
3743
- `[RecordRoom] Migrated ${migrated} records from document-mode, ${skipped} skipped`
3786
+ const blockedCount = blocked.missingSchema + blocked.invalidData + blocked.targetConflict;
3787
+ if (blockedCount > 0) {
3788
+ console.error(
3789
+ `[RecordRoom] Legacy records migration blocked; no records migrated and the records table was retained for retry (${blockedCount} blocked: ${blocked.missingSchema} missing schema, ${blocked.invalidData} invalid data, ${blocked.targetConflict} target conflict)`
3744
3790
  );
3791
+ return;
3745
3792
  }
3746
- this.sql.exec(`DROP TABLE IF EXISTS records`);
3793
+ this.state.storage.transactionSync(() => {
3794
+ for (const record of prepared) {
3795
+ this.sql.exec(record.sql, ...record.params);
3796
+ }
3797
+ this.sql.exec(`DROP TABLE IF EXISTS records`);
3798
+ });
3799
+ console.log(
3800
+ `[RecordRoom] Migrated ${prepared.length} records from document-mode; legacy records table removed`
3801
+ );
3747
3802
  } catch (error) {
3748
- console.error(`[RecordRoom] Records table migration error:`, error);
3803
+ console.error(
3804
+ `[RecordRoom] Legacy records migration failed; no records migrated and the records table was retained for retry:`,
3805
+ error
3806
+ );
3749
3807
  }
3750
3808
  }
3751
3809
  // ============================================================================
@@ -3798,8 +3856,7 @@ var RecordRoom = class extends BaseRoom {
3798
3856
  userImageUrl,
3799
3857
  role: registeredUser.role,
3800
3858
  subscriptions: [],
3801
- yjsSubscriptions: [],
3802
- yjsClientId: this.nextYjsClientId++
3859
+ yjsSubscriptions: []
3803
3860
  };
3804
3861
  this.send(ws, { type: MSG.USER_INFO, payload: registeredUser });
3805
3862
  const totalMs = Date.now() - timing.fetchStart;
@@ -3814,8 +3871,7 @@ var RecordRoom = class extends BaseRoom {
3814
3871
  userEmail: "",
3815
3872
  role: ROLE_ANONYMOUS,
3816
3873
  subscriptions: [],
3817
- yjsSubscriptions: [],
3818
- yjsClientId: this.nextYjsClientId++
3874
+ yjsSubscriptions: []
3819
3875
  };
3820
3876
  this.send(ws, {
3821
3877
  type: MSG.USER_INFO,
@@ -3884,12 +3940,12 @@ var RecordRoom = class extends BaseRoom {
3884
3940
  try {
3885
3941
  const attachment = ws.deserializeAttachment();
3886
3942
  if (attachment) {
3887
- broadcastAwarenessRemoval(this.createYjsContext(), attachment);
3943
+ handleYjsDisconnect(this.createYjsContext(), ws, attachment);
3888
3944
  }
3889
3945
  } catch {
3890
3946
  }
3891
3947
  }
3892
- async webSocketError(ws, error) {
3948
+ async webSocketError(_ws, error) {
3893
3949
  console.error(`[RecordRoom] webSocketError:`, error);
3894
3950
  }
3895
3951
  // ============================================================================
@@ -3932,7 +3988,7 @@ var RecordRoom = class extends BaseRoom {
3932
3988
  await handleYjsJoin(yjsCtx, ws, attachment, payload);
3933
3989
  break;
3934
3990
  case MSG.YJS_LEAVE:
3935
- handleYjsLeave(ws, attachment, payload);
3991
+ handleYjsLeave(yjsCtx, ws, attachment, payload);
3936
3992
  break;
3937
3993
  case MSG.LIST_SCHEMAS:
3938
3994
  this.handleListSchemas(ws);
@@ -4091,7 +4147,7 @@ var YjsRoom = class extends BaseRoom {
4091
4147
  }
4092
4148
  onMessage(_ws, _user, _message) {
4093
4149
  }
4094
- onBinaryMessage(ws, user, data) {
4150
+ onBinaryMessage(ws, _user, data) {
4095
4151
  const bytes = new Uint8Array(data);
4096
4152
  const decoder = createDecoder(bytes);
4097
4153
  const messageType = readVarUint(decoder);
@@ -4384,7 +4440,7 @@ var GameRoom = class extends BaseRoom {
4384
4440
  this.sendTo(ws, { type: MSG.ERROR, payload: { error: `Unknown game message type: ${type}` } });
4385
4441
  }
4386
4442
  }
4387
- onDisconnect(ws, user) {
4443
+ onDisconnect(_ws, user) {
4388
4444
  const player = this.players.get(user.userId);
4389
4445
  if (player) {
4390
4446
  this.players.delete(user.userId);
@@ -4692,7 +4748,7 @@ var CanvasRoom = class extends BaseRoom {
4692
4748
  this.sendTo(ws, { type: MSG.ERROR, payload: { error: `Unknown canvas message type: ${type}` } });
4693
4749
  }
4694
4750
  }
4695
- onDisconnect(ws, user) {
4751
+ onDisconnect(_ws, user) {
4696
4752
  this.viewports.delete(user.userId);
4697
4753
  this.undoStacks.delete(user.userId);
4698
4754
  this.redoStacks.delete(user.userId);
@@ -4886,6 +4942,140 @@ var PresenceRoom = class extends BaseRoom {
4886
4942
  }
4887
4943
  };
4888
4944
 
4945
+ // src/server/rooms/cron-schedule.ts
4946
+ var TASK_NAME_RE = /^[a-z0-9-]+$/;
4947
+ function validateTask(task) {
4948
+ if (!task.name || typeof task.name !== "string") {
4949
+ throw new Error("CronTask.name is required");
4950
+ }
4951
+ if (task.name.length > 64 || !TASK_NAME_RE.test(task.name)) {
4952
+ throw new Error(
4953
+ `CronTask.name "${task.name}" must be lowercase alphanumeric with hyphens, 1-64 chars`
4954
+ );
4955
+ }
4956
+ const hasInterval = task.intervalMinutes != null;
4957
+ const hasSchedule = task.schedule != null;
4958
+ const hasTimezone = task.timezone != null;
4959
+ if (hasInterval && (hasSchedule || hasTimezone)) {
4960
+ throw new Error(`CronTask "${task.name}" cannot mix intervalMinutes with schedule/timezone`);
4961
+ }
4962
+ if (!hasInterval && !(hasSchedule && hasTimezone)) {
4963
+ throw new Error(
4964
+ `CronTask "${task.name}" must declare either intervalMinutes or schedule+timezone`
4965
+ );
4966
+ }
4967
+ if (hasInterval && (!Number.isInteger(task.intervalMinutes) || task.intervalMinutes < 1 || task.intervalMinutes > 10080)) {
4968
+ throw new Error(`CronTask "${task.name}" intervalMinutes must be an integer 1..10080`);
4969
+ }
4970
+ if (hasSchedule) {
4971
+ parseCronExpression(task.schedule);
4972
+ if (typeof task.timezone !== "string" || task.timezone.length === 0) {
4973
+ throw new Error(`CronTask "${task.name}" timezone must be a non-empty IANA string`);
4974
+ }
4975
+ try {
4976
+ new Intl.DateTimeFormat("en-US", { timeZone: task.timezone });
4977
+ } catch {
4978
+ throw new Error(
4979
+ `CronTask "${task.name}" timezone "${task.timezone}" is not a valid IANA timezone`
4980
+ );
4981
+ }
4982
+ }
4983
+ return task;
4984
+ }
4985
+ function parseCronExpression(expression) {
4986
+ const fields = expression.trim().split(/\s+/);
4987
+ if (fields.length !== 5) {
4988
+ throw new Error(
4989
+ `Cron expression must have exactly 5 fields, got ${fields.length}: "${expression}"`
4990
+ );
4991
+ }
4992
+ const [minute, hour, dayOfMonth, month, dayOfWeek] = fields;
4993
+ return {
4994
+ minute: parseField(minute, 0, 59, "minute"),
4995
+ hour: parseField(hour, 0, 23, "hour"),
4996
+ dayOfMonth: parseField(dayOfMonth, 1, 31, "day-of-month"),
4997
+ month: parseField(month, 1, 12, "month"),
4998
+ dayOfWeek: parseField(dayOfWeek, 0, 6, "day-of-week")
4999
+ };
5000
+ }
5001
+ function parseField(field, min, max, label) {
5002
+ const result = /* @__PURE__ */ new Set();
5003
+ for (const part of field.split(",")) {
5004
+ const value = part.trim();
5005
+ if (value === "*") {
5006
+ for (let i = min; i <= max; i++) result.add(i);
5007
+ continue;
5008
+ }
5009
+ const allStep = value.match(/^\*\/(\d+)$/);
5010
+ if (allStep) {
5011
+ const step = Number.parseInt(allStep[1], 10);
5012
+ if (step <= 0) throw new Error(`Cron ${label} step must be positive: "${value}"`);
5013
+ for (let i = min; i <= max; i += step) result.add(i);
5014
+ continue;
5015
+ }
5016
+ const range = value.match(/^(\d+)-(\d+)(?:\/(\d+))?$/);
5017
+ if (range) {
5018
+ const start = Number.parseInt(range[1], 10);
5019
+ const end = Number.parseInt(range[2], 10);
5020
+ const step = range[3] ? Number.parseInt(range[3], 10) : 1;
5021
+ if (start < min || end > max || start > end || step <= 0) {
5022
+ throw new Error(`Cron ${label} range out of bounds: "${value}" (${min}..${max})`);
5023
+ }
5024
+ for (let i = start; i <= end; i += step) result.add(i);
5025
+ continue;
5026
+ }
5027
+ const literal = Number.parseInt(value, 10);
5028
+ if (Number.isNaN(literal) || literal < min || literal > max || String(literal) !== value) {
5029
+ throw new Error(`Cron ${label} value invalid: "${value}" (${min}..${max})`);
5030
+ }
5031
+ result.add(literal);
5032
+ }
5033
+ if (result.size === 0) {
5034
+ throw new Error(`Cron ${label} field "${field}" produced no valid values`);
5035
+ }
5036
+ return result;
5037
+ }
5038
+ function nextCronFire(expression, timezone, from) {
5039
+ const parsed = parseCronExpression(expression);
5040
+ const start = Math.floor(from.getTime() / 6e4) * 6e4 + 6e4;
5041
+ const limit = start + 366 * 24 * 60 * 6e4;
5042
+ for (let timestamp = start; timestamp < limit; timestamp += 6e4) {
5043
+ const candidate = new Date(timestamp);
5044
+ if (cronMatches(parsed, timezone, candidate)) return candidate;
5045
+ }
5046
+ return null;
5047
+ }
5048
+ function cronMatches(parsed, timezone, date) {
5049
+ const formatter = new Intl.DateTimeFormat("en-US", {
5050
+ timeZone: timezone,
5051
+ year: "numeric",
5052
+ month: "numeric",
5053
+ day: "numeric",
5054
+ hour: "numeric",
5055
+ minute: "numeric",
5056
+ weekday: "short",
5057
+ hour12: false
5058
+ });
5059
+ const parts = Object.fromEntries(
5060
+ formatter.formatToParts(date).map((part) => [part.type, part.value])
5061
+ );
5062
+ const minute = Number.parseInt(parts.minute, 10);
5063
+ const hour = Number.parseInt(parts.hour, 10) % 24;
5064
+ const dayOfMonth = Number.parseInt(parts.day, 10);
5065
+ const month = Number.parseInt(parts.month, 10);
5066
+ const weekday = {
5067
+ Sun: 0,
5068
+ Mon: 1,
5069
+ Tue: 2,
5070
+ Wed: 3,
5071
+ Thu: 4,
5072
+ Fri: 5,
5073
+ Sat: 6
5074
+ };
5075
+ const dayOfWeek = weekday[parts.weekday] ?? 0;
5076
+ return parsed.minute.has(minute) && parsed.hour.has(hour) && parsed.dayOfMonth.has(dayOfMonth) && parsed.month.has(month) && parsed.dayOfWeek.has(dayOfWeek);
5077
+ }
5078
+
4889
5079
  // src/server/rooms/cron-room.ts
4890
5080
  var CRON_WRITE_TYPES = /* @__PURE__ */ new Set([
4891
5081
  MSG.CRON_TRIGGER,
@@ -4926,10 +5116,7 @@ var CronRoom = class extends BaseRoom {
4926
5116
  )
4927
5117
  `);
4928
5118
  for (const task of this.tasks) {
4929
- const existing = this.sql.exec(
4930
- `SELECT name FROM cron_tasks WHERE name = ?`,
4931
- task.name
4932
- ).toArray();
5119
+ const existing = this.sql.exec(`SELECT name FROM cron_tasks WHERE name = ?`, task.name).toArray();
4933
5120
  if (existing.length === 0) {
4934
5121
  this.sql.exec(
4935
5122
  `INSERT INTO cron_tasks (name, interval_minutes, schedule, timezone, paused)
@@ -5000,7 +5187,12 @@ var CronRoom = class extends BaseRoom {
5000
5187
  this.sendTo(ws, { type: MSG.ERROR, payload: { error: "Missing taskName" } });
5001
5188
  return;
5002
5189
  }
5003
- await this.executeTask(taskName);
5190
+ if (!await this.executeTask(taskName)) {
5191
+ this.sendTo(ws, {
5192
+ type: MSG.ERROR,
5193
+ payload: { error: `Unknown cron task: ${taskName}` }
5194
+ });
5195
+ }
5004
5196
  break;
5005
5197
  }
5006
5198
  case MSG.CRON_PAUSE: {
@@ -5032,7 +5224,10 @@ var CronRoom = class extends BaseRoom {
5032
5224
  break;
5033
5225
  }
5034
5226
  default:
5035
- this.sendTo(ws, { type: MSG.ERROR, payload: { error: `Unknown cron message type: ${type}` } });
5227
+ this.sendTo(ws, {
5228
+ type: MSG.ERROR,
5229
+ payload: { error: `Unknown cron message type: ${type}` }
5230
+ });
5036
5231
  }
5037
5232
  }
5038
5233
  async onAlarm() {
@@ -5052,6 +5247,8 @@ var CronRoom = class extends BaseRoom {
5052
5247
  // Task Execution
5053
5248
  // ==========================================================================
5054
5249
  async executeTask(taskName) {
5250
+ const taskRow = this.sql.exec(`SELECT interval_minutes, schedule, timezone FROM cron_tasks WHERE name = ?`, taskName).toArray()[0];
5251
+ if (!taskRow) return false;
5055
5252
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
5056
5253
  const start = Date.now();
5057
5254
  let success = true;
@@ -5076,21 +5273,18 @@ var CronRoom = class extends BaseRoom {
5076
5273
  error ?? null,
5077
5274
  completedAt
5078
5275
  );
5079
- const taskRow = this.sql.exec(
5080
- `SELECT interval_minutes, schedule, timezone FROM cron_tasks WHERE name = ?`,
5276
+ const nextRunAt = computeNextRunAt(taskRow, new Date(completedAt));
5277
+ this.sql.exec(
5278
+ `UPDATE cron_tasks SET last_run_at = ?, next_run_at = ? WHERE name = ?`,
5279
+ completedAt,
5280
+ nextRunAt,
5081
5281
  taskName
5082
- ).toArray()[0];
5083
- if (taskRow) {
5084
- const nextRunAt = computeNextRunAt(taskRow, new Date(completedAt));
5085
- this.sql.exec(
5086
- `UPDATE cron_tasks SET last_run_at = ?, next_run_at = ? WHERE name = ?`,
5087
- completedAt,
5088
- nextRunAt,
5089
- taskName
5090
- );
5091
- }
5092
- this.sql.exec(`DELETE FROM cron_history WHERE id NOT IN (SELECT id FROM cron_history ORDER BY id DESC LIMIT 500)`);
5282
+ );
5283
+ this.sql.exec(
5284
+ `DELETE FROM cron_history WHERE id NOT IN (SELECT id FROM cron_history ORDER BY id DESC LIMIT 500)`
5285
+ );
5093
5286
  this.broadcastStatus();
5287
+ return true;
5094
5288
  }
5095
5289
  // ==========================================================================
5096
5290
  // Scheduling
@@ -5139,10 +5333,7 @@ var CronRoom = class extends BaseRoom {
5139
5333
  });
5140
5334
  }
5141
5335
  getRecentHistory(limit) {
5142
- return this.sql.exec(
5143
- `SELECT * FROM cron_history ORDER BY id DESC LIMIT ?`,
5144
- limit
5145
- ).toArray().map((row) => {
5336
+ return this.sql.exec(`SELECT * FROM cron_history ORDER BY id DESC LIMIT ?`, limit).toArray().map((row) => {
5146
5337
  const r = row;
5147
5338
  return {
5148
5339
  taskName: r.task_name,
@@ -5164,100 +5355,6 @@ var CronRoom = class extends BaseRoom {
5164
5355
  });
5165
5356
  }
5166
5357
  };
5167
- var TASK_NAME_RE = /^[a-z0-9-]+$/;
5168
- function validateTask(task) {
5169
- if (!task.name || typeof task.name !== "string") {
5170
- throw new Error(`CronTask.name is required`);
5171
- }
5172
- if (task.name.length > 64 || !TASK_NAME_RE.test(task.name)) {
5173
- throw new Error(
5174
- `CronTask.name "${task.name}" must be lowercase alphanumeric with hyphens, 1-64 chars`
5175
- );
5176
- }
5177
- const hasInterval = task.intervalMinutes != null;
5178
- const hasSchedule = task.schedule != null;
5179
- const hasTimezone = task.timezone != null;
5180
- if (hasInterval && (hasSchedule || hasTimezone)) {
5181
- throw new Error(
5182
- `CronTask "${task.name}" cannot mix intervalMinutes with schedule/timezone`
5183
- );
5184
- }
5185
- if (!hasInterval && !(hasSchedule && hasTimezone)) {
5186
- throw new Error(
5187
- `CronTask "${task.name}" must declare either intervalMinutes or schedule+timezone`
5188
- );
5189
- }
5190
- if (hasInterval) {
5191
- if (!Number.isInteger(task.intervalMinutes) || task.intervalMinutes < 1 || task.intervalMinutes > 10080) {
5192
- throw new Error(
5193
- `CronTask "${task.name}" intervalMinutes must be an integer 1..10080`
5194
- );
5195
- }
5196
- }
5197
- if (hasSchedule) {
5198
- parseCronExpression(task.schedule);
5199
- if (typeof task.timezone !== "string" || task.timezone.length === 0) {
5200
- throw new Error(`CronTask "${task.name}" timezone must be a non-empty IANA string`);
5201
- }
5202
- try {
5203
- new Intl.DateTimeFormat("en-US", { timeZone: task.timezone });
5204
- } catch {
5205
- throw new Error(`CronTask "${task.name}" timezone "${task.timezone}" is not a valid IANA timezone`);
5206
- }
5207
- }
5208
- return task;
5209
- }
5210
- function parseCronExpression(expression) {
5211
- const fields = expression.trim().split(/\s+/);
5212
- if (fields.length !== 5) {
5213
- throw new Error(`Cron expression must have exactly 5 fields, got ${fields.length}: "${expression}"`);
5214
- }
5215
- const [cMin, cHour, cDom, cMonth, cDow] = fields;
5216
- return {
5217
- minute: parseField(cMin, 0, 59, "minute"),
5218
- hour: parseField(cHour, 0, 23, "hour"),
5219
- dayOfMonth: parseField(cDom, 1, 31, "day-of-month"),
5220
- month: parseField(cMonth, 1, 12, "month"),
5221
- dayOfWeek: parseField(cDow, 0, 6, "day-of-week")
5222
- };
5223
- }
5224
- function parseField(field, min, max, label) {
5225
- const result = /* @__PURE__ */ new Set();
5226
- for (const part of field.split(",")) {
5227
- const trimmed = part.trim();
5228
- if (trimmed === "*") {
5229
- for (let i = min; i <= max; i++) result.add(i);
5230
- continue;
5231
- }
5232
- const allStep = trimmed.match(/^\*\/(\d+)$/);
5233
- if (allStep) {
5234
- const step = parseInt(allStep[1], 10);
5235
- if (step <= 0) throw new Error(`Cron ${label} step must be positive: "${trimmed}"`);
5236
- for (let i = min; i <= max; i += step) result.add(i);
5237
- continue;
5238
- }
5239
- const rangeMatch = trimmed.match(/^(\d+)-(\d+)(?:\/(\d+))?$/);
5240
- if (rangeMatch) {
5241
- const start = parseInt(rangeMatch[1], 10);
5242
- const end = parseInt(rangeMatch[2], 10);
5243
- const step = rangeMatch[3] ? parseInt(rangeMatch[3], 10) : 1;
5244
- if (start < min || end > max || start > end || step <= 0) {
5245
- throw new Error(`Cron ${label} range out of bounds: "${trimmed}" (${min}..${max})`);
5246
- }
5247
- for (let i = start; i <= end; i += step) result.add(i);
5248
- continue;
5249
- }
5250
- const num = parseInt(trimmed, 10);
5251
- if (isNaN(num) || num < min || num > max || String(num) !== trimmed) {
5252
- throw new Error(`Cron ${label} value invalid: "${trimmed}" (${min}..${max})`);
5253
- }
5254
- result.add(num);
5255
- }
5256
- if (result.size === 0) {
5257
- throw new Error(`Cron ${label} field "${field}" produced no valid values`);
5258
- }
5259
- return result;
5260
- }
5261
5358
  function computeNextRunAt(task, from) {
5262
5359
  if (task.interval_minutes) {
5263
5360
  return new Date(from.getTime() + task.interval_minutes * 60 * 1e3).toISOString();
@@ -5268,40 +5365,6 @@ function computeNextRunAt(task, from) {
5268
5365
  }
5269
5366
  return null;
5270
5367
  }
5271
- function nextCronFire(expression, timezone, from) {
5272
- const parsed = parseCronExpression(expression);
5273
- const start = new Date(Math.floor(from.getTime() / 6e4) * 6e4 + 6e4);
5274
- const limit = new Date(start.getTime() + 366 * 24 * 60 * 6e4);
5275
- for (let t = start.getTime(); t < limit.getTime(); t += 6e4) {
5276
- const candidate = new Date(t);
5277
- if (cronMatches(parsed, timezone, candidate)) {
5278
- return candidate;
5279
- }
5280
- }
5281
- return null;
5282
- }
5283
- function cronMatches(parsed, timezone, date) {
5284
- const formatter = new Intl.DateTimeFormat("en-US", {
5285
- timeZone: timezone,
5286
- year: "numeric",
5287
- month: "numeric",
5288
- day: "numeric",
5289
- hour: "numeric",
5290
- minute: "numeric",
5291
- weekday: "short",
5292
- hour12: false
5293
- });
5294
- const parts = Object.fromEntries(
5295
- formatter.formatToParts(date).map((p) => [p.type, p.value])
5296
- );
5297
- const minute = parseInt(parts.minute, 10);
5298
- const hour = parseInt(parts.hour, 10) % 24;
5299
- const dayOfMonth = parseInt(parts.day, 10);
5300
- const month = parseInt(parts.month, 10);
5301
- const weekdayMap = { Sun: 0, Mon: 1, Tue: 2, Wed: 3, Thu: 4, Fri: 5, Sat: 6 };
5302
- const dayOfWeek = weekdayMap[parts.weekday] ?? 0;
5303
- return parsed.minute.has(minute) && parsed.hour.has(hour) && parsed.dayOfMonth.has(dayOfMonth) && parsed.month.has(month) && parsed.dayOfWeek.has(dayOfWeek);
5304
- }
5305
5368
 
5306
5369
  // src/server/rooms/job-room.ts
5307
5370
  var RUNNING_STALE_MS = 16 * 60 * 1e3;
@@ -5414,6 +5477,7 @@ var JobRoom = class extends BaseRoom {
5414
5477
  }
5415
5478
  async onAlarm() {
5416
5479
  this.ensureInitialized();
5480
+ this.recoverStuckRunning();
5417
5481
  await this.drainDueJobs();
5418
5482
  this.pruneExpired();
5419
5483
  this.scheduleNextAlarm();
@@ -5716,22 +5780,36 @@ var JobRoom = class extends BaseRoom {
5716
5780
  // Scheduling / recovery / pruning
5717
5781
  // ==========================================================================
5718
5782
  scheduleNextAlarm() {
5719
- const row = this.sql.exec(
5783
+ const queued = this.sql.exec(
5720
5784
  `SELECT next_run_at FROM jobs
5721
5785
  WHERE status = 'queued'
5722
5786
  ORDER BY (next_run_at IS NULL) ASC, next_run_at ASC
5723
5787
  LIMIT 1`
5724
5788
  ).toArray()[0];
5725
- if (!row) return;
5726
5789
  const now = Date.now();
5727
- const target = row.next_run_at ? Math.max(now, new Date(row.next_run_at).getTime()) : now;
5728
- this.state.storage.setAlarm(Math.max(target, now + 50));
5790
+ const targets = [];
5791
+ if (queued) {
5792
+ const queuedAt = queued.next_run_at ? new Date(queued.next_run_at).getTime() : now;
5793
+ targets.push(Number.isFinite(queuedAt) ? Math.max(now, queuedAt) : now);
5794
+ }
5795
+ const running = this.sql.exec(
5796
+ `SELECT started_at FROM jobs
5797
+ WHERE status = 'running'
5798
+ ORDER BY started_at ASC
5799
+ LIMIT 1`
5800
+ ).toArray()[0];
5801
+ if (running) {
5802
+ const startedAt = running.started_at ? new Date(running.started_at).getTime() : Number.NaN;
5803
+ targets.push(Number.isFinite(startedAt) ? Math.max(now, startedAt + RUNNING_STALE_MS) : now);
5804
+ }
5805
+ if (targets.length === 0) return;
5806
+ this.state.storage.setAlarm(Math.max(Math.min(...targets), now + 50));
5729
5807
  }
5730
5808
  recoverStuckRunning() {
5731
5809
  const cutoff = new Date(Date.now() - RUNNING_STALE_MS).toISOString();
5732
5810
  const stuck = this.sql.exec(
5733
5811
  `SELECT id, attempts, max_attempts FROM jobs
5734
- WHERE status = 'running' AND started_at < ?`,
5812
+ WHERE status = 'running' AND (started_at IS NULL OR started_at <= ?)`,
5735
5813
  cutoff
5736
5814
  ).toArray();
5737
5815
  for (const r of stuck) {
@@ -6985,6 +7063,16 @@ function createDeepSpaceAuth(config) {
6985
7063
  database: config.database,
6986
7064
  baseURL: config.baseURL,
6987
7065
  secret: config.secret,
7066
+ // Login is a one-time human act; everything after it is agents minting
7067
+ // short-lived JWTs from this session. 30 days idle (vs the 7-day
7068
+ // default) with daily sliding renewal means any agent active within a
7069
+ // month keeps the session alive indefinitely — the human re-logs-in
7070
+ // only after a full month of zero activity. Short-lived access stays
7071
+ // short: CLI JWTs expire in minutes (auth-worker /api/auth/token).
7072
+ session: {
7073
+ expiresIn: 60 * 60 * 24 * 30,
7074
+ updateAge: 60 * 60 * 24
7075
+ },
6988
7076
  emailAndPassword: {
6989
7077
  enabled: config.emailAndPassword ?? true
6990
7078
  },
@@ -7013,9 +7101,41 @@ function createDeepSpaceAuth(config) {
7013
7101
  }
7014
7102
  }
7015
7103
  } : {},
7104
+ // Observer hook for new-user creation (see DeepSpaceAuthConfig.onUserCreated).
7105
+ // Two-layer wiring guard — be honest about what each layer catches:
7106
+ // 1. buildDatabaseHooks's return type is pinned to
7107
+ // BetterAuthOptions['databaseHooks'], so a better-auth upgrade that
7108
+ // renames/reshapes the option fails tsc.
7109
+ // 2. A LOCAL typo of this key would still compile (betterAuth's generic
7110
+ // options parameter defeats excess-property checking even for plain
7111
+ // keys) — that case is caught by the runtime assertions in
7112
+ // __tests__/betterAuth-hooks.test.ts, which read the wired hook back
7113
+ // off auth.options. Don't rename the key without running them.
7114
+ databaseHooks: buildDatabaseHooks(config.onUserCreated),
7016
7115
  plugins: [organization(), twoFactor()]
7017
7116
  });
7018
7117
  }
7118
+ function buildDatabaseHooks(onUserCreated) {
7119
+ if (!onUserCreated) return void 0;
7120
+ return {
7121
+ user: {
7122
+ create: {
7123
+ after: async (user, ctx) => {
7124
+ try {
7125
+ await onUserCreated(
7126
+ { id: user.id, email: user.email, name: user.name },
7127
+ // Forward BOTH request and standalone headers — some better-auth
7128
+ // entry points populate ctx.headers without a request.
7129
+ ctx ? { request: ctx.request, headers: ctx.headers ?? void 0 } : null
7130
+ );
7131
+ } catch (err) {
7132
+ console.error("[deepspace] onUserCreated hook failed (signup unaffected)", err);
7133
+ }
7134
+ }
7135
+ }
7136
+ }
7137
+ };
7138
+ }
7019
7139
  var cachedAppleSecret = null;
7020
7140
  async function generateAppleClientSecret(apple) {
7021
7141
  const now = Math.floor(Date.now() / 1e3);
@@ -7172,6 +7292,7 @@ export {
7172
7292
  AI_CHATS_SCHEMA,
7173
7293
  AI_MESSAGES_SCHEMA,
7174
7294
  ALLOWED_BINDING_TYPES,
7295
+ APP_ID_RE,
7175
7296
  APP_NAME_RULES,
7176
7297
  AUTO_PROVISIONABLE_TYPES,
7177
7298
  AUTO_PROVISION_SENTINEL,
@@ -7189,6 +7310,7 @@ export {
7189
7310
  DEFAULT_CONTEXT_CONFIG,
7190
7311
  DEFAULT_DO_MANIFEST,
7191
7312
  DEFAULT_QUERY_LIMIT,
7313
+ DEPLOYED_APPS_DEFAULTS,
7192
7314
  DIRECTORY_SCHEMAS,
7193
7315
  GLOBAL_DO_TYPES,
7194
7316
  GLOBAL_DO_TYPE_NAMES,
@@ -7198,10 +7320,13 @@ export {
7198
7320
  PresenceRoom,
7199
7321
  REACTIONS_SCHEMA,
7200
7322
  READ_RECEIPTS_SCHEMA,
7323
+ REGISTRY_INSTANCE,
7201
7324
  RESERVED_BINDING_NAMES,
7202
7325
  RESERVED_COLLECTION_NAMES,
7203
7326
  RecordRoom,
7204
7327
  RefundError,
7328
+ RegistryClientError,
7329
+ STRICT_APP_ID_RE,
7205
7330
  SYSTEM_COLLECTION_SCHEMAS,
7206
7331
  SYSTEM_MANAGED_COLUMNS,
7207
7332
  SchemaRegistry,
@@ -7224,6 +7349,7 @@ export {
7224
7349
  buildCronContext,
7225
7350
  buildTableSelect,
7226
7351
  buildUiParts,
7352
+ callRegistry,
7227
7353
  canCreate,
7228
7354
  canDelete,
7229
7355
  canRead,
@@ -7278,10 +7404,13 @@ export {
7278
7404
  lintSchema,
7279
7405
  loadMessages,
7280
7406
  makeDefaultSummarizer,
7407
+ matchOrigin,
7281
7408
  meterAi,
7282
7409
  meterUsage,
7283
7410
  meterVectorize,
7284
7411
  noopPermissionContext,
7412
+ parseQuotaLimit,
7413
+ platformCorsPatterns,
7285
7414
  platformWorkerFetch,
7286
7415
  prepareMessagesWithCompaction,
7287
7416
  priceBindingUsageEvent,
@@ -7290,6 +7419,8 @@ export {
7290
7419
  recordMatchesSubscription,
7291
7420
  refundInvoice,
7292
7421
  registerUser,
7422
+ registryErrorJson,
7423
+ registryStub,
7293
7424
  requireSubscription,
7294
7425
  resolveAppName,
7295
7426
  resolveColumn,