deepspace 0.3.8 → 0.3.9

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
@@ -473,14 +473,50 @@ var BASE_USERS_SCHEMA = {
473
473
  admin: { read: true, create: false, update: true, delete: true }
474
474
  }
475
475
  };
476
+ function lintSchema(schema) {
477
+ const warnings = [];
478
+ const VISIBILITY_AWARE = ["published", "shared"];
479
+ if (schema.visibilityField) {
480
+ const offendingRoles = Object.entries(schema.permissions).filter(([, perms]) => perms.read === true).map(([role]) => role);
481
+ if (offendingRoles.length > 0) {
482
+ const usesAware = Object.values(schema.permissions).some(
483
+ (p) => typeof p.read === "string" && VISIBILITY_AWARE.includes(p.read)
484
+ );
485
+ if (!usesAware) {
486
+ warnings.push(
487
+ `[${schema.name}] visibilityField is declared but no role uses read: 'published' or 'shared'. Roles with read: true (${offendingRoles.join(", ")}) will see every row regardless of visibility. Change those to read: 'published' (owner OR public) or 'shared' (owner OR collaborator OR public) to actually enforce the filter, or remove visibilityField if you don't intend to gate reads by it.`
488
+ );
489
+ }
490
+ }
491
+ }
492
+ if (schema.ownerField) {
493
+ const col = schema.columns.find((c) => c.name === schema.ownerField);
494
+ if (col && !col.userBound) {
495
+ warnings.push(
496
+ `[${schema.name}] ownerField is '${schema.ownerField}' but that column is not marked userBound: true. A client can create a row with someone else's id in this field, bypassing 'own' permission checks. Add userBound: true (and ideally immutable: true) to the column.`
497
+ );
498
+ }
499
+ }
500
+ for (const col of schema.columns) {
501
+ if (col.userBound && col.storage !== "text") {
502
+ warnings.push(
503
+ `[${schema.name}] column '${col.name}' is userBound but storage is '${col.storage}'. userBound stamps the user id (a string); use storage: 'text'.`
504
+ );
505
+ }
506
+ }
507
+ return warnings;
508
+ }
476
509
  var SchemaRegistry = class {
477
510
  trusted = /* @__PURE__ */ new Map();
478
511
  constructor(schemas = []) {
479
512
  for (const schema of schemas) {
480
- this.trusted.set(schema.name, schema);
513
+ this.registerTrusted(schema);
481
514
  }
482
515
  }
483
516
  registerTrusted(schema) {
517
+ for (const w of lintSchema(schema)) {
518
+ console.warn(`[schema-lint] ${w}`);
519
+ }
484
520
  this.trusted.set(schema.name, schema);
485
521
  }
486
522
  get(name) {
@@ -1579,6 +1615,9 @@ function executeTableQuery(ctx, query, schema, userId, userRole, isSystem, skipU
1579
1615
  if (col) {
1580
1616
  whereClauses.push(`"${col.id}" = ?`);
1581
1617
  params.push(value);
1618
+ } else if (fieldName === "recordId") {
1619
+ whereClauses.push(`_row_id = ?`);
1620
+ params.push(value);
1582
1621
  } else if (fieldName === "createdBy") {
1583
1622
  whereClauses.push(`_created_by = ?`);
1584
1623
  params.push(value);
@@ -2111,11 +2150,11 @@ async function handleToolExecute(ctx, request) {
2111
2150
  getPermissionContext: ctx.getPermissionContext,
2112
2151
  send: ctx.send
2113
2152
  };
2114
- const result = executeTool(recordCtx, tool, params, userId, userRole, isAppAction);
2153
+ const result = await executeTool(recordCtx, tool, params, userId, userRole, isAppAction);
2115
2154
  const status = result.success ? 200 : result.error?.includes("not found") ? 404 : 400;
2116
2155
  return Response.json(result, { status, headers: CORS_HEADERS });
2117
2156
  }
2118
- function executeTool(ctx, toolName, params, userId, userRole, skipUserRbac) {
2157
+ async function executeTool(ctx, toolName, params, userId, userRole, skipUserRbac) {
2119
2158
  switch (toolName) {
2120
2159
  // ---- Records ----
2121
2160
  case "records.query": {
@@ -2162,6 +2201,33 @@ function executeTool(ctx, toolName, params, userId, userRole, skipUserRbac) {
2162
2201
  if (!recordId) return { success: false, error: "Missing required param: recordId" };
2163
2202
  return deleteRecord(ctx, collection, recordId, userId, userRole, skipUserRbac);
2164
2203
  }
2204
+ // ---- Users ----
2205
+ // Bootstrap or refresh the `users` row for a given userId. Same path
2206
+ // the SDK uses on WS connect via registerUser, exposed for server
2207
+ // contexts (CLI-only callers, action handlers) where there's no WS
2208
+ // connection to trigger the natural registration. System-managed
2209
+ // columns (email, name, imageUrl, role) are written through
2210
+ // registerUser's privileged path, bypassing the SYSTEM_MANAGED
2211
+ // stripping that blocks naive tools.create / tools.update.
2212
+ case "users.register": {
2213
+ const targetUserId = params.userId || userId;
2214
+ if (!targetUserId) return { success: false, error: "Missing userId" };
2215
+ const name = typeof params.name === "string" ? params.name : "";
2216
+ const email = typeof params.email === "string" ? params.email : "";
2217
+ const imageUrl = typeof params.imageUrl === "string" ? params.imageUrl : void 0;
2218
+ const isAdmin = params.isAdmin === true;
2219
+ const user = await registerUser(
2220
+ ctx.sql,
2221
+ targetUserId,
2222
+ name,
2223
+ email,
2224
+ imageUrl,
2225
+ isAdmin,
2226
+ "member",
2227
+ ctx.schemaRegistry
2228
+ );
2229
+ return { success: true, data: { user } };
2230
+ }
2165
2231
  // ---- Schema ----
2166
2232
  case "schema.list": {
2167
2233
  const schemas = ctx.schemaRegistry.all();
@@ -2994,6 +3060,7 @@ import * as Y4 from "yjs";
2994
3060
  var YjsRoom = class extends BaseRoom {
2995
3061
  doc = null;
2996
3062
  initialized = false;
3063
+ awarenessStates = /* @__PURE__ */ new Map();
2997
3064
  constructor(state, env) {
2998
3065
  super(state, env);
2999
3066
  }
@@ -3062,6 +3129,7 @@ var YjsRoom = class extends BaseRoom {
3062
3129
  ws.send(toUint8Array(step2).buffer);
3063
3130
  }
3064
3131
  ws.send(JSON.stringify({ type: "auth", canWrite }));
3132
+ this.sendAwarenessSnapshot(ws);
3065
3133
  return attachment;
3066
3134
  }
3067
3135
  onMessage(ws, user, message) {
@@ -3078,18 +3146,17 @@ var YjsRoom = class extends BaseRoom {
3078
3146
  }
3079
3147
  onDisconnect(ws, user) {
3080
3148
  const attachment = ws.deserializeAttachment();
3081
- if (!attachment?.awarenessClientId) return;
3149
+ if (attachment?.awarenessClientId == null) return;
3082
3150
  const clientId = attachment.awarenessClientId;
3083
- const stateJson = new TextEncoder().encode("null");
3084
- const inner = createEncoder();
3085
- writeVarUint(inner, clientId);
3086
- writeVarUint(inner, 16777215);
3087
- writeVarUint8Array(inner, stateJson);
3088
- const outer = createEncoder();
3089
- writeVarUint(outer, MSG_AWARENESS);
3090
- writeVarUint(outer, 1);
3091
- for (const b of inner.data) outer.data.push(b);
3092
- const msg = toUint8Array(outer);
3151
+ const prevClock = this.awarenessStates.get(clientId)?.clock ?? 0;
3152
+ this.awarenessStates.delete(clientId);
3153
+ const msg = this.encodeAwarenessMessage([
3154
+ {
3155
+ clientId,
3156
+ clock: prevClock + 1,
3157
+ stateBytes: new TextEncoder().encode("null")
3158
+ }
3159
+ ]);
3093
3160
  for (const peer of this.state.getWebSockets()) {
3094
3161
  if (peer === ws) continue;
3095
3162
  try {
@@ -3127,20 +3194,66 @@ var YjsRoom = class extends BaseRoom {
3127
3194
  }
3128
3195
  }
3129
3196
  handleAwareness(ws, decoder, rawMessage) {
3197
+ const updates = this.readAwarenessUpdates(decoder);
3198
+ if (updates.length === 0) return;
3130
3199
  const attachment = ws.deserializeAttachment();
3131
3200
  if (attachment && attachment.awarenessClientId === null) {
3132
- try {
3133
- const savedPos = decoder.pos;
3134
- const _len = readVarUint(decoder);
3135
- const clientId = readVarUint(decoder);
3136
- decoder.pos = savedPos;
3137
- attachment.awarenessClientId = clientId;
3138
- ws.serializeAttachment(attachment);
3139
- } catch {
3201
+ attachment.awarenessClientId = updates[0].clientId;
3202
+ ws.serializeAttachment(attachment);
3203
+ }
3204
+ for (const update of updates) {
3205
+ const existing = this.awarenessStates.get(update.clientId);
3206
+ if (existing && existing.clock >= update.clock) continue;
3207
+ if (new TextDecoder().decode(update.stateBytes) === "null") {
3208
+ this.awarenessStates.delete(update.clientId);
3209
+ } else {
3210
+ this.awarenessStates.set(update.clientId, {
3211
+ clock: update.clock,
3212
+ stateBytes: update.stateBytes
3213
+ });
3140
3214
  }
3141
3215
  }
3142
3216
  this.broadcastRaw(ws, rawMessage);
3143
3217
  }
3218
+ readAwarenessUpdates(decoder) {
3219
+ const update = readVarUint8Array(decoder);
3220
+ const updateDecoder = createDecoder(update);
3221
+ const count = readVarUint(updateDecoder);
3222
+ const updates = [];
3223
+ for (let i = 0; i < count; i++) {
3224
+ const clientId = readVarUint(updateDecoder);
3225
+ const clock = readVarUint(updateDecoder);
3226
+ const stateBytes = readVarUint8Array(updateDecoder);
3227
+ updates.push({ clientId, clock, stateBytes });
3228
+ }
3229
+ return updates;
3230
+ }
3231
+ encodeAwarenessMessage(updates) {
3232
+ const inner = createEncoder();
3233
+ writeVarUint(inner, updates.length);
3234
+ for (const update of updates) {
3235
+ writeVarUint(inner, update.clientId);
3236
+ writeVarUint(inner, update.clock);
3237
+ writeVarUint8Array(inner, update.stateBytes);
3238
+ }
3239
+ const outer = createEncoder();
3240
+ writeVarUint(outer, MSG_AWARENESS);
3241
+ writeVarUint8Array(outer, toUint8Array(inner));
3242
+ return toUint8Array(outer);
3243
+ }
3244
+ sendAwarenessSnapshot(ws) {
3245
+ if (this.awarenessStates.size === 0) return;
3246
+ const updates = Array.from(this.awarenessStates, ([clientId, entry]) => ({
3247
+ clientId,
3248
+ clock: entry.clock,
3249
+ stateBytes: entry.stateBytes
3250
+ }));
3251
+ const msg = this.encodeAwarenessMessage(updates);
3252
+ try {
3253
+ ws.send(msg.buffer);
3254
+ } catch {
3255
+ }
3256
+ }
3144
3257
  // --------------------------------------------------------------------------
3145
3258
  // Broadcasting
3146
3259
  // --------------------------------------------------------------------------
@@ -3668,10 +3781,35 @@ var PresenceRoom = class extends BaseRoom {
3668
3781
  constructor(state, env) {
3669
3782
  super(state, env);
3670
3783
  }
3784
+ /**
3785
+ * Durable Objects can hibernate and clear heap while Cloudflare keeps
3786
+ * WebSocket connections. Deserialize attachments from already-connected
3787
+ * sockets so `peers` matches reality before we send PRESENCE_SYNC.
3788
+ */
3789
+ hydratePeersFromLiveSockets(connectingWs) {
3790
+ for (const existing of this.state.getWebSockets()) {
3791
+ if (existing === connectingWs) continue;
3792
+ const att = existing.deserializeAttachment();
3793
+ if (!att?.userId) continue;
3794
+ if (this.peers.has(att.userId)) continue;
3795
+ const joinedAt = typeof att.joinedAt === "string" ? att.joinedAt : (/* @__PURE__ */ new Date()).toISOString();
3796
+ const recovered = {
3797
+ userId: att.userId,
3798
+ userName: att.userName,
3799
+ userEmail: att.userEmail,
3800
+ userImageUrl: att.userImageUrl,
3801
+ joinedAt,
3802
+ state: {}
3803
+ };
3804
+ this.peers.set(att.userId, recovered);
3805
+ this.peerSockets.set(att.userId, existing);
3806
+ }
3807
+ }
3671
3808
  // --------------------------------------------------------------------------
3672
3809
  // BaseRoom Lifecycle
3673
3810
  // --------------------------------------------------------------------------
3674
3811
  onConnect(ws, user) {
3812
+ this.hydratePeersFromLiveSockets(ws);
3675
3813
  const now = (/* @__PURE__ */ new Date()).toISOString();
3676
3814
  const peer = {
3677
3815
  userId: user.userId,
@@ -3698,8 +3836,21 @@ var PresenceRoom = class extends BaseRoom {
3698
3836
  const { type, payload } = message;
3699
3837
  switch (type) {
3700
3838
  case MSG.PRESENCE_UPDATE: {
3701
- const peer = this.peers.get(user.userId);
3702
- if (!peer) break;
3839
+ let peer = this.peers.get(user.userId);
3840
+ if (!peer) {
3841
+ const att = user;
3842
+ const joinedAt = typeof att.joinedAt === "string" ? att.joinedAt : (/* @__PURE__ */ new Date()).toISOString();
3843
+ peer = {
3844
+ userId: user.userId,
3845
+ userName: user.userName,
3846
+ userEmail: user.userEmail,
3847
+ userImageUrl: user.userImageUrl,
3848
+ joinedAt,
3849
+ state: {}
3850
+ };
3851
+ this.peers.set(user.userId, peer);
3852
+ this.peerSockets.set(user.userId, ws);
3853
+ }
3703
3854
  peer.state = { ...peer.state, ...payload };
3704
3855
  this.peers.set(user.userId, peer);
3705
3856
  this.broadcast(
@@ -5136,8 +5287,27 @@ async function handleUpload(request, url, bucket, prefix, userId) {
5136
5287
  fileName = body.name;
5137
5288
  mimeType = body.mimeType || "application/octet-stream";
5138
5289
  }
5139
- const fileKey = `${Date.now()}-${Math.random().toString(36).slice(2, 11)}-${fileName}`;
5140
- const key = `${prefix}${fileKey}`;
5290
+ const requestedKey = url.searchParams.get("key");
5291
+ let key;
5292
+ if (requestedKey !== null) {
5293
+ const safe = sanitizeSubpath(requestedKey);
5294
+ if (safe === null) {
5295
+ return Response.json(
5296
+ { error: "Invalid key: traversal not allowed" },
5297
+ { status: 400, headers: CORS_HEADERS3 }
5298
+ );
5299
+ }
5300
+ if (!safe) {
5301
+ return Response.json(
5302
+ { error: "Invalid key: empty" },
5303
+ { status: 400, headers: CORS_HEADERS3 }
5304
+ );
5305
+ }
5306
+ key = `${prefix}${safe}`;
5307
+ } else {
5308
+ const fileKey = `${Date.now()}-${Math.random().toString(36).slice(2, 11)}-${fileName}`;
5309
+ key = `${prefix}${fileKey}`;
5310
+ }
5141
5311
  await bucket.put(key, fileData, {
5142
5312
  httpMetadata: { contentType: mimeType },
5143
5313
  customMetadata: {
@@ -5442,9 +5612,10 @@ function createDeepSpaceAuth(config) {
5442
5612
  }
5443
5613
 
5444
5614
  // src/server/utils/cron.ts
5615
+ var DEFAULT_API_BASE_URL = "https://deepspace-api.eudaimonicincorporated.workers.dev";
5445
5616
  function buildCronContext(env, ownerUserId, roomId = "default") {
5446
5617
  const hmacSecret = env.INTERNAL_STORAGE_HMAC_SECRET;
5447
- const apiBase = env.API_BASE_URL || "https://api.deep.space";
5618
+ const apiBase = env.API_BASE_URL || DEFAULT_API_BASE_URL;
5448
5619
  const roomIdObj = env.RECORD_ROOMS.idFromName(roomId);
5449
5620
  const room = env.RECORD_ROOMS.get(roomIdObj);
5450
5621
  async function executeTool3(tool, params) {
@@ -5659,6 +5830,36 @@ function createDeepSpaceAI(env, provider, options = {}) {
5659
5830
  }
5660
5831
  }
5661
5832
 
5833
+ // src/server/utils/screenshot.ts
5834
+ async function captureScreenshot(env, opts) {
5835
+ const headers = new Headers({
5836
+ "content-type": "application/json",
5837
+ "x-app-identity-token": env.APP_IDENTITY_TOKEN,
5838
+ "x-app-name": env.APP_NAME
5839
+ });
5840
+ const res = await platformWorkerFetch(
5841
+ env,
5842
+ new Request("https://platform-internal/internal/screenshot", {
5843
+ method: "POST",
5844
+ headers,
5845
+ body: JSON.stringify(opts)
5846
+ })
5847
+ );
5848
+ if (!res.ok) {
5849
+ let detail = "";
5850
+ try {
5851
+ detail = (await res.text()).slice(0, 300);
5852
+ } catch {
5853
+ }
5854
+ console.warn(`[captureScreenshot] platform returned ${res.status}:`, detail);
5855
+ return null;
5856
+ }
5857
+ return {
5858
+ body: await res.arrayBuffer(),
5859
+ contentType: res.headers.get("content-type") || "image/png"
5860
+ };
5861
+ }
5862
+
5662
5863
  // src/server/utils/chat-context.ts
5663
5864
  import { generateText } from "ai";
5664
5865
  var DEFAULT_CONTEXT_CONFIG = {
@@ -6105,8 +6306,12 @@ var VECTORIZE_QUERIED_USD_PER_M_DIMS = 0.01;
6105
6306
  var VECTORIZE_STORED_USD_PER_100M_DIMS = 0.05;
6106
6307
  var COST_RATES = {
6107
6308
  ai: {
6108
- /** USD per character (input or output). */
6109
- perChar: AI_EMBED_USD_PER_M_TOKENS / 1e6 / CHARS_PER_TOKEN
6309
+ /**
6310
+ * USD per character of *embedding input* (bge-m3 / qwen3-embedding tier).
6311
+ * Renamed from `perChar` to make explicit that this rate does NOT
6312
+ * apply to LLM-generation output — see `priceBindingUsageEvent`.
6313
+ */
6314
+ embedInputPerChar: AI_EMBED_USD_PER_M_TOKENS / 1e6 / CHARS_PER_TOKEN
6110
6315
  },
6111
6316
  vectorize: {
6112
6317
  /** USD per queried dimension (per query, per stored vector compared). */
@@ -6115,6 +6320,15 @@ var COST_RATES = {
6115
6320
  storedPerDimPerMonth: VECTORIZE_STORED_USD_PER_100M_DIMS / 1e8
6116
6321
  }
6117
6322
  };
6323
+ function priceBindingUsageEvent(kind, op, units) {
6324
+ if (kind === "ai" && op === "input") {
6325
+ return units * COST_RATES.ai.embedInputPerChar;
6326
+ }
6327
+ if (kind === "vectorize" && op === "query") {
6328
+ return units * COST_RATES.vectorize.queriedPerDim;
6329
+ }
6330
+ return 0;
6331
+ }
6118
6332
 
6119
6333
  // src/server/utils/d1-migrations.ts
6120
6334
  var META_TABLE = "_dpc_migrations";
@@ -6210,6 +6424,7 @@ export {
6210
6424
  canRead,
6211
6425
  canUpdate,
6212
6426
  capToolResultSize,
6427
+ captureScreenshot,
6213
6428
  checkFieldPermissions,
6214
6429
  clientBuild,
6215
6430
  coerceValue,
@@ -6242,6 +6457,7 @@ export {
6242
6457
  hasContent,
6243
6458
  isAutoProvision,
6244
6459
  isOwner,
6460
+ lintSchema,
6245
6461
  loadMessages,
6246
6462
  makeDefaultSummarizer,
6247
6463
  meterAi,
@@ -6250,6 +6466,7 @@ export {
6250
6466
  noopPermissionContext,
6251
6467
  platformWorkerFetch,
6252
6468
  prepareMessagesWithCompaction,
6469
+ priceBindingUsageEvent,
6253
6470
  readVarUint,
6254
6471
  readVarUint8Array,
6255
6472
  resolveAppName,