@lotics/app-sdk 0.41.0 → 0.42.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/AGENTS.md CHANGED
@@ -39,7 +39,10 @@ Pick by intent. (→ open the `.d.ts` for the exact signature.)
39
39
  - **Mutate (the ONLY write path)** — **`useWorkflow(alias)`** → an async fn returning a typed
40
40
  `WorkflowResult` `{ status, message?, files?, data? }`. `data` is whatever the workflow
41
41
  `return({ data })`'d (typed automatically from the alias contract). After a known mutation,
42
- call the owning query's `refetch()`.
42
+ call the owning query's `refetch()`. Handle failures by checking `result.status === "error"`,
43
+ not just `try/catch`: a transport/gateway failure (a 524 timeout on a long run, any 5xx, a
44
+ non-JSON error page) **resolves** with `{ status: "error", message }` (a friendly, body-free
45
+ message) — it does not throw an HTML body.
43
46
  - **Upload a file** — **`useFileUpload()`** → `{ upload, uploading, error }`. Mints a presigned URL
44
47
  and PUTs the bytes straight to storage; the file is inert until a workflow attaches it to a
45
48
  `files` field. Emits `app_file_uploaded`.
@@ -52,7 +55,10 @@ Pick by intent. (→ open the `.d.ts` for the exact signature.)
52
55
  to a `DisplayFile` (snake → camel — `mime_type`→`mimeType`, `preview_url`→`url` — the app owns this
53
56
  data→UI adapter; the SDK never imports `@lotics/ui`) for
54
57
  `<FileThumbnail file={{ id, filename, mimeType, url }} uploading={f.status === "uploading"} />`;
55
- `sendDisabled` gates on `uploading` and the send payload is `fileIds`. Don't hand-roll
58
+ `sendDisabled` gates on `uploading` and the send payload is `fileIds`. To persist several files into ONE
59
+ record, declare the workflow input `{ type: "file", multi: true }` — the body receives `fileIds` as
60
+ `ReadonlyArray<FileId>` and writes it straight to a `files` field (a single `file` input is one id → wrap `[id]`).
61
+ Don't hand-roll
56
62
  `createObjectURL`/upload/revoke per app. For a full add-files SCREEN (not the composer pill), map
57
63
  each `AttachedFile` to a `FileUpload` (ready → `{ status: "complete", id, file }`, else
58
64
  `{ status, id, filename, mimeType: mime_type, previewUrl: preview_url }`) and feed `@lotics/ui`
@@ -164,6 +164,12 @@ type UseWorkflowFn<K extends keyof AppWorkflows & string> = AppWorkflows[K] exte
164
164
  *
165
165
  * `data` is the structured value the workflow returned via `return({ data })`,
166
166
  * typed per the alias's declared `outputs` schema (`unknown` when none was declared).
167
+ *
168
+ * A transport/gateway failure (a Cloudflare 524 timeout on a long run, any 5xx,
169
+ * or a non-JSON error page) **resolves** with `{ status: "error", message }` —
170
+ * a body-free, friendly message — rather than rejecting with a raw HTML body.
171
+ * So an app handles every failure (handled workflow error AND transport error)
172
+ * by checking `result.status === "error"`; it never receives gateway HTML.
167
173
  */
168
174
  export interface WorkflowResult<TData = unknown> {
169
175
  status: "success" | "error";
package/dist/src/hooks.js CHANGED
@@ -26,7 +26,7 @@ export function useWorkflow(alias) {
26
26
  return useCallback(async (inputs) => {
27
27
  try {
28
28
  const result = await rpc("workflow", { alias, inputs: inputs ?? {} });
29
- captureAppEvent("app_workflow_run", { alias, ok: true });
29
+ captureAppEvent("app_workflow_run", { alias, ok: result.status !== "error" });
30
30
  return result;
31
31
  }
32
32
  catch (err) {
package/dist/src/rpc.d.ts CHANGED
@@ -86,3 +86,11 @@ export declare function subscribeUrlParams(cb: (params: UrlParams) => void): ()
86
86
  * standalone: the SDK reads the public endpoint's body directly.
87
87
  */
88
88
  export declare function rpcAgentRun(payload: AgentRunPayload, onText: (chunk: string) => void, onRunId?: (runId: string) => void): AgentRunHandle;
89
+ /**
90
+ * The error message for a non-ok response. A genuine JSON error (a 4xx carrying
91
+ * a `message`) surfaces verbatim; a non-JSON body (a gateway HTML page), any
92
+ * 5xx, or a JSON body without a `message` falls back to a body-free,
93
+ * status-derived message — so a raw HTML body never becomes the message.
94
+ * `parsed` is the JSON.parse of the body, or `null` if it wasn't JSON.
95
+ */
96
+ export declare function transportErrorMessage(status: number, parsed: unknown): string;
package/dist/src/rpc.js CHANGED
@@ -334,6 +334,37 @@ async function acquireSessionToken(appId) {
334
334
  },
335
335
  });
336
336
  }
337
+ /**
338
+ * A user-facing message for a transport/gateway failure — derived from the HTTP
339
+ * status, never from the response body. A 524 (Cloudflare edge timeout on a long
340
+ * run), any 5xx, or a non-JSON body (an HTML error page) must NOT surface its raw
341
+ * body as the error message. Kept in parity (by value, no shared dep) with the
342
+ * dev-loop transport in `packages/sdk/src/client.ts`.
343
+ */
344
+ function gatewayErrorMessage(status) {
345
+ if (status === 524) {
346
+ return "The request took too long to finish (gateway timeout). It may still be running — check back in a moment, or try again.";
347
+ }
348
+ if (status >= 500) {
349
+ return "The service is temporarily unavailable. Please try again shortly.";
350
+ }
351
+ return "The service returned an unexpected response. Please try again.";
352
+ }
353
+ /**
354
+ * The error message for a non-ok response. A genuine JSON error (a 4xx carrying
355
+ * a `message`) surfaces verbatim; a non-JSON body (a gateway HTML page), any
356
+ * 5xx, or a JSON body without a `message` falls back to a body-free,
357
+ * status-derived message — so a raw HTML body never becomes the message.
358
+ * `parsed` is the JSON.parse of the body, or `null` if it wasn't JSON.
359
+ */
360
+ export function transportErrorMessage(status, parsed) {
361
+ const jsonMessage = parsed && typeof parsed.message === "string"
362
+ ? parsed.message
363
+ : null;
364
+ return parsed === null || status >= 500 || jsonMessage === null
365
+ ? gatewayErrorMessage(status)
366
+ : jsonMessage;
367
+ }
337
368
  async function apiCall(method, path, body, opts) {
338
369
  const headers = {};
339
370
  if (body)
@@ -368,8 +399,9 @@ async function apiCall(method, path, body, opts) {
368
399
  await acquireSessionToken(appId);
369
400
  return apiCall(method, path, body, { ...opts, appId });
370
401
  }
371
- const message = errorBody?.message ?? text;
372
- throw new Error(message || `HTTP ${res.status}`);
402
+ // Never surface a non-JSON body (a gateway HTML error page) or a 5xx body as
403
+ // the message emit a body-free, status-derived message instead.
404
+ throw new Error(transportErrorMessage(res.status, parsed));
373
405
  }
374
406
  return parsed ?? (text ? text : {});
375
407
  }
@@ -480,7 +512,16 @@ async function standaloneFieldOptions(p) {
480
512
  }
481
513
  async function standaloneWorkflow(p) {
482
514
  const { app_id } = await boot();
483
- return apiCall("POST", `/v1/apps/${app_id}/workflows/${encodeURIComponent(p.alias)}/execute`, { inputs: p.inputs }, { appId: app_id });
515
+ try {
516
+ return await apiCall("POST", `/v1/apps/${app_id}/workflows/${encodeURIComponent(p.alias)}/execute`, { inputs: p.inputs }, { appId: app_id });
517
+ }
518
+ catch (err) {
519
+ // A transport/gateway failure resolves to a WorkflowResult error (never a
520
+ // rejection carrying a raw body) so an app reads `result.status === "error"`
521
+ // uniformly with a handled workflow error. `apiCall` already sanitized the
522
+ // message, so it never contains an HTML body.
523
+ return { status: "error", message: err instanceof Error ? err.message : "The workflow failed to run." };
524
+ }
484
525
  }
485
526
  async function standaloneAgentRuns(p) {
486
527
  const { app_id } = await boot();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotics/app-sdk",
3
- "version": "0.41.0",
3
+ "version": "0.42.1",
4
4
  "description": "Runtime SDK for Lotics custom-code apps — typed hooks, postMessage bridge, mount entry point",
5
5
  "type": "module",
6
6
  "exports": {