deepspace 0.22.0 → 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/CHANGELOG.md +20 -0
- package/dist/build.d.ts +115 -0
- package/dist/build.js +66 -0
- package/dist/build.js.map +1 -0
- package/dist/{chunk-7TDBFEB7.js → chunk-A7VKZRCB.js} +2 -2
- package/dist/{chunk-7TDBFEB7.js.map → chunk-A7VKZRCB.js.map} +1 -1
- package/dist/cli.js +272 -52
- package/dist/cli.js.map +1 -1
- package/dist/documentation-client-core.js +1 -1
- package/dist/documentation-default-renderer.cjs +1 -1
- package/dist/documentation-react.js +1 -1
- package/dist/documentation-runtime.js +1 -1
- package/dist/documentation-server-core.js +1 -1
- package/dist/documentation.js +1 -1
- package/dist/documentation.js.map +1 -1
- package/dist/index.d.ts +22 -6
- package/dist/index.js +23 -29
- package/dist/index.js.map +1 -1
- package/dist/schema.js +6 -0
- package/dist/schema.js.map +1 -1
- package/dist/server.d.ts +51 -12
- package/dist/server.js +193 -150
- package/dist/server.js.map +1 -1
- package/dist/testing.d.ts +2 -0
- package/dist/testing.js +21 -9
- package/dist/testing.js.map +1 -1
- package/dist/worker.d.ts +54 -12
- package/dist/worker.js +119 -75
- package/dist/worker.js.map +1 -1
- package/package.json +5 -1
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
|
|
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
|
|
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
|
|
1617
|
+
return executeTool2("records.create", { collection, data });
|
|
1618
1618
|
},
|
|
1619
1619
|
async update(collection, recordId, data) {
|
|
1620
|
-
return
|
|
1620
|
+
return executeTool2("records.update", { collection, recordId, data });
|
|
1621
1621
|
},
|
|
1622
1622
|
async delete(collection, recordId) {
|
|
1623
|
-
return
|
|
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
|
|
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
|
|
2587
|
+
const result = await executeToolAsApp(stub, userId, "records.get", {
|
|
2514
2588
|
collection: "ai-chats",
|
|
2515
2589
|
recordId: chatId
|
|
2516
2590
|
});
|
|
2517
|
-
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
3246
|
-
|
|
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
|
|
3627
|
-
|
|
3628
|
-
|
|
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
|
-
|
|
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
|
|
3787
|
-
|
|
3788
|
-
|
|
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:
|
|
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
|
|
3813
|
-
|
|
3814
|
-
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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, {
|
|
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(
|
|
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
|
|
4126
|
-
|
|
4127
|
-
if (!schema
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
4695
|
-
if (!
|
|
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:
|
|
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,
|