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.
Files changed (120) hide show
  1. package/dist/cli.js +331 -115
  2. package/dist/cli.js.map +1 -1
  3. package/dist/index.d.ts +199 -16
  4. package/dist/index.js +354 -166
  5. package/dist/index.js.map +1 -1
  6. package/dist/server.d.ts +242 -73
  7. package/dist/server.js +985 -374
  8. package/dist/server.js.map +1 -1
  9. package/dist/worker.d.ts +322 -73
  10. package/dist/worker.js +961 -343
  11. package/dist/worker.js.map +1 -1
  12. package/features/admin-page/feature.json +29 -0
  13. package/features/admin-page/src/AdminPage.tsx +292 -0
  14. package/features/admin-page/src/admin-schema.ts +20 -0
  15. package/features/ai-chat/feature.json +41 -0
  16. package/features/ai-chat/src/AiChatPage.tsx +692 -0
  17. package/features/ai-chat/src/ChatPanel.tsx +1098 -0
  18. package/features/ai-chat/src/ai-chat-schema.ts +11 -0
  19. package/features/canvas/feature.json +45 -0
  20. package/features/canvas/src/CanvasEditorPage.tsx +87 -0
  21. package/features/canvas/src/CanvasListPage.tsx +202 -0
  22. package/features/canvas/src/canvas-schema.ts +33 -0
  23. package/features/canvas/src/components/CanvasToolbar.tsx +170 -0
  24. package/features/canvas/src/components/CanvasView.tsx +369 -0
  25. package/features/canvas/src/components/ShapeRenderer.tsx +114 -0
  26. package/features/cron/feature.json +33 -0
  27. package/features/cron/src/CronLogPage.tsx +100 -0
  28. package/features/cron/src/cron.ts +25 -0
  29. package/features/docs/feature.json +96 -0
  30. package/features/docs/src/DocsEditorPage.tsx +1012 -0
  31. package/features/docs/src/DocsListPage.tsx +951 -0
  32. package/features/docs/src/DocsOutlinePanel.tsx +68 -0
  33. package/features/docs/src/DocsPresence.tsx +138 -0
  34. package/features/docs/src/InviteDialog.tsx +336 -0
  35. package/features/docs/src/LibrarySidebar.tsx +464 -0
  36. package/features/docs/src/docs-favorites.ts +18 -0
  37. package/features/docs/src/docs-library-types.ts +15 -0
  38. package/features/docs/src/docs-schema.ts +80 -0
  39. package/features/docs/src/docs-ui.css +309 -0
  40. package/features/docs/src/editor/DocEditorSurface.tsx +257 -0
  41. package/features/docs/src/editor/DocsTiptapToolbar.tsx +560 -0
  42. package/features/docs/src/editor/useDocEditor.ts +206 -0
  43. package/features/docs/src/use-yjs-room-with-awareness.ts +285 -0
  44. package/features/file-attachments/feature.json +42 -0
  45. package/features/file-attachments/src/AttachmentsPage.tsx +864 -0
  46. package/features/file-attachments/src/attachments-constants.ts +9 -0
  47. package/features/file-attachments/src/attachments-schema.ts +45 -0
  48. package/features/file-attachments/src/useFilePreview.ts +471 -0
  49. package/features/file-attachments/src/useMimeTypeDetection.ts +138 -0
  50. package/features/file-manager/feature.json +24 -0
  51. package/features/file-manager/src/FileManagerPage.tsx +330 -0
  52. package/features/integration-test/feature.json +18 -0
  53. package/features/integration-test/src/IntegrationTestPage.tsx +423 -0
  54. package/features/integration-test/src/integration.spec.ts +42 -0
  55. package/features/items/feature.json +33 -0
  56. package/features/items/src/ItemsPage.tsx +325 -0
  57. package/features/items/src/items-constants.ts +10 -0
  58. package/features/items/src/items-schema.ts +27 -0
  59. package/features/kanban/feature.json +18 -0
  60. package/features/kanban/src/Kanban.tsx +283 -0
  61. package/features/landing/feature.json +62 -0
  62. package/features/landing/src/LandingPage.tsx +627 -0
  63. package/features/landing/src/primitives.tsx +389 -0
  64. package/features/landing/src/sections/FAQSection.tsx +110 -0
  65. package/features/landing/src/sections/FeaturesGridSection.tsx +311 -0
  66. package/features/landing/src/sections/HowItWorksSection.tsx +62 -0
  67. package/features/landing/src/sections/LogoCloudSection.tsx +58 -0
  68. package/features/landing/src/sections/ShowcaseSection.tsx +63 -0
  69. package/features/landing/src/sections/StatsSection.tsx +31 -0
  70. package/features/landing/src/sections/TeamSection.tsx +86 -0
  71. package/features/landing/src/sections/TestimonialsSection.tsx +68 -0
  72. package/features/landing/src/sections/VideoSection.tsx +71 -0
  73. package/features/leaderboard/feature.json +33 -0
  74. package/features/leaderboard/src/LeaderboardPage.tsx +372 -0
  75. package/features/leaderboard/src/leaderboard-constants.ts +21 -0
  76. package/features/leaderboard/src/leaderboard-schema.ts +36 -0
  77. package/features/messaging/feature.json +111 -0
  78. package/features/messaging/src/ChatMultiPage.tsx +300 -0
  79. package/features/messaging/src/ChatPage.tsx +75 -0
  80. package/features/messaging/src/components/chat/ChatHeader.tsx +76 -0
  81. package/features/messaging/src/components/chat/MessageActionSheet.tsx +168 -0
  82. package/features/messaging/src/components/chat/MessageInput.tsx +65 -0
  83. package/features/messaging/src/components/chat/MessageItem.tsx +352 -0
  84. package/features/messaging/src/components/chat/MessageList.tsx +248 -0
  85. package/features/messaging/src/components/chat/ReactionPicker.tsx +2 -0
  86. package/features/messaging/src/components/chat/ThreadPanel.tsx +107 -0
  87. package/features/messaging/src/components/chat/UserProfilePopover.tsx +158 -0
  88. package/features/messaging/src/components/chat-multi/AddMemberModal.tsx +188 -0
  89. package/features/messaging/src/components/chat-multi/BrowseGroupsModal.tsx +163 -0
  90. package/features/messaging/src/components/chat-multi/ChannelListItem.tsx +147 -0
  91. package/features/messaging/src/components/chat-multi/ChannelSettingsPanel.tsx +344 -0
  92. package/features/messaging/src/components/chat-multi/ChannelSidebar.tsx +460 -0
  93. package/features/messaging/src/components/chat-multi/CreateGroupModal.tsx +190 -0
  94. package/features/messaging/src/components/chat-multi/InvitationList.tsx +149 -0
  95. package/features/messaging/src/components/chat-multi/NewMessageModal.tsx +159 -0
  96. package/features/messaging/src/hooks/useChatChannel.ts +64 -0
  97. package/features/messaging/src/hooks/useLongPress.ts +74 -0
  98. package/features/messaging/src/hooks/useMultiChannel.ts +142 -0
  99. package/features/messaging/src/messaging-schema.ts +25 -0
  100. package/features/presence-test/feature.json +21 -0
  101. package/features/presence-test/src/PresenceTestPage.tsx +120 -0
  102. package/features/sidebar/feature.json +19 -0
  103. package/features/sidebar/src/AppSidebar.tsx +558 -0
  104. package/features/tasks/feature.json +33 -0
  105. package/features/tasks/src/TasksPage.tsx +598 -0
  106. package/features/tasks/src/tasks-constants.ts +43 -0
  107. package/features/tasks/src/tasks-schema.ts +56 -0
  108. package/features/testing/feature.json +49 -0
  109. package/features/testing/src/TestPage.tsx +145 -0
  110. package/features/testing/src/cpu-burn-action.ts +51 -0
  111. package/features/testing/src/subrequest-burn-action.ts +53 -0
  112. package/features/testing/src/test-schema.ts +23 -0
  113. package/features/topbar/feature.json +18 -0
  114. package/features/topbar/src/Topbar.tsx +353 -0
  115. package/features/tree/feature.json +26 -0
  116. package/features/tree/src/TreeSidebar.tsx +607 -0
  117. package/features/tree/src/tree-constants.ts +68 -0
  118. package/features/tree/src/tree-hooks.ts +258 -0
  119. package/package.json +4 -2
  120. 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", `inline; filename="${object.customMetadata.originalName}"`);
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/auth/jwtVerifier.ts
5363
- import { importSPKI, jwtVerify } from "jose";
5364
-
5365
- // src/server/auth/utils.ts
5366
- var BASE64_URL_REGEX = /^[A-Za-z0-9-_]+$/;
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
- return [value];
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 decodeJwtPayload(token) {
5383
- if (!token) return void 0;
5384
- const parts = token.split(".");
5385
- if (parts.length < 2) return void 0;
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 nowMs() {
5406
- return Date.now();
5407
- }
5408
- function base64ToUint8Array(value) {
5409
- if (typeof atob === "function") {
5410
- const binary = atob(value);
5411
- const out = new Uint8Array(binary.length);
5412
- for (let i = 0; i < binary.length; i += 1) {
5413
- out[i] = binary.charCodeAt(i);
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 (typeof Buffer !== "undefined") {
5418
- return Uint8Array.from(Buffer.from(value, "base64"));
6058
+ if (transport.kind === "binding") {
6059
+ return `https://${internalHost}${pathAndSearch}`;
5419
6060
  }
5420
- throw new Error("No base64 decoder available in current environment");
6061
+ return `${transport.baseUrl}${pathAndSearch}`;
5421
6062
  }
5422
-
5423
- // src/server/auth/jwtVerifier.ts
5424
- var DEFAULT_CLOCK_SKEW_MS = 5e3;
5425
- function matchesAuthorizedParty(azp, patterns) {
5426
- if (!azp) return true;
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 false;
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
- async function verifyJwt(config, token) {
5452
- if (!token) {
5453
- return { result: null };
5454
- }
5455
- try {
5456
- const publicKey = await getPublicKey(config.publicKey);
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
- // src/server/auth/internalAuth.ts
5495
- var DEFAULT_MAX_SKEW_MS = 5 * 6e4;
5496
- var nodeCryptoPromise = null;
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 nodeCryptoPromise;
5502
- }
5503
- function hasSubtleCrypto() {
5504
- return typeof globalThis.crypto?.subtle?.importKey === "function";
6086
+ return fetch(rewritten);
5505
6087
  }
5506
- async function computeHmacHex(secret, payload) {
5507
- if (hasSubtleCrypto()) {
5508
- const encoder = new TextEncoder();
5509
- const key = await crypto.subtle.importKey(
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
- return diff === 0;
5544
- }
5545
- async function signInternalPayload({
5546
- secret,
5547
- payload,
5548
- timestamp
5549
- }) {
5550
- const ts = timestamp ?? nowMs().toString();
5551
- const signature = await computeHmacHex(secret, `${ts}.${payload}`);
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 betterAuth({
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 (!hmacSecret) {
5658
- throw new Error("INTERNAL_STORAGE_HMAC_SECRET not configured \u2014 cannot call integrations");
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 payload = { userId: ownerUserId, ...params };
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
- "x-internal-timestamp": timestamp,
5671
- "x-internal-signature": signature
6156
+ Authorization: `Bearer ${env.APP_OWNER_JWT}`
5672
6157
  },
5673
- body: payloadStr
6158
+ body: JSON.stringify(params)
5674
6159
  });
5675
- if (!response.ok) {
5676
- const errText = await response.text();
5677
- throw new Error(`Integration call ${endpoint} failed (${response.status}): ${errText}`);
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 response.json();
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,