deepspace 0.17.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +169 -1
- package/README.md +39 -41
- package/dist/cli.js +1585 -1403
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +18 -8
- package/dist/index.js +148 -25
- package/dist/index.js.map +1 -1
- package/dist/schema.js.map +1 -1
- package/dist/server.d.ts +40 -9
- package/dist/server.js +151 -44
- package/dist/server.js.map +1 -1
- package/dist/testing.d.ts +2 -2
- package/dist/testing.js +28 -25
- package/dist/testing.js.map +1 -1
- package/dist/worker.d.ts +61 -9
- package/dist/worker.js +190 -44
- package/dist/worker.js.map +1 -1
- package/features/documentation/feature.json +2 -1
- package/features/documents/feature.json +1 -1
- package/features/documents/src/DocumentsEditorPage.tsx +16 -52
- package/features/documents/src/LibrarySidebar.tsx +6 -8
- package/features/documents/src/editor/useDocEditor.ts +4 -9
- package/features/documents/src/use-documents-presence-access.ts +1 -3
- package/features/landing/feature.json +1 -1
- package/features/messaging/src/components/chat/MessageItem.tsx +83 -34
- package/features/messaging/src/hooks/useChatChannel.ts +2 -8
- package/features/tasks/src/TasksPage.tsx +6 -3
- package/package.json +2 -2
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(
|
|
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
|
|
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.
|
|
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 };
|
|
@@ -2910,6 +2964,10 @@ function putRecord(ctx, collection, recordId, data, userId, userRole, skipUserRb
|
|
|
2910
2964
|
};
|
|
2911
2965
|
}
|
|
2912
2966
|
}
|
|
2967
|
+
const ownerError = checkUnclaimedOwnerTransition(schema, userRole, finalData, userId);
|
|
2968
|
+
if (ownerError) {
|
|
2969
|
+
return { success: false, error: `FIELD ERROR: ${ownerError}` };
|
|
2970
|
+
}
|
|
2913
2971
|
}
|
|
2914
2972
|
const colValues = dataToColumnValues(finalData, columns);
|
|
2915
2973
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -3162,11 +3220,17 @@ function handleYjsJoin(ctx, ws, attachment, payload) {
|
|
|
3162
3220
|
}
|
|
3163
3221
|
function handleYjsLeave(ctx, ws, attachment, payload) {
|
|
3164
3222
|
const { collection, recordId, fieldName } = payload;
|
|
3223
|
+
const docKey = getYjsDocKey(collection, recordId, fieldName);
|
|
3224
|
+
const clientId = attachment.awarenessClientIds?.[docKey];
|
|
3225
|
+
if (clientId !== void 0) {
|
|
3226
|
+
broadcastAwarenessClientRemoval(ctx, docKey, clientId);
|
|
3227
|
+
delete attachment.awarenessClientIds[docKey];
|
|
3228
|
+
}
|
|
3165
3229
|
attachment.yjsSubscriptions = attachment.yjsSubscriptions.filter(
|
|
3166
3230
|
(s) => !(s.collection === collection && s.recordId === recordId && s.fieldName === fieldName)
|
|
3167
3231
|
);
|
|
3168
3232
|
ws.serializeAttachment(attachment);
|
|
3169
|
-
unloadYjsDocIfUnused(ctx,
|
|
3233
|
+
unloadYjsDocIfUnused(ctx, docKey, ws);
|
|
3170
3234
|
}
|
|
3171
3235
|
function handleYjsBinaryMessage(ctx, ws, attachment, data) {
|
|
3172
3236
|
const decoder = createDecoder(data);
|
|
@@ -3178,12 +3242,13 @@ function handleYjsBinaryMessage(ctx, ws, attachment, data) {
|
|
|
3178
3242
|
const parsed2 = parseYjsDocKey(docKey2);
|
|
3179
3243
|
if (!parsed2) return;
|
|
3180
3244
|
if (!hasYjsSubscription(attachment, parsed2)) return;
|
|
3181
|
-
if (attachment.
|
|
3245
|
+
if (attachment.awarenessClientIds?.[docKey2] === void 0) {
|
|
3182
3246
|
try {
|
|
3183
3247
|
const pd = createDecoder(payload);
|
|
3184
3248
|
const count = readVarUint(pd);
|
|
3185
3249
|
if (count > 0) {
|
|
3186
|
-
attachment.
|
|
3250
|
+
attachment.awarenessClientIds ??= {};
|
|
3251
|
+
attachment.awarenessClientIds[docKey2] = readVarUint(pd);
|
|
3187
3252
|
ws.serializeAttachment(attachment);
|
|
3188
3253
|
}
|
|
3189
3254
|
} catch {
|
|
@@ -3265,18 +3330,21 @@ function broadcastAwareness(ctx, docKey, payload, excludeWs) {
|
|
|
3265
3330
|
}
|
|
3266
3331
|
}
|
|
3267
3332
|
function broadcastAwarenessRemoval(ctx, attachment) {
|
|
3268
|
-
|
|
3269
|
-
|
|
3333
|
+
if (!attachment.awarenessClientIds) return;
|
|
3334
|
+
for (const sub of attachment.yjsSubscriptions) {
|
|
3335
|
+
const docKey = getYjsDocKey(sub.collection, sub.recordId, sub.fieldName);
|
|
3336
|
+
const clientId = attachment.awarenessClientIds[docKey];
|
|
3337
|
+
if (clientId !== void 0) broadcastAwarenessClientRemoval(ctx, docKey, clientId);
|
|
3338
|
+
}
|
|
3339
|
+
}
|
|
3340
|
+
function broadcastAwarenessClientRemoval(ctx, docKey, clientId) {
|
|
3270
3341
|
const payloadEncoder = createEncoder();
|
|
3271
3342
|
writeVarUint(payloadEncoder, 1);
|
|
3272
3343
|
writeVarUint(payloadEncoder, clientId);
|
|
3273
3344
|
writeVarUint(payloadEncoder, 16777215);
|
|
3274
3345
|
writeVarUint8Array(payloadEncoder, new TextEncoder().encode("null"));
|
|
3275
3346
|
const removalPayload = toUint8Array(payloadEncoder);
|
|
3276
|
-
|
|
3277
|
-
const docKey = getYjsDocKey(sub.collection, sub.recordId, sub.fieldName);
|
|
3278
|
-
broadcastAwareness(ctx, docKey, removalPayload, null);
|
|
3279
|
-
}
|
|
3347
|
+
broadcastAwareness(ctx, docKey, removalPayload, null);
|
|
3280
3348
|
}
|
|
3281
3349
|
function handleYjsDisconnect(ctx, ws, attachment) {
|
|
3282
3350
|
broadcastAwarenessRemoval(ctx, attachment);
|
|
@@ -3648,10 +3716,6 @@ async function handleSetRole(ctx, ws, attachment, payload) {
|
|
|
3648
3716
|
now,
|
|
3649
3717
|
payload.userId
|
|
3650
3718
|
);
|
|
3651
|
-
if (attachment.userId === payload.userId) {
|
|
3652
|
-
attachment.role = payload.role;
|
|
3653
|
-
ws.serializeAttachment(attachment);
|
|
3654
|
-
}
|
|
3655
3719
|
const userRecords = getAllUserRecords(ctx.sql, ctx.schemaRegistry);
|
|
3656
3720
|
const users = userRecords.map((r) => ({
|
|
3657
3721
|
id: r.recordId,
|
|
@@ -3660,9 +3724,12 @@ async function handleSetRole(ctx, ws, attachment, payload) {
|
|
|
3660
3724
|
for (const otherWs of ctx.state.getWebSockets()) {
|
|
3661
3725
|
const otherAttachment = otherWs.deserializeAttachment();
|
|
3662
3726
|
if (!otherAttachment) continue;
|
|
3663
|
-
if (otherAttachment.userId === payload.userId
|
|
3664
|
-
|
|
3665
|
-
|
|
3727
|
+
if (otherAttachment.userId === payload.userId) {
|
|
3728
|
+
try {
|
|
3729
|
+
otherWs.close(1008, "role-changed");
|
|
3730
|
+
} catch {
|
|
3731
|
+
}
|
|
3732
|
+
continue;
|
|
3666
3733
|
}
|
|
3667
3734
|
if (otherAttachment.role === "admin") {
|
|
3668
3735
|
ctx.send(otherWs, { type: MSG.USER_LIST, payload: { users } });
|
|
@@ -5086,6 +5153,13 @@ var CanvasRoom = class extends BaseRoom {
|
|
|
5086
5153
|
};
|
|
5087
5154
|
|
|
5088
5155
|
// src/server/rooms/presence-room.ts
|
|
5156
|
+
var MAX_PRESENCE_STATE_BYTES = 16 * 1024;
|
|
5157
|
+
function isPlainObject(value) {
|
|
5158
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
5159
|
+
}
|
|
5160
|
+
function stateSize(state) {
|
|
5161
|
+
return new TextEncoder().encode(JSON.stringify(state)).byteLength;
|
|
5162
|
+
}
|
|
5089
5163
|
var PresenceRoom = class extends BaseRoom {
|
|
5090
5164
|
peers = /* @__PURE__ */ new Map();
|
|
5091
5165
|
peerSockets = /* @__PURE__ */ new Map();
|
|
@@ -5107,8 +5181,6 @@ var PresenceRoom = class extends BaseRoom {
|
|
|
5107
5181
|
const recovered = {
|
|
5108
5182
|
userId: att.userId,
|
|
5109
5183
|
userName: att.userName,
|
|
5110
|
-
userEmail: att.userEmail,
|
|
5111
|
-
userImageUrl: att.userImageUrl,
|
|
5112
5184
|
joinedAt,
|
|
5113
5185
|
state: {}
|
|
5114
5186
|
};
|
|
@@ -5125,8 +5197,6 @@ var PresenceRoom = class extends BaseRoom {
|
|
|
5125
5197
|
const peer = {
|
|
5126
5198
|
userId: user.userId,
|
|
5127
5199
|
userName: user.userName,
|
|
5128
|
-
userEmail: user.userEmail,
|
|
5129
|
-
userImageUrl: user.userImageUrl,
|
|
5130
5200
|
joinedAt: now,
|
|
5131
5201
|
state: {}
|
|
5132
5202
|
};
|
|
@@ -5154,15 +5224,28 @@ var PresenceRoom = class extends BaseRoom {
|
|
|
5154
5224
|
peer = {
|
|
5155
5225
|
userId: user.userId,
|
|
5156
5226
|
userName: user.userName,
|
|
5157
|
-
userEmail: user.userEmail,
|
|
5158
|
-
userImageUrl: user.userImageUrl,
|
|
5159
5227
|
joinedAt,
|
|
5160
5228
|
state: {}
|
|
5161
5229
|
};
|
|
5162
5230
|
this.peers.set(user.userId, peer);
|
|
5163
5231
|
this.peerSockets.set(user.userId, ws);
|
|
5164
5232
|
}
|
|
5165
|
-
|
|
5233
|
+
if (!isPlainObject(payload)) {
|
|
5234
|
+
this.sendTo(ws, {
|
|
5235
|
+
type: MSG.ERROR,
|
|
5236
|
+
payload: { error: "Presence state must be an object" }
|
|
5237
|
+
});
|
|
5238
|
+
return;
|
|
5239
|
+
}
|
|
5240
|
+
const nextState = { ...peer.state, ...payload };
|
|
5241
|
+
if (stateSize(nextState) > MAX_PRESENCE_STATE_BYTES) {
|
|
5242
|
+
this.sendTo(ws, {
|
|
5243
|
+
type: MSG.ERROR,
|
|
5244
|
+
payload: { error: "Presence state exceeds 16 KiB" }
|
|
5245
|
+
});
|
|
5246
|
+
return;
|
|
5247
|
+
}
|
|
5248
|
+
peer.state = nextState;
|
|
5166
5249
|
this.peers.set(user.userId, peer);
|
|
5167
5250
|
this.broadcast(
|
|
5168
5251
|
{
|
|
@@ -5174,7 +5257,10 @@ var PresenceRoom = class extends BaseRoom {
|
|
|
5174
5257
|
break;
|
|
5175
5258
|
}
|
|
5176
5259
|
default:
|
|
5177
|
-
this.sendTo(ws, {
|
|
5260
|
+
this.sendTo(ws, {
|
|
5261
|
+
type: MSG.ERROR,
|
|
5262
|
+
payload: { error: `Unknown presence message type: ${type}` }
|
|
5263
|
+
});
|
|
5178
5264
|
}
|
|
5179
5265
|
}
|
|
5180
5266
|
onDisconnect(ws, user) {
|
|
@@ -5618,6 +5704,7 @@ var JobRoom = class extends BaseRoom {
|
|
|
5618
5704
|
retentionMs;
|
|
5619
5705
|
snapshotLimit;
|
|
5620
5706
|
retryBackoffMs;
|
|
5707
|
+
authorizeWrite;
|
|
5621
5708
|
/** In-flight jobs in this isolate; lets same-isolate JOB_CANCEL fire the signal. */
|
|
5622
5709
|
inFlight = /* @__PURE__ */ new Map();
|
|
5623
5710
|
/** Set by `ctx.continue` inside `executeJob`; reset before every run. */
|
|
@@ -5628,6 +5715,7 @@ var JobRoom = class extends BaseRoom {
|
|
|
5628
5715
|
this.retentionMs = Math.max(0, config.retentionMs ?? DEFAULT_RETENTION_MS);
|
|
5629
5716
|
this.snapshotLimit = Math.max(1, config.snapshotLimit ?? DEFAULT_SNAPSHOT_LIMIT);
|
|
5630
5717
|
this.retryBackoffMs = Math.max(0, config.retryBackoffMs ?? 1e3);
|
|
5718
|
+
this.authorizeWrite = config.authorizeWrite ?? ((user) => user.role === ROLES.MEMBER || user.role === ROLES.ADMIN);
|
|
5631
5719
|
}
|
|
5632
5720
|
ensureInitialized() {
|
|
5633
5721
|
if (this.initialized) return;
|
|
@@ -5668,14 +5756,31 @@ var JobRoom = class extends BaseRoom {
|
|
|
5668
5756
|
this.ensureInitialized();
|
|
5669
5757
|
return super.fetch(request);
|
|
5670
5758
|
}
|
|
5671
|
-
onConnect(ws, user) {
|
|
5759
|
+
async onConnect(ws, user) {
|
|
5672
5760
|
this.ensureInitialized();
|
|
5761
|
+
const canWrite = await this.authorizeWrite(user);
|
|
5762
|
+
this.sendTo(ws, { type: MSG.AUTH, payload: { canWrite } });
|
|
5673
5763
|
this.sendTo(ws, serverBuild.jobSnapshot(this.getRecentJobs(this.snapshotLimit)));
|
|
5674
|
-
return user;
|
|
5764
|
+
return { ...user, canWrite };
|
|
5675
5765
|
}
|
|
5676
5766
|
async onMessage(ws, user, message) {
|
|
5677
5767
|
this.ensureInitialized();
|
|
5678
5768
|
const { type, payload } = message;
|
|
5769
|
+
if (type === MSG.JOB_ENQUEUE || type === MSG.JOB_CANCEL || type === MSG.JOB_RETRY) {
|
|
5770
|
+
const attachment = user;
|
|
5771
|
+
const canWrite = await this.authorizeWrite(user);
|
|
5772
|
+
if (attachment.canWrite !== canWrite) {
|
|
5773
|
+
attachment.canWrite = canWrite;
|
|
5774
|
+
ws.serializeAttachment(attachment);
|
|
5775
|
+
this.sendTo(ws, { type: MSG.AUTH, payload: { canWrite } });
|
|
5776
|
+
} else if (!canWrite) {
|
|
5777
|
+
this.sendTo(ws, { type: MSG.AUTH, payload: { canWrite: false } });
|
|
5778
|
+
}
|
|
5779
|
+
if (!canWrite) {
|
|
5780
|
+
this.sendTo(ws, serverBuild.error("Write access denied: job queue requires member access"));
|
|
5781
|
+
return;
|
|
5782
|
+
}
|
|
5783
|
+
}
|
|
5679
5784
|
switch (type) {
|
|
5680
5785
|
case MSG.JOB_ENQUEUE: {
|
|
5681
5786
|
const requestId = String(payload.requestId ?? "");
|
|
@@ -7674,6 +7779,7 @@ export {
|
|
|
7674
7779
|
capToolResultSize,
|
|
7675
7780
|
captureScreenshot,
|
|
7676
7781
|
checkFieldPermissions,
|
|
7782
|
+
checkUnclaimedOwnerTransition,
|
|
7677
7783
|
coerceValue,
|
|
7678
7784
|
collectionTableName,
|
|
7679
7785
|
columnId,
|
|
@@ -7742,6 +7848,7 @@ export {
|
|
|
7742
7848
|
registryStub,
|
|
7743
7849
|
requireSubscription,
|
|
7744
7850
|
resolveAppName,
|
|
7851
|
+
resolveAppRole,
|
|
7745
7852
|
resolveColumn,
|
|
7746
7853
|
rowToData,
|
|
7747
7854
|
runMigrations,
|