deepspace 0.23.0 → 0.23.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.
- package/CHANGELOG.md +15 -0
- package/dist/build.d.ts +2 -1
- package/dist/build.js +89 -16
- package/dist/build.js.map +1 -1
- package/dist/{chunk-A7VKZRCB.js → chunk-UUCAFDYW.js} +2 -2
- package/dist/{chunk-A7VKZRCB.js.map → chunk-UUCAFDYW.js.map} +1 -1
- package/dist/cli.js +735 -559
- 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 +20 -2
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/schema.d.ts +10 -0
- package/dist/schema.js.map +1 -1
- package/dist/server.d.ts +84 -14
- package/dist/server.js +261 -88
- package/dist/server.js.map +1 -1
- package/dist/worker.d.ts +70 -3
- package/dist/worker.js +444 -272
- package/dist/worker.js.map +1 -1
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -96,6 +96,8 @@ function registryErrorJson(c, err) {
|
|
|
96
96
|
|
|
97
97
|
// src/server/utils/tools.ts
|
|
98
98
|
var DEFAULT_QUERY_LIMIT = 50;
|
|
99
|
+
var DEFAULT_DELETE_WHERE_LIMIT = 100;
|
|
100
|
+
var MAX_DELETE_WHERE_LIMIT = 500;
|
|
99
101
|
function applyAiToolDefaults(toolName, params) {
|
|
100
102
|
if (toolName === "records.query" && params.limit === void 0) {
|
|
101
103
|
return { ...params, limit: DEFAULT_QUERY_LIMIT };
|
|
@@ -149,6 +151,15 @@ var BUILT_IN_TOOLS = [
|
|
|
149
151
|
recordId: { type: "string", description: "Record ID to delete", required: true }
|
|
150
152
|
}
|
|
151
153
|
},
|
|
154
|
+
{
|
|
155
|
+
name: "records.deleteWhere",
|
|
156
|
+
description: `Delete every record matching a filter, in one bounded batch. Deletes at most \`limit\` records (default ${DEFAULT_DELETE_WHERE_LIMIT}, max ${MAX_DELETE_WHERE_LIMIT}) and returns \`{ deleted }\`; repeat the same call until \`deleted\` is less than the limit to drain a larger set. A non-empty \`where\` whose every key names a real field (\`recordId\`, \`createdBy\`, or a schema column) is required \u2014 an unknown key is refused rather than ignored, so this tool never deletes a whole collection. Every matched record goes through the same delete permission check as \`records.delete\`; if any of them is denied, nothing in the batch is deleted.`,
|
|
157
|
+
params: {
|
|
158
|
+
collection: { type: "string", description: "Collection name (must have a schema)", required: true },
|
|
159
|
+
where: { type: "object", description: "Filter object with field=value equality conditions; every key must be recordId, createdBy, or a schema column", required: true },
|
|
160
|
+
limit: { type: "number", description: `Maximum number of records to delete in this call (default: ${DEFAULT_DELETE_WHERE_LIMIT}, max: ${MAX_DELETE_WHERE_LIMIT})`, required: false }
|
|
161
|
+
}
|
|
162
|
+
},
|
|
152
163
|
// Schema tools
|
|
153
164
|
{
|
|
154
165
|
name: "schema.list",
|
|
@@ -2540,6 +2551,10 @@ var MSG = {
|
|
|
2540
2551
|
var MSG_YJS_SYNC = 22;
|
|
2541
2552
|
var MSG_YJS_AWARENESS = 23;
|
|
2542
2553
|
var ROLE_ANONYMOUS = "viewer";
|
|
2554
|
+
var ANONYMOUS_USER_ID_PREFIX = "anon-";
|
|
2555
|
+
function isAnonymousUserId(userId) {
|
|
2556
|
+
return userId.startsWith(ANONYMOUS_USER_ID_PREFIX);
|
|
2557
|
+
}
|
|
2543
2558
|
var RECORD_NOT_FOUND = "Record not found";
|
|
2544
2559
|
|
|
2545
2560
|
// src/server/utils/chat-history.ts
|
|
@@ -2607,43 +2622,35 @@ async function createChat(stub, userId, opts = {}) {
|
|
|
2607
2622
|
return toChatRow(result.record);
|
|
2608
2623
|
}
|
|
2609
2624
|
async function updateChat(stub, chatId, userId, patch) {
|
|
2625
|
+
if (!await getChat(stub, chatId, userId)) return false;
|
|
2610
2626
|
await executeToolAsApp(stub, userId, "records.update", {
|
|
2611
2627
|
collection: "ai-chats",
|
|
2612
2628
|
recordId: chatId,
|
|
2613
2629
|
data: patch
|
|
2614
2630
|
});
|
|
2631
|
+
return true;
|
|
2615
2632
|
}
|
|
2633
|
+
var MESSAGE_DELETE_PAGE = 200;
|
|
2616
2634
|
async function deleteChatCascade(stub, chatId, userId) {
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
for (const env of list.records) {
|
|
2623
|
-
try {
|
|
2624
|
-
await executeToolAsApp(stub, userId, "records.delete", {
|
|
2635
|
+
let messagesError = null;
|
|
2636
|
+
try {
|
|
2637
|
+
let deleted = MESSAGE_DELETE_PAGE;
|
|
2638
|
+
while (deleted === MESSAGE_DELETE_PAGE) {
|
|
2639
|
+
const page = await executeToolAsApp(stub, userId, "records.deleteWhere", {
|
|
2625
2640
|
collection: "ai-messages",
|
|
2626
|
-
|
|
2641
|
+
where: { chatId },
|
|
2642
|
+
limit: MESSAGE_DELETE_PAGE
|
|
2627
2643
|
});
|
|
2628
|
-
|
|
2629
|
-
errors.push(err);
|
|
2644
|
+
deleted = page.deleted;
|
|
2630
2645
|
}
|
|
2631
|
-
}
|
|
2632
|
-
try {
|
|
2633
|
-
await executeToolAsApp(stub, userId, "records.delete", {
|
|
2634
|
-
collection: "ai-chats",
|
|
2635
|
-
recordId: chatId
|
|
2636
|
-
});
|
|
2637
2646
|
} catch (err) {
|
|
2638
|
-
|
|
2639
|
-
}
|
|
2640
|
-
if (errors.length > 0) {
|
|
2641
|
-
const first = errors[0];
|
|
2642
|
-
throw new Error(
|
|
2643
|
-
`deleteChatCascade: ${errors.length} delete(s) failed; first: ${first instanceof Error ? first.message : String(first)}`,
|
|
2644
|
-
{ cause: errors }
|
|
2645
|
-
);
|
|
2647
|
+
messagesError = err;
|
|
2646
2648
|
}
|
|
2649
|
+
await executeToolAsApp(stub, userId, "records.delete", {
|
|
2650
|
+
collection: "ai-chats",
|
|
2651
|
+
recordId: chatId
|
|
2652
|
+
});
|
|
2653
|
+
if (messagesError) throw messagesError;
|
|
2647
2654
|
}
|
|
2648
2655
|
async function loadMessages(stub, chatId, userId) {
|
|
2649
2656
|
const result = await executeToolAsApp(stub, userId, "records.query", {
|
|
@@ -2655,6 +2662,7 @@ async function loadMessages(stub, chatId, userId) {
|
|
|
2655
2662
|
return result.records.map(toMessageRow);
|
|
2656
2663
|
}
|
|
2657
2664
|
async function appendMessage(stub, msg) {
|
|
2665
|
+
if (!await getChat(stub, msg.chatId, msg.userId)) return false;
|
|
2658
2666
|
const data = {
|
|
2659
2667
|
chatId: msg.chatId,
|
|
2660
2668
|
userId: msg.userId,
|
|
@@ -2667,6 +2675,7 @@ async function appendMessage(stub, msg) {
|
|
|
2667
2675
|
recordId: msg.id,
|
|
2668
2676
|
data
|
|
2669
2677
|
});
|
|
2678
|
+
return true;
|
|
2670
2679
|
}
|
|
2671
2680
|
|
|
2672
2681
|
// src/server/utils/metering.ts
|
|
@@ -2789,7 +2798,7 @@ function decodeRoomIdentityHeader(value) {
|
|
|
2789
2798
|
// src/server/rooms/base-room.ts
|
|
2790
2799
|
function connectionAttachmentFromRequest(request) {
|
|
2791
2800
|
return {
|
|
2792
|
-
userId: decodeRoomIdentityHeader(request.headers.get("x-user-id")) ||
|
|
2801
|
+
userId: decodeRoomIdentityHeader(request.headers.get("x-user-id")) || `${ANONYMOUS_USER_ID_PREFIX}${crypto.randomUUID()}`,
|
|
2793
2802
|
userName: decodeRoomIdentityHeader(request.headers.get("x-user-name")) || "Anonymous",
|
|
2794
2803
|
userEmail: decodeRoomIdentityHeader(request.headers.get("x-user-email")) || "",
|
|
2795
2804
|
userImageUrl: decodeRoomIdentityHeader(request.headers.get("x-user-image-url")),
|
|
@@ -3302,12 +3311,28 @@ function lintSchema(schema) {
|
|
|
3302
3311
|
}
|
|
3303
3312
|
return warnings;
|
|
3304
3313
|
}
|
|
3314
|
+
function lintSchemas(schemas) {
|
|
3315
|
+
return [...schemas.flatMap((schema) => lintSchema(schema)), ...lintSchemaSet(schemas)];
|
|
3316
|
+
}
|
|
3317
|
+
function lintSchemaSet(schemas) {
|
|
3318
|
+
const warnings = [];
|
|
3319
|
+
if (!schemas.some((schema) => schema.name === "team_members")) {
|
|
3320
|
+
for (const schema of schemas) {
|
|
3321
|
+
const teamRoles = Object.entries(schema.permissions).filter(([, p]) => [p.read, p.update, p.delete].includes("team")).map(([role]) => role);
|
|
3322
|
+
if (teamRoles.length > 0) {
|
|
3323
|
+
warnings.push(
|
|
3324
|
+
`[${schema.name}] role(s) ${teamRoles.join(", ")} use the 'team' permission level, but no 'team_members' collection is registered. 'team' resolves membership from team_members (teamId, userId, status), so without it those roles are denied everything \u2014 silently. Register a team_members schema or use another level.`
|
|
3325
|
+
);
|
|
3326
|
+
}
|
|
3327
|
+
}
|
|
3328
|
+
}
|
|
3329
|
+
return warnings;
|
|
3330
|
+
}
|
|
3305
3331
|
var SchemaRegistry = class {
|
|
3306
3332
|
trusted = /* @__PURE__ */ new Map();
|
|
3307
3333
|
constructor(schemas = []) {
|
|
3308
|
-
for (const
|
|
3309
|
-
|
|
3310
|
-
}
|
|
3334
|
+
for (const w of lintSchemaSet(schemas)) console.warn(`[schema-lint] ${w}`);
|
|
3335
|
+
for (const schema of schemas) this.registerTrusted(schema);
|
|
3311
3336
|
}
|
|
3312
3337
|
registerTrusted(schema) {
|
|
3313
3338
|
for (const w of lintSchema(schema)) {
|
|
@@ -3811,6 +3836,81 @@ function deleteRecord(ctx, collection, recordId, userId, userRole, skipUserRbac
|
|
|
3811
3836
|
notifyTeamMembershipChange(ctx, collection, record);
|
|
3812
3837
|
return { success: true, data: { deleted: true } };
|
|
3813
3838
|
}
|
|
3839
|
+
function deleteWhere(ctx, collection, where, limit, userId, userRole, skipUserRbac = false) {
|
|
3840
|
+
const resolved = resolveCollection(ctx, collection);
|
|
3841
|
+
if (!resolved.ok) return { success: false, error: resolved.error };
|
|
3842
|
+
const schema = resolved.schema;
|
|
3843
|
+
if (!schema) {
|
|
3844
|
+
return {
|
|
3845
|
+
success: false,
|
|
3846
|
+
error: `records.deleteWhere: collection "${collection}" has no schema, so it has no filterable fields \u2014 delete its rows by recordId with records.delete`
|
|
3847
|
+
};
|
|
3848
|
+
}
|
|
3849
|
+
if (!where || typeof where !== "object" || Array.isArray(where) || Object.keys(where).length === 0) {
|
|
3850
|
+
return { success: false, error: "Missing required param: where (must match at least one field)" };
|
|
3851
|
+
}
|
|
3852
|
+
const columns = (schema.columns ?? []).map(resolveColumn);
|
|
3853
|
+
const filterable = ["recordId", "createdBy", ...columns.map((c) => c.name)];
|
|
3854
|
+
const unknown = Object.keys(where).filter(
|
|
3855
|
+
(key) => key !== "recordId" && key !== "createdBy" && !columns.some((c) => c.id === columnId(key) || c.name === key)
|
|
3856
|
+
);
|
|
3857
|
+
if (unknown.length > 0) {
|
|
3858
|
+
return {
|
|
3859
|
+
success: false,
|
|
3860
|
+
error: `Unknown field(s) in where for "${collection}": ${unknown.join(", ")} \u2014 filterable fields: ${filterable.join(", ")}`
|
|
3861
|
+
};
|
|
3862
|
+
}
|
|
3863
|
+
const nonPrimitive = Object.entries(where).filter(([, v]) => v !== null && typeof v === "object");
|
|
3864
|
+
if (nonPrimitive.length > 0) {
|
|
3865
|
+
return {
|
|
3866
|
+
success: false,
|
|
3867
|
+
error: `where values must be primitives (equality only): ${nonPrimitive.map(([k]) => k).join(", ")}`
|
|
3868
|
+
};
|
|
3869
|
+
}
|
|
3870
|
+
if (limit !== void 0 && !(typeof limit === "number" && Number.isFinite(limit))) {
|
|
3871
|
+
return { success: false, error: "limit must be a finite number" };
|
|
3872
|
+
}
|
|
3873
|
+
const page = Math.min(
|
|
3874
|
+
Math.max(1, Math.floor(limit ?? DEFAULT_DELETE_WHERE_LIMIT)),
|
|
3875
|
+
MAX_DELETE_WHERE_LIMIT
|
|
3876
|
+
);
|
|
3877
|
+
const scan = { capped: false };
|
|
3878
|
+
const matched = executeQuery(
|
|
3879
|
+
ctx,
|
|
3880
|
+
{ collection, where, limit: page },
|
|
3881
|
+
userId,
|
|
3882
|
+
userRole,
|
|
3883
|
+
skipUserRbac,
|
|
3884
|
+
scan
|
|
3885
|
+
);
|
|
3886
|
+
if (scan.capped && matched.length < page) {
|
|
3887
|
+
return {
|
|
3888
|
+
success: false,
|
|
3889
|
+
error: `records.deleteWhere scanned ${MAX_FILTERED_SCAN_ROWS} rows without filling a page of readable matches in "${collection}" \u2014 narrow \`where\` (or delete by recordId) and retry`
|
|
3890
|
+
};
|
|
3891
|
+
}
|
|
3892
|
+
if (!skipUserRbac && schema) {
|
|
3893
|
+
const permCtx = ctx.getPermissionContext();
|
|
3894
|
+
for (const record of matched) {
|
|
3895
|
+
if (!canDelete(schema, userRole, record, userId, permCtx)) {
|
|
3896
|
+
return { success: false, error: `DELETE DENIED: role=${userRole}, collection=${collection}` };
|
|
3897
|
+
}
|
|
3898
|
+
}
|
|
3899
|
+
}
|
|
3900
|
+
let deleted = 0;
|
|
3901
|
+
for (const record of matched) {
|
|
3902
|
+
if (deleteRecord(ctx, collection, record.recordId, userId, userRole, skipUserRbac).success) {
|
|
3903
|
+
deleted++;
|
|
3904
|
+
}
|
|
3905
|
+
}
|
|
3906
|
+
if (deleted < matched.length) {
|
|
3907
|
+
return {
|
|
3908
|
+
success: false,
|
|
3909
|
+
error: `Deleted ${deleted} of ${matched.length} matched record(s) in "${collection}"; the rest failed \u2014 retry`
|
|
3910
|
+
};
|
|
3911
|
+
}
|
|
3912
|
+
return { success: true, data: { deleted } };
|
|
3913
|
+
}
|
|
3814
3914
|
function readRecord(ctx, collection, recordId, userId, userRole, skipUserRbac = false) {
|
|
3815
3915
|
const resolved = resolveCollection(ctx, collection);
|
|
3816
3916
|
if (!resolved.ok) return { success: false, error: resolved.error };
|
|
@@ -4146,7 +4246,8 @@ function handleSubscribe(ctx, ws, attachment, payload) {
|
|
|
4146
4246
|
}
|
|
4147
4247
|
function handleUnsubscribe(_ctx, _ws, _attachment, _payload) {
|
|
4148
4248
|
}
|
|
4149
|
-
|
|
4249
|
+
var MAX_FILTERED_SCAN_ROWS = 5e3;
|
|
4250
|
+
function executeQuery(ctx, query, userId, userRole, skipUserRbac = false, scanState) {
|
|
4150
4251
|
const resolved = resolveCollection(ctx, query.collection);
|
|
4151
4252
|
if (!resolved.ok) return [];
|
|
4152
4253
|
if (!resolved.schema) {
|
|
@@ -4159,7 +4260,8 @@ function executeQuery(ctx, query, userId, userRole, skipUserRbac = false) {
|
|
|
4159
4260
|
userId,
|
|
4160
4261
|
userRole,
|
|
4161
4262
|
resolved.isSystem,
|
|
4162
|
-
skipUserRbac
|
|
4263
|
+
skipUserRbac,
|
|
4264
|
+
scanState
|
|
4163
4265
|
);
|
|
4164
4266
|
}
|
|
4165
4267
|
function executeSystemQuery(ctx, query) {
|
|
@@ -4196,7 +4298,7 @@ function preloadUserTeamIds(ctx, userId) {
|
|
|
4196
4298
|
);
|
|
4197
4299
|
return cursor.toArray().map((r) => r.col_teamid);
|
|
4198
4300
|
}
|
|
4199
|
-
function executeTableQuery(ctx, query, schema, userId, userRole, isSystem, skipUserRbac = false) {
|
|
4301
|
+
function executeTableQuery(ctx, query, schema, userId, userRole, isSystem, skipUserRbac = false, scanState) {
|
|
4200
4302
|
const columns = (schema.columns ?? []).map(resolveColumn);
|
|
4201
4303
|
const perms = getRolePermissions(schema, userRole);
|
|
4202
4304
|
let sql = buildTableSelect(query.collection, columns);
|
|
@@ -4204,16 +4306,27 @@ function executeTableQuery(ctx, query, schema, userId, userRole, isSystem, skipU
|
|
|
4204
4306
|
const whereClauses = [];
|
|
4205
4307
|
let skipPerRecordCheck = skipUserRbac;
|
|
4206
4308
|
if (!skipUserRbac && schema.teamField && perms.read === "team") {
|
|
4207
|
-
const filterCol = schema.teamField === "_rowId" ? "_row_id" : columns.find((c) => c.name === schema.teamField)?.id
|
|
4208
|
-
const teamIds = preloadUserTeamIds(ctx, userId);
|
|
4209
|
-
if (
|
|
4210
|
-
|
|
4211
|
-
|
|
4212
|
-
|
|
4213
|
-
|
|
4214
|
-
|
|
4309
|
+
const filterCol = schema.teamField === "_rowId" ? "_row_id" : columns.find((c) => c.name === schema.teamField)?.id;
|
|
4310
|
+
const teamIds = filterCol ? preloadUserTeamIds(ctx, userId) : null;
|
|
4311
|
+
if (filterCol) {
|
|
4312
|
+
if (teamIds && teamIds.length > 0) {
|
|
4313
|
+
const placeholders = teamIds.map(() => "?").join(", ");
|
|
4314
|
+
whereClauses.push(`("${filterCol}" IN (${placeholders}) OR _created_by = ?)`);
|
|
4315
|
+
params.push(...teamIds, userId);
|
|
4316
|
+
} else {
|
|
4317
|
+
whereClauses.push(`_created_by = ?`);
|
|
4318
|
+
params.push(userId);
|
|
4319
|
+
}
|
|
4320
|
+
skipPerRecordCheck = true;
|
|
4321
|
+
}
|
|
4322
|
+
} else if (!skipUserRbac && perms.read === "own") {
|
|
4323
|
+
const ownerCol = schema.ownerField ? columns.find((c) => c.name === schema.ownerField)?.id : "_created_by";
|
|
4324
|
+
if (ownerCol) {
|
|
4325
|
+
whereClauses.push(`"${ownerCol}" = ?`);
|
|
4215
4326
|
params.push(userId);
|
|
4327
|
+
skipPerRecordCheck = true;
|
|
4216
4328
|
}
|
|
4329
|
+
} else if (!skipUserRbac && perms.read === true) {
|
|
4217
4330
|
skipPerRecordCheck = true;
|
|
4218
4331
|
}
|
|
4219
4332
|
if (query.where) {
|
|
@@ -4251,27 +4364,44 @@ function executeTableQuery(ctx, query, schema, userId, userRole, isSystem, skipU
|
|
|
4251
4364
|
} else {
|
|
4252
4365
|
sql += ` ORDER BY _created_at DESC`;
|
|
4253
4366
|
}
|
|
4254
|
-
|
|
4255
|
-
sql
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
|
|
4367
|
+
const readPage = (limitClause, limitParams) => {
|
|
4368
|
+
const cursor = ctx.sql.exec(sql + limitClause, ...params, ...limitParams);
|
|
4369
|
+
const rows = cursor.toArray();
|
|
4370
|
+
const records = [];
|
|
4371
|
+
for (const row of rows) {
|
|
4372
|
+
const r = row;
|
|
4373
|
+
const record = {
|
|
4374
|
+
recordId: r._row_id,
|
|
4375
|
+
data: rowToData(r, columns),
|
|
4376
|
+
createdBy: r._created_by,
|
|
4377
|
+
createdAt: r._created_at,
|
|
4378
|
+
updatedAt: r._updated_at
|
|
4379
|
+
};
|
|
4380
|
+
if (isSystem || skipPerRecordCheck || canRead(schema, userRole, { data: record.data, createdBy: record.createdBy, recordId: record.recordId }, userId, ctx.getPermissionContext())) {
|
|
4381
|
+
records.push(record);
|
|
4382
|
+
}
|
|
4383
|
+
}
|
|
4384
|
+
return { records, scanned: rows.length };
|
|
4385
|
+
};
|
|
4386
|
+
if (!query.limit) return readPage("", []).records;
|
|
4387
|
+
const rowFiltered = !isSystem && !skipPerRecordCheck;
|
|
4388
|
+
if (!rowFiltered) return readPage(" LIMIT ?", [query.limit]).records;
|
|
4259
4389
|
const results = [];
|
|
4260
|
-
|
|
4261
|
-
|
|
4262
|
-
const
|
|
4263
|
-
const record
|
|
4264
|
-
recordId: r._row_id,
|
|
4265
|
-
data,
|
|
4266
|
-
createdBy: r._created_by,
|
|
4267
|
-
createdAt: r._created_at,
|
|
4268
|
-
updatedAt: r._updated_at
|
|
4269
|
-
};
|
|
4270
|
-
if (isSystem || skipPerRecordCheck || canRead(schema, userRole, { data: record.data, createdBy: record.createdBy, recordId: record.recordId }, userId, ctx.getPermissionContext())) {
|
|
4390
|
+
const batch = Math.max(query.limit, 200);
|
|
4391
|
+
for (let offset = 0; ; offset += batch) {
|
|
4392
|
+
const { records, scanned } = readPage(" LIMIT ? OFFSET ?", [batch, offset]);
|
|
4393
|
+
for (const record of records) {
|
|
4271
4394
|
results.push(record);
|
|
4395
|
+
if (results.length >= query.limit) return results;
|
|
4396
|
+
}
|
|
4397
|
+
if (scanned < batch) return results;
|
|
4398
|
+
if (offset + batch >= MAX_FILTERED_SCAN_ROWS) {
|
|
4399
|
+
if (scanState && readPage(" LIMIT ? OFFSET ?", [1, offset + batch]).scanned > 0) {
|
|
4400
|
+
scanState.capped = true;
|
|
4401
|
+
}
|
|
4402
|
+
return results;
|
|
4272
4403
|
}
|
|
4273
4404
|
}
|
|
4274
|
-
return results;
|
|
4275
4405
|
}
|
|
4276
4406
|
function recordMatchesSubscription(record, collection, query, userId, userRole, schema, ctx) {
|
|
4277
4407
|
if (query.collection !== collection) return false;
|
|
@@ -4468,30 +4598,56 @@ async function registerUser(sql, userId, name, email, imageUrl, isAdmin, default
|
|
|
4468
4598
|
);
|
|
4469
4599
|
return { id: userId, email, name, imageUrl, role, createdAt: now, lastSeenAt: now };
|
|
4470
4600
|
}
|
|
4601
|
+
function rosterFor(ctx, schema, records, publicRoster, attachment) {
|
|
4602
|
+
if (isAnonymousUserId(attachment.userId)) return [];
|
|
4603
|
+
if (attachment.role === "admin") {
|
|
4604
|
+
const permissionContext = ctx.getPermissionContext();
|
|
4605
|
+
return records.filter((record) => canRead(schema, "admin", record, attachment.userId, permissionContext)).map((record) => ({ id: record.recordId, ...record.data }));
|
|
4606
|
+
}
|
|
4607
|
+
if (!getRolePermissions(schema, attachment.role).read) return [];
|
|
4608
|
+
if (schema.roster === "read-policy") {
|
|
4609
|
+
const permissionContext = ctx.getPermissionContext();
|
|
4610
|
+
const readable = new Set(
|
|
4611
|
+
records.filter((record) => canRead(schema, attachment.role, record, attachment.userId, permissionContext)).map((record) => record.recordId)
|
|
4612
|
+
);
|
|
4613
|
+
return publicRoster.filter((user) => readable.has(user.id));
|
|
4614
|
+
}
|
|
4615
|
+
return publicRoster;
|
|
4616
|
+
}
|
|
4617
|
+
function publicIdentities(records) {
|
|
4618
|
+
return records.map(({ recordId, data }) => ({
|
|
4619
|
+
id: recordId,
|
|
4620
|
+
name: data.name,
|
|
4621
|
+
...data.imageUrl ? { imageUrl: data.imageUrl } : {},
|
|
4622
|
+
role: data.role
|
|
4623
|
+
}));
|
|
4624
|
+
}
|
|
4471
4625
|
function handleUserList(ctx, ws, attachment) {
|
|
4472
4626
|
const schema = ctx.schemaRegistry.get("users");
|
|
4473
|
-
|
|
4474
|
-
|
|
4475
|
-
|
|
4476
|
-
|
|
4477
|
-
|
|
4478
|
-
|
|
4479
|
-
attachment.userId,
|
|
4480
|
-
permissionContext
|
|
4481
|
-
)
|
|
4482
|
-
);
|
|
4483
|
-
const users = visibleRecords.map((record) => {
|
|
4484
|
-
if (attachment.role === "admin") return { id: record.recordId, ...record.data };
|
|
4485
|
-
const { name, imageUrl, role } = record.data;
|
|
4486
|
-
return {
|
|
4487
|
-
id: record.recordId,
|
|
4488
|
-
name,
|
|
4489
|
-
...imageUrl ? { imageUrl } : {},
|
|
4490
|
-
role
|
|
4491
|
-
};
|
|
4492
|
-
});
|
|
4627
|
+
if (!schema) {
|
|
4628
|
+
ctx.send(ws, { type: MSG.USER_LIST, payload: { users: [] } });
|
|
4629
|
+
return;
|
|
4630
|
+
}
|
|
4631
|
+
const records = getAllUserRecords(ctx.sql, ctx.schemaRegistry);
|
|
4632
|
+
const users = rosterFor(ctx, schema, records, publicIdentities(records), attachment);
|
|
4493
4633
|
ctx.send(ws, { type: MSG.USER_LIST, payload: { users } });
|
|
4494
4634
|
}
|
|
4635
|
+
function broadcastUserList(ctx, except) {
|
|
4636
|
+
const schema = ctx.schemaRegistry.get("users");
|
|
4637
|
+
if (!schema) return;
|
|
4638
|
+
const skip = except instanceof Set ? except : new Set(except ? [except] : []);
|
|
4639
|
+
const records = getAllUserRecords(ctx.sql, ctx.schemaRegistry);
|
|
4640
|
+
const publicRoster = publicIdentities(records);
|
|
4641
|
+
for (const other of ctx.state.getWebSockets()) {
|
|
4642
|
+
if (skip.has(other)) continue;
|
|
4643
|
+
const attachment = other.deserializeAttachment();
|
|
4644
|
+
if (!attachment) continue;
|
|
4645
|
+
ctx.send(other, {
|
|
4646
|
+
type: MSG.USER_LIST,
|
|
4647
|
+
payload: { users: rosterFor(ctx, schema, records, publicRoster, attachment) }
|
|
4648
|
+
});
|
|
4649
|
+
}
|
|
4650
|
+
}
|
|
4495
4651
|
function handleUserUpdate(ctx, _ws, attachment, payload) {
|
|
4496
4652
|
const existing = getUserRecord(ctx.sql, attachment.userId, ctx.schemaRegistry);
|
|
4497
4653
|
if (!existing) return;
|
|
@@ -4500,6 +4656,8 @@ function handleUserUpdate(ctx, _ws, attachment, payload) {
|
|
|
4500
4656
|
if (payload.email) data.email = payload.email;
|
|
4501
4657
|
if (payload.imageUrl !== void 0) data.imageUrl = payload.imageUrl || void 0;
|
|
4502
4658
|
putRecord(ctx, "users", attachment.userId, data, attachment.userId, "admin", true, true);
|
|
4659
|
+
const rosterChanged = data.name !== void 0 && data.name !== existing.data.name || data.imageUrl !== void 0 && data.imageUrl !== existing.data.imageUrl;
|
|
4660
|
+
if (rosterChanged) broadcastUserList(ctx);
|
|
4503
4661
|
}
|
|
4504
4662
|
async function handleSetRole(ctx, ws, attachment, payload) {
|
|
4505
4663
|
if (attachment.role !== "admin") {
|
|
@@ -4518,20 +4676,17 @@ async function handleSetRole(ctx, ws, attachment, payload) {
|
|
|
4518
4676
|
now,
|
|
4519
4677
|
payload.userId
|
|
4520
4678
|
);
|
|
4679
|
+
const closing = /* @__PURE__ */ new Set();
|
|
4521
4680
|
for (const otherWs of ctx.state.getWebSockets()) {
|
|
4522
4681
|
const otherAttachment = otherWs.deserializeAttachment();
|
|
4523
|
-
if (
|
|
4524
|
-
|
|
4525
|
-
|
|
4526
|
-
|
|
4527
|
-
|
|
4528
|
-
}
|
|
4529
|
-
continue;
|
|
4530
|
-
}
|
|
4531
|
-
if (otherAttachment.role === "admin") {
|
|
4532
|
-
handleUserList(ctx, otherWs, otherAttachment);
|
|
4682
|
+
if (otherAttachment?.userId !== payload.userId) continue;
|
|
4683
|
+
closing.add(otherWs);
|
|
4684
|
+
try {
|
|
4685
|
+
otherWs.close(1008, "role-changed");
|
|
4686
|
+
} catch {
|
|
4533
4687
|
}
|
|
4534
4688
|
}
|
|
4689
|
+
broadcastUserList(ctx, closing);
|
|
4535
4690
|
}
|
|
4536
4691
|
|
|
4537
4692
|
// src/server/handlers/tools-api.ts
|
|
@@ -4659,6 +4814,15 @@ async function executeTool(ctx, toolName, params, userId, userRole, skipUserRbac
|
|
|
4659
4814
|
if (!recordId) return { success: false, error: "Missing required param: recordId" };
|
|
4660
4815
|
return deleteRecord(ctx, collection, recordId, userId, userRole, skipUserRbac);
|
|
4661
4816
|
}
|
|
4817
|
+
// Bounded batch delete — one subrequest per page instead of one per row.
|
|
4818
|
+
// Same authorization posture as `records.delete`; `deleteWhere` itself
|
|
4819
|
+
// validates `where` (every key must name a real field) and `limit`, so no
|
|
4820
|
+
// dispatch path can reach a collection-wide truncation.
|
|
4821
|
+
case "records.deleteWhere": {
|
|
4822
|
+
const collection = params.collection;
|
|
4823
|
+
if (!collection) return { success: false, error: "Missing required param: collection" };
|
|
4824
|
+
return deleteWhere(ctx, collection, params.where, params.limit, userId, userRole, skipUserRbac);
|
|
4825
|
+
}
|
|
4662
4826
|
// ---- Users ----
|
|
4663
4827
|
// Bootstrap or refresh the `users` row for a given userId. Same path
|
|
4664
4828
|
// the SDK uses on WS connect via registerUser, exposed for server
|
|
@@ -5206,12 +5370,13 @@ var RecordRoom = class extends BaseRoom {
|
|
|
5206
5370
|
const userName = user.userName;
|
|
5207
5371
|
const userEmail = user.userEmail;
|
|
5208
5372
|
const userImageUrl = user.userImageUrl;
|
|
5209
|
-
const isAuthenticated = !userId
|
|
5373
|
+
const isAuthenticated = !isAnonymousUserId(userId);
|
|
5210
5374
|
let attachment;
|
|
5211
5375
|
if (isAuthenticated) {
|
|
5212
5376
|
const isOwner2 = this.ownerUserId != null && userId === this.ownerUserId;
|
|
5213
5377
|
const regStart = Date.now();
|
|
5214
5378
|
const defaultRole = this.schemaRegistry.get("users")?.defaultRole ?? "member";
|
|
5379
|
+
const before = getUser(this.sql, userId, this.schemaRegistry);
|
|
5215
5380
|
const registeredUser = await registerUser(
|
|
5216
5381
|
this.sql,
|
|
5217
5382
|
userId,
|
|
@@ -5223,6 +5388,9 @@ var RecordRoom = class extends BaseRoom {
|
|
|
5223
5388
|
this.schemaRegistry
|
|
5224
5389
|
);
|
|
5225
5390
|
const regMs = Date.now() - regStart;
|
|
5391
|
+
if (!before || before.name !== registeredUser.name || before.imageUrl !== registeredUser.imageUrl || before.role !== registeredUser.role) {
|
|
5392
|
+
broadcastUserList(this.createUserContext(), ws);
|
|
5393
|
+
}
|
|
5226
5394
|
attachment = {
|
|
5227
5395
|
userId: registeredUser.id,
|
|
5228
5396
|
userName,
|
|
@@ -8617,6 +8785,7 @@ export {
|
|
|
8617
8785
|
CanvasRoom,
|
|
8618
8786
|
CronRoom,
|
|
8619
8787
|
DEFAULT_CONTEXT_CONFIG,
|
|
8788
|
+
DEFAULT_DELETE_WHERE_LIMIT,
|
|
8620
8789
|
DEFAULT_DO_MANIFEST,
|
|
8621
8790
|
DEFAULT_QUERY_LIMIT,
|
|
8622
8791
|
DEPLOYED_APPS_DEFAULTS,
|
|
@@ -8627,6 +8796,7 @@ export {
|
|
|
8627
8796
|
GLOBAL_DO_TYPE_NAMES,
|
|
8628
8797
|
JobRoom,
|
|
8629
8798
|
MAX_APP_FILE_BYTES,
|
|
8799
|
+
MAX_DELETE_WHERE_LIMIT,
|
|
8630
8800
|
MAX_DEPLOY_ASSET_FILE_BYTES,
|
|
8631
8801
|
MAX_UPLOAD_PARTS,
|
|
8632
8802
|
MAX_UPLOAD_REQUEST_BYTES,
|
|
@@ -8663,6 +8833,7 @@ export {
|
|
|
8663
8833
|
authWorkerFetch,
|
|
8664
8834
|
bindingManifestFromOutputConfig,
|
|
8665
8835
|
broadcastChange,
|
|
8836
|
+
broadcastUserList,
|
|
8666
8837
|
broadcastYjsUpdate,
|
|
8667
8838
|
buildCronContext,
|
|
8668
8839
|
buildTableSelect,
|
|
@@ -8724,6 +8895,8 @@ export {
|
|
|
8724
8895
|
isTestAccountEmail,
|
|
8725
8896
|
isTestAccountTier,
|
|
8726
8897
|
lintSchema,
|
|
8898
|
+
lintSchemaSet,
|
|
8899
|
+
lintSchemas,
|
|
8727
8900
|
loadMessages,
|
|
8728
8901
|
makeDefaultSummarizer,
|
|
8729
8902
|
matchOrigin,
|