deepspace 0.18.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 +24 -0
- package/README.md +21 -0
- package/dist/cli.js +416 -83
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +18 -8
- package/dist/index.js +93 -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/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 +1 -1
package/dist/worker.js
CHANGED
|
@@ -1,4 +1,63 @@
|
|
|
1
|
+
// src/shared/room-identity-headers.ts
|
|
2
|
+
var BASE64URL = /^[A-Za-z0-9_-]*$/;
|
|
3
|
+
var IDENTITY_QUERY_PARAMS = ["userId", "userName", "userEmail", "userImageUrl", "role"];
|
|
4
|
+
var IDENTITY_HEADERS = [
|
|
5
|
+
"x-user-id",
|
|
6
|
+
"x-user-name",
|
|
7
|
+
"x-user-email",
|
|
8
|
+
"x-user-image-url",
|
|
9
|
+
"x-user-role"
|
|
10
|
+
];
|
|
11
|
+
function encodeRoomIdentityHeader(value) {
|
|
12
|
+
const bytes = new TextEncoder().encode(value);
|
|
13
|
+
let binary = "";
|
|
14
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
15
|
+
return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
|
|
16
|
+
}
|
|
17
|
+
function decodeRoomIdentityHeader(value) {
|
|
18
|
+
if (value === null || !BASE64URL.test(value) || value.length % 4 === 1) return void 0;
|
|
19
|
+
const base64 = value.replaceAll("-", "+").replaceAll("_", "/").padEnd(value.length + (4 - value.length % 4) % 4, "=");
|
|
20
|
+
try {
|
|
21
|
+
const binary = atob(base64);
|
|
22
|
+
const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
|
|
23
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
24
|
+
} catch {
|
|
25
|
+
return void 0;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function authenticatedRoomRequest(request, auth, identity) {
|
|
29
|
+
const url = new URL(request.url);
|
|
30
|
+
url.searchParams.delete("token");
|
|
31
|
+
for (const key of IDENTITY_QUERY_PARAMS) url.searchParams.delete(key);
|
|
32
|
+
const forwarded = new Request(url.toString(), request);
|
|
33
|
+
for (const header of IDENTITY_HEADERS) forwarded.headers.delete(header);
|
|
34
|
+
if (!auth) return forwarded;
|
|
35
|
+
forwarded.headers.set("x-user-id", encodeRoomIdentityHeader(auth.userId));
|
|
36
|
+
if (auth.claims.name) {
|
|
37
|
+
forwarded.headers.set("x-user-name", encodeRoomIdentityHeader(auth.claims.name));
|
|
38
|
+
}
|
|
39
|
+
if (auth.claims.email) {
|
|
40
|
+
forwarded.headers.set("x-user-email", encodeRoomIdentityHeader(auth.claims.email));
|
|
41
|
+
}
|
|
42
|
+
if (auth.claims.image) {
|
|
43
|
+
forwarded.headers.set("x-user-image-url", encodeRoomIdentityHeader(auth.claims.image));
|
|
44
|
+
}
|
|
45
|
+
if (identity?.role) {
|
|
46
|
+
forwarded.headers.set("x-user-role", encodeRoomIdentityHeader(identity.role));
|
|
47
|
+
}
|
|
48
|
+
return forwarded;
|
|
49
|
+
}
|
|
50
|
+
|
|
1
51
|
// src/server/rooms/base-room.ts
|
|
52
|
+
function connectionAttachmentFromRequest(request) {
|
|
53
|
+
return {
|
|
54
|
+
userId: decodeRoomIdentityHeader(request.headers.get("x-user-id")) || `anon-${crypto.randomUUID()}`,
|
|
55
|
+
userName: decodeRoomIdentityHeader(request.headers.get("x-user-name")) || "Anonymous",
|
|
56
|
+
userEmail: decodeRoomIdentityHeader(request.headers.get("x-user-email")) || "",
|
|
57
|
+
userImageUrl: decodeRoomIdentityHeader(request.headers.get("x-user-image-url")),
|
|
58
|
+
role: decodeRoomIdentityHeader(request.headers.get("x-user-role"))
|
|
59
|
+
};
|
|
60
|
+
}
|
|
2
61
|
var BaseRoom = class {
|
|
3
62
|
state;
|
|
4
63
|
env;
|
|
@@ -7,9 +66,7 @@ var BaseRoom = class {
|
|
|
7
66
|
this.state = state;
|
|
8
67
|
this.env = env ?? {};
|
|
9
68
|
this.sql = state.storage.sql;
|
|
10
|
-
this.state.setWebSocketAutoResponse(
|
|
11
|
-
new WebSocketRequestResponsePair("ping", "pong")
|
|
12
|
-
);
|
|
69
|
+
this.state.setWebSocketAutoResponse(new WebSocketRequestResponsePair("ping", "pong"));
|
|
13
70
|
}
|
|
14
71
|
// ==========================================================================
|
|
15
72
|
// HTTP Entry Point
|
|
@@ -59,22 +116,11 @@ var BaseRoom = class {
|
|
|
59
116
|
// ==========================================================================
|
|
60
117
|
// WebSocket Connection
|
|
61
118
|
// ==========================================================================
|
|
62
|
-
async handleWebSocketUpgrade(
|
|
63
|
-
const userId = url.searchParams.get("userId") || void 0;
|
|
64
|
-
const userName = url.searchParams.get("userName") || "Anonymous";
|
|
65
|
-
const userEmail = url.searchParams.get("userEmail") || "";
|
|
66
|
-
const userImageUrl = url.searchParams.get("userImageUrl") || void 0;
|
|
119
|
+
async handleWebSocketUpgrade(request, _url) {
|
|
67
120
|
const pair = new WebSocketPair();
|
|
68
121
|
const [client, server] = Object.values(pair);
|
|
69
122
|
this.state.acceptWebSocket(server);
|
|
70
|
-
const
|
|
71
|
-
const attachment = {
|
|
72
|
-
userId: userId ?? `anon-${crypto.randomUUID()}`,
|
|
73
|
-
userName,
|
|
74
|
-
userEmail,
|
|
75
|
-
userImageUrl,
|
|
76
|
-
role
|
|
77
|
-
};
|
|
123
|
+
const attachment = connectionAttachmentFromRequest(request);
|
|
78
124
|
const augmented = await this.onConnect(server, attachment) ?? attachment;
|
|
79
125
|
server.serializeAttachment(augmented);
|
|
80
126
|
return new Response(null, { status: 101, webSocket: client });
|
|
@@ -374,7 +420,7 @@ function dataToColumnValues(data, columns) {
|
|
|
374
420
|
const values = {};
|
|
375
421
|
for (const col of columns) {
|
|
376
422
|
if (col.expression || col.readonly) continue;
|
|
377
|
-
const val = data[col.
|
|
423
|
+
const val = data[col.name];
|
|
378
424
|
if (val !== void 0) {
|
|
379
425
|
values[col.id] = coerceValue(val, col.storage, col.interpretation);
|
|
380
426
|
}
|
|
@@ -523,6 +569,15 @@ function checkFieldPermissions(schema, role, newData, existingData) {
|
|
|
523
569
|
}
|
|
524
570
|
return null;
|
|
525
571
|
}
|
|
572
|
+
function checkUnclaimedOwnerTransition(schema, role, nextData, userId) {
|
|
573
|
+
const permissions = getRolePermissions(schema, role);
|
|
574
|
+
if (permissions.update !== "unclaimed-or-own" || !schema.ownerField) return null;
|
|
575
|
+
const nextOwner = nextData[schema.ownerField];
|
|
576
|
+
if (nextOwner === void 0 || nextOwner === null || nextOwner === "" || nextOwner === userId) {
|
|
577
|
+
return null;
|
|
578
|
+
}
|
|
579
|
+
return `Role '${role}' can set '${schema.ownerField}' only to their own user id or leave it unclaimed`;
|
|
580
|
+
}
|
|
526
581
|
var SYSTEM_MANAGED_COLUMNS = /* @__PURE__ */ new Set([
|
|
527
582
|
"email",
|
|
528
583
|
"name",
|
|
@@ -566,7 +621,7 @@ function lintSchema(schema) {
|
|
|
566
621
|
if (schema.ownerField) {
|
|
567
622
|
const col = schema.columns.find((c) => c.name === schema.ownerField);
|
|
568
623
|
const spoofableRoles = Object.entries(schema.permissions).filter(
|
|
569
|
-
([role, permissions]) => role !== "owner" && role !== "admin" && permissions.create === true
|
|
624
|
+
([role, permissions]) => role !== "owner" && role !== "admin" && permissions.create === true && permissions.update !== "unclaimed-or-own"
|
|
570
625
|
).map(([role]) => role);
|
|
571
626
|
if (col && !col.userBound && spoofableRoles.length > 0) {
|
|
572
627
|
warnings.push(
|
|
@@ -1193,6 +1248,14 @@ function putRecord(ctx, collection, recordId, data, userId, userRole, skipUserRb
|
|
|
1193
1248
|
return { success: false, error: `Schema not registered for collection: ${collection}` };
|
|
1194
1249
|
}
|
|
1195
1250
|
const columns = getResolvedColumns(schema);
|
|
1251
|
+
for (const col of columns) {
|
|
1252
|
+
if (col.id !== col.name && Object.hasOwn(data, col.id)) {
|
|
1253
|
+
return {
|
|
1254
|
+
success: false,
|
|
1255
|
+
error: `FIELD ERROR: physical column id '${col.id}' is not a writable field; use '${col.name}'`
|
|
1256
|
+
};
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1196
1259
|
const existing = getRecord(ctx.sql, collection, recordId, schema);
|
|
1197
1260
|
const isUpdate = existing !== null;
|
|
1198
1261
|
const mergedData = isUpdate ? { ...existing.data, ...data } : { ...data };
|
|
@@ -1267,6 +1330,10 @@ function putRecord(ctx, collection, recordId, data, userId, userRole, skipUserRb
|
|
|
1267
1330
|
};
|
|
1268
1331
|
}
|
|
1269
1332
|
}
|
|
1333
|
+
const ownerError = checkUnclaimedOwnerTransition(schema, userRole, finalData, userId);
|
|
1334
|
+
if (ownerError) {
|
|
1335
|
+
return { success: false, error: `FIELD ERROR: ${ownerError}` };
|
|
1336
|
+
}
|
|
1270
1337
|
}
|
|
1271
1338
|
const colValues = dataToColumnValues(finalData, columns);
|
|
1272
1339
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -1519,11 +1586,17 @@ function handleYjsJoin(ctx, ws, attachment, payload) {
|
|
|
1519
1586
|
}
|
|
1520
1587
|
function handleYjsLeave(ctx, ws, attachment, payload) {
|
|
1521
1588
|
const { collection, recordId, fieldName } = payload;
|
|
1589
|
+
const docKey = getYjsDocKey(collection, recordId, fieldName);
|
|
1590
|
+
const clientId = attachment.awarenessClientIds?.[docKey];
|
|
1591
|
+
if (clientId !== void 0) {
|
|
1592
|
+
broadcastAwarenessClientRemoval(ctx, docKey, clientId);
|
|
1593
|
+
delete attachment.awarenessClientIds[docKey];
|
|
1594
|
+
}
|
|
1522
1595
|
attachment.yjsSubscriptions = attachment.yjsSubscriptions.filter(
|
|
1523
1596
|
(s) => !(s.collection === collection && s.recordId === recordId && s.fieldName === fieldName)
|
|
1524
1597
|
);
|
|
1525
1598
|
ws.serializeAttachment(attachment);
|
|
1526
|
-
unloadYjsDocIfUnused(ctx,
|
|
1599
|
+
unloadYjsDocIfUnused(ctx, docKey, ws);
|
|
1527
1600
|
}
|
|
1528
1601
|
function handleYjsBinaryMessage(ctx, ws, attachment, data) {
|
|
1529
1602
|
const decoder = createDecoder(data);
|
|
@@ -1535,12 +1608,13 @@ function handleYjsBinaryMessage(ctx, ws, attachment, data) {
|
|
|
1535
1608
|
const parsed2 = parseYjsDocKey(docKey2);
|
|
1536
1609
|
if (!parsed2) return;
|
|
1537
1610
|
if (!hasYjsSubscription(attachment, parsed2)) return;
|
|
1538
|
-
if (attachment.
|
|
1611
|
+
if (attachment.awarenessClientIds?.[docKey2] === void 0) {
|
|
1539
1612
|
try {
|
|
1540
1613
|
const pd = createDecoder(payload);
|
|
1541
1614
|
const count = readVarUint(pd);
|
|
1542
1615
|
if (count > 0) {
|
|
1543
|
-
attachment.
|
|
1616
|
+
attachment.awarenessClientIds ??= {};
|
|
1617
|
+
attachment.awarenessClientIds[docKey2] = readVarUint(pd);
|
|
1544
1618
|
ws.serializeAttachment(attachment);
|
|
1545
1619
|
}
|
|
1546
1620
|
} catch {
|
|
@@ -1622,18 +1696,21 @@ function broadcastAwareness(ctx, docKey, payload, excludeWs) {
|
|
|
1622
1696
|
}
|
|
1623
1697
|
}
|
|
1624
1698
|
function broadcastAwarenessRemoval(ctx, attachment) {
|
|
1625
|
-
|
|
1626
|
-
|
|
1699
|
+
if (!attachment.awarenessClientIds) return;
|
|
1700
|
+
for (const sub of attachment.yjsSubscriptions) {
|
|
1701
|
+
const docKey = getYjsDocKey(sub.collection, sub.recordId, sub.fieldName);
|
|
1702
|
+
const clientId = attachment.awarenessClientIds[docKey];
|
|
1703
|
+
if (clientId !== void 0) broadcastAwarenessClientRemoval(ctx, docKey, clientId);
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
function broadcastAwarenessClientRemoval(ctx, docKey, clientId) {
|
|
1627
1707
|
const payloadEncoder = createEncoder();
|
|
1628
1708
|
writeVarUint(payloadEncoder, 1);
|
|
1629
1709
|
writeVarUint(payloadEncoder, clientId);
|
|
1630
1710
|
writeVarUint(payloadEncoder, 16777215);
|
|
1631
1711
|
writeVarUint8Array(payloadEncoder, new TextEncoder().encode("null"));
|
|
1632
1712
|
const removalPayload = toUint8Array(payloadEncoder);
|
|
1633
|
-
|
|
1634
|
-
const docKey = getYjsDocKey(sub.collection, sub.recordId, sub.fieldName);
|
|
1635
|
-
broadcastAwareness(ctx, docKey, removalPayload, null);
|
|
1636
|
-
}
|
|
1713
|
+
broadcastAwareness(ctx, docKey, removalPayload, null);
|
|
1637
1714
|
}
|
|
1638
1715
|
function handleYjsDisconnect(ctx, ws, attachment) {
|
|
1639
1716
|
broadcastAwarenessRemoval(ctx, attachment);
|
|
@@ -2005,10 +2082,6 @@ async function handleSetRole(ctx, ws, attachment, payload) {
|
|
|
2005
2082
|
now,
|
|
2006
2083
|
payload.userId
|
|
2007
2084
|
);
|
|
2008
|
-
if (attachment.userId === payload.userId) {
|
|
2009
|
-
attachment.role = payload.role;
|
|
2010
|
-
ws.serializeAttachment(attachment);
|
|
2011
|
-
}
|
|
2012
2085
|
const userRecords = getAllUserRecords(ctx.sql, ctx.schemaRegistry);
|
|
2013
2086
|
const users = userRecords.map((r) => ({
|
|
2014
2087
|
id: r.recordId,
|
|
@@ -2017,9 +2090,12 @@ async function handleSetRole(ctx, ws, attachment, payload) {
|
|
|
2017
2090
|
for (const otherWs of ctx.state.getWebSockets()) {
|
|
2018
2091
|
const otherAttachment = otherWs.deserializeAttachment();
|
|
2019
2092
|
if (!otherAttachment) continue;
|
|
2020
|
-
if (otherAttachment.userId === payload.userId
|
|
2021
|
-
|
|
2022
|
-
|
|
2093
|
+
if (otherAttachment.userId === payload.userId) {
|
|
2094
|
+
try {
|
|
2095
|
+
otherWs.close(1008, "role-changed");
|
|
2096
|
+
} catch {
|
|
2097
|
+
}
|
|
2098
|
+
continue;
|
|
2023
2099
|
}
|
|
2024
2100
|
if (otherAttachment.role === "admin") {
|
|
2025
2101
|
ctx.send(otherWs, { type: MSG.USER_LIST, payload: { users } });
|
|
@@ -3551,6 +3627,13 @@ var CanvasRoom = class extends BaseRoom {
|
|
|
3551
3627
|
};
|
|
3552
3628
|
|
|
3553
3629
|
// src/server/rooms/presence-room.ts
|
|
3630
|
+
var MAX_PRESENCE_STATE_BYTES = 16 * 1024;
|
|
3631
|
+
function isPlainObject(value) {
|
|
3632
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
3633
|
+
}
|
|
3634
|
+
function stateSize(state) {
|
|
3635
|
+
return new TextEncoder().encode(JSON.stringify(state)).byteLength;
|
|
3636
|
+
}
|
|
3554
3637
|
var PresenceRoom = class extends BaseRoom {
|
|
3555
3638
|
peers = /* @__PURE__ */ new Map();
|
|
3556
3639
|
peerSockets = /* @__PURE__ */ new Map();
|
|
@@ -3572,8 +3655,6 @@ var PresenceRoom = class extends BaseRoom {
|
|
|
3572
3655
|
const recovered = {
|
|
3573
3656
|
userId: att.userId,
|
|
3574
3657
|
userName: att.userName,
|
|
3575
|
-
userEmail: att.userEmail,
|
|
3576
|
-
userImageUrl: att.userImageUrl,
|
|
3577
3658
|
joinedAt,
|
|
3578
3659
|
state: {}
|
|
3579
3660
|
};
|
|
@@ -3590,8 +3671,6 @@ var PresenceRoom = class extends BaseRoom {
|
|
|
3590
3671
|
const peer = {
|
|
3591
3672
|
userId: user.userId,
|
|
3592
3673
|
userName: user.userName,
|
|
3593
|
-
userEmail: user.userEmail,
|
|
3594
|
-
userImageUrl: user.userImageUrl,
|
|
3595
3674
|
joinedAt: now,
|
|
3596
3675
|
state: {}
|
|
3597
3676
|
};
|
|
@@ -3619,15 +3698,28 @@ var PresenceRoom = class extends BaseRoom {
|
|
|
3619
3698
|
peer = {
|
|
3620
3699
|
userId: user.userId,
|
|
3621
3700
|
userName: user.userName,
|
|
3622
|
-
userEmail: user.userEmail,
|
|
3623
|
-
userImageUrl: user.userImageUrl,
|
|
3624
3701
|
joinedAt,
|
|
3625
3702
|
state: {}
|
|
3626
3703
|
};
|
|
3627
3704
|
this.peers.set(user.userId, peer);
|
|
3628
3705
|
this.peerSockets.set(user.userId, ws);
|
|
3629
3706
|
}
|
|
3630
|
-
|
|
3707
|
+
if (!isPlainObject(payload)) {
|
|
3708
|
+
this.sendTo(ws, {
|
|
3709
|
+
type: MSG.ERROR,
|
|
3710
|
+
payload: { error: "Presence state must be an object" }
|
|
3711
|
+
});
|
|
3712
|
+
return;
|
|
3713
|
+
}
|
|
3714
|
+
const nextState = { ...peer.state, ...payload };
|
|
3715
|
+
if (stateSize(nextState) > MAX_PRESENCE_STATE_BYTES) {
|
|
3716
|
+
this.sendTo(ws, {
|
|
3717
|
+
type: MSG.ERROR,
|
|
3718
|
+
payload: { error: "Presence state exceeds 16 KiB" }
|
|
3719
|
+
});
|
|
3720
|
+
return;
|
|
3721
|
+
}
|
|
3722
|
+
peer.state = nextState;
|
|
3631
3723
|
this.peers.set(user.userId, peer);
|
|
3632
3724
|
this.broadcast(
|
|
3633
3725
|
{
|
|
@@ -3639,7 +3731,10 @@ var PresenceRoom = class extends BaseRoom {
|
|
|
3639
3731
|
break;
|
|
3640
3732
|
}
|
|
3641
3733
|
default:
|
|
3642
|
-
this.sendTo(ws, {
|
|
3734
|
+
this.sendTo(ws, {
|
|
3735
|
+
type: MSG.ERROR,
|
|
3736
|
+
payload: { error: `Unknown presence message type: ${type}` }
|
|
3737
|
+
});
|
|
3643
3738
|
}
|
|
3644
3739
|
}
|
|
3645
3740
|
onDisconnect(ws, user) {
|
|
@@ -4083,6 +4178,7 @@ var JobRoom = class extends BaseRoom {
|
|
|
4083
4178
|
retentionMs;
|
|
4084
4179
|
snapshotLimit;
|
|
4085
4180
|
retryBackoffMs;
|
|
4181
|
+
authorizeWrite;
|
|
4086
4182
|
/** In-flight jobs in this isolate; lets same-isolate JOB_CANCEL fire the signal. */
|
|
4087
4183
|
inFlight = /* @__PURE__ */ new Map();
|
|
4088
4184
|
/** Set by `ctx.continue` inside `executeJob`; reset before every run. */
|
|
@@ -4093,6 +4189,7 @@ var JobRoom = class extends BaseRoom {
|
|
|
4093
4189
|
this.retentionMs = Math.max(0, config.retentionMs ?? DEFAULT_RETENTION_MS);
|
|
4094
4190
|
this.snapshotLimit = Math.max(1, config.snapshotLimit ?? DEFAULT_SNAPSHOT_LIMIT);
|
|
4095
4191
|
this.retryBackoffMs = Math.max(0, config.retryBackoffMs ?? 1e3);
|
|
4192
|
+
this.authorizeWrite = config.authorizeWrite ?? ((user) => user.role === ROLES.MEMBER || user.role === ROLES.ADMIN);
|
|
4096
4193
|
}
|
|
4097
4194
|
ensureInitialized() {
|
|
4098
4195
|
if (this.initialized) return;
|
|
@@ -4133,14 +4230,31 @@ var JobRoom = class extends BaseRoom {
|
|
|
4133
4230
|
this.ensureInitialized();
|
|
4134
4231
|
return super.fetch(request);
|
|
4135
4232
|
}
|
|
4136
|
-
onConnect(ws, user) {
|
|
4233
|
+
async onConnect(ws, user) {
|
|
4137
4234
|
this.ensureInitialized();
|
|
4235
|
+
const canWrite = await this.authorizeWrite(user);
|
|
4236
|
+
this.sendTo(ws, { type: MSG.AUTH, payload: { canWrite } });
|
|
4138
4237
|
this.sendTo(ws, serverBuild.jobSnapshot(this.getRecentJobs(this.snapshotLimit)));
|
|
4139
|
-
return user;
|
|
4238
|
+
return { ...user, canWrite };
|
|
4140
4239
|
}
|
|
4141
4240
|
async onMessage(ws, user, message) {
|
|
4142
4241
|
this.ensureInitialized();
|
|
4143
4242
|
const { type, payload } = message;
|
|
4243
|
+
if (type === MSG.JOB_ENQUEUE || type === MSG.JOB_CANCEL || type === MSG.JOB_RETRY) {
|
|
4244
|
+
const attachment = user;
|
|
4245
|
+
const canWrite = await this.authorizeWrite(user);
|
|
4246
|
+
if (attachment.canWrite !== canWrite) {
|
|
4247
|
+
attachment.canWrite = canWrite;
|
|
4248
|
+
ws.serializeAttachment(attachment);
|
|
4249
|
+
this.sendTo(ws, { type: MSG.AUTH, payload: { canWrite } });
|
|
4250
|
+
} else if (!canWrite) {
|
|
4251
|
+
this.sendTo(ws, { type: MSG.AUTH, payload: { canWrite: false } });
|
|
4252
|
+
}
|
|
4253
|
+
if (!canWrite) {
|
|
4254
|
+
this.sendTo(ws, serverBuild.error("Write access denied: job queue requires member access"));
|
|
4255
|
+
return;
|
|
4256
|
+
}
|
|
4257
|
+
}
|
|
4144
4258
|
switch (type) {
|
|
4145
4259
|
case MSG.JOB_ENQUEUE: {
|
|
4146
4260
|
const requestId = String(payload.requestId ?? "");
|
|
@@ -5606,6 +5720,7 @@ var RESERVED_COLLECTION_NAMES = new Set(
|
|
|
5606
5720
|
var APP_MIGRATION_ID_RE = /^[a-z0-9][a-z0-9._-]{0,127}$/;
|
|
5607
5721
|
var MAX_APP_MIGRATION_IDS = 256;
|
|
5608
5722
|
var WORKER_OWNED_NOT_FOUND_MIGRATION_ID = "2026-08-worker-owned-not-found";
|
|
5723
|
+
var SECURE_ROOM_BOUNDARIES_MIGRATION_ID = "2026-08-secure-room-boundaries";
|
|
5609
5724
|
function validateAppMigrationIds(value) {
|
|
5610
5725
|
if (!Array.isArray(value)) {
|
|
5611
5726
|
return { valid: false, reason: "app migrations must be a JSON string array" };
|
|
@@ -6638,6 +6753,33 @@ function buildCronContext(env, ownerUserId, roomId = "default") {
|
|
|
6638
6753
|
return { records, integrations, ownerUserId };
|
|
6639
6754
|
}
|
|
6640
6755
|
|
|
6756
|
+
// src/server/utils/app-role.ts
|
|
6757
|
+
async function resolveAppRole(env, userId) {
|
|
6758
|
+
if (userId === env.OWNER_USER_ID) return "admin";
|
|
6759
|
+
const stub = env.RECORD_ROOMS.get(env.RECORD_ROOMS.idFromName(`app:${env.DEEPSPACE_APP_ID}`));
|
|
6760
|
+
try {
|
|
6761
|
+
const res = await stub.fetch(
|
|
6762
|
+
new Request("https://internal/api/tools/execute", {
|
|
6763
|
+
method: "POST",
|
|
6764
|
+
headers: {
|
|
6765
|
+
"Content-Type": "application/json",
|
|
6766
|
+
"X-User-Id": env.OWNER_USER_ID,
|
|
6767
|
+
"X-App-Action": "true"
|
|
6768
|
+
},
|
|
6769
|
+
body: JSON.stringify({
|
|
6770
|
+
tool: "records.get",
|
|
6771
|
+
params: { collection: "users", recordId: userId }
|
|
6772
|
+
})
|
|
6773
|
+
})
|
|
6774
|
+
);
|
|
6775
|
+
const json = await res.json();
|
|
6776
|
+
const role = json.data?.record?.data?.role;
|
|
6777
|
+
return json.success && (role === "admin" || role === "member") ? role : "viewer";
|
|
6778
|
+
} catch {
|
|
6779
|
+
return "viewer";
|
|
6780
|
+
}
|
|
6781
|
+
}
|
|
6782
|
+
|
|
6641
6783
|
// src/server/utils/ai.ts
|
|
6642
6784
|
import { createAnthropic } from "@ai-sdk/anthropic";
|
|
6643
6785
|
import { createOpenAI } from "@ai-sdk/openai";
|
|
@@ -9137,6 +9279,7 @@ export {
|
|
|
9137
9279
|
RecordRoom,
|
|
9138
9280
|
RegistryClientError,
|
|
9139
9281
|
SDK_RUN_WORKER_FIRST,
|
|
9282
|
+
SECURE_ROOM_BOUNDARIES_MIGRATION_ID,
|
|
9140
9283
|
STRICT_APP_ID_RE,
|
|
9141
9284
|
SYSTEM_COLLECTIONS,
|
|
9142
9285
|
SYSTEM_MANAGED_COLUMNS,
|
|
@@ -9155,6 +9298,7 @@ export {
|
|
|
9155
9298
|
applyAiToolDefaults,
|
|
9156
9299
|
applySlidingWindow,
|
|
9157
9300
|
authWorkerFetch,
|
|
9301
|
+
authenticatedRoomRequest,
|
|
9158
9302
|
bindingManifestFromOutputConfig,
|
|
9159
9303
|
buildCronContext,
|
|
9160
9304
|
buildTableSelect,
|
|
@@ -9167,6 +9311,7 @@ export {
|
|
|
9167
9311
|
capToolResultSize,
|
|
9168
9312
|
captureScreenshot,
|
|
9169
9313
|
checkFieldPermissions,
|
|
9314
|
+
checkUnclaimedOwnerTransition,
|
|
9170
9315
|
clientBuild,
|
|
9171
9316
|
coerceValue,
|
|
9172
9317
|
collectionTableName,
|
|
@@ -9231,6 +9376,7 @@ export {
|
|
|
9231
9376
|
registryErrorJson,
|
|
9232
9377
|
registryStub,
|
|
9233
9378
|
resolveAppName,
|
|
9379
|
+
resolveAppRole,
|
|
9234
9380
|
resolveColumn,
|
|
9235
9381
|
resolveCompatibilityFlags,
|
|
9236
9382
|
resolveDeepSpaceAgentModel,
|