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/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
- const list = await executeToolAsApp(stub, userId, "records.query", {
2618
- collection: "ai-messages",
2619
- where: { chatId }
2620
- });
2621
- const errors = [];
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
- recordId: env.recordId
2641
+ where: { chatId },
2642
+ limit: MESSAGE_DELETE_PAGE
2627
2643
  });
2628
- } catch (err) {
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
- errors.push(err);
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")) || `anon-${crypto.randomUUID()}`,
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 schema of schemas) {
3309
- this.registerTrusted(schema);
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
- function executeQuery(ctx, query, userId, userRole, skipUserRbac = false) {
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 ?? columnId(schema.teamField);
4208
- const teamIds = preloadUserTeamIds(ctx, userId);
4209
- if (teamIds && teamIds.length > 0) {
4210
- const placeholders = teamIds.map(() => "?").join(", ");
4211
- whereClauses.push(`("${filterCol}" IN (${placeholders}) OR _created_by = ?)`);
4212
- params.push(...teamIds, userId);
4213
- } else {
4214
- whereClauses.push(`_created_by = ?`);
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
- if (query.limit) {
4255
- sql += ` LIMIT ?`;
4256
- params.push(query.limit);
4257
- }
4258
- const cursor = ctx.sql.exec(sql, ...params);
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
- for (const row of cursor.toArray()) {
4261
- const r = row;
4262
- const data = rowToData(r, columns);
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
- const permissionContext = ctx.getPermissionContext();
4474
- const visibleRecords = attachment.userId.startsWith("anon-") || !schema ? [] : getAllUserRecords(ctx.sql, ctx.schemaRegistry).filter(
4475
- (record) => canRead(
4476
- schema,
4477
- attachment.role,
4478
- record,
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 (!otherAttachment) continue;
4524
- if (otherAttachment.userId === payload.userId) {
4525
- try {
4526
- otherWs.close(1008, "role-changed");
4527
- } catch {
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.startsWith("anon-");
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,