deepspace 0.3.11 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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("/");
@@ -201,7 +206,9 @@ function createScopedR2Handler(config) {
201
206
  { status: 400, headers: CORS_HEADERS }
202
207
  );
203
208
  }
204
- const scope = url.searchParams.get("scope") || "self";
209
+ const isKeyedGet = subpath !== "" && request.method === "GET";
210
+ const defaultScope = isKeyedGet ? "app" : "self";
211
+ const scope = url.searchParams.get("scope") || defaultScope;
205
212
  const result = config.resolvePrefix(scope, { userId: auth.userId, url });
206
213
  if (!result.prefix) {
207
214
  const errorMsg = result.error ?? "Invalid scope";
@@ -352,339 +359,11 @@ async function handleDownload(bucket, key) {
352
359
  headers.set("Cache-Control", "public, max-age=31536000, immutable");
353
360
  headers.set("Access-Control-Allow-Origin", "*");
354
361
  if (object.customMetadata?.originalName) {
355
- headers.set("Content-Disposition", `inline; filename="${object.customMetadata.originalName}"`);
362
+ headers.set("Content-Disposition", contentDisposition(object.customMetadata.originalName));
356
363
  }
357
364
  return new Response(object.body, { headers });
358
365
  }
359
366
 
360
- // src/server/auth/jwtVerifier.ts
361
- import { importSPKI, jwtVerify } from "jose";
362
-
363
- // src/server/auth/utils.ts
364
- var BASE64_URL_REGEX = /^[A-Za-z0-9-_]+$/;
365
- function bufferToHex(bytes) {
366
- const view = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
367
- let hex = "";
368
- for (const byte of view) {
369
- hex += byte.toString(16).padStart(2, "0");
370
- }
371
- return hex;
372
- }
373
- function normalizeArray(value) {
374
- if (value == null) return void 0;
375
- if (Array.isArray(value)) {
376
- return value.length ? value : void 0;
377
- }
378
- return [value];
379
- }
380
- function decodeJwtPayload(token) {
381
- if (!token) return void 0;
382
- const parts = token.split(".");
383
- if (parts.length < 2) return void 0;
384
- const payloadSegment = parts[1];
385
- if (!payloadSegment) return void 0;
386
- if (!BASE64_URL_REGEX.test(payloadSegment)) return void 0;
387
- try {
388
- const normalized = payloadSegment.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(payloadSegment.length / 4) * 4, "=");
389
- const bytes = base64ToUint8Array(normalized);
390
- const json = new TextDecoder().decode(bytes);
391
- const payload = JSON.parse(json);
392
- return {
393
- iss: payload.iss ?? null,
394
- aud: payload.aud ?? null,
395
- azp: payload?.azp ?? null,
396
- exp: payload.exp ?? null,
397
- iat: payload.iat ?? null
398
- };
399
- } catch {
400
- return void 0;
401
- }
402
- }
403
- function nowMs() {
404
- return Date.now();
405
- }
406
- function base64ToUint8Array(value) {
407
- if (typeof atob === "function") {
408
- const binary = atob(value);
409
- const out = new Uint8Array(binary.length);
410
- for (let i = 0; i < binary.length; i += 1) {
411
- out[i] = binary.charCodeAt(i);
412
- }
413
- return out;
414
- }
415
- if (typeof Buffer !== "undefined") {
416
- return Uint8Array.from(Buffer.from(value, "base64"));
417
- }
418
- throw new Error("No base64 decoder available in current environment");
419
- }
420
-
421
- // src/server/auth/jwtVerifier.ts
422
- var DEFAULT_CLOCK_SKEW_MS = 5e3;
423
- function matchesAuthorizedParty(azp, patterns) {
424
- if (!azp) return true;
425
- for (const pattern of patterns) {
426
- if (pattern === azp) return true;
427
- if (pattern.includes("*")) {
428
- const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
429
- const regexPattern = escaped.replace(/\*/g, "[^/]*");
430
- const regex = new RegExp(`^${regexPattern}$`);
431
- if (regex.test(azp)) return true;
432
- if (pattern.includes("*.")) {
433
- const rootPattern = pattern.replace("*.", "");
434
- if (rootPattern === azp) return true;
435
- }
436
- }
437
- }
438
- return false;
439
- }
440
- var keyCache = /* @__PURE__ */ new Map();
441
- async function getPublicKey(pem) {
442
- const cached = keyCache.get(pem);
443
- if (cached) return cached;
444
- const normalized = pem.replace(/\\n/g, "\n");
445
- const key = await importSPKI(normalized, "ES256");
446
- keyCache.set(pem, key);
447
- return key;
448
- }
449
- async function verifyJwt(config, token) {
450
- if (!token) {
451
- return { result: null };
452
- }
453
- try {
454
- const publicKey = await getPublicKey(config.publicKey);
455
- const audience = normalizeArray(config.audience);
456
- const clockTolerance = Math.floor(
457
- (config.clockSkewMs ?? DEFAULT_CLOCK_SKEW_MS) / 1e3
458
- );
459
- const { payload } = await jwtVerify(token, publicKey, {
460
- issuer: config.issuer,
461
- audience: audience && audience.length === 1 ? audience[0] : audience,
462
- clockTolerance
463
- });
464
- const claims = payload;
465
- if (!claims.sub) {
466
- throw new Error("JWT verification succeeded but subject claim is missing");
467
- }
468
- const authorizedParties = normalizeArray(config.authorizedParties);
469
- if (authorizedParties) {
470
- const azp = claims.azp;
471
- if (!matchesAuthorizedParty(azp, authorizedParties)) {
472
- throw new Error(
473
- `Authorized party '${azp}' does not match any allowed pattern`
474
- );
475
- }
476
- }
477
- return {
478
- result: {
479
- userId: claims.sub,
480
- claims
481
- }
482
- };
483
- } catch (error) {
484
- return {
485
- result: null,
486
- error,
487
- debug: decodeJwtPayload(token)
488
- };
489
- }
490
- }
491
-
492
- // src/server/auth/internalAuth.ts
493
- var DEFAULT_MAX_SKEW_MS = 5 * 6e4;
494
- var nodeCryptoPromise = null;
495
- async function loadNodeCrypto() {
496
- if (!nodeCryptoPromise) {
497
- nodeCryptoPromise = import("crypto").then((module) => module).catch(() => null);
498
- }
499
- return nodeCryptoPromise;
500
- }
501
- function hasSubtleCrypto() {
502
- return typeof globalThis.crypto?.subtle?.importKey === "function";
503
- }
504
- async function computeHmacHex(secret, payload) {
505
- if (hasSubtleCrypto()) {
506
- const encoder = new TextEncoder();
507
- const key = await crypto.subtle.importKey(
508
- "raw",
509
- encoder.encode(secret),
510
- { name: "HMAC", hash: "SHA-256" },
511
- false,
512
- ["sign"]
513
- );
514
- const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(payload));
515
- return bufferToHex(signature);
516
- }
517
- const nodeCrypto = await loadNodeCrypto();
518
- if (!nodeCrypto) {
519
- throw new Error("Crypto implementation unavailable for HMAC computation");
520
- }
521
- return nodeCrypto.createHmac("sha256", secret).update(payload).digest("hex");
522
- }
523
- async function timingSafeEqualHex(a, b) {
524
- if (a.length !== b.length) {
525
- return false;
526
- }
527
- const nodeCrypto = await loadNodeCrypto();
528
- if (nodeCrypto?.timingSafeEqual) {
529
- const aBuf = Buffer.from(a, "hex");
530
- const bBuf = Buffer.from(b, "hex");
531
- try {
532
- return nodeCrypto.timingSafeEqual(aBuf, bBuf);
533
- } catch {
534
- return false;
535
- }
536
- }
537
- let diff = 0;
538
- for (let i = 0; i < a.length; i += 1) {
539
- diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
540
- }
541
- return diff === 0;
542
- }
543
- async function signInternalPayload({
544
- secret,
545
- payload,
546
- timestamp
547
- }) {
548
- const ts = timestamp ?? nowMs().toString();
549
- const signature = await computeHmacHex(secret, `${ts}.${payload}`);
550
- return { timestamp: ts, signature };
551
- }
552
- async function verifyInternalSignature({
553
- secret,
554
- timestamp,
555
- signature,
556
- payload,
557
- maxSkewMs = DEFAULT_MAX_SKEW_MS
558
- }) {
559
- if (!secret || !timestamp || !signature) {
560
- return false;
561
- }
562
- const tsNumber = Number(timestamp);
563
- if (!Number.isFinite(tsNumber)) {
564
- return false;
565
- }
566
- const skew = Math.abs(nowMs() - tsNumber);
567
- if (skew > maxSkewMs) {
568
- return false;
569
- }
570
- const expected = await computeHmacHex(secret, `${timestamp}.${payload}`);
571
- return timingSafeEqualHex(signature.toLowerCase(), expected.toLowerCase());
572
- }
573
- function buildInternalPayload(body) {
574
- if (body === void 0) return "{}";
575
- if (typeof body === "string") return body;
576
- try {
577
- return JSON.stringify(body);
578
- } catch {
579
- return "{}";
580
- }
581
- }
582
-
583
- // src/server/auth/betterAuth.ts
584
- import { betterAuth } from "better-auth";
585
- import { organization, twoFactor } from "better-auth/plugins";
586
- function createDeepSpaceAuth(config) {
587
- const socialProviders = {};
588
- if (config.google) {
589
- socialProviders.google = config.google;
590
- }
591
- if (config.github) {
592
- socialProviders.github = config.github;
593
- }
594
- return betterAuth({
595
- database: config.database,
596
- baseURL: config.baseURL,
597
- secret: config.secret,
598
- emailAndPassword: {
599
- enabled: config.emailAndPassword ?? true
600
- },
601
- socialProviders,
602
- trustedOrigins: config.trustedOrigins ?? [
603
- "https://deep.space",
604
- "https://*.deep.space",
605
- "https://*.app.space",
606
- "http://localhost:*"
607
- ],
608
- plugins: [organization(), twoFactor()]
609
- });
610
- }
611
-
612
- // src/server/utils/cron.ts
613
- var DEFAULT_API_BASE_URL = "https://api-worker.deep.space";
614
- function buildCronContext(env, ownerUserId, roomId = "default") {
615
- const hmacSecret = env.INTERNAL_STORAGE_HMAC_SECRET;
616
- const apiBase = env.API_BASE_URL || DEFAULT_API_BASE_URL;
617
- const roomIdObj = env.RECORD_ROOMS.idFromName(roomId);
618
- const room = env.RECORD_ROOMS.get(roomIdObj);
619
- async function executeTool3(tool, params) {
620
- const response = await room.fetch(new Request("https://internal/api/tools/execute", {
621
- method: "POST",
622
- headers: {
623
- "Content-Type": "application/json",
624
- "X-User-Id": ownerUserId,
625
- "X-App-Action": "true"
626
- },
627
- body: JSON.stringify({ tool, params })
628
- }));
629
- const result = await response.json();
630
- if (!result.success) {
631
- throw new Error(`RecordRoom tool ${tool} failed: ${result.error || "Unknown error"}`);
632
- }
633
- return result.data;
634
- }
635
- const records = {
636
- async query(collection, opts) {
637
- const params = { collection };
638
- if (opts?.where) params.where = opts.where;
639
- if (opts?.limit) params.limit = opts.limit;
640
- const data = await executeTool3("records.query", params);
641
- return data.records || [];
642
- },
643
- async create(collection, data) {
644
- return executeTool3("records.create", { collection, data });
645
- },
646
- async update(collection, recordId, data) {
647
- return executeTool3("records.update", { collection, recordId, data });
648
- },
649
- async delete(collection, recordId) {
650
- return executeTool3("records.delete", { collection, recordId });
651
- }
652
- };
653
- const integrations = {
654
- async call(endpoint, params) {
655
- if (!hmacSecret) {
656
- throw new Error("INTERNAL_STORAGE_HMAC_SECRET not configured \u2014 cannot call integrations");
657
- }
658
- const payload = { userId: ownerUserId, ...params };
659
- const payloadStr = buildInternalPayload(payload);
660
- const { timestamp, signature } = await signInternalPayload({
661
- secret: hmacSecret,
662
- payload: payloadStr
663
- });
664
- const response = await fetch(`${apiBase}/api/internal/integrations/${endpoint}`, {
665
- method: "POST",
666
- headers: {
667
- "Content-Type": "application/json",
668
- "x-internal-timestamp": timestamp,
669
- "x-internal-signature": signature
670
- },
671
- body: payloadStr
672
- });
673
- if (!response.ok) {
674
- const errText = await response.text();
675
- throw new Error(`Integration call ${endpoint} failed (${response.status}): ${errText}`);
676
- }
677
- return response.json();
678
- }
679
- };
680
- return { records, integrations, ownerUserId };
681
- }
682
-
683
- // src/server/utils/ai.ts
684
- import { createAnthropic } from "@ai-sdk/anthropic";
685
- import { createOpenAI } from "@ai-sdk/openai";
686
- import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
687
-
688
367
  // src/server/utils/proxies.ts
689
368
  function resolveApiTransport(env) {
690
369
  if (env.API_WORKER) return { kind: "binding", fetcher: env.API_WORKER };
@@ -763,7 +442,79 @@ function authWorkerFetch(env, path, init) {
763
442
  return fetch(`${baseUrl}${pathAndSearch}`, init);
764
443
  }
765
444
 
445
+ // src/server/utils/cron.ts
446
+ function buildCronContext(env, ownerUserId, roomId = "default") {
447
+ const roomIdObj = env.RECORD_ROOMS.idFromName(roomId);
448
+ const room = env.RECORD_ROOMS.get(roomIdObj);
449
+ async function executeTool3(tool, params) {
450
+ const response = await room.fetch(new Request("https://internal/api/tools/execute", {
451
+ method: "POST",
452
+ headers: {
453
+ "Content-Type": "application/json",
454
+ "X-User-Id": ownerUserId,
455
+ "X-App-Action": "true"
456
+ },
457
+ body: JSON.stringify({ tool, params })
458
+ }));
459
+ const result = await response.json();
460
+ if (!result.success) {
461
+ throw new Error(`RecordRoom tool ${tool} failed: ${result.error || "Unknown error"}`);
462
+ }
463
+ return result.data;
464
+ }
465
+ const records = {
466
+ async query(collection, opts) {
467
+ const params = { collection };
468
+ if (opts?.where) params.where = opts.where;
469
+ if (opts?.limit) params.limit = opts.limit;
470
+ const data = await executeTool3("records.query", params);
471
+ return data.records || [];
472
+ },
473
+ async create(collection, data) {
474
+ return executeTool3("records.create", { collection, data });
475
+ },
476
+ async update(collection, recordId, data) {
477
+ return executeTool3("records.update", { collection, recordId, data });
478
+ },
479
+ async delete(collection, recordId) {
480
+ return executeTool3("records.delete", { collection, recordId });
481
+ }
482
+ };
483
+ const integrations = {
484
+ async call(endpoint, params = {}) {
485
+ if (!env.APP_OWNER_JWT) {
486
+ throw new Error(
487
+ "integrations.call requires env.APP_OWNER_JWT. Deployed apps receive this automatically; in dev, `deepspace dev` writes it into .dev.vars."
488
+ );
489
+ }
490
+ const response = await apiWorkerFetch(env, `/api/integrations/${endpoint}`, {
491
+ method: "POST",
492
+ headers: {
493
+ "Content-Type": "application/json",
494
+ Authorization: `Bearer ${env.APP_OWNER_JWT}`
495
+ },
496
+ body: JSON.stringify(params)
497
+ });
498
+ const text = await response.text();
499
+ let result = {};
500
+ try {
501
+ result = text ? JSON.parse(text) : {};
502
+ } catch {
503
+ }
504
+ if (!response.ok || !result.success) {
505
+ const detail = result.message || result.error || text || `HTTP ${response.status}`;
506
+ throw new Error(`Integration call ${endpoint} failed: ${detail}`);
507
+ }
508
+ return result.data;
509
+ }
510
+ };
511
+ return { records, integrations, ownerUserId };
512
+ }
513
+
766
514
  // src/server/utils/ai.ts
515
+ import { createAnthropic } from "@ai-sdk/anthropic";
516
+ import { createOpenAI } from "@ai-sdk/openai";
517
+ import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
767
518
  var ANTHROPIC_DEFAULT_MAX_OUTPUT_TOKENS = 4096;
768
519
  function withDefaultAnthropicMaxTokens(body) {
769
520
  if (typeof body !== "string") return body;
@@ -1566,6 +1317,13 @@ var MSG = {
1566
1317
  USER_LIST: "user.list",
1567
1318
  SET_ROLE: "user.set_role",
1568
1319
  USER_UPDATE: "user.update",
1320
+ // ---- Auth signal -----------------------------------------------------
1321
+ // Server → client one-shot on connect from rooms that enforce role-based
1322
+ // writes (Canvas, Game, Cron). Tells the client whether this connection
1323
+ // can write so its hook can disable local write APIs and the UI can
1324
+ // render controls accordingly. Without this, a viewer's click would
1325
+ // round-trip to the server and silently come back as ERROR.
1326
+ AUTH: "auth",
1569
1327
  // ---- Yjs JSON control frames (binary sync lives in ./yjs.ts) ---------
1570
1328
  YJS_JOIN: "yjs.join",
1571
1329
  YJS_LEAVE: "yjs.leave",
@@ -4201,7 +3959,20 @@ var YjsRoom = class extends BaseRoom {
4201
3959
  }
4202
3960
  };
4203
3961
 
3962
+ // src/shared/roles.ts
3963
+ var ROLES = {
3964
+ VIEWER: "viewer",
3965
+ MEMBER: "member",
3966
+ ADMIN: "admin"
3967
+ };
3968
+
4204
3969
  // src/server/rooms/game-room.ts
3970
+ var GAME_WRITE_TYPES = /* @__PURE__ */ new Set([
3971
+ MSG.GAME_INPUT,
3972
+ MSG.GAME_PLAYER_READY,
3973
+ MSG.GAME_START,
3974
+ MSG.GAME_END
3975
+ ]);
4205
3976
  var GameRoom = class extends BaseRoom {
4206
3977
  config;
4207
3978
  players = /* @__PURE__ */ new Map();
@@ -4257,10 +4028,26 @@ var GameRoom = class extends BaseRoom {
4257
4028
  // ==========================================================================
4258
4029
  onConnect(ws, user) {
4259
4030
  this.ensureInitialized();
4031
+ const role = user.role ?? ROLES.VIEWER;
4032
+ const canWrite = role === ROLES.MEMBER || role === ROLES.ADMIN;
4260
4033
  const attachment = {
4261
4034
  ...user,
4262
- joinedAt: (/* @__PURE__ */ new Date()).toISOString()
4035
+ joinedAt: (/* @__PURE__ */ new Date()).toISOString(),
4036
+ canWrite
4263
4037
  };
4038
+ this.sendTo(ws, { type: MSG.AUTH, payload: { canWrite } });
4039
+ if (!canWrite) {
4040
+ this.sendTo(ws, {
4041
+ type: MSG.GAME_STATE,
4042
+ payload: {
4043
+ state: this.gameState,
4044
+ tick: this.currentTick,
4045
+ players: Array.from(this.players.values()),
4046
+ running: this.running
4047
+ }
4048
+ });
4049
+ return attachment;
4050
+ }
4264
4051
  const player = {
4265
4052
  userId: user.userId,
4266
4053
  userName: user.userName,
@@ -4289,6 +4076,13 @@ var GameRoom = class extends BaseRoom {
4289
4076
  async onMessage(ws, user, message) {
4290
4077
  this.ensureInitialized();
4291
4078
  const { type, payload } = message;
4079
+ if (GAME_WRITE_TYPES.has(type) && !user.canWrite) {
4080
+ this.sendTo(ws, {
4081
+ type: MSG.ERROR,
4082
+ payload: { error: "Write access denied: spectators cannot drive the game" }
4083
+ });
4084
+ return;
4085
+ }
4292
4086
  switch (type) {
4293
4087
  case MSG.GAME_INPUT: {
4294
4088
  this.inputBuffer.push({
@@ -4437,6 +4231,15 @@ var GameRoom = class extends BaseRoom {
4437
4231
 
4438
4232
  // src/server/rooms/canvas-room.ts
4439
4233
  import * as Y5 from "yjs";
4234
+ var CANVAS_WRITE_TYPES = /* @__PURE__ */ new Set([
4235
+ MSG.CANVAS_ADD,
4236
+ MSG.CANVAS_MOVE,
4237
+ MSG.CANVAS_RESIZE,
4238
+ MSG.CANVAS_DELETE,
4239
+ MSG.CANVAS_UPDATE,
4240
+ MSG.CANVAS_UNDO,
4241
+ MSG.CANVAS_REDO
4242
+ ]);
4440
4243
  var CanvasRoom = class extends BaseRoom {
4441
4244
  doc = null;
4442
4245
  initialized = false;
@@ -4496,10 +4299,14 @@ var CanvasRoom = class extends BaseRoom {
4496
4299
  }
4497
4300
  onConnect(ws, user) {
4498
4301
  this.ensureInitialized();
4302
+ const role = user.role ?? ROLES.VIEWER;
4303
+ const canWrite = role === ROLES.MEMBER || role === ROLES.ADMIN;
4499
4304
  const attachment = {
4500
4305
  ...user,
4501
- viewport: null
4306
+ viewport: null,
4307
+ canWrite
4502
4308
  };
4309
+ this.sendTo(ws, { type: MSG.AUTH, payload: { canWrite } });
4503
4310
  const shapes = this.getAllShapes();
4504
4311
  this.sendTo(ws, {
4505
4312
  type: MSG.CANVAS_SHAPES,
@@ -4513,6 +4320,13 @@ var CanvasRoom = class extends BaseRoom {
4513
4320
  async onMessage(ws, user, message) {
4514
4321
  this.ensureInitialized();
4515
4322
  const { type, payload } = message;
4323
+ if (CANVAS_WRITE_TYPES.has(type) && !user.canWrite) {
4324
+ this.sendTo(ws, {
4325
+ type: MSG.ERROR,
4326
+ payload: { error: "Write access denied: viewer role cannot modify the canvas" }
4327
+ });
4328
+ return;
4329
+ }
4516
4330
  switch (type) {
4517
4331
  case MSG.CANVAS_ADD: {
4518
4332
  const shape = payload;
@@ -4807,6 +4621,11 @@ var PresenceRoom = class extends BaseRoom {
4807
4621
  };
4808
4622
 
4809
4623
  // src/server/rooms/cron-room.ts
4624
+ var CRON_WRITE_TYPES = /* @__PURE__ */ new Set([
4625
+ MSG.CRON_TRIGGER,
4626
+ MSG.CRON_PAUSE,
4627
+ MSG.CRON_RESUME
4628
+ ]);
4810
4629
  var CronRoom = class extends BaseRoom {
4811
4630
  tasks;
4812
4631
  initialized = false;
@@ -4885,6 +4704,9 @@ var CronRoom = class extends BaseRoom {
4885
4704
  }
4886
4705
  onConnect(ws, user) {
4887
4706
  this.ensureInitialized();
4707
+ const role = user.role ?? ROLES.VIEWER;
4708
+ const canWrite = role === ROLES.MEMBER || role === ROLES.ADMIN;
4709
+ this.sendTo(ws, { type: MSG.AUTH, payload: { canWrite } });
4888
4710
  this.sendTo(ws, {
4889
4711
  type: MSG.CRON_TASKS,
4890
4712
  payload: { tasks: this.getTaskStates() }
@@ -4893,11 +4715,18 @@ var CronRoom = class extends BaseRoom {
4893
4715
  type: MSG.CRON_HISTORY,
4894
4716
  payload: { history: this.getRecentHistory(50) }
4895
4717
  });
4896
- return user;
4718
+ return { ...user, canWrite };
4897
4719
  }
4898
4720
  async onMessage(ws, user, message) {
4899
4721
  this.ensureInitialized();
4900
4722
  const { type, payload } = message;
4723
+ if (CRON_WRITE_TYPES.has(type) && !user.canWrite) {
4724
+ this.sendTo(ws, {
4725
+ type: MSG.ERROR,
4726
+ payload: { error: "Write access denied: viewer role cannot modify cron tasks" }
4727
+ });
4728
+ return;
4729
+ }
4901
4730
  switch (type) {
4902
4731
  case MSG.CRON_TRIGGER: {
4903
4732
  const taskName = payload.taskName;
@@ -5825,7 +5654,6 @@ var RESERVED_BINDING_NAMES = /* @__PURE__ */ new Set([
5825
5654
  "AUTH_WORKER_URL",
5826
5655
  "APP_IDENTITY_TOKEN",
5827
5656
  "APP_OWNER_JWT",
5828
- "INTERNAL_STORAGE_HMAC_SECRET",
5829
5657
  "USAGE_EVENTS"
5830
5658
  ]);
5831
5659
  function validateBindingManifest(manifest) {
@@ -6657,6 +6485,215 @@ var RESERVED_COLLECTION_NAMES = new Set(
6657
6485
  GLOBAL_DO_TYPES.flatMap((t) => t.schemas.map((s) => s.name))
6658
6486
  );
6659
6487
 
6488
+ // src/server/auth/jwtVerifier.ts
6489
+ import { importSPKI, jwtVerify } from "jose";
6490
+
6491
+ // src/server/auth/utils.ts
6492
+ var BASE64_URL_REGEX = /^[A-Za-z0-9-_]+$/;
6493
+ function bufferToHex(bytes) {
6494
+ const view = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
6495
+ let hex = "";
6496
+ for (const byte of view) {
6497
+ hex += byte.toString(16).padStart(2, "0");
6498
+ }
6499
+ return hex;
6500
+ }
6501
+ function normalizeArray(value) {
6502
+ if (value == null) return void 0;
6503
+ if (Array.isArray(value)) {
6504
+ return value.length ? value : void 0;
6505
+ }
6506
+ return [value];
6507
+ }
6508
+ function decodeJwtPayload(token) {
6509
+ if (!token) return void 0;
6510
+ const parts = token.split(".");
6511
+ if (parts.length < 2) return void 0;
6512
+ const payloadSegment = parts[1];
6513
+ if (!payloadSegment) return void 0;
6514
+ if (!BASE64_URL_REGEX.test(payloadSegment)) return void 0;
6515
+ try {
6516
+ const normalized = payloadSegment.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(payloadSegment.length / 4) * 4, "=");
6517
+ const bytes = base64ToUint8Array(normalized);
6518
+ const json = new TextDecoder().decode(bytes);
6519
+ const payload = JSON.parse(json);
6520
+ return {
6521
+ iss: payload.iss ?? null,
6522
+ aud: payload.aud ?? null,
6523
+ azp: payload?.azp ?? null,
6524
+ exp: payload.exp ?? null,
6525
+ iat: payload.iat ?? null
6526
+ };
6527
+ } catch {
6528
+ return void 0;
6529
+ }
6530
+ }
6531
+ function base64ToUint8Array(value) {
6532
+ if (typeof atob === "function") {
6533
+ const binary = atob(value);
6534
+ const out = new Uint8Array(binary.length);
6535
+ for (let i = 0; i < binary.length; i += 1) {
6536
+ out[i] = binary.charCodeAt(i);
6537
+ }
6538
+ return out;
6539
+ }
6540
+ if (typeof Buffer !== "undefined") {
6541
+ return Uint8Array.from(Buffer.from(value, "base64"));
6542
+ }
6543
+ throw new Error("No base64 decoder available in current environment");
6544
+ }
6545
+
6546
+ // src/server/auth/jwtVerifier.ts
6547
+ var DEFAULT_CLOCK_SKEW_MS = 5e3;
6548
+ function matchesAuthorizedParty(azp, patterns) {
6549
+ if (!azp) return true;
6550
+ for (const pattern of patterns) {
6551
+ if (pattern === azp) return true;
6552
+ if (pattern.includes("*")) {
6553
+ const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
6554
+ const regexPattern = escaped.replace(/\*/g, "[^/]*");
6555
+ const regex = new RegExp(`^${regexPattern}$`);
6556
+ if (regex.test(azp)) return true;
6557
+ if (pattern.includes("*.")) {
6558
+ const rootPattern = pattern.replace("*.", "");
6559
+ if (rootPattern === azp) return true;
6560
+ }
6561
+ }
6562
+ }
6563
+ return false;
6564
+ }
6565
+ var keyCache = /* @__PURE__ */ new Map();
6566
+ async function getPublicKey(pem) {
6567
+ const cached = keyCache.get(pem);
6568
+ if (cached) return cached;
6569
+ const normalized = pem.replace(/\\n/g, "\n");
6570
+ const key = await importSPKI(normalized, "ES256");
6571
+ keyCache.set(pem, key);
6572
+ return key;
6573
+ }
6574
+ async function verifyJwt(config, token) {
6575
+ if (!token) {
6576
+ return { result: null };
6577
+ }
6578
+ try {
6579
+ const publicKey = await getPublicKey(config.publicKey);
6580
+ const audience = normalizeArray(config.audience);
6581
+ const clockTolerance = Math.floor(
6582
+ (config.clockSkewMs ?? DEFAULT_CLOCK_SKEW_MS) / 1e3
6583
+ );
6584
+ const { payload } = await jwtVerify(token, publicKey, {
6585
+ issuer: config.issuer,
6586
+ audience: audience && audience.length === 1 ? audience[0] : audience,
6587
+ clockTolerance
6588
+ });
6589
+ const claims = payload;
6590
+ if (!claims.sub) {
6591
+ throw new Error("JWT verification succeeded but subject claim is missing");
6592
+ }
6593
+ const authorizedParties = normalizeArray(config.authorizedParties);
6594
+ if (authorizedParties) {
6595
+ const azp = claims.azp;
6596
+ if (!matchesAuthorizedParty(azp, authorizedParties)) {
6597
+ throw new Error(
6598
+ `Authorized party '${azp}' does not match any allowed pattern`
6599
+ );
6600
+ }
6601
+ }
6602
+ return {
6603
+ result: {
6604
+ userId: claims.sub,
6605
+ claims
6606
+ }
6607
+ };
6608
+ } catch (error) {
6609
+ return {
6610
+ result: null,
6611
+ error,
6612
+ debug: decodeJwtPayload(token)
6613
+ };
6614
+ }
6615
+ }
6616
+
6617
+ // src/server/auth/internalAuth.ts
6618
+ var nodeCryptoPromise = null;
6619
+ async function loadNodeCrypto() {
6620
+ if (!nodeCryptoPromise) {
6621
+ nodeCryptoPromise = import("crypto").then((module) => module).catch(() => null);
6622
+ }
6623
+ return nodeCryptoPromise;
6624
+ }
6625
+ function hasSubtleCrypto() {
6626
+ return typeof globalThis.crypto?.subtle?.importKey === "function";
6627
+ }
6628
+ async function computeHmacHex(secret, payload) {
6629
+ if (hasSubtleCrypto()) {
6630
+ const encoder = new TextEncoder();
6631
+ const key = await crypto.subtle.importKey(
6632
+ "raw",
6633
+ encoder.encode(secret),
6634
+ { name: "HMAC", hash: "SHA-256" },
6635
+ false,
6636
+ ["sign"]
6637
+ );
6638
+ const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(payload));
6639
+ return bufferToHex(signature);
6640
+ }
6641
+ const nodeCrypto = await loadNodeCrypto();
6642
+ if (!nodeCrypto) {
6643
+ throw new Error("Crypto implementation unavailable for HMAC computation");
6644
+ }
6645
+ return nodeCrypto.createHmac("sha256", secret).update(payload).digest("hex");
6646
+ }
6647
+ async function timingSafeEqualHex(a, b) {
6648
+ if (a.length !== b.length) {
6649
+ return false;
6650
+ }
6651
+ const nodeCrypto = await loadNodeCrypto();
6652
+ if (nodeCrypto?.timingSafeEqual) {
6653
+ const aBuf = Buffer.from(a, "hex");
6654
+ const bBuf = Buffer.from(b, "hex");
6655
+ try {
6656
+ return nodeCrypto.timingSafeEqual(aBuf, bBuf);
6657
+ } catch {
6658
+ return false;
6659
+ }
6660
+ }
6661
+ let diff = 0;
6662
+ for (let i = 0; i < a.length; i += 1) {
6663
+ diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
6664
+ }
6665
+ return diff === 0;
6666
+ }
6667
+
6668
+ // src/server/auth/betterAuth.ts
6669
+ import { betterAuth } from "better-auth";
6670
+ import { organization, twoFactor } from "better-auth/plugins";
6671
+ function createDeepSpaceAuth(config) {
6672
+ const socialProviders = {};
6673
+ if (config.google) {
6674
+ socialProviders.google = config.google;
6675
+ }
6676
+ if (config.github) {
6677
+ socialProviders.github = config.github;
6678
+ }
6679
+ return betterAuth({
6680
+ database: config.database,
6681
+ baseURL: config.baseURL,
6682
+ secret: config.secret,
6683
+ emailAndPassword: {
6684
+ enabled: config.emailAndPassword ?? true
6685
+ },
6686
+ socialProviders,
6687
+ trustedOrigins: config.trustedOrigins ?? [
6688
+ "https://deep.space",
6689
+ "https://*.deep.space",
6690
+ "https://*.app.space",
6691
+ "http://localhost:*"
6692
+ ],
6693
+ plugins: [organization(), twoFactor()]
6694
+ });
6695
+ }
6696
+
6660
6697
  // src/shared/subscription.ts
6661
6698
  var ENTITLED_STATUSES = /* @__PURE__ */ new Set([
6662
6699
  "active",
@@ -6821,7 +6858,6 @@ export {
6821
6858
  CronRoom,
6822
6859
  DEFAULT_CONTEXT_CONFIG,
6823
6860
  DEFAULT_DO_MANIFEST,
6824
- DEFAULT_MAX_SKEW_MS,
6825
6861
  DIRECTORY_SCHEMAS,
6826
6862
  GLOBAL_DO_TYPES,
6827
6863
  GLOBAL_DO_TYPE_NAMES,
@@ -6853,7 +6889,6 @@ export {
6853
6889
  broadcastChange,
6854
6890
  broadcastYjsUpdate,
6855
6891
  buildCronContext,
6856
- buildInternalPayload,
6857
6892
  buildTableSelect,
6858
6893
  buildUiParts,
6859
6894
  canCreate,
@@ -6924,7 +6959,6 @@ export {
6924
6959
  rowToData,
6925
6960
  runMigrations,
6926
6961
  saveYjsDoc,
6927
- signInternalPayload,
6928
6962
  timingSafeEqualHex,
6929
6963
  totalChars,
6930
6964
  truncateOldToolResults,
@@ -6934,7 +6968,6 @@ export {
6934
6968
  validateAppName,
6935
6969
  validateBindingManifest,
6936
6970
  validateDoManifest,
6937
- verifyInternalSignature,
6938
6971
  verifyJwt,
6939
6972
  workspaceAccountsSchema,
6940
6973
  workspaceContentSharesSchema,