deepspace 0.19.5 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/{chunk-A7UFIOWO.js → chunk-KDCDRHBA.js} +7 -2
  3. package/dist/chunk-KDCDRHBA.js.map +1 -0
  4. package/dist/cli.js +219 -94
  5. package/dist/cli.js.map +1 -1
  6. package/dist/documentation-client-core.d.ts +3 -2
  7. package/dist/documentation-client-core.js +1 -1
  8. package/dist/documentation-default-renderer.cjs +48 -48
  9. package/dist/documentation-react.d.ts +2 -1
  10. package/dist/documentation-react.js +1 -1
  11. package/dist/documentation-runtime.js +7 -7
  12. package/dist/documentation-server-core.d.ts +3 -2
  13. package/dist/documentation-server-core.js +1 -1
  14. package/dist/documentation.d.ts +40 -12
  15. package/dist/documentation.js +40 -7
  16. package/dist/documentation.js.map +1 -1
  17. package/dist/index.d.ts +225 -21
  18. package/dist/index.js +262 -112
  19. package/dist/index.js.map +1 -1
  20. package/dist/{public-CFAj1hL2.d.ts → public-DenQ1TsZ.d.ts} +36 -11
  21. package/dist/server.d.ts +46 -29
  22. package/dist/server.js +1041 -254
  23. package/dist/server.js.map +1 -1
  24. package/dist/testing-mcp.d.ts +69 -0
  25. package/dist/testing-mcp.js +99 -0
  26. package/dist/testing-mcp.js.map +1 -0
  27. package/dist/testing.d.ts +17 -20
  28. package/dist/testing.js +47 -25
  29. package/dist/testing.js.map +1 -1
  30. package/dist/worker.d.ts +73 -32
  31. package/dist/worker.js +1094 -244
  32. package/dist/worker.js.map +1 -1
  33. package/features/ai-chat/feature.json +2 -2
  34. package/features/ai-chat/src/AiChatPage.tsx +56 -21
  35. package/features/ai-chat/src/ChatPanel.tsx +33 -10
  36. package/features/ai-chat/src/__tests__/ChatPanel.lifecycle.test.tsx +156 -0
  37. package/features/cron/src/CronLogPage.tsx +107 -69
  38. package/features/file-attachments/feature.json +2 -1
  39. package/features/file-manager/feature.json +2 -1
  40. package/features/file-manager/src/FileManagerPage.tsx +227 -194
  41. package/features/integration-test/src/IntegrationTestPage.tsx +54 -34
  42. package/package.json +5 -1
  43. package/dist/chunk-A7UFIOWO.js.map +0 -1
package/dist/index.js CHANGED
@@ -587,6 +587,9 @@ var MSG = {
587
587
  CRON_PAUSE: "cron.pause",
588
588
  CRON_RESUME: "cron.resume",
589
589
  CRON_STATUS: "cron.status",
590
+ // Per-mutation receipt, sent only to the socket whose trigger/pause/
591
+ // resume carried a `requestId` — frames without one stay fire-and-forget.
592
+ CRON_ACK: "cron.ack",
590
593
  // ---- JobRoom ---------------------------------------------------------
591
594
  // Background-job queue with durable, alarm-driven execution. See
592
595
  // server/rooms/job-room.ts. Clients enqueue and subscribe to updates;
@@ -643,9 +646,9 @@ var clientBuild = {
643
646
  canvasUndo: () => ({ type: MSG.CANVAS_UNDO, payload: EMPTY }),
644
647
  canvasRedo: () => ({ type: MSG.CANVAS_REDO, payload: EMPTY }),
645
648
  // Cron
646
- cronTrigger: (taskName) => ({ type: MSG.CRON_TRIGGER, payload: { taskName } }),
647
- cronPause: (taskName) => ({ type: MSG.CRON_PAUSE, payload: { taskName } }),
648
- cronResume: (taskName) => ({ type: MSG.CRON_RESUME, payload: { taskName } }),
649
+ cronTrigger: (taskName, requestId) => ({ type: MSG.CRON_TRIGGER, payload: { taskName, requestId } }),
650
+ cronPause: (taskName, requestId) => ({ type: MSG.CRON_PAUSE, payload: { taskName, requestId } }),
651
+ cronResume: (taskName, requestId) => ({ type: MSG.CRON_RESUME, payload: { taskName, requestId } }),
649
652
  // Jobs
650
653
  jobEnqueue: (requestId, type, payload, maxAttempts) => ({
651
654
  type: MSG.JOB_ENQUEUE,
@@ -699,6 +702,14 @@ var serverBuild = {
699
702
  cronTasks: (tasks) => ({ type: MSG.CRON_TASKS, payload: { tasks } }),
700
703
  cronHistory: (history) => ({ type: MSG.CRON_HISTORY, payload: { history } }),
701
704
  cronStatus: (tasks, recentHistory) => ({ type: MSG.CRON_STATUS, payload: { tasks, recentHistory } }),
705
+ cronAckSuccess: (requestId, taskName) => ({
706
+ type: MSG.CRON_ACK,
707
+ payload: { requestId, taskName, ok: true }
708
+ }),
709
+ cronAckFailure: (requestId, reason, error, taskName) => ({
710
+ type: MSG.CRON_ACK,
711
+ payload: { requestId, taskName, ok: false, reason, error }
712
+ }),
702
713
  // Jobs
703
714
  jobSnapshot: (jobs) => ({ type: MSG.JOB_UPDATE, payload: { kind: "snapshot", jobs } }),
704
715
  jobEnqueued: (job, requestId) => ({
@@ -950,7 +961,10 @@ import { useState } from "react";
950
961
  import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
951
962
  function AuthOverlay({
952
963
  onClose,
953
- providers = ["github", "google"]
964
+ providers = ["github", "google"],
965
+ title = "Sign in to DeepSpace",
966
+ description = "Sync your data across devices",
967
+ logo
954
968
  } = {}) {
955
969
  const { isLoaded, isSignedIn } = useAuth();
956
970
  const [showEmailForm, setShowEmailForm] = useState(false);
@@ -996,7 +1010,7 @@ function AuthOverlay({
996
1010
  onClick: onClose ? (e) => {
997
1011
  if (e.target === e.currentTarget) onClose();
998
1012
  } : void 0,
999
- className: "fixed inset-0 z-[99999] flex items-center justify-center bg-black/55 backdrop-blur-2xl",
1013
+ className: "fixed inset-0 z-[99999] flex items-center justify-center bg-[var(--ds-auth-backdrop,#0000008c)] backdrop-blur-2xl",
1000
1014
  style: {
1001
1015
  opacity: 0,
1002
1016
  animation: "ds-auth-overlay-in 0.4s ease forwards"
@@ -1020,8 +1034,9 @@ function AuthOverlay({
1020
1034
  children: "\u2715"
1021
1035
  }
1022
1036
  ),
1023
- /* @__PURE__ */ jsx2("h2", { className: "mb-2 text-[22px] font-bold leading-snug text-foreground", children: "Sign in to DeepSpace" }),
1024
- /* @__PURE__ */ jsx2("p", { className: "mb-6 text-sm leading-snug text-muted-foreground", children: "Sync your data across devices" }),
1037
+ logo && /* @__PURE__ */ jsx2("div", { className: "mb-4 flex justify-center", children: logo }),
1038
+ /* @__PURE__ */ jsx2("h2", { className: "mb-2 text-[22px] font-bold leading-snug text-foreground", children: title }),
1039
+ /* @__PURE__ */ jsx2("p", { className: "mb-6 text-sm leading-snug text-muted-foreground", children: description }),
1025
1040
  /* @__PURE__ */ jsxs2("div", { className: "flex flex-col gap-2", children: [
1026
1041
  providers.includes("github") && /* @__PURE__ */ jsx2(OAuthButton, { onClick: () => handleSocial("github"), disabled: loading, icon: /* @__PURE__ */ jsx2(GitHubIcon, {}), children: "Continue with GitHub" }),
1027
1042
  providers.includes("google") && /* @__PURE__ */ jsx2(OAuthButton, { onClick: () => handleSocial("google"), disabled: loading, icon: /* @__PURE__ */ jsx2(GoogleIcon, {}), children: "Continue with Google" })
@@ -3221,11 +3236,17 @@ function useCronMonitor(roomId) {
3221
3236
  const [history, setHistory] = useState9([]);
3222
3237
  const [connected, setConnected] = useState9(false);
3223
3238
  const [canWrite, setCanWrite] = useState9(false);
3239
+ const [lastError, setLastError] = useState9(null);
3224
3240
  const wsRef = useRef10(null);
3241
+ const pendingRef = useRef10(/* @__PURE__ */ new Map());
3225
3242
  useEffect12(() => {
3226
3243
  let ws = null;
3227
3244
  let reconnectTimer = null;
3228
3245
  let alive = true;
3246
+ const drainPending = (result) => {
3247
+ for (const resolve of pendingRef.current.values()) resolve(result);
3248
+ pendingRef.current.clear();
3249
+ };
3229
3250
  const connect = async () => {
3230
3251
  if (!alive) return;
3231
3252
  const token = await getAuthToken();
@@ -3250,6 +3271,17 @@ function useCronMonitor(roomId) {
3250
3271
  [MSG.CRON_STATUS]: (p) => {
3251
3272
  setTasks(p.tasks);
3252
3273
  setHistory(p.recentHistory);
3274
+ },
3275
+ [MSG.CRON_ACK]: (p) => {
3276
+ const resolve = pendingRef.current.get(p.requestId);
3277
+ if (!resolve) return;
3278
+ pendingRef.current.delete(p.requestId);
3279
+ resolve(
3280
+ p.ok ? { ok: true, taskName: p.taskName, requestId: p.requestId } : { ok: false, reason: p.reason, error: p.error }
3281
+ );
3282
+ },
3283
+ [MSG.ERROR]: (p) => {
3284
+ setLastError(p.error);
3253
3285
  }
3254
3286
  });
3255
3287
  };
@@ -3257,6 +3289,7 @@ function useCronMonitor(roomId) {
3257
3289
  wsRef.current = null;
3258
3290
  setConnected(false);
3259
3291
  setCanWrite(false);
3292
+ drainPending({ ok: false, reason: "not_connected" });
3260
3293
  if (alive) reconnectTimer = setTimeout(connect, 1e3);
3261
3294
  };
3262
3295
  ws.onerror = () => ws?.close();
@@ -3265,32 +3298,49 @@ function useCronMonitor(roomId) {
3265
3298
  return () => {
3266
3299
  alive = false;
3267
3300
  if (reconnectTimer) clearTimeout(reconnectTimer);
3301
+ drainPending({ ok: false, reason: "not_connected" });
3268
3302
  ws?.close();
3269
3303
  wsRef.current = null;
3270
3304
  };
3271
3305
  }, [roomId]);
3272
3306
  const sendWrite = useCallback11(
3273
- (message) => {
3274
- if (!canWrite) return;
3307
+ (build) => {
3308
+ setLastError(null);
3309
+ if (!canWrite) return Promise.resolve({ ok: false, reason: "read_only" });
3275
3310
  const ws = wsRef.current;
3276
- if (!ws || ws.readyState !== WebSocket.OPEN) return;
3277
- ws.send(encode(message));
3311
+ if (!ws || ws.readyState !== WebSocket.OPEN) {
3312
+ return Promise.resolve({ ok: false, reason: "not_connected" });
3313
+ }
3314
+ return new Promise((resolve) => {
3315
+ const requestId = crypto.randomUUID();
3316
+ pendingRef.current.set(requestId, resolve);
3317
+ try {
3318
+ ws.send(encode(build(requestId)));
3319
+ } catch (e) {
3320
+ pendingRef.current.delete(requestId);
3321
+ resolve({
3322
+ ok: false,
3323
+ reason: "not_connected",
3324
+ error: e instanceof Error ? e.message : String(e)
3325
+ });
3326
+ }
3327
+ });
3278
3328
  },
3279
3329
  [canWrite]
3280
3330
  );
3281
3331
  const trigger = useCallback11(
3282
- (taskName) => sendWrite(clientBuild.cronTrigger(taskName)),
3332
+ (taskName) => sendWrite((requestId) => clientBuild.cronTrigger(taskName, requestId)),
3283
3333
  [sendWrite]
3284
3334
  );
3285
3335
  const pause = useCallback11(
3286
- (taskName) => sendWrite(clientBuild.cronPause(taskName)),
3336
+ (taskName) => sendWrite((requestId) => clientBuild.cronPause(taskName, requestId)),
3287
3337
  [sendWrite]
3288
3338
  );
3289
3339
  const resume = useCallback11(
3290
- (taskName) => sendWrite(clientBuild.cronResume(taskName)),
3340
+ (taskName) => sendWrite((requestId) => clientBuild.cronResume(taskName, requestId)),
3291
3341
  [sendWrite]
3292
3342
  );
3293
- return { tasks, history, connected, canWrite, trigger, pause, resume };
3343
+ return { tasks, history, connected, canWrite, lastError, trigger, pause, resume };
3294
3344
  }
3295
3345
 
3296
3346
  // src/client/storage/hooks/useJobs.ts
@@ -3548,13 +3598,13 @@ var MAX_UPLOAD_REQUEST_BYTES = 25 * 1024 * 1024;
3548
3598
  var UPLOAD_PART_BYTES = 20 * 1024 * 1024;
3549
3599
  var MAX_UPLOAD_PARTS = Math.ceil(MAX_APP_FILE_BYTES / UPLOAD_PART_BYTES);
3550
3600
  var MAX_BASE64_UPLOAD_BYTES = Math.floor(MAX_UPLOAD_REQUEST_BYTES * 3 / 4);
3551
- function planUploadParts(totalBytes, partSize) {
3601
+ function planUploadParts(totalBytes) {
3552
3602
  const parts = [];
3553
- for (let start = 0; start < totalBytes; start += partSize) {
3603
+ for (let start = 0; start < totalBytes; start += UPLOAD_PART_BYTES) {
3554
3604
  parts.push({
3555
3605
  partNumber: parts.length + 1,
3556
3606
  start,
3557
- end: Math.min(start + partSize, totalBytes)
3607
+ end: Math.min(start + UPLOAD_PART_BYTES, totalBytes)
3558
3608
  });
3559
3609
  }
3560
3610
  return parts;
@@ -3627,24 +3677,43 @@ function resolveFile(fileOrKey) {
3627
3677
  }
3628
3678
  return { key: fileOrKey.key, name: fileOrKey.originalName };
3629
3679
  }
3630
- async function uploadInParts(file, name, scopeParams, authHeaders, onProgress) {
3680
+ function withUploadKey(params, key) {
3681
+ if (!key) return params;
3682
+ const pair = `key=${encodeURIComponent(key)}`;
3683
+ return params ? `${params}&${pair}` : pair;
3684
+ }
3685
+ async function uploadInParts(file, name, key, scopeParams, authHeaders, onProgress) {
3631
3686
  const headers = await authHeaders();
3632
3687
  const json = { ...headers, "Content-Type": "application/json" };
3633
- const initResponse = await fetch(`/api/files/multipart?${scopeParams}`, {
3688
+ const requestId = crypto.randomUUID();
3689
+ const initBody = JSON.stringify({
3690
+ requestId,
3691
+ name,
3692
+ mimeType: file.type || "application/octet-stream",
3693
+ size: file.size
3694
+ });
3695
+ const sendInit = () => fetch(`/api/files/multipart?${withUploadKey(scopeParams, key)}`, {
3634
3696
  method: "POST",
3635
3697
  headers: json,
3636
- body: JSON.stringify({ name, mimeType: file.type || "application/octet-stream", size: file.size })
3698
+ body: initBody
3637
3699
  });
3700
+ let initResponse = await sendInit().catch(() => null);
3701
+ if (!initResponse || initResponse.status === 429 || initResponse.status >= 500) {
3702
+ initResponse = await sendInit().catch(() => null);
3703
+ }
3704
+ if (!initResponse) {
3705
+ return { success: false, error: "The files service could not be reached." };
3706
+ }
3638
3707
  if (!initResponse.ok) return await readUploadResult(initResponse);
3639
3708
  const session = await initResponse.json();
3640
- if (!Number.isSafeInteger(session.partSize) || session.partSize < 1) {
3709
+ if (!session.uploadId || !session.uploadKey || !session.reservationId || session.partSize !== UPLOAD_PART_BYTES) {
3641
3710
  return {
3642
3711
  success: false,
3643
- error: `The files service advertised an unusable part size (${session.partSize}).`
3712
+ error: `The files service returned an unusable part size or multipart session (${session.partSize}).`
3644
3713
  };
3645
3714
  }
3646
- const query = `${scopeParams}&uploadKey=${encodeKeyPath(session.uploadKey)}&uploadId=${encodeURIComponent(session.uploadId)}`;
3647
- const plan = planUploadParts(file.size, session.partSize);
3715
+ const query = `${scopeParams}&uploadKey=${encodeKeyPath(session.uploadKey)}&uploadId=${encodeURIComponent(session.uploadId)}&reservationId=${encodeURIComponent(session.reservationId)}`;
3716
+ const plan = planUploadParts(file.size);
3648
3717
  const parts = [];
3649
3718
  let assembled = false;
3650
3719
  try {
@@ -3702,17 +3771,27 @@ function useR2Files(options) {
3702
3771
  if (tooBig) return tooBig;
3703
3772
  const fileName = name ?? (file instanceof File ? file.name : "upload");
3704
3773
  if (file.size > UPLOAD_PART_BYTES) {
3705
- return await uploadInParts(file, fileName, scopeParams, authHeaders, options2?.onProgress);
3774
+ return await uploadInParts(
3775
+ file,
3776
+ fileName,
3777
+ options2?.key,
3778
+ scopeParams,
3779
+ authHeaders,
3780
+ options2?.onProgress
3781
+ );
3706
3782
  }
3707
3783
  const headers = await authHeaders();
3708
3784
  const formData = new FormData();
3709
3785
  formData.append("file", file);
3710
3786
  if (name) formData.append("name", name);
3711
- const response = await fetch(`/api/files/upload?${scopeParams}`, {
3712
- method: "POST",
3713
- headers,
3714
- body: formData
3715
- });
3787
+ const response = await fetch(
3788
+ `/api/files/upload?${withUploadKey(scopeParams, options2?.key)}`,
3789
+ {
3790
+ method: "POST",
3791
+ headers,
3792
+ body: formData
3793
+ }
3794
+ );
3716
3795
  options2?.onProgress?.(file.size, file.size);
3717
3796
  return await readUploadResult(response);
3718
3797
  } catch (err) {
@@ -3724,20 +3803,20 @@ function useR2Files(options) {
3724
3803
  [scopeParams, authHeaders]
3725
3804
  );
3726
3805
  const uploadBase64 = useCallback14(
3727
- async (base64Data, name, mimeType) => {
3806
+ async (base64Data, name, mimeType, options2) => {
3728
3807
  setIsUploading(true);
3729
3808
  try {
3730
- const tooBig = oversize(
3731
- Math.floor(base64Data.length * 3 / 4),
3732
- MAX_BASE64_UPLOAD_BYTES
3733
- );
3809
+ const tooBig = oversize(Math.floor(base64Data.length * 3 / 4), MAX_BASE64_UPLOAD_BYTES);
3734
3810
  if (tooBig) return tooBig;
3735
3811
  const headers = await authHeaders();
3736
- const response = await fetch(`/api/files/upload?${scopeParams}`, {
3737
- method: "POST",
3738
- headers: { ...headers, "Content-Type": "application/json" },
3739
- body: JSON.stringify({ data: base64Data, name, mimeType })
3740
- });
3812
+ const response = await fetch(
3813
+ `/api/files/upload?${withUploadKey(scopeParams, options2?.key)}`,
3814
+ {
3815
+ method: "POST",
3816
+ headers: { ...headers, "Content-Type": "application/json" },
3817
+ body: JSON.stringify({ data: base64Data, name, mimeType })
3818
+ }
3819
+ );
3741
3820
  return await readUploadResult(response);
3742
3821
  } catch (err) {
3743
3822
  return { success: false, error: err instanceof Error ? err.message : "Upload failed" };
@@ -3747,12 +3826,18 @@ function useR2Files(options) {
3747
3826
  },
3748
3827
  [scopeParams, authHeaders]
3749
3828
  );
3829
+ const getUrl = useCallback14(
3830
+ (fileOrKey) => {
3831
+ const { key } = resolveFile(fileOrKey);
3832
+ return `/api/files/${encodeKeyPath(key)}?${scopeParams}`;
3833
+ },
3834
+ [scopeParams]
3835
+ );
3750
3836
  const deleteFile = useCallback14(
3751
3837
  async (fileOrKey) => {
3752
- const { key } = resolveFile(fileOrKey);
3753
3838
  try {
3754
3839
  const headers = await authHeaders();
3755
- const response = await fetch(`/api/files/${encodeKeyPath(key)}?${scopeParams}`, {
3840
+ const response = await fetch(getUrl(fileOrKey), {
3756
3841
  method: "DELETE",
3757
3842
  headers
3758
3843
  });
@@ -3761,32 +3846,40 @@ function useR2Files(options) {
3761
3846
  return { success: false, error: err instanceof Error ? err.message : "Delete failed" };
3762
3847
  }
3763
3848
  },
3764
- [scopeParams, authHeaders]
3849
+ [getUrl, authHeaders]
3765
3850
  );
3766
- const list = useCallback14(
3767
- async (prefix) => {
3768
- try {
3769
- const headers = await authHeaders();
3770
- const params = new URLSearchParams(scopeParams);
3771
- if (prefix) params.set("prefix", prefix);
3772
- const response = await fetch(`/api/files?${params.toString()}`, { headers });
3773
- const result = await response.json();
3774
- return result.files || [];
3775
- } catch {
3776
- return [];
3851
+ const listPage = useCallback14(
3852
+ async (prefix, options2) => {
3853
+ const headers = await authHeaders();
3854
+ const params = new URLSearchParams(scopeParams);
3855
+ if (prefix) params.set("prefix", prefix);
3856
+ if (options2?.limit !== void 0) params.set("limit", String(options2.limit));
3857
+ if (options2?.cursor) params.set("cursor", options2.cursor);
3858
+ const response = await fetch(`/api/files?${params.toString()}`, { headers });
3859
+ if (!response.ok) {
3860
+ throw new Error(describeFilesFailure(response.status, await response.text()));
3777
3861
  }
3862
+ const result = await response.json();
3863
+ return {
3864
+ files: result.files || [],
3865
+ cursor: result.cursor,
3866
+ truncated: result.truncated ?? false
3867
+ };
3778
3868
  },
3779
3869
  [scopeParams, authHeaders]
3780
3870
  );
3871
+ const list = useCallback14(
3872
+ async (prefix, options2) => (await listPage(prefix, options2)).files,
3873
+ [listPage]
3874
+ );
3781
3875
  const downloadFile = useCallback14(
3782
3876
  async (fileOrKey, fileName) => {
3783
3877
  const { key, name: infoName } = resolveFile(fileOrKey);
3784
3878
  try {
3785
3879
  const headers = await authHeaders();
3786
- const response = await fetch(`/api/files/${encodeKeyPath(key)}?${scopeParams}`, { headers });
3880
+ const response = await fetch(getUrl(key), { headers });
3787
3881
  if (!response.ok) {
3788
- const body = await response.json().catch(() => null);
3789
- return { success: false, error: body?.error ?? `Download failed (${response.status})` };
3882
+ return { success: false, error: describeFilesFailure(response.status, await response.text()) };
3790
3883
  }
3791
3884
  const disposition = response.headers.get("Content-Disposition");
3792
3885
  const dispositionName = disposition?.match(/filename="(.+?)"/)?.[1];
@@ -3806,29 +3899,30 @@ function useR2Files(options) {
3806
3899
  return { success: false, error: err instanceof Error ? err.message : "Download failed" };
3807
3900
  }
3808
3901
  },
3809
- [scopeParams, authHeaders]
3902
+ [getUrl, authHeaders]
3810
3903
  );
3811
3904
  const readFile = useCallback14(
3812
3905
  async (fileOrKey) => {
3813
- const { key } = resolveFile(fileOrKey);
3814
3906
  const headers = await authHeaders();
3815
- const response = await fetch(`/api/files/${encodeKeyPath(key)}?${scopeParams}`, { headers });
3907
+ const response = await fetch(getUrl(fileOrKey), { headers });
3816
3908
  if (!response.ok) {
3817
- const body = await response.text().catch(() => "");
3818
- throw new Error(`Failed to read file (${response.status}): ${body}`);
3909
+ throw new Error(describeFilesFailure(response.status, await response.text()));
3819
3910
  }
3820
3911
  return response;
3821
3912
  },
3822
- [scopeParams, authHeaders]
3913
+ [getUrl, authHeaders]
3823
3914
  );
3824
- const getUrl = useCallback14(
3825
- (fileOrKey) => {
3826
- const { key } = resolveFile(fileOrKey);
3827
- return `/api/files/${encodeKeyPath(key)}?${scopeParams}`;
3828
- },
3829
- [scopeParams]
3830
- );
3831
- return { upload, uploadBase64, deleteFile, downloadFile, readFile, list, getUrl, isUploading };
3915
+ return {
3916
+ upload,
3917
+ uploadBase64,
3918
+ deleteFile,
3919
+ downloadFile,
3920
+ readFile,
3921
+ list,
3922
+ listPage,
3923
+ getUrl,
3924
+ isUploading
3925
+ };
3832
3926
  }
3833
3927
 
3834
3928
  // src/client/storage/file-attachment-utils.ts
@@ -5164,6 +5258,68 @@ function usePlatformWS(options) {
5164
5258
  return { state, send };
5165
5259
  }
5166
5260
 
5261
+ // src/shared/api-error.ts
5262
+ var SLUG_RE = /^[a-z0-9]+(?:[_.-][a-z0-9]+)*$/;
5263
+ var HUMAN_MESSAGES = {
5264
+ owner_connect_not_ready: "This app isn't ready to accept payments yet \u2014 the app owner needs to finish payment setup.",
5265
+ invalid_json: "The request body was not valid JSON.",
5266
+ invalid_body: "The request body is invalid.",
5267
+ invalid_product_id: "Invalid product id.",
5268
+ duplicate_product_id: "Duplicate product id.",
5269
+ unknown_product: "Unknown product.",
5270
+ too_many_products: "Too many products declared.",
5271
+ invalid_amount: "Invalid amount.",
5272
+ invalid_name: "Invalid product name.",
5273
+ invalid_description: "Invalid product description.",
5274
+ unsupported_currency: "Unsupported currency.",
5275
+ unknown_app: "Unknown app.",
5276
+ not_app_owner: "Only the app owner (or a collaborator) can do this.",
5277
+ invalid_return_url: "Invalid return URL.",
5278
+ invalid_cancel_url: "Invalid cancel URL.",
5279
+ unknown_plan: "Unknown subscription plan.",
5280
+ unknown_price: "No price is configured for that billing interval.",
5281
+ no_subscription: "No subscription found for this account."
5282
+ };
5283
+ function normalizeApiError(status, body) {
5284
+ const payload = body !== null && typeof body === "object" ? body : {};
5285
+ const rawError = typeof payload.error === "string" ? payload.error : void 0;
5286
+ const message = typeof payload.message === "string" ? payload.message : void 0;
5287
+ const humanError = rawError !== void 0 && !SLUG_RE.test(rawError) ? rawError : void 0;
5288
+ const code = rawError !== void 0 && SLUG_RE.test(rawError) ? rawError : typeof payload.code === "string" && SLUG_RE.test(payload.code) ? payload.code : void 0;
5289
+ const details = {};
5290
+ for (const [key, value] of Object.entries(payload)) {
5291
+ if (key === "success" || key === "error" || key === "message" || key === "issues" || key === "code")
5292
+ continue;
5293
+ details[key] = value;
5294
+ }
5295
+ return {
5296
+ error: message ?? humanError ?? (code !== void 0 ? HUMAN_MESSAGES[code] : void 0) ?? rawError ?? `Request failed (${status})`,
5297
+ ...code !== void 0 ? { code } : {},
5298
+ status,
5299
+ ...Object.keys(details).length > 0 ? { details } : {},
5300
+ ...Array.isArray(payload.issues) ? { issues: payload.issues } : {}
5301
+ };
5302
+ }
5303
+ var PlatformApiError = class extends Error {
5304
+ /** The machine slug (e.g. 'owner_connect_not_ready'), when the server sent one. */
5305
+ code;
5306
+ /** HTTP status of the response; 0 when no HTTP response arrived. */
5307
+ status;
5308
+ /** Structured fields the server sent alongside the error. */
5309
+ details;
5310
+ /** Zod issues accompanying a `validation_failed` response. */
5311
+ issues;
5312
+ constructor(normalized) {
5313
+ super(normalized.error);
5314
+ this.name = "PlatformApiError";
5315
+ this.code = normalized.code;
5316
+ this.status = normalized.status;
5317
+ this.details = normalized.details;
5318
+ this.issues = normalized.issues;
5319
+ }
5320
+ };
5321
+ var apiErrorCode = (e) => e instanceof PlatformApiError ? e.code ?? null : null;
5322
+
5167
5323
  // src/client/integration.ts
5168
5324
  var ENDPOINT_PREFIX = "/api/integrations";
5169
5325
  var DEFAULT_TIMEOUT_MS2 = 12e4;
@@ -5185,7 +5341,7 @@ async function request(method, endpoint, body, options, query) {
5185
5341
  const url = `${resolveUrl(endpoint)}${queryString ? `?${queryString}` : ""}`;
5186
5342
  const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
5187
5343
  if (options?.signal?.aborted) {
5188
- return { success: false, error: "Request cancelled" };
5344
+ return { success: false, error: "Request cancelled", status: 0 };
5189
5345
  }
5190
5346
  const headers = {
5191
5347
  "Content-Type": "application/json",
@@ -5215,14 +5371,10 @@ async function request(method, endpoint, body, options, query) {
5215
5371
  try {
5216
5372
  payload = await res.json();
5217
5373
  } catch {
5218
- return { success: false, error: `Request failed (${res.status})` };
5374
+ return { success: false, error: `Request failed (${res.status})`, status: res.status };
5219
5375
  }
5220
5376
  if (!res.ok || payload.success === false) {
5221
- return {
5222
- success: false,
5223
- error: payload.error ?? payload.message ?? `Request failed (${res.status})`,
5224
- ...payload.issues ? { issues: payload.issues } : {}
5225
- };
5377
+ return { success: false, ...normalizeApiError(res.status, payload) };
5226
5378
  }
5227
5379
  if ("data" in payload) {
5228
5380
  return { success: true, data: payload.data };
@@ -5234,12 +5386,14 @@ async function request(method, endpoint, body, options, query) {
5234
5386
  if (err instanceof DOMException && err.name === "AbortError") {
5235
5387
  return {
5236
5388
  success: false,
5237
- error: timedOut ? `Request timed out after ${timeoutMs}ms` : "Request cancelled"
5389
+ error: timedOut ? `Request timed out after ${timeoutMs}ms` : "Request cancelled",
5390
+ status: 0
5238
5391
  };
5239
5392
  }
5240
5393
  return {
5241
5394
  success: false,
5242
- error: err instanceof Error ? err.message : "Request failed"
5395
+ error: err instanceof Error ? err.message : "Request failed",
5396
+ status: 0
5243
5397
  };
5244
5398
  } finally {
5245
5399
  clearTimeout(timeout);
@@ -5923,17 +6077,7 @@ function usePagedResource(fetchPage, deps, options = {}) {
5923
6077
  // src/client/subscriptions/useSubscription.ts
5924
6078
  import { useCallback as useCallback27, useEffect as useEffect21, useState as useState18 } from "react";
5925
6079
 
5926
- // src/shared/subscription.ts
5927
- var ENTITLED_STATUSES = /* @__PURE__ */ new Set([
5928
- "active",
5929
- "trialing"
5930
- ]);
5931
- function isEntitled(tier, status) {
5932
- if (tier === "free") return true;
5933
- return ENTITLED_STATUSES.has(status);
5934
- }
5935
-
5936
- // src/client/subscriptions/useSubscription.ts
6080
+ // src/client/authed-json.ts
5937
6081
  async function authedJson(path, init) {
5938
6082
  const token = await getAuthToken();
5939
6083
  const res = await fetch(path, {
@@ -5947,10 +6091,22 @@ async function authedJson(path, init) {
5947
6091
  });
5948
6092
  const data = await res.json().catch(() => ({}));
5949
6093
  if (!res.ok) {
5950
- throw new Error(data.error ?? `Request failed (${res.status})`);
6094
+ throw new PlatformApiError(normalizeApiError(res.status, data));
5951
6095
  }
5952
6096
  return data;
5953
6097
  }
6098
+
6099
+ // src/shared/subscription.ts
6100
+ var ENTITLED_STATUSES = /* @__PURE__ */ new Set([
6101
+ "active",
6102
+ "trialing"
6103
+ ]);
6104
+ function isEntitled(tier, status) {
6105
+ if (tier === "free") return true;
6106
+ return ENTITLED_STATUSES.has(status);
6107
+ }
6108
+
6109
+ // src/client/subscriptions/useSubscription.ts
5954
6110
  function useSubscription() {
5955
6111
  const [state, setState] = useState18({
5956
6112
  tier: "free",
@@ -5963,6 +6119,7 @@ function useSubscription() {
5963
6119
  });
5964
6120
  const [isLoading, setLoading] = useState18(true);
5965
6121
  const [error, setError] = useState18(null);
6122
+ const [errorCode, setErrorCode] = useState18(null);
5966
6123
  const refresh = useCallback27(async () => {
5967
6124
  setLoading(true);
5968
6125
  try {
@@ -5977,8 +6134,10 @@ function useSubscription() {
5977
6134
  plans: r.plans ?? []
5978
6135
  });
5979
6136
  setError(null);
6137
+ setErrorCode(null);
5980
6138
  } catch (e) {
5981
6139
  setError(e instanceof Error ? e.message : "Failed to load subscription");
6140
+ setErrorCode(apiErrorCode(e));
5982
6141
  } finally {
5983
6142
  setLoading(false);
5984
6143
  }
@@ -6040,6 +6199,7 @@ function useSubscription() {
6040
6199
  trialEndsAt: state.trialEndsAt,
6041
6200
  isLoading,
6042
6201
  error,
6202
+ errorCode,
6043
6203
  hasTier,
6044
6204
  isAtLeast,
6045
6205
  plans: state.plans,
@@ -6091,37 +6251,23 @@ function PricingTable(props) {
6091
6251
 
6092
6252
  // src/client/charges/useCheckout.ts
6093
6253
  import { useCallback as useCallback28, useEffect as useEffect22, useMemo as useMemo17, useState as useState19 } from "react";
6094
- async function authedJson2(path, init) {
6095
- const token = await getAuthToken();
6096
- const res = await fetch(path, {
6097
- ...init,
6098
- credentials: "include",
6099
- headers: {
6100
- "Content-Type": "application/json",
6101
- ...token ? { Authorization: `Bearer ${token}` } : {},
6102
- ...init?.headers
6103
- }
6104
- });
6105
- const data = await res.json().catch(() => ({}));
6106
- if (!res.ok) {
6107
- throw new Error(data.error ?? `Request failed (${res.status})`);
6108
- }
6109
- return data;
6110
- }
6111
6254
  function useCheckout(opts) {
6112
6255
  const [purchases, setPurchases] = useState19([]);
6113
6256
  const [isLoading, setLoading] = useState19(true);
6114
6257
  const [error, setError] = useState19(null);
6258
+ const [errorCode, setErrorCode] = useState19(null);
6115
6259
  const refresh = useCallback28(async () => {
6116
6260
  setLoading(true);
6117
6261
  try {
6118
- const r = await authedJson2(
6262
+ const r = await authedJson(
6119
6263
  "/_deepspace/charges/me"
6120
6264
  );
6121
6265
  setPurchases(r.purchases ?? []);
6122
6266
  setError(null);
6267
+ setErrorCode(null);
6123
6268
  } catch (e) {
6124
6269
  setError(e instanceof Error ? e.message : "Failed to load purchases");
6270
+ setErrorCode(apiErrorCode(e));
6125
6271
  } finally {
6126
6272
  setLoading(false);
6127
6273
  }
@@ -6132,6 +6278,7 @@ function useCheckout(opts) {
6132
6278
  const chargeOnce = useCallback28(
6133
6279
  async (chargeOpts) => {
6134
6280
  setError(null);
6281
+ setErrorCode(null);
6135
6282
  const common = {
6136
6283
  returnUrl: chargeOpts.returnUrl ?? window.location.href,
6137
6284
  cancelUrl: chargeOpts.cancelUrl ?? window.location.href,
@@ -6144,7 +6291,7 @@ function useCheckout(opts) {
6144
6291
  ...chargeOpts.description ? { description: chargeOpts.description } : {}
6145
6292
  };
6146
6293
  try {
6147
- const result = await authedJson2(
6294
+ const result = await authedJson(
6148
6295
  "/_deepspace/charges/create",
6149
6296
  {
6150
6297
  method: "POST",
@@ -6158,6 +6305,7 @@ function useCheckout(opts) {
6158
6305
  } catch (e) {
6159
6306
  const msg = e instanceof Error ? e.message : "Failed to start checkout";
6160
6307
  setError(msg);
6308
+ setErrorCode(apiErrorCode(e));
6161
6309
  throw e;
6162
6310
  }
6163
6311
  },
@@ -6175,6 +6323,7 @@ function useCheckout(opts) {
6175
6323
  chargeOnce,
6176
6324
  isLoading,
6177
6325
  error,
6326
+ errorCode,
6178
6327
  purchases,
6179
6328
  owned,
6180
6329
  ownsProduct,
@@ -6351,6 +6500,7 @@ export {
6351
6500
  MSG_SYNC_UPDATE,
6352
6501
  MSG_YJS_AWARENESS,
6353
6502
  MSG_YJS_SYNC,
6503
+ PlatformApiError,
6354
6504
  PlatformContext,
6355
6505
  PlatformProvider,
6356
6506
  PricingTable,