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/server.js CHANGED
@@ -165,6 +165,11 @@ var BUILT_IN_TOOLS = [
165
165
 
166
166
  // src/server/utils/scoped-r2-files.ts
167
167
  var CORS_HEADERS = { "Access-Control-Allow-Origin": "*" };
168
+ function contentDisposition(originalName) {
169
+ const safe = originalName.replace(/[\x00-\x1f\x7f"\\]/g, "_") || "download";
170
+ const encoded = encodeURIComponent(originalName);
171
+ return `inline; filename="${safe}"; filename*=UTF-8''${encoded}`;
172
+ }
168
173
  function sanitizeSubpath(raw) {
169
174
  if (!raw) return "";
170
175
  const segments = raw.split("/");
@@ -174,6 +179,20 @@ function sanitizeSubpath(raw) {
174
179
  function isKeyWithinPrefix(key, prefix) {
175
180
  return key.startsWith(prefix);
176
181
  }
182
+ var DANGEROUS_MIME_TYPES = /* @__PURE__ */ new Set([
183
+ "text/html",
184
+ "application/xhtml+xml",
185
+ "image/svg+xml",
186
+ "application/xml",
187
+ "text/xml",
188
+ "application/javascript",
189
+ "text/javascript",
190
+ "application/ecmascript",
191
+ "text/ecmascript"
192
+ ]);
193
+ function normalizeMimeType(raw) {
194
+ return raw.split(";")[0].trim().toLowerCase();
195
+ }
177
196
  function createScopedR2Handler(config) {
178
197
  const requireAuthForMutations = config.requireAuthForMutations ?? true;
179
198
  return async (request, url, bucket, auth) => {
@@ -263,6 +282,13 @@ async function handleUpload(request, url, bucket, prefix, userId) {
263
282
  fileName = body.name;
264
283
  mimeType = body.mimeType || "application/octet-stream";
265
284
  }
285
+ mimeType = normalizeMimeType(mimeType) || "application/octet-stream";
286
+ if (DANGEROUS_MIME_TYPES.has(mimeType)) {
287
+ return Response.json(
288
+ { error: `Unsupported media type: ${mimeType}` },
289
+ { status: 415, headers: CORS_HEADERS }
290
+ );
291
+ }
266
292
  const requestedKey = url.searchParams.get("key");
267
293
  let key;
268
294
  if (requestedKey !== null) {
@@ -327,342 +353,15 @@ async function handleDownload(bucket, key) {
327
353
  }
328
354
  const headers = new Headers();
329
355
  headers.set("Content-Type", object.httpMetadata?.contentType || "application/octet-stream");
356
+ headers.set("X-Content-Type-Options", "nosniff");
330
357
  headers.set("Cache-Control", "public, max-age=31536000, immutable");
331
358
  headers.set("Access-Control-Allow-Origin", "*");
332
359
  if (object.customMetadata?.originalName) {
333
- headers.set("Content-Disposition", `inline; filename="${object.customMetadata.originalName}"`);
360
+ headers.set("Content-Disposition", contentDisposition(object.customMetadata.originalName));
334
361
  }
335
362
  return new Response(object.body, { headers });
336
363
  }
337
364
 
338
- // src/server/auth/jwtVerifier.ts
339
- import { importSPKI, jwtVerify } from "jose";
340
-
341
- // src/server/auth/utils.ts
342
- var BASE64_URL_REGEX = /^[A-Za-z0-9-_]+$/;
343
- function bufferToHex(bytes) {
344
- const view = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
345
- let hex = "";
346
- for (const byte of view) {
347
- hex += byte.toString(16).padStart(2, "0");
348
- }
349
- return hex;
350
- }
351
- function normalizeArray(value) {
352
- if (value == null) return void 0;
353
- if (Array.isArray(value)) {
354
- return value.length ? value : void 0;
355
- }
356
- return [value];
357
- }
358
- function decodeJwtPayload(token) {
359
- if (!token) return void 0;
360
- const parts = token.split(".");
361
- if (parts.length < 2) return void 0;
362
- const payloadSegment = parts[1];
363
- if (!payloadSegment) return void 0;
364
- if (!BASE64_URL_REGEX.test(payloadSegment)) return void 0;
365
- try {
366
- const normalized = payloadSegment.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(payloadSegment.length / 4) * 4, "=");
367
- const bytes = base64ToUint8Array(normalized);
368
- const json = new TextDecoder().decode(bytes);
369
- const payload = JSON.parse(json);
370
- return {
371
- iss: payload.iss ?? null,
372
- aud: payload.aud ?? null,
373
- azp: payload?.azp ?? null,
374
- exp: payload.exp ?? null,
375
- iat: payload.iat ?? null
376
- };
377
- } catch {
378
- return void 0;
379
- }
380
- }
381
- function nowMs() {
382
- return Date.now();
383
- }
384
- function base64ToUint8Array(value) {
385
- if (typeof atob === "function") {
386
- const binary = atob(value);
387
- const out = new Uint8Array(binary.length);
388
- for (let i = 0; i < binary.length; i += 1) {
389
- out[i] = binary.charCodeAt(i);
390
- }
391
- return out;
392
- }
393
- if (typeof Buffer !== "undefined") {
394
- return Uint8Array.from(Buffer.from(value, "base64"));
395
- }
396
- throw new Error("No base64 decoder available in current environment");
397
- }
398
-
399
- // src/server/auth/jwtVerifier.ts
400
- var DEFAULT_CLOCK_SKEW_MS = 5e3;
401
- function matchesAuthorizedParty(azp, patterns) {
402
- if (!azp) return true;
403
- for (const pattern of patterns) {
404
- if (pattern === azp) return true;
405
- if (pattern.includes("*")) {
406
- const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
407
- const regexPattern = escaped.replace(/\*/g, "[^/]*");
408
- const regex = new RegExp(`^${regexPattern}$`);
409
- if (regex.test(azp)) return true;
410
- if (pattern.includes("*.")) {
411
- const rootPattern = pattern.replace("*.", "");
412
- if (rootPattern === azp) return true;
413
- }
414
- }
415
- }
416
- return false;
417
- }
418
- var keyCache = /* @__PURE__ */ new Map();
419
- async function getPublicKey(pem) {
420
- const cached = keyCache.get(pem);
421
- if (cached) return cached;
422
- const normalized = pem.replace(/\\n/g, "\n");
423
- const key = await importSPKI(normalized, "ES256");
424
- keyCache.set(pem, key);
425
- return key;
426
- }
427
- async function verifyJwt(config, token) {
428
- if (!token) {
429
- return { result: null };
430
- }
431
- try {
432
- const publicKey = await getPublicKey(config.publicKey);
433
- const audience = normalizeArray(config.audience);
434
- const clockTolerance = Math.floor(
435
- (config.clockSkewMs ?? DEFAULT_CLOCK_SKEW_MS) / 1e3
436
- );
437
- const { payload } = await jwtVerify(token, publicKey, {
438
- issuer: config.issuer,
439
- audience: audience && audience.length === 1 ? audience[0] : audience,
440
- clockTolerance
441
- });
442
- const claims = payload;
443
- if (!claims.sub) {
444
- throw new Error("JWT verification succeeded but subject claim is missing");
445
- }
446
- const authorizedParties = normalizeArray(config.authorizedParties);
447
- if (authorizedParties) {
448
- const azp = claims.azp;
449
- if (!matchesAuthorizedParty(azp, authorizedParties)) {
450
- throw new Error(
451
- `Authorized party '${azp}' does not match any allowed pattern`
452
- );
453
- }
454
- }
455
- return {
456
- result: {
457
- userId: claims.sub,
458
- claims
459
- }
460
- };
461
- } catch (error) {
462
- return {
463
- result: null,
464
- error,
465
- debug: decodeJwtPayload(token)
466
- };
467
- }
468
- }
469
-
470
- // src/server/auth/internalAuth.ts
471
- var DEFAULT_MAX_SKEW_MS = 5 * 6e4;
472
- var nodeCryptoPromise = null;
473
- async function loadNodeCrypto() {
474
- if (!nodeCryptoPromise) {
475
- nodeCryptoPromise = import("crypto").then((module) => module).catch(() => null);
476
- }
477
- return nodeCryptoPromise;
478
- }
479
- function hasSubtleCrypto() {
480
- return typeof globalThis.crypto?.subtle?.importKey === "function";
481
- }
482
- async function computeHmacHex(secret, payload) {
483
- if (hasSubtleCrypto()) {
484
- const encoder = new TextEncoder();
485
- const key = await crypto.subtle.importKey(
486
- "raw",
487
- encoder.encode(secret),
488
- { name: "HMAC", hash: "SHA-256" },
489
- false,
490
- ["sign"]
491
- );
492
- const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(payload));
493
- return bufferToHex(signature);
494
- }
495
- const nodeCrypto = await loadNodeCrypto();
496
- if (!nodeCrypto) {
497
- throw new Error("Crypto implementation unavailable for HMAC computation");
498
- }
499
- return nodeCrypto.createHmac("sha256", secret).update(payload).digest("hex");
500
- }
501
- async function timingSafeEqualHex(a, b) {
502
- if (a.length !== b.length) {
503
- return false;
504
- }
505
- const nodeCrypto = await loadNodeCrypto();
506
- if (nodeCrypto?.timingSafeEqual) {
507
- const aBuf = Buffer.from(a, "hex");
508
- const bBuf = Buffer.from(b, "hex");
509
- try {
510
- return nodeCrypto.timingSafeEqual(aBuf, bBuf);
511
- } catch {
512
- return false;
513
- }
514
- }
515
- let diff = 0;
516
- for (let i = 0; i < a.length; i += 1) {
517
- diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
518
- }
519
- return diff === 0;
520
- }
521
- async function signInternalPayload({
522
- secret,
523
- payload,
524
- timestamp
525
- }) {
526
- const ts = timestamp ?? nowMs().toString();
527
- const signature = await computeHmacHex(secret, `${ts}.${payload}`);
528
- return { timestamp: ts, signature };
529
- }
530
- async function verifyInternalSignature({
531
- secret,
532
- timestamp,
533
- signature,
534
- payload,
535
- maxSkewMs = DEFAULT_MAX_SKEW_MS
536
- }) {
537
- if (!secret || !timestamp || !signature) {
538
- return false;
539
- }
540
- const tsNumber = Number(timestamp);
541
- if (!Number.isFinite(tsNumber)) {
542
- return false;
543
- }
544
- const skew = Math.abs(nowMs() - tsNumber);
545
- if (skew > maxSkewMs) {
546
- return false;
547
- }
548
- const expected = await computeHmacHex(secret, `${timestamp}.${payload}`);
549
- return timingSafeEqualHex(signature.toLowerCase(), expected.toLowerCase());
550
- }
551
- function buildInternalPayload(body) {
552
- if (body === void 0) return "{}";
553
- if (typeof body === "string") return body;
554
- try {
555
- return JSON.stringify(body);
556
- } catch {
557
- return "{}";
558
- }
559
- }
560
-
561
- // src/server/auth/betterAuth.ts
562
- import { betterAuth } from "better-auth";
563
- import { organization, twoFactor } from "better-auth/plugins";
564
- function createDeepSpaceAuth(config) {
565
- const socialProviders = {};
566
- if (config.google) {
567
- socialProviders.google = config.google;
568
- }
569
- if (config.github) {
570
- socialProviders.github = config.github;
571
- }
572
- return betterAuth({
573
- database: config.database,
574
- baseURL: config.baseURL,
575
- secret: config.secret,
576
- emailAndPassword: {
577
- enabled: config.emailAndPassword ?? true
578
- },
579
- socialProviders,
580
- trustedOrigins: config.trustedOrigins ?? [
581
- "https://deep.space",
582
- "https://*.deep.space",
583
- "https://*.app.space",
584
- "http://localhost:*"
585
- ],
586
- plugins: [organization(), twoFactor()]
587
- });
588
- }
589
-
590
- // src/server/utils/cron.ts
591
- var DEFAULT_API_BASE_URL = "https://api-worker.deep.space";
592
- function buildCronContext(env, ownerUserId, roomId = "default") {
593
- const hmacSecret = env.INTERNAL_STORAGE_HMAC_SECRET;
594
- const apiBase = env.API_BASE_URL || DEFAULT_API_BASE_URL;
595
- const roomIdObj = env.RECORD_ROOMS.idFromName(roomId);
596
- const room = env.RECORD_ROOMS.get(roomIdObj);
597
- async function executeTool3(tool, params) {
598
- const response = await room.fetch(new Request("https://internal/api/tools/execute", {
599
- method: "POST",
600
- headers: {
601
- "Content-Type": "application/json",
602
- "X-User-Id": ownerUserId,
603
- "X-App-Action": "true"
604
- },
605
- body: JSON.stringify({ tool, params })
606
- }));
607
- const result = await response.json();
608
- if (!result.success) {
609
- throw new Error(`RecordRoom tool ${tool} failed: ${result.error || "Unknown error"}`);
610
- }
611
- return result.data;
612
- }
613
- const records = {
614
- async query(collection, opts) {
615
- const params = { collection };
616
- if (opts?.where) params.where = opts.where;
617
- if (opts?.limit) params.limit = opts.limit;
618
- const data = await executeTool3("records.query", params);
619
- return data.records || [];
620
- },
621
- async create(collection, data) {
622
- return executeTool3("records.create", { collection, data });
623
- },
624
- async update(collection, recordId, data) {
625
- return executeTool3("records.update", { collection, recordId, data });
626
- },
627
- async delete(collection, recordId) {
628
- return executeTool3("records.delete", { collection, recordId });
629
- }
630
- };
631
- const integrations = {
632
- async call(endpoint, params) {
633
- if (!hmacSecret) {
634
- throw new Error("INTERNAL_STORAGE_HMAC_SECRET not configured \u2014 cannot call integrations");
635
- }
636
- const payload = { userId: ownerUserId, ...params };
637
- const payloadStr = buildInternalPayload(payload);
638
- const { timestamp, signature } = await signInternalPayload({
639
- secret: hmacSecret,
640
- payload: payloadStr
641
- });
642
- const response = await fetch(`${apiBase}/api/internal/integrations/${endpoint}`, {
643
- method: "POST",
644
- headers: {
645
- "Content-Type": "application/json",
646
- "x-internal-timestamp": timestamp,
647
- "x-internal-signature": signature
648
- },
649
- body: payloadStr
650
- });
651
- if (!response.ok) {
652
- const errText = await response.text();
653
- throw new Error(`Integration call ${endpoint} failed (${response.status}): ${errText}`);
654
- }
655
- return response.json();
656
- }
657
- };
658
- return { records, integrations, ownerUserId };
659
- }
660
-
661
- // src/server/utils/ai.ts
662
- import { createAnthropic } from "@ai-sdk/anthropic";
663
- import { createOpenAI } from "@ai-sdk/openai";
664
- import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
665
-
666
365
  // src/server/utils/proxies.ts
667
366
  function resolveApiTransport(env) {
668
367
  if (env.API_WORKER) return { kind: "binding", fetcher: env.API_WORKER };
@@ -738,10 +437,82 @@ function authWorkerFetch(env, path, init) {
738
437
  } else {
739
438
  pathAndSearch = "/" + path;
740
439
  }
741
- return fetch(`${baseUrl}${pathAndSearch}`, init);
440
+ return fetch(`${baseUrl}${pathAndSearch}`, init);
441
+ }
442
+
443
+ // src/server/utils/cron.ts
444
+ function buildCronContext(env, ownerUserId, roomId = "default") {
445
+ const roomIdObj = env.RECORD_ROOMS.idFromName(roomId);
446
+ const room = env.RECORD_ROOMS.get(roomIdObj);
447
+ async function executeTool3(tool, params) {
448
+ const response = await room.fetch(new Request("https://internal/api/tools/execute", {
449
+ method: "POST",
450
+ headers: {
451
+ "Content-Type": "application/json",
452
+ "X-User-Id": ownerUserId,
453
+ "X-App-Action": "true"
454
+ },
455
+ body: JSON.stringify({ tool, params })
456
+ }));
457
+ const result = await response.json();
458
+ if (!result.success) {
459
+ throw new Error(`RecordRoom tool ${tool} failed: ${result.error || "Unknown error"}`);
460
+ }
461
+ return result.data;
462
+ }
463
+ const records = {
464
+ async query(collection, opts) {
465
+ const params = { collection };
466
+ if (opts?.where) params.where = opts.where;
467
+ if (opts?.limit) params.limit = opts.limit;
468
+ const data = await executeTool3("records.query", params);
469
+ return data.records || [];
470
+ },
471
+ async create(collection, data) {
472
+ return executeTool3("records.create", { collection, data });
473
+ },
474
+ async update(collection, recordId, data) {
475
+ return executeTool3("records.update", { collection, recordId, data });
476
+ },
477
+ async delete(collection, recordId) {
478
+ return executeTool3("records.delete", { collection, recordId });
479
+ }
480
+ };
481
+ const integrations = {
482
+ async call(endpoint, params = {}) {
483
+ if (!env.APP_OWNER_JWT) {
484
+ throw new Error(
485
+ "integrations.call requires env.APP_OWNER_JWT. Deployed apps receive this automatically; in dev, `deepspace dev` writes it into .dev.vars."
486
+ );
487
+ }
488
+ const response = await apiWorkerFetch(env, `/api/integrations/${endpoint}`, {
489
+ method: "POST",
490
+ headers: {
491
+ "Content-Type": "application/json",
492
+ Authorization: `Bearer ${env.APP_OWNER_JWT}`
493
+ },
494
+ body: JSON.stringify(params)
495
+ });
496
+ const text = await response.text();
497
+ let result = {};
498
+ try {
499
+ result = text ? JSON.parse(text) : {};
500
+ } catch {
501
+ }
502
+ if (!response.ok || !result.success) {
503
+ const detail = result.message || result.error || text || `HTTP ${response.status}`;
504
+ throw new Error(`Integration call ${endpoint} failed: ${detail}`);
505
+ }
506
+ return result.data;
507
+ }
508
+ };
509
+ return { records, integrations, ownerUserId };
742
510
  }
743
511
 
744
512
  // src/server/utils/ai.ts
513
+ import { createAnthropic } from "@ai-sdk/anthropic";
514
+ import { createOpenAI } from "@ai-sdk/openai";
515
+ import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
745
516
  var ANTHROPIC_DEFAULT_MAX_OUTPUT_TOKENS = 4096;
746
517
  function withDefaultAnthropicMaxTokens(body) {
747
518
  if (typeof body !== "string") return body;
@@ -1544,6 +1315,13 @@ var MSG = {
1544
1315
  USER_LIST: "user.list",
1545
1316
  SET_ROLE: "user.set_role",
1546
1317
  USER_UPDATE: "user.update",
1318
+ // ---- Auth signal -----------------------------------------------------
1319
+ // Server → client one-shot on connect from rooms that enforce role-based
1320
+ // writes (Canvas, Game, Cron). Tells the client whether this connection
1321
+ // can write so its hook can disable local write APIs and the UI can
1322
+ // render controls accordingly. Without this, a viewer's click would
1323
+ // round-trip to the server and silently come back as ERROR.
1324
+ AUTH: "auth",
1547
1325
  // ---- Yjs JSON control frames (binary sync lives in ./yjs.ts) ---------
1548
1326
  YJS_JOIN: "yjs.join",
1549
1327
  YJS_LEAVE: "yjs.leave",
@@ -1577,6 +1355,14 @@ var MSG = {
1577
1355
  CRON_PAUSE: "cron.pause",
1578
1356
  CRON_RESUME: "cron.resume",
1579
1357
  CRON_STATUS: "cron.status",
1358
+ // ---- JobRoom ---------------------------------------------------------
1359
+ // Background-job queue with durable, alarm-driven execution. See
1360
+ // server/rooms/job-room.ts. Clients enqueue and subscribe to updates;
1361
+ // the room broadcasts JOB_UPDATE for every status transition.
1362
+ JOB_ENQUEUE: "job.enqueue",
1363
+ JOB_CANCEL: "job.cancel",
1364
+ JOB_RETRY: "job.retry",
1365
+ JOB_UPDATE: "job.update",
1580
1366
  // ---- PresenceRoom ----------------------------------------------------
1581
1367
  PRESENCE_SYNC: "presence.sync",
1582
1368
  PRESENCE_JOIN: "presence.join",
@@ -2069,6 +1855,17 @@ var serverBuild = {
2069
1855
  cronTasks: (tasks) => ({ type: MSG.CRON_TASKS, payload: { tasks } }),
2070
1856
  cronHistory: (history) => ({ type: MSG.CRON_HISTORY, payload: { history } }),
2071
1857
  cronStatus: (tasks, recentHistory) => ({ type: MSG.CRON_STATUS, payload: { tasks, recentHistory } }),
1858
+ // Jobs
1859
+ jobSnapshot: (jobs) => ({ type: MSG.JOB_UPDATE, payload: { kind: "snapshot", jobs } }),
1860
+ jobEnqueued: (job, requestId) => ({
1861
+ type: MSG.JOB_UPDATE,
1862
+ payload: { kind: "enqueued", job, requestId }
1863
+ }),
1864
+ jobProgress: (job) => ({ type: MSG.JOB_UPDATE, payload: { kind: "progress", job } }),
1865
+ jobSucceeded: (job) => ({ type: MSG.JOB_UPDATE, payload: { kind: "succeeded", job } }),
1866
+ jobFailed: (job) => ({ type: MSG.JOB_UPDATE, payload: { kind: "failed", job } }),
1867
+ jobCanceled: (job) => ({ type: MSG.JOB_UPDATE, payload: { kind: "canceled", job } }),
1868
+ jobRetried: (job) => ({ type: MSG.JOB_UPDATE, payload: { kind: "retried", job } }),
2072
1869
  // Presence
2073
1870
  presenceSync: (peers) => ({ type: MSG.PRESENCE_SYNC, payload: { peers } }),
2074
1871
  presenceJoin: (peer) => ({ type: MSG.PRESENCE_JOIN, payload: { peer } }),
@@ -4160,7 +3957,20 @@ var YjsRoom = class extends BaseRoom {
4160
3957
  }
4161
3958
  };
4162
3959
 
3960
+ // src/shared/roles.ts
3961
+ var ROLES = {
3962
+ VIEWER: "viewer",
3963
+ MEMBER: "member",
3964
+ ADMIN: "admin"
3965
+ };
3966
+
4163
3967
  // src/server/rooms/game-room.ts
3968
+ var GAME_WRITE_TYPES = /* @__PURE__ */ new Set([
3969
+ MSG.GAME_INPUT,
3970
+ MSG.GAME_PLAYER_READY,
3971
+ MSG.GAME_START,
3972
+ MSG.GAME_END
3973
+ ]);
4164
3974
  var GameRoom = class extends BaseRoom {
4165
3975
  config;
4166
3976
  players = /* @__PURE__ */ new Map();
@@ -4216,10 +4026,26 @@ var GameRoom = class extends BaseRoom {
4216
4026
  // ==========================================================================
4217
4027
  onConnect(ws, user) {
4218
4028
  this.ensureInitialized();
4029
+ const role = user.role ?? ROLES.VIEWER;
4030
+ const canWrite = role === ROLES.MEMBER || role === ROLES.ADMIN;
4219
4031
  const attachment = {
4220
4032
  ...user,
4221
- joinedAt: (/* @__PURE__ */ new Date()).toISOString()
4033
+ joinedAt: (/* @__PURE__ */ new Date()).toISOString(),
4034
+ canWrite
4222
4035
  };
4036
+ this.sendTo(ws, { type: MSG.AUTH, payload: { canWrite } });
4037
+ if (!canWrite) {
4038
+ this.sendTo(ws, {
4039
+ type: MSG.GAME_STATE,
4040
+ payload: {
4041
+ state: this.gameState,
4042
+ tick: this.currentTick,
4043
+ players: Array.from(this.players.values()),
4044
+ running: this.running
4045
+ }
4046
+ });
4047
+ return attachment;
4048
+ }
4223
4049
  const player = {
4224
4050
  userId: user.userId,
4225
4051
  userName: user.userName,
@@ -4248,6 +4074,13 @@ var GameRoom = class extends BaseRoom {
4248
4074
  async onMessage(ws, user, message) {
4249
4075
  this.ensureInitialized();
4250
4076
  const { type, payload } = message;
4077
+ if (GAME_WRITE_TYPES.has(type) && !user.canWrite) {
4078
+ this.sendTo(ws, {
4079
+ type: MSG.ERROR,
4080
+ payload: { error: "Write access denied: spectators cannot drive the game" }
4081
+ });
4082
+ return;
4083
+ }
4251
4084
  switch (type) {
4252
4085
  case MSG.GAME_INPUT: {
4253
4086
  this.inputBuffer.push({
@@ -4396,6 +4229,15 @@ var GameRoom = class extends BaseRoom {
4396
4229
 
4397
4230
  // src/server/rooms/canvas-room.ts
4398
4231
  import * as Y5 from "yjs";
4232
+ var CANVAS_WRITE_TYPES = /* @__PURE__ */ new Set([
4233
+ MSG.CANVAS_ADD,
4234
+ MSG.CANVAS_MOVE,
4235
+ MSG.CANVAS_RESIZE,
4236
+ MSG.CANVAS_DELETE,
4237
+ MSG.CANVAS_UPDATE,
4238
+ MSG.CANVAS_UNDO,
4239
+ MSG.CANVAS_REDO
4240
+ ]);
4399
4241
  var CanvasRoom = class extends BaseRoom {
4400
4242
  doc = null;
4401
4243
  initialized = false;
@@ -4455,10 +4297,14 @@ var CanvasRoom = class extends BaseRoom {
4455
4297
  }
4456
4298
  onConnect(ws, user) {
4457
4299
  this.ensureInitialized();
4300
+ const role = user.role ?? ROLES.VIEWER;
4301
+ const canWrite = role === ROLES.MEMBER || role === ROLES.ADMIN;
4458
4302
  const attachment = {
4459
4303
  ...user,
4460
- viewport: null
4304
+ viewport: null,
4305
+ canWrite
4461
4306
  };
4307
+ this.sendTo(ws, { type: MSG.AUTH, payload: { canWrite } });
4462
4308
  const shapes = this.getAllShapes();
4463
4309
  this.sendTo(ws, {
4464
4310
  type: MSG.CANVAS_SHAPES,
@@ -4472,6 +4318,13 @@ var CanvasRoom = class extends BaseRoom {
4472
4318
  async onMessage(ws, user, message) {
4473
4319
  this.ensureInitialized();
4474
4320
  const { type, payload } = message;
4321
+ if (CANVAS_WRITE_TYPES.has(type) && !user.canWrite) {
4322
+ this.sendTo(ws, {
4323
+ type: MSG.ERROR,
4324
+ payload: { error: "Write access denied: viewer role cannot modify the canvas" }
4325
+ });
4326
+ return;
4327
+ }
4475
4328
  switch (type) {
4476
4329
  case MSG.CANVAS_ADD: {
4477
4330
  const shape = payload;
@@ -4766,6 +4619,11 @@ var PresenceRoom = class extends BaseRoom {
4766
4619
  };
4767
4620
 
4768
4621
  // src/server/rooms/cron-room.ts
4622
+ var CRON_WRITE_TYPES = /* @__PURE__ */ new Set([
4623
+ MSG.CRON_TRIGGER,
4624
+ MSG.CRON_PAUSE,
4625
+ MSG.CRON_RESUME
4626
+ ]);
4769
4627
  var CronRoom = class extends BaseRoom {
4770
4628
  tasks;
4771
4629
  initialized = false;
@@ -4844,6 +4702,9 @@ var CronRoom = class extends BaseRoom {
4844
4702
  }
4845
4703
  onConnect(ws, user) {
4846
4704
  this.ensureInitialized();
4705
+ const role = user.role ?? ROLES.VIEWER;
4706
+ const canWrite = role === ROLES.MEMBER || role === ROLES.ADMIN;
4707
+ this.sendTo(ws, { type: MSG.AUTH, payload: { canWrite } });
4847
4708
  this.sendTo(ws, {
4848
4709
  type: MSG.CRON_TASKS,
4849
4710
  payload: { tasks: this.getTaskStates() }
@@ -4852,11 +4713,18 @@ var CronRoom = class extends BaseRoom {
4852
4713
  type: MSG.CRON_HISTORY,
4853
4714
  payload: { history: this.getRecentHistory(50) }
4854
4715
  });
4855
- return user;
4716
+ return { ...user, canWrite };
4856
4717
  }
4857
4718
  async onMessage(ws, user, message) {
4858
4719
  this.ensureInitialized();
4859
4720
  const { type, payload } = message;
4721
+ if (CRON_WRITE_TYPES.has(type) && !user.canWrite) {
4722
+ this.sendTo(ws, {
4723
+ type: MSG.ERROR,
4724
+ payload: { error: "Write access denied: viewer role cannot modify cron tasks" }
4725
+ });
4726
+ return;
4727
+ }
4860
4728
  switch (type) {
4861
4729
  case MSG.CRON_TRIGGER: {
4862
4730
  const taskName = payload.taskName;
@@ -5126,45 +4994,582 @@ function computeNextRunAt(task, from) {
5126
4994
  if (task.interval_minutes) {
5127
4995
  return new Date(from.getTime() + task.interval_minutes * 60 * 1e3).toISOString();
5128
4996
  }
5129
- if (task.schedule && task.timezone) {
5130
- const next = nextCronFire(task.schedule, task.timezone, from);
5131
- return next ? next.toISOString() : null;
4997
+ if (task.schedule && task.timezone) {
4998
+ const next = nextCronFire(task.schedule, task.timezone, from);
4999
+ return next ? next.toISOString() : null;
5000
+ }
5001
+ return null;
5002
+ }
5003
+ function nextCronFire(expression, timezone, from) {
5004
+ const parsed = parseCronExpression(expression);
5005
+ const start = new Date(Math.floor(from.getTime() / 6e4) * 6e4 + 6e4);
5006
+ const limit = new Date(start.getTime() + 366 * 24 * 60 * 6e4);
5007
+ for (let t = start.getTime(); t < limit.getTime(); t += 6e4) {
5008
+ const candidate = new Date(t);
5009
+ if (cronMatches(parsed, timezone, candidate)) {
5010
+ return candidate;
5011
+ }
5012
+ }
5013
+ return null;
5014
+ }
5015
+ function cronMatches(parsed, timezone, date) {
5016
+ const formatter = new Intl.DateTimeFormat("en-US", {
5017
+ timeZone: timezone,
5018
+ year: "numeric",
5019
+ month: "numeric",
5020
+ day: "numeric",
5021
+ hour: "numeric",
5022
+ minute: "numeric",
5023
+ weekday: "short",
5024
+ hour12: false
5025
+ });
5026
+ const parts = Object.fromEntries(
5027
+ formatter.formatToParts(date).map((p) => [p.type, p.value])
5028
+ );
5029
+ const minute = parseInt(parts.minute, 10);
5030
+ const hour = parseInt(parts.hour, 10) % 24;
5031
+ const dayOfMonth = parseInt(parts.day, 10);
5032
+ const month = parseInt(parts.month, 10);
5033
+ const weekdayMap = { Sun: 0, Mon: 1, Tue: 2, Wed: 3, Thu: 4, Fri: 5, Sat: 6 };
5034
+ const dayOfWeek = weekdayMap[parts.weekday] ?? 0;
5035
+ return parsed.minute.has(minute) && parsed.hour.has(hour) && parsed.dayOfMonth.has(dayOfMonth) && parsed.month.has(month) && parsed.dayOfWeek.has(dayOfWeek);
5036
+ }
5037
+
5038
+ // src/server/rooms/job-room.ts
5039
+ var RUNNING_STALE_MS = 16 * 60 * 1e3;
5040
+ var DEFAULT_RETENTION_MS = 24 * 60 * 60 * 1e3;
5041
+ var DEFAULT_SNAPSHOT_LIMIT = 100;
5042
+ var JobRoom = class extends BaseRoom {
5043
+ initialized = false;
5044
+ defaultMaxAttempts;
5045
+ retentionMs;
5046
+ snapshotLimit;
5047
+ retryBackoffMs;
5048
+ /** In-flight jobs in this isolate; lets same-isolate JOB_CANCEL fire the signal. */
5049
+ inFlight = /* @__PURE__ */ new Map();
5050
+ /** Set by `ctx.continue` inside `executeJob`; reset before every run. */
5051
+ continueState = null;
5052
+ constructor(state, env, config = {}) {
5053
+ super(state, env);
5054
+ this.defaultMaxAttempts = Math.max(1, config.defaultMaxAttempts ?? 1);
5055
+ this.retentionMs = Math.max(0, config.retentionMs ?? DEFAULT_RETENTION_MS);
5056
+ this.snapshotLimit = Math.max(1, config.snapshotLimit ?? DEFAULT_SNAPSHOT_LIMIT);
5057
+ this.retryBackoffMs = Math.max(0, config.retryBackoffMs ?? 1e3);
5058
+ }
5059
+ ensureInitialized() {
5060
+ if (this.initialized) return;
5061
+ this.initialized = true;
5062
+ this.sql.exec(`
5063
+ CREATE TABLE IF NOT EXISTS jobs (
5064
+ id TEXT PRIMARY KEY,
5065
+ type TEXT NOT NULL,
5066
+ status TEXT NOT NULL,
5067
+ payload TEXT,
5068
+ result TEXT,
5069
+ error TEXT,
5070
+ progress REAL,
5071
+ progress_message TEXT,
5072
+ checkpoint TEXT,
5073
+ attempts INTEGER NOT NULL DEFAULT 0,
5074
+ max_attempts INTEGER NOT NULL DEFAULT 1,
5075
+ enqueued_at TEXT NOT NULL,
5076
+ started_at TEXT,
5077
+ completed_at TEXT,
5078
+ enqueued_by TEXT,
5079
+ next_run_at TEXT
5080
+ )
5081
+ `);
5082
+ this.sql.exec(
5083
+ `CREATE INDEX IF NOT EXISTS jobs_status_next_run ON jobs (status, next_run_at)`
5084
+ );
5085
+ this.sql.exec(
5086
+ `CREATE INDEX IF NOT EXISTS jobs_completed_at ON jobs (completed_at)`
5087
+ );
5088
+ this.recoverStuckRunning();
5089
+ this.scheduleNextAlarm();
5090
+ }
5091
+ // ==========================================================================
5092
+ // BaseRoom Lifecycle
5093
+ // ==========================================================================
5094
+ async fetch(request) {
5095
+ this.ensureInitialized();
5096
+ return super.fetch(request);
5097
+ }
5098
+ onConnect(ws, user) {
5099
+ this.ensureInitialized();
5100
+ this.sendTo(ws, serverBuild.jobSnapshot(this.getRecentJobs(this.snapshotLimit)));
5101
+ return user;
5102
+ }
5103
+ async onMessage(ws, user, message) {
5104
+ this.ensureInitialized();
5105
+ const { type, payload } = message;
5106
+ switch (type) {
5107
+ case MSG.JOB_ENQUEUE: {
5108
+ const requestId = String(payload.requestId ?? "");
5109
+ const jobType = String(payload.type ?? "");
5110
+ if (!jobType) {
5111
+ this.sendTo(ws, serverBuild.error("JOB_ENQUEUE missing type"));
5112
+ return;
5113
+ }
5114
+ const job = this.insertJob({
5115
+ type: jobType,
5116
+ payload: payload.payload,
5117
+ maxAttempts: typeof payload.maxAttempts === "number" ? Math.max(1, Math.floor(payload.maxAttempts)) : void 0,
5118
+ enqueuedBy: user.userId
5119
+ });
5120
+ this.sendTo(ws, serverBuild.jobEnqueued(job, requestId));
5121
+ this.broadcast(serverBuild.jobEnqueued(job), ws);
5122
+ this.scheduleNextAlarm();
5123
+ break;
5124
+ }
5125
+ case MSG.JOB_CANCEL: {
5126
+ const jobId = String(payload.jobId ?? "");
5127
+ if (!jobId) {
5128
+ this.sendTo(ws, serverBuild.error("JOB_CANCEL missing jobId"));
5129
+ return;
5130
+ }
5131
+ this.cancelJob(jobId);
5132
+ break;
5133
+ }
5134
+ case MSG.JOB_RETRY: {
5135
+ const jobId = String(payload.jobId ?? "");
5136
+ if (!jobId) {
5137
+ this.sendTo(ws, serverBuild.error("JOB_RETRY missing jobId"));
5138
+ return;
5139
+ }
5140
+ this.retryJob(jobId);
5141
+ break;
5142
+ }
5143
+ default:
5144
+ this.sendTo(ws, serverBuild.error(`Unknown job message type: ${type}`));
5145
+ }
5146
+ }
5147
+ async onAlarm() {
5148
+ this.ensureInitialized();
5149
+ await this.drainDueJobs();
5150
+ this.pruneExpired();
5151
+ this.scheduleNextAlarm();
5152
+ }
5153
+ // ==========================================================================
5154
+ // HTTP — internal enqueue path for cross-isolate callers
5155
+ // ==========================================================================
5156
+ /**
5157
+ * One internal endpoint, `POST /enqueue`, used by the `enqueueJob`
5158
+ * helper at the bottom of this file. The room is not meant to be
5159
+ * reachable from the public internet; callers route through the app
5160
+ * worker (or call `this.enqueue(...)` directly from a subclass).
5161
+ *
5162
+ * Subclasses overriding `onRequest` should call `super.onRequest(req)`
5163
+ * for paths they don't handle.
5164
+ */
5165
+ async onRequest(request) {
5166
+ this.ensureInitialized();
5167
+ const url = new URL(request.url);
5168
+ if (request.method !== "POST" || !url.pathname.endsWith("/enqueue")) {
5169
+ return new Response("Not Found", { status: 404 });
5170
+ }
5171
+ const body = await request.json().catch(() => null);
5172
+ if (!body || typeof body.type !== "string" || !body.type) {
5173
+ return new Response(JSON.stringify({ error: "type required" }), {
5174
+ status: 400,
5175
+ headers: { "Content-Type": "application/json" }
5176
+ });
5177
+ }
5178
+ const job = this.insertJob({
5179
+ type: body.type,
5180
+ payload: body.payload,
5181
+ maxAttempts: typeof body.maxAttempts === "number" ? Math.max(1, Math.floor(body.maxAttempts)) : void 0,
5182
+ enqueuedBy: typeof body.enqueuedBy === "string" ? body.enqueuedBy : void 0
5183
+ });
5184
+ this.broadcast(serverBuild.jobEnqueued(job));
5185
+ this.scheduleNextAlarm();
5186
+ return new Response(JSON.stringify({ job }), {
5187
+ status: 200,
5188
+ headers: { "Content-Type": "application/json" }
5189
+ });
5190
+ }
5191
+ // ==========================================================================
5192
+ // Public enqueue surface (server-side callers in the same isolate)
5193
+ // ==========================================================================
5194
+ /**
5195
+ * Synchronously persist a new job and arm the alarm. Returns the new
5196
+ * job. This is the in-isolate path — useful for a subclass `onJob`
5197
+ * that wants to chain follow-up work without paying a fetch hop:
5198
+ *
5199
+ * protected async onJob(job: Job, ctx: JobContext) {
5200
+ * // ... do work ...
5201
+ * if (needsFollowup) this.enqueue('followup', { parent: job.id })
5202
+ * }
5203
+ *
5204
+ * Workers code that lives outside the DO isolate (HTTP routes, cron
5205
+ * tasks, server actions) cannot call this directly — they must go
5206
+ * through the `enqueueJob` helper at the bottom of this file, which
5207
+ * routes via the DO's `/enqueue` HTTP endpoint.
5208
+ */
5209
+ enqueue(type, payload, options) {
5210
+ this.ensureInitialized();
5211
+ const job = this.insertJob({
5212
+ type,
5213
+ payload,
5214
+ maxAttempts: options?.maxAttempts,
5215
+ enqueuedBy: options?.enqueuedBy
5216
+ });
5217
+ this.broadcast(serverBuild.jobEnqueued(job));
5218
+ this.scheduleNextAlarm();
5219
+ return job;
5220
+ }
5221
+ // ==========================================================================
5222
+ // Mutation helpers
5223
+ // ==========================================================================
5224
+ insertJob(input) {
5225
+ const id = crypto.randomUUID();
5226
+ const enqueuedAt = (/* @__PURE__ */ new Date()).toISOString();
5227
+ const maxAttempts = Math.max(1, input.maxAttempts ?? this.defaultMaxAttempts);
5228
+ const payloadJson = input.payload === void 0 ? null : safeStringify(input.payload);
5229
+ this.sql.exec(
5230
+ `INSERT INTO jobs (id, type, status, payload, attempts, max_attempts, enqueued_at, enqueued_by, next_run_at)
5231
+ VALUES (?, ?, 'queued', ?, 0, ?, ?, ?, ?)`,
5232
+ id,
5233
+ input.type,
5234
+ payloadJson,
5235
+ maxAttempts,
5236
+ enqueuedAt,
5237
+ input.enqueuedBy ?? null,
5238
+ enqueuedAt
5239
+ );
5240
+ return this.getJobById(id);
5241
+ }
5242
+ cancelJob(jobId) {
5243
+ const row = this.sql.exec(`SELECT status FROM jobs WHERE id = ?`, jobId).toArray()[0];
5244
+ if (!row) return;
5245
+ if (row.status === "succeeded" || row.status === "failed" || row.status === "canceled") {
5246
+ return;
5247
+ }
5248
+ const completedAt = (/* @__PURE__ */ new Date()).toISOString();
5249
+ this.sql.exec(
5250
+ `UPDATE jobs
5251
+ SET status = 'canceled',
5252
+ completed_at = ?,
5253
+ checkpoint = NULL,
5254
+ next_run_at = NULL
5255
+ WHERE id = ?`,
5256
+ completedAt,
5257
+ jobId
5258
+ );
5259
+ const controller = this.inFlight.get(jobId);
5260
+ if (controller) controller.abort();
5261
+ const job = this.getJobById(jobId);
5262
+ if (job) this.broadcast(serverBuild.jobCanceled(job));
5263
+ }
5264
+ retryJob(jobId) {
5265
+ const row = this.sql.exec(`SELECT status, attempts, max_attempts FROM jobs WHERE id = ?`, jobId).toArray()[0];
5266
+ if (!row) return;
5267
+ if (row.status !== "failed" && row.status !== "canceled") return;
5268
+ const newMax = Math.max(row.max_attempts, row.attempts + 1);
5269
+ this.sql.exec(
5270
+ `UPDATE jobs
5271
+ SET status = 'queued',
5272
+ max_attempts = ?,
5273
+ error = NULL,
5274
+ result = NULL,
5275
+ completed_at = NULL,
5276
+ next_run_at = ?
5277
+ WHERE id = ?`,
5278
+ newMax,
5279
+ (/* @__PURE__ */ new Date()).toISOString(),
5280
+ jobId
5281
+ );
5282
+ const job = this.getJobById(jobId);
5283
+ if (job) this.broadcast(serverBuild.jobRetried(job));
5284
+ this.scheduleNextAlarm();
5285
+ }
5286
+ // ==========================================================================
5287
+ // Alarm / execution
5288
+ // ==========================================================================
5289
+ /**
5290
+ * Drain due jobs in FIFO order. All due jobs share the alarm's
5291
+ * 15-min wall budget (a CF property, not a per-job allocation).
5292
+ */
5293
+ async drainDueJobs() {
5294
+ const cutoff = (/* @__PURE__ */ new Date()).toISOString();
5295
+ while (true) {
5296
+ const row = this.sql.exec(
5297
+ `SELECT * FROM jobs
5298
+ WHERE status = 'queued'
5299
+ AND (next_run_at IS NULL OR next_run_at <= ?)
5300
+ ORDER BY enqueued_at ASC
5301
+ LIMIT 1`,
5302
+ cutoff
5303
+ ).toArray()[0];
5304
+ if (!row) return;
5305
+ await this.executeJob(row);
5306
+ }
5307
+ }
5308
+ async executeJob(row) {
5309
+ const startedAt = row.started_at ?? (/* @__PURE__ */ new Date()).toISOString();
5310
+ const attemptsBefore = row.attempts;
5311
+ const attempt = attemptsBefore + 1;
5312
+ this.sql.exec(
5313
+ `UPDATE jobs SET status = 'running', started_at = ?, attempts = ? WHERE id = ?`,
5314
+ startedAt,
5315
+ attempt,
5316
+ row.id
5317
+ );
5318
+ const controller = new AbortController();
5319
+ this.inFlight.set(row.id, controller);
5320
+ this.continueState = null;
5321
+ const job = {
5322
+ ...rowToJobView(row),
5323
+ status: "running",
5324
+ attempts: attempt,
5325
+ startedAt
5326
+ };
5327
+ let outcome = "succeeded";
5328
+ let result;
5329
+ let errorMessage;
5330
+ try {
5331
+ const ctx = this.makeContext(row.id, controller.signal);
5332
+ result = await this.onJob(job, ctx);
5333
+ if (this.continueState) outcome = "continued";
5334
+ } catch (e) {
5335
+ outcome = "failed";
5336
+ errorMessage = e instanceof Error ? e.message : String(e);
5337
+ console.error(`[JobRoom] Job "${row.type}" (${row.id}) failed:`, e);
5338
+ } finally {
5339
+ this.inFlight.delete(row.id);
5340
+ }
5341
+ const currentStatus = this.readStatus(row.id);
5342
+ if (currentStatus === "canceled") {
5343
+ this.continueState = null;
5344
+ return;
5345
+ }
5346
+ if (outcome === "continued") {
5347
+ const state = this.continueState;
5348
+ this.continueState = null;
5349
+ const nextRunAt = new Date(Date.now() + Math.max(0, state.afterMs)).toISOString();
5350
+ this.sql.exec(
5351
+ `UPDATE jobs
5352
+ SET status = 'queued',
5353
+ checkpoint = ?,
5354
+ next_run_at = ?
5355
+ WHERE id = ?`,
5356
+ safeStringify(state.checkpoint),
5357
+ nextRunAt,
5358
+ row.id
5359
+ );
5360
+ const updated2 = this.getJobById(row.id);
5361
+ if (updated2) this.broadcast(serverBuild.jobProgress(updated2));
5362
+ return;
5363
+ }
5364
+ const completedAt = (/* @__PURE__ */ new Date()).toISOString();
5365
+ if (outcome === "succeeded") {
5366
+ this.sql.exec(
5367
+ `UPDATE jobs
5368
+ SET status = 'succeeded',
5369
+ result = ?,
5370
+ completed_at = ?,
5371
+ error = NULL,
5372
+ checkpoint = NULL,
5373
+ next_run_at = NULL,
5374
+ progress = NULL,
5375
+ progress_message = NULL
5376
+ WHERE id = ?`,
5377
+ result === void 0 ? null : safeStringify(result),
5378
+ completedAt,
5379
+ row.id
5380
+ );
5381
+ const updated2 = this.getJobById(row.id);
5382
+ if (updated2) this.broadcast(serverBuild.jobSucceeded(updated2));
5383
+ return;
5384
+ }
5385
+ if (attempt < row.max_attempts) {
5386
+ const nextRunAt = new Date(Date.now() + this.retryBackoffMs).toISOString();
5387
+ this.sql.exec(
5388
+ `UPDATE jobs
5389
+ SET status = 'queued',
5390
+ error = ?,
5391
+ next_run_at = ?,
5392
+ started_at = NULL,
5393
+ progress = NULL,
5394
+ progress_message = NULL
5395
+ WHERE id = ?`,
5396
+ errorMessage ?? "unknown error",
5397
+ nextRunAt,
5398
+ row.id
5399
+ );
5400
+ const updated2 = this.getJobById(row.id);
5401
+ if (updated2) this.broadcast(serverBuild.jobRetried(updated2));
5402
+ return;
5403
+ }
5404
+ this.sql.exec(
5405
+ `UPDATE jobs
5406
+ SET status = 'failed',
5407
+ error = ?,
5408
+ completed_at = ?,
5409
+ checkpoint = NULL,
5410
+ next_run_at = NULL,
5411
+ progress = NULL,
5412
+ progress_message = NULL
5413
+ WHERE id = ?`,
5414
+ errorMessage ?? "unknown error",
5415
+ completedAt,
5416
+ row.id
5417
+ );
5418
+ const updated = this.getJobById(row.id);
5419
+ if (updated) this.broadcast(serverBuild.jobFailed(updated));
5420
+ }
5421
+ makeContext(jobId, signal) {
5422
+ return {
5423
+ progress: (value, message) => {
5424
+ const clamped = Math.max(0, Math.min(1, value));
5425
+ this.sql.exec(
5426
+ `UPDATE jobs SET progress = ?, progress_message = ? WHERE id = ?`,
5427
+ clamped,
5428
+ message ?? null,
5429
+ jobId
5430
+ );
5431
+ const updated = this.getJobById(jobId);
5432
+ if (updated) this.broadcast(serverBuild.jobProgress(updated));
5433
+ },
5434
+ continue: (state, options) => {
5435
+ this.continueState = {
5436
+ checkpoint: state,
5437
+ afterMs: Math.max(0, options?.afterMs ?? 0)
5438
+ };
5439
+ },
5440
+ signal
5441
+ };
5442
+ }
5443
+ readStatus(jobId) {
5444
+ const row = this.sql.exec(`SELECT status FROM jobs WHERE id = ?`, jobId).toArray()[0];
5445
+ return row?.status ?? null;
5446
+ }
5447
+ // ==========================================================================
5448
+ // Scheduling / recovery / pruning
5449
+ // ==========================================================================
5450
+ scheduleNextAlarm() {
5451
+ const row = this.sql.exec(
5452
+ `SELECT next_run_at FROM jobs
5453
+ WHERE status = 'queued'
5454
+ ORDER BY (next_run_at IS NULL) ASC, next_run_at ASC
5455
+ LIMIT 1`
5456
+ ).toArray()[0];
5457
+ if (!row) return;
5458
+ const now = Date.now();
5459
+ const target = row.next_run_at ? Math.max(now, new Date(row.next_run_at).getTime()) : now;
5460
+ this.state.storage.setAlarm(Math.max(target, now + 50));
5461
+ }
5462
+ recoverStuckRunning() {
5463
+ const cutoff = new Date(Date.now() - RUNNING_STALE_MS).toISOString();
5464
+ const stuck = this.sql.exec(
5465
+ `SELECT id, attempts, max_attempts FROM jobs
5466
+ WHERE status = 'running' AND started_at < ?`,
5467
+ cutoff
5468
+ ).toArray();
5469
+ for (const r of stuck) {
5470
+ if (r.attempts >= r.max_attempts) {
5471
+ this.sql.exec(
5472
+ `UPDATE jobs
5473
+ SET status = 'failed',
5474
+ error = 'Isolate recycled or wall time exceeded',
5475
+ completed_at = ?,
5476
+ checkpoint = NULL,
5477
+ next_run_at = NULL
5478
+ WHERE id = ?`,
5479
+ (/* @__PURE__ */ new Date()).toISOString(),
5480
+ r.id
5481
+ );
5482
+ } else {
5483
+ this.sql.exec(
5484
+ `UPDATE jobs
5485
+ SET status = 'queued',
5486
+ started_at = NULL,
5487
+ next_run_at = ?,
5488
+ error = 'Previous attempt killed mid-run; retrying',
5489
+ progress = NULL,
5490
+ progress_message = NULL
5491
+ WHERE id = ?`,
5492
+ (/* @__PURE__ */ new Date()).toISOString(),
5493
+ r.id
5494
+ );
5495
+ }
5496
+ }
5497
+ }
5498
+ pruneExpired() {
5499
+ if (this.retentionMs === 0) return;
5500
+ const cutoff = new Date(Date.now() - this.retentionMs).toISOString();
5501
+ this.sql.exec(
5502
+ `DELETE FROM jobs
5503
+ WHERE status IN ('succeeded', 'failed', 'canceled')
5504
+ AND completed_at IS NOT NULL
5505
+ AND completed_at < ?`,
5506
+ cutoff
5507
+ );
5508
+ }
5509
+ // ==========================================================================
5510
+ // Read helpers
5511
+ // ==========================================================================
5512
+ getRecentJobs(limit) {
5513
+ const rows = this.sql.exec(
5514
+ `SELECT * FROM jobs
5515
+ ORDER BY (status IN ('queued','running')) DESC, enqueued_at DESC
5516
+ LIMIT ?`,
5517
+ limit
5518
+ ).toArray();
5519
+ return rows.map(rowToJobView);
5520
+ }
5521
+ getJobById(id) {
5522
+ const row = this.sql.exec(`SELECT * FROM jobs WHERE id = ?`, id).toArray()[0];
5523
+ return row ? rowToJobView(row) : null;
5132
5524
  }
5133
- return null;
5134
- }
5135
- function nextCronFire(expression, timezone, from) {
5136
- const parsed = parseCronExpression(expression);
5137
- const start = new Date(Math.floor(from.getTime() / 6e4) * 6e4 + 6e4);
5138
- const limit = new Date(start.getTime() + 366 * 24 * 60 * 6e4);
5139
- for (let t = start.getTime(); t < limit.getTime(); t += 6e4) {
5140
- const candidate = new Date(t);
5141
- if (cronMatches(parsed, timezone, candidate)) {
5142
- return candidate;
5143
- }
5525
+ };
5526
+ async function enqueueJob(namespace, roomId, type, payload, options) {
5527
+ const stub = namespace.get(namespace.idFromName(roomId));
5528
+ const res = await stub.fetch(
5529
+ new Request("https://job-room.internal/enqueue", {
5530
+ method: "POST",
5531
+ headers: { "Content-Type": "application/json" },
5532
+ body: JSON.stringify({
5533
+ type,
5534
+ payload,
5535
+ maxAttempts: options?.maxAttempts,
5536
+ enqueuedBy: options?.enqueuedBy
5537
+ })
5538
+ })
5539
+ );
5540
+ if (!res.ok) {
5541
+ const text = await res.text().catch(() => "");
5542
+ throw new Error(`enqueueJob failed: ${res.status} ${text}`);
5144
5543
  }
5145
- return null;
5544
+ const data = await res.json();
5545
+ if (!data.job?.id) throw new Error("enqueueJob: malformed response");
5546
+ return data.job.id;
5146
5547
  }
5147
- function cronMatches(parsed, timezone, date) {
5148
- const formatter = new Intl.DateTimeFormat("en-US", {
5149
- timeZone: timezone,
5150
- year: "numeric",
5151
- month: "numeric",
5152
- day: "numeric",
5153
- hour: "numeric",
5154
- minute: "numeric",
5155
- weekday: "short",
5156
- hour12: false
5157
- });
5158
- const parts = Object.fromEntries(
5159
- formatter.formatToParts(date).map((p) => [p.type, p.value])
5160
- );
5161
- const minute = parseInt(parts.minute, 10);
5162
- const hour = parseInt(parts.hour, 10) % 24;
5163
- const dayOfMonth = parseInt(parts.day, 10);
5164
- const month = parseInt(parts.month, 10);
5165
- const weekdayMap = { Sun: 0, Mon: 1, Tue: 2, Wed: 3, Thu: 4, Fri: 5, Sat: 6 };
5166
- const dayOfWeek = weekdayMap[parts.weekday] ?? 0;
5167
- return parsed.minute.has(minute) && parsed.hour.has(hour) && parsed.dayOfMonth.has(dayOfMonth) && parsed.month.has(month) && parsed.dayOfWeek.has(dayOfWeek);
5548
+ function rowToJobView(row) {
5549
+ return {
5550
+ id: row.id,
5551
+ type: row.type,
5552
+ status: row.status,
5553
+ payload: row.payload == null ? void 0 : JSON.parse(row.payload),
5554
+ result: row.result == null ? void 0 : JSON.parse(row.result),
5555
+ error: row.error ?? void 0,
5556
+ progress: row.progress ?? void 0,
5557
+ progressMessage: row.progress_message ?? void 0,
5558
+ attempts: row.attempts,
5559
+ maxAttempts: row.max_attempts,
5560
+ enqueuedAt: row.enqueued_at,
5561
+ startedAt: row.started_at,
5562
+ completedAt: row.completed_at,
5563
+ enqueuedBy: row.enqueued_by,
5564
+ resumeFrom: row.checkpoint == null ? void 0 : JSON.parse(row.checkpoint)
5565
+ };
5566
+ }
5567
+ function safeStringify(value) {
5568
+ try {
5569
+ return JSON.stringify(value);
5570
+ } catch {
5571
+ return JSON.stringify({ __unserializable__: String(value) });
5572
+ }
5168
5573
  }
5169
5574
 
5170
5575
  // src/server/rooms/do-manifest.ts
@@ -5247,7 +5652,6 @@ var RESERVED_BINDING_NAMES = /* @__PURE__ */ new Set([
5247
5652
  "AUTH_WORKER_URL",
5248
5653
  "APP_IDENTITY_TOKEN",
5249
5654
  "APP_OWNER_JWT",
5250
- "INTERNAL_STORAGE_HMAC_SECRET",
5251
5655
  "USAGE_EVENTS"
5252
5656
  ]);
5253
5657
  function validateBindingManifest(manifest) {
@@ -6079,6 +6483,215 @@ var RESERVED_COLLECTION_NAMES = new Set(
6079
6483
  GLOBAL_DO_TYPES.flatMap((t) => t.schemas.map((s) => s.name))
6080
6484
  );
6081
6485
 
6486
+ // src/server/auth/jwtVerifier.ts
6487
+ import { importSPKI, jwtVerify } from "jose";
6488
+
6489
+ // src/server/auth/utils.ts
6490
+ var BASE64_URL_REGEX = /^[A-Za-z0-9-_]+$/;
6491
+ function bufferToHex(bytes) {
6492
+ const view = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
6493
+ let hex = "";
6494
+ for (const byte of view) {
6495
+ hex += byte.toString(16).padStart(2, "0");
6496
+ }
6497
+ return hex;
6498
+ }
6499
+ function normalizeArray(value) {
6500
+ if (value == null) return void 0;
6501
+ if (Array.isArray(value)) {
6502
+ return value.length ? value : void 0;
6503
+ }
6504
+ return [value];
6505
+ }
6506
+ function decodeJwtPayload(token) {
6507
+ if (!token) return void 0;
6508
+ const parts = token.split(".");
6509
+ if (parts.length < 2) return void 0;
6510
+ const payloadSegment = parts[1];
6511
+ if (!payloadSegment) return void 0;
6512
+ if (!BASE64_URL_REGEX.test(payloadSegment)) return void 0;
6513
+ try {
6514
+ const normalized = payloadSegment.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(payloadSegment.length / 4) * 4, "=");
6515
+ const bytes = base64ToUint8Array(normalized);
6516
+ const json = new TextDecoder().decode(bytes);
6517
+ const payload = JSON.parse(json);
6518
+ return {
6519
+ iss: payload.iss ?? null,
6520
+ aud: payload.aud ?? null,
6521
+ azp: payload?.azp ?? null,
6522
+ exp: payload.exp ?? null,
6523
+ iat: payload.iat ?? null
6524
+ };
6525
+ } catch {
6526
+ return void 0;
6527
+ }
6528
+ }
6529
+ function base64ToUint8Array(value) {
6530
+ if (typeof atob === "function") {
6531
+ const binary = atob(value);
6532
+ const out = new Uint8Array(binary.length);
6533
+ for (let i = 0; i < binary.length; i += 1) {
6534
+ out[i] = binary.charCodeAt(i);
6535
+ }
6536
+ return out;
6537
+ }
6538
+ if (typeof Buffer !== "undefined") {
6539
+ return Uint8Array.from(Buffer.from(value, "base64"));
6540
+ }
6541
+ throw new Error("No base64 decoder available in current environment");
6542
+ }
6543
+
6544
+ // src/server/auth/jwtVerifier.ts
6545
+ var DEFAULT_CLOCK_SKEW_MS = 5e3;
6546
+ function matchesAuthorizedParty(azp, patterns) {
6547
+ if (!azp) return true;
6548
+ for (const pattern of patterns) {
6549
+ if (pattern === azp) return true;
6550
+ if (pattern.includes("*")) {
6551
+ const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
6552
+ const regexPattern = escaped.replace(/\*/g, "[^/]*");
6553
+ const regex = new RegExp(`^${regexPattern}$`);
6554
+ if (regex.test(azp)) return true;
6555
+ if (pattern.includes("*.")) {
6556
+ const rootPattern = pattern.replace("*.", "");
6557
+ if (rootPattern === azp) return true;
6558
+ }
6559
+ }
6560
+ }
6561
+ return false;
6562
+ }
6563
+ var keyCache = /* @__PURE__ */ new Map();
6564
+ async function getPublicKey(pem) {
6565
+ const cached = keyCache.get(pem);
6566
+ if (cached) return cached;
6567
+ const normalized = pem.replace(/\\n/g, "\n");
6568
+ const key = await importSPKI(normalized, "ES256");
6569
+ keyCache.set(pem, key);
6570
+ return key;
6571
+ }
6572
+ async function verifyJwt(config, token) {
6573
+ if (!token) {
6574
+ return { result: null };
6575
+ }
6576
+ try {
6577
+ const publicKey = await getPublicKey(config.publicKey);
6578
+ const audience = normalizeArray(config.audience);
6579
+ const clockTolerance = Math.floor(
6580
+ (config.clockSkewMs ?? DEFAULT_CLOCK_SKEW_MS) / 1e3
6581
+ );
6582
+ const { payload } = await jwtVerify(token, publicKey, {
6583
+ issuer: config.issuer,
6584
+ audience: audience && audience.length === 1 ? audience[0] : audience,
6585
+ clockTolerance
6586
+ });
6587
+ const claims = payload;
6588
+ if (!claims.sub) {
6589
+ throw new Error("JWT verification succeeded but subject claim is missing");
6590
+ }
6591
+ const authorizedParties = normalizeArray(config.authorizedParties);
6592
+ if (authorizedParties) {
6593
+ const azp = claims.azp;
6594
+ if (!matchesAuthorizedParty(azp, authorizedParties)) {
6595
+ throw new Error(
6596
+ `Authorized party '${azp}' does not match any allowed pattern`
6597
+ );
6598
+ }
6599
+ }
6600
+ return {
6601
+ result: {
6602
+ userId: claims.sub,
6603
+ claims
6604
+ }
6605
+ };
6606
+ } catch (error) {
6607
+ return {
6608
+ result: null,
6609
+ error,
6610
+ debug: decodeJwtPayload(token)
6611
+ };
6612
+ }
6613
+ }
6614
+
6615
+ // src/server/auth/internalAuth.ts
6616
+ var nodeCryptoPromise = null;
6617
+ async function loadNodeCrypto() {
6618
+ if (!nodeCryptoPromise) {
6619
+ nodeCryptoPromise = import("crypto").then((module) => module).catch(() => null);
6620
+ }
6621
+ return nodeCryptoPromise;
6622
+ }
6623
+ function hasSubtleCrypto() {
6624
+ return typeof globalThis.crypto?.subtle?.importKey === "function";
6625
+ }
6626
+ async function computeHmacHex(secret, payload) {
6627
+ if (hasSubtleCrypto()) {
6628
+ const encoder = new TextEncoder();
6629
+ const key = await crypto.subtle.importKey(
6630
+ "raw",
6631
+ encoder.encode(secret),
6632
+ { name: "HMAC", hash: "SHA-256" },
6633
+ false,
6634
+ ["sign"]
6635
+ );
6636
+ const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(payload));
6637
+ return bufferToHex(signature);
6638
+ }
6639
+ const nodeCrypto = await loadNodeCrypto();
6640
+ if (!nodeCrypto) {
6641
+ throw new Error("Crypto implementation unavailable for HMAC computation");
6642
+ }
6643
+ return nodeCrypto.createHmac("sha256", secret).update(payload).digest("hex");
6644
+ }
6645
+ async function timingSafeEqualHex(a, b) {
6646
+ if (a.length !== b.length) {
6647
+ return false;
6648
+ }
6649
+ const nodeCrypto = await loadNodeCrypto();
6650
+ if (nodeCrypto?.timingSafeEqual) {
6651
+ const aBuf = Buffer.from(a, "hex");
6652
+ const bBuf = Buffer.from(b, "hex");
6653
+ try {
6654
+ return nodeCrypto.timingSafeEqual(aBuf, bBuf);
6655
+ } catch {
6656
+ return false;
6657
+ }
6658
+ }
6659
+ let diff = 0;
6660
+ for (let i = 0; i < a.length; i += 1) {
6661
+ diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
6662
+ }
6663
+ return diff === 0;
6664
+ }
6665
+
6666
+ // src/server/auth/betterAuth.ts
6667
+ import { betterAuth } from "better-auth";
6668
+ import { organization, twoFactor } from "better-auth/plugins";
6669
+ function createDeepSpaceAuth(config) {
6670
+ const socialProviders = {};
6671
+ if (config.google) {
6672
+ socialProviders.google = config.google;
6673
+ }
6674
+ if (config.github) {
6675
+ socialProviders.github = config.github;
6676
+ }
6677
+ return betterAuth({
6678
+ database: config.database,
6679
+ baseURL: config.baseURL,
6680
+ secret: config.secret,
6681
+ emailAndPassword: {
6682
+ enabled: config.emailAndPassword ?? true
6683
+ },
6684
+ socialProviders,
6685
+ trustedOrigins: config.trustedOrigins ?? [
6686
+ "https://deep.space",
6687
+ "https://*.deep.space",
6688
+ "https://*.app.space",
6689
+ "http://localhost:*"
6690
+ ],
6691
+ plugins: [organization(), twoFactor()]
6692
+ });
6693
+ }
6694
+
6082
6695
  // src/shared/subscription.ts
6083
6696
  var ENTITLED_STATUSES = /* @__PURE__ */ new Set([
6084
6697
  "active",
@@ -6243,11 +6856,11 @@ export {
6243
6856
  CronRoom,
6244
6857
  DEFAULT_CONTEXT_CONFIG,
6245
6858
  DEFAULT_DO_MANIFEST,
6246
- DEFAULT_MAX_SKEW_MS,
6247
6859
  DIRECTORY_SCHEMAS,
6248
6860
  GLOBAL_DO_TYPES,
6249
6861
  GLOBAL_DO_TYPE_NAMES,
6250
6862
  GameRoom,
6863
+ JobRoom,
6251
6864
  MESSAGES_SCHEMA,
6252
6865
  PresenceRoom,
6253
6866
  REACTIONS_SCHEMA,
@@ -6274,7 +6887,6 @@ export {
6274
6887
  broadcastChange,
6275
6888
  broadcastYjsUpdate,
6276
6889
  buildCronContext,
6277
- buildInternalPayload,
6278
6890
  buildTableSelect,
6279
6891
  buildUiParts,
6280
6892
  canCreate,
@@ -6298,6 +6910,7 @@ export {
6298
6910
  decodeJwtPayload,
6299
6911
  deleteChatCascade,
6300
6912
  deleteRecord,
6913
+ enqueueJob,
6301
6914
  executeQuery,
6302
6915
  getAllUsers,
6303
6916
  getChat,
@@ -6344,7 +6957,6 @@ export {
6344
6957
  rowToData,
6345
6958
  runMigrations,
6346
6959
  saveYjsDoc,
6347
- signInternalPayload,
6348
6960
  timingSafeEqualHex,
6349
6961
  totalChars,
6350
6962
  truncateOldToolResults,
@@ -6354,7 +6966,6 @@ export {
6354
6966
  validateAppName,
6355
6967
  validateBindingManifest,
6356
6968
  validateDoManifest,
6357
- verifyInternalSignature,
6358
6969
  verifyJwt,
6359
6970
  workspaceAccountsSchema,
6360
6971
  workspaceContentSharesSchema,