deepspace 0.3.11 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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("/");
@@ -5939,418 +6021,162 @@ async function handleDownload(bucket, key) {
5939
6021
  headers.set("Cache-Control", "public, max-age=31536000, immutable");
5940
6022
  headers.set("Access-Control-Allow-Origin", "*");
5941
6023
  if (object.customMetadata?.originalName) {
5942
- headers.set("Content-Disposition", `inline; filename="${object.customMetadata.originalName}"`);
6024
+ headers.set("Content-Disposition", contentDisposition(object.customMetadata.originalName));
5943
6025
  }
5944
6026
  return new Response(object.body, { headers });
5945
6027
  }
5946
6028
 
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");
6029
+ // src/server/utils/proxies.ts
6030
+ function resolveApiTransport(env) {
6031
+ if (env.API_WORKER) return { kind: "binding", fetcher: env.API_WORKER };
6032
+ if (env.API_WORKER_URL) {
6033
+ return { kind: "url", baseUrl: env.API_WORKER_URL.replace(/\/$/, "") };
5957
6034
  }
5958
- return hex;
6035
+ throw new Error(
6036
+ "apiWorkerFetch: neither env.API_WORKER nor env.API_WORKER_URL is set. Add a [[services]] binding in wrangler.toml for production, or let `deepspace dev` write API_WORKER_URL into .dev.vars for local development."
6037
+ );
5959
6038
  }
5960
- function normalizeArray(value) {
5961
- if (value == null) return void 0;
5962
- if (Array.isArray(value)) {
5963
- return value.length ? value : void 0;
6039
+ function resolvePlatformTransport(env) {
6040
+ if (env.PLATFORM_WORKER) return { kind: "binding", fetcher: env.PLATFORM_WORKER };
6041
+ if (env.PLATFORM_WORKER_URL) {
6042
+ return { kind: "url", baseUrl: env.PLATFORM_WORKER_URL.replace(/\/$/, "") };
5964
6043
  }
5965
- return [value];
6044
+ throw new Error(
6045
+ "platformWorkerFetch: neither env.PLATFORM_WORKER nor env.PLATFORM_WORKER_URL is set. Add a [[services]] binding in wrangler.toml for production, or let `deepspace dev` write PLATFORM_WORKER_URL into .dev.vars for local development."
6046
+ );
5966
6047
  }
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;
6048
+ function buildTargetUrl(transport, path, internalHost) {
6049
+ let pathAndSearch;
6050
+ if (path.startsWith("http://") || path.startsWith("https://")) {
6051
+ const u = new URL(path);
6052
+ pathAndSearch = u.pathname + u.search;
6053
+ } else if (path.startsWith("/")) {
6054
+ pathAndSearch = path;
6055
+ } else {
6056
+ pathAndSearch = "/" + path;
6057
+ }
6058
+ if (transport.kind === "binding") {
6059
+ return `https://${internalHost}${pathAndSearch}`;
5988
6060
  }
6061
+ return `${transport.baseUrl}${pathAndSearch}`;
5989
6062
  }
5990
- function nowMs() {
5991
- return Date.now();
6063
+ function apiWorkerFetch(env, path, init) {
6064
+ const transport = resolveApiTransport(env);
6065
+ const url = buildTargetUrl(transport, path, "api-worker");
6066
+ if (transport.kind === "binding") {
6067
+ return transport.fetcher.fetch(url, init);
6068
+ }
6069
+ return fetch(url, init);
5992
6070
  }
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);
6071
+ function platformWorkerFetch(env, pathOrRequest, init) {
6072
+ const transport = resolvePlatformTransport(env);
6073
+ if (typeof pathOrRequest === "string") {
6074
+ const url2 = buildTargetUrl(transport, pathOrRequest, "platform-worker");
6075
+ if (transport.kind === "binding") {
6076
+ return transport.fetcher.fetch(url2, init);
5999
6077
  }
6000
- return out;
6078
+ return fetch(url2, init);
6001
6079
  }
6002
- if (typeof Buffer !== "undefined") {
6003
- return Uint8Array.from(Buffer.from(value, "base64"));
6080
+ const req = pathOrRequest;
6081
+ const url = buildTargetUrl(transport, req.url, "platform-worker");
6082
+ const rewritten = new Request(url, req);
6083
+ if (transport.kind === "binding") {
6084
+ return transport.fetcher.fetch(rewritten);
6004
6085
  }
6005
- throw new Error("No base64 decoder available in current environment");
6086
+ return fetch(rewritten);
6006
6087
  }
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
- }
6088
+ function authWorkerFetch(env, path, init) {
6089
+ if (!env.AUTH_WORKER_URL) {
6090
+ throw new Error(
6091
+ "authWorkerFetch: env.AUTH_WORKER_URL is not set. `deepspace dev` should write this into .dev.vars; for production, set it as a wrangler var or secret."
6092
+ );
6024
6093
  }
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;
6094
+ const baseUrl = env.AUTH_WORKER_URL.replace(/\/$/, "");
6095
+ let pathAndSearch;
6096
+ if (path.startsWith("http://") || path.startsWith("https://")) {
6097
+ const u = new URL(path);
6098
+ pathAndSearch = u.pathname + u.search;
6099
+ } else if (path.startsWith("/")) {
6100
+ pathAndSearch = path;
6101
+ } else {
6102
+ pathAndSearch = "/" + path;
6103
+ }
6104
+ return fetch(`${baseUrl}${pathAndSearch}`, init);
6035
6105
  }
6036
- async function verifyJwt(config, token) {
6037
- if (!token) {
6038
- return { result: null };
6106
+
6107
+ // src/server/utils/cron.ts
6108
+ function buildCronContext(env, ownerUserId, roomId = "default") {
6109
+ const roomIdObj = env.RECORD_ROOMS.idFromName(roomId);
6110
+ const room = env.RECORD_ROOMS.get(roomIdObj);
6111
+ async function executeTool3(tool, params) {
6112
+ const response = await room.fetch(new Request("https://internal/api/tools/execute", {
6113
+ method: "POST",
6114
+ headers: {
6115
+ "Content-Type": "application/json",
6116
+ "X-User-Id": ownerUserId,
6117
+ "X-App-Action": "true"
6118
+ },
6119
+ body: JSON.stringify({ tool, params })
6120
+ }));
6121
+ const result = await response.json();
6122
+ if (!result.success) {
6123
+ throw new Error(`RecordRoom tool ${tool} failed: ${result.error || "Unknown error"}`);
6124
+ }
6125
+ return result.data;
6039
6126
  }
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");
6127
+ const records = {
6128
+ async query(collection, opts) {
6129
+ const params = { collection };
6130
+ if (opts?.where) params.where = opts.where;
6131
+ if (opts?.limit) params.limit = opts.limit;
6132
+ const data = await executeTool3("records.query", params);
6133
+ return data.records || [];
6134
+ },
6135
+ async create(collection, data) {
6136
+ return executeTool3("records.create", { collection, data });
6137
+ },
6138
+ async update(collection, recordId, data) {
6139
+ return executeTool3("records.update", { collection, recordId, data });
6140
+ },
6141
+ async delete(collection, recordId) {
6142
+ return executeTool3("records.delete", { collection, recordId });
6054
6143
  }
6055
- const authorizedParties = normalizeArray(config.authorizedParties);
6056
- if (authorizedParties) {
6057
- const azp = claims.azp;
6058
- if (!matchesAuthorizedParty(azp, authorizedParties)) {
6144
+ };
6145
+ const integrations = {
6146
+ async call(endpoint, params = {}) {
6147
+ if (!env.APP_OWNER_JWT) {
6059
6148
  throw new Error(
6060
- `Authorized party '${azp}' does not match any allowed pattern`
6149
+ "integrations.call requires env.APP_OWNER_JWT. Deployed apps receive this automatically; in dev, `deepspace dev` writes it into .dev.vars."
6061
6150
  );
6062
6151
  }
6063
- }
6064
- return {
6065
- result: {
6066
- userId: claims.sub,
6067
- claims
6152
+ const response = await apiWorkerFetch(env, `/api/integrations/${endpoint}`, {
6153
+ method: "POST",
6154
+ headers: {
6155
+ "Content-Type": "application/json",
6156
+ Authorization: `Bearer ${env.APP_OWNER_JWT}`
6157
+ },
6158
+ body: JSON.stringify(params)
6159
+ });
6160
+ const text = await response.text();
6161
+ let result = {};
6162
+ try {
6163
+ result = text ? JSON.parse(text) : {};
6164
+ } catch {
6068
6165
  }
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 };
6166
+ if (!response.ok || !result.success) {
6167
+ const detail = result.message || result.error || text || `HTTP ${response.status}`;
6168
+ throw new Error(`Integration call ${endpoint} failed: ${detail}`);
6169
+ }
6170
+ return result.data;
6171
+ }
6172
+ };
6173
+ return { records, integrations, ownerUserId };
6268
6174
  }
6269
6175
 
6270
6176
  // src/server/utils/ai.ts
6271
6177
  import { createAnthropic } from "@ai-sdk/anthropic";
6272
6178
  import { createOpenAI } from "@ai-sdk/openai";
6273
6179
  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
6180
  var ANTHROPIC_DEFAULT_MAX_OUTPUT_TOKENS = 4096;
6355
6181
  function withDefaultAnthropicMaxTokens(body) {
6356
6182
  if (typeof body !== "string") return body;
@@ -6944,6 +6770,215 @@ async function runMigrations(db, migrations) {
6944
6770
  function splitStatements(sql) {
6945
6771
  return sql.split(";").map((s) => s.trim()).filter((s) => s.length > 0);
6946
6772
  }
6773
+
6774
+ // src/server/auth/jwtVerifier.ts
6775
+ import { importSPKI, jwtVerify } from "jose";
6776
+
6777
+ // src/server/auth/utils.ts
6778
+ var BASE64_URL_REGEX = /^[A-Za-z0-9-_]+$/;
6779
+ function bufferToHex(bytes) {
6780
+ const view = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
6781
+ let hex = "";
6782
+ for (const byte of view) {
6783
+ hex += byte.toString(16).padStart(2, "0");
6784
+ }
6785
+ return hex;
6786
+ }
6787
+ function normalizeArray(value) {
6788
+ if (value == null) return void 0;
6789
+ if (Array.isArray(value)) {
6790
+ return value.length ? value : void 0;
6791
+ }
6792
+ return [value];
6793
+ }
6794
+ function decodeJwtPayload(token) {
6795
+ if (!token) return void 0;
6796
+ const parts = token.split(".");
6797
+ if (parts.length < 2) return void 0;
6798
+ const payloadSegment = parts[1];
6799
+ if (!payloadSegment) return void 0;
6800
+ if (!BASE64_URL_REGEX.test(payloadSegment)) return void 0;
6801
+ try {
6802
+ const normalized = payloadSegment.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(payloadSegment.length / 4) * 4, "=");
6803
+ const bytes = base64ToUint8Array(normalized);
6804
+ const json = new TextDecoder().decode(bytes);
6805
+ const payload = JSON.parse(json);
6806
+ return {
6807
+ iss: payload.iss ?? null,
6808
+ aud: payload.aud ?? null,
6809
+ azp: payload?.azp ?? null,
6810
+ exp: payload.exp ?? null,
6811
+ iat: payload.iat ?? null
6812
+ };
6813
+ } catch {
6814
+ return void 0;
6815
+ }
6816
+ }
6817
+ function base64ToUint8Array(value) {
6818
+ if (typeof atob === "function") {
6819
+ const binary = atob(value);
6820
+ const out = new Uint8Array(binary.length);
6821
+ for (let i = 0; i < binary.length; i += 1) {
6822
+ out[i] = binary.charCodeAt(i);
6823
+ }
6824
+ return out;
6825
+ }
6826
+ if (typeof Buffer !== "undefined") {
6827
+ return Uint8Array.from(Buffer.from(value, "base64"));
6828
+ }
6829
+ throw new Error("No base64 decoder available in current environment");
6830
+ }
6831
+
6832
+ // src/server/auth/jwtVerifier.ts
6833
+ var DEFAULT_CLOCK_SKEW_MS = 5e3;
6834
+ function matchesAuthorizedParty(azp, patterns) {
6835
+ if (!azp) return true;
6836
+ for (const pattern of patterns) {
6837
+ if (pattern === azp) return true;
6838
+ if (pattern.includes("*")) {
6839
+ const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
6840
+ const regexPattern = escaped.replace(/\*/g, "[^/]*");
6841
+ const regex = new RegExp(`^${regexPattern}$`);
6842
+ if (regex.test(azp)) return true;
6843
+ if (pattern.includes("*.")) {
6844
+ const rootPattern = pattern.replace("*.", "");
6845
+ if (rootPattern === azp) return true;
6846
+ }
6847
+ }
6848
+ }
6849
+ return false;
6850
+ }
6851
+ var keyCache = /* @__PURE__ */ new Map();
6852
+ async function getPublicKey(pem) {
6853
+ const cached = keyCache.get(pem);
6854
+ if (cached) return cached;
6855
+ const normalized = pem.replace(/\\n/g, "\n");
6856
+ const key = await importSPKI(normalized, "ES256");
6857
+ keyCache.set(pem, key);
6858
+ return key;
6859
+ }
6860
+ async function verifyJwt(config, token) {
6861
+ if (!token) {
6862
+ return { result: null };
6863
+ }
6864
+ try {
6865
+ const publicKey = await getPublicKey(config.publicKey);
6866
+ const audience = normalizeArray(config.audience);
6867
+ const clockTolerance = Math.floor(
6868
+ (config.clockSkewMs ?? DEFAULT_CLOCK_SKEW_MS) / 1e3
6869
+ );
6870
+ const { payload } = await jwtVerify(token, publicKey, {
6871
+ issuer: config.issuer,
6872
+ audience: audience && audience.length === 1 ? audience[0] : audience,
6873
+ clockTolerance
6874
+ });
6875
+ const claims = payload;
6876
+ if (!claims.sub) {
6877
+ throw new Error("JWT verification succeeded but subject claim is missing");
6878
+ }
6879
+ const authorizedParties = normalizeArray(config.authorizedParties);
6880
+ if (authorizedParties) {
6881
+ const azp = claims.azp;
6882
+ if (!matchesAuthorizedParty(azp, authorizedParties)) {
6883
+ throw new Error(
6884
+ `Authorized party '${azp}' does not match any allowed pattern`
6885
+ );
6886
+ }
6887
+ }
6888
+ return {
6889
+ result: {
6890
+ userId: claims.sub,
6891
+ claims
6892
+ }
6893
+ };
6894
+ } catch (error) {
6895
+ return {
6896
+ result: null,
6897
+ error,
6898
+ debug: decodeJwtPayload(token)
6899
+ };
6900
+ }
6901
+ }
6902
+
6903
+ // src/server/auth/internalAuth.ts
6904
+ var nodeCryptoPromise = null;
6905
+ async function loadNodeCrypto() {
6906
+ if (!nodeCryptoPromise) {
6907
+ nodeCryptoPromise = import("crypto").then((module) => module).catch(() => null);
6908
+ }
6909
+ return nodeCryptoPromise;
6910
+ }
6911
+ function hasSubtleCrypto() {
6912
+ return typeof globalThis.crypto?.subtle?.importKey === "function";
6913
+ }
6914
+ async function computeHmacHex(secret, payload) {
6915
+ if (hasSubtleCrypto()) {
6916
+ const encoder = new TextEncoder();
6917
+ const key = await crypto.subtle.importKey(
6918
+ "raw",
6919
+ encoder.encode(secret),
6920
+ { name: "HMAC", hash: "SHA-256" },
6921
+ false,
6922
+ ["sign"]
6923
+ );
6924
+ const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(payload));
6925
+ return bufferToHex(signature);
6926
+ }
6927
+ const nodeCrypto = await loadNodeCrypto();
6928
+ if (!nodeCrypto) {
6929
+ throw new Error("Crypto implementation unavailable for HMAC computation");
6930
+ }
6931
+ return nodeCrypto.createHmac("sha256", secret).update(payload).digest("hex");
6932
+ }
6933
+ async function timingSafeEqualHex(a, b) {
6934
+ if (a.length !== b.length) {
6935
+ return false;
6936
+ }
6937
+ const nodeCrypto = await loadNodeCrypto();
6938
+ if (nodeCrypto?.timingSafeEqual) {
6939
+ const aBuf = Buffer.from(a, "hex");
6940
+ const bBuf = Buffer.from(b, "hex");
6941
+ try {
6942
+ return nodeCrypto.timingSafeEqual(aBuf, bBuf);
6943
+ } catch {
6944
+ return false;
6945
+ }
6946
+ }
6947
+ let diff = 0;
6948
+ for (let i = 0; i < a.length; i += 1) {
6949
+ diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
6950
+ }
6951
+ return diff === 0;
6952
+ }
6953
+
6954
+ // src/server/auth/betterAuth.ts
6955
+ import { betterAuth } from "better-auth";
6956
+ import { organization, twoFactor } from "better-auth/plugins";
6957
+ function createDeepSpaceAuth(config) {
6958
+ const socialProviders = {};
6959
+ if (config.google) {
6960
+ socialProviders.google = config.google;
6961
+ }
6962
+ if (config.github) {
6963
+ socialProviders.github = config.github;
6964
+ }
6965
+ return betterAuth({
6966
+ database: config.database,
6967
+ baseURL: config.baseURL,
6968
+ secret: config.secret,
6969
+ emailAndPassword: {
6970
+ enabled: config.emailAndPassword ?? true
6971
+ },
6972
+ socialProviders,
6973
+ trustedOrigins: config.trustedOrigins ?? [
6974
+ "https://deep.space",
6975
+ "https://*.deep.space",
6976
+ "https://*.app.space",
6977
+ "http://localhost:*"
6978
+ ],
6979
+ plugins: [organization(), twoFactor()]
6980
+ });
6981
+ }
6947
6982
  export {
6948
6983
  AI_CHATS_SCHEMA,
6949
6984
  AI_MESSAGES_SCHEMA,
@@ -6964,7 +6999,6 @@ export {
6964
6999
  CronRoom,
6965
7000
  DEFAULT_CONTEXT_CONFIG,
6966
7001
  DEFAULT_DO_MANIFEST,
6967
- DEFAULT_MAX_SKEW_MS,
6968
7002
  DIRECTORY_SCHEMAS,
6969
7003
  GLOBAL_DO_TYPES,
6970
7004
  GLOBAL_DO_TYPE_NAMES,
@@ -7002,7 +7036,6 @@ export {
7002
7036
  authWorkerFetch,
7003
7037
  bindingManifestFromOutputConfig,
7004
7038
  buildCronContext,
7005
- buildInternalPayload,
7006
7039
  buildTableSelect,
7007
7040
  buildUiParts,
7008
7041
  canCreate,
@@ -7061,7 +7094,6 @@ export {
7061
7094
  rowToData,
7062
7095
  runMigrations,
7063
7096
  serverBuild,
7064
- signInternalPayload,
7065
7097
  timingSafeEqualHex,
7066
7098
  toUint8Array,
7067
7099
  totalChars,
@@ -7072,7 +7104,6 @@ export {
7072
7104
  validateAppName,
7073
7105
  validateBindingManifest,
7074
7106
  validateDoManifest,
7075
- verifyInternalSignature,
7076
7107
  verifyJwt,
7077
7108
  workspaceAccountsSchema,
7078
7109
  workspaceContentSharesSchema,