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/CHANGELOG.md +47 -0
- package/README.md +21 -0
- package/dist/cli.js +889 -323
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +26 -12
- package/dist/index.js +100 -27
- package/dist/index.js.map +1 -1
- package/dist/schema.js.map +1 -1
- package/dist/server.d.ts +51 -15
- package/dist/server.js +212 -61
- 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 +69 -10
- package/dist/worker.js +251 -61
- package/dist/worker.js.map +1 -1
- package/features/admin-page/src/AdminPage.tsx +4 -4
- package/features/documentation/feature.json +2 -1
- package/features/documents/feature.json +3 -2
- package/features/documents/src/DocumentsEditorPage.tsx +47 -62
- package/features/documents/src/DocumentsListPage.tsx +59 -26
- package/features/documents/src/InviteDialog.tsx +49 -14
- package/features/documents/src/LibrarySidebar.tsx +92 -84
- package/features/documents/src/__tests__/documents-presence-access.test.ts +15 -2
- package/features/documents/src/documents-library-types.ts +6 -0
- package/features/documents/src/editor/useDocEditor.ts +1 -1
- package/features/documents/src/use-documents-presence-access.ts +1 -3
- package/features/tasks/src/TasksPage.tsx +6 -3
- package/package.json +5 -5
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 };
|
|
@@ -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,
|
|
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.
|
|
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.
|
|
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
|
-
|
|
3269
|
-
|
|
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
|
-
|
|
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,
|
|
3618
|
-
const
|
|
3619
|
-
const
|
|
3620
|
-
|
|
3621
|
-
|
|
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
|
|
3664
|
-
|
|
3665
|
-
|
|
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
|
|
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
|
-
|
|
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, {
|
|
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
|
-
|
|
5674
|
-
|
|
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,
|