deepspace 0.23.0 → 0.23.2
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 +21 -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-2U2VG6L5.js} +2 -2
- package/dist/{chunk-A7VKZRCB.js.map → chunk-2U2VG6L5.js.map} +1 -1
- package/dist/cli.js +937 -739
- 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 +279 -90
- package/dist/server.js.map +1 -1
- package/dist/worker.d.ts +70 -3
- package/dist/worker.js +461 -273
- 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 };
|
|
@@ -109,7 +111,7 @@ var BUILT_IN_TOOLS = [
|
|
|
109
111
|
description: `Query records from a collection with optional filtering, ordering, and pagination. Returns at most \`limit\` records (default ${DEFAULT_QUERY_LIMIT}). Large result sets are capped at ~30KB: an oversized page is truncated to the leading records that fit and flagged with \`{ truncated, returned, total }\`, so narrow with a \`where\` filter (or page via \`orderBy\` + \`limit\`) to see the rest.`,
|
|
110
112
|
params: {
|
|
111
113
|
collection: { type: "string", description: "Collection name to query", required: true },
|
|
112
|
-
where: { type: "object", description: "Filter object with field=value equality conditions", required: false },
|
|
114
|
+
where: { type: "object", description: "Filter object with field=value equality conditions; every key must be recordId, createdBy, or a schema column (an unknown key is refused, not ignored)", required: false },
|
|
113
115
|
orderBy: { type: "string", description: 'Field to order by (or "createdAt"/"updatedAt")', required: false },
|
|
114
116
|
orderDir: { type: "string", description: 'Order direction: "asc" or "desc" (default: "desc")', required: false },
|
|
115
117
|
limit: { type: "number", description: `Maximum number of records to return (default: ${DEFAULT_QUERY_LIMIT}). Oversized pages are still capped at ~30KB and truncated with returned/total flags.`, required: false }
|
|
@@ -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,86 @@ 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 refuseUnknownWhere(collection, schema, where) {
|
|
3840
|
+
const columns = (schema.columns ?? []).map(resolveColumn);
|
|
3841
|
+
const filterable = ["recordId", "createdBy", ...columns.map((c) => c.name)];
|
|
3842
|
+
const unknown = Object.keys(where).filter(
|
|
3843
|
+
(key) => key !== "recordId" && key !== "createdBy" && !columns.some((c) => c.id === columnId(key) || c.name === key)
|
|
3844
|
+
);
|
|
3845
|
+
if (unknown.length > 0) {
|
|
3846
|
+
return {
|
|
3847
|
+
success: false,
|
|
3848
|
+
error: `Unknown field(s) in where for "${collection}": ${unknown.join(", ")} \u2014 filterable fields: ${filterable.join(", ")}`
|
|
3849
|
+
};
|
|
3850
|
+
}
|
|
3851
|
+
const nonPrimitive = Object.entries(where).filter(([, v]) => v !== null && typeof v === "object");
|
|
3852
|
+
if (nonPrimitive.length > 0) {
|
|
3853
|
+
return {
|
|
3854
|
+
success: false,
|
|
3855
|
+
error: `where values must be primitives (equality only): ${nonPrimitive.map(([k]) => k).join(", ")}`
|
|
3856
|
+
};
|
|
3857
|
+
}
|
|
3858
|
+
return null;
|
|
3859
|
+
}
|
|
3860
|
+
function deleteWhere(ctx, collection, where, limit, userId, userRole, skipUserRbac = false) {
|
|
3861
|
+
const resolved = resolveCollection(ctx, collection);
|
|
3862
|
+
if (!resolved.ok) return { success: false, error: resolved.error };
|
|
3863
|
+
const schema = resolved.schema;
|
|
3864
|
+
if (!schema) {
|
|
3865
|
+
return {
|
|
3866
|
+
success: false,
|
|
3867
|
+
error: `records.deleteWhere: collection "${collection}" has no schema, so it has no filterable fields \u2014 delete its rows by recordId with records.delete`
|
|
3868
|
+
};
|
|
3869
|
+
}
|
|
3870
|
+
if (!where || typeof where !== "object" || Array.isArray(where) || Object.keys(where).length === 0) {
|
|
3871
|
+
return { success: false, error: "Missing required param: where (must match at least one field)" };
|
|
3872
|
+
}
|
|
3873
|
+
const whereRefusal = refuseUnknownWhere(collection, schema, where);
|
|
3874
|
+
if (whereRefusal) return whereRefusal;
|
|
3875
|
+
if (limit !== void 0 && !(typeof limit === "number" && Number.isFinite(limit))) {
|
|
3876
|
+
return { success: false, error: "limit must be a finite number" };
|
|
3877
|
+
}
|
|
3878
|
+
const page = Math.min(
|
|
3879
|
+
Math.max(1, Math.floor(limit ?? DEFAULT_DELETE_WHERE_LIMIT)),
|
|
3880
|
+
MAX_DELETE_WHERE_LIMIT
|
|
3881
|
+
);
|
|
3882
|
+
const scan = { capped: false };
|
|
3883
|
+
const matched = executeQuery(
|
|
3884
|
+
ctx,
|
|
3885
|
+
{ collection, where, limit: page },
|
|
3886
|
+
userId,
|
|
3887
|
+
userRole,
|
|
3888
|
+
skipUserRbac,
|
|
3889
|
+
scan
|
|
3890
|
+
);
|
|
3891
|
+
if (scan.capped && matched.length < page) {
|
|
3892
|
+
return {
|
|
3893
|
+
success: false,
|
|
3894
|
+
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`
|
|
3895
|
+
};
|
|
3896
|
+
}
|
|
3897
|
+
if (!skipUserRbac && schema) {
|
|
3898
|
+
const permCtx = ctx.getPermissionContext();
|
|
3899
|
+
for (const record of matched) {
|
|
3900
|
+
if (!canDelete(schema, userRole, record, userId, permCtx)) {
|
|
3901
|
+
return { success: false, error: `DELETE DENIED: role=${userRole}, collection=${collection}` };
|
|
3902
|
+
}
|
|
3903
|
+
}
|
|
3904
|
+
}
|
|
3905
|
+
let deleted = 0;
|
|
3906
|
+
for (const record of matched) {
|
|
3907
|
+
if (deleteRecord(ctx, collection, record.recordId, userId, userRole, skipUserRbac).success) {
|
|
3908
|
+
deleted++;
|
|
3909
|
+
}
|
|
3910
|
+
}
|
|
3911
|
+
if (deleted < matched.length) {
|
|
3912
|
+
return {
|
|
3913
|
+
success: false,
|
|
3914
|
+
error: `Deleted ${deleted} of ${matched.length} matched record(s) in "${collection}"; the rest failed \u2014 retry`
|
|
3915
|
+
};
|
|
3916
|
+
}
|
|
3917
|
+
return { success: true, data: { deleted } };
|
|
3918
|
+
}
|
|
3814
3919
|
function readRecord(ctx, collection, recordId, userId, userRole, skipUserRbac = false) {
|
|
3815
3920
|
const resolved = resolveCollection(ctx, collection);
|
|
3816
3921
|
if (!resolved.ok) return { success: false, error: resolved.error };
|
|
@@ -4146,7 +4251,8 @@ function handleSubscribe(ctx, ws, attachment, payload) {
|
|
|
4146
4251
|
}
|
|
4147
4252
|
function handleUnsubscribe(_ctx, _ws, _attachment, _payload) {
|
|
4148
4253
|
}
|
|
4149
|
-
|
|
4254
|
+
var MAX_FILTERED_SCAN_ROWS = 5e3;
|
|
4255
|
+
function executeQuery(ctx, query, userId, userRole, skipUserRbac = false, scanState) {
|
|
4150
4256
|
const resolved = resolveCollection(ctx, query.collection);
|
|
4151
4257
|
if (!resolved.ok) return [];
|
|
4152
4258
|
if (!resolved.schema) {
|
|
@@ -4159,7 +4265,8 @@ function executeQuery(ctx, query, userId, userRole, skipUserRbac = false) {
|
|
|
4159
4265
|
userId,
|
|
4160
4266
|
userRole,
|
|
4161
4267
|
resolved.isSystem,
|
|
4162
|
-
skipUserRbac
|
|
4268
|
+
skipUserRbac,
|
|
4269
|
+
scanState
|
|
4163
4270
|
);
|
|
4164
4271
|
}
|
|
4165
4272
|
function executeSystemQuery(ctx, query) {
|
|
@@ -4196,7 +4303,7 @@ function preloadUserTeamIds(ctx, userId) {
|
|
|
4196
4303
|
);
|
|
4197
4304
|
return cursor.toArray().map((r) => r.col_teamid);
|
|
4198
4305
|
}
|
|
4199
|
-
function executeTableQuery(ctx, query, schema, userId, userRole, isSystem, skipUserRbac = false) {
|
|
4306
|
+
function executeTableQuery(ctx, query, schema, userId, userRole, isSystem, skipUserRbac = false, scanState) {
|
|
4200
4307
|
const columns = (schema.columns ?? []).map(resolveColumn);
|
|
4201
4308
|
const perms = getRolePermissions(schema, userRole);
|
|
4202
4309
|
let sql = buildTableSelect(query.collection, columns);
|
|
@@ -4204,16 +4311,27 @@ function executeTableQuery(ctx, query, schema, userId, userRole, isSystem, skipU
|
|
|
4204
4311
|
const whereClauses = [];
|
|
4205
4312
|
let skipPerRecordCheck = skipUserRbac;
|
|
4206
4313
|
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
|
-
|
|
4314
|
+
const filterCol = schema.teamField === "_rowId" ? "_row_id" : columns.find((c) => c.name === schema.teamField)?.id;
|
|
4315
|
+
const teamIds = filterCol ? preloadUserTeamIds(ctx, userId) : null;
|
|
4316
|
+
if (filterCol) {
|
|
4317
|
+
if (teamIds && teamIds.length > 0) {
|
|
4318
|
+
const placeholders = teamIds.map(() => "?").join(", ");
|
|
4319
|
+
whereClauses.push(`("${filterCol}" IN (${placeholders}) OR _created_by = ?)`);
|
|
4320
|
+
params.push(...teamIds, userId);
|
|
4321
|
+
} else {
|
|
4322
|
+
whereClauses.push(`_created_by = ?`);
|
|
4323
|
+
params.push(userId);
|
|
4324
|
+
}
|
|
4325
|
+
skipPerRecordCheck = true;
|
|
4326
|
+
}
|
|
4327
|
+
} else if (!skipUserRbac && perms.read === "own") {
|
|
4328
|
+
const ownerCol = schema.ownerField ? columns.find((c) => c.name === schema.ownerField)?.id : "_created_by";
|
|
4329
|
+
if (ownerCol) {
|
|
4330
|
+
whereClauses.push(`"${ownerCol}" = ?`);
|
|
4215
4331
|
params.push(userId);
|
|
4332
|
+
skipPerRecordCheck = true;
|
|
4216
4333
|
}
|
|
4334
|
+
} else if (!skipUserRbac && perms.read === true) {
|
|
4217
4335
|
skipPerRecordCheck = true;
|
|
4218
4336
|
}
|
|
4219
4337
|
if (query.where) {
|
|
@@ -4251,27 +4369,44 @@ function executeTableQuery(ctx, query, schema, userId, userRole, isSystem, skipU
|
|
|
4251
4369
|
} else {
|
|
4252
4370
|
sql += ` ORDER BY _created_at DESC`;
|
|
4253
4371
|
}
|
|
4254
|
-
|
|
4255
|
-
sql
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
|
|
4372
|
+
const readPage = (limitClause, limitParams) => {
|
|
4373
|
+
const cursor = ctx.sql.exec(sql + limitClause, ...params, ...limitParams);
|
|
4374
|
+
const rows = cursor.toArray();
|
|
4375
|
+
const records = [];
|
|
4376
|
+
for (const row of rows) {
|
|
4377
|
+
const r = row;
|
|
4378
|
+
const record = {
|
|
4379
|
+
recordId: r._row_id,
|
|
4380
|
+
data: rowToData(r, columns),
|
|
4381
|
+
createdBy: r._created_by,
|
|
4382
|
+
createdAt: r._created_at,
|
|
4383
|
+
updatedAt: r._updated_at
|
|
4384
|
+
};
|
|
4385
|
+
if (isSystem || skipPerRecordCheck || canRead(schema, userRole, { data: record.data, createdBy: record.createdBy, recordId: record.recordId }, userId, ctx.getPermissionContext())) {
|
|
4386
|
+
records.push(record);
|
|
4387
|
+
}
|
|
4388
|
+
}
|
|
4389
|
+
return { records, scanned: rows.length };
|
|
4390
|
+
};
|
|
4391
|
+
if (!query.limit) return readPage("", []).records;
|
|
4392
|
+
const rowFiltered = !isSystem && !skipPerRecordCheck;
|
|
4393
|
+
if (!rowFiltered) return readPage(" LIMIT ?", [query.limit]).records;
|
|
4259
4394
|
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())) {
|
|
4395
|
+
const batch = Math.max(query.limit, 200);
|
|
4396
|
+
for (let offset = 0; ; offset += batch) {
|
|
4397
|
+
const { records, scanned } = readPage(" LIMIT ? OFFSET ?", [batch, offset]);
|
|
4398
|
+
for (const record of records) {
|
|
4271
4399
|
results.push(record);
|
|
4400
|
+
if (results.length >= query.limit) return results;
|
|
4401
|
+
}
|
|
4402
|
+
if (scanned < batch) return results;
|
|
4403
|
+
if (offset + batch >= MAX_FILTERED_SCAN_ROWS) {
|
|
4404
|
+
if (scanState && readPage(" LIMIT ? OFFSET ?", [1, offset + batch]).scanned > 0) {
|
|
4405
|
+
scanState.capped = true;
|
|
4406
|
+
}
|
|
4407
|
+
return results;
|
|
4272
4408
|
}
|
|
4273
4409
|
}
|
|
4274
|
-
return results;
|
|
4275
4410
|
}
|
|
4276
4411
|
function recordMatchesSubscription(record, collection, query, userId, userRole, schema, ctx) {
|
|
4277
4412
|
if (query.collection !== collection) return false;
|
|
@@ -4468,30 +4603,56 @@ async function registerUser(sql, userId, name, email, imageUrl, isAdmin, default
|
|
|
4468
4603
|
);
|
|
4469
4604
|
return { id: userId, email, name, imageUrl, role, createdAt: now, lastSeenAt: now };
|
|
4470
4605
|
}
|
|
4606
|
+
function rosterFor(ctx, schema, records, publicRoster, attachment) {
|
|
4607
|
+
if (isAnonymousUserId(attachment.userId)) return [];
|
|
4608
|
+
if (attachment.role === "admin") {
|
|
4609
|
+
const permissionContext = ctx.getPermissionContext();
|
|
4610
|
+
return records.filter((record) => canRead(schema, "admin", record, attachment.userId, permissionContext)).map((record) => ({ id: record.recordId, ...record.data }));
|
|
4611
|
+
}
|
|
4612
|
+
if (!getRolePermissions(schema, attachment.role).read) return [];
|
|
4613
|
+
if (schema.roster === "read-policy") {
|
|
4614
|
+
const permissionContext = ctx.getPermissionContext();
|
|
4615
|
+
const readable = new Set(
|
|
4616
|
+
records.filter((record) => canRead(schema, attachment.role, record, attachment.userId, permissionContext)).map((record) => record.recordId)
|
|
4617
|
+
);
|
|
4618
|
+
return publicRoster.filter((user) => readable.has(user.id));
|
|
4619
|
+
}
|
|
4620
|
+
return publicRoster;
|
|
4621
|
+
}
|
|
4622
|
+
function publicIdentities(records) {
|
|
4623
|
+
return records.map(({ recordId, data }) => ({
|
|
4624
|
+
id: recordId,
|
|
4625
|
+
name: data.name,
|
|
4626
|
+
...data.imageUrl ? { imageUrl: data.imageUrl } : {},
|
|
4627
|
+
role: data.role
|
|
4628
|
+
}));
|
|
4629
|
+
}
|
|
4471
4630
|
function handleUserList(ctx, ws, attachment) {
|
|
4472
4631
|
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
|
-
});
|
|
4632
|
+
if (!schema) {
|
|
4633
|
+
ctx.send(ws, { type: MSG.USER_LIST, payload: { users: [] } });
|
|
4634
|
+
return;
|
|
4635
|
+
}
|
|
4636
|
+
const records = getAllUserRecords(ctx.sql, ctx.schemaRegistry);
|
|
4637
|
+
const users = rosterFor(ctx, schema, records, publicIdentities(records), attachment);
|
|
4493
4638
|
ctx.send(ws, { type: MSG.USER_LIST, payload: { users } });
|
|
4494
4639
|
}
|
|
4640
|
+
function broadcastUserList(ctx, except) {
|
|
4641
|
+
const schema = ctx.schemaRegistry.get("users");
|
|
4642
|
+
if (!schema) return;
|
|
4643
|
+
const skip = except instanceof Set ? except : new Set(except ? [except] : []);
|
|
4644
|
+
const records = getAllUserRecords(ctx.sql, ctx.schemaRegistry);
|
|
4645
|
+
const publicRoster = publicIdentities(records);
|
|
4646
|
+
for (const other of ctx.state.getWebSockets()) {
|
|
4647
|
+
if (skip.has(other)) continue;
|
|
4648
|
+
const attachment = other.deserializeAttachment();
|
|
4649
|
+
if (!attachment) continue;
|
|
4650
|
+
ctx.send(other, {
|
|
4651
|
+
type: MSG.USER_LIST,
|
|
4652
|
+
payload: { users: rosterFor(ctx, schema, records, publicRoster, attachment) }
|
|
4653
|
+
});
|
|
4654
|
+
}
|
|
4655
|
+
}
|
|
4495
4656
|
function handleUserUpdate(ctx, _ws, attachment, payload) {
|
|
4496
4657
|
const existing = getUserRecord(ctx.sql, attachment.userId, ctx.schemaRegistry);
|
|
4497
4658
|
if (!existing) return;
|
|
@@ -4500,6 +4661,8 @@ function handleUserUpdate(ctx, _ws, attachment, payload) {
|
|
|
4500
4661
|
if (payload.email) data.email = payload.email;
|
|
4501
4662
|
if (payload.imageUrl !== void 0) data.imageUrl = payload.imageUrl || void 0;
|
|
4502
4663
|
putRecord(ctx, "users", attachment.userId, data, attachment.userId, "admin", true, true);
|
|
4664
|
+
const rosterChanged = data.name !== void 0 && data.name !== existing.data.name || data.imageUrl !== void 0 && data.imageUrl !== existing.data.imageUrl;
|
|
4665
|
+
if (rosterChanged) broadcastUserList(ctx);
|
|
4503
4666
|
}
|
|
4504
4667
|
async function handleSetRole(ctx, ws, attachment, payload) {
|
|
4505
4668
|
if (attachment.role !== "admin") {
|
|
@@ -4518,20 +4681,17 @@ async function handleSetRole(ctx, ws, attachment, payload) {
|
|
|
4518
4681
|
now,
|
|
4519
4682
|
payload.userId
|
|
4520
4683
|
);
|
|
4684
|
+
const closing = /* @__PURE__ */ new Set();
|
|
4521
4685
|
for (const otherWs of ctx.state.getWebSockets()) {
|
|
4522
4686
|
const otherAttachment = otherWs.deserializeAttachment();
|
|
4523
|
-
if (
|
|
4524
|
-
|
|
4525
|
-
|
|
4526
|
-
|
|
4527
|
-
|
|
4528
|
-
}
|
|
4529
|
-
continue;
|
|
4530
|
-
}
|
|
4531
|
-
if (otherAttachment.role === "admin") {
|
|
4532
|
-
handleUserList(ctx, otherWs, otherAttachment);
|
|
4687
|
+
if (otherAttachment?.userId !== payload.userId) continue;
|
|
4688
|
+
closing.add(otherWs);
|
|
4689
|
+
try {
|
|
4690
|
+
otherWs.close(1008, "role-changed");
|
|
4691
|
+
} catch {
|
|
4533
4692
|
}
|
|
4534
4693
|
}
|
|
4694
|
+
broadcastUserList(ctx, closing);
|
|
4535
4695
|
}
|
|
4536
4696
|
|
|
4537
4697
|
// src/server/handlers/tools-api.ts
|
|
@@ -4618,9 +4778,20 @@ async function executeTool(ctx, toolName, params, userId, userRole, skipUserRbac
|
|
|
4618
4778
|
if (!collection) return { success: false, error: "Missing required param: collection" };
|
|
4619
4779
|
const resolved = resolveCollection(ctx, collection);
|
|
4620
4780
|
if (!resolved.ok) return { success: false, error: resolved.error };
|
|
4781
|
+
const where = params.where;
|
|
4782
|
+
if (where && typeof where === "object" && Object.keys(where).length > 0) {
|
|
4783
|
+
if (!resolved.schema) {
|
|
4784
|
+
return {
|
|
4785
|
+
success: false,
|
|
4786
|
+
error: `records.query: collection "${collection}" has no schema, so it has no filterable fields \u2014 query it without \`where\``
|
|
4787
|
+
};
|
|
4788
|
+
}
|
|
4789
|
+
const refusal = refuseUnknownWhere(collection, resolved.schema, where);
|
|
4790
|
+
if (refusal) return refusal;
|
|
4791
|
+
}
|
|
4621
4792
|
const query = {
|
|
4622
4793
|
collection,
|
|
4623
|
-
where
|
|
4794
|
+
where,
|
|
4624
4795
|
orderBy: params.orderBy,
|
|
4625
4796
|
orderDir: params.orderDir,
|
|
4626
4797
|
limit: params.limit
|
|
@@ -4659,6 +4830,15 @@ async function executeTool(ctx, toolName, params, userId, userRole, skipUserRbac
|
|
|
4659
4830
|
if (!recordId) return { success: false, error: "Missing required param: recordId" };
|
|
4660
4831
|
return deleteRecord(ctx, collection, recordId, userId, userRole, skipUserRbac);
|
|
4661
4832
|
}
|
|
4833
|
+
// Bounded batch delete — one subrequest per page instead of one per row.
|
|
4834
|
+
// Same authorization posture as `records.delete`; `deleteWhere` itself
|
|
4835
|
+
// validates `where` (every key must name a real field) and `limit`, so no
|
|
4836
|
+
// dispatch path can reach a collection-wide truncation.
|
|
4837
|
+
case "records.deleteWhere": {
|
|
4838
|
+
const collection = params.collection;
|
|
4839
|
+
if (!collection) return { success: false, error: "Missing required param: collection" };
|
|
4840
|
+
return deleteWhere(ctx, collection, params.where, params.limit, userId, userRole, skipUserRbac);
|
|
4841
|
+
}
|
|
4662
4842
|
// ---- Users ----
|
|
4663
4843
|
// Bootstrap or refresh the `users` row for a given userId. Same path
|
|
4664
4844
|
// the SDK uses on WS connect via registerUser, exposed for server
|
|
@@ -5206,12 +5386,13 @@ var RecordRoom = class extends BaseRoom {
|
|
|
5206
5386
|
const userName = user.userName;
|
|
5207
5387
|
const userEmail = user.userEmail;
|
|
5208
5388
|
const userImageUrl = user.userImageUrl;
|
|
5209
|
-
const isAuthenticated = !userId
|
|
5389
|
+
const isAuthenticated = !isAnonymousUserId(userId);
|
|
5210
5390
|
let attachment;
|
|
5211
5391
|
if (isAuthenticated) {
|
|
5212
5392
|
const isOwner2 = this.ownerUserId != null && userId === this.ownerUserId;
|
|
5213
5393
|
const regStart = Date.now();
|
|
5214
5394
|
const defaultRole = this.schemaRegistry.get("users")?.defaultRole ?? "member";
|
|
5395
|
+
const before = getUser(this.sql, userId, this.schemaRegistry);
|
|
5215
5396
|
const registeredUser = await registerUser(
|
|
5216
5397
|
this.sql,
|
|
5217
5398
|
userId,
|
|
@@ -5223,6 +5404,9 @@ var RecordRoom = class extends BaseRoom {
|
|
|
5223
5404
|
this.schemaRegistry
|
|
5224
5405
|
);
|
|
5225
5406
|
const regMs = Date.now() - regStart;
|
|
5407
|
+
if (!before || before.name !== registeredUser.name || before.imageUrl !== registeredUser.imageUrl || before.role !== registeredUser.role) {
|
|
5408
|
+
broadcastUserList(this.createUserContext(), ws);
|
|
5409
|
+
}
|
|
5226
5410
|
attachment = {
|
|
5227
5411
|
userId: registeredUser.id,
|
|
5228
5412
|
userName,
|
|
@@ -8617,6 +8801,7 @@ export {
|
|
|
8617
8801
|
CanvasRoom,
|
|
8618
8802
|
CronRoom,
|
|
8619
8803
|
DEFAULT_CONTEXT_CONFIG,
|
|
8804
|
+
DEFAULT_DELETE_WHERE_LIMIT,
|
|
8620
8805
|
DEFAULT_DO_MANIFEST,
|
|
8621
8806
|
DEFAULT_QUERY_LIMIT,
|
|
8622
8807
|
DEPLOYED_APPS_DEFAULTS,
|
|
@@ -8627,6 +8812,7 @@ export {
|
|
|
8627
8812
|
GLOBAL_DO_TYPE_NAMES,
|
|
8628
8813
|
JobRoom,
|
|
8629
8814
|
MAX_APP_FILE_BYTES,
|
|
8815
|
+
MAX_DELETE_WHERE_LIMIT,
|
|
8630
8816
|
MAX_DEPLOY_ASSET_FILE_BYTES,
|
|
8631
8817
|
MAX_UPLOAD_PARTS,
|
|
8632
8818
|
MAX_UPLOAD_REQUEST_BYTES,
|
|
@@ -8663,6 +8849,7 @@ export {
|
|
|
8663
8849
|
authWorkerFetch,
|
|
8664
8850
|
bindingManifestFromOutputConfig,
|
|
8665
8851
|
broadcastChange,
|
|
8852
|
+
broadcastUserList,
|
|
8666
8853
|
broadcastYjsUpdate,
|
|
8667
8854
|
buildCronContext,
|
|
8668
8855
|
buildTableSelect,
|
|
@@ -8724,6 +8911,8 @@ export {
|
|
|
8724
8911
|
isTestAccountEmail,
|
|
8725
8912
|
isTestAccountTier,
|
|
8726
8913
|
lintSchema,
|
|
8914
|
+
lintSchemaSet,
|
|
8915
|
+
lintSchemas,
|
|
8727
8916
|
loadMessages,
|
|
8728
8917
|
makeDefaultSummarizer,
|
|
8729
8918
|
matchOrigin,
|