deepspace 0.22.1 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.js CHANGED
@@ -1586,7 +1586,7 @@ function authWorkerFetch(env, path, init) {
1586
1586
  function buildCronContext(env, ownerUserId, roomId = "default") {
1587
1587
  const roomIdObj = env.RECORD_ROOMS.idFromName(roomId);
1588
1588
  const room = env.RECORD_ROOMS.get(roomIdObj);
1589
- async function executeTool3(tool2, params) {
1589
+ async function executeTool2(tool2, params) {
1590
1590
  const response = await room.fetch(
1591
1591
  new Request("https://internal/api/tools/execute", {
1592
1592
  method: "POST",
@@ -1609,18 +1609,18 @@ function buildCronContext(env, ownerUserId, roomId = "default") {
1609
1609
  const params = { collection };
1610
1610
  if (opts?.where) params.where = opts.where;
1611
1611
  if (opts?.limit) params.limit = opts.limit;
1612
- const data = await executeTool3("records.query", params);
1612
+ const data = await executeTool2("records.query", params);
1613
1613
  const records2 = data?.records;
1614
1614
  return records2 ?? [];
1615
1615
  },
1616
1616
  async create(collection, data) {
1617
- return executeTool3("records.create", { collection, data });
1617
+ return executeTool2("records.create", { collection, data });
1618
1618
  },
1619
1619
  async update(collection, recordId, data) {
1620
- return executeTool3("records.update", { collection, recordId, data });
1620
+ return executeTool2("records.update", { collection, recordId, data });
1621
1621
  },
1622
1622
  async delete(collection, recordId) {
1623
- return executeTool3("records.delete", { collection, recordId });
1623
+ return executeTool2("records.delete", { collection, recordId });
1624
1624
  }
1625
1625
  };
1626
1626
  const integrations = {
@@ -2468,8 +2468,82 @@ function makeDefaultSummarizer(env, options = {}) {
2468
2468
  };
2469
2469
  }
2470
2470
 
2471
+ // src/shared/protocol/constants.ts
2472
+ var MSG = {
2473
+ // ---- Records / CRUD --------------------------------------------------
2474
+ SUBSCRIBE: "core.subscribe",
2475
+ UNSUBSCRIBE: "core.unsubscribe",
2476
+ QUERY_RESULT: "core.query_result",
2477
+ RECORD_CHANGE: "core.record_change",
2478
+ PUT: "core.put",
2479
+ DELETE: "core.delete",
2480
+ ERROR: "core.error",
2481
+ // ---- Users -----------------------------------------------------------
2482
+ USER_INFO: "user.info",
2483
+ USER_LIST: "user.list",
2484
+ SET_ROLE: "user.set_role",
2485
+ USER_UPDATE: "user.update",
2486
+ // ---- Auth signal -----------------------------------------------------
2487
+ // Server → client one-shot on connect from rooms that enforce role-based
2488
+ // writes (Canvas, Game, Cron). Tells the client whether this connection
2489
+ // can write so its hook can disable local write APIs and the UI can
2490
+ // render controls accordingly. Without this, a viewer's click would
2491
+ // round-trip to the server and silently come back as ERROR.
2492
+ AUTH: "auth",
2493
+ // ---- Yjs JSON control frames (binary sync lives in ./yjs.ts) ---------
2494
+ YJS_JOIN: "yjs.join",
2495
+ YJS_LEAVE: "yjs.leave",
2496
+ // ---- Records room misc -----------------------------------------------
2497
+ ACK: "records.ack",
2498
+ LIST_SCHEMAS: "records.list_schemas",
2499
+ RESUBSCRIBE: "records.resubscribe",
2500
+ // ---- CanvasRoom ------------------------------------------------------
2501
+ CANVAS_SHAPES: "canvas.shapes",
2502
+ CANVAS_ADD: "canvas.add",
2503
+ CANVAS_MOVE: "canvas.move",
2504
+ CANVAS_RESIZE: "canvas.resize",
2505
+ CANVAS_DELETE: "canvas.delete",
2506
+ CANVAS_UPDATE: "canvas.update",
2507
+ CANVAS_VIEWPORT: "canvas.viewport",
2508
+ CANVAS_UNDO: "canvas.undo",
2509
+ CANVAS_REDO: "canvas.redo",
2510
+ // ---- CronRoom --------------------------------------------------------
2511
+ CRON_TASKS: "cron.tasks",
2512
+ CRON_HISTORY: "cron.history",
2513
+ CRON_TRIGGER: "cron.trigger",
2514
+ CRON_PAUSE: "cron.pause",
2515
+ CRON_RESUME: "cron.resume",
2516
+ CRON_STATUS: "cron.status",
2517
+ // Per-mutation receipt, sent only to the socket whose trigger/pause/
2518
+ // resume carried a `requestId` — frames without one stay fire-and-forget.
2519
+ CRON_ACK: "cron.ack",
2520
+ // ---- JobRoom ---------------------------------------------------------
2521
+ // Background-job queue with durable, alarm-driven execution. See
2522
+ // server/rooms/job-room.ts. Clients enqueue and subscribe to updates;
2523
+ // the room broadcasts JOB_UPDATE for every status transition.
2524
+ JOB_ENQUEUE: "job.enqueue",
2525
+ JOB_CANCEL: "job.cancel",
2526
+ JOB_RETRY: "job.retry",
2527
+ JOB_UPDATE: "job.update",
2528
+ // ---- PresenceRoom ----------------------------------------------------
2529
+ PRESENCE_SYNC: "presence.sync",
2530
+ PRESENCE_JOIN: "presence.join",
2531
+ PRESENCE_LEAVE: "presence.leave",
2532
+ PRESENCE_UPDATE: "presence.update",
2533
+ // ---- Gateway multiplexing --------------------------------------------
2534
+ GW_SCOPE_CONNECT: "gateway.scope_connect",
2535
+ GW_SCOPE_DISCONNECT: "gateway.scope_disconnect",
2536
+ GW_SCOPE_ERROR: "gateway.scope_error",
2537
+ GW_TOKEN_REFRESH: "gateway.token_refresh",
2538
+ GW_USER_UPDATE: "gateway.user_update"
2539
+ };
2540
+ var MSG_YJS_SYNC = 22;
2541
+ var MSG_YJS_AWARENESS = 23;
2542
+ var ROLE_ANONYMOUS = "viewer";
2543
+ var RECORD_NOT_FOUND = "Record not found";
2544
+
2471
2545
  // src/server/utils/chat-history.ts
2472
- async function executeTool(stub, userId, tool2, params) {
2546
+ async function executeToolAsApp(stub, userId, tool2, params) {
2473
2547
  const res = await stub.fetch(new Request("https://internal/api/tools/execute", {
2474
2548
  method: "POST",
2475
2549
  headers: {
@@ -2510,13 +2584,15 @@ function toMessageRow(env) {
2510
2584
  }
2511
2585
  async function getChat(stub, chatId, userId) {
2512
2586
  try {
2513
- const result = await executeTool(stub, userId, "records.get", {
2587
+ const result = await executeToolAsApp(stub, userId, "records.get", {
2514
2588
  collection: "ai-chats",
2515
2589
  recordId: chatId
2516
2590
  });
2517
- return result.record ? toChatRow(result.record) : null;
2591
+ if (!result.record) return null;
2592
+ const chat = toChatRow(result.record);
2593
+ return chat.userId === userId ? chat : null;
2518
2594
  } catch (err) {
2519
- if (err instanceof Error && err.message.includes("Record not found")) return null;
2595
+ if (err instanceof Error && err.message.includes(RECORD_NOT_FOUND)) return null;
2520
2596
  throw err;
2521
2597
  }
2522
2598
  }
@@ -2524,28 +2600,28 @@ async function createChat(stub, userId, opts = {}) {
2524
2600
  const data = { userId };
2525
2601
  if (opts.title !== void 0) data.title = opts.title;
2526
2602
  if (opts.model !== void 0) data.model = opts.model;
2527
- const result = await executeTool(stub, userId, "records.create", {
2603
+ const result = await executeToolAsApp(stub, userId, "records.create", {
2528
2604
  collection: "ai-chats",
2529
2605
  data
2530
2606
  });
2531
2607
  return toChatRow(result.record);
2532
2608
  }
2533
2609
  async function updateChat(stub, chatId, userId, patch) {
2534
- await executeTool(stub, userId, "records.update", {
2610
+ await executeToolAsApp(stub, userId, "records.update", {
2535
2611
  collection: "ai-chats",
2536
2612
  recordId: chatId,
2537
2613
  data: patch
2538
2614
  });
2539
2615
  }
2540
2616
  async function deleteChatCascade(stub, chatId, userId) {
2541
- const list = await executeTool(stub, userId, "records.query", {
2617
+ const list = await executeToolAsApp(stub, userId, "records.query", {
2542
2618
  collection: "ai-messages",
2543
2619
  where: { chatId }
2544
2620
  });
2545
2621
  const errors = [];
2546
2622
  for (const env of list.records) {
2547
2623
  try {
2548
- await executeTool(stub, userId, "records.delete", {
2624
+ await executeToolAsApp(stub, userId, "records.delete", {
2549
2625
  collection: "ai-messages",
2550
2626
  recordId: env.recordId
2551
2627
  });
@@ -2554,7 +2630,7 @@ async function deleteChatCascade(stub, chatId, userId) {
2554
2630
  }
2555
2631
  }
2556
2632
  try {
2557
- await executeTool(stub, userId, "records.delete", {
2633
+ await executeToolAsApp(stub, userId, "records.delete", {
2558
2634
  collection: "ai-chats",
2559
2635
  recordId: chatId
2560
2636
  });
@@ -2570,7 +2646,7 @@ async function deleteChatCascade(stub, chatId, userId) {
2570
2646
  }
2571
2647
  }
2572
2648
  async function loadMessages(stub, chatId, userId) {
2573
- const result = await executeTool(stub, userId, "records.query", {
2649
+ const result = await executeToolAsApp(stub, userId, "records.query", {
2574
2650
  collection: "ai-messages",
2575
2651
  where: { chatId, userId },
2576
2652
  orderBy: "createdAt",
@@ -2586,7 +2662,7 @@ async function appendMessage(stub, msg) {
2586
2662
  content: msg.content
2587
2663
  };
2588
2664
  if (msg.parts !== void 0) data.parts = msg.parts;
2589
- await executeTool(stub, msg.userId, "records.create", {
2665
+ await executeToolAsApp(stub, msg.userId, "records.create", {
2590
2666
  collection: "ai-messages",
2591
2667
  recordId: msg.id,
2592
2668
  data
@@ -2967,79 +3043,6 @@ var BaseRoom = class {
2967
3043
  }
2968
3044
  };
2969
3045
 
2970
- // src/shared/protocol/constants.ts
2971
- var MSG = {
2972
- // ---- Records / CRUD --------------------------------------------------
2973
- SUBSCRIBE: "core.subscribe",
2974
- UNSUBSCRIBE: "core.unsubscribe",
2975
- QUERY_RESULT: "core.query_result",
2976
- RECORD_CHANGE: "core.record_change",
2977
- PUT: "core.put",
2978
- DELETE: "core.delete",
2979
- ERROR: "core.error",
2980
- // ---- Users -----------------------------------------------------------
2981
- USER_INFO: "user.info",
2982
- USER_LIST: "user.list",
2983
- SET_ROLE: "user.set_role",
2984
- USER_UPDATE: "user.update",
2985
- // ---- Auth signal -----------------------------------------------------
2986
- // Server → client one-shot on connect from rooms that enforce role-based
2987
- // writes (Canvas, Game, Cron). Tells the client whether this connection
2988
- // can write so its hook can disable local write APIs and the UI can
2989
- // render controls accordingly. Without this, a viewer's click would
2990
- // round-trip to the server and silently come back as ERROR.
2991
- AUTH: "auth",
2992
- // ---- Yjs JSON control frames (binary sync lives in ./yjs.ts) ---------
2993
- YJS_JOIN: "yjs.join",
2994
- YJS_LEAVE: "yjs.leave",
2995
- // ---- Records room misc -----------------------------------------------
2996
- ACK: "records.ack",
2997
- LIST_SCHEMAS: "records.list_schemas",
2998
- RESUBSCRIBE: "records.resubscribe",
2999
- // ---- CanvasRoom ------------------------------------------------------
3000
- CANVAS_SHAPES: "canvas.shapes",
3001
- CANVAS_ADD: "canvas.add",
3002
- CANVAS_MOVE: "canvas.move",
3003
- CANVAS_RESIZE: "canvas.resize",
3004
- CANVAS_DELETE: "canvas.delete",
3005
- CANVAS_UPDATE: "canvas.update",
3006
- CANVAS_VIEWPORT: "canvas.viewport",
3007
- CANVAS_UNDO: "canvas.undo",
3008
- CANVAS_REDO: "canvas.redo",
3009
- // ---- CronRoom --------------------------------------------------------
3010
- CRON_TASKS: "cron.tasks",
3011
- CRON_HISTORY: "cron.history",
3012
- CRON_TRIGGER: "cron.trigger",
3013
- CRON_PAUSE: "cron.pause",
3014
- CRON_RESUME: "cron.resume",
3015
- CRON_STATUS: "cron.status",
3016
- // Per-mutation receipt, sent only to the socket whose trigger/pause/
3017
- // resume carried a `requestId` — frames without one stay fire-and-forget.
3018
- CRON_ACK: "cron.ack",
3019
- // ---- JobRoom ---------------------------------------------------------
3020
- // Background-job queue with durable, alarm-driven execution. See
3021
- // server/rooms/job-room.ts. Clients enqueue and subscribe to updates;
3022
- // the room broadcasts JOB_UPDATE for every status transition.
3023
- JOB_ENQUEUE: "job.enqueue",
3024
- JOB_CANCEL: "job.cancel",
3025
- JOB_RETRY: "job.retry",
3026
- JOB_UPDATE: "job.update",
3027
- // ---- PresenceRoom ----------------------------------------------------
3028
- PRESENCE_SYNC: "presence.sync",
3029
- PRESENCE_JOIN: "presence.join",
3030
- PRESENCE_LEAVE: "presence.leave",
3031
- PRESENCE_UPDATE: "presence.update",
3032
- // ---- Gateway multiplexing --------------------------------------------
3033
- GW_SCOPE_CONNECT: "gateway.scope_connect",
3034
- GW_SCOPE_DISCONNECT: "gateway.scope_disconnect",
3035
- GW_SCOPE_ERROR: "gateway.scope_error",
3036
- GW_TOKEN_REFRESH: "gateway.token_refresh",
3037
- GW_USER_UPDATE: "gateway.user_update"
3038
- };
3039
- var MSG_YJS_SYNC = 22;
3040
- var MSG_YJS_AWARENESS = 23;
3041
- var ROLE_ANONYMOUS = "viewer";
3042
-
3043
3046
  // src/server/schemas/registry.ts
3044
3047
  function collectionTableName(name) {
3045
3048
  return `c_${name.replace(/[^a-zA-Z0-9_]/g, "_")}`;
@@ -3241,13 +3244,11 @@ function checkUnclaimedOwnerTransition(schema, role, nextData, userId) {
3241
3244
  }
3242
3245
  return `Role '${role}' can set '${schema.ownerField}' only to their own user id or leave it unclaimed`;
3243
3246
  }
3247
+ var SYSTEM_ASSIGNED_COLUMNS = /* @__PURE__ */ new Set(["email", "name", "imageUrl", "role"]);
3248
+ var SYSTEM_MAINTAINED_COLUMNS = /* @__PURE__ */ new Set(["createdAt", "lastSeenAt"]);
3244
3249
  var SYSTEM_MANAGED_COLUMNS = /* @__PURE__ */ new Set([
3245
- "email",
3246
- "name",
3247
- "imageUrl",
3248
- "role",
3249
- "createdAt",
3250
- "lastSeenAt"
3250
+ ...SYSTEM_ASSIGNED_COLUMNS,
3251
+ ...SYSTEM_MAINTAINED_COLUMNS
3251
3252
  ]);
3252
3253
  var USERS_COLUMNS = [
3253
3254
  { name: "email", storage: "text", interpretation: "plain" },
@@ -3553,6 +3554,11 @@ var serverBuild = {
3553
3554
  function getResolvedColumns(schema) {
3554
3555
  return (schema.columns ?? []).map(resolveColumn);
3555
3556
  }
3557
+ function systemManagedColumnError(column, isUpdate) {
3558
+ const roleHint = column === "role" ? ", or the admin-gated useUsers().setRole() to change a role" : "";
3559
+ const remedy = isUpdate ? `Re-sending the current value of '${column}' unchanged is accepted and ignored.` : `A users row is created by registration, so no value for '${column}' is accepted here \u2014 omit it.`;
3560
+ return `FIELD ERROR: '${column}' is system-managed and cannot be set by a record write \u2014 nothing was written. Use tools.registerUser({ userId, isAdmin })${roleHint}. ${remedy}`;
3561
+ }
3556
3562
  function notifyTeamMembershipChange(ctx, collection, record) {
3557
3563
  if (collection !== "team_members") return;
3558
3564
  const affectedUserId = record.data.UserId;
@@ -3623,11 +3629,9 @@ function handleDelete(ctx, ws, attachment, payload) {
3623
3629
  }
3624
3630
  }
3625
3631
  function putRecord(ctx, collection, recordId, data, userId, userRole, skipUserRbac = false, systemUpdate = false) {
3626
- const isSystem = SYSTEM_COLLECTIONS.has(collection);
3627
- const schema = ctx.schemaRegistry.get(collection);
3628
- if (!schema && !isSystem) {
3629
- return { success: false, error: `Schema not registered for collection: ${collection}` };
3630
- }
3632
+ const resolved = resolveCollection(ctx, collection);
3633
+ if (!resolved.ok) return { success: false, error: resolved.error };
3634
+ const schema = resolved.schema;
3631
3635
  if (!schema) {
3632
3636
  return { success: false, error: `Schema not registered for collection: ${collection}` };
3633
3637
  }
@@ -3682,17 +3686,7 @@ function putRecord(ctx, collection, recordId, data, userId, userRole, skipUserRb
3682
3686
  }
3683
3687
  }
3684
3688
  }
3685
- let finalData = mergedData;
3686
- if (!systemUpdate && schema.name === "users") {
3687
- finalData = { ...mergedData };
3688
- for (const key of SYSTEM_MANAGED_COLUMNS) {
3689
- if (existing?.data[key] !== void 0) {
3690
- finalData[key] = existing.data[key];
3691
- } else {
3692
- delete finalData[key];
3693
- }
3694
- }
3695
- }
3689
+ const finalData = { ...mergedData };
3696
3690
  if (!skipUserRbac) {
3697
3691
  const permCtx = ctx.getPermissionContext();
3698
3692
  if (isUpdate) {
@@ -3724,6 +3718,17 @@ function putRecord(ctx, collection, recordId, data, userId, userRole, skipUserRb
3724
3718
  return { success: false, error: `FIELD ERROR: ${ownerError}` };
3725
3719
  }
3726
3720
  }
3721
+ if (!systemUpdate && schema.name === "users") {
3722
+ for (const col of columns) {
3723
+ if (!SYSTEM_MANAGED_COLUMNS.has(col.name)) continue;
3724
+ const current = existing?.data[col.name];
3725
+ if (SYSTEM_ASSIGNED_COLUMNS.has(col.name) && Object.hasOwn(data, col.name) && coerceValue(data[col.name], col.storage, col.interpretation) !== coerceValue(current, col.storage, col.interpretation)) {
3726
+ return { success: false, error: systemManagedColumnError(col.name, isUpdate) };
3727
+ }
3728
+ if (current !== void 0) finalData[col.name] = current;
3729
+ else delete finalData[col.name];
3730
+ }
3731
+ }
3727
3732
  const colValues = dataToColumnValues(finalData, columns);
3728
3733
  const now = (/* @__PURE__ */ new Date()).toISOString();
3729
3734
  const tbl = collectionTableName(collection);
@@ -3783,14 +3788,12 @@ function putRecord(ctx, collection, recordId, data, userId, userRole, skipUserRb
3783
3788
  return { success: true, data: { recordId, record } };
3784
3789
  }
3785
3790
  function deleteRecord(ctx, collection, recordId, userId, userRole, skipUserRbac = false) {
3786
- const isSystem = SYSTEM_COLLECTIONS.has(collection);
3787
- const schema = ctx.schemaRegistry.get(collection);
3788
- if (!schema && !isSystem) {
3789
- return { success: false, error: `Schema not registered for collection: ${collection}` };
3790
- }
3791
+ const resolved = resolveCollection(ctx, collection);
3792
+ if (!resolved.ok) return { success: false, error: resolved.error };
3793
+ const schema = resolved.schema;
3791
3794
  const existing = getRecord(ctx.sql, collection, recordId, schema);
3792
3795
  if (!existing) {
3793
- return { success: false, error: "Record not found" };
3796
+ return { success: false, error: RECORD_NOT_FOUND };
3794
3797
  }
3795
3798
  if (!skipUserRbac && schema && !canDelete(schema, userRole, { ...existing, recordId }, userId, ctx.getPermissionContext())) {
3796
3799
  return { success: false, error: `DELETE DENIED: role=${userRole}, collection=${collection}` };
@@ -3809,14 +3812,12 @@ function deleteRecord(ctx, collection, recordId, userId, userRole, skipUserRbac
3809
3812
  return { success: true, data: { deleted: true } };
3810
3813
  }
3811
3814
  function readRecord(ctx, collection, recordId, userId, userRole, skipUserRbac = false) {
3812
- const isSystem = SYSTEM_COLLECTIONS.has(collection);
3813
- const schema = ctx.schemaRegistry.get(collection);
3814
- if (!schema && !isSystem) {
3815
- return { success: false, error: `Schema not registered for collection: ${collection}` };
3816
- }
3815
+ const resolved = resolveCollection(ctx, collection);
3816
+ if (!resolved.ok) return { success: false, error: resolved.error };
3817
+ const schema = resolved.schema;
3817
3818
  const existing = getRecord(ctx.sql, collection, recordId, schema);
3818
3819
  if (!existing) {
3819
- return { success: false, error: "Record not found" };
3820
+ return { success: false, error: RECORD_NOT_FOUND };
3820
3821
  }
3821
3822
  if (!skipUserRbac && schema && !canRead(schema, userRole, { ...existing, recordId }, userId, ctx.getPermissionContext())) {
3822
3823
  return { success: false, error: `READ DENIED: role=${userRole}, collection=${collection}` };
@@ -3847,9 +3848,7 @@ function hasYjsSubscription(attachment, target) {
3847
3848
  (subscription) => subscription.collection === target.collection && subscription.recordId === target.recordId && subscription.fieldName === target.fieldName
3848
3849
  );
3849
3850
  }
3850
- var SYSTEM_COLLECTIONS = /* @__PURE__ */ new Set([
3851
- "canvas-settings"
3852
- ]);
3851
+ var SYSTEM_COLLECTIONS = /* @__PURE__ */ new Set(["canvas-settings"]);
3853
3852
  var SYSTEM_COLLECTION_SCHEMAS = [
3854
3853
  {
3855
3854
  name: "canvas-settings",
@@ -3866,10 +3865,7 @@ function getOrCreateYjsDoc(ctx, docKey) {
3866
3865
  let doc = ctx.yjsDocs.get(docKey);
3867
3866
  if (doc) return doc;
3868
3867
  doc = new Y2.Doc();
3869
- const row = ctx.sql.exec(
3870
- `SELECT state FROM yjs_docs WHERE doc_key = ?`,
3871
- docKey
3872
- ).toArray()[0];
3868
+ const row = ctx.sql.exec(`SELECT state FROM yjs_docs WHERE doc_key = ?`, docKey).toArray()[0];
3873
3869
  if (row?.state) {
3874
3870
  try {
3875
3871
  Y2.applyUpdate(doc, new Uint8Array(row.state));
@@ -3934,11 +3930,17 @@ function handleYjsJoin(ctx, ws, attachment, payload) {
3934
3930
  }
3935
3931
  } else {
3936
3932
  if (!schema) {
3937
- ctx.send(ws, { type: MSG.ERROR, payload: { error: `Schema not registered for collection: ${collection}` } });
3933
+ const resolution = resolveCollection(ctx, collection);
3934
+ ctx.send(ws, {
3935
+ type: MSG.ERROR,
3936
+ payload: {
3937
+ error: resolution.ok ? `Schema not registered for collection: ${collection}` : resolution.error
3938
+ }
3939
+ });
3938
3940
  return;
3939
3941
  }
3940
3942
  if (!record) {
3941
- ctx.send(ws, { type: MSG.ERROR, payload: { error: "Record not found" } });
3943
+ ctx.send(ws, { type: MSG.ERROR, payload: { error: RECORD_NOT_FOUND } });
3942
3944
  return;
3943
3945
  }
3944
3946
  const permCtx = ctx.getPermissionContext();
@@ -3971,7 +3973,10 @@ function handleYjsJoin(ctx, ws, attachment, payload) {
3971
3973
  writeVarUint8Array(encoder2, fullState);
3972
3974
  ctx.sendBinary(ws, toUint8Array(encoder2));
3973
3975
  }
3974
- ctx.send(ws, { type: MSG.YJS_JOIN, payload: { collection, recordId, fieldName, canWrite: hasWriteAccess } });
3976
+ ctx.send(ws, {
3977
+ type: MSG.YJS_JOIN,
3978
+ payload: { collection, recordId, fieldName, canWrite: hasWriteAccess }
3979
+ });
3975
3980
  }
3976
3981
  function handleYjsLeave(ctx, ws, attachment, payload) {
3977
3982
  const { collection, recordId, fieldName } = payload;
@@ -4040,7 +4045,14 @@ function handleYjsBinaryMessage(ctx, ws, attachment, data) {
4040
4045
  if (!syncSchema) return;
4041
4046
  const record = getRecord(ctx.sql, collection, recordId, syncSchema);
4042
4047
  if (!record) return;
4043
- if (!canUpdate(syncSchema, attachment.role, { ...record, recordId }, attachment.userId, ctx.getPermissionContext())) return;
4048
+ if (!canUpdate(
4049
+ syncSchema,
4050
+ attachment.role,
4051
+ { ...record, recordId },
4052
+ attachment.userId,
4053
+ ctx.getPermissionContext()
4054
+ ))
4055
+ return;
4044
4056
  }
4045
4057
  const update = readVarUint8Array(decoder);
4046
4058
  Y2.applyUpdate(doc, update, "client");
@@ -4114,6 +4126,19 @@ function handleYjsDisconnect(ctx, ws, attachment) {
4114
4126
  }
4115
4127
 
4116
4128
  // src/server/handlers/subscriptions.ts
4129
+ function resolveCollection(ctx, collection) {
4130
+ const isSystem = SYSTEM_COLLECTIONS.has(collection);
4131
+ const schema = ctx.schemaRegistry.get(collection);
4132
+ if (!schema && !isSystem) {
4133
+ const known = ctx.schemaRegistry.names().sort().join(", ") || "(none)";
4134
+ const room = ctx.scopeId ? `Room '${ctx.scopeId}'` : "This room";
4135
+ return {
4136
+ ok: false,
4137
+ error: `Schema not registered for collection: ${collection} \u2014 ${room} serves only: ${known}. Records are sharded by scope, so address the room that owns '${collection}', or add it to the schemas this room is constructed with.`
4138
+ };
4139
+ }
4140
+ return { ok: true, schema, isSystem };
4141
+ }
4117
4142
  function handleSubscribe(ctx, ws, attachment, payload) {
4118
4143
  const { subscriptionId, query } = payload;
4119
4144
  const records = executeQuery(ctx, query, attachment.userId, attachment.role);
@@ -4122,15 +4147,20 @@ function handleSubscribe(ctx, ws, attachment, payload) {
4122
4147
  function handleUnsubscribe(_ctx, _ws, _attachment, _payload) {
4123
4148
  }
4124
4149
  function executeQuery(ctx, query, userId, userRole, skipUserRbac = false) {
4125
- const isSystem = SYSTEM_COLLECTIONS.has(query.collection);
4126
- const schema = ctx.schemaRegistry.get(query.collection);
4127
- if (!schema && !isSystem) {
4128
- return [];
4129
- }
4130
- if (!schema) {
4150
+ const resolved = resolveCollection(ctx, query.collection);
4151
+ if (!resolved.ok) return [];
4152
+ if (!resolved.schema) {
4131
4153
  return executeSystemQuery(ctx, query);
4132
4154
  }
4133
- return executeTableQuery(ctx, query, schema, userId, userRole, isSystem, skipUserRbac);
4155
+ return executeTableQuery(
4156
+ ctx,
4157
+ query,
4158
+ resolved.schema,
4159
+ userId,
4160
+ userRole,
4161
+ resolved.isSystem,
4162
+ skipUserRbac
4163
+ );
4134
4164
  }
4135
4165
  function executeSystemQuery(ctx, query) {
4136
4166
  const tbl = collectionTableName(query.collection);
@@ -4523,7 +4553,10 @@ async function handleToolsRequest(ctx, request, path) {
4523
4553
  const toolName = path.replace("tools/describe/", "");
4524
4554
  const tool2 = BUILT_IN_TOOLS.find((t) => t.name === toolName);
4525
4555
  if (!tool2) {
4526
- return Response.json({ error: `Tool not found: ${toolName}` }, { status: 404, headers: CORS_HEADERS3 });
4556
+ return Response.json(
4557
+ { error: `Tool not found: ${toolName}` },
4558
+ { status: 404, headers: CORS_HEADERS3 }
4559
+ );
4527
4560
  }
4528
4561
  return Response.json({ tool: tool2 }, { headers: CORS_HEADERS3 });
4529
4562
  }
@@ -4568,20 +4601,23 @@ async function handleToolExecute(ctx, request) {
4568
4601
  const recordCtx = {
4569
4602
  sql: ctx.sql,
4570
4603
  schemaRegistry: ctx.schemaRegistry,
4604
+ scopeId: ctx.scopeId,
4571
4605
  state: ctx.state,
4572
4606
  getPermissionContext: ctx.getPermissionContext,
4573
4607
  send: ctx.send
4574
4608
  };
4575
- const result = await executeTool2(recordCtx, tool2, params, userId, userRole, isAppAction);
4609
+ const result = await executeTool(recordCtx, tool2, params, userId, userRole, isAppAction);
4576
4610
  const status = result.success ? 200 : result.error?.includes("not found") ? 404 : 400;
4577
4611
  return Response.json(result, { status, headers: CORS_HEADERS3 });
4578
4612
  }
4579
- async function executeTool2(ctx, toolName, params, userId, userRole, skipUserRbac) {
4613
+ async function executeTool(ctx, toolName, params, userId, userRole, skipUserRbac) {
4580
4614
  switch (toolName) {
4581
4615
  // ---- Records ----
4582
4616
  case "records.query": {
4583
4617
  const collection = params.collection;
4584
4618
  if (!collection) return { success: false, error: "Missing required param: collection" };
4619
+ const resolved = resolveCollection(ctx, collection);
4620
+ if (!resolved.ok) return { success: false, error: resolved.error };
4585
4621
  const query = {
4586
4622
  collection,
4587
4623
  where: params.where,
@@ -4691,10 +4727,13 @@ async function executeTool2(ctx, toolName, params, userId, userRole, skipUserRba
4691
4727
  }
4692
4728
  function checkYjsPermission(ctx, collection, recordId, userId, userRole, action) {
4693
4729
  if (SYSTEM_COLLECTIONS.has(collection)) return null;
4694
- const schema = ctx.schemaRegistry.get(collection);
4695
- if (!schema) return { success: false, error: `Schema not registered for collection: ${collection}` };
4730
+ const resolution = resolveCollection(ctx, collection);
4731
+ if (!resolution.ok) return { success: false, error: resolution.error };
4732
+ const schema = resolution.schema;
4733
+ if (!schema)
4734
+ return { success: false, error: `Schema not registered for collection: ${collection}` };
4696
4735
  const record = getRecord(ctx.sql, collection, recordId, schema);
4697
- if (!record) return { success: false, error: `Record not found: ${collection}/${recordId}` };
4736
+ if (!record) return { success: false, error: `${RECORD_NOT_FOUND}: ${collection}/${recordId}` };
4698
4737
  const recordWithId = { ...record, recordId };
4699
4738
  const permCtx = ctx.getPermissionContext();
4700
4739
  if (action === "read" && !canRead(schema, userRole, recordWithId, userId, permCtx)) {
@@ -4727,9 +4766,7 @@ function withYjsDoc(ctx, docKey, operation) {
4727
4766
  function executeYjsTool(ctx, toolName, params, userId, userRole) {
4728
4767
  switch (toolName) {
4729
4768
  case "yjs.list": {
4730
- const rows = ctx.sql.exec(
4731
- `SELECT doc_key, updated_at FROM yjs_docs ORDER BY updated_at DESC`
4732
- ).toArray();
4769
+ const rows = ctx.sql.exec(`SELECT doc_key, updated_at FROM yjs_docs ORDER BY updated_at DESC`).toArray();
4733
4770
  const docs = rows.map((row) => {
4734
4771
  const firstColon = row.doc_key.indexOf(":");
4735
4772
  const lastColon = row.doc_key.lastIndexOf(":");
@@ -5352,6 +5389,10 @@ var RecordRoom = class extends BaseRoom {
5352
5389
  sql: this.sql,
5353
5390
  state: this.state,
5354
5391
  schemaRegistry: this.schemaRegistry,
5392
+ // Every record room stub is addressed with `idFromName(scopeId)`, so the
5393
+ // DO's own name is the scope. Handlers use it to name the room when a
5394
+ // caller asks it for a collection that lives in a different scope.
5395
+ scopeId: this.state.id.name,
5355
5396
  getPermissionContext: () => this.getPermissionContext(),
5356
5397
  send: (ws, msg) => this.send(ws, msg),
5357
5398
  sendBinary: (ws, data) => this.sendBinaryHelper(ws, data),
@@ -8601,7 +8642,9 @@ export {
8601
8642
  RefundError,
8602
8643
  RegistryClientError,
8603
8644
  STRICT_APP_ID_RE,
8645
+ SYSTEM_ASSIGNED_COLUMNS,
8604
8646
  SYSTEM_COLLECTION_SCHEMAS,
8647
+ SYSTEM_MAINTAINED_COLUMNS,
8605
8648
  SYSTEM_MANAGED_COLUMNS,
8606
8649
  SchemaRegistry,
8607
8650
  SubscriptionAuthError,