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/worker.js CHANGED
@@ -206,6 +206,13 @@ var MSG = {
206
206
  USER_LIST: "user.list",
207
207
  SET_ROLE: "user.set_role",
208
208
  USER_UPDATE: "user.update",
209
+ // ---- Auth signal -----------------------------------------------------
210
+ // Server → client one-shot on connect from rooms that enforce role-based
211
+ // writes (Canvas, Game, Cron). Tells the client whether this connection
212
+ // can write so its hook can disable local write APIs and the UI can
213
+ // render controls accordingly. Without this, a viewer's click would
214
+ // round-trip to the server and silently come back as ERROR.
215
+ AUTH: "auth",
209
216
  // ---- Yjs JSON control frames (binary sync lives in ./yjs.ts) ---------
210
217
  YJS_JOIN: "yjs.join",
211
218
  YJS_LEAVE: "yjs.leave",
@@ -3294,7 +3301,20 @@ var YjsRoom = class extends BaseRoom {
3294
3301
  }
3295
3302
  };
3296
3303
 
3304
+ // src/shared/roles.ts
3305
+ var ROLES = {
3306
+ VIEWER: "viewer",
3307
+ MEMBER: "member",
3308
+ ADMIN: "admin"
3309
+ };
3310
+
3297
3311
  // src/server/rooms/game-room.ts
3312
+ var GAME_WRITE_TYPES = /* @__PURE__ */ new Set([
3313
+ MSG.GAME_INPUT,
3314
+ MSG.GAME_PLAYER_READY,
3315
+ MSG.GAME_START,
3316
+ MSG.GAME_END
3317
+ ]);
3298
3318
  var GameRoom = class extends BaseRoom {
3299
3319
  config;
3300
3320
  players = /* @__PURE__ */ new Map();
@@ -3350,10 +3370,26 @@ var GameRoom = class extends BaseRoom {
3350
3370
  // ==========================================================================
3351
3371
  onConnect(ws, user) {
3352
3372
  this.ensureInitialized();
3373
+ const role = user.role ?? ROLES.VIEWER;
3374
+ const canWrite = role === ROLES.MEMBER || role === ROLES.ADMIN;
3353
3375
  const attachment = {
3354
3376
  ...user,
3355
- joinedAt: (/* @__PURE__ */ new Date()).toISOString()
3377
+ joinedAt: (/* @__PURE__ */ new Date()).toISOString(),
3378
+ canWrite
3356
3379
  };
3380
+ this.sendTo(ws, { type: MSG.AUTH, payload: { canWrite } });
3381
+ if (!canWrite) {
3382
+ this.sendTo(ws, {
3383
+ type: MSG.GAME_STATE,
3384
+ payload: {
3385
+ state: this.gameState,
3386
+ tick: this.currentTick,
3387
+ players: Array.from(this.players.values()),
3388
+ running: this.running
3389
+ }
3390
+ });
3391
+ return attachment;
3392
+ }
3357
3393
  const player = {
3358
3394
  userId: user.userId,
3359
3395
  userName: user.userName,
@@ -3382,6 +3418,13 @@ var GameRoom = class extends BaseRoom {
3382
3418
  async onMessage(ws, user, message) {
3383
3419
  this.ensureInitialized();
3384
3420
  const { type, payload } = message;
3421
+ if (GAME_WRITE_TYPES.has(type) && !user.canWrite) {
3422
+ this.sendTo(ws, {
3423
+ type: MSG.ERROR,
3424
+ payload: { error: "Write access denied: spectators cannot drive the game" }
3425
+ });
3426
+ return;
3427
+ }
3385
3428
  switch (type) {
3386
3429
  case MSG.GAME_INPUT: {
3387
3430
  this.inputBuffer.push({
@@ -3530,6 +3573,15 @@ var GameRoom = class extends BaseRoom {
3530
3573
 
3531
3574
  // src/server/rooms/canvas-room.ts
3532
3575
  import * as Y5 from "yjs";
3576
+ var CANVAS_WRITE_TYPES = /* @__PURE__ */ new Set([
3577
+ MSG.CANVAS_ADD,
3578
+ MSG.CANVAS_MOVE,
3579
+ MSG.CANVAS_RESIZE,
3580
+ MSG.CANVAS_DELETE,
3581
+ MSG.CANVAS_UPDATE,
3582
+ MSG.CANVAS_UNDO,
3583
+ MSG.CANVAS_REDO
3584
+ ]);
3533
3585
  var CanvasRoom = class extends BaseRoom {
3534
3586
  doc = null;
3535
3587
  initialized = false;
@@ -3589,10 +3641,14 @@ var CanvasRoom = class extends BaseRoom {
3589
3641
  }
3590
3642
  onConnect(ws, user) {
3591
3643
  this.ensureInitialized();
3644
+ const role = user.role ?? ROLES.VIEWER;
3645
+ const canWrite = role === ROLES.MEMBER || role === ROLES.ADMIN;
3592
3646
  const attachment = {
3593
3647
  ...user,
3594
- viewport: null
3648
+ viewport: null,
3649
+ canWrite
3595
3650
  };
3651
+ this.sendTo(ws, { type: MSG.AUTH, payload: { canWrite } });
3596
3652
  const shapes = this.getAllShapes();
3597
3653
  this.sendTo(ws, {
3598
3654
  type: MSG.CANVAS_SHAPES,
@@ -3606,6 +3662,13 @@ var CanvasRoom = class extends BaseRoom {
3606
3662
  async onMessage(ws, user, message) {
3607
3663
  this.ensureInitialized();
3608
3664
  const { type, payload } = message;
3665
+ if (CANVAS_WRITE_TYPES.has(type) && !user.canWrite) {
3666
+ this.sendTo(ws, {
3667
+ type: MSG.ERROR,
3668
+ payload: { error: "Write access denied: viewer role cannot modify the canvas" }
3669
+ });
3670
+ return;
3671
+ }
3609
3672
  switch (type) {
3610
3673
  case MSG.CANVAS_ADD: {
3611
3674
  const shape = payload;
@@ -3900,6 +3963,11 @@ var PresenceRoom = class extends BaseRoom {
3900
3963
  };
3901
3964
 
3902
3965
  // src/server/rooms/cron-room.ts
3966
+ var CRON_WRITE_TYPES = /* @__PURE__ */ new Set([
3967
+ MSG.CRON_TRIGGER,
3968
+ MSG.CRON_PAUSE,
3969
+ MSG.CRON_RESUME
3970
+ ]);
3903
3971
  var CronRoom = class extends BaseRoom {
3904
3972
  tasks;
3905
3973
  initialized = false;
@@ -3978,6 +4046,9 @@ var CronRoom = class extends BaseRoom {
3978
4046
  }
3979
4047
  onConnect(ws, user) {
3980
4048
  this.ensureInitialized();
4049
+ const role = user.role ?? ROLES.VIEWER;
4050
+ const canWrite = role === ROLES.MEMBER || role === ROLES.ADMIN;
4051
+ this.sendTo(ws, { type: MSG.AUTH, payload: { canWrite } });
3981
4052
  this.sendTo(ws, {
3982
4053
  type: MSG.CRON_TASKS,
3983
4054
  payload: { tasks: this.getTaskStates() }
@@ -3986,11 +4057,18 @@ var CronRoom = class extends BaseRoom {
3986
4057
  type: MSG.CRON_HISTORY,
3987
4058
  payload: { history: this.getRecentHistory(50) }
3988
4059
  });
3989
- return user;
4060
+ return { ...user, canWrite };
3990
4061
  }
3991
4062
  async onMessage(ws, user, message) {
3992
4063
  this.ensureInitialized();
3993
4064
  const { type, payload } = message;
4065
+ if (CRON_WRITE_TYPES.has(type) && !user.canWrite) {
4066
+ this.sendTo(ws, {
4067
+ type: MSG.ERROR,
4068
+ payload: { error: "Write access denied: viewer role cannot modify cron tasks" }
4069
+ });
4070
+ return;
4071
+ }
3994
4072
  switch (type) {
3995
4073
  case MSG.CRON_TRIGGER: {
3996
4074
  const taskName = payload.taskName;
@@ -4918,7 +4996,6 @@ var RESERVED_BINDING_NAMES = /* @__PURE__ */ new Set([
4918
4996
  "AUTH_WORKER_URL",
4919
4997
  "APP_IDENTITY_TOKEN",
4920
4998
  "APP_OWNER_JWT",
4921
- "INTERNAL_STORAGE_HMAC_SECRET",
4922
4999
  "USAGE_EVENTS"
4923
5000
  ]);
4924
5001
  function validateBindingManifest(manifest) {
@@ -5752,6 +5829,11 @@ var RESERVED_COLLECTION_NAMES = new Set(
5752
5829
 
5753
5830
  // src/server/utils/scoped-r2-files.ts
5754
5831
  var CORS_HEADERS3 = { "Access-Control-Allow-Origin": "*" };
5832
+ function contentDisposition(originalName) {
5833
+ const safe = originalName.replace(/[\x00-\x1f\x7f"\\]/g, "_") || "download";
5834
+ const encoded = encodeURIComponent(originalName);
5835
+ return `inline; filename="${safe}"; filename*=UTF-8''${encoded}`;
5836
+ }
5755
5837
  function sanitizeSubpath(raw) {
5756
5838
  if (!raw) return "";
5757
5839
  const segments = raw.split("/");
@@ -5788,7 +5870,9 @@ function createScopedR2Handler(config) {
5788
5870
  { status: 400, headers: CORS_HEADERS3 }
5789
5871
  );
5790
5872
  }
5791
- const scope = url.searchParams.get("scope") || "self";
5873
+ const isKeyedGet = subpath !== "" && request.method === "GET";
5874
+ const defaultScope = isKeyedGet ? "app" : "self";
5875
+ const scope = url.searchParams.get("scope") || defaultScope;
5792
5876
  const result = config.resolvePrefix(scope, { userId: auth.userId, url });
5793
5877
  if (!result.prefix) {
5794
5878
  const errorMsg = result.error ?? "Invalid scope";
@@ -5939,418 +6023,162 @@ async function handleDownload(bucket, key) {
5939
6023
  headers.set("Cache-Control", "public, max-age=31536000, immutable");
5940
6024
  headers.set("Access-Control-Allow-Origin", "*");
5941
6025
  if (object.customMetadata?.originalName) {
5942
- headers.set("Content-Disposition", `inline; filename="${object.customMetadata.originalName}"`);
6026
+ headers.set("Content-Disposition", contentDisposition(object.customMetadata.originalName));
5943
6027
  }
5944
6028
  return new Response(object.body, { headers });
5945
6029
  }
5946
6030
 
5947
- // src/server/auth/jwtVerifier.ts
5948
- import { importSPKI, jwtVerify } from "jose";
5949
-
5950
- // src/server/auth/utils.ts
5951
- var BASE64_URL_REGEX = /^[A-Za-z0-9-_]+$/;
5952
- function bufferToHex(bytes) {
5953
- const view = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
5954
- let hex = "";
5955
- for (const byte of view) {
5956
- hex += byte.toString(16).padStart(2, "0");
6031
+ // src/server/utils/proxies.ts
6032
+ function resolveApiTransport(env) {
6033
+ if (env.API_WORKER) return { kind: "binding", fetcher: env.API_WORKER };
6034
+ if (env.API_WORKER_URL) {
6035
+ return { kind: "url", baseUrl: env.API_WORKER_URL.replace(/\/$/, "") };
5957
6036
  }
5958
- return hex;
6037
+ throw new Error(
6038
+ "apiWorkerFetch: neither env.API_WORKER nor env.API_WORKER_URL is set. Add a [[services]] binding in wrangler.toml for production, or let `deepspace dev` write API_WORKER_URL into .dev.vars for local development."
6039
+ );
5959
6040
  }
5960
- function normalizeArray(value) {
5961
- if (value == null) return void 0;
5962
- if (Array.isArray(value)) {
5963
- return value.length ? value : void 0;
6041
+ function resolvePlatformTransport(env) {
6042
+ if (env.PLATFORM_WORKER) return { kind: "binding", fetcher: env.PLATFORM_WORKER };
6043
+ if (env.PLATFORM_WORKER_URL) {
6044
+ return { kind: "url", baseUrl: env.PLATFORM_WORKER_URL.replace(/\/$/, "") };
5964
6045
  }
5965
- return [value];
6046
+ throw new Error(
6047
+ "platformWorkerFetch: neither env.PLATFORM_WORKER nor env.PLATFORM_WORKER_URL is set. Add a [[services]] binding in wrangler.toml for production, or let `deepspace dev` write PLATFORM_WORKER_URL into .dev.vars for local development."
6048
+ );
5966
6049
  }
5967
- function decodeJwtPayload(token) {
5968
- if (!token) return void 0;
5969
- const parts = token.split(".");
5970
- if (parts.length < 2) return void 0;
5971
- const payloadSegment = parts[1];
5972
- if (!payloadSegment) return void 0;
5973
- if (!BASE64_URL_REGEX.test(payloadSegment)) return void 0;
5974
- try {
5975
- const normalized = payloadSegment.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(payloadSegment.length / 4) * 4, "=");
5976
- const bytes = base64ToUint8Array(normalized);
5977
- const json = new TextDecoder().decode(bytes);
5978
- const payload = JSON.parse(json);
5979
- return {
5980
- iss: payload.iss ?? null,
5981
- aud: payload.aud ?? null,
5982
- azp: payload?.azp ?? null,
5983
- exp: payload.exp ?? null,
5984
- iat: payload.iat ?? null
5985
- };
5986
- } catch {
5987
- return void 0;
6050
+ function buildTargetUrl(transport, path, internalHost) {
6051
+ let pathAndSearch;
6052
+ if (path.startsWith("http://") || path.startsWith("https://")) {
6053
+ const u = new URL(path);
6054
+ pathAndSearch = u.pathname + u.search;
6055
+ } else if (path.startsWith("/")) {
6056
+ pathAndSearch = path;
6057
+ } else {
6058
+ pathAndSearch = "/" + path;
6059
+ }
6060
+ if (transport.kind === "binding") {
6061
+ return `https://${internalHost}${pathAndSearch}`;
5988
6062
  }
6063
+ return `${transport.baseUrl}${pathAndSearch}`;
5989
6064
  }
5990
- function nowMs() {
5991
- return Date.now();
6065
+ function apiWorkerFetch(env, path, init) {
6066
+ const transport = resolveApiTransport(env);
6067
+ const url = buildTargetUrl(transport, path, "api-worker");
6068
+ if (transport.kind === "binding") {
6069
+ return transport.fetcher.fetch(url, init);
6070
+ }
6071
+ return fetch(url, init);
5992
6072
  }
5993
- function base64ToUint8Array(value) {
5994
- if (typeof atob === "function") {
5995
- const binary = atob(value);
5996
- const out = new Uint8Array(binary.length);
5997
- for (let i = 0; i < binary.length; i += 1) {
5998
- out[i] = binary.charCodeAt(i);
6073
+ function platformWorkerFetch(env, pathOrRequest, init) {
6074
+ const transport = resolvePlatformTransport(env);
6075
+ if (typeof pathOrRequest === "string") {
6076
+ const url2 = buildTargetUrl(transport, pathOrRequest, "platform-worker");
6077
+ if (transport.kind === "binding") {
6078
+ return transport.fetcher.fetch(url2, init);
5999
6079
  }
6000
- return out;
6080
+ return fetch(url2, init);
6001
6081
  }
6002
- if (typeof Buffer !== "undefined") {
6003
- return Uint8Array.from(Buffer.from(value, "base64"));
6082
+ const req = pathOrRequest;
6083
+ const url = buildTargetUrl(transport, req.url, "platform-worker");
6084
+ const rewritten = new Request(url, req);
6085
+ if (transport.kind === "binding") {
6086
+ return transport.fetcher.fetch(rewritten);
6004
6087
  }
6005
- throw new Error("No base64 decoder available in current environment");
6088
+ return fetch(rewritten);
6006
6089
  }
6007
-
6008
- // src/server/auth/jwtVerifier.ts
6009
- var DEFAULT_CLOCK_SKEW_MS = 5e3;
6010
- function matchesAuthorizedParty(azp, patterns) {
6011
- if (!azp) return true;
6012
- for (const pattern of patterns) {
6013
- if (pattern === azp) return true;
6014
- if (pattern.includes("*")) {
6015
- const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
6016
- const regexPattern = escaped.replace(/\*/g, "[^/]*");
6017
- const regex = new RegExp(`^${regexPattern}$`);
6018
- if (regex.test(azp)) return true;
6019
- if (pattern.includes("*.")) {
6020
- const rootPattern = pattern.replace("*.", "");
6021
- if (rootPattern === azp) return true;
6022
- }
6023
- }
6090
+ function authWorkerFetch(env, path, init) {
6091
+ if (!env.AUTH_WORKER_URL) {
6092
+ throw new Error(
6093
+ "authWorkerFetch: env.AUTH_WORKER_URL is not set. `deepspace dev` should write this into .dev.vars; for production, set it as a wrangler var or secret."
6094
+ );
6024
6095
  }
6025
- return false;
6026
- }
6027
- var keyCache = /* @__PURE__ */ new Map();
6028
- async function getPublicKey(pem) {
6029
- const cached = keyCache.get(pem);
6030
- if (cached) return cached;
6031
- const normalized = pem.replace(/\\n/g, "\n");
6032
- const key = await importSPKI(normalized, "ES256");
6033
- keyCache.set(pem, key);
6034
- return key;
6096
+ const baseUrl = env.AUTH_WORKER_URL.replace(/\/$/, "");
6097
+ let pathAndSearch;
6098
+ if (path.startsWith("http://") || path.startsWith("https://")) {
6099
+ const u = new URL(path);
6100
+ pathAndSearch = u.pathname + u.search;
6101
+ } else if (path.startsWith("/")) {
6102
+ pathAndSearch = path;
6103
+ } else {
6104
+ pathAndSearch = "/" + path;
6105
+ }
6106
+ return fetch(`${baseUrl}${pathAndSearch}`, init);
6035
6107
  }
6036
- async function verifyJwt(config, token) {
6037
- if (!token) {
6038
- return { result: null };
6108
+
6109
+ // src/server/utils/cron.ts
6110
+ function buildCronContext(env, ownerUserId, roomId = "default") {
6111
+ const roomIdObj = env.RECORD_ROOMS.idFromName(roomId);
6112
+ const room = env.RECORD_ROOMS.get(roomIdObj);
6113
+ async function executeTool3(tool, params) {
6114
+ const response = await room.fetch(new Request("https://internal/api/tools/execute", {
6115
+ method: "POST",
6116
+ headers: {
6117
+ "Content-Type": "application/json",
6118
+ "X-User-Id": ownerUserId,
6119
+ "X-App-Action": "true"
6120
+ },
6121
+ body: JSON.stringify({ tool, params })
6122
+ }));
6123
+ const result = await response.json();
6124
+ if (!result.success) {
6125
+ throw new Error(`RecordRoom tool ${tool} failed: ${result.error || "Unknown error"}`);
6126
+ }
6127
+ return result.data;
6039
6128
  }
6040
- try {
6041
- const publicKey = await getPublicKey(config.publicKey);
6042
- const audience = normalizeArray(config.audience);
6043
- const clockTolerance = Math.floor(
6044
- (config.clockSkewMs ?? DEFAULT_CLOCK_SKEW_MS) / 1e3
6045
- );
6046
- const { payload } = await jwtVerify(token, publicKey, {
6047
- issuer: config.issuer,
6048
- audience: audience && audience.length === 1 ? audience[0] : audience,
6049
- clockTolerance
6050
- });
6051
- const claims = payload;
6052
- if (!claims.sub) {
6053
- throw new Error("JWT verification succeeded but subject claim is missing");
6129
+ const records = {
6130
+ async query(collection, opts) {
6131
+ const params = { collection };
6132
+ if (opts?.where) params.where = opts.where;
6133
+ if (opts?.limit) params.limit = opts.limit;
6134
+ const data = await executeTool3("records.query", params);
6135
+ return data.records || [];
6136
+ },
6137
+ async create(collection, data) {
6138
+ return executeTool3("records.create", { collection, data });
6139
+ },
6140
+ async update(collection, recordId, data) {
6141
+ return executeTool3("records.update", { collection, recordId, data });
6142
+ },
6143
+ async delete(collection, recordId) {
6144
+ return executeTool3("records.delete", { collection, recordId });
6054
6145
  }
6055
- const authorizedParties = normalizeArray(config.authorizedParties);
6056
- if (authorizedParties) {
6057
- const azp = claims.azp;
6058
- if (!matchesAuthorizedParty(azp, authorizedParties)) {
6146
+ };
6147
+ const integrations = {
6148
+ async call(endpoint, params = {}) {
6149
+ if (!env.APP_OWNER_JWT) {
6059
6150
  throw new Error(
6060
- `Authorized party '${azp}' does not match any allowed pattern`
6151
+ "integrations.call requires env.APP_OWNER_JWT. Deployed apps receive this automatically; in dev, `deepspace dev` writes it into .dev.vars."
6061
6152
  );
6062
6153
  }
6063
- }
6064
- return {
6065
- result: {
6066
- userId: claims.sub,
6067
- claims
6154
+ const response = await apiWorkerFetch(env, `/api/integrations/${endpoint}`, {
6155
+ method: "POST",
6156
+ headers: {
6157
+ "Content-Type": "application/json",
6158
+ Authorization: `Bearer ${env.APP_OWNER_JWT}`
6159
+ },
6160
+ body: JSON.stringify(params)
6161
+ });
6162
+ const text = await response.text();
6163
+ let result = {};
6164
+ try {
6165
+ result = text ? JSON.parse(text) : {};
6166
+ } catch {
6068
6167
  }
6069
- };
6070
- } catch (error) {
6071
- return {
6072
- result: null,
6073
- error,
6074
- debug: decodeJwtPayload(token)
6075
- };
6076
- }
6077
- }
6078
-
6079
- // src/server/auth/internalAuth.ts
6080
- var DEFAULT_MAX_SKEW_MS = 5 * 6e4;
6081
- var nodeCryptoPromise = null;
6082
- async function loadNodeCrypto() {
6083
- if (!nodeCryptoPromise) {
6084
- nodeCryptoPromise = import("crypto").then((module) => module).catch(() => null);
6085
- }
6086
- return nodeCryptoPromise;
6087
- }
6088
- function hasSubtleCrypto() {
6089
- return typeof globalThis.crypto?.subtle?.importKey === "function";
6090
- }
6091
- async function computeHmacHex(secret, payload) {
6092
- if (hasSubtleCrypto()) {
6093
- const encoder = new TextEncoder();
6094
- const key = await crypto.subtle.importKey(
6095
- "raw",
6096
- encoder.encode(secret),
6097
- { name: "HMAC", hash: "SHA-256" },
6098
- false,
6099
- ["sign"]
6100
- );
6101
- const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(payload));
6102
- return bufferToHex(signature);
6103
- }
6104
- const nodeCrypto = await loadNodeCrypto();
6105
- if (!nodeCrypto) {
6106
- throw new Error("Crypto implementation unavailable for HMAC computation");
6107
- }
6108
- return nodeCrypto.createHmac("sha256", secret).update(payload).digest("hex");
6109
- }
6110
- async function timingSafeEqualHex(a, b) {
6111
- if (a.length !== b.length) {
6112
- return false;
6113
- }
6114
- const nodeCrypto = await loadNodeCrypto();
6115
- if (nodeCrypto?.timingSafeEqual) {
6116
- const aBuf = Buffer.from(a, "hex");
6117
- const bBuf = Buffer.from(b, "hex");
6118
- try {
6119
- return nodeCrypto.timingSafeEqual(aBuf, bBuf);
6120
- } catch {
6121
- return false;
6122
- }
6123
- }
6124
- let diff = 0;
6125
- for (let i = 0; i < a.length; i += 1) {
6126
- diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
6127
- }
6128
- return diff === 0;
6129
- }
6130
- async function signInternalPayload({
6131
- secret,
6132
- payload,
6133
- timestamp
6134
- }) {
6135
- const ts = timestamp ?? nowMs().toString();
6136
- const signature = await computeHmacHex(secret, `${ts}.${payload}`);
6137
- return { timestamp: ts, signature };
6138
- }
6139
- async function verifyInternalSignature({
6140
- secret,
6141
- timestamp,
6142
- signature,
6143
- payload,
6144
- maxSkewMs = DEFAULT_MAX_SKEW_MS
6145
- }) {
6146
- if (!secret || !timestamp || !signature) {
6147
- return false;
6148
- }
6149
- const tsNumber = Number(timestamp);
6150
- if (!Number.isFinite(tsNumber)) {
6151
- return false;
6152
- }
6153
- const skew = Math.abs(nowMs() - tsNumber);
6154
- if (skew > maxSkewMs) {
6155
- return false;
6156
- }
6157
- const expected = await computeHmacHex(secret, `${timestamp}.${payload}`);
6158
- return timingSafeEqualHex(signature.toLowerCase(), expected.toLowerCase());
6159
- }
6160
- function buildInternalPayload(body) {
6161
- if (body === void 0) return "{}";
6162
- if (typeof body === "string") return body;
6163
- try {
6164
- return JSON.stringify(body);
6165
- } catch {
6166
- return "{}";
6167
- }
6168
- }
6169
-
6170
- // src/server/auth/betterAuth.ts
6171
- import { betterAuth } from "better-auth";
6172
- import { organization, twoFactor } from "better-auth/plugins";
6173
- function createDeepSpaceAuth(config) {
6174
- const socialProviders = {};
6175
- if (config.google) {
6176
- socialProviders.google = config.google;
6177
- }
6178
- if (config.github) {
6179
- socialProviders.github = config.github;
6180
- }
6181
- return betterAuth({
6182
- database: config.database,
6183
- baseURL: config.baseURL,
6184
- secret: config.secret,
6185
- emailAndPassword: {
6186
- enabled: config.emailAndPassword ?? true
6187
- },
6188
- socialProviders,
6189
- trustedOrigins: config.trustedOrigins ?? [
6190
- "https://deep.space",
6191
- "https://*.deep.space",
6192
- "https://*.app.space",
6193
- "http://localhost:*"
6194
- ],
6195
- plugins: [organization(), twoFactor()]
6196
- });
6197
- }
6198
-
6199
- // src/server/utils/cron.ts
6200
- var DEFAULT_API_BASE_URL = "https://api-worker.deep.space";
6201
- function buildCronContext(env, ownerUserId, roomId = "default") {
6202
- const hmacSecret = env.INTERNAL_STORAGE_HMAC_SECRET;
6203
- const apiBase = env.API_BASE_URL || DEFAULT_API_BASE_URL;
6204
- const roomIdObj = env.RECORD_ROOMS.idFromName(roomId);
6205
- const room = env.RECORD_ROOMS.get(roomIdObj);
6206
- async function executeTool3(tool, params) {
6207
- const response = await room.fetch(new Request("https://internal/api/tools/execute", {
6208
- method: "POST",
6209
- headers: {
6210
- "Content-Type": "application/json",
6211
- "X-User-Id": ownerUserId,
6212
- "X-App-Action": "true"
6213
- },
6214
- body: JSON.stringify({ tool, params })
6215
- }));
6216
- const result = await response.json();
6217
- if (!result.success) {
6218
- throw new Error(`RecordRoom tool ${tool} failed: ${result.error || "Unknown error"}`);
6219
- }
6220
- return result.data;
6221
- }
6222
- const records = {
6223
- async query(collection, opts) {
6224
- const params = { collection };
6225
- if (opts?.where) params.where = opts.where;
6226
- if (opts?.limit) params.limit = opts.limit;
6227
- const data = await executeTool3("records.query", params);
6228
- return data.records || [];
6229
- },
6230
- async create(collection, data) {
6231
- return executeTool3("records.create", { collection, data });
6232
- },
6233
- async update(collection, recordId, data) {
6234
- return executeTool3("records.update", { collection, recordId, data });
6235
- },
6236
- async delete(collection, recordId) {
6237
- return executeTool3("records.delete", { collection, recordId });
6238
- }
6239
- };
6240
- const integrations = {
6241
- async call(endpoint, params) {
6242
- if (!hmacSecret) {
6243
- throw new Error("INTERNAL_STORAGE_HMAC_SECRET not configured \u2014 cannot call integrations");
6244
- }
6245
- const payload = { userId: ownerUserId, ...params };
6246
- const payloadStr = buildInternalPayload(payload);
6247
- const { timestamp, signature } = await signInternalPayload({
6248
- secret: hmacSecret,
6249
- payload: payloadStr
6250
- });
6251
- const response = await fetch(`${apiBase}/api/internal/integrations/${endpoint}`, {
6252
- method: "POST",
6253
- headers: {
6254
- "Content-Type": "application/json",
6255
- "x-internal-timestamp": timestamp,
6256
- "x-internal-signature": signature
6257
- },
6258
- body: payloadStr
6259
- });
6260
- if (!response.ok) {
6261
- const errText = await response.text();
6262
- throw new Error(`Integration call ${endpoint} failed (${response.status}): ${errText}`);
6263
- }
6264
- return response.json();
6265
- }
6266
- };
6267
- return { records, integrations, ownerUserId };
6168
+ if (!response.ok || !result.success) {
6169
+ const detail = result.message || result.error || text || `HTTP ${response.status}`;
6170
+ throw new Error(`Integration call ${endpoint} failed: ${detail}`);
6171
+ }
6172
+ return result.data;
6173
+ }
6174
+ };
6175
+ return { records, integrations, ownerUserId };
6268
6176
  }
6269
6177
 
6270
6178
  // src/server/utils/ai.ts
6271
6179
  import { createAnthropic } from "@ai-sdk/anthropic";
6272
6180
  import { createOpenAI } from "@ai-sdk/openai";
6273
6181
  import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
6274
-
6275
- // src/server/utils/proxies.ts
6276
- function resolveApiTransport(env) {
6277
- if (env.API_WORKER) return { kind: "binding", fetcher: env.API_WORKER };
6278
- if (env.API_WORKER_URL) {
6279
- return { kind: "url", baseUrl: env.API_WORKER_URL.replace(/\/$/, "") };
6280
- }
6281
- throw new Error(
6282
- "apiWorkerFetch: neither env.API_WORKER nor env.API_WORKER_URL is set. Add a [[services]] binding in wrangler.toml for production, or let `deepspace dev` write API_WORKER_URL into .dev.vars for local development."
6283
- );
6284
- }
6285
- function resolvePlatformTransport(env) {
6286
- if (env.PLATFORM_WORKER) return { kind: "binding", fetcher: env.PLATFORM_WORKER };
6287
- if (env.PLATFORM_WORKER_URL) {
6288
- return { kind: "url", baseUrl: env.PLATFORM_WORKER_URL.replace(/\/$/, "") };
6289
- }
6290
- throw new Error(
6291
- "platformWorkerFetch: neither env.PLATFORM_WORKER nor env.PLATFORM_WORKER_URL is set. Add a [[services]] binding in wrangler.toml for production, or let `deepspace dev` write PLATFORM_WORKER_URL into .dev.vars for local development."
6292
- );
6293
- }
6294
- function buildTargetUrl(transport, path, internalHost) {
6295
- let pathAndSearch;
6296
- if (path.startsWith("http://") || path.startsWith("https://")) {
6297
- const u = new URL(path);
6298
- pathAndSearch = u.pathname + u.search;
6299
- } else if (path.startsWith("/")) {
6300
- pathAndSearch = path;
6301
- } else {
6302
- pathAndSearch = "/" + path;
6303
- }
6304
- if (transport.kind === "binding") {
6305
- return `https://${internalHost}${pathAndSearch}`;
6306
- }
6307
- return `${transport.baseUrl}${pathAndSearch}`;
6308
- }
6309
- function apiWorkerFetch(env, path, init) {
6310
- const transport = resolveApiTransport(env);
6311
- const url = buildTargetUrl(transport, path, "api-worker");
6312
- if (transport.kind === "binding") {
6313
- return transport.fetcher.fetch(url, init);
6314
- }
6315
- return fetch(url, init);
6316
- }
6317
- function platformWorkerFetch(env, pathOrRequest, init) {
6318
- const transport = resolvePlatformTransport(env);
6319
- if (typeof pathOrRequest === "string") {
6320
- const url2 = buildTargetUrl(transport, pathOrRequest, "platform-worker");
6321
- if (transport.kind === "binding") {
6322
- return transport.fetcher.fetch(url2, init);
6323
- }
6324
- return fetch(url2, init);
6325
- }
6326
- const req = pathOrRequest;
6327
- const url = buildTargetUrl(transport, req.url, "platform-worker");
6328
- const rewritten = new Request(url, req);
6329
- if (transport.kind === "binding") {
6330
- return transport.fetcher.fetch(rewritten);
6331
- }
6332
- return fetch(rewritten);
6333
- }
6334
- function authWorkerFetch(env, path, init) {
6335
- if (!env.AUTH_WORKER_URL) {
6336
- throw new Error(
6337
- "authWorkerFetch: env.AUTH_WORKER_URL is not set. `deepspace dev` should write this into .dev.vars; for production, set it as a wrangler var or secret."
6338
- );
6339
- }
6340
- const baseUrl = env.AUTH_WORKER_URL.replace(/\/$/, "");
6341
- let pathAndSearch;
6342
- if (path.startsWith("http://") || path.startsWith("https://")) {
6343
- const u = new URL(path);
6344
- pathAndSearch = u.pathname + u.search;
6345
- } else if (path.startsWith("/")) {
6346
- pathAndSearch = path;
6347
- } else {
6348
- pathAndSearch = "/" + path;
6349
- }
6350
- return fetch(`${baseUrl}${pathAndSearch}`, init);
6351
- }
6352
-
6353
- // src/server/utils/ai.ts
6354
6182
  var ANTHROPIC_DEFAULT_MAX_OUTPUT_TOKENS = 4096;
6355
6183
  function withDefaultAnthropicMaxTokens(body) {
6356
6184
  if (typeof body !== "string") return body;
@@ -6944,6 +6772,215 @@ async function runMigrations(db, migrations) {
6944
6772
  function splitStatements(sql) {
6945
6773
  return sql.split(";").map((s) => s.trim()).filter((s) => s.length > 0);
6946
6774
  }
6775
+
6776
+ // src/server/auth/jwtVerifier.ts
6777
+ import { importSPKI, jwtVerify } from "jose";
6778
+
6779
+ // src/server/auth/utils.ts
6780
+ var BASE64_URL_REGEX = /^[A-Za-z0-9-_]+$/;
6781
+ function bufferToHex(bytes) {
6782
+ const view = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
6783
+ let hex = "";
6784
+ for (const byte of view) {
6785
+ hex += byte.toString(16).padStart(2, "0");
6786
+ }
6787
+ return hex;
6788
+ }
6789
+ function normalizeArray(value) {
6790
+ if (value == null) return void 0;
6791
+ if (Array.isArray(value)) {
6792
+ return value.length ? value : void 0;
6793
+ }
6794
+ return [value];
6795
+ }
6796
+ function decodeJwtPayload(token) {
6797
+ if (!token) return void 0;
6798
+ const parts = token.split(".");
6799
+ if (parts.length < 2) return void 0;
6800
+ const payloadSegment = parts[1];
6801
+ if (!payloadSegment) return void 0;
6802
+ if (!BASE64_URL_REGEX.test(payloadSegment)) return void 0;
6803
+ try {
6804
+ const normalized = payloadSegment.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(payloadSegment.length / 4) * 4, "=");
6805
+ const bytes = base64ToUint8Array(normalized);
6806
+ const json = new TextDecoder().decode(bytes);
6807
+ const payload = JSON.parse(json);
6808
+ return {
6809
+ iss: payload.iss ?? null,
6810
+ aud: payload.aud ?? null,
6811
+ azp: payload?.azp ?? null,
6812
+ exp: payload.exp ?? null,
6813
+ iat: payload.iat ?? null
6814
+ };
6815
+ } catch {
6816
+ return void 0;
6817
+ }
6818
+ }
6819
+ function base64ToUint8Array(value) {
6820
+ if (typeof atob === "function") {
6821
+ const binary = atob(value);
6822
+ const out = new Uint8Array(binary.length);
6823
+ for (let i = 0; i < binary.length; i += 1) {
6824
+ out[i] = binary.charCodeAt(i);
6825
+ }
6826
+ return out;
6827
+ }
6828
+ if (typeof Buffer !== "undefined") {
6829
+ return Uint8Array.from(Buffer.from(value, "base64"));
6830
+ }
6831
+ throw new Error("No base64 decoder available in current environment");
6832
+ }
6833
+
6834
+ // src/server/auth/jwtVerifier.ts
6835
+ var DEFAULT_CLOCK_SKEW_MS = 5e3;
6836
+ function matchesAuthorizedParty(azp, patterns) {
6837
+ if (!azp) return true;
6838
+ for (const pattern of patterns) {
6839
+ if (pattern === azp) return true;
6840
+ if (pattern.includes("*")) {
6841
+ const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
6842
+ const regexPattern = escaped.replace(/\*/g, "[^/]*");
6843
+ const regex = new RegExp(`^${regexPattern}$`);
6844
+ if (regex.test(azp)) return true;
6845
+ if (pattern.includes("*.")) {
6846
+ const rootPattern = pattern.replace("*.", "");
6847
+ if (rootPattern === azp) return true;
6848
+ }
6849
+ }
6850
+ }
6851
+ return false;
6852
+ }
6853
+ var keyCache = /* @__PURE__ */ new Map();
6854
+ async function getPublicKey(pem) {
6855
+ const cached = keyCache.get(pem);
6856
+ if (cached) return cached;
6857
+ const normalized = pem.replace(/\\n/g, "\n");
6858
+ const key = await importSPKI(normalized, "ES256");
6859
+ keyCache.set(pem, key);
6860
+ return key;
6861
+ }
6862
+ async function verifyJwt(config, token) {
6863
+ if (!token) {
6864
+ return { result: null };
6865
+ }
6866
+ try {
6867
+ const publicKey = await getPublicKey(config.publicKey);
6868
+ const audience = normalizeArray(config.audience);
6869
+ const clockTolerance = Math.floor(
6870
+ (config.clockSkewMs ?? DEFAULT_CLOCK_SKEW_MS) / 1e3
6871
+ );
6872
+ const { payload } = await jwtVerify(token, publicKey, {
6873
+ issuer: config.issuer,
6874
+ audience: audience && audience.length === 1 ? audience[0] : audience,
6875
+ clockTolerance
6876
+ });
6877
+ const claims = payload;
6878
+ if (!claims.sub) {
6879
+ throw new Error("JWT verification succeeded but subject claim is missing");
6880
+ }
6881
+ const authorizedParties = normalizeArray(config.authorizedParties);
6882
+ if (authorizedParties) {
6883
+ const azp = claims.azp;
6884
+ if (!matchesAuthorizedParty(azp, authorizedParties)) {
6885
+ throw new Error(
6886
+ `Authorized party '${azp}' does not match any allowed pattern`
6887
+ );
6888
+ }
6889
+ }
6890
+ return {
6891
+ result: {
6892
+ userId: claims.sub,
6893
+ claims
6894
+ }
6895
+ };
6896
+ } catch (error) {
6897
+ return {
6898
+ result: null,
6899
+ error,
6900
+ debug: decodeJwtPayload(token)
6901
+ };
6902
+ }
6903
+ }
6904
+
6905
+ // src/server/auth/internalAuth.ts
6906
+ var nodeCryptoPromise = null;
6907
+ async function loadNodeCrypto() {
6908
+ if (!nodeCryptoPromise) {
6909
+ nodeCryptoPromise = import("crypto").then((module) => module).catch(() => null);
6910
+ }
6911
+ return nodeCryptoPromise;
6912
+ }
6913
+ function hasSubtleCrypto() {
6914
+ return typeof globalThis.crypto?.subtle?.importKey === "function";
6915
+ }
6916
+ async function computeHmacHex(secret, payload) {
6917
+ if (hasSubtleCrypto()) {
6918
+ const encoder = new TextEncoder();
6919
+ const key = await crypto.subtle.importKey(
6920
+ "raw",
6921
+ encoder.encode(secret),
6922
+ { name: "HMAC", hash: "SHA-256" },
6923
+ false,
6924
+ ["sign"]
6925
+ );
6926
+ const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(payload));
6927
+ return bufferToHex(signature);
6928
+ }
6929
+ const nodeCrypto = await loadNodeCrypto();
6930
+ if (!nodeCrypto) {
6931
+ throw new Error("Crypto implementation unavailable for HMAC computation");
6932
+ }
6933
+ return nodeCrypto.createHmac("sha256", secret).update(payload).digest("hex");
6934
+ }
6935
+ async function timingSafeEqualHex(a, b) {
6936
+ if (a.length !== b.length) {
6937
+ return false;
6938
+ }
6939
+ const nodeCrypto = await loadNodeCrypto();
6940
+ if (nodeCrypto?.timingSafeEqual) {
6941
+ const aBuf = Buffer.from(a, "hex");
6942
+ const bBuf = Buffer.from(b, "hex");
6943
+ try {
6944
+ return nodeCrypto.timingSafeEqual(aBuf, bBuf);
6945
+ } catch {
6946
+ return false;
6947
+ }
6948
+ }
6949
+ let diff = 0;
6950
+ for (let i = 0; i < a.length; i += 1) {
6951
+ diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
6952
+ }
6953
+ return diff === 0;
6954
+ }
6955
+
6956
+ // src/server/auth/betterAuth.ts
6957
+ import { betterAuth } from "better-auth";
6958
+ import { organization, twoFactor } from "better-auth/plugins";
6959
+ function createDeepSpaceAuth(config) {
6960
+ const socialProviders = {};
6961
+ if (config.google) {
6962
+ socialProviders.google = config.google;
6963
+ }
6964
+ if (config.github) {
6965
+ socialProviders.github = config.github;
6966
+ }
6967
+ return betterAuth({
6968
+ database: config.database,
6969
+ baseURL: config.baseURL,
6970
+ secret: config.secret,
6971
+ emailAndPassword: {
6972
+ enabled: config.emailAndPassword ?? true
6973
+ },
6974
+ socialProviders,
6975
+ trustedOrigins: config.trustedOrigins ?? [
6976
+ "https://deep.space",
6977
+ "https://*.deep.space",
6978
+ "https://*.app.space",
6979
+ "http://localhost:*"
6980
+ ],
6981
+ plugins: [organization(), twoFactor()]
6982
+ });
6983
+ }
6947
6984
  export {
6948
6985
  AI_CHATS_SCHEMA,
6949
6986
  AI_MESSAGES_SCHEMA,
@@ -6964,7 +7001,6 @@ export {
6964
7001
  CronRoom,
6965
7002
  DEFAULT_CONTEXT_CONFIG,
6966
7003
  DEFAULT_DO_MANIFEST,
6967
- DEFAULT_MAX_SKEW_MS,
6968
7004
  DIRECTORY_SCHEMAS,
6969
7005
  GLOBAL_DO_TYPES,
6970
7006
  GLOBAL_DO_TYPE_NAMES,
@@ -7002,7 +7038,6 @@ export {
7002
7038
  authWorkerFetch,
7003
7039
  bindingManifestFromOutputConfig,
7004
7040
  buildCronContext,
7005
- buildInternalPayload,
7006
7041
  buildTableSelect,
7007
7042
  buildUiParts,
7008
7043
  canCreate,
@@ -7061,7 +7096,6 @@ export {
7061
7096
  rowToData,
7062
7097
  runMigrations,
7063
7098
  serverBuild,
7064
- signInternalPayload,
7065
7099
  timingSafeEqualHex,
7066
7100
  toUint8Array,
7067
7101
  totalChars,
@@ -7072,7 +7106,6 @@ export {
7072
7106
  validateAppName,
7073
7107
  validateBindingManifest,
7074
7108
  validateDoManifest,
7075
- verifyInternalSignature,
7076
7109
  verifyJwt,
7077
7110
  workspaceAccountsSchema,
7078
7111
  workspaceContentSharesSchema,