deepspace 0.7.0 → 0.8.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 +37 -0
- package/README.md +22 -8
- package/dist/cli.js +9812 -4674
- package/dist/cli.js.map +1 -1
- package/dist/feature-installer.js +638 -0
- package/dist/feature-installer.js.map +1 -0
- package/dist/index.d.ts +107 -117
- package/dist/index.js +610 -779
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts +403 -167
- package/dist/server.js +498 -367
- package/dist/server.js.map +1 -1
- package/dist/testing.d.ts +115 -0
- package/dist/testing.js +3 -3
- package/dist/testing.js.map +1 -1
- package/dist/worker.d.ts +397 -160
- package/dist/worker.js +506 -367
- package/dist/worker.js.map +1 -1
- package/features/ai-chat/feature.json +2 -0
- package/features/ai-chat/src/AiChatPage.tsx +2 -3
- package/features/ai-chat/src/ChatPanel.messages.tsx +344 -0
- package/features/ai-chat/src/ChatPanel.stream.ts +397 -0
- package/features/ai-chat/src/ChatPanel.tsx +158 -862
- package/features/ai-chat/src/__tests__/ChatPanel.stream.test.ts +163 -0
- package/features/canvas/src/CanvasListPage.tsx +1 -1
- package/features/docs/feature.json +4 -8
- package/features/docs/src/DocsEditorPage.tsx +33 -326
- package/features/docs/src/DocsListPage.tsx +33 -29
- package/features/docs/src/InviteDialog.tsx +11 -40
- package/features/docs/src/__tests__/docs-presence-access.test.ts +71 -0
- package/features/docs/src/docs-library-types.ts +28 -3
- package/features/docs/src/docs-ui.css +1 -4
- package/features/docs/src/use-docs-presence-access.ts +330 -0
- package/features/file-attachments/feature.json +2 -2
- package/features/file-attachments/src/AttachmentPreview.tsx +428 -0
- package/features/file-attachments/src/AttachmentsPage.tsx +100 -462
- package/features/file-attachments/src/useFilePreview.ts +54 -68
- package/features/file-manager/src/FileManagerPage.tsx +1 -1
- package/features/integration-test/src/integration.spec.ts +1 -1
- package/features/items/feature.json +1 -5
- package/features/items/src/ItemsPage.tsx +207 -135
- package/features/landing/feature.json +3 -0
- package/features/landing/src/LandingPage.tsx +4 -18
- package/features/leaderboard/feature.json +1 -5
- package/features/leaderboard/src/LeaderboardPage.tsx +333 -198
- package/features/messaging/feature.json +0 -4
- package/features/messaging/src/ChatMultiPage.tsx +1 -3
- package/features/messaging/src/ChatPage.tsx +1 -3
- package/features/messaging/src/components/chat/MessageActionSheet.tsx +40 -16
- package/features/search-bar/feature.json +8 -3
- package/features/search-bar/src/SearchOverlay.tsx +108 -252
- package/features/search-bar/src/__tests__/search-model.test.ts +40 -0
- package/features/search-bar/src/search-model.ts +223 -0
- package/features/tasks/src/TasksPage.tsx +3 -3
- package/features/topbar/src/Topbar.tsx +0 -17
- package/package.json +2 -3
- package/features/docs/src/docs-favorites.ts +0 -18
- package/features/docs/src/use-yjs-room-with-awareness.ts +0 -289
- package/features/file-attachments/src/attachments-constants.ts +0 -9
- package/features/items/src/items-constants.ts +0 -10
- package/features/leaderboard/src/leaderboard-constants.ts +0 -21
- package/features/messaging/src/components/chat/ReactionPicker.tsx +0 -2
- package/scripts/add-feature.cjs +0 -807
package/dist/worker.js
CHANGED
|
@@ -59,7 +59,7 @@ var BaseRoom = class {
|
|
|
59
59
|
// ==========================================================================
|
|
60
60
|
// WebSocket Connection
|
|
61
61
|
// ==========================================================================
|
|
62
|
-
async handleWebSocketUpgrade(
|
|
62
|
+
async handleWebSocketUpgrade(_request, url) {
|
|
63
63
|
const userId = url.searchParams.get("userId") || void 0;
|
|
64
64
|
const userName = url.searchParams.get("userName") || "Anonymous";
|
|
65
65
|
const userEmail = url.searchParams.get("userEmail") || "";
|
|
@@ -111,7 +111,7 @@ var BaseRoom = class {
|
|
|
111
111
|
}
|
|
112
112
|
}
|
|
113
113
|
}
|
|
114
|
-
async webSocketError(
|
|
114
|
+
async webSocketError(_ws, error) {
|
|
115
115
|
console.error(`[${this.constructor.name}] webSocketError:`, error);
|
|
116
116
|
}
|
|
117
117
|
async alarm() {
|
|
@@ -955,6 +955,14 @@ var Awareness = class {
|
|
|
955
955
|
this.emit({ added: [], updated: [], removed });
|
|
956
956
|
}
|
|
957
957
|
}
|
|
958
|
+
/** Release all state and listeners owned by this awareness instance. */
|
|
959
|
+
destroy() {
|
|
960
|
+
this.setLocalState(null);
|
|
961
|
+
this.states.clear();
|
|
962
|
+
this.meta.clear();
|
|
963
|
+
this.updateListeners.clear();
|
|
964
|
+
this.changeListeners.clear();
|
|
965
|
+
}
|
|
958
966
|
};
|
|
959
967
|
function encodeAwarenessMessage(awareness, clientIds) {
|
|
960
968
|
const encoder = createEncoder();
|
|
@@ -1397,6 +1405,11 @@ function parseYjsDocKey(docKey) {
|
|
|
1397
1405
|
fieldName: docKey.slice(lastColon + 1)
|
|
1398
1406
|
};
|
|
1399
1407
|
}
|
|
1408
|
+
function hasYjsSubscription(attachment, target) {
|
|
1409
|
+
return attachment.yjsSubscriptions.some(
|
|
1410
|
+
(subscription) => subscription.collection === target.collection && subscription.recordId === target.recordId && subscription.fieldName === target.fieldName
|
|
1411
|
+
);
|
|
1412
|
+
}
|
|
1400
1413
|
var SYSTEM_COLLECTIONS = /* @__PURE__ */ new Set([
|
|
1401
1414
|
"canvas-settings"
|
|
1402
1415
|
]);
|
|
@@ -1412,7 +1425,7 @@ var SYSTEM_COLLECTION_SCHEMAS = [
|
|
|
1412
1425
|
function getYjsDocKey(collection, recordId, fieldName) {
|
|
1413
1426
|
return `${collection}:${recordId}:${fieldName}`;
|
|
1414
1427
|
}
|
|
1415
|
-
|
|
1428
|
+
function getOrCreateYjsDoc(ctx, docKey) {
|
|
1416
1429
|
let doc = ctx.yjsDocs.get(docKey);
|
|
1417
1430
|
if (doc) return doc;
|
|
1418
1431
|
doc = new Y2.Doc();
|
|
@@ -1434,6 +1447,20 @@ async function getOrCreateYjsDoc(ctx, docKey) {
|
|
|
1434
1447
|
ctx.yjsDocs.set(docKey, doc);
|
|
1435
1448
|
return doc;
|
|
1436
1449
|
}
|
|
1450
|
+
function unloadYjsDocIfUnused(ctx, docKey, excludeWs = null) {
|
|
1451
|
+
const target = parseYjsDocKey(docKey);
|
|
1452
|
+
if (!target) return false;
|
|
1453
|
+
for (const ws of ctx.state.getWebSockets()) {
|
|
1454
|
+
if (ws === excludeWs) continue;
|
|
1455
|
+
const attachment = ws.deserializeAttachment();
|
|
1456
|
+
if (attachment && hasYjsSubscription(attachment, target)) return false;
|
|
1457
|
+
}
|
|
1458
|
+
const doc = ctx.yjsDocs.get(docKey);
|
|
1459
|
+
if (!doc) return false;
|
|
1460
|
+
ctx.yjsDocs.delete(docKey);
|
|
1461
|
+
doc.destroy();
|
|
1462
|
+
return true;
|
|
1463
|
+
}
|
|
1437
1464
|
function saveYjsDoc(sql, docKey, doc) {
|
|
1438
1465
|
const state = Y2.encodeStateAsUpdate(doc);
|
|
1439
1466
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -1447,7 +1474,7 @@ function saveYjsDoc(sql, docKey, doc) {
|
|
|
1447
1474
|
now
|
|
1448
1475
|
);
|
|
1449
1476
|
}
|
|
1450
|
-
|
|
1477
|
+
function handleYjsJoin(ctx, ws, attachment, payload) {
|
|
1451
1478
|
const { collection, recordId, fieldName } = payload;
|
|
1452
1479
|
const isSystem = SYSTEM_COLLECTIONS.has(collection);
|
|
1453
1480
|
const schema = ctx.schemaRegistry.get(collection);
|
|
@@ -1486,10 +1513,8 @@ async function handleYjsJoin(ctx, ws, attachment, payload) {
|
|
|
1486
1513
|
hasWriteAccess = canUpdate(schema, attachment.role, recordWithId, attachment.userId, permCtx);
|
|
1487
1514
|
}
|
|
1488
1515
|
const docKey = getYjsDocKey(collection, recordId, fieldName);
|
|
1489
|
-
const doc =
|
|
1490
|
-
if (!attachment
|
|
1491
|
-
(s) => s.collection === collection && s.recordId === recordId && s.fieldName === fieldName
|
|
1492
|
-
)) {
|
|
1516
|
+
const doc = getOrCreateYjsDoc(ctx, docKey);
|
|
1517
|
+
if (!hasYjsSubscription(attachment, payload)) {
|
|
1493
1518
|
attachment.yjsSubscriptions.push({ collection, recordId, fieldName });
|
|
1494
1519
|
ws.serializeAttachment(attachment);
|
|
1495
1520
|
}
|
|
@@ -1511,14 +1536,15 @@ async function handleYjsJoin(ctx, ws, attachment, payload) {
|
|
|
1511
1536
|
}
|
|
1512
1537
|
ctx.send(ws, { type: MSG.YJS_JOIN, payload: { collection, recordId, fieldName, canWrite: hasWriteAccess } });
|
|
1513
1538
|
}
|
|
1514
|
-
function handleYjsLeave(ws, attachment, payload) {
|
|
1539
|
+
function handleYjsLeave(ctx, ws, attachment, payload) {
|
|
1515
1540
|
const { collection, recordId, fieldName } = payload;
|
|
1516
1541
|
attachment.yjsSubscriptions = attachment.yjsSubscriptions.filter(
|
|
1517
1542
|
(s) => !(s.collection === collection && s.recordId === recordId && s.fieldName === fieldName)
|
|
1518
1543
|
);
|
|
1519
1544
|
ws.serializeAttachment(attachment);
|
|
1545
|
+
unloadYjsDocIfUnused(ctx, getYjsDocKey(collection, recordId, fieldName), ws);
|
|
1520
1546
|
}
|
|
1521
|
-
|
|
1547
|
+
function handleYjsBinaryMessage(ctx, ws, attachment, data) {
|
|
1522
1548
|
const decoder = createDecoder(data);
|
|
1523
1549
|
const messageType = readVarUint(decoder);
|
|
1524
1550
|
if (messageType === MSG_YJS_AWARENESS) {
|
|
@@ -1527,11 +1553,7 @@ async function handleYjsBinaryMessage(ctx, ws, attachment, data) {
|
|
|
1527
1553
|
const payload = readVarUint8Array(decoder);
|
|
1528
1554
|
const parsed2 = parseYjsDocKey(docKey2);
|
|
1529
1555
|
if (!parsed2) return;
|
|
1530
|
-
|
|
1531
|
-
const isSubscribed2 = attachment.yjsSubscriptions.some(
|
|
1532
|
-
(s) => s.collection === collection2 && s.recordId === recordId2 && s.fieldName === fieldName2
|
|
1533
|
-
);
|
|
1534
|
-
if (!isSubscribed2) return;
|
|
1556
|
+
if (!hasYjsSubscription(attachment, parsed2)) return;
|
|
1535
1557
|
if (attachment.awarenessClientId == null) {
|
|
1536
1558
|
try {
|
|
1537
1559
|
const pd = createDecoder(payload);
|
|
@@ -1551,12 +1573,9 @@ async function handleYjsBinaryMessage(ctx, ws, attachment, data) {
|
|
|
1551
1573
|
const docKey = new TextDecoder().decode(docKeyBytes);
|
|
1552
1574
|
const parsed = parseYjsDocKey(docKey);
|
|
1553
1575
|
if (!parsed) return;
|
|
1554
|
-
const { collection, recordId
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
);
|
|
1558
|
-
if (!isSubscribed) return;
|
|
1559
|
-
const doc = await getOrCreateYjsDoc(ctx, docKey);
|
|
1576
|
+
const { collection, recordId } = parsed;
|
|
1577
|
+
if (!hasYjsSubscription(attachment, parsed)) return;
|
|
1578
|
+
const doc = getOrCreateYjsDoc(ctx, docKey);
|
|
1560
1579
|
const syncType = readVarUint(decoder);
|
|
1561
1580
|
switch (syncType) {
|
|
1562
1581
|
case MSG_SYNC_STEP1: {
|
|
@@ -1581,7 +1600,6 @@ async function handleYjsBinaryMessage(ctx, ws, attachment, data) {
|
|
|
1581
1600
|
}
|
|
1582
1601
|
const update = readVarUint8Array(decoder);
|
|
1583
1602
|
Y2.applyUpdate(doc, update, "client");
|
|
1584
|
-
saveYjsDoc(ctx.sql, docKey, doc);
|
|
1585
1603
|
broadcastYjsUpdate(ctx, docKey, update, ws);
|
|
1586
1604
|
break;
|
|
1587
1605
|
}
|
|
@@ -1590,7 +1608,6 @@ async function handleYjsBinaryMessage(ctx, ws, attachment, data) {
|
|
|
1590
1608
|
function broadcastYjsUpdate(ctx, docKey, update, excludeWs) {
|
|
1591
1609
|
const parsed = parseYjsDocKey(docKey);
|
|
1592
1610
|
if (!parsed) return;
|
|
1593
|
-
const { collection, recordId, fieldName } = parsed;
|
|
1594
1611
|
const encoder = createEncoder();
|
|
1595
1612
|
writeVarUint(encoder, MSG_YJS_SYNC);
|
|
1596
1613
|
writeVarUint8Array(encoder, new TextEncoder().encode(docKey));
|
|
@@ -1601,10 +1618,7 @@ function broadcastYjsUpdate(ctx, docKey, update, excludeWs) {
|
|
|
1601
1618
|
if (ws === excludeWs) continue;
|
|
1602
1619
|
const attachment = ws.deserializeAttachment();
|
|
1603
1620
|
if (!attachment) continue;
|
|
1604
|
-
|
|
1605
|
-
(s) => s.collection === collection && s.recordId === recordId && s.fieldName === fieldName
|
|
1606
|
-
);
|
|
1607
|
-
if (isSubscribed) {
|
|
1621
|
+
if (hasYjsSubscription(attachment, parsed)) {
|
|
1608
1622
|
ctx.sendBinary(ws, message);
|
|
1609
1623
|
}
|
|
1610
1624
|
}
|
|
@@ -1612,7 +1626,6 @@ function broadcastYjsUpdate(ctx, docKey, update, excludeWs) {
|
|
|
1612
1626
|
function broadcastAwareness(ctx, docKey, payload, excludeWs) {
|
|
1613
1627
|
const parsed = parseYjsDocKey(docKey);
|
|
1614
1628
|
if (!parsed) return;
|
|
1615
|
-
const { collection, recordId, fieldName } = parsed;
|
|
1616
1629
|
const encoder = createEncoder();
|
|
1617
1630
|
writeVarUint(encoder, MSG_YJS_AWARENESS);
|
|
1618
1631
|
writeVarUint8Array(encoder, new TextEncoder().encode(docKey));
|
|
@@ -1622,10 +1635,7 @@ function broadcastAwareness(ctx, docKey, payload, excludeWs) {
|
|
|
1622
1635
|
if (ws === excludeWs) continue;
|
|
1623
1636
|
const attachment = ws.deserializeAttachment();
|
|
1624
1637
|
if (!attachment) continue;
|
|
1625
|
-
|
|
1626
|
-
(s) => s.collection === collection && s.recordId === recordId && s.fieldName === fieldName
|
|
1627
|
-
);
|
|
1628
|
-
if (isSubscribed) {
|
|
1638
|
+
if (hasYjsSubscription(attachment, parsed)) {
|
|
1629
1639
|
ctx.sendBinary(ws, message);
|
|
1630
1640
|
}
|
|
1631
1641
|
}
|
|
@@ -1644,6 +1654,17 @@ function broadcastAwarenessRemoval(ctx, attachment) {
|
|
|
1644
1654
|
broadcastAwareness(ctx, docKey, removalPayload, null);
|
|
1645
1655
|
}
|
|
1646
1656
|
}
|
|
1657
|
+
function handleYjsDisconnect(ctx, ws, attachment) {
|
|
1658
|
+
broadcastAwarenessRemoval(ctx, attachment);
|
|
1659
|
+
const docKeys = new Set(
|
|
1660
|
+
attachment.yjsSubscriptions.map(
|
|
1661
|
+
(subscription) => getYjsDocKey(subscription.collection, subscription.recordId, subscription.fieldName)
|
|
1662
|
+
)
|
|
1663
|
+
);
|
|
1664
|
+
for (const docKey of docKeys) {
|
|
1665
|
+
unloadYjsDocIfUnused(ctx, docKey, ws);
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1647
1668
|
|
|
1648
1669
|
// src/server/handlers/subscriptions.ts
|
|
1649
1670
|
function handleSubscribe(ctx, ws, attachment, payload) {
|
|
@@ -1977,7 +1998,7 @@ function handleUserList(ctx, ws, _attachment) {
|
|
|
1977
1998
|
}));
|
|
1978
1999
|
ctx.send(ws, { type: MSG.USER_LIST, payload: { users } });
|
|
1979
2000
|
}
|
|
1980
|
-
function handleUserUpdate(ctx,
|
|
2001
|
+
function handleUserUpdate(ctx, _ws, attachment, payload) {
|
|
1981
2002
|
const existing = getUserRecord(ctx.sql, attachment.userId, ctx.schemaRegistry);
|
|
1982
2003
|
if (!existing) return;
|
|
1983
2004
|
const data = { lastSeenAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
@@ -2107,72 +2128,6 @@ var BUILT_IN_TOOLS = [
|
|
|
2107
2128
|
description: "List all users in this room",
|
|
2108
2129
|
params: {}
|
|
2109
2130
|
},
|
|
2110
|
-
// Storage tools (legacy key-value)
|
|
2111
|
-
{
|
|
2112
|
-
name: "storage.list",
|
|
2113
|
-
description: "List all keys in a storage scope",
|
|
2114
|
-
params: {
|
|
2115
|
-
scope: { type: "string", description: "Storage scope: global, files, user, userFiles", required: true },
|
|
2116
|
-
prefix: { type: "string", description: "Optional key prefix filter", required: false },
|
|
2117
|
-
userId: { type: "string", description: "User ID (required for user/userFiles scopes)", required: false }
|
|
2118
|
-
}
|
|
2119
|
-
},
|
|
2120
|
-
{
|
|
2121
|
-
name: "storage.read",
|
|
2122
|
-
description: "Read one or more keys from storage",
|
|
2123
|
-
params: {
|
|
2124
|
-
scope: { type: "string", description: "Storage scope: global, files, user, userFiles", required: true },
|
|
2125
|
-
keys: { type: "array", description: "Array of keys to read (or single key string)", required: true },
|
|
2126
|
-
userId: { type: "string", description: "User ID (required for user/userFiles scopes)", required: false }
|
|
2127
|
-
}
|
|
2128
|
-
},
|
|
2129
|
-
{
|
|
2130
|
-
name: "storage.write",
|
|
2131
|
-
description: "Write one or more key-value pairs to storage (batch supported)",
|
|
2132
|
-
params: {
|
|
2133
|
-
scope: { type: "string", description: "Storage scope: global, files, user, userFiles", required: true },
|
|
2134
|
-
data: { type: "object", description: "Object with key-value pairs to write", required: true },
|
|
2135
|
-
userId: { type: "string", description: "User ID (required for user/userFiles scopes)", required: false }
|
|
2136
|
-
}
|
|
2137
|
-
},
|
|
2138
|
-
{
|
|
2139
|
-
name: "storage.delete",
|
|
2140
|
-
description: "Delete one or more keys from storage",
|
|
2141
|
-
params: {
|
|
2142
|
-
scope: { type: "string", description: "Storage scope: global, files, user, userFiles", required: true },
|
|
2143
|
-
keys: { type: "array", description: "Array of keys to delete", required: true },
|
|
2144
|
-
userId: { type: "string", description: "User ID (required for user/userFiles scopes)", required: false }
|
|
2145
|
-
}
|
|
2146
|
-
},
|
|
2147
|
-
// Backup tools
|
|
2148
|
-
{
|
|
2149
|
-
name: "backup.create",
|
|
2150
|
-
description: "Create a backup of the current Yjs document state to R2",
|
|
2151
|
-
params: {
|
|
2152
|
-
description: { type: "string", description: "Optional description for the backup", required: false }
|
|
2153
|
-
}
|
|
2154
|
-
},
|
|
2155
|
-
{
|
|
2156
|
-
name: "backup.list",
|
|
2157
|
-
description: "List all available backups",
|
|
2158
|
-
params: {
|
|
2159
|
-
limit: { type: "number", description: "Maximum number of backups to return (default: 50)", required: false }
|
|
2160
|
-
}
|
|
2161
|
-
},
|
|
2162
|
-
{
|
|
2163
|
-
name: "backup.restore",
|
|
2164
|
-
description: "Restore the Yjs document from a backup",
|
|
2165
|
-
params: {
|
|
2166
|
-
backupId: { type: "string", description: "The backup ID to restore from", required: true }
|
|
2167
|
-
}
|
|
2168
|
-
},
|
|
2169
|
-
{
|
|
2170
|
-
name: "backup.delete",
|
|
2171
|
-
description: "Delete a specific backup",
|
|
2172
|
-
params: {
|
|
2173
|
-
backupId: { type: "string", description: "The backup ID to delete", required: true }
|
|
2174
|
-
}
|
|
2175
|
-
},
|
|
2176
2131
|
// Yjs tools (collaborative document access)
|
|
2177
2132
|
{
|
|
2178
2133
|
name: "yjs.list",
|
|
@@ -2256,7 +2211,7 @@ async function handleToolExecute(ctx, request) {
|
|
|
2256
2211
|
}
|
|
2257
2212
|
}
|
|
2258
2213
|
if (tool2.startsWith("yjs.")) {
|
|
2259
|
-
const result2 =
|
|
2214
|
+
const result2 = executeYjsTool(ctx, tool2, params, userId, userRole);
|
|
2260
2215
|
const status2 = result2.success ? 200 : result2.error?.includes("not found") ? 404 : 400;
|
|
2261
2216
|
return Response.json(result2, { status: status2, headers: CORS_HEADERS });
|
|
2262
2217
|
}
|
|
@@ -2411,7 +2366,15 @@ function buildYjsCtx(ctx) {
|
|
|
2411
2366
|
sendBinary: ctx.sendBinary
|
|
2412
2367
|
};
|
|
2413
2368
|
}
|
|
2414
|
-
|
|
2369
|
+
function withYjsDoc(ctx, docKey, operation) {
|
|
2370
|
+
const doc = getOrCreateYjsDoc(ctx, docKey);
|
|
2371
|
+
try {
|
|
2372
|
+
return operation(doc);
|
|
2373
|
+
} finally {
|
|
2374
|
+
unloadYjsDocIfUnused(ctx, docKey);
|
|
2375
|
+
}
|
|
2376
|
+
}
|
|
2377
|
+
function executeYjsTool(ctx, toolName, params, userId, userRole) {
|
|
2415
2378
|
switch (toolName) {
|
|
2416
2379
|
case "yjs.list": {
|
|
2417
2380
|
const rows = ctx.sql.exec(
|
|
@@ -2439,20 +2402,21 @@ async function executeYjsTool(ctx, toolName, params, userId, userRole) {
|
|
|
2439
2402
|
if (permError) return permError;
|
|
2440
2403
|
const docKey = getYjsDocKey(collection, recordId, fieldName);
|
|
2441
2404
|
const yjsCtx = buildYjsCtx(ctx);
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2405
|
+
return withYjsDoc(yjsCtx, docKey, (doc) => {
|
|
2406
|
+
const ytext = doc.getText(fieldName);
|
|
2407
|
+
const text = ytext.toString();
|
|
2408
|
+
const sharedTypes = {};
|
|
2409
|
+
for (const [key, type] of doc.share) {
|
|
2410
|
+
if (type instanceof Y3.Text) {
|
|
2411
|
+
sharedTypes[key || "(default)"] = `Y.Text (${type.length} chars)`;
|
|
2412
|
+
} else if (type instanceof Y3.Map) {
|
|
2413
|
+
sharedTypes[key] = `Y.Map (${type.size} entries)`;
|
|
2414
|
+
} else if (type instanceof Y3.Array) {
|
|
2415
|
+
sharedTypes[key] = `Y.Array (${type.length} items)`;
|
|
2416
|
+
}
|
|
2453
2417
|
}
|
|
2454
|
-
|
|
2455
|
-
|
|
2418
|
+
return { success: true, data: { docKey, text, sharedTypes } };
|
|
2419
|
+
});
|
|
2456
2420
|
}
|
|
2457
2421
|
case "yjs.setText": {
|
|
2458
2422
|
const { collection, recordId, fieldName, text } = params;
|
|
@@ -2466,22 +2430,26 @@ async function executeYjsTool(ctx, toolName, params, userId, userRole) {
|
|
|
2466
2430
|
if (permError) return permError;
|
|
2467
2431
|
const docKey = getYjsDocKey(collection, recordId, fieldName);
|
|
2468
2432
|
const yjsCtx = buildYjsCtx(ctx);
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2433
|
+
return withYjsDoc(yjsCtx, docKey, (doc) => {
|
|
2434
|
+
const ytext = doc.getText(fieldName);
|
|
2435
|
+
let capturedUpdate = null;
|
|
2436
|
+
const updateHandler = (update) => {
|
|
2437
|
+
capturedUpdate = update;
|
|
2438
|
+
};
|
|
2439
|
+
doc.on("update", updateHandler);
|
|
2440
|
+
try {
|
|
2441
|
+
doc.transact(() => {
|
|
2442
|
+
ytext.delete(0, ytext.length);
|
|
2443
|
+
ytext.insert(0, text);
|
|
2444
|
+
});
|
|
2445
|
+
} finally {
|
|
2446
|
+
doc.off("update", updateHandler);
|
|
2447
|
+
}
|
|
2448
|
+
if (capturedUpdate) {
|
|
2449
|
+
broadcastYjsUpdate(yjsCtx, docKey, capturedUpdate, null);
|
|
2450
|
+
}
|
|
2451
|
+
return { success: true, data: { docKey, length: text.length } };
|
|
2479
2452
|
});
|
|
2480
|
-
doc.off("update", updateHandler);
|
|
2481
|
-
if (capturedUpdate) {
|
|
2482
|
-
broadcastYjsUpdate(yjsCtx, docKey, capturedUpdate, null);
|
|
2483
|
-
}
|
|
2484
|
-
return { success: true, data: { docKey, length: text.length } };
|
|
2485
2453
|
}
|
|
2486
2454
|
default:
|
|
2487
2455
|
return { success: false, error: `Unknown Yjs tool: ${toolName}` };
|
|
@@ -2724,8 +2692,6 @@ var RecordRoom = class extends BaseRoom {
|
|
|
2724
2692
|
initPromise = null;
|
|
2725
2693
|
/** Yjs docs loaded in memory (key: collection:recordId:fieldName) */
|
|
2726
2694
|
yjsDocs = /* @__PURE__ */ new Map();
|
|
2727
|
-
/** Next Yjs client ID counter */
|
|
2728
|
-
nextYjsClientId = 1;
|
|
2729
2695
|
/** Owner user ID — gets admin role automatically */
|
|
2730
2696
|
ownerUserId;
|
|
2731
2697
|
/** True until the first fetch() completes — detects hibernation wake-up */
|
|
@@ -2742,7 +2708,7 @@ var RecordRoom = class extends BaseRoom {
|
|
|
2742
2708
|
* The HTTP debug API (`/api/debug/*`) runs arbitrary SQL and role changes
|
|
2743
2709
|
* with no auth of its own, so it is gated here at the DO's single ingress.
|
|
2744
2710
|
* Off unless a deployment opts in with `ALLOW_DEBUG_ROUTES=true`
|
|
2745
|
-
* (`deepspace dev`/`test` set it automatically). Deployments holding shared
|
|
2711
|
+
* (`deepspace dev start`/`test run` set it automatically). Deployments holding shared
|
|
2746
2712
|
* data override this to always return false.
|
|
2747
2713
|
*/
|
|
2748
2714
|
get debugRoutesEnabled() {
|
|
@@ -2877,13 +2843,17 @@ var RecordRoom = class extends BaseRoom {
|
|
|
2877
2843
|
this.sql.exec(`DROP TABLE IF EXISTS records`);
|
|
2878
2844
|
return;
|
|
2879
2845
|
}
|
|
2880
|
-
|
|
2881
|
-
|
|
2846
|
+
const prepared = [];
|
|
2847
|
+
const blocked = {
|
|
2848
|
+
missingSchema: 0,
|
|
2849
|
+
invalidData: 0,
|
|
2850
|
+
targetConflict: 0
|
|
2851
|
+
};
|
|
2882
2852
|
for (const row of rows) {
|
|
2883
2853
|
const r = row;
|
|
2884
2854
|
const schema = this.schemaRegistry.get(r.collection);
|
|
2885
2855
|
if (!schema || !schema.columns) {
|
|
2886
|
-
|
|
2856
|
+
blocked.missingSchema++;
|
|
2887
2857
|
continue;
|
|
2888
2858
|
}
|
|
2889
2859
|
const tbl = collectionTableName(r.collection);
|
|
@@ -2893,14 +2863,18 @@ var RecordRoom = class extends BaseRoom {
|
|
|
2893
2863
|
}
|
|
2894
2864
|
const existing = this.sql.exec(`SELECT 1 FROM "${tbl}" WHERE _row_id = ?`, r.record_id).toArray();
|
|
2895
2865
|
if (existing.length > 0) {
|
|
2896
|
-
|
|
2866
|
+
blocked.targetConflict++;
|
|
2897
2867
|
continue;
|
|
2898
2868
|
}
|
|
2899
2869
|
let data;
|
|
2900
2870
|
try {
|
|
2901
2871
|
data = JSON.parse(r.data);
|
|
2902
2872
|
} catch {
|
|
2903
|
-
|
|
2873
|
+
blocked.invalidData++;
|
|
2874
|
+
continue;
|
|
2875
|
+
}
|
|
2876
|
+
if (data === null || typeof data !== "object" || Array.isArray(data)) {
|
|
2877
|
+
blocked.invalidData++;
|
|
2904
2878
|
continue;
|
|
2905
2879
|
}
|
|
2906
2880
|
const columns = schema.columns.map(resolveColumn);
|
|
@@ -2921,20 +2895,32 @@ var RecordRoom = class extends BaseRoom {
|
|
|
2921
2895
|
r.updated_at,
|
|
2922
2896
|
...colIds.map((c) => colValues[c])
|
|
2923
2897
|
];
|
|
2924
|
-
|
|
2925
|
-
`INSERT INTO "${tbl}" (${allCols.join(", ")}) VALUES (${placeholders})`,
|
|
2926
|
-
|
|
2927
|
-
);
|
|
2928
|
-
migrated++;
|
|
2898
|
+
prepared.push({
|
|
2899
|
+
sql: `INSERT INTO "${tbl}" (${allCols.join(", ")}) VALUES (${placeholders})`,
|
|
2900
|
+
params
|
|
2901
|
+
});
|
|
2929
2902
|
}
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2903
|
+
const blockedCount = blocked.missingSchema + blocked.invalidData + blocked.targetConflict;
|
|
2904
|
+
if (blockedCount > 0) {
|
|
2905
|
+
console.error(
|
|
2906
|
+
`[RecordRoom] Legacy records migration blocked; no records migrated and the records table was retained for retry (${blockedCount} blocked: ${blocked.missingSchema} missing schema, ${blocked.invalidData} invalid data, ${blocked.targetConflict} target conflict)`
|
|
2933
2907
|
);
|
|
2908
|
+
return;
|
|
2934
2909
|
}
|
|
2935
|
-
this.
|
|
2910
|
+
this.state.storage.transactionSync(() => {
|
|
2911
|
+
for (const record of prepared) {
|
|
2912
|
+
this.sql.exec(record.sql, ...record.params);
|
|
2913
|
+
}
|
|
2914
|
+
this.sql.exec(`DROP TABLE IF EXISTS records`);
|
|
2915
|
+
});
|
|
2916
|
+
console.log(
|
|
2917
|
+
`[RecordRoom] Migrated ${prepared.length} records from document-mode; legacy records table removed`
|
|
2918
|
+
);
|
|
2936
2919
|
} catch (error) {
|
|
2937
|
-
console.error(
|
|
2920
|
+
console.error(
|
|
2921
|
+
`[RecordRoom] Legacy records migration failed; no records migrated and the records table was retained for retry:`,
|
|
2922
|
+
error
|
|
2923
|
+
);
|
|
2938
2924
|
}
|
|
2939
2925
|
}
|
|
2940
2926
|
// ============================================================================
|
|
@@ -2987,8 +2973,7 @@ var RecordRoom = class extends BaseRoom {
|
|
|
2987
2973
|
userImageUrl,
|
|
2988
2974
|
role: registeredUser.role,
|
|
2989
2975
|
subscriptions: [],
|
|
2990
|
-
yjsSubscriptions: []
|
|
2991
|
-
yjsClientId: this.nextYjsClientId++
|
|
2976
|
+
yjsSubscriptions: []
|
|
2992
2977
|
};
|
|
2993
2978
|
this.send(ws, { type: MSG.USER_INFO, payload: registeredUser });
|
|
2994
2979
|
const totalMs = Date.now() - timing.fetchStart;
|
|
@@ -3003,8 +2988,7 @@ var RecordRoom = class extends BaseRoom {
|
|
|
3003
2988
|
userEmail: "",
|
|
3004
2989
|
role: ROLE_ANONYMOUS,
|
|
3005
2990
|
subscriptions: [],
|
|
3006
|
-
yjsSubscriptions: []
|
|
3007
|
-
yjsClientId: this.nextYjsClientId++
|
|
2991
|
+
yjsSubscriptions: []
|
|
3008
2992
|
};
|
|
3009
2993
|
this.send(ws, {
|
|
3010
2994
|
type: MSG.USER_INFO,
|
|
@@ -3073,12 +3057,12 @@ var RecordRoom = class extends BaseRoom {
|
|
|
3073
3057
|
try {
|
|
3074
3058
|
const attachment = ws.deserializeAttachment();
|
|
3075
3059
|
if (attachment) {
|
|
3076
|
-
|
|
3060
|
+
handleYjsDisconnect(this.createYjsContext(), ws, attachment);
|
|
3077
3061
|
}
|
|
3078
3062
|
} catch {
|
|
3079
3063
|
}
|
|
3080
3064
|
}
|
|
3081
|
-
async webSocketError(
|
|
3065
|
+
async webSocketError(_ws, error) {
|
|
3082
3066
|
console.error(`[RecordRoom] webSocketError:`, error);
|
|
3083
3067
|
}
|
|
3084
3068
|
// ============================================================================
|
|
@@ -3121,7 +3105,7 @@ var RecordRoom = class extends BaseRoom {
|
|
|
3121
3105
|
await handleYjsJoin(yjsCtx, ws, attachment, payload);
|
|
3122
3106
|
break;
|
|
3123
3107
|
case MSG.YJS_LEAVE:
|
|
3124
|
-
handleYjsLeave(ws, attachment, payload);
|
|
3108
|
+
handleYjsLeave(yjsCtx, ws, attachment, payload);
|
|
3125
3109
|
break;
|
|
3126
3110
|
case MSG.LIST_SCHEMAS:
|
|
3127
3111
|
this.handleListSchemas(ws);
|
|
@@ -3280,7 +3264,7 @@ var YjsRoom = class extends BaseRoom {
|
|
|
3280
3264
|
}
|
|
3281
3265
|
onMessage(_ws, _user, _message) {
|
|
3282
3266
|
}
|
|
3283
|
-
onBinaryMessage(ws,
|
|
3267
|
+
onBinaryMessage(ws, _user, data) {
|
|
3284
3268
|
const bytes = new Uint8Array(data);
|
|
3285
3269
|
const decoder = createDecoder(bytes);
|
|
3286
3270
|
const messageType = readVarUint(decoder);
|
|
@@ -3573,7 +3557,7 @@ var GameRoom = class extends BaseRoom {
|
|
|
3573
3557
|
this.sendTo(ws, { type: MSG.ERROR, payload: { error: `Unknown game message type: ${type}` } });
|
|
3574
3558
|
}
|
|
3575
3559
|
}
|
|
3576
|
-
onDisconnect(
|
|
3560
|
+
onDisconnect(_ws, user) {
|
|
3577
3561
|
const player = this.players.get(user.userId);
|
|
3578
3562
|
if (player) {
|
|
3579
3563
|
this.players.delete(user.userId);
|
|
@@ -3881,7 +3865,7 @@ var CanvasRoom = class extends BaseRoom {
|
|
|
3881
3865
|
this.sendTo(ws, { type: MSG.ERROR, payload: { error: `Unknown canvas message type: ${type}` } });
|
|
3882
3866
|
}
|
|
3883
3867
|
}
|
|
3884
|
-
onDisconnect(
|
|
3868
|
+
onDisconnect(_ws, user) {
|
|
3885
3869
|
this.viewports.delete(user.userId);
|
|
3886
3870
|
this.undoStacks.delete(user.userId);
|
|
3887
3871
|
this.redoStacks.delete(user.userId);
|
|
@@ -4075,6 +4059,140 @@ var PresenceRoom = class extends BaseRoom {
|
|
|
4075
4059
|
}
|
|
4076
4060
|
};
|
|
4077
4061
|
|
|
4062
|
+
// src/server/rooms/cron-schedule.ts
|
|
4063
|
+
var TASK_NAME_RE = /^[a-z0-9-]+$/;
|
|
4064
|
+
function validateTask(task) {
|
|
4065
|
+
if (!task.name || typeof task.name !== "string") {
|
|
4066
|
+
throw new Error("CronTask.name is required");
|
|
4067
|
+
}
|
|
4068
|
+
if (task.name.length > 64 || !TASK_NAME_RE.test(task.name)) {
|
|
4069
|
+
throw new Error(
|
|
4070
|
+
`CronTask.name "${task.name}" must be lowercase alphanumeric with hyphens, 1-64 chars`
|
|
4071
|
+
);
|
|
4072
|
+
}
|
|
4073
|
+
const hasInterval = task.intervalMinutes != null;
|
|
4074
|
+
const hasSchedule = task.schedule != null;
|
|
4075
|
+
const hasTimezone = task.timezone != null;
|
|
4076
|
+
if (hasInterval && (hasSchedule || hasTimezone)) {
|
|
4077
|
+
throw new Error(`CronTask "${task.name}" cannot mix intervalMinutes with schedule/timezone`);
|
|
4078
|
+
}
|
|
4079
|
+
if (!hasInterval && !(hasSchedule && hasTimezone)) {
|
|
4080
|
+
throw new Error(
|
|
4081
|
+
`CronTask "${task.name}" must declare either intervalMinutes or schedule+timezone`
|
|
4082
|
+
);
|
|
4083
|
+
}
|
|
4084
|
+
if (hasInterval && (!Number.isInteger(task.intervalMinutes) || task.intervalMinutes < 1 || task.intervalMinutes > 10080)) {
|
|
4085
|
+
throw new Error(`CronTask "${task.name}" intervalMinutes must be an integer 1..10080`);
|
|
4086
|
+
}
|
|
4087
|
+
if (hasSchedule) {
|
|
4088
|
+
parseCronExpression(task.schedule);
|
|
4089
|
+
if (typeof task.timezone !== "string" || task.timezone.length === 0) {
|
|
4090
|
+
throw new Error(`CronTask "${task.name}" timezone must be a non-empty IANA string`);
|
|
4091
|
+
}
|
|
4092
|
+
try {
|
|
4093
|
+
new Intl.DateTimeFormat("en-US", { timeZone: task.timezone });
|
|
4094
|
+
} catch {
|
|
4095
|
+
throw new Error(
|
|
4096
|
+
`CronTask "${task.name}" timezone "${task.timezone}" is not a valid IANA timezone`
|
|
4097
|
+
);
|
|
4098
|
+
}
|
|
4099
|
+
}
|
|
4100
|
+
return task;
|
|
4101
|
+
}
|
|
4102
|
+
function parseCronExpression(expression) {
|
|
4103
|
+
const fields = expression.trim().split(/\s+/);
|
|
4104
|
+
if (fields.length !== 5) {
|
|
4105
|
+
throw new Error(
|
|
4106
|
+
`Cron expression must have exactly 5 fields, got ${fields.length}: "${expression}"`
|
|
4107
|
+
);
|
|
4108
|
+
}
|
|
4109
|
+
const [minute, hour, dayOfMonth, month, dayOfWeek] = fields;
|
|
4110
|
+
return {
|
|
4111
|
+
minute: parseField(minute, 0, 59, "minute"),
|
|
4112
|
+
hour: parseField(hour, 0, 23, "hour"),
|
|
4113
|
+
dayOfMonth: parseField(dayOfMonth, 1, 31, "day-of-month"),
|
|
4114
|
+
month: parseField(month, 1, 12, "month"),
|
|
4115
|
+
dayOfWeek: parseField(dayOfWeek, 0, 6, "day-of-week")
|
|
4116
|
+
};
|
|
4117
|
+
}
|
|
4118
|
+
function parseField(field, min, max, label) {
|
|
4119
|
+
const result = /* @__PURE__ */ new Set();
|
|
4120
|
+
for (const part of field.split(",")) {
|
|
4121
|
+
const value = part.trim();
|
|
4122
|
+
if (value === "*") {
|
|
4123
|
+
for (let i = min; i <= max; i++) result.add(i);
|
|
4124
|
+
continue;
|
|
4125
|
+
}
|
|
4126
|
+
const allStep = value.match(/^\*\/(\d+)$/);
|
|
4127
|
+
if (allStep) {
|
|
4128
|
+
const step = Number.parseInt(allStep[1], 10);
|
|
4129
|
+
if (step <= 0) throw new Error(`Cron ${label} step must be positive: "${value}"`);
|
|
4130
|
+
for (let i = min; i <= max; i += step) result.add(i);
|
|
4131
|
+
continue;
|
|
4132
|
+
}
|
|
4133
|
+
const range = value.match(/^(\d+)-(\d+)(?:\/(\d+))?$/);
|
|
4134
|
+
if (range) {
|
|
4135
|
+
const start = Number.parseInt(range[1], 10);
|
|
4136
|
+
const end = Number.parseInt(range[2], 10);
|
|
4137
|
+
const step = range[3] ? Number.parseInt(range[3], 10) : 1;
|
|
4138
|
+
if (start < min || end > max || start > end || step <= 0) {
|
|
4139
|
+
throw new Error(`Cron ${label} range out of bounds: "${value}" (${min}..${max})`);
|
|
4140
|
+
}
|
|
4141
|
+
for (let i = start; i <= end; i += step) result.add(i);
|
|
4142
|
+
continue;
|
|
4143
|
+
}
|
|
4144
|
+
const literal = Number.parseInt(value, 10);
|
|
4145
|
+
if (Number.isNaN(literal) || literal < min || literal > max || String(literal) !== value) {
|
|
4146
|
+
throw new Error(`Cron ${label} value invalid: "${value}" (${min}..${max})`);
|
|
4147
|
+
}
|
|
4148
|
+
result.add(literal);
|
|
4149
|
+
}
|
|
4150
|
+
if (result.size === 0) {
|
|
4151
|
+
throw new Error(`Cron ${label} field "${field}" produced no valid values`);
|
|
4152
|
+
}
|
|
4153
|
+
return result;
|
|
4154
|
+
}
|
|
4155
|
+
function nextCronFire(expression, timezone, from) {
|
|
4156
|
+
const parsed = parseCronExpression(expression);
|
|
4157
|
+
const start = Math.floor(from.getTime() / 6e4) * 6e4 + 6e4;
|
|
4158
|
+
const limit = start + 366 * 24 * 60 * 6e4;
|
|
4159
|
+
for (let timestamp = start; timestamp < limit; timestamp += 6e4) {
|
|
4160
|
+
const candidate = new Date(timestamp);
|
|
4161
|
+
if (cronMatches(parsed, timezone, candidate)) return candidate;
|
|
4162
|
+
}
|
|
4163
|
+
return null;
|
|
4164
|
+
}
|
|
4165
|
+
function cronMatches(parsed, timezone, date) {
|
|
4166
|
+
const formatter = new Intl.DateTimeFormat("en-US", {
|
|
4167
|
+
timeZone: timezone,
|
|
4168
|
+
year: "numeric",
|
|
4169
|
+
month: "numeric",
|
|
4170
|
+
day: "numeric",
|
|
4171
|
+
hour: "numeric",
|
|
4172
|
+
minute: "numeric",
|
|
4173
|
+
weekday: "short",
|
|
4174
|
+
hour12: false
|
|
4175
|
+
});
|
|
4176
|
+
const parts = Object.fromEntries(
|
|
4177
|
+
formatter.formatToParts(date).map((part) => [part.type, part.value])
|
|
4178
|
+
);
|
|
4179
|
+
const minute = Number.parseInt(parts.minute, 10);
|
|
4180
|
+
const hour = Number.parseInt(parts.hour, 10) % 24;
|
|
4181
|
+
const dayOfMonth = Number.parseInt(parts.day, 10);
|
|
4182
|
+
const month = Number.parseInt(parts.month, 10);
|
|
4183
|
+
const weekday = {
|
|
4184
|
+
Sun: 0,
|
|
4185
|
+
Mon: 1,
|
|
4186
|
+
Tue: 2,
|
|
4187
|
+
Wed: 3,
|
|
4188
|
+
Thu: 4,
|
|
4189
|
+
Fri: 5,
|
|
4190
|
+
Sat: 6
|
|
4191
|
+
};
|
|
4192
|
+
const dayOfWeek = weekday[parts.weekday] ?? 0;
|
|
4193
|
+
return parsed.minute.has(minute) && parsed.hour.has(hour) && parsed.dayOfMonth.has(dayOfMonth) && parsed.month.has(month) && parsed.dayOfWeek.has(dayOfWeek);
|
|
4194
|
+
}
|
|
4195
|
+
|
|
4078
4196
|
// src/server/rooms/cron-room.ts
|
|
4079
4197
|
var CRON_WRITE_TYPES = /* @__PURE__ */ new Set([
|
|
4080
4198
|
MSG.CRON_TRIGGER,
|
|
@@ -4115,10 +4233,7 @@ var CronRoom = class extends BaseRoom {
|
|
|
4115
4233
|
)
|
|
4116
4234
|
`);
|
|
4117
4235
|
for (const task of this.tasks) {
|
|
4118
|
-
const existing = this.sql.exec(
|
|
4119
|
-
`SELECT name FROM cron_tasks WHERE name = ?`,
|
|
4120
|
-
task.name
|
|
4121
|
-
).toArray();
|
|
4236
|
+
const existing = this.sql.exec(`SELECT name FROM cron_tasks WHERE name = ?`, task.name).toArray();
|
|
4122
4237
|
if (existing.length === 0) {
|
|
4123
4238
|
this.sql.exec(
|
|
4124
4239
|
`INSERT INTO cron_tasks (name, interval_minutes, schedule, timezone, paused)
|
|
@@ -4189,7 +4304,12 @@ var CronRoom = class extends BaseRoom {
|
|
|
4189
4304
|
this.sendTo(ws, { type: MSG.ERROR, payload: { error: "Missing taskName" } });
|
|
4190
4305
|
return;
|
|
4191
4306
|
}
|
|
4192
|
-
await this.executeTask(taskName)
|
|
4307
|
+
if (!await this.executeTask(taskName)) {
|
|
4308
|
+
this.sendTo(ws, {
|
|
4309
|
+
type: MSG.ERROR,
|
|
4310
|
+
payload: { error: `Unknown cron task: ${taskName}` }
|
|
4311
|
+
});
|
|
4312
|
+
}
|
|
4193
4313
|
break;
|
|
4194
4314
|
}
|
|
4195
4315
|
case MSG.CRON_PAUSE: {
|
|
@@ -4221,7 +4341,10 @@ var CronRoom = class extends BaseRoom {
|
|
|
4221
4341
|
break;
|
|
4222
4342
|
}
|
|
4223
4343
|
default:
|
|
4224
|
-
this.sendTo(ws, {
|
|
4344
|
+
this.sendTo(ws, {
|
|
4345
|
+
type: MSG.ERROR,
|
|
4346
|
+
payload: { error: `Unknown cron message type: ${type}` }
|
|
4347
|
+
});
|
|
4225
4348
|
}
|
|
4226
4349
|
}
|
|
4227
4350
|
async onAlarm() {
|
|
@@ -4241,6 +4364,8 @@ var CronRoom = class extends BaseRoom {
|
|
|
4241
4364
|
// Task Execution
|
|
4242
4365
|
// ==========================================================================
|
|
4243
4366
|
async executeTask(taskName) {
|
|
4367
|
+
const taskRow = this.sql.exec(`SELECT interval_minutes, schedule, timezone FROM cron_tasks WHERE name = ?`, taskName).toArray()[0];
|
|
4368
|
+
if (!taskRow) return false;
|
|
4244
4369
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4245
4370
|
const start = Date.now();
|
|
4246
4371
|
let success = true;
|
|
@@ -4265,21 +4390,18 @@ var CronRoom = class extends BaseRoom {
|
|
|
4265
4390
|
error ?? null,
|
|
4266
4391
|
completedAt
|
|
4267
4392
|
);
|
|
4268
|
-
const
|
|
4269
|
-
|
|
4393
|
+
const nextRunAt = computeNextRunAt(taskRow, new Date(completedAt));
|
|
4394
|
+
this.sql.exec(
|
|
4395
|
+
`UPDATE cron_tasks SET last_run_at = ?, next_run_at = ? WHERE name = ?`,
|
|
4396
|
+
completedAt,
|
|
4397
|
+
nextRunAt,
|
|
4270
4398
|
taskName
|
|
4271
|
-
)
|
|
4272
|
-
|
|
4273
|
-
|
|
4274
|
-
|
|
4275
|
-
`UPDATE cron_tasks SET last_run_at = ?, next_run_at = ? WHERE name = ?`,
|
|
4276
|
-
completedAt,
|
|
4277
|
-
nextRunAt,
|
|
4278
|
-
taskName
|
|
4279
|
-
);
|
|
4280
|
-
}
|
|
4281
|
-
this.sql.exec(`DELETE FROM cron_history WHERE id NOT IN (SELECT id FROM cron_history ORDER BY id DESC LIMIT 500)`);
|
|
4399
|
+
);
|
|
4400
|
+
this.sql.exec(
|
|
4401
|
+
`DELETE FROM cron_history WHERE id NOT IN (SELECT id FROM cron_history ORDER BY id DESC LIMIT 500)`
|
|
4402
|
+
);
|
|
4282
4403
|
this.broadcastStatus();
|
|
4404
|
+
return true;
|
|
4283
4405
|
}
|
|
4284
4406
|
// ==========================================================================
|
|
4285
4407
|
// Scheduling
|
|
@@ -4328,10 +4450,7 @@ var CronRoom = class extends BaseRoom {
|
|
|
4328
4450
|
});
|
|
4329
4451
|
}
|
|
4330
4452
|
getRecentHistory(limit) {
|
|
4331
|
-
return this.sql.exec(
|
|
4332
|
-
`SELECT * FROM cron_history ORDER BY id DESC LIMIT ?`,
|
|
4333
|
-
limit
|
|
4334
|
-
).toArray().map((row) => {
|
|
4453
|
+
return this.sql.exec(`SELECT * FROM cron_history ORDER BY id DESC LIMIT ?`, limit).toArray().map((row) => {
|
|
4335
4454
|
const r = row;
|
|
4336
4455
|
return {
|
|
4337
4456
|
taskName: r.task_name,
|
|
@@ -4353,100 +4472,6 @@ var CronRoom = class extends BaseRoom {
|
|
|
4353
4472
|
});
|
|
4354
4473
|
}
|
|
4355
4474
|
};
|
|
4356
|
-
var TASK_NAME_RE = /^[a-z0-9-]+$/;
|
|
4357
|
-
function validateTask(task) {
|
|
4358
|
-
if (!task.name || typeof task.name !== "string") {
|
|
4359
|
-
throw new Error(`CronTask.name is required`);
|
|
4360
|
-
}
|
|
4361
|
-
if (task.name.length > 64 || !TASK_NAME_RE.test(task.name)) {
|
|
4362
|
-
throw new Error(
|
|
4363
|
-
`CronTask.name "${task.name}" must be lowercase alphanumeric with hyphens, 1-64 chars`
|
|
4364
|
-
);
|
|
4365
|
-
}
|
|
4366
|
-
const hasInterval = task.intervalMinutes != null;
|
|
4367
|
-
const hasSchedule = task.schedule != null;
|
|
4368
|
-
const hasTimezone = task.timezone != null;
|
|
4369
|
-
if (hasInterval && (hasSchedule || hasTimezone)) {
|
|
4370
|
-
throw new Error(
|
|
4371
|
-
`CronTask "${task.name}" cannot mix intervalMinutes with schedule/timezone`
|
|
4372
|
-
);
|
|
4373
|
-
}
|
|
4374
|
-
if (!hasInterval && !(hasSchedule && hasTimezone)) {
|
|
4375
|
-
throw new Error(
|
|
4376
|
-
`CronTask "${task.name}" must declare either intervalMinutes or schedule+timezone`
|
|
4377
|
-
);
|
|
4378
|
-
}
|
|
4379
|
-
if (hasInterval) {
|
|
4380
|
-
if (!Number.isInteger(task.intervalMinutes) || task.intervalMinutes < 1 || task.intervalMinutes > 10080) {
|
|
4381
|
-
throw new Error(
|
|
4382
|
-
`CronTask "${task.name}" intervalMinutes must be an integer 1..10080`
|
|
4383
|
-
);
|
|
4384
|
-
}
|
|
4385
|
-
}
|
|
4386
|
-
if (hasSchedule) {
|
|
4387
|
-
parseCronExpression(task.schedule);
|
|
4388
|
-
if (typeof task.timezone !== "string" || task.timezone.length === 0) {
|
|
4389
|
-
throw new Error(`CronTask "${task.name}" timezone must be a non-empty IANA string`);
|
|
4390
|
-
}
|
|
4391
|
-
try {
|
|
4392
|
-
new Intl.DateTimeFormat("en-US", { timeZone: task.timezone });
|
|
4393
|
-
} catch {
|
|
4394
|
-
throw new Error(`CronTask "${task.name}" timezone "${task.timezone}" is not a valid IANA timezone`);
|
|
4395
|
-
}
|
|
4396
|
-
}
|
|
4397
|
-
return task;
|
|
4398
|
-
}
|
|
4399
|
-
function parseCronExpression(expression) {
|
|
4400
|
-
const fields = expression.trim().split(/\s+/);
|
|
4401
|
-
if (fields.length !== 5) {
|
|
4402
|
-
throw new Error(`Cron expression must have exactly 5 fields, got ${fields.length}: "${expression}"`);
|
|
4403
|
-
}
|
|
4404
|
-
const [cMin, cHour, cDom, cMonth, cDow] = fields;
|
|
4405
|
-
return {
|
|
4406
|
-
minute: parseField(cMin, 0, 59, "minute"),
|
|
4407
|
-
hour: parseField(cHour, 0, 23, "hour"),
|
|
4408
|
-
dayOfMonth: parseField(cDom, 1, 31, "day-of-month"),
|
|
4409
|
-
month: parseField(cMonth, 1, 12, "month"),
|
|
4410
|
-
dayOfWeek: parseField(cDow, 0, 6, "day-of-week")
|
|
4411
|
-
};
|
|
4412
|
-
}
|
|
4413
|
-
function parseField(field, min, max, label) {
|
|
4414
|
-
const result = /* @__PURE__ */ new Set();
|
|
4415
|
-
for (const part of field.split(",")) {
|
|
4416
|
-
const trimmed = part.trim();
|
|
4417
|
-
if (trimmed === "*") {
|
|
4418
|
-
for (let i = min; i <= max; i++) result.add(i);
|
|
4419
|
-
continue;
|
|
4420
|
-
}
|
|
4421
|
-
const allStep = trimmed.match(/^\*\/(\d+)$/);
|
|
4422
|
-
if (allStep) {
|
|
4423
|
-
const step = parseInt(allStep[1], 10);
|
|
4424
|
-
if (step <= 0) throw new Error(`Cron ${label} step must be positive: "${trimmed}"`);
|
|
4425
|
-
for (let i = min; i <= max; i += step) result.add(i);
|
|
4426
|
-
continue;
|
|
4427
|
-
}
|
|
4428
|
-
const rangeMatch = trimmed.match(/^(\d+)-(\d+)(?:\/(\d+))?$/);
|
|
4429
|
-
if (rangeMatch) {
|
|
4430
|
-
const start = parseInt(rangeMatch[1], 10);
|
|
4431
|
-
const end = parseInt(rangeMatch[2], 10);
|
|
4432
|
-
const step = rangeMatch[3] ? parseInt(rangeMatch[3], 10) : 1;
|
|
4433
|
-
if (start < min || end > max || start > end || step <= 0) {
|
|
4434
|
-
throw new Error(`Cron ${label} range out of bounds: "${trimmed}" (${min}..${max})`);
|
|
4435
|
-
}
|
|
4436
|
-
for (let i = start; i <= end; i += step) result.add(i);
|
|
4437
|
-
continue;
|
|
4438
|
-
}
|
|
4439
|
-
const num = parseInt(trimmed, 10);
|
|
4440
|
-
if (isNaN(num) || num < min || num > max || String(num) !== trimmed) {
|
|
4441
|
-
throw new Error(`Cron ${label} value invalid: "${trimmed}" (${min}..${max})`);
|
|
4442
|
-
}
|
|
4443
|
-
result.add(num);
|
|
4444
|
-
}
|
|
4445
|
-
if (result.size === 0) {
|
|
4446
|
-
throw new Error(`Cron ${label} field "${field}" produced no valid values`);
|
|
4447
|
-
}
|
|
4448
|
-
return result;
|
|
4449
|
-
}
|
|
4450
4475
|
function computeNextRunAt(task, from) {
|
|
4451
4476
|
if (task.interval_minutes) {
|
|
4452
4477
|
return new Date(from.getTime() + task.interval_minutes * 60 * 1e3).toISOString();
|
|
@@ -4457,40 +4482,6 @@ function computeNextRunAt(task, from) {
|
|
|
4457
4482
|
}
|
|
4458
4483
|
return null;
|
|
4459
4484
|
}
|
|
4460
|
-
function nextCronFire(expression, timezone, from) {
|
|
4461
|
-
const parsed = parseCronExpression(expression);
|
|
4462
|
-
const start = new Date(Math.floor(from.getTime() / 6e4) * 6e4 + 6e4);
|
|
4463
|
-
const limit = new Date(start.getTime() + 366 * 24 * 60 * 6e4);
|
|
4464
|
-
for (let t = start.getTime(); t < limit.getTime(); t += 6e4) {
|
|
4465
|
-
const candidate = new Date(t);
|
|
4466
|
-
if (cronMatches(parsed, timezone, candidate)) {
|
|
4467
|
-
return candidate;
|
|
4468
|
-
}
|
|
4469
|
-
}
|
|
4470
|
-
return null;
|
|
4471
|
-
}
|
|
4472
|
-
function cronMatches(parsed, timezone, date) {
|
|
4473
|
-
const formatter = new Intl.DateTimeFormat("en-US", {
|
|
4474
|
-
timeZone: timezone,
|
|
4475
|
-
year: "numeric",
|
|
4476
|
-
month: "numeric",
|
|
4477
|
-
day: "numeric",
|
|
4478
|
-
hour: "numeric",
|
|
4479
|
-
minute: "numeric",
|
|
4480
|
-
weekday: "short",
|
|
4481
|
-
hour12: false
|
|
4482
|
-
});
|
|
4483
|
-
const parts = Object.fromEntries(
|
|
4484
|
-
formatter.formatToParts(date).map((p) => [p.type, p.value])
|
|
4485
|
-
);
|
|
4486
|
-
const minute = parseInt(parts.minute, 10);
|
|
4487
|
-
const hour = parseInt(parts.hour, 10) % 24;
|
|
4488
|
-
const dayOfMonth = parseInt(parts.day, 10);
|
|
4489
|
-
const month = parseInt(parts.month, 10);
|
|
4490
|
-
const weekdayMap = { Sun: 0, Mon: 1, Tue: 2, Wed: 3, Thu: 4, Fri: 5, Sat: 6 };
|
|
4491
|
-
const dayOfWeek = weekdayMap[parts.weekday] ?? 0;
|
|
4492
|
-
return parsed.minute.has(minute) && parsed.hour.has(hour) && parsed.dayOfMonth.has(dayOfMonth) && parsed.month.has(month) && parsed.dayOfWeek.has(dayOfWeek);
|
|
4493
|
-
}
|
|
4494
4485
|
|
|
4495
4486
|
// src/server/rooms/job-room.ts
|
|
4496
4487
|
var RUNNING_STALE_MS = 16 * 60 * 1e3;
|
|
@@ -4603,6 +4594,7 @@ var JobRoom = class extends BaseRoom {
|
|
|
4603
4594
|
}
|
|
4604
4595
|
async onAlarm() {
|
|
4605
4596
|
this.ensureInitialized();
|
|
4597
|
+
this.recoverStuckRunning();
|
|
4606
4598
|
await this.drainDueJobs();
|
|
4607
4599
|
this.pruneExpired();
|
|
4608
4600
|
this.scheduleNextAlarm();
|
|
@@ -4905,22 +4897,36 @@ var JobRoom = class extends BaseRoom {
|
|
|
4905
4897
|
// Scheduling / recovery / pruning
|
|
4906
4898
|
// ==========================================================================
|
|
4907
4899
|
scheduleNextAlarm() {
|
|
4908
|
-
const
|
|
4900
|
+
const queued = this.sql.exec(
|
|
4909
4901
|
`SELECT next_run_at FROM jobs
|
|
4910
4902
|
WHERE status = 'queued'
|
|
4911
4903
|
ORDER BY (next_run_at IS NULL) ASC, next_run_at ASC
|
|
4912
4904
|
LIMIT 1`
|
|
4913
4905
|
).toArray()[0];
|
|
4914
|
-
if (!row) return;
|
|
4915
4906
|
const now = Date.now();
|
|
4916
|
-
const
|
|
4917
|
-
|
|
4907
|
+
const targets = [];
|
|
4908
|
+
if (queued) {
|
|
4909
|
+
const queuedAt = queued.next_run_at ? new Date(queued.next_run_at).getTime() : now;
|
|
4910
|
+
targets.push(Number.isFinite(queuedAt) ? Math.max(now, queuedAt) : now);
|
|
4911
|
+
}
|
|
4912
|
+
const running = this.sql.exec(
|
|
4913
|
+
`SELECT started_at FROM jobs
|
|
4914
|
+
WHERE status = 'running'
|
|
4915
|
+
ORDER BY started_at ASC
|
|
4916
|
+
LIMIT 1`
|
|
4917
|
+
).toArray()[0];
|
|
4918
|
+
if (running) {
|
|
4919
|
+
const startedAt = running.started_at ? new Date(running.started_at).getTime() : Number.NaN;
|
|
4920
|
+
targets.push(Number.isFinite(startedAt) ? Math.max(now, startedAt + RUNNING_STALE_MS) : now);
|
|
4921
|
+
}
|
|
4922
|
+
if (targets.length === 0) return;
|
|
4923
|
+
this.state.storage.setAlarm(Math.max(Math.min(...targets), now + 50));
|
|
4918
4924
|
}
|
|
4919
4925
|
recoverStuckRunning() {
|
|
4920
4926
|
const cutoff = new Date(Date.now() - RUNNING_STALE_MS).toISOString();
|
|
4921
4927
|
const stuck = this.sql.exec(
|
|
4922
4928
|
`SELECT id, attempts, max_attempts FROM jobs
|
|
4923
|
-
WHERE status = 'running' AND started_at
|
|
4929
|
+
WHERE status = 'running' AND (started_at IS NULL OR started_at <= ?)`,
|
|
4924
4930
|
cutoff
|
|
4925
4931
|
).toArray();
|
|
4926
4932
|
for (const r of stuck) {
|
|
@@ -5940,11 +5946,83 @@ var RESERVED_COLLECTION_NAMES = new Set(
|
|
|
5940
5946
|
GLOBAL_DO_TYPES.flatMap((t) => t.schemas.map((s) => s.name))
|
|
5941
5947
|
);
|
|
5942
5948
|
|
|
5949
|
+
// src/server/utils/cors.ts
|
|
5950
|
+
function matchOrigin(origin, patterns) {
|
|
5951
|
+
if (!origin) return null;
|
|
5952
|
+
const matches = patterns.some((pattern) => {
|
|
5953
|
+
const rx = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\\\*/g, "[^./]+");
|
|
5954
|
+
return new RegExp(`^${rx}$`).test(origin);
|
|
5955
|
+
});
|
|
5956
|
+
return matches ? origin : null;
|
|
5957
|
+
}
|
|
5958
|
+
function platformCorsPatterns(env) {
|
|
5959
|
+
const app = env.APP_DOMAIN;
|
|
5960
|
+
const platform = env.PLATFORM_DOMAIN;
|
|
5961
|
+
return [
|
|
5962
|
+
.../* @__PURE__ */ new Set([`https://${platform}`, `https://*.${platform}`, `https://*.${app}`]),
|
|
5963
|
+
"http://localhost:*"
|
|
5964
|
+
];
|
|
5965
|
+
}
|
|
5966
|
+
|
|
5967
|
+
// src/server/utils/plan-quotas.ts
|
|
5968
|
+
var DEPLOYED_APPS_DEFAULTS = {
|
|
5969
|
+
free: 1,
|
|
5970
|
+
starter: 4,
|
|
5971
|
+
premium: 10,
|
|
5972
|
+
admin: 100
|
|
5973
|
+
};
|
|
5974
|
+
function parseQuotaLimit(raw, fallback) {
|
|
5975
|
+
if (!raw) return fallback;
|
|
5976
|
+
const n = Number(raw);
|
|
5977
|
+
return Number.isInteger(n) && n >= 0 ? n : fallback;
|
|
5978
|
+
}
|
|
5979
|
+
|
|
5980
|
+
// src/server/utils/registry-client.ts
|
|
5981
|
+
var REGISTRY_INSTANCE = "v1:global";
|
|
5982
|
+
var APP_ID_RE = /^(app_[0-9A-HJKMNP-TV-Z]{26}|[a-z0-9][a-z0-9_-]{0,63})$/;
|
|
5983
|
+
var STRICT_APP_ID_RE = /^app_[0-9A-HJKMNP-TV-Z]{26}$/;
|
|
5984
|
+
var RegistryClientError = class extends Error {
|
|
5985
|
+
constructor(message, code, status) {
|
|
5986
|
+
super(message);
|
|
5987
|
+
this.code = code;
|
|
5988
|
+
this.status = status;
|
|
5989
|
+
this.name = "RegistryClientError";
|
|
5990
|
+
}
|
|
5991
|
+
};
|
|
5992
|
+
function registryStub(ns) {
|
|
5993
|
+
return ns.get(ns.idFromName(REGISTRY_INSTANCE));
|
|
5994
|
+
}
|
|
5995
|
+
async function callRegistry(stub, body) {
|
|
5996
|
+
const res = await stub.fetch("https://registry/", {
|
|
5997
|
+
method: "POST",
|
|
5998
|
+
headers: { "Content-Type": "application/json" },
|
|
5999
|
+
body: JSON.stringify(body)
|
|
6000
|
+
});
|
|
6001
|
+
const json = await res.json().catch(() => ({}));
|
|
6002
|
+
if (!res.ok) {
|
|
6003
|
+
throw new RegistryClientError(
|
|
6004
|
+
json.error ?? `Registry error (${res.status})`,
|
|
6005
|
+
json.code ?? "error",
|
|
6006
|
+
res.status
|
|
6007
|
+
);
|
|
6008
|
+
}
|
|
6009
|
+
return json;
|
|
6010
|
+
}
|
|
6011
|
+
function registryErrorJson(c, err) {
|
|
6012
|
+
if (err instanceof RegistryClientError) {
|
|
6013
|
+
return c.json({ error: err.message, code: err.code }, err.status);
|
|
6014
|
+
}
|
|
6015
|
+
throw err;
|
|
6016
|
+
}
|
|
6017
|
+
|
|
5943
6018
|
// src/server/utils/scoped-r2-files.ts
|
|
5944
6019
|
var CORS_HEADERS3 = { "Access-Control-Allow-Origin": "*" };
|
|
5945
6020
|
function contentDisposition(originalName) {
|
|
5946
|
-
const safe = originalName.replace(/[
|
|
5947
|
-
const encoded = encodeURIComponent(originalName)
|
|
6021
|
+
const safe = originalName.replace(/[^\x20-\x7e]|["\\]/g, "_") || "download";
|
|
6022
|
+
const encoded = encodeURIComponent(originalName).replace(
|
|
6023
|
+
/[!'()*]/g,
|
|
6024
|
+
(char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`
|
|
6025
|
+
);
|
|
5948
6026
|
return `inline; filename="${safe}"; filename*=UTF-8''${encoded}`;
|
|
5949
6027
|
}
|
|
5950
6028
|
function sanitizeSubpath(raw) {
|
|
@@ -5953,6 +6031,9 @@ function sanitizeSubpath(raw) {
|
|
|
5953
6031
|
if (segments.some((s) => s === ".." || s === ".")) return null;
|
|
5954
6032
|
return raw;
|
|
5955
6033
|
}
|
|
6034
|
+
function encodeKeyPath(key) {
|
|
6035
|
+
return key.split("/").map(encodeURIComponent).join("/");
|
|
6036
|
+
}
|
|
5956
6037
|
function isKeyWithinPrefix(key, prefix) {
|
|
5957
6038
|
return key.startsWith(prefix);
|
|
5958
6039
|
}
|
|
@@ -5973,9 +6054,15 @@ function normalizeMimeType(raw) {
|
|
|
5973
6054
|
function createScopedR2Handler(config) {
|
|
5974
6055
|
const requireAuthForMutations = config.requireAuthForMutations ?? true;
|
|
5975
6056
|
return async (request, url, bucket, auth) => {
|
|
5976
|
-
|
|
5977
|
-
|
|
5978
|
-
|
|
6057
|
+
let subpathRaw;
|
|
6058
|
+
try {
|
|
6059
|
+
subpathRaw = decodeURIComponent(url.pathname.replace("/api/files", "").replace(/^\//, ""));
|
|
6060
|
+
} catch {
|
|
6061
|
+
return Response.json(
|
|
6062
|
+
{ error: "Invalid path encoding" },
|
|
6063
|
+
{ status: 400, headers: CORS_HEADERS3 }
|
|
6064
|
+
);
|
|
6065
|
+
}
|
|
5979
6066
|
const subpath = sanitizeSubpath(subpathRaw);
|
|
5980
6067
|
if (subpath === null) {
|
|
5981
6068
|
return Response.json(
|
|
@@ -5983,9 +6070,10 @@ function createScopedR2Handler(config) {
|
|
|
5983
6070
|
{ status: 400, headers: CORS_HEADERS3 }
|
|
5984
6071
|
);
|
|
5985
6072
|
}
|
|
5986
|
-
const
|
|
5987
|
-
|
|
5988
|
-
|
|
6073
|
+
const scope = url.searchParams.get("scope") || "self";
|
|
6074
|
+
if (scope !== "self" && scope !== "app") {
|
|
6075
|
+
return Response.json({ error: "Invalid scope" }, { status: 400, headers: CORS_HEADERS3 });
|
|
6076
|
+
}
|
|
5989
6077
|
const result = config.resolvePrefix(scope, { userId: auth.userId, url });
|
|
5990
6078
|
if (!result.prefix) {
|
|
5991
6079
|
const errorMsg = result.error ?? "Invalid scope";
|
|
@@ -6000,10 +6088,10 @@ function createScopedR2Handler(config) {
|
|
|
6000
6088
|
{ status: 401, headers: CORS_HEADERS3 }
|
|
6001
6089
|
);
|
|
6002
6090
|
}
|
|
6003
|
-
return handleUpload(request, url, bucket, prefix, auth.userId);
|
|
6091
|
+
return handleUpload(request, url, bucket, prefix, auth.userId, scope);
|
|
6004
6092
|
}
|
|
6005
6093
|
if (!subpath && request.method === "GET") {
|
|
6006
|
-
return handleList(bucket, prefix, url);
|
|
6094
|
+
return handleList(bucket, prefix, url, scope);
|
|
6007
6095
|
}
|
|
6008
6096
|
if (subpath && request.method === "GET") {
|
|
6009
6097
|
if (!isKeyWithinPrefix(subpath, prefix)) {
|
|
@@ -6012,7 +6100,7 @@ function createScopedR2Handler(config) {
|
|
|
6012
6100
|
{ status: 403, headers: CORS_HEADERS3 }
|
|
6013
6101
|
);
|
|
6014
6102
|
}
|
|
6015
|
-
return handleDownload(bucket, subpath);
|
|
6103
|
+
return handleDownload(bucket, subpath, scope);
|
|
6016
6104
|
}
|
|
6017
6105
|
if (subpath && request.method === "DELETE") {
|
|
6018
6106
|
if (requireAuthForMutations && !auth.userId) {
|
|
@@ -6033,7 +6121,7 @@ function createScopedR2Handler(config) {
|
|
|
6033
6121
|
return Response.json({ error: "Method not allowed" }, { status: 405, headers: CORS_HEADERS3 });
|
|
6034
6122
|
};
|
|
6035
6123
|
}
|
|
6036
|
-
async function handleUpload(request, url, bucket, prefix, userId) {
|
|
6124
|
+
async function handleUpload(request, url, bucket, prefix, userId, scope) {
|
|
6037
6125
|
try {
|
|
6038
6126
|
const contentType = request.headers.get("content-type") || "";
|
|
6039
6127
|
let fileData;
|
|
@@ -6097,35 +6185,37 @@ async function handleUpload(request, url, bucket, prefix, userId) {
|
|
|
6097
6185
|
uploadedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
6098
6186
|
}
|
|
6099
6187
|
});
|
|
6100
|
-
const fileUrl =
|
|
6188
|
+
const fileUrl = new URL(`/api/files/${encodeKeyPath(key)}`, url.origin);
|
|
6189
|
+
fileUrl.searchParams.set("scope", scope);
|
|
6101
6190
|
return Response.json(
|
|
6102
|
-
{ success: true, key, url: fileUrl, name: fileName },
|
|
6191
|
+
{ success: true, key, url: fileUrl.toString(), name: fileName },
|
|
6103
6192
|
{ headers: CORS_HEADERS3 }
|
|
6104
6193
|
);
|
|
6105
6194
|
} catch (err) {
|
|
6106
6195
|
const msg = err instanceof Error ? err.message : "Upload failed";
|
|
6107
6196
|
console.error("[handleUpload] Error:", msg, err instanceof Error ? err.stack : "");
|
|
6108
|
-
return Response.json(
|
|
6109
|
-
{ error: msg },
|
|
6110
|
-
{ status: 500, headers: CORS_HEADERS3 }
|
|
6111
|
-
);
|
|
6197
|
+
return Response.json({ error: msg }, { status: 500, headers: CORS_HEADERS3 });
|
|
6112
6198
|
}
|
|
6113
6199
|
}
|
|
6114
|
-
async function handleList(bucket, prefix, url) {
|
|
6200
|
+
async function handleList(bucket, prefix, url, scope) {
|
|
6115
6201
|
const userPrefix = url.searchParams.get("prefix") || "";
|
|
6116
6202
|
const listPrefix = `${prefix}${userPrefix}`;
|
|
6117
6203
|
const limit = parseInt(url.searchParams.get("limit") || "100", 10);
|
|
6118
6204
|
const listed = await bucket.list({ prefix: listPrefix, limit });
|
|
6119
|
-
const files = listed.objects.map((obj) =>
|
|
6120
|
-
|
|
6121
|
-
|
|
6122
|
-
|
|
6123
|
-
|
|
6124
|
-
|
|
6125
|
-
|
|
6205
|
+
const files = listed.objects.map((obj) => {
|
|
6206
|
+
const fileUrl = new URL(`/api/files/${encodeKeyPath(obj.key)}`, url.origin);
|
|
6207
|
+
fileUrl.searchParams.set("scope", scope);
|
|
6208
|
+
return {
|
|
6209
|
+
key: obj.key,
|
|
6210
|
+
size: obj.size,
|
|
6211
|
+
uploaded: obj.uploaded.toISOString(),
|
|
6212
|
+
url: fileUrl.toString(),
|
|
6213
|
+
...obj.customMetadata
|
|
6214
|
+
};
|
|
6215
|
+
});
|
|
6126
6216
|
return Response.json({ files, truncated: listed.truncated }, { headers: CORS_HEADERS3 });
|
|
6127
6217
|
}
|
|
6128
|
-
async function handleDownload(bucket, key) {
|
|
6218
|
+
async function handleDownload(bucket, key, scope) {
|
|
6129
6219
|
const object = await bucket.get(key);
|
|
6130
6220
|
if (!object) {
|
|
6131
6221
|
return Response.json({ error: "File not found" }, { status: 404, headers: CORS_HEADERS3 });
|
|
@@ -6133,7 +6223,10 @@ async function handleDownload(bucket, key) {
|
|
|
6133
6223
|
const headers = new Headers();
|
|
6134
6224
|
headers.set("Content-Type", object.httpMetadata?.contentType || "application/octet-stream");
|
|
6135
6225
|
headers.set("X-Content-Type-Options", "nosniff");
|
|
6136
|
-
headers.set(
|
|
6226
|
+
headers.set(
|
|
6227
|
+
"Cache-Control",
|
|
6228
|
+
scope === "app" ? "public, max-age=31536000, immutable" : "private, no-store"
|
|
6229
|
+
);
|
|
6137
6230
|
headers.set("Access-Control-Allow-Origin", "*");
|
|
6138
6231
|
if (object.customMetadata?.originalName) {
|
|
6139
6232
|
headers.set("Content-Disposition", contentDisposition(object.customMetadata.originalName));
|
|
@@ -6148,7 +6241,7 @@ function resolveApiTransport(env) {
|
|
|
6148
6241
|
return { kind: "url", baseUrl: env.API_WORKER_URL.replace(/\/$/, "") };
|
|
6149
6242
|
}
|
|
6150
6243
|
throw new Error(
|
|
6151
|
-
"apiWorkerFetch: neither env.API_WORKER nor env.API_WORKER_URL is set. Add a [[services]] binding in wrangler.toml for production, or let `deepspace dev` write API_WORKER_URL into .dev.vars for local development."
|
|
6244
|
+
"apiWorkerFetch: neither env.API_WORKER nor env.API_WORKER_URL is set. Add a [[services]] binding in wrangler.toml for production, or let `deepspace dev start` write API_WORKER_URL into .dev.vars for local development."
|
|
6152
6245
|
);
|
|
6153
6246
|
}
|
|
6154
6247
|
function resolvePlatformTransport(env) {
|
|
@@ -6157,7 +6250,7 @@ function resolvePlatformTransport(env) {
|
|
|
6157
6250
|
return { kind: "url", baseUrl: env.PLATFORM_WORKER_URL.replace(/\/$/, "") };
|
|
6158
6251
|
}
|
|
6159
6252
|
throw new Error(
|
|
6160
|
-
"platformWorkerFetch: neither env.PLATFORM_WORKER nor env.PLATFORM_WORKER_URL is set. Add a [[services]] binding in wrangler.toml for production, or let `deepspace dev` write PLATFORM_WORKER_URL into .dev.vars for local development."
|
|
6253
|
+
"platformWorkerFetch: neither env.PLATFORM_WORKER nor env.PLATFORM_WORKER_URL is set. Add a [[services]] binding in wrangler.toml for production, or let `deepspace dev start` write PLATFORM_WORKER_URL into .dev.vars for local development."
|
|
6161
6254
|
);
|
|
6162
6255
|
}
|
|
6163
6256
|
function buildTargetUrl(transport, path, internalHost) {
|
|
@@ -6203,7 +6296,7 @@ function platformWorkerFetch(env, pathOrRequest, init) {
|
|
|
6203
6296
|
function authWorkerFetch(env, path, init) {
|
|
6204
6297
|
if (!env.AUTH_WORKER_URL) {
|
|
6205
6298
|
throw new Error(
|
|
6206
|
-
"authWorkerFetch: env.AUTH_WORKER_URL is not set. `deepspace dev` should write this into .dev.vars; for production, set it as a wrangler var or secret."
|
|
6299
|
+
"authWorkerFetch: env.AUTH_WORKER_URL is not set. `deepspace dev start` should write this into .dev.vars; for production, set it as a wrangler var or secret."
|
|
6207
6300
|
);
|
|
6208
6301
|
}
|
|
6209
6302
|
const baseUrl = env.AUTH_WORKER_URL.replace(/\/$/, "");
|
|
@@ -6224,15 +6317,17 @@ function buildCronContext(env, ownerUserId, roomId = "default") {
|
|
|
6224
6317
|
const roomIdObj = env.RECORD_ROOMS.idFromName(roomId);
|
|
6225
6318
|
const room = env.RECORD_ROOMS.get(roomIdObj);
|
|
6226
6319
|
async function executeTool3(tool2, params) {
|
|
6227
|
-
const response = await room.fetch(
|
|
6228
|
-
|
|
6229
|
-
|
|
6230
|
-
|
|
6231
|
-
|
|
6232
|
-
|
|
6233
|
-
|
|
6234
|
-
|
|
6235
|
-
|
|
6320
|
+
const response = await room.fetch(
|
|
6321
|
+
new Request("https://internal/api/tools/execute", {
|
|
6322
|
+
method: "POST",
|
|
6323
|
+
headers: {
|
|
6324
|
+
"Content-Type": "application/json",
|
|
6325
|
+
"X-User-Id": ownerUserId,
|
|
6326
|
+
"X-App-Action": "true"
|
|
6327
|
+
},
|
|
6328
|
+
body: JSON.stringify({ tool: tool2, params })
|
|
6329
|
+
})
|
|
6330
|
+
);
|
|
6236
6331
|
const result = await response.json();
|
|
6237
6332
|
if (!result.success) {
|
|
6238
6333
|
throw new Error(`RecordRoom tool ${tool2} failed: ${result.error || "Unknown error"}`);
|
|
@@ -6262,7 +6357,7 @@ function buildCronContext(env, ownerUserId, roomId = "default") {
|
|
|
6262
6357
|
async call(endpoint, params = {}) {
|
|
6263
6358
|
if (!env.APP_OWNER_JWT) {
|
|
6264
6359
|
throw new Error(
|
|
6265
|
-
"integrations.call requires env.APP_OWNER_JWT. Deployed apps receive this automatically; in dev, `deepspace dev` writes it into .dev.vars."
|
|
6360
|
+
"integrations.call requires env.APP_OWNER_JWT. Deployed apps receive this automatically; in dev, `deepspace dev start` writes it into .dev.vars."
|
|
6266
6361
|
);
|
|
6267
6362
|
}
|
|
6268
6363
|
const response = await apiWorkerFetch(env, `/api/integrations/${endpoint}`, {
|
|
@@ -6313,7 +6408,7 @@ function createDeepSpaceAI(env, provider, options = {}) {
|
|
|
6313
6408
|
const authToken = options.authToken ?? env.APP_OWNER_JWT;
|
|
6314
6409
|
if (!authToken) {
|
|
6315
6410
|
throw new Error(
|
|
6316
|
-
"createDeepSpaceAI: no auth token available. Either pass `options.authToken` explicitly (for user-initiated calls), or ensure `env.APP_OWNER_JWT` is set (injected at deploy time or by `deepspace dev`)."
|
|
6411
|
+
"createDeepSpaceAI: no auth token available. Either pass `options.authToken` explicitly (for user-initiated calls), or ensure `env.APP_OWNER_JWT` is set (injected at deploy time or by `deepspace dev start`)."
|
|
6317
6412
|
);
|
|
6318
6413
|
}
|
|
6319
6414
|
const proxyFetch = (input, init) => {
|
|
@@ -6449,7 +6544,7 @@ function appendAppIdentity(headers, env) {
|
|
|
6449
6544
|
}
|
|
6450
6545
|
|
|
6451
6546
|
// src/server/utils/screenshot.ts
|
|
6452
|
-
async function captureScreenshot(env,
|
|
6547
|
+
async function captureScreenshot(env, url) {
|
|
6453
6548
|
const headers = new Headers({ "content-type": "application/json" });
|
|
6454
6549
|
appendAppIdentity(headers, env);
|
|
6455
6550
|
const res = await platformWorkerFetch(
|
|
@@ -6457,16 +6552,11 @@ async function captureScreenshot(env, opts) {
|
|
|
6457
6552
|
new Request("https://platform-internal/internal/screenshot", {
|
|
6458
6553
|
method: "POST",
|
|
6459
6554
|
headers,
|
|
6460
|
-
body: JSON.stringify(
|
|
6555
|
+
body: JSON.stringify({ url })
|
|
6461
6556
|
})
|
|
6462
6557
|
);
|
|
6463
6558
|
if (!res.ok) {
|
|
6464
|
-
|
|
6465
|
-
try {
|
|
6466
|
-
detail = (await res.text()).slice(0, 300);
|
|
6467
|
-
} catch {
|
|
6468
|
-
}
|
|
6469
|
-
console.warn(`[captureScreenshot] platform returned ${res.status}:`, detail);
|
|
6559
|
+
console.warn(`[captureScreenshot] platform returned ${res.status}`);
|
|
6470
6560
|
return null;
|
|
6471
6561
|
}
|
|
6472
6562
|
return {
|
|
@@ -6832,8 +6922,6 @@ async function executeTool2(stub, userId, tool2, params) {
|
|
|
6832
6922
|
function toChatRow(env) {
|
|
6833
6923
|
return {
|
|
6834
6924
|
recordId: env.recordId,
|
|
6835
|
-
// Deprecated alias — populated for back-compat with callers reading `id`.
|
|
6836
|
-
id: env.recordId,
|
|
6837
6925
|
userId: env.data.userId,
|
|
6838
6926
|
title: env.data.title ?? "",
|
|
6839
6927
|
model: env.data.model,
|
|
@@ -6846,8 +6934,6 @@ function toChatRow(env) {
|
|
|
6846
6934
|
function toMessageRow(env) {
|
|
6847
6935
|
return {
|
|
6848
6936
|
recordId: env.recordId,
|
|
6849
|
-
// Deprecated alias — populated for back-compat with callers reading `id`.
|
|
6850
|
-
id: env.recordId,
|
|
6851
6937
|
chatId: env.data.chatId,
|
|
6852
6938
|
userId: env.data.userId,
|
|
6853
6939
|
role: env.data.role,
|
|
@@ -7273,6 +7359,16 @@ function createDeepSpaceAuth(config) {
|
|
|
7273
7359
|
database: config.database,
|
|
7274
7360
|
baseURL: config.baseURL,
|
|
7275
7361
|
secret: config.secret,
|
|
7362
|
+
// Login is a one-time human act; everything after it is agents minting
|
|
7363
|
+
// short-lived JWTs from this session. 30 days idle (vs the 7-day
|
|
7364
|
+
// default) with daily sliding renewal means any agent active within a
|
|
7365
|
+
// month keeps the session alive indefinitely — the human re-logs-in
|
|
7366
|
+
// only after a full month of zero activity. Short-lived access stays
|
|
7367
|
+
// short: CLI JWTs expire in minutes (auth-worker /api/auth/token).
|
|
7368
|
+
session: {
|
|
7369
|
+
expiresIn: 60 * 60 * 24 * 30,
|
|
7370
|
+
updateAge: 60 * 60 * 24
|
|
7371
|
+
},
|
|
7276
7372
|
emailAndPassword: {
|
|
7277
7373
|
enabled: config.emailAndPassword ?? true
|
|
7278
7374
|
},
|
|
@@ -7301,9 +7397,41 @@ function createDeepSpaceAuth(config) {
|
|
|
7301
7397
|
}
|
|
7302
7398
|
}
|
|
7303
7399
|
} : {},
|
|
7400
|
+
// Observer hook for new-user creation (see DeepSpaceAuthConfig.onUserCreated).
|
|
7401
|
+
// Two-layer wiring guard — be honest about what each layer catches:
|
|
7402
|
+
// 1. buildDatabaseHooks's return type is pinned to
|
|
7403
|
+
// BetterAuthOptions['databaseHooks'], so a better-auth upgrade that
|
|
7404
|
+
// renames/reshapes the option fails tsc.
|
|
7405
|
+
// 2. A LOCAL typo of this key would still compile (betterAuth's generic
|
|
7406
|
+
// options parameter defeats excess-property checking even for plain
|
|
7407
|
+
// keys) — that case is caught by the runtime assertions in
|
|
7408
|
+
// __tests__/betterAuth-hooks.test.ts, which read the wired hook back
|
|
7409
|
+
// off auth.options. Don't rename the key without running them.
|
|
7410
|
+
databaseHooks: buildDatabaseHooks(config.onUserCreated),
|
|
7304
7411
|
plugins: [organization(), twoFactor()]
|
|
7305
7412
|
});
|
|
7306
7413
|
}
|
|
7414
|
+
function buildDatabaseHooks(onUserCreated) {
|
|
7415
|
+
if (!onUserCreated) return void 0;
|
|
7416
|
+
return {
|
|
7417
|
+
user: {
|
|
7418
|
+
create: {
|
|
7419
|
+
after: async (user, ctx) => {
|
|
7420
|
+
try {
|
|
7421
|
+
await onUserCreated(
|
|
7422
|
+
{ id: user.id, email: user.email, name: user.name },
|
|
7423
|
+
// Forward BOTH request and standalone headers — some better-auth
|
|
7424
|
+
// entry points populate ctx.headers without a request.
|
|
7425
|
+
ctx ? { request: ctx.request, headers: ctx.headers ?? void 0 } : null
|
|
7426
|
+
);
|
|
7427
|
+
} catch (err) {
|
|
7428
|
+
console.error("[deepspace] onUserCreated hook failed (signup unaffected)", err);
|
|
7429
|
+
}
|
|
7430
|
+
}
|
|
7431
|
+
}
|
|
7432
|
+
}
|
|
7433
|
+
};
|
|
7434
|
+
}
|
|
7307
7435
|
var cachedAppleSecret = null;
|
|
7308
7436
|
async function generateAppleClientSecret(apple) {
|
|
7309
7437
|
const now = Math.floor(Date.now() / 1e3);
|
|
@@ -7421,6 +7549,7 @@ export {
|
|
|
7421
7549
|
AI_CHATS_SCHEMA,
|
|
7422
7550
|
AI_MESSAGES_SCHEMA,
|
|
7423
7551
|
ALLOWED_BINDING_TYPES,
|
|
7552
|
+
APP_ID_RE,
|
|
7424
7553
|
APP_LOG_EVENT_KEYS,
|
|
7425
7554
|
APP_NAME_RULES,
|
|
7426
7555
|
AUTO_PROVISIONABLE_TYPES,
|
|
@@ -7440,6 +7569,7 @@ export {
|
|
|
7440
7569
|
DEFAULT_CONTEXT_CONFIG,
|
|
7441
7570
|
DEFAULT_DO_MANIFEST,
|
|
7442
7571
|
DEFAULT_QUERY_LIMIT,
|
|
7572
|
+
DEPLOYED_APPS_DEFAULTS,
|
|
7443
7573
|
DIRECTORY_SCHEMAS,
|
|
7444
7574
|
GLOBAL_DO_TYPES,
|
|
7445
7575
|
GLOBAL_DO_TYPE_NAMES,
|
|
@@ -7458,12 +7588,15 @@ export {
|
|
|
7458
7588
|
PresenceRoom,
|
|
7459
7589
|
REACTIONS_SCHEMA,
|
|
7460
7590
|
READ_RECEIPTS_SCHEMA,
|
|
7591
|
+
REGISTRY_INSTANCE,
|
|
7461
7592
|
RESERVED_BINDING_NAMES,
|
|
7462
7593
|
RESERVED_COLLECTION_NAMES,
|
|
7463
7594
|
ROLE_ADMIN,
|
|
7464
7595
|
ROLE_ANONYMOUS,
|
|
7465
7596
|
ROLE_DEFAULT,
|
|
7466
7597
|
RecordRoom,
|
|
7598
|
+
RegistryClientError,
|
|
7599
|
+
STRICT_APP_ID_RE,
|
|
7467
7600
|
SYSTEM_COLLECTIONS,
|
|
7468
7601
|
SYSTEM_MANAGED_COLUMNS,
|
|
7469
7602
|
SchemaRegistry,
|
|
@@ -7482,6 +7615,7 @@ export {
|
|
|
7482
7615
|
buildCronContext,
|
|
7483
7616
|
buildTableSelect,
|
|
7484
7617
|
buildUiParts,
|
|
7618
|
+
callRegistry,
|
|
7485
7619
|
canCreate,
|
|
7486
7620
|
canDelete,
|
|
7487
7621
|
canRead,
|
|
@@ -7529,16 +7663,21 @@ export {
|
|
|
7529
7663
|
lintSchema,
|
|
7530
7664
|
loadMessages,
|
|
7531
7665
|
makeDefaultSummarizer,
|
|
7666
|
+
matchOrigin,
|
|
7532
7667
|
meterAi,
|
|
7533
7668
|
meterUsage,
|
|
7534
7669
|
meterVectorize,
|
|
7535
7670
|
noopPermissionContext,
|
|
7671
|
+
parseQuotaLimit,
|
|
7672
|
+
platformCorsPatterns,
|
|
7536
7673
|
platformWorkerFetch,
|
|
7537
7674
|
prepareMessagesWithCompaction,
|
|
7538
7675
|
priceBindingUsageEvent,
|
|
7539
7676
|
readVarUint,
|
|
7540
7677
|
readVarUint8Array,
|
|
7541
7678
|
registerClientErrorRoute,
|
|
7679
|
+
registryErrorJson,
|
|
7680
|
+
registryStub,
|
|
7542
7681
|
resolveAppName,
|
|
7543
7682
|
resolveColumn,
|
|
7544
7683
|
rowToData,
|