deepspace 0.18.0 → 0.19.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
@@ -915,6 +915,33 @@ function buildCronContext(env, ownerUserId, roomId = "default") {
915
915
  return { records, integrations, ownerUserId };
916
916
  }
917
917
 
918
+ // src/server/utils/app-role.ts
919
+ async function resolveAppRole(env, userId) {
920
+ if (userId === env.OWNER_USER_ID) return "admin";
921
+ const stub = env.RECORD_ROOMS.get(env.RECORD_ROOMS.idFromName(`app:${env.DEEPSPACE_APP_ID}`));
922
+ try {
923
+ const res = await stub.fetch(
924
+ new Request("https://internal/api/tools/execute", {
925
+ method: "POST",
926
+ headers: {
927
+ "Content-Type": "application/json",
928
+ "X-User-Id": env.OWNER_USER_ID,
929
+ "X-App-Action": "true"
930
+ },
931
+ body: JSON.stringify({
932
+ tool: "records.get",
933
+ params: { collection: "users", recordId: userId }
934
+ })
935
+ })
936
+ );
937
+ const json = await res.json();
938
+ const role = json.data?.record?.data?.role;
939
+ return json.success && (role === "admin" || role === "member") ? role : "viewer";
940
+ } catch {
941
+ return "viewer";
942
+ }
943
+ }
944
+
918
945
  // src/server/utils/ai.ts
919
946
  import { createAnthropic } from "@ai-sdk/anthropic";
920
947
  import { createOpenAI } from "@ai-sdk/openai";
@@ -1930,7 +1957,30 @@ function splitStatements(sql) {
1930
1957
  return sql.split(";").map((s) => s.trim()).filter((s) => s.length > 0);
1931
1958
  }
1932
1959
 
1960
+ // src/shared/room-identity-headers.ts
1961
+ var BASE64URL = /^[A-Za-z0-9_-]*$/;
1962
+ function decodeRoomIdentityHeader(value) {
1963
+ if (value === null || !BASE64URL.test(value) || value.length % 4 === 1) return void 0;
1964
+ const base64 = value.replaceAll("-", "+").replaceAll("_", "/").padEnd(value.length + (4 - value.length % 4) % 4, "=");
1965
+ try {
1966
+ const binary = atob(base64);
1967
+ const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
1968
+ return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
1969
+ } catch {
1970
+ return void 0;
1971
+ }
1972
+ }
1973
+
1933
1974
  // src/server/rooms/base-room.ts
1975
+ function connectionAttachmentFromRequest(request) {
1976
+ return {
1977
+ userId: decodeRoomIdentityHeader(request.headers.get("x-user-id")) || `anon-${crypto.randomUUID()}`,
1978
+ userName: decodeRoomIdentityHeader(request.headers.get("x-user-name")) || "Anonymous",
1979
+ userEmail: decodeRoomIdentityHeader(request.headers.get("x-user-email")) || "",
1980
+ userImageUrl: decodeRoomIdentityHeader(request.headers.get("x-user-image-url")),
1981
+ role: decodeRoomIdentityHeader(request.headers.get("x-user-role"))
1982
+ };
1983
+ }
1934
1984
  var BaseRoom = class {
1935
1985
  state;
1936
1986
  env;
@@ -1939,9 +1989,7 @@ var BaseRoom = class {
1939
1989
  this.state = state;
1940
1990
  this.env = env ?? {};
1941
1991
  this.sql = state.storage.sql;
1942
- this.state.setWebSocketAutoResponse(
1943
- new WebSocketRequestResponsePair("ping", "pong")
1944
- );
1992
+ this.state.setWebSocketAutoResponse(new WebSocketRequestResponsePair("ping", "pong"));
1945
1993
  }
1946
1994
  // ==========================================================================
1947
1995
  // HTTP Entry Point
@@ -1991,22 +2039,11 @@ var BaseRoom = class {
1991
2039
  // ==========================================================================
1992
2040
  // WebSocket Connection
1993
2041
  // ==========================================================================
1994
- async handleWebSocketUpgrade(_request, url) {
1995
- const userId = url.searchParams.get("userId") || void 0;
1996
- const userName = url.searchParams.get("userName") || "Anonymous";
1997
- const userEmail = url.searchParams.get("userEmail") || "";
1998
- const userImageUrl = url.searchParams.get("userImageUrl") || void 0;
2042
+ async handleWebSocketUpgrade(request, _url) {
1999
2043
  const pair = new WebSocketPair();
2000
2044
  const [client, server] = Object.values(pair);
2001
2045
  this.state.acceptWebSocket(server);
2002
- const role = url.searchParams.get("role") || void 0;
2003
- const attachment = {
2004
- userId: userId ?? `anon-${crypto.randomUUID()}`,
2005
- userName,
2006
- userEmail,
2007
- userImageUrl,
2008
- role
2009
- };
2046
+ const attachment = connectionAttachmentFromRequest(request);
2010
2047
  const augmented = await this.onConnect(server, attachment) ?? attachment;
2011
2048
  server.serializeAttachment(augmented);
2012
2049
  return new Response(null, { status: 101, webSocket: client });
@@ -2304,7 +2341,7 @@ function dataToColumnValues(data, columns) {
2304
2341
  const values = {};
2305
2342
  for (const col of columns) {
2306
2343
  if (col.expression || col.readonly) continue;
2307
- const val = data[col.id] ?? data[col.name];
2344
+ const val = data[col.name];
2308
2345
  if (val !== void 0) {
2309
2346
  values[col.id] = coerceValue(val, col.storage, col.interpretation);
2310
2347
  }
@@ -2453,6 +2490,15 @@ function checkFieldPermissions(schema, role, newData, existingData) {
2453
2490
  }
2454
2491
  return null;
2455
2492
  }
2493
+ function checkUnclaimedOwnerTransition(schema, role, nextData, userId) {
2494
+ const permissions = getRolePermissions(schema, role);
2495
+ if (permissions.update !== "unclaimed-or-own" || !schema.ownerField) return null;
2496
+ const nextOwner = nextData[schema.ownerField];
2497
+ if (nextOwner === void 0 || nextOwner === null || nextOwner === "" || nextOwner === userId) {
2498
+ return null;
2499
+ }
2500
+ return `Role '${role}' can set '${schema.ownerField}' only to their own user id or leave it unclaimed`;
2501
+ }
2456
2502
  var SYSTEM_MANAGED_COLUMNS = /* @__PURE__ */ new Set([
2457
2503
  "email",
2458
2504
  "name",
@@ -2496,7 +2542,7 @@ function lintSchema(schema) {
2496
2542
  if (schema.ownerField) {
2497
2543
  const col = schema.columns.find((c) => c.name === schema.ownerField);
2498
2544
  const spoofableRoles = Object.entries(schema.permissions).filter(
2499
- ([role, permissions]) => role !== "owner" && role !== "admin" && permissions.create === true
2545
+ ([role, permissions]) => role !== "owner" && role !== "admin" && permissions.create === true && permissions.update !== "unclaimed-or-own"
2500
2546
  ).map(([role]) => role);
2501
2547
  if (col && !col.userBound && spoofableRoles.length > 0) {
2502
2548
  warnings.push(
@@ -2836,6 +2882,14 @@ function putRecord(ctx, collection, recordId, data, userId, userRole, skipUserRb
2836
2882
  return { success: false, error: `Schema not registered for collection: ${collection}` };
2837
2883
  }
2838
2884
  const columns = getResolvedColumns(schema);
2885
+ for (const col of columns) {
2886
+ if (col.id !== col.name && Object.hasOwn(data, col.id)) {
2887
+ return {
2888
+ success: false,
2889
+ error: `FIELD ERROR: physical column id '${col.id}' is not a writable field; use '${col.name}'`
2890
+ };
2891
+ }
2892
+ }
2839
2893
  const existing = getRecord(ctx.sql, collection, recordId, schema);
2840
2894
  const isUpdate = existing !== null;
2841
2895
  const mergedData = isUpdate ? { ...existing.data, ...data } : { ...data };
@@ -2898,10 +2952,6 @@ function putRecord(ctx, collection, recordId, data, userId, userRole, skipUserRb
2898
2952
  error: `UPDATE DENIED: role=${userRole}, collection=${collection}`
2899
2953
  };
2900
2954
  }
2901
- const fieldError = checkFieldPermissions(schema, userRole, finalData, existing.data);
2902
- if (fieldError) {
2903
- return { success: false, error: `FIELD ERROR: ${fieldError}` };
2904
- }
2905
2955
  } else {
2906
2956
  if (!canCreate(schema, userRole)) {
2907
2957
  return {
@@ -2910,6 +2960,19 @@ function putRecord(ctx, collection, recordId, data, userId, userRole, skipUserRb
2910
2960
  };
2911
2961
  }
2912
2962
  }
2963
+ const fieldError = checkFieldPermissions(
2964
+ schema,
2965
+ userRole,
2966
+ data,
2967
+ isUpdate ? existing.data : void 0
2968
+ );
2969
+ if (fieldError) {
2970
+ return { success: false, error: `FIELD ERROR: ${fieldError}` };
2971
+ }
2972
+ const ownerError = checkUnclaimedOwnerTransition(schema, userRole, finalData, userId);
2973
+ if (ownerError) {
2974
+ return { success: false, error: `FIELD ERROR: ${ownerError}` };
2975
+ }
2913
2976
  }
2914
2977
  const colValues = dataToColumnValues(finalData, columns);
2915
2978
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -3162,11 +3225,17 @@ function handleYjsJoin(ctx, ws, attachment, payload) {
3162
3225
  }
3163
3226
  function handleYjsLeave(ctx, ws, attachment, payload) {
3164
3227
  const { collection, recordId, fieldName } = payload;
3228
+ const docKey = getYjsDocKey(collection, recordId, fieldName);
3229
+ const clientId = attachment.awarenessClientIds?.[docKey];
3230
+ if (clientId !== void 0) {
3231
+ broadcastAwarenessClientRemoval(ctx, docKey, clientId);
3232
+ delete attachment.awarenessClientIds[docKey];
3233
+ }
3165
3234
  attachment.yjsSubscriptions = attachment.yjsSubscriptions.filter(
3166
3235
  (s) => !(s.collection === collection && s.recordId === recordId && s.fieldName === fieldName)
3167
3236
  );
3168
3237
  ws.serializeAttachment(attachment);
3169
- unloadYjsDocIfUnused(ctx, getYjsDocKey(collection, recordId, fieldName), ws);
3238
+ unloadYjsDocIfUnused(ctx, docKey, ws);
3170
3239
  }
3171
3240
  function handleYjsBinaryMessage(ctx, ws, attachment, data) {
3172
3241
  const decoder = createDecoder(data);
@@ -3178,12 +3247,13 @@ function handleYjsBinaryMessage(ctx, ws, attachment, data) {
3178
3247
  const parsed2 = parseYjsDocKey(docKey2);
3179
3248
  if (!parsed2) return;
3180
3249
  if (!hasYjsSubscription(attachment, parsed2)) return;
3181
- if (attachment.awarenessClientId == null) {
3250
+ if (attachment.awarenessClientIds?.[docKey2] === void 0) {
3182
3251
  try {
3183
3252
  const pd = createDecoder(payload);
3184
3253
  const count = readVarUint(pd);
3185
3254
  if (count > 0) {
3186
- attachment.awarenessClientId = readVarUint(pd);
3255
+ attachment.awarenessClientIds ??= {};
3256
+ attachment.awarenessClientIds[docKey2] = readVarUint(pd);
3187
3257
  ws.serializeAttachment(attachment);
3188
3258
  }
3189
3259
  } catch {
@@ -3265,18 +3335,21 @@ function broadcastAwareness(ctx, docKey, payload, excludeWs) {
3265
3335
  }
3266
3336
  }
3267
3337
  function broadcastAwarenessRemoval(ctx, attachment) {
3268
- const clientId = attachment.awarenessClientId;
3269
- if (clientId == null || attachment.yjsSubscriptions.length === 0) return;
3338
+ if (!attachment.awarenessClientIds) return;
3339
+ for (const sub of attachment.yjsSubscriptions) {
3340
+ const docKey = getYjsDocKey(sub.collection, sub.recordId, sub.fieldName);
3341
+ const clientId = attachment.awarenessClientIds[docKey];
3342
+ if (clientId !== void 0) broadcastAwarenessClientRemoval(ctx, docKey, clientId);
3343
+ }
3344
+ }
3345
+ function broadcastAwarenessClientRemoval(ctx, docKey, clientId) {
3270
3346
  const payloadEncoder = createEncoder();
3271
3347
  writeVarUint(payloadEncoder, 1);
3272
3348
  writeVarUint(payloadEncoder, clientId);
3273
3349
  writeVarUint(payloadEncoder, 16777215);
3274
3350
  writeVarUint8Array(payloadEncoder, new TextEncoder().encode("null"));
3275
3351
  const removalPayload = toUint8Array(payloadEncoder);
3276
- for (const sub of attachment.yjsSubscriptions) {
3277
- const docKey = getYjsDocKey(sub.collection, sub.recordId, sub.fieldName);
3278
- broadcastAwareness(ctx, docKey, removalPayload, null);
3279
- }
3352
+ broadcastAwareness(ctx, docKey, removalPayload, null);
3280
3353
  }
3281
3354
  function handleYjsDisconnect(ctx, ws, attachment) {
3282
3355
  broadcastAwarenessRemoval(ctx, attachment);
@@ -3553,6 +3626,7 @@ function getAllUserRecords(sql, schemaRegistry) {
3553
3626
  const r = row;
3554
3627
  return {
3555
3628
  recordId: r._row_id,
3629
+ createdBy: r._created_by,
3556
3630
  data: columns.length > 0 ? rowToUserRecord(r, columns) : {
3557
3631
  email: r.col_email || "",
3558
3632
  name: r.col_name || "",
@@ -3614,12 +3688,28 @@ async function registerUser(sql, userId, name, email, imageUrl, isAdmin, default
3614
3688
  );
3615
3689
  return { id: userId, email, name, imageUrl, role, createdAt: now, lastSeenAt: now };
3616
3690
  }
3617
- function handleUserList(ctx, ws, _attachment) {
3618
- const userRecords = getAllUserRecords(ctx.sql, ctx.schemaRegistry);
3619
- const users = userRecords.map((r) => ({
3620
- id: r.recordId,
3621
- ...r.data
3622
- }));
3691
+ function handleUserList(ctx, ws, attachment) {
3692
+ const schema = ctx.schemaRegistry.get("users");
3693
+ const permissionContext = ctx.getPermissionContext();
3694
+ const visibleRecords = attachment.userId.startsWith("anon-") || !schema ? [] : getAllUserRecords(ctx.sql, ctx.schemaRegistry).filter(
3695
+ (record) => canRead(
3696
+ schema,
3697
+ attachment.role,
3698
+ record,
3699
+ attachment.userId,
3700
+ permissionContext
3701
+ )
3702
+ );
3703
+ const users = visibleRecords.map((record) => {
3704
+ if (attachment.role === "admin") return { id: record.recordId, ...record.data };
3705
+ const { name, imageUrl, role } = record.data;
3706
+ return {
3707
+ id: record.recordId,
3708
+ name,
3709
+ ...imageUrl ? { imageUrl } : {},
3710
+ role
3711
+ };
3712
+ });
3623
3713
  ctx.send(ws, { type: MSG.USER_LIST, payload: { users } });
3624
3714
  }
3625
3715
  function handleUserUpdate(ctx, _ws, attachment, payload) {
@@ -3648,24 +3738,18 @@ async function handleSetRole(ctx, ws, attachment, payload) {
3648
3738
  now,
3649
3739
  payload.userId
3650
3740
  );
3651
- if (attachment.userId === payload.userId) {
3652
- attachment.role = payload.role;
3653
- ws.serializeAttachment(attachment);
3654
- }
3655
- const userRecords = getAllUserRecords(ctx.sql, ctx.schemaRegistry);
3656
- const users = userRecords.map((r) => ({
3657
- id: r.recordId,
3658
- ...r.data
3659
- }));
3660
3741
  for (const otherWs of ctx.state.getWebSockets()) {
3661
3742
  const otherAttachment = otherWs.deserializeAttachment();
3662
3743
  if (!otherAttachment) continue;
3663
- if (otherAttachment.userId === payload.userId && otherWs !== ws) {
3664
- otherAttachment.role = payload.role;
3665
- otherWs.serializeAttachment(otherAttachment);
3744
+ if (otherAttachment.userId === payload.userId) {
3745
+ try {
3746
+ otherWs.close(1008, "role-changed");
3747
+ } catch {
3748
+ }
3749
+ continue;
3666
3750
  }
3667
3751
  if (otherAttachment.role === "admin") {
3668
- ctx.send(otherWs, { type: MSG.USER_LIST, payload: { users } });
3752
+ handleUserList(ctx, otherWs, otherAttachment);
3669
3753
  }
3670
3754
  }
3671
3755
  }
@@ -4536,6 +4620,7 @@ var RecordRoom = class extends BaseRoom {
4536
4620
  sql: this.sql,
4537
4621
  state: this.state,
4538
4622
  schemaRegistry: this.schemaRegistry,
4623
+ getPermissionContext: () => this.getPermissionContext(),
4539
4624
  send: (ws, msg) => this.send(ws, msg)
4540
4625
  };
4541
4626
  }
@@ -5086,6 +5171,13 @@ var CanvasRoom = class extends BaseRoom {
5086
5171
  };
5087
5172
 
5088
5173
  // src/server/rooms/presence-room.ts
5174
+ var MAX_PRESENCE_STATE_BYTES = 16 * 1024;
5175
+ function isPlainObject(value) {
5176
+ return value !== null && typeof value === "object" && !Array.isArray(value);
5177
+ }
5178
+ function stateSize(state) {
5179
+ return new TextEncoder().encode(JSON.stringify(state)).byteLength;
5180
+ }
5089
5181
  var PresenceRoom = class extends BaseRoom {
5090
5182
  peers = /* @__PURE__ */ new Map();
5091
5183
  peerSockets = /* @__PURE__ */ new Map();
@@ -5107,8 +5199,6 @@ var PresenceRoom = class extends BaseRoom {
5107
5199
  const recovered = {
5108
5200
  userId: att.userId,
5109
5201
  userName: att.userName,
5110
- userEmail: att.userEmail,
5111
- userImageUrl: att.userImageUrl,
5112
5202
  joinedAt,
5113
5203
  state: {}
5114
5204
  };
@@ -5125,8 +5215,6 @@ var PresenceRoom = class extends BaseRoom {
5125
5215
  const peer = {
5126
5216
  userId: user.userId,
5127
5217
  userName: user.userName,
5128
- userEmail: user.userEmail,
5129
- userImageUrl: user.userImageUrl,
5130
5218
  joinedAt: now,
5131
5219
  state: {}
5132
5220
  };
@@ -5154,15 +5242,28 @@ var PresenceRoom = class extends BaseRoom {
5154
5242
  peer = {
5155
5243
  userId: user.userId,
5156
5244
  userName: user.userName,
5157
- userEmail: user.userEmail,
5158
- userImageUrl: user.userImageUrl,
5159
5245
  joinedAt,
5160
5246
  state: {}
5161
5247
  };
5162
5248
  this.peers.set(user.userId, peer);
5163
5249
  this.peerSockets.set(user.userId, ws);
5164
5250
  }
5165
- peer.state = { ...peer.state, ...payload };
5251
+ if (!isPlainObject(payload)) {
5252
+ this.sendTo(ws, {
5253
+ type: MSG.ERROR,
5254
+ payload: { error: "Presence state must be an object" }
5255
+ });
5256
+ return;
5257
+ }
5258
+ const nextState = { ...peer.state, ...payload };
5259
+ if (stateSize(nextState) > MAX_PRESENCE_STATE_BYTES) {
5260
+ this.sendTo(ws, {
5261
+ type: MSG.ERROR,
5262
+ payload: { error: "Presence state exceeds 16 KiB" }
5263
+ });
5264
+ return;
5265
+ }
5266
+ peer.state = nextState;
5166
5267
  this.peers.set(user.userId, peer);
5167
5268
  this.broadcast(
5168
5269
  {
@@ -5174,7 +5275,10 @@ var PresenceRoom = class extends BaseRoom {
5174
5275
  break;
5175
5276
  }
5176
5277
  default:
5177
- this.sendTo(ws, { type: MSG.ERROR, payload: { error: `Unknown presence message type: ${type}` } });
5278
+ this.sendTo(ws, {
5279
+ type: MSG.ERROR,
5280
+ payload: { error: `Unknown presence message type: ${type}` }
5281
+ });
5178
5282
  }
5179
5283
  }
5180
5284
  onDisconnect(ws, user) {
@@ -5618,6 +5722,9 @@ var JobRoom = class extends BaseRoom {
5618
5722
  retentionMs;
5619
5723
  snapshotLimit;
5620
5724
  retryBackoffMs;
5725
+ authorizeWrite;
5726
+ authorizeRead;
5727
+ broadcastQueue = Promise.resolve();
5621
5728
  /** In-flight jobs in this isolate; lets same-isolate JOB_CANCEL fire the signal. */
5622
5729
  inFlight = /* @__PURE__ */ new Map();
5623
5730
  /** Set by `ctx.continue` inside `executeJob`; reset before every run. */
@@ -5628,6 +5735,8 @@ var JobRoom = class extends BaseRoom {
5628
5735
  this.retentionMs = Math.max(0, config.retentionMs ?? DEFAULT_RETENTION_MS);
5629
5736
  this.snapshotLimit = Math.max(1, config.snapshotLimit ?? DEFAULT_SNAPSHOT_LIMIT);
5630
5737
  this.retryBackoffMs = Math.max(0, config.retryBackoffMs ?? 1e3);
5738
+ this.authorizeWrite = config.authorizeWrite ?? ((user) => user.role === ROLES.MEMBER || user.role === ROLES.ADMIN);
5739
+ this.authorizeRead = config.authorizeRead ?? this.authorizeWrite;
5631
5740
  }
5632
5741
  ensureInitialized() {
5633
5742
  if (this.initialized) return;
@@ -5668,14 +5777,31 @@ var JobRoom = class extends BaseRoom {
5668
5777
  this.ensureInitialized();
5669
5778
  return super.fetch(request);
5670
5779
  }
5671
- onConnect(ws, user) {
5780
+ async onConnect(ws, user) {
5672
5781
  this.ensureInitialized();
5673
- this.sendTo(ws, serverBuild.jobSnapshot(this.getRecentJobs(this.snapshotLimit)));
5674
- return user;
5782
+ const { canRead: canRead2, canWrite } = await this.resolveAccess(user);
5783
+ this.sendTo(ws, { type: MSG.AUTH, payload: { canWrite } });
5784
+ this.sendTo(ws, serverBuild.jobSnapshot(canRead2 ? this.getRecentJobs(this.snapshotLimit) : []));
5785
+ return { ...user, canWrite };
5675
5786
  }
5676
5787
  async onMessage(ws, user, message) {
5677
5788
  this.ensureInitialized();
5678
5789
  const { type, payload } = message;
5790
+ if (type === MSG.JOB_ENQUEUE || type === MSG.JOB_CANCEL || type === MSG.JOB_RETRY) {
5791
+ const attachment = user;
5792
+ const { canWrite } = await this.resolveAccess(user);
5793
+ if (attachment.canWrite !== canWrite) {
5794
+ attachment.canWrite = canWrite;
5795
+ ws.serializeAttachment(attachment);
5796
+ this.sendTo(ws, { type: MSG.AUTH, payload: { canWrite } });
5797
+ } else if (!canWrite) {
5798
+ this.sendTo(ws, { type: MSG.AUTH, payload: { canWrite: false } });
5799
+ }
5800
+ if (!canWrite) {
5801
+ this.sendTo(ws, serverBuild.error("Write access denied: job queue requires member access"));
5802
+ return;
5803
+ }
5804
+ }
5679
5805
  switch (type) {
5680
5806
  case MSG.JOB_ENQUEUE: {
5681
5807
  const requestId = String(payload.requestId ?? "");
@@ -6018,6 +6144,29 @@ var JobRoom = class extends BaseRoom {
6018
6144
  const row = this.sql.exec(`SELECT status FROM jobs WHERE id = ?`, jobId).toArray()[0];
6019
6145
  return row?.status ?? null;
6020
6146
  }
6147
+ /** Reauthorize every live event so role revocation does not leave a stale reader. */
6148
+ broadcast(message, exclude) {
6149
+ this.broadcastQueue = this.broadcastQueue.then(async () => {
6150
+ await Promise.all(
6151
+ this.getWebSockets().map(async (ws) => {
6152
+ if (ws === exclude) return;
6153
+ try {
6154
+ const user = this.getAttachment(ws);
6155
+ if (!user) return;
6156
+ if (await this.authorizeRead(user)) this.sendTo(ws, message);
6157
+ } catch {
6158
+ }
6159
+ })
6160
+ );
6161
+ });
6162
+ this.state.waitUntil(this.broadcastQueue);
6163
+ }
6164
+ async resolveAccess(user) {
6165
+ const canRead2 = await this.authorizeRead(user);
6166
+ if (!canRead2) return { canRead: false, canWrite: false };
6167
+ const canWrite = this.authorizeRead === this.authorizeWrite || await this.authorizeWrite(user);
6168
+ return { canRead: true, canWrite };
6169
+ }
6021
6170
  // ==========================================================================
6022
6171
  // Scheduling / recovery / pruning
6023
6172
  // ==========================================================================
@@ -7674,6 +7823,7 @@ export {
7674
7823
  capToolResultSize,
7675
7824
  captureScreenshot,
7676
7825
  checkFieldPermissions,
7826
+ checkUnclaimedOwnerTransition,
7677
7827
  coerceValue,
7678
7828
  collectionTableName,
7679
7829
  columnId,
@@ -7742,6 +7892,7 @@ export {
7742
7892
  registryStub,
7743
7893
  requireSubscription,
7744
7894
  resolveAppName,
7895
+ resolveAppRole,
7745
7896
  resolveColumn,
7746
7897
  rowToData,
7747
7898
  runMigrations,