deepspace 0.3.10 → 0.4.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/dist/cli.js +331 -115
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +199 -16
- package/dist/index.js +354 -166
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts +242 -73
- package/dist/server.js +985 -374
- package/dist/server.js.map +1 -1
- package/dist/worker.d.ts +322 -73
- package/dist/worker.js +961 -343
- package/dist/worker.js.map +1 -1
- package/features/admin-page/feature.json +29 -0
- package/features/admin-page/src/AdminPage.tsx +292 -0
- package/features/admin-page/src/admin-schema.ts +20 -0
- package/features/ai-chat/feature.json +41 -0
- package/features/ai-chat/src/AiChatPage.tsx +692 -0
- package/features/ai-chat/src/ChatPanel.tsx +1098 -0
- package/features/ai-chat/src/ai-chat-schema.ts +11 -0
- package/features/canvas/feature.json +45 -0
- package/features/canvas/src/CanvasEditorPage.tsx +87 -0
- package/features/canvas/src/CanvasListPage.tsx +202 -0
- package/features/canvas/src/canvas-schema.ts +33 -0
- package/features/canvas/src/components/CanvasToolbar.tsx +170 -0
- package/features/canvas/src/components/CanvasView.tsx +369 -0
- package/features/canvas/src/components/ShapeRenderer.tsx +114 -0
- package/features/cron/feature.json +33 -0
- package/features/cron/src/CronLogPage.tsx +100 -0
- package/features/cron/src/cron.ts +25 -0
- package/features/docs/feature.json +96 -0
- package/features/docs/src/DocsEditorPage.tsx +1012 -0
- package/features/docs/src/DocsListPage.tsx +951 -0
- package/features/docs/src/DocsOutlinePanel.tsx +68 -0
- package/features/docs/src/DocsPresence.tsx +138 -0
- package/features/docs/src/InviteDialog.tsx +336 -0
- package/features/docs/src/LibrarySidebar.tsx +464 -0
- package/features/docs/src/docs-favorites.ts +18 -0
- package/features/docs/src/docs-library-types.ts +15 -0
- package/features/docs/src/docs-schema.ts +80 -0
- package/features/docs/src/docs-ui.css +309 -0
- package/features/docs/src/editor/DocEditorSurface.tsx +257 -0
- package/features/docs/src/editor/DocsTiptapToolbar.tsx +560 -0
- package/features/docs/src/editor/useDocEditor.ts +206 -0
- package/features/docs/src/use-yjs-room-with-awareness.ts +285 -0
- package/features/file-attachments/feature.json +42 -0
- package/features/file-attachments/src/AttachmentsPage.tsx +864 -0
- package/features/file-attachments/src/attachments-constants.ts +9 -0
- package/features/file-attachments/src/attachments-schema.ts +45 -0
- package/features/file-attachments/src/useFilePreview.ts +471 -0
- package/features/file-attachments/src/useMimeTypeDetection.ts +138 -0
- package/features/file-manager/feature.json +24 -0
- package/features/file-manager/src/FileManagerPage.tsx +330 -0
- package/features/integration-test/feature.json +18 -0
- package/features/integration-test/src/IntegrationTestPage.tsx +423 -0
- package/features/integration-test/src/integration.spec.ts +42 -0
- package/features/items/feature.json +33 -0
- package/features/items/src/ItemsPage.tsx +325 -0
- package/features/items/src/items-constants.ts +10 -0
- package/features/items/src/items-schema.ts +27 -0
- package/features/kanban/feature.json +18 -0
- package/features/kanban/src/Kanban.tsx +283 -0
- package/features/landing/feature.json +62 -0
- package/features/landing/src/LandingPage.tsx +627 -0
- package/features/landing/src/primitives.tsx +389 -0
- package/features/landing/src/sections/FAQSection.tsx +110 -0
- package/features/landing/src/sections/FeaturesGridSection.tsx +311 -0
- package/features/landing/src/sections/HowItWorksSection.tsx +62 -0
- package/features/landing/src/sections/LogoCloudSection.tsx +58 -0
- package/features/landing/src/sections/ShowcaseSection.tsx +63 -0
- package/features/landing/src/sections/StatsSection.tsx +31 -0
- package/features/landing/src/sections/TeamSection.tsx +86 -0
- package/features/landing/src/sections/TestimonialsSection.tsx +68 -0
- package/features/landing/src/sections/VideoSection.tsx +71 -0
- package/features/leaderboard/feature.json +33 -0
- package/features/leaderboard/src/LeaderboardPage.tsx +372 -0
- package/features/leaderboard/src/leaderboard-constants.ts +21 -0
- package/features/leaderboard/src/leaderboard-schema.ts +36 -0
- package/features/messaging/feature.json +111 -0
- package/features/messaging/src/ChatMultiPage.tsx +300 -0
- package/features/messaging/src/ChatPage.tsx +75 -0
- package/features/messaging/src/components/chat/ChatHeader.tsx +76 -0
- package/features/messaging/src/components/chat/MessageActionSheet.tsx +168 -0
- package/features/messaging/src/components/chat/MessageInput.tsx +65 -0
- package/features/messaging/src/components/chat/MessageItem.tsx +352 -0
- package/features/messaging/src/components/chat/MessageList.tsx +248 -0
- package/features/messaging/src/components/chat/ReactionPicker.tsx +2 -0
- package/features/messaging/src/components/chat/ThreadPanel.tsx +107 -0
- package/features/messaging/src/components/chat/UserProfilePopover.tsx +158 -0
- package/features/messaging/src/components/chat-multi/AddMemberModal.tsx +188 -0
- package/features/messaging/src/components/chat-multi/BrowseGroupsModal.tsx +163 -0
- package/features/messaging/src/components/chat-multi/ChannelListItem.tsx +147 -0
- package/features/messaging/src/components/chat-multi/ChannelSettingsPanel.tsx +344 -0
- package/features/messaging/src/components/chat-multi/ChannelSidebar.tsx +460 -0
- package/features/messaging/src/components/chat-multi/CreateGroupModal.tsx +190 -0
- package/features/messaging/src/components/chat-multi/InvitationList.tsx +149 -0
- package/features/messaging/src/components/chat-multi/NewMessageModal.tsx +159 -0
- package/features/messaging/src/hooks/useChatChannel.ts +64 -0
- package/features/messaging/src/hooks/useLongPress.ts +74 -0
- package/features/messaging/src/hooks/useMultiChannel.ts +142 -0
- package/features/messaging/src/messaging-schema.ts +25 -0
- package/features/presence-test/feature.json +21 -0
- package/features/presence-test/src/PresenceTestPage.tsx +120 -0
- package/features/sidebar/feature.json +19 -0
- package/features/sidebar/src/AppSidebar.tsx +558 -0
- package/features/tasks/feature.json +33 -0
- package/features/tasks/src/TasksPage.tsx +598 -0
- package/features/tasks/src/tasks-constants.ts +43 -0
- package/features/tasks/src/tasks-schema.ts +56 -0
- package/features/testing/feature.json +49 -0
- package/features/testing/src/TestPage.tsx +145 -0
- package/features/testing/src/cpu-burn-action.ts +51 -0
- package/features/testing/src/subrequest-burn-action.ts +53 -0
- package/features/testing/src/test-schema.ts +23 -0
- package/features/topbar/feature.json +18 -0
- package/features/topbar/src/Topbar.tsx +353 -0
- package/features/tree/feature.json +26 -0
- package/features/tree/src/TreeSidebar.tsx +607 -0
- package/features/tree/src/tree-constants.ts +68 -0
- package/features/tree/src/tree-hooks.ts +258 -0
- package/package.json +4 -2
- package/scripts/add-feature.cjs +654 -0
package/dist/worker.js
CHANGED
|
@@ -206,6 +206,13 @@ var MSG = {
|
|
|
206
206
|
USER_LIST: "user.list",
|
|
207
207
|
SET_ROLE: "user.set_role",
|
|
208
208
|
USER_UPDATE: "user.update",
|
|
209
|
+
// ---- Auth signal -----------------------------------------------------
|
|
210
|
+
// Server → client one-shot on connect from rooms that enforce role-based
|
|
211
|
+
// writes (Canvas, Game, Cron). Tells the client whether this connection
|
|
212
|
+
// can write so its hook can disable local write APIs and the UI can
|
|
213
|
+
// render controls accordingly. Without this, a viewer's click would
|
|
214
|
+
// round-trip to the server and silently come back as ERROR.
|
|
215
|
+
AUTH: "auth",
|
|
209
216
|
// ---- Yjs JSON control frames (binary sync lives in ./yjs.ts) ---------
|
|
210
217
|
YJS_JOIN: "yjs.join",
|
|
211
218
|
YJS_LEAVE: "yjs.leave",
|
|
@@ -239,6 +246,14 @@ var MSG = {
|
|
|
239
246
|
CRON_PAUSE: "cron.pause",
|
|
240
247
|
CRON_RESUME: "cron.resume",
|
|
241
248
|
CRON_STATUS: "cron.status",
|
|
249
|
+
// ---- JobRoom ---------------------------------------------------------
|
|
250
|
+
// Background-job queue with durable, alarm-driven execution. See
|
|
251
|
+
// server/rooms/job-room.ts. Clients enqueue and subscribe to updates;
|
|
252
|
+
// the room broadcasts JOB_UPDATE for every status transition.
|
|
253
|
+
JOB_ENQUEUE: "job.enqueue",
|
|
254
|
+
JOB_CANCEL: "job.cancel",
|
|
255
|
+
JOB_RETRY: "job.retry",
|
|
256
|
+
JOB_UPDATE: "job.update",
|
|
242
257
|
// ---- PresenceRoom ----------------------------------------------------
|
|
243
258
|
PRESENCE_SYNC: "presence.sync",
|
|
244
259
|
PRESENCE_JOIN: "presence.join",
|
|
@@ -934,6 +949,13 @@ var clientBuild = {
|
|
|
934
949
|
cronTrigger: (taskName) => ({ type: MSG.CRON_TRIGGER, payload: { taskName } }),
|
|
935
950
|
cronPause: (taskName) => ({ type: MSG.CRON_PAUSE, payload: { taskName } }),
|
|
936
951
|
cronResume: (taskName) => ({ type: MSG.CRON_RESUME, payload: { taskName } }),
|
|
952
|
+
// Jobs
|
|
953
|
+
jobEnqueue: (requestId, type, payload, maxAttempts) => ({
|
|
954
|
+
type: MSG.JOB_ENQUEUE,
|
|
955
|
+
payload: { requestId, type, payload, maxAttempts }
|
|
956
|
+
}),
|
|
957
|
+
jobCancel: (jobId) => ({ type: MSG.JOB_CANCEL, payload: { jobId } }),
|
|
958
|
+
jobRetry: (jobId) => ({ type: MSG.JOB_RETRY, payload: { jobId } }),
|
|
937
959
|
// Presence
|
|
938
960
|
presenceUpdate: (state) => ({ type: MSG.PRESENCE_UPDATE, payload: state }),
|
|
939
961
|
// Gateway
|
|
@@ -988,6 +1010,17 @@ var serverBuild = {
|
|
|
988
1010
|
cronTasks: (tasks) => ({ type: MSG.CRON_TASKS, payload: { tasks } }),
|
|
989
1011
|
cronHistory: (history) => ({ type: MSG.CRON_HISTORY, payload: { history } }),
|
|
990
1012
|
cronStatus: (tasks, recentHistory) => ({ type: MSG.CRON_STATUS, payload: { tasks, recentHistory } }),
|
|
1013
|
+
// Jobs
|
|
1014
|
+
jobSnapshot: (jobs) => ({ type: MSG.JOB_UPDATE, payload: { kind: "snapshot", jobs } }),
|
|
1015
|
+
jobEnqueued: (job, requestId) => ({
|
|
1016
|
+
type: MSG.JOB_UPDATE,
|
|
1017
|
+
payload: { kind: "enqueued", job, requestId }
|
|
1018
|
+
}),
|
|
1019
|
+
jobProgress: (job) => ({ type: MSG.JOB_UPDATE, payload: { kind: "progress", job } }),
|
|
1020
|
+
jobSucceeded: (job) => ({ type: MSG.JOB_UPDATE, payload: { kind: "succeeded", job } }),
|
|
1021
|
+
jobFailed: (job) => ({ type: MSG.JOB_UPDATE, payload: { kind: "failed", job } }),
|
|
1022
|
+
jobCanceled: (job) => ({ type: MSG.JOB_UPDATE, payload: { kind: "canceled", job } }),
|
|
1023
|
+
jobRetried: (job) => ({ type: MSG.JOB_UPDATE, payload: { kind: "retried", job } }),
|
|
991
1024
|
// Presence
|
|
992
1025
|
presenceSync: (peers) => ({ type: MSG.PRESENCE_SYNC, payload: { peers } }),
|
|
993
1026
|
presenceJoin: (peer) => ({ type: MSG.PRESENCE_JOIN, payload: { peer } }),
|
|
@@ -3268,7 +3301,20 @@ var YjsRoom = class extends BaseRoom {
|
|
|
3268
3301
|
}
|
|
3269
3302
|
};
|
|
3270
3303
|
|
|
3304
|
+
// src/shared/roles.ts
|
|
3305
|
+
var ROLES = {
|
|
3306
|
+
VIEWER: "viewer",
|
|
3307
|
+
MEMBER: "member",
|
|
3308
|
+
ADMIN: "admin"
|
|
3309
|
+
};
|
|
3310
|
+
|
|
3271
3311
|
// src/server/rooms/game-room.ts
|
|
3312
|
+
var GAME_WRITE_TYPES = /* @__PURE__ */ new Set([
|
|
3313
|
+
MSG.GAME_INPUT,
|
|
3314
|
+
MSG.GAME_PLAYER_READY,
|
|
3315
|
+
MSG.GAME_START,
|
|
3316
|
+
MSG.GAME_END
|
|
3317
|
+
]);
|
|
3272
3318
|
var GameRoom = class extends BaseRoom {
|
|
3273
3319
|
config;
|
|
3274
3320
|
players = /* @__PURE__ */ new Map();
|
|
@@ -3324,10 +3370,26 @@ var GameRoom = class extends BaseRoom {
|
|
|
3324
3370
|
// ==========================================================================
|
|
3325
3371
|
onConnect(ws, user) {
|
|
3326
3372
|
this.ensureInitialized();
|
|
3373
|
+
const role = user.role ?? ROLES.VIEWER;
|
|
3374
|
+
const canWrite = role === ROLES.MEMBER || role === ROLES.ADMIN;
|
|
3327
3375
|
const attachment = {
|
|
3328
3376
|
...user,
|
|
3329
|
-
joinedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3377
|
+
joinedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3378
|
+
canWrite
|
|
3330
3379
|
};
|
|
3380
|
+
this.sendTo(ws, { type: MSG.AUTH, payload: { canWrite } });
|
|
3381
|
+
if (!canWrite) {
|
|
3382
|
+
this.sendTo(ws, {
|
|
3383
|
+
type: MSG.GAME_STATE,
|
|
3384
|
+
payload: {
|
|
3385
|
+
state: this.gameState,
|
|
3386
|
+
tick: this.currentTick,
|
|
3387
|
+
players: Array.from(this.players.values()),
|
|
3388
|
+
running: this.running
|
|
3389
|
+
}
|
|
3390
|
+
});
|
|
3391
|
+
return attachment;
|
|
3392
|
+
}
|
|
3331
3393
|
const player = {
|
|
3332
3394
|
userId: user.userId,
|
|
3333
3395
|
userName: user.userName,
|
|
@@ -3356,6 +3418,13 @@ var GameRoom = class extends BaseRoom {
|
|
|
3356
3418
|
async onMessage(ws, user, message) {
|
|
3357
3419
|
this.ensureInitialized();
|
|
3358
3420
|
const { type, payload } = message;
|
|
3421
|
+
if (GAME_WRITE_TYPES.has(type) && !user.canWrite) {
|
|
3422
|
+
this.sendTo(ws, {
|
|
3423
|
+
type: MSG.ERROR,
|
|
3424
|
+
payload: { error: "Write access denied: spectators cannot drive the game" }
|
|
3425
|
+
});
|
|
3426
|
+
return;
|
|
3427
|
+
}
|
|
3359
3428
|
switch (type) {
|
|
3360
3429
|
case MSG.GAME_INPUT: {
|
|
3361
3430
|
this.inputBuffer.push({
|
|
@@ -3504,6 +3573,15 @@ var GameRoom = class extends BaseRoom {
|
|
|
3504
3573
|
|
|
3505
3574
|
// src/server/rooms/canvas-room.ts
|
|
3506
3575
|
import * as Y5 from "yjs";
|
|
3576
|
+
var CANVAS_WRITE_TYPES = /* @__PURE__ */ new Set([
|
|
3577
|
+
MSG.CANVAS_ADD,
|
|
3578
|
+
MSG.CANVAS_MOVE,
|
|
3579
|
+
MSG.CANVAS_RESIZE,
|
|
3580
|
+
MSG.CANVAS_DELETE,
|
|
3581
|
+
MSG.CANVAS_UPDATE,
|
|
3582
|
+
MSG.CANVAS_UNDO,
|
|
3583
|
+
MSG.CANVAS_REDO
|
|
3584
|
+
]);
|
|
3507
3585
|
var CanvasRoom = class extends BaseRoom {
|
|
3508
3586
|
doc = null;
|
|
3509
3587
|
initialized = false;
|
|
@@ -3563,10 +3641,14 @@ var CanvasRoom = class extends BaseRoom {
|
|
|
3563
3641
|
}
|
|
3564
3642
|
onConnect(ws, user) {
|
|
3565
3643
|
this.ensureInitialized();
|
|
3644
|
+
const role = user.role ?? ROLES.VIEWER;
|
|
3645
|
+
const canWrite = role === ROLES.MEMBER || role === ROLES.ADMIN;
|
|
3566
3646
|
const attachment = {
|
|
3567
3647
|
...user,
|
|
3568
|
-
viewport: null
|
|
3648
|
+
viewport: null,
|
|
3649
|
+
canWrite
|
|
3569
3650
|
};
|
|
3651
|
+
this.sendTo(ws, { type: MSG.AUTH, payload: { canWrite } });
|
|
3570
3652
|
const shapes = this.getAllShapes();
|
|
3571
3653
|
this.sendTo(ws, {
|
|
3572
3654
|
type: MSG.CANVAS_SHAPES,
|
|
@@ -3580,6 +3662,13 @@ var CanvasRoom = class extends BaseRoom {
|
|
|
3580
3662
|
async onMessage(ws, user, message) {
|
|
3581
3663
|
this.ensureInitialized();
|
|
3582
3664
|
const { type, payload } = message;
|
|
3665
|
+
if (CANVAS_WRITE_TYPES.has(type) && !user.canWrite) {
|
|
3666
|
+
this.sendTo(ws, {
|
|
3667
|
+
type: MSG.ERROR,
|
|
3668
|
+
payload: { error: "Write access denied: viewer role cannot modify the canvas" }
|
|
3669
|
+
});
|
|
3670
|
+
return;
|
|
3671
|
+
}
|
|
3583
3672
|
switch (type) {
|
|
3584
3673
|
case MSG.CANVAS_ADD: {
|
|
3585
3674
|
const shape = payload;
|
|
@@ -3874,6 +3963,11 @@ var PresenceRoom = class extends BaseRoom {
|
|
|
3874
3963
|
};
|
|
3875
3964
|
|
|
3876
3965
|
// src/server/rooms/cron-room.ts
|
|
3966
|
+
var CRON_WRITE_TYPES = /* @__PURE__ */ new Set([
|
|
3967
|
+
MSG.CRON_TRIGGER,
|
|
3968
|
+
MSG.CRON_PAUSE,
|
|
3969
|
+
MSG.CRON_RESUME
|
|
3970
|
+
]);
|
|
3877
3971
|
var CronRoom = class extends BaseRoom {
|
|
3878
3972
|
tasks;
|
|
3879
3973
|
initialized = false;
|
|
@@ -3952,6 +4046,9 @@ var CronRoom = class extends BaseRoom {
|
|
|
3952
4046
|
}
|
|
3953
4047
|
onConnect(ws, user) {
|
|
3954
4048
|
this.ensureInitialized();
|
|
4049
|
+
const role = user.role ?? ROLES.VIEWER;
|
|
4050
|
+
const canWrite = role === ROLES.MEMBER || role === ROLES.ADMIN;
|
|
4051
|
+
this.sendTo(ws, { type: MSG.AUTH, payload: { canWrite } });
|
|
3955
4052
|
this.sendTo(ws, {
|
|
3956
4053
|
type: MSG.CRON_TASKS,
|
|
3957
4054
|
payload: { tasks: this.getTaskStates() }
|
|
@@ -3960,11 +4057,18 @@ var CronRoom = class extends BaseRoom {
|
|
|
3960
4057
|
type: MSG.CRON_HISTORY,
|
|
3961
4058
|
payload: { history: this.getRecentHistory(50) }
|
|
3962
4059
|
});
|
|
3963
|
-
return user;
|
|
4060
|
+
return { ...user, canWrite };
|
|
3964
4061
|
}
|
|
3965
4062
|
async onMessage(ws, user, message) {
|
|
3966
4063
|
this.ensureInitialized();
|
|
3967
4064
|
const { type, payload } = message;
|
|
4065
|
+
if (CRON_WRITE_TYPES.has(type) && !user.canWrite) {
|
|
4066
|
+
this.sendTo(ws, {
|
|
4067
|
+
type: MSG.ERROR,
|
|
4068
|
+
payload: { error: "Write access denied: viewer role cannot modify cron tasks" }
|
|
4069
|
+
});
|
|
4070
|
+
return;
|
|
4071
|
+
}
|
|
3968
4072
|
switch (type) {
|
|
3969
4073
|
case MSG.CRON_TRIGGER: {
|
|
3970
4074
|
const taskName = payload.taskName;
|
|
@@ -4275,6 +4379,543 @@ function cronMatches(parsed, timezone, date) {
|
|
|
4275
4379
|
return parsed.minute.has(minute) && parsed.hour.has(hour) && parsed.dayOfMonth.has(dayOfMonth) && parsed.month.has(month) && parsed.dayOfWeek.has(dayOfWeek);
|
|
4276
4380
|
}
|
|
4277
4381
|
|
|
4382
|
+
// src/server/rooms/job-room.ts
|
|
4383
|
+
var RUNNING_STALE_MS = 16 * 60 * 1e3;
|
|
4384
|
+
var DEFAULT_RETENTION_MS = 24 * 60 * 60 * 1e3;
|
|
4385
|
+
var DEFAULT_SNAPSHOT_LIMIT = 100;
|
|
4386
|
+
var JobRoom = class extends BaseRoom {
|
|
4387
|
+
initialized = false;
|
|
4388
|
+
defaultMaxAttempts;
|
|
4389
|
+
retentionMs;
|
|
4390
|
+
snapshotLimit;
|
|
4391
|
+
retryBackoffMs;
|
|
4392
|
+
/** In-flight jobs in this isolate; lets same-isolate JOB_CANCEL fire the signal. */
|
|
4393
|
+
inFlight = /* @__PURE__ */ new Map();
|
|
4394
|
+
/** Set by `ctx.continue` inside `executeJob`; reset before every run. */
|
|
4395
|
+
continueState = null;
|
|
4396
|
+
constructor(state, env, config = {}) {
|
|
4397
|
+
super(state, env);
|
|
4398
|
+
this.defaultMaxAttempts = Math.max(1, config.defaultMaxAttempts ?? 1);
|
|
4399
|
+
this.retentionMs = Math.max(0, config.retentionMs ?? DEFAULT_RETENTION_MS);
|
|
4400
|
+
this.snapshotLimit = Math.max(1, config.snapshotLimit ?? DEFAULT_SNAPSHOT_LIMIT);
|
|
4401
|
+
this.retryBackoffMs = Math.max(0, config.retryBackoffMs ?? 1e3);
|
|
4402
|
+
}
|
|
4403
|
+
ensureInitialized() {
|
|
4404
|
+
if (this.initialized) return;
|
|
4405
|
+
this.initialized = true;
|
|
4406
|
+
this.sql.exec(`
|
|
4407
|
+
CREATE TABLE IF NOT EXISTS jobs (
|
|
4408
|
+
id TEXT PRIMARY KEY,
|
|
4409
|
+
type TEXT NOT NULL,
|
|
4410
|
+
status TEXT NOT NULL,
|
|
4411
|
+
payload TEXT,
|
|
4412
|
+
result TEXT,
|
|
4413
|
+
error TEXT,
|
|
4414
|
+
progress REAL,
|
|
4415
|
+
progress_message TEXT,
|
|
4416
|
+
checkpoint TEXT,
|
|
4417
|
+
attempts INTEGER NOT NULL DEFAULT 0,
|
|
4418
|
+
max_attempts INTEGER NOT NULL DEFAULT 1,
|
|
4419
|
+
enqueued_at TEXT NOT NULL,
|
|
4420
|
+
started_at TEXT,
|
|
4421
|
+
completed_at TEXT,
|
|
4422
|
+
enqueued_by TEXT,
|
|
4423
|
+
next_run_at TEXT
|
|
4424
|
+
)
|
|
4425
|
+
`);
|
|
4426
|
+
this.sql.exec(
|
|
4427
|
+
`CREATE INDEX IF NOT EXISTS jobs_status_next_run ON jobs (status, next_run_at)`
|
|
4428
|
+
);
|
|
4429
|
+
this.sql.exec(
|
|
4430
|
+
`CREATE INDEX IF NOT EXISTS jobs_completed_at ON jobs (completed_at)`
|
|
4431
|
+
);
|
|
4432
|
+
this.recoverStuckRunning();
|
|
4433
|
+
this.scheduleNextAlarm();
|
|
4434
|
+
}
|
|
4435
|
+
// ==========================================================================
|
|
4436
|
+
// BaseRoom Lifecycle
|
|
4437
|
+
// ==========================================================================
|
|
4438
|
+
async fetch(request) {
|
|
4439
|
+
this.ensureInitialized();
|
|
4440
|
+
return super.fetch(request);
|
|
4441
|
+
}
|
|
4442
|
+
onConnect(ws, user) {
|
|
4443
|
+
this.ensureInitialized();
|
|
4444
|
+
this.sendTo(ws, serverBuild.jobSnapshot(this.getRecentJobs(this.snapshotLimit)));
|
|
4445
|
+
return user;
|
|
4446
|
+
}
|
|
4447
|
+
async onMessage(ws, user, message) {
|
|
4448
|
+
this.ensureInitialized();
|
|
4449
|
+
const { type, payload } = message;
|
|
4450
|
+
switch (type) {
|
|
4451
|
+
case MSG.JOB_ENQUEUE: {
|
|
4452
|
+
const requestId = String(payload.requestId ?? "");
|
|
4453
|
+
const jobType = String(payload.type ?? "");
|
|
4454
|
+
if (!jobType) {
|
|
4455
|
+
this.sendTo(ws, serverBuild.error("JOB_ENQUEUE missing type"));
|
|
4456
|
+
return;
|
|
4457
|
+
}
|
|
4458
|
+
const job = this.insertJob({
|
|
4459
|
+
type: jobType,
|
|
4460
|
+
payload: payload.payload,
|
|
4461
|
+
maxAttempts: typeof payload.maxAttempts === "number" ? Math.max(1, Math.floor(payload.maxAttempts)) : void 0,
|
|
4462
|
+
enqueuedBy: user.userId
|
|
4463
|
+
});
|
|
4464
|
+
this.sendTo(ws, serverBuild.jobEnqueued(job, requestId));
|
|
4465
|
+
this.broadcast(serverBuild.jobEnqueued(job), ws);
|
|
4466
|
+
this.scheduleNextAlarm();
|
|
4467
|
+
break;
|
|
4468
|
+
}
|
|
4469
|
+
case MSG.JOB_CANCEL: {
|
|
4470
|
+
const jobId = String(payload.jobId ?? "");
|
|
4471
|
+
if (!jobId) {
|
|
4472
|
+
this.sendTo(ws, serverBuild.error("JOB_CANCEL missing jobId"));
|
|
4473
|
+
return;
|
|
4474
|
+
}
|
|
4475
|
+
this.cancelJob(jobId);
|
|
4476
|
+
break;
|
|
4477
|
+
}
|
|
4478
|
+
case MSG.JOB_RETRY: {
|
|
4479
|
+
const jobId = String(payload.jobId ?? "");
|
|
4480
|
+
if (!jobId) {
|
|
4481
|
+
this.sendTo(ws, serverBuild.error("JOB_RETRY missing jobId"));
|
|
4482
|
+
return;
|
|
4483
|
+
}
|
|
4484
|
+
this.retryJob(jobId);
|
|
4485
|
+
break;
|
|
4486
|
+
}
|
|
4487
|
+
default:
|
|
4488
|
+
this.sendTo(ws, serverBuild.error(`Unknown job message type: ${type}`));
|
|
4489
|
+
}
|
|
4490
|
+
}
|
|
4491
|
+
async onAlarm() {
|
|
4492
|
+
this.ensureInitialized();
|
|
4493
|
+
await this.drainDueJobs();
|
|
4494
|
+
this.pruneExpired();
|
|
4495
|
+
this.scheduleNextAlarm();
|
|
4496
|
+
}
|
|
4497
|
+
// ==========================================================================
|
|
4498
|
+
// HTTP — internal enqueue path for cross-isolate callers
|
|
4499
|
+
// ==========================================================================
|
|
4500
|
+
/**
|
|
4501
|
+
* One internal endpoint, `POST /enqueue`, used by the `enqueueJob`
|
|
4502
|
+
* helper at the bottom of this file. The room is not meant to be
|
|
4503
|
+
* reachable from the public internet; callers route through the app
|
|
4504
|
+
* worker (or call `this.enqueue(...)` directly from a subclass).
|
|
4505
|
+
*
|
|
4506
|
+
* Subclasses overriding `onRequest` should call `super.onRequest(req)`
|
|
4507
|
+
* for paths they don't handle.
|
|
4508
|
+
*/
|
|
4509
|
+
async onRequest(request) {
|
|
4510
|
+
this.ensureInitialized();
|
|
4511
|
+
const url = new URL(request.url);
|
|
4512
|
+
if (request.method !== "POST" || !url.pathname.endsWith("/enqueue")) {
|
|
4513
|
+
return new Response("Not Found", { status: 404 });
|
|
4514
|
+
}
|
|
4515
|
+
const body = await request.json().catch(() => null);
|
|
4516
|
+
if (!body || typeof body.type !== "string" || !body.type) {
|
|
4517
|
+
return new Response(JSON.stringify({ error: "type required" }), {
|
|
4518
|
+
status: 400,
|
|
4519
|
+
headers: { "Content-Type": "application/json" }
|
|
4520
|
+
});
|
|
4521
|
+
}
|
|
4522
|
+
const job = this.insertJob({
|
|
4523
|
+
type: body.type,
|
|
4524
|
+
payload: body.payload,
|
|
4525
|
+
maxAttempts: typeof body.maxAttempts === "number" ? Math.max(1, Math.floor(body.maxAttempts)) : void 0,
|
|
4526
|
+
enqueuedBy: typeof body.enqueuedBy === "string" ? body.enqueuedBy : void 0
|
|
4527
|
+
});
|
|
4528
|
+
this.broadcast(serverBuild.jobEnqueued(job));
|
|
4529
|
+
this.scheduleNextAlarm();
|
|
4530
|
+
return new Response(JSON.stringify({ job }), {
|
|
4531
|
+
status: 200,
|
|
4532
|
+
headers: { "Content-Type": "application/json" }
|
|
4533
|
+
});
|
|
4534
|
+
}
|
|
4535
|
+
// ==========================================================================
|
|
4536
|
+
// Public enqueue surface (server-side callers in the same isolate)
|
|
4537
|
+
// ==========================================================================
|
|
4538
|
+
/**
|
|
4539
|
+
* Synchronously persist a new job and arm the alarm. Returns the new
|
|
4540
|
+
* job. This is the in-isolate path — useful for a subclass `onJob`
|
|
4541
|
+
* that wants to chain follow-up work without paying a fetch hop:
|
|
4542
|
+
*
|
|
4543
|
+
* protected async onJob(job: Job, ctx: JobContext) {
|
|
4544
|
+
* // ... do work ...
|
|
4545
|
+
* if (needsFollowup) this.enqueue('followup', { parent: job.id })
|
|
4546
|
+
* }
|
|
4547
|
+
*
|
|
4548
|
+
* Workers code that lives outside the DO isolate (HTTP routes, cron
|
|
4549
|
+
* tasks, server actions) cannot call this directly — they must go
|
|
4550
|
+
* through the `enqueueJob` helper at the bottom of this file, which
|
|
4551
|
+
* routes via the DO's `/enqueue` HTTP endpoint.
|
|
4552
|
+
*/
|
|
4553
|
+
enqueue(type, payload, options) {
|
|
4554
|
+
this.ensureInitialized();
|
|
4555
|
+
const job = this.insertJob({
|
|
4556
|
+
type,
|
|
4557
|
+
payload,
|
|
4558
|
+
maxAttempts: options?.maxAttempts,
|
|
4559
|
+
enqueuedBy: options?.enqueuedBy
|
|
4560
|
+
});
|
|
4561
|
+
this.broadcast(serverBuild.jobEnqueued(job));
|
|
4562
|
+
this.scheduleNextAlarm();
|
|
4563
|
+
return job;
|
|
4564
|
+
}
|
|
4565
|
+
// ==========================================================================
|
|
4566
|
+
// Mutation helpers
|
|
4567
|
+
// ==========================================================================
|
|
4568
|
+
insertJob(input) {
|
|
4569
|
+
const id = crypto.randomUUID();
|
|
4570
|
+
const enqueuedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4571
|
+
const maxAttempts = Math.max(1, input.maxAttempts ?? this.defaultMaxAttempts);
|
|
4572
|
+
const payloadJson = input.payload === void 0 ? null : safeStringify(input.payload);
|
|
4573
|
+
this.sql.exec(
|
|
4574
|
+
`INSERT INTO jobs (id, type, status, payload, attempts, max_attempts, enqueued_at, enqueued_by, next_run_at)
|
|
4575
|
+
VALUES (?, ?, 'queued', ?, 0, ?, ?, ?, ?)`,
|
|
4576
|
+
id,
|
|
4577
|
+
input.type,
|
|
4578
|
+
payloadJson,
|
|
4579
|
+
maxAttempts,
|
|
4580
|
+
enqueuedAt,
|
|
4581
|
+
input.enqueuedBy ?? null,
|
|
4582
|
+
enqueuedAt
|
|
4583
|
+
);
|
|
4584
|
+
return this.getJobById(id);
|
|
4585
|
+
}
|
|
4586
|
+
cancelJob(jobId) {
|
|
4587
|
+
const row = this.sql.exec(`SELECT status FROM jobs WHERE id = ?`, jobId).toArray()[0];
|
|
4588
|
+
if (!row) return;
|
|
4589
|
+
if (row.status === "succeeded" || row.status === "failed" || row.status === "canceled") {
|
|
4590
|
+
return;
|
|
4591
|
+
}
|
|
4592
|
+
const completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4593
|
+
this.sql.exec(
|
|
4594
|
+
`UPDATE jobs
|
|
4595
|
+
SET status = 'canceled',
|
|
4596
|
+
completed_at = ?,
|
|
4597
|
+
checkpoint = NULL,
|
|
4598
|
+
next_run_at = NULL
|
|
4599
|
+
WHERE id = ?`,
|
|
4600
|
+
completedAt,
|
|
4601
|
+
jobId
|
|
4602
|
+
);
|
|
4603
|
+
const controller = this.inFlight.get(jobId);
|
|
4604
|
+
if (controller) controller.abort();
|
|
4605
|
+
const job = this.getJobById(jobId);
|
|
4606
|
+
if (job) this.broadcast(serverBuild.jobCanceled(job));
|
|
4607
|
+
}
|
|
4608
|
+
retryJob(jobId) {
|
|
4609
|
+
const row = this.sql.exec(`SELECT status, attempts, max_attempts FROM jobs WHERE id = ?`, jobId).toArray()[0];
|
|
4610
|
+
if (!row) return;
|
|
4611
|
+
if (row.status !== "failed" && row.status !== "canceled") return;
|
|
4612
|
+
const newMax = Math.max(row.max_attempts, row.attempts + 1);
|
|
4613
|
+
this.sql.exec(
|
|
4614
|
+
`UPDATE jobs
|
|
4615
|
+
SET status = 'queued',
|
|
4616
|
+
max_attempts = ?,
|
|
4617
|
+
error = NULL,
|
|
4618
|
+
result = NULL,
|
|
4619
|
+
completed_at = NULL,
|
|
4620
|
+
next_run_at = ?
|
|
4621
|
+
WHERE id = ?`,
|
|
4622
|
+
newMax,
|
|
4623
|
+
(/* @__PURE__ */ new Date()).toISOString(),
|
|
4624
|
+
jobId
|
|
4625
|
+
);
|
|
4626
|
+
const job = this.getJobById(jobId);
|
|
4627
|
+
if (job) this.broadcast(serverBuild.jobRetried(job));
|
|
4628
|
+
this.scheduleNextAlarm();
|
|
4629
|
+
}
|
|
4630
|
+
// ==========================================================================
|
|
4631
|
+
// Alarm / execution
|
|
4632
|
+
// ==========================================================================
|
|
4633
|
+
/**
|
|
4634
|
+
* Drain due jobs in FIFO order. All due jobs share the alarm's
|
|
4635
|
+
* 15-min wall budget (a CF property, not a per-job allocation).
|
|
4636
|
+
*/
|
|
4637
|
+
async drainDueJobs() {
|
|
4638
|
+
const cutoff = (/* @__PURE__ */ new Date()).toISOString();
|
|
4639
|
+
while (true) {
|
|
4640
|
+
const row = this.sql.exec(
|
|
4641
|
+
`SELECT * FROM jobs
|
|
4642
|
+
WHERE status = 'queued'
|
|
4643
|
+
AND (next_run_at IS NULL OR next_run_at <= ?)
|
|
4644
|
+
ORDER BY enqueued_at ASC
|
|
4645
|
+
LIMIT 1`,
|
|
4646
|
+
cutoff
|
|
4647
|
+
).toArray()[0];
|
|
4648
|
+
if (!row) return;
|
|
4649
|
+
await this.executeJob(row);
|
|
4650
|
+
}
|
|
4651
|
+
}
|
|
4652
|
+
async executeJob(row) {
|
|
4653
|
+
const startedAt = row.started_at ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
4654
|
+
const attemptsBefore = row.attempts;
|
|
4655
|
+
const attempt = attemptsBefore + 1;
|
|
4656
|
+
this.sql.exec(
|
|
4657
|
+
`UPDATE jobs SET status = 'running', started_at = ?, attempts = ? WHERE id = ?`,
|
|
4658
|
+
startedAt,
|
|
4659
|
+
attempt,
|
|
4660
|
+
row.id
|
|
4661
|
+
);
|
|
4662
|
+
const controller = new AbortController();
|
|
4663
|
+
this.inFlight.set(row.id, controller);
|
|
4664
|
+
this.continueState = null;
|
|
4665
|
+
const job = {
|
|
4666
|
+
...rowToJobView(row),
|
|
4667
|
+
status: "running",
|
|
4668
|
+
attempts: attempt,
|
|
4669
|
+
startedAt
|
|
4670
|
+
};
|
|
4671
|
+
let outcome = "succeeded";
|
|
4672
|
+
let result;
|
|
4673
|
+
let errorMessage;
|
|
4674
|
+
try {
|
|
4675
|
+
const ctx = this.makeContext(row.id, controller.signal);
|
|
4676
|
+
result = await this.onJob(job, ctx);
|
|
4677
|
+
if (this.continueState) outcome = "continued";
|
|
4678
|
+
} catch (e) {
|
|
4679
|
+
outcome = "failed";
|
|
4680
|
+
errorMessage = e instanceof Error ? e.message : String(e);
|
|
4681
|
+
console.error(`[JobRoom] Job "${row.type}" (${row.id}) failed:`, e);
|
|
4682
|
+
} finally {
|
|
4683
|
+
this.inFlight.delete(row.id);
|
|
4684
|
+
}
|
|
4685
|
+
const currentStatus = this.readStatus(row.id);
|
|
4686
|
+
if (currentStatus === "canceled") {
|
|
4687
|
+
this.continueState = null;
|
|
4688
|
+
return;
|
|
4689
|
+
}
|
|
4690
|
+
if (outcome === "continued") {
|
|
4691
|
+
const state = this.continueState;
|
|
4692
|
+
this.continueState = null;
|
|
4693
|
+
const nextRunAt = new Date(Date.now() + Math.max(0, state.afterMs)).toISOString();
|
|
4694
|
+
this.sql.exec(
|
|
4695
|
+
`UPDATE jobs
|
|
4696
|
+
SET status = 'queued',
|
|
4697
|
+
checkpoint = ?,
|
|
4698
|
+
next_run_at = ?
|
|
4699
|
+
WHERE id = ?`,
|
|
4700
|
+
safeStringify(state.checkpoint),
|
|
4701
|
+
nextRunAt,
|
|
4702
|
+
row.id
|
|
4703
|
+
);
|
|
4704
|
+
const updated2 = this.getJobById(row.id);
|
|
4705
|
+
if (updated2) this.broadcast(serverBuild.jobProgress(updated2));
|
|
4706
|
+
return;
|
|
4707
|
+
}
|
|
4708
|
+
const completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4709
|
+
if (outcome === "succeeded") {
|
|
4710
|
+
this.sql.exec(
|
|
4711
|
+
`UPDATE jobs
|
|
4712
|
+
SET status = 'succeeded',
|
|
4713
|
+
result = ?,
|
|
4714
|
+
completed_at = ?,
|
|
4715
|
+
error = NULL,
|
|
4716
|
+
checkpoint = NULL,
|
|
4717
|
+
next_run_at = NULL,
|
|
4718
|
+
progress = NULL,
|
|
4719
|
+
progress_message = NULL
|
|
4720
|
+
WHERE id = ?`,
|
|
4721
|
+
result === void 0 ? null : safeStringify(result),
|
|
4722
|
+
completedAt,
|
|
4723
|
+
row.id
|
|
4724
|
+
);
|
|
4725
|
+
const updated2 = this.getJobById(row.id);
|
|
4726
|
+
if (updated2) this.broadcast(serverBuild.jobSucceeded(updated2));
|
|
4727
|
+
return;
|
|
4728
|
+
}
|
|
4729
|
+
if (attempt < row.max_attempts) {
|
|
4730
|
+
const nextRunAt = new Date(Date.now() + this.retryBackoffMs).toISOString();
|
|
4731
|
+
this.sql.exec(
|
|
4732
|
+
`UPDATE jobs
|
|
4733
|
+
SET status = 'queued',
|
|
4734
|
+
error = ?,
|
|
4735
|
+
next_run_at = ?,
|
|
4736
|
+
started_at = NULL,
|
|
4737
|
+
progress = NULL,
|
|
4738
|
+
progress_message = NULL
|
|
4739
|
+
WHERE id = ?`,
|
|
4740
|
+
errorMessage ?? "unknown error",
|
|
4741
|
+
nextRunAt,
|
|
4742
|
+
row.id
|
|
4743
|
+
);
|
|
4744
|
+
const updated2 = this.getJobById(row.id);
|
|
4745
|
+
if (updated2) this.broadcast(serverBuild.jobRetried(updated2));
|
|
4746
|
+
return;
|
|
4747
|
+
}
|
|
4748
|
+
this.sql.exec(
|
|
4749
|
+
`UPDATE jobs
|
|
4750
|
+
SET status = 'failed',
|
|
4751
|
+
error = ?,
|
|
4752
|
+
completed_at = ?,
|
|
4753
|
+
checkpoint = NULL,
|
|
4754
|
+
next_run_at = NULL,
|
|
4755
|
+
progress = NULL,
|
|
4756
|
+
progress_message = NULL
|
|
4757
|
+
WHERE id = ?`,
|
|
4758
|
+
errorMessage ?? "unknown error",
|
|
4759
|
+
completedAt,
|
|
4760
|
+
row.id
|
|
4761
|
+
);
|
|
4762
|
+
const updated = this.getJobById(row.id);
|
|
4763
|
+
if (updated) this.broadcast(serverBuild.jobFailed(updated));
|
|
4764
|
+
}
|
|
4765
|
+
makeContext(jobId, signal) {
|
|
4766
|
+
return {
|
|
4767
|
+
progress: (value, message) => {
|
|
4768
|
+
const clamped = Math.max(0, Math.min(1, value));
|
|
4769
|
+
this.sql.exec(
|
|
4770
|
+
`UPDATE jobs SET progress = ?, progress_message = ? WHERE id = ?`,
|
|
4771
|
+
clamped,
|
|
4772
|
+
message ?? null,
|
|
4773
|
+
jobId
|
|
4774
|
+
);
|
|
4775
|
+
const updated = this.getJobById(jobId);
|
|
4776
|
+
if (updated) this.broadcast(serverBuild.jobProgress(updated));
|
|
4777
|
+
},
|
|
4778
|
+
continue: (state, options) => {
|
|
4779
|
+
this.continueState = {
|
|
4780
|
+
checkpoint: state,
|
|
4781
|
+
afterMs: Math.max(0, options?.afterMs ?? 0)
|
|
4782
|
+
};
|
|
4783
|
+
},
|
|
4784
|
+
signal
|
|
4785
|
+
};
|
|
4786
|
+
}
|
|
4787
|
+
readStatus(jobId) {
|
|
4788
|
+
const row = this.sql.exec(`SELECT status FROM jobs WHERE id = ?`, jobId).toArray()[0];
|
|
4789
|
+
return row?.status ?? null;
|
|
4790
|
+
}
|
|
4791
|
+
// ==========================================================================
|
|
4792
|
+
// Scheduling / recovery / pruning
|
|
4793
|
+
// ==========================================================================
|
|
4794
|
+
scheduleNextAlarm() {
|
|
4795
|
+
const row = this.sql.exec(
|
|
4796
|
+
`SELECT next_run_at FROM jobs
|
|
4797
|
+
WHERE status = 'queued'
|
|
4798
|
+
ORDER BY (next_run_at IS NULL) ASC, next_run_at ASC
|
|
4799
|
+
LIMIT 1`
|
|
4800
|
+
).toArray()[0];
|
|
4801
|
+
if (!row) return;
|
|
4802
|
+
const now = Date.now();
|
|
4803
|
+
const target = row.next_run_at ? Math.max(now, new Date(row.next_run_at).getTime()) : now;
|
|
4804
|
+
this.state.storage.setAlarm(Math.max(target, now + 50));
|
|
4805
|
+
}
|
|
4806
|
+
recoverStuckRunning() {
|
|
4807
|
+
const cutoff = new Date(Date.now() - RUNNING_STALE_MS).toISOString();
|
|
4808
|
+
const stuck = this.sql.exec(
|
|
4809
|
+
`SELECT id, attempts, max_attempts FROM jobs
|
|
4810
|
+
WHERE status = 'running' AND started_at < ?`,
|
|
4811
|
+
cutoff
|
|
4812
|
+
).toArray();
|
|
4813
|
+
for (const r of stuck) {
|
|
4814
|
+
if (r.attempts >= r.max_attempts) {
|
|
4815
|
+
this.sql.exec(
|
|
4816
|
+
`UPDATE jobs
|
|
4817
|
+
SET status = 'failed',
|
|
4818
|
+
error = 'Isolate recycled or wall time exceeded',
|
|
4819
|
+
completed_at = ?,
|
|
4820
|
+
checkpoint = NULL,
|
|
4821
|
+
next_run_at = NULL
|
|
4822
|
+
WHERE id = ?`,
|
|
4823
|
+
(/* @__PURE__ */ new Date()).toISOString(),
|
|
4824
|
+
r.id
|
|
4825
|
+
);
|
|
4826
|
+
} else {
|
|
4827
|
+
this.sql.exec(
|
|
4828
|
+
`UPDATE jobs
|
|
4829
|
+
SET status = 'queued',
|
|
4830
|
+
started_at = NULL,
|
|
4831
|
+
next_run_at = ?,
|
|
4832
|
+
error = 'Previous attempt killed mid-run; retrying',
|
|
4833
|
+
progress = NULL,
|
|
4834
|
+
progress_message = NULL
|
|
4835
|
+
WHERE id = ?`,
|
|
4836
|
+
(/* @__PURE__ */ new Date()).toISOString(),
|
|
4837
|
+
r.id
|
|
4838
|
+
);
|
|
4839
|
+
}
|
|
4840
|
+
}
|
|
4841
|
+
}
|
|
4842
|
+
pruneExpired() {
|
|
4843
|
+
if (this.retentionMs === 0) return;
|
|
4844
|
+
const cutoff = new Date(Date.now() - this.retentionMs).toISOString();
|
|
4845
|
+
this.sql.exec(
|
|
4846
|
+
`DELETE FROM jobs
|
|
4847
|
+
WHERE status IN ('succeeded', 'failed', 'canceled')
|
|
4848
|
+
AND completed_at IS NOT NULL
|
|
4849
|
+
AND completed_at < ?`,
|
|
4850
|
+
cutoff
|
|
4851
|
+
);
|
|
4852
|
+
}
|
|
4853
|
+
// ==========================================================================
|
|
4854
|
+
// Read helpers
|
|
4855
|
+
// ==========================================================================
|
|
4856
|
+
getRecentJobs(limit) {
|
|
4857
|
+
const rows = this.sql.exec(
|
|
4858
|
+
`SELECT * FROM jobs
|
|
4859
|
+
ORDER BY (status IN ('queued','running')) DESC, enqueued_at DESC
|
|
4860
|
+
LIMIT ?`,
|
|
4861
|
+
limit
|
|
4862
|
+
).toArray();
|
|
4863
|
+
return rows.map(rowToJobView);
|
|
4864
|
+
}
|
|
4865
|
+
getJobById(id) {
|
|
4866
|
+
const row = this.sql.exec(`SELECT * FROM jobs WHERE id = ?`, id).toArray()[0];
|
|
4867
|
+
return row ? rowToJobView(row) : null;
|
|
4868
|
+
}
|
|
4869
|
+
};
|
|
4870
|
+
async function enqueueJob(namespace, roomId, type, payload, options) {
|
|
4871
|
+
const stub = namespace.get(namespace.idFromName(roomId));
|
|
4872
|
+
const res = await stub.fetch(
|
|
4873
|
+
new Request("https://job-room.internal/enqueue", {
|
|
4874
|
+
method: "POST",
|
|
4875
|
+
headers: { "Content-Type": "application/json" },
|
|
4876
|
+
body: JSON.stringify({
|
|
4877
|
+
type,
|
|
4878
|
+
payload,
|
|
4879
|
+
maxAttempts: options?.maxAttempts,
|
|
4880
|
+
enqueuedBy: options?.enqueuedBy
|
|
4881
|
+
})
|
|
4882
|
+
})
|
|
4883
|
+
);
|
|
4884
|
+
if (!res.ok) {
|
|
4885
|
+
const text = await res.text().catch(() => "");
|
|
4886
|
+
throw new Error(`enqueueJob failed: ${res.status} ${text}`);
|
|
4887
|
+
}
|
|
4888
|
+
const data = await res.json();
|
|
4889
|
+
if (!data.job?.id) throw new Error("enqueueJob: malformed response");
|
|
4890
|
+
return data.job.id;
|
|
4891
|
+
}
|
|
4892
|
+
function rowToJobView(row) {
|
|
4893
|
+
return {
|
|
4894
|
+
id: row.id,
|
|
4895
|
+
type: row.type,
|
|
4896
|
+
status: row.status,
|
|
4897
|
+
payload: row.payload == null ? void 0 : JSON.parse(row.payload),
|
|
4898
|
+
result: row.result == null ? void 0 : JSON.parse(row.result),
|
|
4899
|
+
error: row.error ?? void 0,
|
|
4900
|
+
progress: row.progress ?? void 0,
|
|
4901
|
+
progressMessage: row.progress_message ?? void 0,
|
|
4902
|
+
attempts: row.attempts,
|
|
4903
|
+
maxAttempts: row.max_attempts,
|
|
4904
|
+
enqueuedAt: row.enqueued_at,
|
|
4905
|
+
startedAt: row.started_at,
|
|
4906
|
+
completedAt: row.completed_at,
|
|
4907
|
+
enqueuedBy: row.enqueued_by,
|
|
4908
|
+
resumeFrom: row.checkpoint == null ? void 0 : JSON.parse(row.checkpoint)
|
|
4909
|
+
};
|
|
4910
|
+
}
|
|
4911
|
+
function safeStringify(value) {
|
|
4912
|
+
try {
|
|
4913
|
+
return JSON.stringify(value);
|
|
4914
|
+
} catch {
|
|
4915
|
+
return JSON.stringify({ __unserializable__: String(value) });
|
|
4916
|
+
}
|
|
4917
|
+
}
|
|
4918
|
+
|
|
4278
4919
|
// src/server/rooms/do-manifest.ts
|
|
4279
4920
|
var DEFAULT_DO_MANIFEST = [
|
|
4280
4921
|
{ binding: "RECORD_ROOMS", className: "AppRecordRoom", sqlite: true },
|
|
@@ -4355,7 +4996,6 @@ var RESERVED_BINDING_NAMES = /* @__PURE__ */ new Set([
|
|
|
4355
4996
|
"AUTH_WORKER_URL",
|
|
4356
4997
|
"APP_IDENTITY_TOKEN",
|
|
4357
4998
|
"APP_OWNER_JWT",
|
|
4358
|
-
"INTERNAL_STORAGE_HMAC_SECRET",
|
|
4359
4999
|
"USAGE_EVENTS"
|
|
4360
5000
|
]);
|
|
4361
5001
|
function validateBindingManifest(manifest) {
|
|
@@ -5189,6 +5829,11 @@ var RESERVED_COLLECTION_NAMES = new Set(
|
|
|
5189
5829
|
|
|
5190
5830
|
// src/server/utils/scoped-r2-files.ts
|
|
5191
5831
|
var CORS_HEADERS3 = { "Access-Control-Allow-Origin": "*" };
|
|
5832
|
+
function contentDisposition(originalName) {
|
|
5833
|
+
const safe = originalName.replace(/[\x00-\x1f\x7f"\\]/g, "_") || "download";
|
|
5834
|
+
const encoded = encodeURIComponent(originalName);
|
|
5835
|
+
return `inline; filename="${safe}"; filename*=UTF-8''${encoded}`;
|
|
5836
|
+
}
|
|
5192
5837
|
function sanitizeSubpath(raw) {
|
|
5193
5838
|
if (!raw) return "";
|
|
5194
5839
|
const segments = raw.split("/");
|
|
@@ -5198,6 +5843,20 @@ function sanitizeSubpath(raw) {
|
|
|
5198
5843
|
function isKeyWithinPrefix(key, prefix) {
|
|
5199
5844
|
return key.startsWith(prefix);
|
|
5200
5845
|
}
|
|
5846
|
+
var DANGEROUS_MIME_TYPES = /* @__PURE__ */ new Set([
|
|
5847
|
+
"text/html",
|
|
5848
|
+
"application/xhtml+xml",
|
|
5849
|
+
"image/svg+xml",
|
|
5850
|
+
"application/xml",
|
|
5851
|
+
"text/xml",
|
|
5852
|
+
"application/javascript",
|
|
5853
|
+
"text/javascript",
|
|
5854
|
+
"application/ecmascript",
|
|
5855
|
+
"text/ecmascript"
|
|
5856
|
+
]);
|
|
5857
|
+
function normalizeMimeType(raw) {
|
|
5858
|
+
return raw.split(";")[0].trim().toLowerCase();
|
|
5859
|
+
}
|
|
5201
5860
|
function createScopedR2Handler(config) {
|
|
5202
5861
|
const requireAuthForMutations = config.requireAuthForMutations ?? true;
|
|
5203
5862
|
return async (request, url, bucket, auth) => {
|
|
@@ -5287,6 +5946,13 @@ async function handleUpload(request, url, bucket, prefix, userId) {
|
|
|
5287
5946
|
fileName = body.name;
|
|
5288
5947
|
mimeType = body.mimeType || "application/octet-stream";
|
|
5289
5948
|
}
|
|
5949
|
+
mimeType = normalizeMimeType(mimeType) || "application/octet-stream";
|
|
5950
|
+
if (DANGEROUS_MIME_TYPES.has(mimeType)) {
|
|
5951
|
+
return Response.json(
|
|
5952
|
+
{ error: `Unsupported media type: ${mimeType}` },
|
|
5953
|
+
{ status: 415, headers: CORS_HEADERS3 }
|
|
5954
|
+
);
|
|
5955
|
+
}
|
|
5290
5956
|
const requestedKey = url.searchParams.get("key");
|
|
5291
5957
|
let key;
|
|
5292
5958
|
if (requestedKey !== null) {
|
|
@@ -5351,271 +6017,95 @@ async function handleDownload(bucket, key) {
|
|
|
5351
6017
|
}
|
|
5352
6018
|
const headers = new Headers();
|
|
5353
6019
|
headers.set("Content-Type", object.httpMetadata?.contentType || "application/octet-stream");
|
|
6020
|
+
headers.set("X-Content-Type-Options", "nosniff");
|
|
5354
6021
|
headers.set("Cache-Control", "public, max-age=31536000, immutable");
|
|
5355
6022
|
headers.set("Access-Control-Allow-Origin", "*");
|
|
5356
6023
|
if (object.customMetadata?.originalName) {
|
|
5357
|
-
headers.set("Content-Disposition",
|
|
6024
|
+
headers.set("Content-Disposition", contentDisposition(object.customMetadata.originalName));
|
|
5358
6025
|
}
|
|
5359
6026
|
return new Response(object.body, { headers });
|
|
5360
6027
|
}
|
|
5361
6028
|
|
|
5362
|
-
// src/server/
|
|
5363
|
-
|
|
5364
|
-
|
|
5365
|
-
|
|
5366
|
-
|
|
5367
|
-
function bufferToHex(bytes) {
|
|
5368
|
-
const view = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
|
|
5369
|
-
let hex = "";
|
|
5370
|
-
for (const byte of view) {
|
|
5371
|
-
hex += byte.toString(16).padStart(2, "0");
|
|
5372
|
-
}
|
|
5373
|
-
return hex;
|
|
5374
|
-
}
|
|
5375
|
-
function normalizeArray(value) {
|
|
5376
|
-
if (value == null) return void 0;
|
|
5377
|
-
if (Array.isArray(value)) {
|
|
5378
|
-
return value.length ? value : void 0;
|
|
6029
|
+
// src/server/utils/proxies.ts
|
|
6030
|
+
function resolveApiTransport(env) {
|
|
6031
|
+
if (env.API_WORKER) return { kind: "binding", fetcher: env.API_WORKER };
|
|
6032
|
+
if (env.API_WORKER_URL) {
|
|
6033
|
+
return { kind: "url", baseUrl: env.API_WORKER_URL.replace(/\/$/, "") };
|
|
5379
6034
|
}
|
|
5380
|
-
|
|
6035
|
+
throw new Error(
|
|
6036
|
+
"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."
|
|
6037
|
+
);
|
|
5381
6038
|
}
|
|
5382
|
-
function
|
|
5383
|
-
if (
|
|
5384
|
-
|
|
5385
|
-
|
|
5386
|
-
const payloadSegment = parts[1];
|
|
5387
|
-
if (!payloadSegment) return void 0;
|
|
5388
|
-
if (!BASE64_URL_REGEX.test(payloadSegment)) return void 0;
|
|
5389
|
-
try {
|
|
5390
|
-
const normalized = payloadSegment.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(payloadSegment.length / 4) * 4, "=");
|
|
5391
|
-
const bytes = base64ToUint8Array(normalized);
|
|
5392
|
-
const json = new TextDecoder().decode(bytes);
|
|
5393
|
-
const payload = JSON.parse(json);
|
|
5394
|
-
return {
|
|
5395
|
-
iss: payload.iss ?? null,
|
|
5396
|
-
aud: payload.aud ?? null,
|
|
5397
|
-
azp: payload?.azp ?? null,
|
|
5398
|
-
exp: payload.exp ?? null,
|
|
5399
|
-
iat: payload.iat ?? null
|
|
5400
|
-
};
|
|
5401
|
-
} catch {
|
|
5402
|
-
return void 0;
|
|
6039
|
+
function resolvePlatformTransport(env) {
|
|
6040
|
+
if (env.PLATFORM_WORKER) return { kind: "binding", fetcher: env.PLATFORM_WORKER };
|
|
6041
|
+
if (env.PLATFORM_WORKER_URL) {
|
|
6042
|
+
return { kind: "url", baseUrl: env.PLATFORM_WORKER_URL.replace(/\/$/, "") };
|
|
5403
6043
|
}
|
|
6044
|
+
throw new Error(
|
|
6045
|
+
"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."
|
|
6046
|
+
);
|
|
5404
6047
|
}
|
|
5405
|
-
function
|
|
5406
|
-
|
|
5407
|
-
|
|
5408
|
-
|
|
5409
|
-
|
|
5410
|
-
|
|
5411
|
-
|
|
5412
|
-
|
|
5413
|
-
|
|
5414
|
-
}
|
|
5415
|
-
return out;
|
|
6048
|
+
function buildTargetUrl(transport, path, internalHost) {
|
|
6049
|
+
let pathAndSearch;
|
|
6050
|
+
if (path.startsWith("http://") || path.startsWith("https://")) {
|
|
6051
|
+
const u = new URL(path);
|
|
6052
|
+
pathAndSearch = u.pathname + u.search;
|
|
6053
|
+
} else if (path.startsWith("/")) {
|
|
6054
|
+
pathAndSearch = path;
|
|
6055
|
+
} else {
|
|
6056
|
+
pathAndSearch = "/" + path;
|
|
5416
6057
|
}
|
|
5417
|
-
if (
|
|
5418
|
-
return
|
|
6058
|
+
if (transport.kind === "binding") {
|
|
6059
|
+
return `https://${internalHost}${pathAndSearch}`;
|
|
5419
6060
|
}
|
|
5420
|
-
|
|
6061
|
+
return `${transport.baseUrl}${pathAndSearch}`;
|
|
5421
6062
|
}
|
|
5422
|
-
|
|
5423
|
-
|
|
5424
|
-
|
|
5425
|
-
|
|
5426
|
-
|
|
5427
|
-
for (const pattern of patterns) {
|
|
5428
|
-
if (pattern === azp) return true;
|
|
5429
|
-
if (pattern.includes("*")) {
|
|
5430
|
-
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
|
|
5431
|
-
const regexPattern = escaped.replace(/\*/g, "[^/]*");
|
|
5432
|
-
const regex = new RegExp(`^${regexPattern}$`);
|
|
5433
|
-
if (regex.test(azp)) return true;
|
|
5434
|
-
if (pattern.includes("*.")) {
|
|
5435
|
-
const rootPattern = pattern.replace("*.", "");
|
|
5436
|
-
if (rootPattern === azp) return true;
|
|
5437
|
-
}
|
|
5438
|
-
}
|
|
6063
|
+
function apiWorkerFetch(env, path, init) {
|
|
6064
|
+
const transport = resolveApiTransport(env);
|
|
6065
|
+
const url = buildTargetUrl(transport, path, "api-worker");
|
|
6066
|
+
if (transport.kind === "binding") {
|
|
6067
|
+
return transport.fetcher.fetch(url, init);
|
|
5439
6068
|
}
|
|
5440
|
-
return
|
|
5441
|
-
}
|
|
5442
|
-
var keyCache = /* @__PURE__ */ new Map();
|
|
5443
|
-
async function getPublicKey(pem) {
|
|
5444
|
-
const cached = keyCache.get(pem);
|
|
5445
|
-
if (cached) return cached;
|
|
5446
|
-
const normalized = pem.replace(/\\n/g, "\n");
|
|
5447
|
-
const key = await importSPKI(normalized, "ES256");
|
|
5448
|
-
keyCache.set(pem, key);
|
|
5449
|
-
return key;
|
|
6069
|
+
return fetch(url, init);
|
|
5450
6070
|
}
|
|
5451
|
-
|
|
5452
|
-
|
|
5453
|
-
|
|
5454
|
-
|
|
5455
|
-
|
|
5456
|
-
|
|
5457
|
-
const audience = normalizeArray(config.audience);
|
|
5458
|
-
const clockTolerance = Math.floor(
|
|
5459
|
-
(config.clockSkewMs ?? DEFAULT_CLOCK_SKEW_MS) / 1e3
|
|
5460
|
-
);
|
|
5461
|
-
const { payload } = await jwtVerify(token, publicKey, {
|
|
5462
|
-
issuer: config.issuer,
|
|
5463
|
-
audience: audience && audience.length === 1 ? audience[0] : audience,
|
|
5464
|
-
clockTolerance
|
|
5465
|
-
});
|
|
5466
|
-
const claims = payload;
|
|
5467
|
-
if (!claims.sub) {
|
|
5468
|
-
throw new Error("JWT verification succeeded but subject claim is missing");
|
|
5469
|
-
}
|
|
5470
|
-
const authorizedParties = normalizeArray(config.authorizedParties);
|
|
5471
|
-
if (authorizedParties) {
|
|
5472
|
-
const azp = claims.azp;
|
|
5473
|
-
if (!matchesAuthorizedParty(azp, authorizedParties)) {
|
|
5474
|
-
throw new Error(
|
|
5475
|
-
`Authorized party '${azp}' does not match any allowed pattern`
|
|
5476
|
-
);
|
|
5477
|
-
}
|
|
6071
|
+
function platformWorkerFetch(env, pathOrRequest, init) {
|
|
6072
|
+
const transport = resolvePlatformTransport(env);
|
|
6073
|
+
if (typeof pathOrRequest === "string") {
|
|
6074
|
+
const url2 = buildTargetUrl(transport, pathOrRequest, "platform-worker");
|
|
6075
|
+
if (transport.kind === "binding") {
|
|
6076
|
+
return transport.fetcher.fetch(url2, init);
|
|
5478
6077
|
}
|
|
5479
|
-
return
|
|
5480
|
-
result: {
|
|
5481
|
-
userId: claims.sub,
|
|
5482
|
-
claims
|
|
5483
|
-
}
|
|
5484
|
-
};
|
|
5485
|
-
} catch (error) {
|
|
5486
|
-
return {
|
|
5487
|
-
result: null,
|
|
5488
|
-
error,
|
|
5489
|
-
debug: decodeJwtPayload(token)
|
|
5490
|
-
};
|
|
6078
|
+
return fetch(url2, init);
|
|
5491
6079
|
}
|
|
5492
|
-
|
|
5493
|
-
|
|
5494
|
-
|
|
5495
|
-
|
|
5496
|
-
|
|
5497
|
-
async function loadNodeCrypto() {
|
|
5498
|
-
if (!nodeCryptoPromise) {
|
|
5499
|
-
nodeCryptoPromise = import("crypto").then((module) => module).catch(() => null);
|
|
6080
|
+
const req = pathOrRequest;
|
|
6081
|
+
const url = buildTargetUrl(transport, req.url, "platform-worker");
|
|
6082
|
+
const rewritten = new Request(url, req);
|
|
6083
|
+
if (transport.kind === "binding") {
|
|
6084
|
+
return transport.fetcher.fetch(rewritten);
|
|
5500
6085
|
}
|
|
5501
|
-
return
|
|
5502
|
-
}
|
|
5503
|
-
function hasSubtleCrypto() {
|
|
5504
|
-
return typeof globalThis.crypto?.subtle?.importKey === "function";
|
|
6086
|
+
return fetch(rewritten);
|
|
5505
6087
|
}
|
|
5506
|
-
|
|
5507
|
-
if (
|
|
5508
|
-
|
|
5509
|
-
|
|
5510
|
-
"raw",
|
|
5511
|
-
encoder.encode(secret),
|
|
5512
|
-
{ name: "HMAC", hash: "SHA-256" },
|
|
5513
|
-
false,
|
|
5514
|
-
["sign"]
|
|
6088
|
+
function authWorkerFetch(env, path, init) {
|
|
6089
|
+
if (!env.AUTH_WORKER_URL) {
|
|
6090
|
+
throw new Error(
|
|
6091
|
+
"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."
|
|
5515
6092
|
);
|
|
5516
|
-
const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(payload));
|
|
5517
|
-
return bufferToHex(signature);
|
|
5518
|
-
}
|
|
5519
|
-
const nodeCrypto = await loadNodeCrypto();
|
|
5520
|
-
if (!nodeCrypto) {
|
|
5521
|
-
throw new Error("Crypto implementation unavailable for HMAC computation");
|
|
5522
|
-
}
|
|
5523
|
-
return nodeCrypto.createHmac("sha256", secret).update(payload).digest("hex");
|
|
5524
|
-
}
|
|
5525
|
-
async function timingSafeEqualHex(a, b) {
|
|
5526
|
-
if (a.length !== b.length) {
|
|
5527
|
-
return false;
|
|
5528
|
-
}
|
|
5529
|
-
const nodeCrypto = await loadNodeCrypto();
|
|
5530
|
-
if (nodeCrypto?.timingSafeEqual) {
|
|
5531
|
-
const aBuf = Buffer.from(a, "hex");
|
|
5532
|
-
const bBuf = Buffer.from(b, "hex");
|
|
5533
|
-
try {
|
|
5534
|
-
return nodeCrypto.timingSafeEqual(aBuf, bBuf);
|
|
5535
|
-
} catch {
|
|
5536
|
-
return false;
|
|
5537
|
-
}
|
|
5538
|
-
}
|
|
5539
|
-
let diff = 0;
|
|
5540
|
-
for (let i = 0; i < a.length; i += 1) {
|
|
5541
|
-
diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
5542
6093
|
}
|
|
5543
|
-
|
|
5544
|
-
|
|
5545
|
-
|
|
5546
|
-
|
|
5547
|
-
|
|
5548
|
-
|
|
5549
|
-
|
|
5550
|
-
|
|
5551
|
-
|
|
5552
|
-
return { timestamp: ts, signature };
|
|
5553
|
-
}
|
|
5554
|
-
async function verifyInternalSignature({
|
|
5555
|
-
secret,
|
|
5556
|
-
timestamp,
|
|
5557
|
-
signature,
|
|
5558
|
-
payload,
|
|
5559
|
-
maxSkewMs = DEFAULT_MAX_SKEW_MS
|
|
5560
|
-
}) {
|
|
5561
|
-
if (!secret || !timestamp || !signature) {
|
|
5562
|
-
return false;
|
|
5563
|
-
}
|
|
5564
|
-
const tsNumber = Number(timestamp);
|
|
5565
|
-
if (!Number.isFinite(tsNumber)) {
|
|
5566
|
-
return false;
|
|
5567
|
-
}
|
|
5568
|
-
const skew = Math.abs(nowMs() - tsNumber);
|
|
5569
|
-
if (skew > maxSkewMs) {
|
|
5570
|
-
return false;
|
|
5571
|
-
}
|
|
5572
|
-
const expected = await computeHmacHex(secret, `${timestamp}.${payload}`);
|
|
5573
|
-
return timingSafeEqualHex(signature.toLowerCase(), expected.toLowerCase());
|
|
5574
|
-
}
|
|
5575
|
-
function buildInternalPayload(body) {
|
|
5576
|
-
if (body === void 0) return "{}";
|
|
5577
|
-
if (typeof body === "string") return body;
|
|
5578
|
-
try {
|
|
5579
|
-
return JSON.stringify(body);
|
|
5580
|
-
} catch {
|
|
5581
|
-
return "{}";
|
|
5582
|
-
}
|
|
5583
|
-
}
|
|
5584
|
-
|
|
5585
|
-
// src/server/auth/betterAuth.ts
|
|
5586
|
-
import { betterAuth } from "better-auth";
|
|
5587
|
-
import { organization, twoFactor } from "better-auth/plugins";
|
|
5588
|
-
function createDeepSpaceAuth(config) {
|
|
5589
|
-
const socialProviders = {};
|
|
5590
|
-
if (config.google) {
|
|
5591
|
-
socialProviders.google = config.google;
|
|
5592
|
-
}
|
|
5593
|
-
if (config.github) {
|
|
5594
|
-
socialProviders.github = config.github;
|
|
6094
|
+
const baseUrl = env.AUTH_WORKER_URL.replace(/\/$/, "");
|
|
6095
|
+
let pathAndSearch;
|
|
6096
|
+
if (path.startsWith("http://") || path.startsWith("https://")) {
|
|
6097
|
+
const u = new URL(path);
|
|
6098
|
+
pathAndSearch = u.pathname + u.search;
|
|
6099
|
+
} else if (path.startsWith("/")) {
|
|
6100
|
+
pathAndSearch = path;
|
|
6101
|
+
} else {
|
|
6102
|
+
pathAndSearch = "/" + path;
|
|
5595
6103
|
}
|
|
5596
|
-
return
|
|
5597
|
-
database: config.database,
|
|
5598
|
-
baseURL: config.baseURL,
|
|
5599
|
-
secret: config.secret,
|
|
5600
|
-
emailAndPassword: {
|
|
5601
|
-
enabled: config.emailAndPassword ?? true
|
|
5602
|
-
},
|
|
5603
|
-
socialProviders,
|
|
5604
|
-
trustedOrigins: config.trustedOrigins ?? [
|
|
5605
|
-
"https://deep.space",
|
|
5606
|
-
"https://*.deep.space",
|
|
5607
|
-
"https://*.app.space",
|
|
5608
|
-
"http://localhost:*"
|
|
5609
|
-
],
|
|
5610
|
-
plugins: [organization(), twoFactor()]
|
|
5611
|
-
});
|
|
6104
|
+
return fetch(`${baseUrl}${pathAndSearch}`, init);
|
|
5612
6105
|
}
|
|
5613
6106
|
|
|
5614
6107
|
// src/server/utils/cron.ts
|
|
5615
|
-
var DEFAULT_API_BASE_URL = "https://api-worker.deep.space";
|
|
5616
6108
|
function buildCronContext(env, ownerUserId, roomId = "default") {
|
|
5617
|
-
const hmacSecret = env.INTERNAL_STORAGE_HMAC_SECRET;
|
|
5618
|
-
const apiBase = env.API_BASE_URL || DEFAULT_API_BASE_URL;
|
|
5619
6109
|
const roomIdObj = env.RECORD_ROOMS.idFromName(roomId);
|
|
5620
6110
|
const room = env.RECORD_ROOMS.get(roomIdObj);
|
|
5621
6111
|
async function executeTool3(tool, params) {
|
|
@@ -5653,30 +6143,31 @@ function buildCronContext(env, ownerUserId, roomId = "default") {
|
|
|
5653
6143
|
}
|
|
5654
6144
|
};
|
|
5655
6145
|
const integrations = {
|
|
5656
|
-
async call(endpoint, params) {
|
|
5657
|
-
if (!
|
|
5658
|
-
throw new Error(
|
|
6146
|
+
async call(endpoint, params = {}) {
|
|
6147
|
+
if (!env.APP_OWNER_JWT) {
|
|
6148
|
+
throw new Error(
|
|
6149
|
+
"integrations.call requires env.APP_OWNER_JWT. Deployed apps receive this automatically; in dev, `deepspace dev` writes it into .dev.vars."
|
|
6150
|
+
);
|
|
5659
6151
|
}
|
|
5660
|
-
const
|
|
5661
|
-
const payloadStr = buildInternalPayload(payload);
|
|
5662
|
-
const { timestamp, signature } = await signInternalPayload({
|
|
5663
|
-
secret: hmacSecret,
|
|
5664
|
-
payload: payloadStr
|
|
5665
|
-
});
|
|
5666
|
-
const response = await fetch(`${apiBase}/api/internal/integrations/${endpoint}`, {
|
|
6152
|
+
const response = await apiWorkerFetch(env, `/api/integrations/${endpoint}`, {
|
|
5667
6153
|
method: "POST",
|
|
5668
6154
|
headers: {
|
|
5669
6155
|
"Content-Type": "application/json",
|
|
5670
|
-
|
|
5671
|
-
"x-internal-signature": signature
|
|
6156
|
+
Authorization: `Bearer ${env.APP_OWNER_JWT}`
|
|
5672
6157
|
},
|
|
5673
|
-
body:
|
|
6158
|
+
body: JSON.stringify(params)
|
|
5674
6159
|
});
|
|
5675
|
-
|
|
5676
|
-
|
|
5677
|
-
|
|
6160
|
+
const text = await response.text();
|
|
6161
|
+
let result = {};
|
|
6162
|
+
try {
|
|
6163
|
+
result = text ? JSON.parse(text) : {};
|
|
6164
|
+
} catch {
|
|
6165
|
+
}
|
|
6166
|
+
if (!response.ok || !result.success) {
|
|
6167
|
+
const detail = result.message || result.error || text || `HTTP ${response.status}`;
|
|
6168
|
+
throw new Error(`Integration call ${endpoint} failed: ${detail}`);
|
|
5678
6169
|
}
|
|
5679
|
-
return
|
|
6170
|
+
return result.data;
|
|
5680
6171
|
}
|
|
5681
6172
|
};
|
|
5682
6173
|
return { records, integrations, ownerUserId };
|
|
@@ -5686,86 +6177,6 @@ function buildCronContext(env, ownerUserId, roomId = "default") {
|
|
|
5686
6177
|
import { createAnthropic } from "@ai-sdk/anthropic";
|
|
5687
6178
|
import { createOpenAI } from "@ai-sdk/openai";
|
|
5688
6179
|
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
|
|
5689
|
-
|
|
5690
|
-
// src/server/utils/proxies.ts
|
|
5691
|
-
function resolveApiTransport(env) {
|
|
5692
|
-
if (env.API_WORKER) return { kind: "binding", fetcher: env.API_WORKER };
|
|
5693
|
-
if (env.API_WORKER_URL) {
|
|
5694
|
-
return { kind: "url", baseUrl: env.API_WORKER_URL.replace(/\/$/, "") };
|
|
5695
|
-
}
|
|
5696
|
-
throw new Error(
|
|
5697
|
-
"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."
|
|
5698
|
-
);
|
|
5699
|
-
}
|
|
5700
|
-
function resolvePlatformTransport(env) {
|
|
5701
|
-
if (env.PLATFORM_WORKER) return { kind: "binding", fetcher: env.PLATFORM_WORKER };
|
|
5702
|
-
if (env.PLATFORM_WORKER_URL) {
|
|
5703
|
-
return { kind: "url", baseUrl: env.PLATFORM_WORKER_URL.replace(/\/$/, "") };
|
|
5704
|
-
}
|
|
5705
|
-
throw new Error(
|
|
5706
|
-
"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."
|
|
5707
|
-
);
|
|
5708
|
-
}
|
|
5709
|
-
function buildTargetUrl(transport, path, internalHost) {
|
|
5710
|
-
let pathAndSearch;
|
|
5711
|
-
if (path.startsWith("http://") || path.startsWith("https://")) {
|
|
5712
|
-
const u = new URL(path);
|
|
5713
|
-
pathAndSearch = u.pathname + u.search;
|
|
5714
|
-
} else if (path.startsWith("/")) {
|
|
5715
|
-
pathAndSearch = path;
|
|
5716
|
-
} else {
|
|
5717
|
-
pathAndSearch = "/" + path;
|
|
5718
|
-
}
|
|
5719
|
-
if (transport.kind === "binding") {
|
|
5720
|
-
return `https://${internalHost}${pathAndSearch}`;
|
|
5721
|
-
}
|
|
5722
|
-
return `${transport.baseUrl}${pathAndSearch}`;
|
|
5723
|
-
}
|
|
5724
|
-
function apiWorkerFetch(env, path, init) {
|
|
5725
|
-
const transport = resolveApiTransport(env);
|
|
5726
|
-
const url = buildTargetUrl(transport, path, "api-worker");
|
|
5727
|
-
if (transport.kind === "binding") {
|
|
5728
|
-
return transport.fetcher.fetch(url, init);
|
|
5729
|
-
}
|
|
5730
|
-
return fetch(url, init);
|
|
5731
|
-
}
|
|
5732
|
-
function platformWorkerFetch(env, pathOrRequest, init) {
|
|
5733
|
-
const transport = resolvePlatformTransport(env);
|
|
5734
|
-
if (typeof pathOrRequest === "string") {
|
|
5735
|
-
const url2 = buildTargetUrl(transport, pathOrRequest, "platform-worker");
|
|
5736
|
-
if (transport.kind === "binding") {
|
|
5737
|
-
return transport.fetcher.fetch(url2, init);
|
|
5738
|
-
}
|
|
5739
|
-
return fetch(url2, init);
|
|
5740
|
-
}
|
|
5741
|
-
const req = pathOrRequest;
|
|
5742
|
-
const url = buildTargetUrl(transport, req.url, "platform-worker");
|
|
5743
|
-
const rewritten = new Request(url, req);
|
|
5744
|
-
if (transport.kind === "binding") {
|
|
5745
|
-
return transport.fetcher.fetch(rewritten);
|
|
5746
|
-
}
|
|
5747
|
-
return fetch(rewritten);
|
|
5748
|
-
}
|
|
5749
|
-
function authWorkerFetch(env, path, init) {
|
|
5750
|
-
if (!env.AUTH_WORKER_URL) {
|
|
5751
|
-
throw new Error(
|
|
5752
|
-
"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."
|
|
5753
|
-
);
|
|
5754
|
-
}
|
|
5755
|
-
const baseUrl = env.AUTH_WORKER_URL.replace(/\/$/, "");
|
|
5756
|
-
let pathAndSearch;
|
|
5757
|
-
if (path.startsWith("http://") || path.startsWith("https://")) {
|
|
5758
|
-
const u = new URL(path);
|
|
5759
|
-
pathAndSearch = u.pathname + u.search;
|
|
5760
|
-
} else if (path.startsWith("/")) {
|
|
5761
|
-
pathAndSearch = path;
|
|
5762
|
-
} else {
|
|
5763
|
-
pathAndSearch = "/" + path;
|
|
5764
|
-
}
|
|
5765
|
-
return fetch(`${baseUrl}${pathAndSearch}`, init);
|
|
5766
|
-
}
|
|
5767
|
-
|
|
5768
|
-
// src/server/utils/ai.ts
|
|
5769
6180
|
var ANTHROPIC_DEFAULT_MAX_OUTPUT_TOKENS = 4096;
|
|
5770
6181
|
function withDefaultAnthropicMaxTokens(body) {
|
|
5771
6182
|
if (typeof body !== "string") return body;
|
|
@@ -6359,6 +6770,215 @@ async function runMigrations(db, migrations) {
|
|
|
6359
6770
|
function splitStatements(sql) {
|
|
6360
6771
|
return sql.split(";").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
6361
6772
|
}
|
|
6773
|
+
|
|
6774
|
+
// src/server/auth/jwtVerifier.ts
|
|
6775
|
+
import { importSPKI, jwtVerify } from "jose";
|
|
6776
|
+
|
|
6777
|
+
// src/server/auth/utils.ts
|
|
6778
|
+
var BASE64_URL_REGEX = /^[A-Za-z0-9-_]+$/;
|
|
6779
|
+
function bufferToHex(bytes) {
|
|
6780
|
+
const view = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
|
|
6781
|
+
let hex = "";
|
|
6782
|
+
for (const byte of view) {
|
|
6783
|
+
hex += byte.toString(16).padStart(2, "0");
|
|
6784
|
+
}
|
|
6785
|
+
return hex;
|
|
6786
|
+
}
|
|
6787
|
+
function normalizeArray(value) {
|
|
6788
|
+
if (value == null) return void 0;
|
|
6789
|
+
if (Array.isArray(value)) {
|
|
6790
|
+
return value.length ? value : void 0;
|
|
6791
|
+
}
|
|
6792
|
+
return [value];
|
|
6793
|
+
}
|
|
6794
|
+
function decodeJwtPayload(token) {
|
|
6795
|
+
if (!token) return void 0;
|
|
6796
|
+
const parts = token.split(".");
|
|
6797
|
+
if (parts.length < 2) return void 0;
|
|
6798
|
+
const payloadSegment = parts[1];
|
|
6799
|
+
if (!payloadSegment) return void 0;
|
|
6800
|
+
if (!BASE64_URL_REGEX.test(payloadSegment)) return void 0;
|
|
6801
|
+
try {
|
|
6802
|
+
const normalized = payloadSegment.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(payloadSegment.length / 4) * 4, "=");
|
|
6803
|
+
const bytes = base64ToUint8Array(normalized);
|
|
6804
|
+
const json = new TextDecoder().decode(bytes);
|
|
6805
|
+
const payload = JSON.parse(json);
|
|
6806
|
+
return {
|
|
6807
|
+
iss: payload.iss ?? null,
|
|
6808
|
+
aud: payload.aud ?? null,
|
|
6809
|
+
azp: payload?.azp ?? null,
|
|
6810
|
+
exp: payload.exp ?? null,
|
|
6811
|
+
iat: payload.iat ?? null
|
|
6812
|
+
};
|
|
6813
|
+
} catch {
|
|
6814
|
+
return void 0;
|
|
6815
|
+
}
|
|
6816
|
+
}
|
|
6817
|
+
function base64ToUint8Array(value) {
|
|
6818
|
+
if (typeof atob === "function") {
|
|
6819
|
+
const binary = atob(value);
|
|
6820
|
+
const out = new Uint8Array(binary.length);
|
|
6821
|
+
for (let i = 0; i < binary.length; i += 1) {
|
|
6822
|
+
out[i] = binary.charCodeAt(i);
|
|
6823
|
+
}
|
|
6824
|
+
return out;
|
|
6825
|
+
}
|
|
6826
|
+
if (typeof Buffer !== "undefined") {
|
|
6827
|
+
return Uint8Array.from(Buffer.from(value, "base64"));
|
|
6828
|
+
}
|
|
6829
|
+
throw new Error("No base64 decoder available in current environment");
|
|
6830
|
+
}
|
|
6831
|
+
|
|
6832
|
+
// src/server/auth/jwtVerifier.ts
|
|
6833
|
+
var DEFAULT_CLOCK_SKEW_MS = 5e3;
|
|
6834
|
+
function matchesAuthorizedParty(azp, patterns) {
|
|
6835
|
+
if (!azp) return true;
|
|
6836
|
+
for (const pattern of patterns) {
|
|
6837
|
+
if (pattern === azp) return true;
|
|
6838
|
+
if (pattern.includes("*")) {
|
|
6839
|
+
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
|
|
6840
|
+
const regexPattern = escaped.replace(/\*/g, "[^/]*");
|
|
6841
|
+
const regex = new RegExp(`^${regexPattern}$`);
|
|
6842
|
+
if (regex.test(azp)) return true;
|
|
6843
|
+
if (pattern.includes("*.")) {
|
|
6844
|
+
const rootPattern = pattern.replace("*.", "");
|
|
6845
|
+
if (rootPattern === azp) return true;
|
|
6846
|
+
}
|
|
6847
|
+
}
|
|
6848
|
+
}
|
|
6849
|
+
return false;
|
|
6850
|
+
}
|
|
6851
|
+
var keyCache = /* @__PURE__ */ new Map();
|
|
6852
|
+
async function getPublicKey(pem) {
|
|
6853
|
+
const cached = keyCache.get(pem);
|
|
6854
|
+
if (cached) return cached;
|
|
6855
|
+
const normalized = pem.replace(/\\n/g, "\n");
|
|
6856
|
+
const key = await importSPKI(normalized, "ES256");
|
|
6857
|
+
keyCache.set(pem, key);
|
|
6858
|
+
return key;
|
|
6859
|
+
}
|
|
6860
|
+
async function verifyJwt(config, token) {
|
|
6861
|
+
if (!token) {
|
|
6862
|
+
return { result: null };
|
|
6863
|
+
}
|
|
6864
|
+
try {
|
|
6865
|
+
const publicKey = await getPublicKey(config.publicKey);
|
|
6866
|
+
const audience = normalizeArray(config.audience);
|
|
6867
|
+
const clockTolerance = Math.floor(
|
|
6868
|
+
(config.clockSkewMs ?? DEFAULT_CLOCK_SKEW_MS) / 1e3
|
|
6869
|
+
);
|
|
6870
|
+
const { payload } = await jwtVerify(token, publicKey, {
|
|
6871
|
+
issuer: config.issuer,
|
|
6872
|
+
audience: audience && audience.length === 1 ? audience[0] : audience,
|
|
6873
|
+
clockTolerance
|
|
6874
|
+
});
|
|
6875
|
+
const claims = payload;
|
|
6876
|
+
if (!claims.sub) {
|
|
6877
|
+
throw new Error("JWT verification succeeded but subject claim is missing");
|
|
6878
|
+
}
|
|
6879
|
+
const authorizedParties = normalizeArray(config.authorizedParties);
|
|
6880
|
+
if (authorizedParties) {
|
|
6881
|
+
const azp = claims.azp;
|
|
6882
|
+
if (!matchesAuthorizedParty(azp, authorizedParties)) {
|
|
6883
|
+
throw new Error(
|
|
6884
|
+
`Authorized party '${azp}' does not match any allowed pattern`
|
|
6885
|
+
);
|
|
6886
|
+
}
|
|
6887
|
+
}
|
|
6888
|
+
return {
|
|
6889
|
+
result: {
|
|
6890
|
+
userId: claims.sub,
|
|
6891
|
+
claims
|
|
6892
|
+
}
|
|
6893
|
+
};
|
|
6894
|
+
} catch (error) {
|
|
6895
|
+
return {
|
|
6896
|
+
result: null,
|
|
6897
|
+
error,
|
|
6898
|
+
debug: decodeJwtPayload(token)
|
|
6899
|
+
};
|
|
6900
|
+
}
|
|
6901
|
+
}
|
|
6902
|
+
|
|
6903
|
+
// src/server/auth/internalAuth.ts
|
|
6904
|
+
var nodeCryptoPromise = null;
|
|
6905
|
+
async function loadNodeCrypto() {
|
|
6906
|
+
if (!nodeCryptoPromise) {
|
|
6907
|
+
nodeCryptoPromise = import("crypto").then((module) => module).catch(() => null);
|
|
6908
|
+
}
|
|
6909
|
+
return nodeCryptoPromise;
|
|
6910
|
+
}
|
|
6911
|
+
function hasSubtleCrypto() {
|
|
6912
|
+
return typeof globalThis.crypto?.subtle?.importKey === "function";
|
|
6913
|
+
}
|
|
6914
|
+
async function computeHmacHex(secret, payload) {
|
|
6915
|
+
if (hasSubtleCrypto()) {
|
|
6916
|
+
const encoder = new TextEncoder();
|
|
6917
|
+
const key = await crypto.subtle.importKey(
|
|
6918
|
+
"raw",
|
|
6919
|
+
encoder.encode(secret),
|
|
6920
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
6921
|
+
false,
|
|
6922
|
+
["sign"]
|
|
6923
|
+
);
|
|
6924
|
+
const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(payload));
|
|
6925
|
+
return bufferToHex(signature);
|
|
6926
|
+
}
|
|
6927
|
+
const nodeCrypto = await loadNodeCrypto();
|
|
6928
|
+
if (!nodeCrypto) {
|
|
6929
|
+
throw new Error("Crypto implementation unavailable for HMAC computation");
|
|
6930
|
+
}
|
|
6931
|
+
return nodeCrypto.createHmac("sha256", secret).update(payload).digest("hex");
|
|
6932
|
+
}
|
|
6933
|
+
async function timingSafeEqualHex(a, b) {
|
|
6934
|
+
if (a.length !== b.length) {
|
|
6935
|
+
return false;
|
|
6936
|
+
}
|
|
6937
|
+
const nodeCrypto = await loadNodeCrypto();
|
|
6938
|
+
if (nodeCrypto?.timingSafeEqual) {
|
|
6939
|
+
const aBuf = Buffer.from(a, "hex");
|
|
6940
|
+
const bBuf = Buffer.from(b, "hex");
|
|
6941
|
+
try {
|
|
6942
|
+
return nodeCrypto.timingSafeEqual(aBuf, bBuf);
|
|
6943
|
+
} catch {
|
|
6944
|
+
return false;
|
|
6945
|
+
}
|
|
6946
|
+
}
|
|
6947
|
+
let diff = 0;
|
|
6948
|
+
for (let i = 0; i < a.length; i += 1) {
|
|
6949
|
+
diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
6950
|
+
}
|
|
6951
|
+
return diff === 0;
|
|
6952
|
+
}
|
|
6953
|
+
|
|
6954
|
+
// src/server/auth/betterAuth.ts
|
|
6955
|
+
import { betterAuth } from "better-auth";
|
|
6956
|
+
import { organization, twoFactor } from "better-auth/plugins";
|
|
6957
|
+
function createDeepSpaceAuth(config) {
|
|
6958
|
+
const socialProviders = {};
|
|
6959
|
+
if (config.google) {
|
|
6960
|
+
socialProviders.google = config.google;
|
|
6961
|
+
}
|
|
6962
|
+
if (config.github) {
|
|
6963
|
+
socialProviders.github = config.github;
|
|
6964
|
+
}
|
|
6965
|
+
return betterAuth({
|
|
6966
|
+
database: config.database,
|
|
6967
|
+
baseURL: config.baseURL,
|
|
6968
|
+
secret: config.secret,
|
|
6969
|
+
emailAndPassword: {
|
|
6970
|
+
enabled: config.emailAndPassword ?? true
|
|
6971
|
+
},
|
|
6972
|
+
socialProviders,
|
|
6973
|
+
trustedOrigins: config.trustedOrigins ?? [
|
|
6974
|
+
"https://deep.space",
|
|
6975
|
+
"https://*.deep.space",
|
|
6976
|
+
"https://*.app.space",
|
|
6977
|
+
"http://localhost:*"
|
|
6978
|
+
],
|
|
6979
|
+
plugins: [organization(), twoFactor()]
|
|
6980
|
+
});
|
|
6981
|
+
}
|
|
6362
6982
|
export {
|
|
6363
6983
|
AI_CHATS_SCHEMA,
|
|
6364
6984
|
AI_MESSAGES_SCHEMA,
|
|
@@ -6379,11 +6999,11 @@ export {
|
|
|
6379
6999
|
CronRoom,
|
|
6380
7000
|
DEFAULT_CONTEXT_CONFIG,
|
|
6381
7001
|
DEFAULT_DO_MANIFEST,
|
|
6382
|
-
DEFAULT_MAX_SKEW_MS,
|
|
6383
7002
|
DIRECTORY_SCHEMAS,
|
|
6384
7003
|
GLOBAL_DO_TYPES,
|
|
6385
7004
|
GLOBAL_DO_TYPE_NAMES,
|
|
6386
7005
|
GameRoom,
|
|
7006
|
+
JobRoom,
|
|
6387
7007
|
MESSAGES_SCHEMA,
|
|
6388
7008
|
MSG,
|
|
6389
7009
|
MSG_AWARENESS,
|
|
@@ -6416,7 +7036,6 @@ export {
|
|
|
6416
7036
|
authWorkerFetch,
|
|
6417
7037
|
bindingManifestFromOutputConfig,
|
|
6418
7038
|
buildCronContext,
|
|
6419
|
-
buildInternalPayload,
|
|
6420
7039
|
buildTableSelect,
|
|
6421
7040
|
buildUiParts,
|
|
6422
7041
|
canCreate,
|
|
@@ -6447,6 +7066,7 @@ export {
|
|
|
6447
7066
|
encodeSyncStep1,
|
|
6448
7067
|
encodeSyncStep2,
|
|
6449
7068
|
encodeUpdate,
|
|
7069
|
+
enqueueJob,
|
|
6450
7070
|
getChat,
|
|
6451
7071
|
getGlobalDOSchemas,
|
|
6452
7072
|
getGlobalDOType,
|
|
@@ -6474,7 +7094,6 @@ export {
|
|
|
6474
7094
|
rowToData,
|
|
6475
7095
|
runMigrations,
|
|
6476
7096
|
serverBuild,
|
|
6477
|
-
signInternalPayload,
|
|
6478
7097
|
timingSafeEqualHex,
|
|
6479
7098
|
toUint8Array,
|
|
6480
7099
|
totalChars,
|
|
@@ -6485,7 +7104,6 @@ export {
|
|
|
6485
7104
|
validateAppName,
|
|
6486
7105
|
validateBindingManifest,
|
|
6487
7106
|
validateDoManifest,
|
|
6488
|
-
verifyInternalSignature,
|
|
6489
7107
|
verifyJwt,
|
|
6490
7108
|
workspaceAccountsSchema,
|
|
6491
7109
|
workspaceContentSharesSchema,
|