unfazed 0.4.0 → 0.6.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 (91) hide show
  1. package/README.md +323 -1
  2. package/dist/client.d.ts +1 -1
  3. package/dist/client.js +1 -1
  4. package/dist/client.js.map +1 -1
  5. package/dist/errors.d.ts +25 -0
  6. package/dist/errors.js +25 -0
  7. package/dist/errors.js.map +1 -1
  8. package/dist/generated/gql.d.ts +5 -1
  9. package/dist/generated/gql.js +2 -1
  10. package/dist/generated/gql.js.map +1 -1
  11. package/dist/generated/graphql.d.ts +107 -0
  12. package/dist/generated/graphql.js +101 -0
  13. package/dist/generated/graphql.js.map +1 -1
  14. package/dist/index.d.ts +21 -0
  15. package/dist/index.js +22 -0
  16. package/dist/index.js.map +1 -1
  17. package/dist/operations.d.ts +3 -0
  18. package/dist/operations.js +23 -0
  19. package/dist/operations.js.map +1 -1
  20. package/dist/remote/app.d.ts +18 -0
  21. package/dist/remote/app.js +76 -0
  22. package/dist/remote/app.js.map +1 -0
  23. package/dist/remote/documents.d.ts +175 -0
  24. package/dist/remote/documents.js +122 -0
  25. package/dist/remote/documents.js.map +1 -0
  26. package/dist/remote/values.d.ts +67 -0
  27. package/dist/remote/values.js +156 -0
  28. package/dist/remote/values.js.map +1 -0
  29. package/dist/remote/world.d.ts +50 -0
  30. package/dist/remote/world.js +612 -0
  31. package/dist/remote/world.js.map +1 -0
  32. package/dist/unfazed.d.ts +37 -1
  33. package/dist/unfazed.js +13 -1
  34. package/dist/unfazed.js.map +1 -1
  35. package/dist/workflow/config.d.ts +23 -0
  36. package/dist/workflow/config.js +48 -0
  37. package/dist/workflow/config.js.map +1 -0
  38. package/dist/workflow/errors.d.ts +33 -0
  39. package/dist/workflow/errors.js +16 -0
  40. package/dist/workflow/errors.js.map +1 -0
  41. package/dist/workflow/index.d.ts +30 -0
  42. package/dist/workflow/index.js +23 -0
  43. package/dist/workflow/index.js.map +1 -0
  44. package/dist/workflow/inspect-render.d.ts +93 -0
  45. package/dist/workflow/inspect-render.js +302 -0
  46. package/dist/workflow/inspect-render.js.map +1 -0
  47. package/dist/workflow/inspect.d.ts +52 -0
  48. package/dist/workflow/inspect.js +879 -0
  49. package/dist/workflow/inspect.js.map +1 -0
  50. package/dist/workflow/migration-files.d.ts +13 -0
  51. package/dist/workflow/migration-files.js +52 -0
  52. package/dist/workflow/migration-files.js.map +1 -0
  53. package/dist/workflow/migration-types.d.ts +19 -0
  54. package/dist/workflow/migration-types.js +8 -0
  55. package/dist/workflow/migration-types.js.map +1 -0
  56. package/dist/workflow/migrations.d.ts +12 -0
  57. package/dist/workflow/migrations.generated.d.ts +2 -0
  58. package/dist/workflow/migrations.generated.js +34 -0
  59. package/dist/workflow/migrations.generated.js.map +1 -0
  60. package/dist/workflow/migrations.js +94 -0
  61. package/dist/workflow/migrations.js.map +1 -0
  62. package/dist/workflow/nitro-flow-guard.d.ts +4 -0
  63. package/dist/workflow/nitro-flow-guard.js +19 -0
  64. package/dist/workflow/nitro-flow-guard.js.map +1 -0
  65. package/dist/workflow/nitro-inspect.d.ts +3 -0
  66. package/dist/workflow/nitro-inspect.js +19 -0
  67. package/dist/workflow/nitro-inspect.js.map +1 -0
  68. package/dist/workflow/nitro-plugin.d.ts +1 -0
  69. package/dist/workflow/nitro-plugin.js +13 -0
  70. package/dist/workflow/nitro-plugin.js.map +1 -0
  71. package/dist/workflow/nitro.d.ts +70 -0
  72. package/dist/workflow/nitro.js +213 -0
  73. package/dist/workflow/nitro.js.map +1 -0
  74. package/dist/workflow/public-url.d.ts +27 -0
  75. package/dist/workflow/public-url.js +45 -0
  76. package/dist/workflow/public-url.js.map +1 -0
  77. package/dist/workflow/queue.d.ts +128 -0
  78. package/dist/workflow/queue.js +375 -0
  79. package/dist/workflow/queue.js.map +1 -0
  80. package/dist/workflow/state.d.ts +25 -0
  81. package/dist/workflow/state.js +15 -0
  82. package/dist/workflow/state.js.map +1 -0
  83. package/dist/workflow/transport.d.ts +12 -0
  84. package/dist/workflow/transport.js +29 -0
  85. package/dist/workflow/transport.js.map +1 -0
  86. package/dist/workflow/world.d.ts +123 -0
  87. package/dist/workflow/world.js +375 -0
  88. package/dist/workflow/world.js.map +1 -0
  89. package/package.json +66 -5
  90. package/schema/SOURCE.json +4 -4
  91. package/schema/schema.graphqls +36 -0
package/README.md CHANGED
@@ -18,6 +18,10 @@ SDK: one script. Three requests. You read the four lines you cared about.
18
18
  ```
19
19
 
20
20
  - Zero runtime dependencies. Installing `unfazed` installs exactly one package.
21
+ (The optional [`unfazed/workflow`](#durable-workflows-unfazedworkflow) subpath
22
+ uses Workflow SDK packages your app installs; the root entry never loads them.)
23
+ - `npx workflow inspect runs --backend unfazed`: Workflow SDK's CLI and web UI
24
+ read your app's runs through the platform ([details](#inspect-runs-from-your-terminal)).
21
25
  - Strict TypeScript, generated from the platform's own GraphQL schema.
22
26
  - Bun-first, runs unchanged on Node >= 20 and Deno.
23
27
 
@@ -197,7 +201,8 @@ The chains an agent actually performs. Reach `client` from it any time:
197
201
  **Agent runs**
198
202
  | Method | Notes |
199
203
  | --- | --- |
200
- | `createRun(opts)` | `wait: true` polls to a terminal status; a boot-failed or exit-nonzero run comes back as a value, never thrown. `verify: [...]` commands run after the agent and decide whether the task is done |
204
+ | `createRun(opts)` | `wait: true` polls to a terminal status; a boot-failed or exit-nonzero run comes back as a value, never thrown. `verify: [...]` commands run after the agent and decide whether the task is done. `access: "read-write"` lifts the read-only default (read-only integration credentials, read MCP tools, no deploy); `alias` picks the app whose integrations the run gets |
205
+ | `listRunHarnesses()` | the harness presets `createRun({ harness })` accepts, with the secrets each needs |
201
206
  | `waitForRun(id, opts?)` | standalone poller; only a timeout throws |
202
207
  | `getRun(id)` / `listRuns(filter?)` | |
203
208
  | `cancelRun(id)` | idempotent on a CANCELLED run; a finished run is refused |
@@ -412,6 +417,319 @@ So an agent working inside a run can create workspaces, deploy, and read logs
412
417
  through this same SDK with zero configuration and no credential ever written
413
418
  into the prompt.
414
419
 
420
+ ## Durable workflows: `unfazed/workflow`
421
+
422
+ [Workflow SDK](https://workflow-sdk.dev) runs durable functions: a
423
+ `"use workflow"` function orchestrates `"use step"` functions, survives
424
+ crashes and redeploys, sleeps for minutes or months and waits for webhooks.
425
+ `unfazed/workflow` is the **World** that runs it on unfazed:
426
+
427
+ - run storage, streams and `await run.returnValue` are the upstream
428
+ `@workflow/world-postgres` code over **the app's own Postgres** (its
429
+ migrations are applied for you; the upstream graphile-worker queue is never
430
+ started);
431
+ - the **queue is the platform's**: every wake-up (`sleep()`, a retry, the next
432
+ step, a resumed hook) is a delivery the unfazed worker makes to the app's flow
433
+ route, cold-starting an `on_demand` VM when it is asleep. Nothing needs a warm
434
+ process, so the default `on_demand` lifecycle is the right one.
435
+
436
+ ```sh
437
+ bun add unfazed workflow @workflow/world-postgres nitro hono
438
+ ```
439
+
440
+ `workflow`, `@workflow/core`, `@workflow/world`, `@workflow/world-postgres`,
441
+ `pg`, `drizzle-orm` and `nitro` are **optional peer dependencies**: code-mode
442
+ users of `unfazed` install none of them. `@workflow/core` arrives with
443
+ `workflow`, and `@workflow/world`, `pg` and `drizzle-orm` with
444
+ `@workflow/world-postgres`, under npm and Bun; with pnpm or Yarn PnP add them
445
+ to the app as well.
446
+
447
+ ### With Nitro (the recipe)
448
+
449
+ ```ts
450
+ // nitro.config.ts
451
+ import { defineConfig } from "nitro";
452
+
453
+ export default defineConfig({
454
+ modules: ["workflow/nitro", "unfazed/workflow/nitro"],
455
+ routes: { "/**": "./src/app.ts" }, // a Hono app; not named server.ts (Nitro claims that name)
456
+ });
457
+ ```
458
+
459
+ ```json
460
+ // unfazed.json — package.json has "build": "nitro build"
461
+ { "kind": "SERVICE", "runtime": "BUN", "install": "bun install", "build": "bun run build", "entry": "unfazed-entry.ts" }
462
+ ```
463
+
464
+ ```ts
465
+ // unfazed-entry.ts
466
+ export { default } from "./.output/server/index.mjs";
467
+ ```
468
+
469
+ Then bind a Postgres or Neon integration to the app and deploy. In a
470
+ production build the module:
471
+
472
+ - installs the World with `setWorld(createWorld())` from a server plugin, which
473
+ Nitro runs before the first request (a bundle without `node_modules` cannot
474
+ load a World through `WORKFLOW_TARGET_WORLD`);
475
+ - embeds the SQL migrations of **the app's own** `@workflow/world-postgres`, so
476
+ the schema always matches the storage code in the bundle;
477
+ - answers anything that reaches `/.well-known/workflow/v1/flow` without the
478
+ platform's queue token with **401** (including the SDK's `?__health` probe).
479
+ Public ingress strips that header, so only the platform can deliver. The
480
+ webhook route stays public: its unguessable token is its authorization;
481
+ - mounts the **inspection route** the unfazed dashboard and MCP read runs
482
+ through (below), with the build's workflow list and Workflow SDK version;
483
+ - sets `noExternals: true` and builds Nitro's `standard` output (`export
484
+ default { fetch }`), which is what unfazed serves, whenever no preset was
485
+ chosen, whatever Nitro detected on the build machine: `bun` under Bun (as on
486
+ the unfazed build VM), `node-server` under Node, `deno-server` under Deno.
487
+ Their own outputs start a server of their own (under Bun with a 10 s idle
488
+ timeout that cuts every quiet workflow stream). A preset chosen explicitly
489
+ (`preset` in nitro.config.ts or .nitrorc, `NITRO_PRESET`, `SERVER_PRESET`)
490
+ other than `standard` **fails the build** with a message saying so; leave it
491
+ out, or set `preset: "standard"`.
492
+ - serves public assets only when `serveStatic: "inline"` embeds them in the
493
+ bundle (a service VM has nothing next to it to read from disk). That is how
494
+ one service ships an API and its UI (`publicAssets: [{ dir: "dist" }]`
495
+ after a `vite build`).
496
+
497
+ `nitro dev` is left alone and keeps the SDK's Local World.
498
+
499
+ A complete app — steps, a retried step, `sleep`, a webhook (plain
500
+ `createWebhook()` and `webhook.url`), a progress stream — is in
501
+ [`examples/workflow-nitro`](./examples/workflow-nitro). Once it is deployed,
502
+ `UNFZ_APP=<alias> npx workflow inspect runs --backend unfazed` lists its runs
503
+ ([Inspect runs from your terminal](#inspect-runs-from-your-terminal)).
504
+
505
+ ### Without Nitro
506
+
507
+ Install the World in your framework's start-up hook, before it serves:
508
+
509
+ ```ts
510
+ import { setWorld } from "workflow/runtime";
511
+ import { createWorld } from "unfazed/workflow";
512
+
513
+ setWorld(createWorld()); // Next.js: instrumentation.ts register(); SvelteKit: hooks.server.ts init
514
+ ```
515
+
516
+ `createWorld()` is synchronous and opens nothing. Without the Nitro module the
517
+ World applies the migrations embedded in this package (built from the
518
+ `@workflow/world-postgres` version in its devDependencies), so keep the app's
519
+ `@workflow/world-postgres` on that version.
520
+
521
+ Mount the inspection route yourself, as a catch-all under its base path:
522
+
523
+ ```ts
524
+ // e.g. Next.js: app/.well-known/workflow/v1/unfazed/inspect/v1/[op]/route.ts
525
+ import { createInspectHandler } from "unfazed/workflow";
526
+
527
+ const inspect = createInspectHandler({ workflowVersion: "5.2.2" }); // optional: workflows: [...names]
528
+ export const GET = inspect;
529
+ export const POST = inspect;
530
+ ```
531
+
532
+ ### Inspecting runs from the dashboard and MCP
533
+
534
+ The app answers the platform's questions about its runs itself, on
535
+ `/.well-known/workflow/v1/unfazed/inspect/v1/<op>` (unfazed
536
+ `docs/workflow-inspect.md`, guest contract v1): `info`, `runs`, `run`, `steps`,
537
+ `step`, `events`, `event` and `stream` read through the app's own World
538
+ (`getWorld()`), so an `on_demand` app cold-starts for the request and holds no
539
+ database connection afterwards; `wake` and `cancel` are Workflow SDK's own
540
+ `getRun(id).wakeUp()` and `.cancel()`.
541
+
542
+ - Only the platform can call it: anything without `X-Unfz-Queue-Token` equal to
543
+ `UNFZ_STATE_TOKEN` (compared in constant time) gets 401, and public ingress
544
+ strips that header. A World that failed to start answers 503
545
+ `world_unavailable` with its start error, which the owner sees.
546
+ - Payloads (inputs, outputs, errors, event data, stream chunks) are hydrated
547
+ with `workflow/observability` on top of the SDK's common revivers and sent as
548
+ display text, cut at the contract's caps (16 KiB per input or output, 64 KiB
549
+ per event payload, 8 KiB per chunk); every answer stays under 1 MiB, a page
550
+ coming back shorter (with the World's cursor for the rest) when it must.
551
+ - Hook tokens and webhook URLs never leave: hooks are shown by ID, kind and
552
+ state; every `token` key is dropped from event info and event data; every
553
+ occurrence of the run's hook tokens in an event's data reads `[redacted]`;
554
+ and every Workflow SDK webhook URL in any text the route returns (run and
555
+ step inputs and outputs, errors, event info and data, stream chunks,
556
+ attributes) has its token segment replaced, as
557
+ `https://app.example/.well-known/workflow/v1/webhook/[redacted]?answer=yes`,
558
+ also when percent-encoded inside another URL or written with JSON-escaped
559
+ slashes. That happens before any cap, so a cut never leaves part of a token.
560
+ Everything else the app writes is shown as written, as in its logs.
561
+ - A step's input shows as its arguments: Workflow SDK stores it as
562
+ `{ args, closureVars, thisVal }`, and the route answers `args`, or
563
+ `{ args, closureVars?, thisVal? }` with only the parts that hold something
564
+ (any other stored shape is shown as it is).
565
+ - A run page carries at most the latest 500 sleeps and 500 hooks
566
+ (`waits_truncated` / `hooks_truncated` say when there were more); a list
567
+ page may hold fewer items than `limit`, so callers page on `has_more`.
568
+
569
+ ### Inspect runs from your terminal
570
+
571
+ Workflow SDK's own CLI reads an app's runs through the platform, the way
572
+ `--backend vercel` reads a Vercel project. From the app's project (where
573
+ `unfazed` is installed; the CLI loads `createWorld()` from it):
574
+
575
+ ```sh
576
+ export UNFZ_API_TOKEN=... # a token from https://unfazed.dev/auth.md
577
+ UNFZ_APP=birthday-card npx workflow inspect runs --backend unfazed
578
+ ```
579
+
580
+ ```
581
+ runId workflowName status startedAt completedAt
582
+ ------------------------------- -------------------- --------- ----------------------- -----------------------
583
+ wrun_01KPBDAY00000000000000RN01 generateBirthdayCard running 2026-10-15 09:30:01.000
584
+ wrun_01KPBDAY00000000000000RN02 generateBirthdayCard completed 2026-10-15 09:10:01.000 2026-10-15 09:12:00.000
585
+ wrun_01KPBDAY00000000000000RN03 generateBirthdayCard failed 2026-10-15 09:05:01.000 2026-10-15 09:06:00.000
586
+ ```
587
+
588
+ ```sh
589
+ npx workflow inspect run <run-id> --backend unfazed # input, output, error
590
+ npx workflow inspect steps --runId <run-id> --backend unfazed # also: events, hooks, sleeps, streams
591
+ npx workflow inspect step <step-id> --runId <run-id> --backend unfazed
592
+ npx workflow inspect stream <stream-id> --runId <run-id> --backend unfazed # follows it while the run writes
593
+ npx workflow cancel <run-id> --backend unfazed
594
+ npx workflow web --backend unfazed # the local web UI: trace, events, streams,
595
+ # Wake Up Sleep and Cancel
596
+ ```
597
+
598
+ The usual options work (`--json`, `--limit`, `--cursor`, `--all`, `--status`,
599
+ `--workflowName`, `--interactive`); the exceptions are below.
600
+ `export WORKFLOW_TARGET_WORLD=unfazed` saves typing `--backend`.
601
+
602
+ - **Which app:** `UNFZ_APP` (its alias, the `<alias>` of
603
+ `https://<alias>.unfz.to`); without it, the `alias` in `./unfazed.json`, or
604
+ its only `apps[]` entry. `UNFZ_API_URL` points at another control plane.
605
+ - **The token** needs `unfz:read`; cancel and Wake Up Sleep need `unfz:deploy`
606
+ too, and the caller must be allowed to restart the app (owner, team member,
607
+ operator).
608
+ - **Every command is one platform call relayed to the app**, through the same
609
+ inspection route the dashboard reads; an `on_demand` app cold-starts for it,
610
+ as for a visitor.
611
+
612
+ What differs from `--backend vercel`:
613
+
614
+ - **Payloads are the inspection route's display data**: hydrated inside the
615
+ app and cut at its caps (16 KiB per run or step input and output, 64 KiB
616
+ per event payload, 8 KiB per stream chunk). A cut value is a string ending
617
+ in `… [truncated by unfazed inspection: N bytes in full]`. Values are JSON:
618
+ a `Date` reads as its ISO string, a `Map`, `Set`, bytes or `Error` as
619
+ `{"$map": …}`, `{"$set": …}`, `{"$bytes": n}`, `{"$error": …}`. A payload the
620
+ app could not read is `[unreadable payload: <why>]`. Run data on unfazed is
621
+ not encrypted, so `--decrypt` has nothing to do.
622
+ - **Hook tokens never leave the app**: hooks show `token: "[redacted]"`, and
623
+ the web UI cannot resume a hook.
624
+ - **List views carry no payloads**, so `--withData` (deprecated by the CLI for
625
+ lists) shows none; `inspect run <id>` and `inspect step <id>` do. Listed
626
+ events carry their metadata, `inspect event <id>` its payload.
627
+ - **No analytics read path**: `--attribute`, `--since`/`--until` and `inspect
628
+ attributes` need one, and the CLI says it is ignored or unavailable.
629
+ - **Runs list newest first only** (`--sort asc` is refused). A run's steps
630
+ newest first (the CLI's default) are read whole, up to 2 000, and paged
631
+ locally.
632
+ - **Hooks are found through their run**: `inspect hooks` without `--runId`,
633
+ and `inspect hook <id>`, look through the app's running and pending runs.
634
+ - **A run page reads at most 10 000 events**, the latest 500 sleeps and 500
635
+ hooks, and 50 streams (the route's limits).
636
+ - **Writes**: cancel (`workflow cancel`, bulk cancel, the web UI) and wake-up
637
+ (the web UI) go through the platform. Everything else (replay, re-enqueue,
638
+ resuming a hook, starting a run) is refused: this backend is read-only, and
639
+ an app's own World is `unfazed/workflow`. Setting
640
+ `WORKFLOW_TARGET_WORLD=unfazed` inside an app fails loudly for the same
641
+ reason.
642
+
643
+ ### How it behaves
644
+
645
+ - **Start-up is lazy and cheap.** The first World call of a process applies the
646
+ migrations (one query when the database is current) and probes the platform
647
+ queue; every call waits for that, so a request arriving during a cold start
648
+ waits instead of failing. Active runs are then re-enqueued in the background
649
+ with the key `reenqueue:<runId>:<deploymentId>`, which the platform drops as
650
+ a duplicate while that message is stored and for an hour after it settles,
651
+ so a run asleep for days is not re-enqueued on every cold start.
652
+ `world.start()` does the same and lets you await it.
653
+ - **No warm process, no lingering connections.** Postgres connections,
654
+ including the `LISTEN` ones behind stream readers and
655
+ `waitForTerminalStatus`, are held only while a World call or a stream reader
656
+ is open (plus 2 s); idle pool connections close after 5 s. A reaped VM leaves
657
+ nothing behind on the database.
658
+ - **Deliveries** carry `X-Unfz-Queue-*` headers; the handler checks the token
659
+ in constant time against `UNFZ_STATE_TOKEN`, maps the rest to the SDK's
660
+ message metadata, and reports the delivery deadline (15 minutes from the
661
+ moment the VM was ready, so a cold start does not shorten it) through
662
+ `getRuntimeDeadline()`, so the SDK sizes its inline work to fit. Every answer
663
+ is a complete JSON body and never a redirect (the platform follows none, and
664
+ retries a 3xx or a broken 2xx body).
665
+ - **Concurrency:** at most 32 deliveries per app are open at once. Set it per
666
+ app in `unfazed.json` with `"workflow": {"concurrency": N}` (1 to 256;
667
+ `workflow_concurrency` on `create_service_deployment` for an app deployed
668
+ from a workspace). A step that awaits a child run holds one delivery for the
669
+ child's whole run, so keep the concurrency above the number of such waits.
670
+ When the VM stops on its own under deliveries (out of memory, a crash), the
671
+ platform halves the app's concurrency for 5 minutes and records a
672
+ `service.workflow_throttled` event.
673
+ - **Routes for other queue consumers:** `world.registerRoute(prefix, path)` is
674
+ stored on the platform. `registerHandler()` is not offered: the platform
675
+ delivers over HTTP, never inside a process.
676
+ - **Webhook URLs need no code.** `createWebhook().url` (and
677
+ `getWorkflowMetadata().url`) are built on the app's public address. Workflow
678
+ SDK 5.x reads only `VERCEL_URL` for that (`https://${VERCEL_URL}`, else
679
+ `http://localhost:<port>`), so when `VERCEL_URL` is unset and
680
+ `UNFZ_PUBLIC_URL` is a bare https host — a custom domain or
681
+ `https://<alias>.unfz.to` — `createWorld()` sets `VERCEL_URL` to that host in
682
+ the app's own process. Inside the SDK that variable decides nothing else
683
+ (it also skips probing for the local port, which is then unneeded). Your own
684
+ code and libraries see it too, as the public host. Opt out with
685
+ `createWorld({ publicUrl: false })`; an explicit `VERCEL_URL` is never
686
+ changed.
687
+
688
+ A **path-form** public URL (`https://app.unfazed.dev/<alias>`) cannot be
689
+ passed that way: the SDK's base path is fixed at build time and Workflow
690
+ SDK's Nitro integration sets none. Those apps build the URL in a step:
691
+
692
+ ```ts
693
+ async function publicWebhookUrl(token: string) {
694
+ "use step";
695
+ return `${process.env.UNFZ_PUBLIC_URL}/.well-known/workflow/v1/webhook/${token}`;
696
+ }
697
+ ```
698
+ - **Errors** are `UnfazedWorkflowError` with a `code`: `no_database` (bind a
699
+ Postgres or Neon integration), `no_platform` (not inside an unfazed service),
700
+ `queue_not_supported` (the worker has no queue yet), `queue_unavailable`,
701
+ `quota_exceeded` (its message names the limit: 100 000 pending messages,
702
+ 64 MiB of live messages or 200 000 idempotency keys), `payload_too_large`
703
+ (over 1 MiB), `unavailable` (503 or network, after 30 s of retries with the
704
+ same idempotency keys), …
705
+
706
+ | Read from | Meaning |
707
+ | --- | --- |
708
+ | `WORKFLOW_POSTGRES_URL`, `DATABASE_URL_UNPOOLED`, `DATABASE_URL` | run storage, first one set; `sslmode=require` gets `uselibpqcompat=true` |
709
+ | `WORKFLOW_POSTGRES_MAX_POOL_SIZE` | pool size (default 10) |
710
+ | `UNFZ_STATE_URL`, `UNFZ_STATE_TOKEN` | the platform queue and the delivery token (injected by unfazed) |
711
+ | `UNFZ_DEPLOYMENT_ID` | `getDeploymentId()` |
712
+ | `UNFZ_PUBLIC_URL` | the public address webhook URLs are built on (see above) |
713
+
714
+ `createWorld({ ... })` takes the same settings as options (`connectionString`,
715
+ `stateUrl`, `stateToken`, `deploymentId`, `maxPoolSize`, `migrations`,
716
+ `publicUrl`, `retryBudgetMs`, `idleReleaseMs`, `env`, `fetch`).
717
+
718
+ ### Testing it
719
+
720
+ ```sh
721
+ bun test # unit tests, incl. a fake platform implementing the wire contract
722
+ export UNFAZED_TEST_DATABASE_URL=postgres://... # a database whose user may CREATE DATABASE
723
+ bun test test/workflow # + the World against Postgres
724
+ WORKFLOW_POSTGRES_URL=$UNFAZED_TEST_DATABASE_URL bun run test:conformance
725
+ # the SDK's World conformance suite, and every
726
+ # delivery cold-started in a fresh process
727
+ bun run test:example # the example, bundled as unfazed builds it,
728
+ # one fresh process per delivery and request
729
+ bun run test:cli # Workflow SDK's CLI with --backend unfazed (and --web)
730
+ # against a fake of the platform's inspection API
731
+ ```
732
+
415
733
  ## Development
416
734
 
417
735
  ```sh
@@ -423,6 +741,10 @@ bun run build
423
741
  bun run check # all of the above, plus a dist smoke test on Bun AND Node
424
742
  ```
425
743
 
744
+ After bumping the `@workflow/world-postgres` devDependency, run
745
+ `bun run embed-migrations`: `bun run check` fails while
746
+ `src/workflow/migrations.generated.ts` is stale.
747
+
426
748
  ### Regenerating from the schema
427
749
 
428
750
  `schema/schema.graphqls` is a **vendored copy** of `graph/schema.graphqls` from
package/dist/client.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /** SDK version, sent as the User-Agent. Keep in sync with package.json. */
2
- export declare const VERSION = "0.4.0";
2
+ export declare const VERSION = "0.6.0";
3
3
  /**
4
4
  * Default GraphQL endpoint.
5
5
  *
package/dist/client.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { UnfazedAuthError, UnfazedConfigError, UnfazedGraphQLError, UnfazedHTTPError, UnfazedTimeoutError, } from "./errors.js";
2
2
  /** SDK version, sent as the User-Agent. Keep in sync with package.json. */
3
- export const VERSION = "0.4.0";
3
+ export const VERSION = "0.6.0";
4
4
  /**
5
5
  * Default GraphQL endpoint.
6
6
  *
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n UnfazedAuthError,\n UnfazedConfigError,\n UnfazedGraphQLError,\n UnfazedHTTPError,\n UnfazedTimeoutError,\n type GraphQLResponseError,\n} from \"./errors.js\";\n\n/** SDK version, sent as the User-Agent. Keep in sync with package.json. */\nexport const VERSION = \"0.4.0\";\n\n/**\n * Default GraphQL endpoint.\n *\n * Note this is the FULL endpoint, not an origin: the platform's own documented\n * convention (`UNFZ_API_URL=\"https://unfazed.dev/query\"`, and cmd/showcase-deploy\n * in the unfazed repo) treats the /query URL as the configurable unit, so the\n * SDK matches it rather than inventing a base-URL convention.\n */\nexport const DEFAULT_ENDPOINT = \"https://unfazed.dev/query\";\n\n/** Platform origin, used to derive the endpoint when only `baseUrl` is given. */\nexport const DEFAULT_BASE_URL = \"https://unfazed.dev\";\n\n/** Path of the GraphQL endpoint under an origin. */\nexport const GRAPHQL_PATH = \"/query\";\n\n/**\n * Environment variable holding the bearer token.\n *\n * This is the platform's documented name (docs/agent-deployments.md,\n * internal/agentdocs/reference.md). It accepts either an operator API token or\n * an agent access token from POST /agents/register — /query takes both as\n * plain bearers.\n */\nexport const TOKEN_ENV_VAR = \"UNFZ_API_TOKEN\";\n\n/** Environment variable holding the full GraphQL endpoint URL. */\nexport const ENDPOINT_ENV_VAR = \"UNFZ_API_URL\";\n\n/** Minimal structural type of the `fetch` the transport needs. */\nexport type FetchLike = (\n input: string,\n init: {\n method: string;\n headers: Record<string, string>;\n body: string;\n signal?: AbortSignal;\n },\n) => Promise<{\n ok: boolean;\n status: number;\n text(): Promise<string>;\n}>;\n\nexport interface ClientOptions {\n /**\n * Bearer token. Falls back to `UNFZ_API_TOKEN`. Get one from\n * https://unfazed.dev/auth.md — the same token the MCP server uses.\n */\n token?: string;\n /**\n * Full GraphQL endpoint URL. Wins over `baseUrl`. Falls back to\n * `UNFZ_API_URL`, then {@link DEFAULT_ENDPOINT}.\n */\n endpoint?: string;\n /**\n * Platform origin; `/query` is appended. Convenience for self-hosted or\n * staging control planes where only the host differs.\n */\n baseUrl?: string;\n /** Extra headers merged into every request. Authorization always wins. */\n headers?: Record<string, string>;\n /**\n * Per-request timeout in ms. Default 60_000. Pass 0 to disable.\n *\n * Bounds the WHOLE request — connect, headers and reading the response body —\n * so a proxy that answers with headers and then stalls still fails on time.\n */\n timeoutMs?: number;\n /** Injection point for tests and instrumented/proxied transports. */\n fetch?: FetchLike;\n /** Overrides the default `unfazed-sdk/<version>` User-Agent. */\n userAgent?: string;\n}\n\n/**\n * A GraphQL document.\n *\n * Structural rather than `string & {...}` on purpose: graphql-codegen emits\n * `TypedDocumentString`, a String SUBCLASS, which is not assignable to the\n * primitive `string`. Requiring only `toString()` plus the optional phantom\n * `__apiType` lets three things through with full inference:\n *\n * - generated `TypedDocumentString` documents (types inferred),\n * - documents assembled at runtime, like the batched writeFiles mutation,\n * - plain template-literal strings a caller wrote by hand.\n */\nexport interface GraphQLDocument<TResult, TVariables> {\n /** Phantom type carrier. Never called; present only to bind the generics. */\n __apiType?: (variables: TVariables) => TResult;\n toString(): string;\n}\n\n/** Result type carried by a typed document. */\nexport type ResultOf<T> = T extends GraphQLDocument<infer R, infer _V> ? R : unknown;\n\n/** Variables type carried by a typed document. */\nexport type VariablesOf<T> = T extends GraphQLDocument<infer _R, infer V> ? V : never;\n\n/** Per-request knobs. */\nexport interface RequestOptions {\n signal?: AbortSignal;\n timeoutMs?: number;\n operationName?: string;\n}\n\n/**\n * Layer 1 — a typed, dependency-free GraphQL transport over `fetch`.\n *\n * This is the SDK's entire networking surface. It understands auth, timeouts\n * and the GraphQL error envelope, and nothing about deployments. The workflow\n * layer ({@link import(\"./unfazed.js\").Unfazed}) is built strictly on top of\n * it, so you can always drop down here for any operation the workflow layer\n * does not wrap — the schema has ~660 fields and the workflow layer\n * deliberately covers only the agent-facing ones.\n *\n * ```ts\n * const client = new UnfazedClient(); // reads UNFZ_API_TOKEN\n * const data = await client.query<{ whoami: { name: string } }, never>(\n * `query { whoami { name } }`,\n * );\n * ```\n */\nexport class UnfazedClient {\n /** Resolved GraphQL endpoint URL. */\n readonly endpoint: string;\n\n readonly #token: string;\n readonly #headers: Record<string, string>;\n readonly #timeoutMs: number;\n readonly #fetch: FetchLike;\n\n constructor(options: ClientOptions = {}) {\n const token = options.token ?? readEnv(TOKEN_ENV_VAR);\n if (!token) {\n throw new UnfazedAuthError(\n `unfazed: no API token. Pass { token } or set ${TOKEN_ENV_VAR}. ` +\n `Get one at ${DEFAULT_BASE_URL}/auth.md`,\n );\n }\n this.#token = token.trim();\n\n this.endpoint = resolveEndpoint(options);\n\n this.#headers = { ...(options.headers ?? {}) };\n this.#timeoutMs = options.timeoutMs ?? 60_000;\n\n const injected = options.fetch;\n if (injected) {\n this.#fetch = injected;\n } else {\n const globalFetch = (globalThis as { fetch?: unknown }).fetch;\n if (typeof globalFetch !== \"function\") {\n throw new UnfazedConfigError(\n \"unfazed: no global fetch. Use Bun, Node >= 20, or pass { fetch } to the client.\",\n );\n }\n this.#fetch = globalFetch.bind(globalThis) as FetchLike;\n }\n\n const ua = options.userAgent ?? `unfazed-sdk/${VERSION}`;\n if (ua) this.#headers[\"User-Agent\"] = ua;\n }\n\n /** Runs a query document. Identical wire behaviour to {@link mutate}. */\n query<TResult, TVariables>(\n document: GraphQLDocument<TResult, TVariables>,\n variables?: TVariables,\n options?: RequestOptions,\n ): Promise<TResult> {\n return this.request(document, variables, options);\n }\n\n /**\n * Runs a mutation document. Distinct from {@link query} for call-site\n * readability only — GraphQL over HTTP POST makes no distinction.\n */\n mutate<TResult, TVariables>(\n document: GraphQLDocument<TResult, TVariables>,\n variables?: TVariables,\n options?: RequestOptions,\n ): Promise<TResult> {\n return this.request(document, variables, options);\n }\n\n /**\n * The single code path behind {@link query} and {@link mutate}.\n *\n * Throws {@link UnfazedHTTPError} on non-2xx, {@link UnfazedGraphQLError}\n * when the body carries `errors`, {@link UnfazedTimeoutError} on timeout or\n * abort. Resolves to the `data` object on success.\n */\n async request<TResult = unknown>(\n document: string | { toString(): string },\n variables?: unknown,\n options: RequestOptions = {},\n ): Promise<TResult> {\n const timeoutMs = options.timeoutMs ?? this.#timeoutMs;\n const { signal, cleanup, didTimeout } = makeSignal(timeoutMs, options.signal);\n\n // `document` may be a TypedDocumentString (a String object). JSON.stringify\n // would serialise that fine, but String(...) keeps the wire body a plain\n // primitive regardless of how the caller obtained the document.\n const body = JSON.stringify({\n query: String(document),\n ...(variables === undefined ? {} : { variables }),\n ...(options.operationName ? { operationName: options.operationName } : {}),\n });\n\n // The deadline covers BOTH phases — headers AND body. A proxy that answers\n // with headers and then stalls the body is a real failure mode, and\n // stopping the clock at the end of `fetch()` would leave `response.text()`\n // hanging forever. `untilAborted` additionally races the signal itself, so\n // an injected transport that ignores `signal` still cannot outlive the\n // timeout.\n let response: Awaited<ReturnType<FetchLike>>;\n let text: string;\n try {\n response = await untilAborted(\n this.#fetch(this.endpoint, {\n method: \"POST\",\n headers: {\n ...this.#headers,\n \"Content-Type\": \"application/json\",\n Accept: \"application/json\",\n Authorization: `Bearer ${this.#token}`,\n },\n body,\n ...(signal ? { signal } : {}),\n }),\n signal,\n );\n text = await untilAborted(response.text(), signal);\n } catch (cause) {\n if (didTimeout()) {\n throw new UnfazedTimeoutError(`unfazed: request timed out after ${timeoutMs}ms`, { cause });\n }\n if (isAbort(cause)) {\n throw new UnfazedTimeoutError(\"unfazed: request aborted\", { cause });\n }\n throw cause;\n } finally {\n cleanup();\n }\n\n if (!response.ok) {\n throw new UnfazedHTTPError(response.status, text, this.endpoint);\n }\n\n let parsed: { data?: TResult; errors?: GraphQLResponseError[] };\n try {\n parsed = JSON.parse(text) as typeof parsed;\n } catch {\n throw new UnfazedHTTPError(response.status, text, this.endpoint);\n }\n\n // A GraphQL 200 can still be a failure, and can be a PARTIAL failure: the\n // batched writeFiles document resolves each aliased field independently.\n // Attaching `data` to the error is what lets a caller see which writes\n // landed before the failing one.\n if (parsed.errors?.length) {\n throw new UnfazedGraphQLError(parsed.errors, parsed.data);\n }\n if (parsed.data === undefined || parsed.data === null) {\n throw new UnfazedGraphQLError(\n [{ message: \"response contained neither data nor errors\" }],\n undefined,\n );\n }\n return parsed.data;\n }\n}\n\nfunction resolveEndpoint(options: ClientOptions): string {\n if (options.endpoint) return options.endpoint;\n if (options.baseUrl) return trimSlash(options.baseUrl) + GRAPHQL_PATH;\n const env = readEnv(ENDPOINT_ENV_VAR);\n // UNFZ_API_URL is documented as the full /query URL and is used verbatim.\n // Tolerate a bare origin anyway, since that is the obvious mistake and\n // guessing right costs nothing.\n if (env) {\n const trimmed = trimSlash(env);\n return /\\/query$|\\/graphql$/.test(trimmed) ? trimmed : trimmed + GRAPHQL_PATH;\n }\n return DEFAULT_ENDPOINT;\n}\n\nfunction readEnv(name: string): string | undefined {\n // Guarded: the SDK must also work in a browser or worker, where there is no\n // `process` at all.\n const env = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process\n ?.env;\n return env?.[name] || undefined;\n}\n\nfunction trimSlash(u: string): string {\n return u.replace(/\\/+$/, \"\");\n}\n\nfunction isAbort(e: unknown): boolean {\n return typeof e === \"object\" && e !== null && (e as { name?: string }).name === \"AbortError\";\n}\n\nfunction abortError(): Error {\n const e = new Error(\"The operation was aborted.\");\n e.name = \"AbortError\";\n return e;\n}\n\n/**\n * Settles with `promise`, or rejects the moment `signal` aborts.\n *\n * A global `fetch` honours the signal for headers and body alike, but an\n * injected {@link FetchLike} — a proxy, an instrumented transport, a test\n * double — need not. Racing the signal makes the timeout an upper bound on the\n * whole request regardless of what the transport does with it. The loser's\n * rejection is swallowed by the already-settled promise rather than surfacing\n * as an unhandled rejection.\n */\nfunction untilAborted<T>(promise: Promise<T>, signal: AbortSignal | undefined): Promise<T> {\n if (!signal) return promise;\n if (signal.aborted) return Promise.reject(abortError());\n return new Promise<T>((resolve, reject) => {\n const onAbort = () => reject(abortError());\n signal.addEventListener(\"abort\", onAbort, { once: true });\n const done = () => signal.removeEventListener(\"abort\", onAbort);\n promise.then(\n (value) => {\n done();\n resolve(value);\n },\n (error: unknown) => {\n done();\n reject(error as Error);\n },\n );\n });\n}\n\n/**\n * Combines the request timeout with any caller-supplied signal.\n *\n * `didTimeout` is tracked separately from the abort so a caller's own\n * cancellation is not mislabelled as a timeout in the thrown message.\n */\nfunction makeSignal(\n timeoutMs: number,\n external?: AbortSignal,\n): { signal?: AbortSignal; cleanup: () => void; didTimeout: () => boolean } {\n if (!timeoutMs && !external) return { cleanup: () => {}, didTimeout: () => false };\n if (typeof AbortController !== \"function\") {\n return {\n ...(external ? { signal: external } : {}),\n cleanup: () => {},\n didTimeout: () => false,\n };\n }\n\n const controller = new AbortController();\n let timedOut = false;\n let timer: ReturnType<typeof setTimeout> | undefined;\n\n if (timeoutMs > 0) {\n timer = setTimeout(() => {\n timedOut = true;\n controller.abort();\n }, timeoutMs);\n }\n\n const onExternalAbort = () => controller.abort();\n if (external) {\n if (external.aborted) controller.abort();\n else external.addEventListener(\"abort\", onExternalAbort, { once: true });\n }\n\n return {\n signal: controller.signal,\n cleanup: () => {\n if (timer !== undefined) clearTimeout(timer);\n external?.removeEventListener(\"abort\", onExternalAbort);\n },\n didTimeout: () => timedOut,\n };\n}\n"]}
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n UnfazedAuthError,\n UnfazedConfigError,\n UnfazedGraphQLError,\n UnfazedHTTPError,\n UnfazedTimeoutError,\n type GraphQLResponseError,\n} from \"./errors.js\";\n\n/** SDK version, sent as the User-Agent. Keep in sync with package.json. */\nexport const VERSION = \"0.6.0\";\n\n/**\n * Default GraphQL endpoint.\n *\n * Note this is the FULL endpoint, not an origin: the platform's own documented\n * convention (`UNFZ_API_URL=\"https://unfazed.dev/query\"`, and cmd/showcase-deploy\n * in the unfazed repo) treats the /query URL as the configurable unit, so the\n * SDK matches it rather than inventing a base-URL convention.\n */\nexport const DEFAULT_ENDPOINT = \"https://unfazed.dev/query\";\n\n/** Platform origin, used to derive the endpoint when only `baseUrl` is given. */\nexport const DEFAULT_BASE_URL = \"https://unfazed.dev\";\n\n/** Path of the GraphQL endpoint under an origin. */\nexport const GRAPHQL_PATH = \"/query\";\n\n/**\n * Environment variable holding the bearer token.\n *\n * This is the platform's documented name (docs/agent-deployments.md,\n * internal/agentdocs/reference.md). It accepts either an operator API token or\n * an agent access token from POST /agents/register — /query takes both as\n * plain bearers.\n */\nexport const TOKEN_ENV_VAR = \"UNFZ_API_TOKEN\";\n\n/** Environment variable holding the full GraphQL endpoint URL. */\nexport const ENDPOINT_ENV_VAR = \"UNFZ_API_URL\";\n\n/** Minimal structural type of the `fetch` the transport needs. */\nexport type FetchLike = (\n input: string,\n init: {\n method: string;\n headers: Record<string, string>;\n body: string;\n signal?: AbortSignal;\n },\n) => Promise<{\n ok: boolean;\n status: number;\n text(): Promise<string>;\n}>;\n\nexport interface ClientOptions {\n /**\n * Bearer token. Falls back to `UNFZ_API_TOKEN`. Get one from\n * https://unfazed.dev/auth.md — the same token the MCP server uses.\n */\n token?: string;\n /**\n * Full GraphQL endpoint URL. Wins over `baseUrl`. Falls back to\n * `UNFZ_API_URL`, then {@link DEFAULT_ENDPOINT}.\n */\n endpoint?: string;\n /**\n * Platform origin; `/query` is appended. Convenience for self-hosted or\n * staging control planes where only the host differs.\n */\n baseUrl?: string;\n /** Extra headers merged into every request. Authorization always wins. */\n headers?: Record<string, string>;\n /**\n * Per-request timeout in ms. Default 60_000. Pass 0 to disable.\n *\n * Bounds the WHOLE request — connect, headers and reading the response body —\n * so a proxy that answers with headers and then stalls still fails on time.\n */\n timeoutMs?: number;\n /** Injection point for tests and instrumented/proxied transports. */\n fetch?: FetchLike;\n /** Overrides the default `unfazed-sdk/<version>` User-Agent. */\n userAgent?: string;\n}\n\n/**\n * A GraphQL document.\n *\n * Structural rather than `string & {...}` on purpose: graphql-codegen emits\n * `TypedDocumentString`, a String SUBCLASS, which is not assignable to the\n * primitive `string`. Requiring only `toString()` plus the optional phantom\n * `__apiType` lets three things through with full inference:\n *\n * - generated `TypedDocumentString` documents (types inferred),\n * - documents assembled at runtime, like the batched writeFiles mutation,\n * - plain template-literal strings a caller wrote by hand.\n */\nexport interface GraphQLDocument<TResult, TVariables> {\n /** Phantom type carrier. Never called; present only to bind the generics. */\n __apiType?: (variables: TVariables) => TResult;\n toString(): string;\n}\n\n/** Result type carried by a typed document. */\nexport type ResultOf<T> = T extends GraphQLDocument<infer R, infer _V> ? R : unknown;\n\n/** Variables type carried by a typed document. */\nexport type VariablesOf<T> = T extends GraphQLDocument<infer _R, infer V> ? V : never;\n\n/** Per-request knobs. */\nexport interface RequestOptions {\n signal?: AbortSignal;\n timeoutMs?: number;\n operationName?: string;\n}\n\n/**\n * Layer 1 — a typed, dependency-free GraphQL transport over `fetch`.\n *\n * This is the SDK's entire networking surface. It understands auth, timeouts\n * and the GraphQL error envelope, and nothing about deployments. The workflow\n * layer ({@link import(\"./unfazed.js\").Unfazed}) is built strictly on top of\n * it, so you can always drop down here for any operation the workflow layer\n * does not wrap — the schema has ~660 fields and the workflow layer\n * deliberately covers only the agent-facing ones.\n *\n * ```ts\n * const client = new UnfazedClient(); // reads UNFZ_API_TOKEN\n * const data = await client.query<{ whoami: { name: string } }, never>(\n * `query { whoami { name } }`,\n * );\n * ```\n */\nexport class UnfazedClient {\n /** Resolved GraphQL endpoint URL. */\n readonly endpoint: string;\n\n readonly #token: string;\n readonly #headers: Record<string, string>;\n readonly #timeoutMs: number;\n readonly #fetch: FetchLike;\n\n constructor(options: ClientOptions = {}) {\n const token = options.token ?? readEnv(TOKEN_ENV_VAR);\n if (!token) {\n throw new UnfazedAuthError(\n `unfazed: no API token. Pass { token } or set ${TOKEN_ENV_VAR}. ` +\n `Get one at ${DEFAULT_BASE_URL}/auth.md`,\n );\n }\n this.#token = token.trim();\n\n this.endpoint = resolveEndpoint(options);\n\n this.#headers = { ...(options.headers ?? {}) };\n this.#timeoutMs = options.timeoutMs ?? 60_000;\n\n const injected = options.fetch;\n if (injected) {\n this.#fetch = injected;\n } else {\n const globalFetch = (globalThis as { fetch?: unknown }).fetch;\n if (typeof globalFetch !== \"function\") {\n throw new UnfazedConfigError(\n \"unfazed: no global fetch. Use Bun, Node >= 20, or pass { fetch } to the client.\",\n );\n }\n this.#fetch = globalFetch.bind(globalThis) as FetchLike;\n }\n\n const ua = options.userAgent ?? `unfazed-sdk/${VERSION}`;\n if (ua) this.#headers[\"User-Agent\"] = ua;\n }\n\n /** Runs a query document. Identical wire behaviour to {@link mutate}. */\n query<TResult, TVariables>(\n document: GraphQLDocument<TResult, TVariables>,\n variables?: TVariables,\n options?: RequestOptions,\n ): Promise<TResult> {\n return this.request(document, variables, options);\n }\n\n /**\n * Runs a mutation document. Distinct from {@link query} for call-site\n * readability only — GraphQL over HTTP POST makes no distinction.\n */\n mutate<TResult, TVariables>(\n document: GraphQLDocument<TResult, TVariables>,\n variables?: TVariables,\n options?: RequestOptions,\n ): Promise<TResult> {\n return this.request(document, variables, options);\n }\n\n /**\n * The single code path behind {@link query} and {@link mutate}.\n *\n * Throws {@link UnfazedHTTPError} on non-2xx, {@link UnfazedGraphQLError}\n * when the body carries `errors`, {@link UnfazedTimeoutError} on timeout or\n * abort. Resolves to the `data` object on success.\n */\n async request<TResult = unknown>(\n document: string | { toString(): string },\n variables?: unknown,\n options: RequestOptions = {},\n ): Promise<TResult> {\n const timeoutMs = options.timeoutMs ?? this.#timeoutMs;\n const { signal, cleanup, didTimeout } = makeSignal(timeoutMs, options.signal);\n\n // `document` may be a TypedDocumentString (a String object). JSON.stringify\n // would serialise that fine, but String(...) keeps the wire body a plain\n // primitive regardless of how the caller obtained the document.\n const body = JSON.stringify({\n query: String(document),\n ...(variables === undefined ? {} : { variables }),\n ...(options.operationName ? { operationName: options.operationName } : {}),\n });\n\n // The deadline covers BOTH phases — headers AND body. A proxy that answers\n // with headers and then stalls the body is a real failure mode, and\n // stopping the clock at the end of `fetch()` would leave `response.text()`\n // hanging forever. `untilAborted` additionally races the signal itself, so\n // an injected transport that ignores `signal` still cannot outlive the\n // timeout.\n let response: Awaited<ReturnType<FetchLike>>;\n let text: string;\n try {\n response = await untilAborted(\n this.#fetch(this.endpoint, {\n method: \"POST\",\n headers: {\n ...this.#headers,\n \"Content-Type\": \"application/json\",\n Accept: \"application/json\",\n Authorization: `Bearer ${this.#token}`,\n },\n body,\n ...(signal ? { signal } : {}),\n }),\n signal,\n );\n text = await untilAborted(response.text(), signal);\n } catch (cause) {\n if (didTimeout()) {\n throw new UnfazedTimeoutError(`unfazed: request timed out after ${timeoutMs}ms`, { cause });\n }\n if (isAbort(cause)) {\n throw new UnfazedTimeoutError(\"unfazed: request aborted\", { cause });\n }\n throw cause;\n } finally {\n cleanup();\n }\n\n if (!response.ok) {\n throw new UnfazedHTTPError(response.status, text, this.endpoint);\n }\n\n let parsed: { data?: TResult; errors?: GraphQLResponseError[] };\n try {\n parsed = JSON.parse(text) as typeof parsed;\n } catch {\n throw new UnfazedHTTPError(response.status, text, this.endpoint);\n }\n\n // A GraphQL 200 can still be a failure, and can be a PARTIAL failure: the\n // batched writeFiles document resolves each aliased field independently.\n // Attaching `data` to the error is what lets a caller see which writes\n // landed before the failing one.\n if (parsed.errors?.length) {\n throw new UnfazedGraphQLError(parsed.errors, parsed.data);\n }\n if (parsed.data === undefined || parsed.data === null) {\n throw new UnfazedGraphQLError(\n [{ message: \"response contained neither data nor errors\" }],\n undefined,\n );\n }\n return parsed.data;\n }\n}\n\nfunction resolveEndpoint(options: ClientOptions): string {\n if (options.endpoint) return options.endpoint;\n if (options.baseUrl) return trimSlash(options.baseUrl) + GRAPHQL_PATH;\n const env = readEnv(ENDPOINT_ENV_VAR);\n // UNFZ_API_URL is documented as the full /query URL and is used verbatim.\n // Tolerate a bare origin anyway, since that is the obvious mistake and\n // guessing right costs nothing.\n if (env) {\n const trimmed = trimSlash(env);\n return /\\/query$|\\/graphql$/.test(trimmed) ? trimmed : trimmed + GRAPHQL_PATH;\n }\n return DEFAULT_ENDPOINT;\n}\n\nfunction readEnv(name: string): string | undefined {\n // Guarded: the SDK must also work in a browser or worker, where there is no\n // `process` at all.\n const env = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process\n ?.env;\n return env?.[name] || undefined;\n}\n\nfunction trimSlash(u: string): string {\n return u.replace(/\\/+$/, \"\");\n}\n\nfunction isAbort(e: unknown): boolean {\n return typeof e === \"object\" && e !== null && (e as { name?: string }).name === \"AbortError\";\n}\n\nfunction abortError(): Error {\n const e = new Error(\"The operation was aborted.\");\n e.name = \"AbortError\";\n return e;\n}\n\n/**\n * Settles with `promise`, or rejects the moment `signal` aborts.\n *\n * A global `fetch` honours the signal for headers and body alike, but an\n * injected {@link FetchLike} — a proxy, an instrumented transport, a test\n * double — need not. Racing the signal makes the timeout an upper bound on the\n * whole request regardless of what the transport does with it. The loser's\n * rejection is swallowed by the already-settled promise rather than surfacing\n * as an unhandled rejection.\n */\nfunction untilAborted<T>(promise: Promise<T>, signal: AbortSignal | undefined): Promise<T> {\n if (!signal) return promise;\n if (signal.aborted) return Promise.reject(abortError());\n return new Promise<T>((resolve, reject) => {\n const onAbort = () => reject(abortError());\n signal.addEventListener(\"abort\", onAbort, { once: true });\n const done = () => signal.removeEventListener(\"abort\", onAbort);\n promise.then(\n (value) => {\n done();\n resolve(value);\n },\n (error: unknown) => {\n done();\n reject(error as Error);\n },\n );\n });\n}\n\n/**\n * Combines the request timeout with any caller-supplied signal.\n *\n * `didTimeout` is tracked separately from the abort so a caller's own\n * cancellation is not mislabelled as a timeout in the thrown message.\n */\nfunction makeSignal(\n timeoutMs: number,\n external?: AbortSignal,\n): { signal?: AbortSignal; cleanup: () => void; didTimeout: () => boolean } {\n if (!timeoutMs && !external) return { cleanup: () => {}, didTimeout: () => false };\n if (typeof AbortController !== \"function\") {\n return {\n ...(external ? { signal: external } : {}),\n cleanup: () => {},\n didTimeout: () => false,\n };\n }\n\n const controller = new AbortController();\n let timedOut = false;\n let timer: ReturnType<typeof setTimeout> | undefined;\n\n if (timeoutMs > 0) {\n timer = setTimeout(() => {\n timedOut = true;\n controller.abort();\n }, timeoutMs);\n }\n\n const onExternalAbort = () => controller.abort();\n if (external) {\n if (external.aborted) controller.abort();\n else external.addEventListener(\"abort\", onExternalAbort, { once: true });\n }\n\n return {\n signal: controller.signal,\n cleanup: () => {\n if (timer !== undefined) clearTimeout(timer);\n external?.removeEventListener(\"abort\", onExternalAbort);\n },\n didTimeout: () => timedOut,\n };\n}\n"]}
package/dist/errors.d.ts CHANGED
@@ -97,6 +97,31 @@ export declare class UnfazedConflictError extends UnfazedError {
97
97
  /** The requested alias, deployment or release does not exist (or was pruned). */
98
98
  export declare class UnfazedNotFoundError extends UnfazedError {
99
99
  }
100
+ /**
101
+ * What the `unfazed` CLI backend's World (`createWorld()`) throws.
102
+ *
103
+ * Its `name` is the Workflow SDK error a World would throw in its place
104
+ * (`WorkflowWorldError`, `WorkflowRunNotFoundError`, `HookNotFoundError`),
105
+ * because Workflow SDK's CLI and web UI pick their message by `name`,
106
+ * `status` and `code`: a missing run reads as a 404 there, an argument the
107
+ * backend cannot serve (`code: "INVALID_ARGUMENT"`) prints as given.
108
+ * `instanceof UnfazedError` still holds.
109
+ */
110
+ export declare class UnfazedInspectError extends UnfazedError {
111
+ /** HTTP-like status: 404 for a missing run, step, event, hook or app; 401/403 for a refused token. */
112
+ readonly status: number | undefined;
113
+ /** `INVALID_ARGUMENT`, `READ_ONLY`, or undefined. */
114
+ readonly code: string | undefined;
115
+ /** The run, for `WorkflowRunNotFoundError`. */
116
+ readonly runId: string | undefined;
117
+ constructor(message: string, options?: {
118
+ name?: "WorkflowWorldError" | "WorkflowRunNotFoundError" | "HookNotFoundError";
119
+ status?: number;
120
+ code?: string;
121
+ runId?: string;
122
+ cause?: unknown;
123
+ });
124
+ }
100
125
  /**
101
126
  * The server's exact "no such workspace" sentinel.
102
127
  *
package/dist/errors.js CHANGED
@@ -114,6 +114,31 @@ export class UnfazedConflictError extends UnfazedError {
114
114
  /** The requested alias, deployment or release does not exist (or was pruned). */
115
115
  export class UnfazedNotFoundError extends UnfazedError {
116
116
  }
117
+ /**
118
+ * What the `unfazed` CLI backend's World (`createWorld()`) throws.
119
+ *
120
+ * Its `name` is the Workflow SDK error a World would throw in its place
121
+ * (`WorkflowWorldError`, `WorkflowRunNotFoundError`, `HookNotFoundError`),
122
+ * because Workflow SDK's CLI and web UI pick their message by `name`,
123
+ * `status` and `code`: a missing run reads as a 404 there, an argument the
124
+ * backend cannot serve (`code: "INVALID_ARGUMENT"`) prints as given.
125
+ * `instanceof UnfazedError` still holds.
126
+ */
127
+ export class UnfazedInspectError extends UnfazedError {
128
+ /** HTTP-like status: 404 for a missing run, step, event, hook or app; 401/403 for a refused token. */
129
+ status;
130
+ /** `INVALID_ARGUMENT`, `READ_ONLY`, or undefined. */
131
+ code;
132
+ /** The run, for `WorkflowRunNotFoundError`. */
133
+ runId;
134
+ constructor(message, options = {}) {
135
+ super(message, options.cause === undefined ? undefined : { cause: options.cause });
136
+ this.name = options.name ?? "WorkflowWorldError";
137
+ this.status = options.status;
138
+ this.code = options.code;
139
+ this.runId = options.runId;
140
+ }
141
+ }
117
142
  /**
118
143
  * The server's exact "no such workspace" sentinel.
119
144
  *
@@ -1 +1 @@
1
- {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Error taxonomy.\n *\n * Everything the SDK throws is an {@link UnfazedError}, so a code-mode script\n * can `catch (e) { if (e instanceof UnfazedError) ... }` and still\n * discriminate on the subclass to decide whether to retry.\n */\n\n/** A single entry from a GraphQL response's `errors` array. */\nexport interface GraphQLResponseError {\n message: string;\n path?: (string | number)[];\n extensions?: Record<string, unknown>;\n}\n\n/** Base class for everything this SDK throws. */\nexport class UnfazedError extends Error {\n constructor(message: string, options?: { cause?: unknown }) {\n super(message, options);\n this.name = new.target.name;\n }\n}\n\n/** No token was passed and no token environment variable was set. */\nexport class UnfazedAuthError extends UnfazedError {}\n\n/** The runtime has no global `fetch` and none was injected. */\nexport class UnfazedConfigError extends UnfazedError {}\n\n/**\n * The endpoint answered with a non-2xx status. `status` is the HTTP status and\n * `body` the response text, which is where the platform reports non-GraphQL\n * failures such as an expired or wrong-scope bearer token.\n */\nexport class UnfazedHTTPError extends UnfazedError {\n readonly status: number;\n readonly body: string;\n\n constructor(status: number, body: string, url: string) {\n super(`unfazed: HTTP ${status} from ${url}${body ? `: ${truncate(body)}` : \"\"}`);\n this.status = status;\n this.body = body;\n }\n}\n\n/**\n * The endpoint answered 200 with a GraphQL `errors` array.\n *\n * Partial `data` is preserved deliberately: the batched writeFiles document\n * resolves each aliased field independently, so one path can fail while the\n * others commit, and the caller needs to see which landed.\n */\nexport class UnfazedGraphQLError extends UnfazedError {\n readonly errors: GraphQLResponseError[];\n readonly data: unknown;\n\n constructor(errors: GraphQLResponseError[], data: unknown) {\n const summary = errors.map((e) => e.message).join(\"; \") || \"unknown GraphQL error\";\n super(`unfazed: ${summary}`);\n this.errors = errors;\n this.data = data;\n }\n}\n\n/** A request exceeded `timeoutMs`, or the caller's AbortSignal fired. */\nexport class UnfazedTimeoutError extends UnfazedError {}\n\n/**\n * Waiting on a deploy ended badly.\n *\n * `reason` separates the two outcomes because they need different handling:\n * FAILED means the build finished and is broken (read `deployment.error` and\n * fix the source — retrying will not help), TIMEOUT means it is still BUILDING\n * and polling longer may still succeed.\n */\nexport class UnfazedDeployError extends UnfazedError {\n readonly reason: \"FAILED\" | \"TIMEOUT\";\n readonly deploymentId: string;\n /** Last deployment snapshot observed before giving up. */\n readonly deployment: unknown;\n\n constructor(\n reason: \"FAILED\" | \"TIMEOUT\",\n deploymentId: string,\n deployment: unknown,\n message: string,\n ) {\n super(message);\n this.reason = reason;\n this.deploymentId = deploymentId;\n this.deployment = deployment;\n }\n}\n\n/**\n * Waiting on a run timed out.\n *\n * Unlike {@link UnfazedDeployError}, a run's FAILED status is never thrown\n * here: {@link import(\"./unfazed.js\").Unfazed.waitForRun} resolves on\n * SUCCEEDED, FAILED and CANCELLED alike, because a run that executed and\n * exited non-zero is a completed answer — read `run.error`/`run.exitCode` —\n * not a broken build the way a FAILED deployment is. TIMEOUT is the only\n * outcome still worth an exception: the run is, as far as the last poll\n * showed, still QUEUED or RUNNING, and waiting longer may yet succeed.\n */\nexport class UnfazedRunError extends UnfazedError {\n readonly reason: \"TIMEOUT\";\n readonly runId: string;\n /** Last run snapshot observed before giving up. */\n readonly run: unknown;\n\n constructor(reason: \"TIMEOUT\", runId: string, run: unknown, message: string) {\n super(message);\n this.reason = reason;\n this.runId = runId;\n this.run = run;\n }\n}\n\n/**\n * A rollback's compare-and-swap guard was rejected: the alias moved between\n * reading the current release and swapping it.\n *\n * Recovery is always \"re-list releases and rebuild the guard\" — never \"reuse\n * the old tuple\", because the server checks the holder id, epoch AND opaque\n * revision to close same-id ABA. `Unfazed.rollback` does that re-list for you\n * once by default.\n */\nexport class UnfazedConflictError extends UnfazedError {\n readonly errors: GraphQLResponseError[];\n\n constructor(message: string, errors: GraphQLResponseError[]) {\n super(message);\n this.errors = errors;\n }\n}\n\n/** The requested alias, deployment or release does not exist (or was pruned). */\nexport class UnfazedNotFoundError extends UnfazedError {}\n\n/**\n * The server's exact \"no such workspace\" sentinel.\n *\n * A missing workspace and a workspace belonging to another tenant return the\n * SAME message on purpose, so the API cannot be used to probe for someone\n * else's ids. Treat it as \"not yours to see\", not strictly as \"absent\".\n */\nexport const WORKSPACE_NOT_FOUND = \"workspace was not found\";\n\n/** The server's exact \"no such deployment\" sentinel. See {@link WORKSPACE_NOT_FOUND}. */\nexport const DEPLOYMENT_NOT_FOUND = \"deployment was not found\";\n\n/**\n * Does this error carry EXACTLY the given not-found sentinel?\n *\n * Exact equality, deliberately. The tempting version — \"does any message\n * contain 'not found'\" — also swallows `release not found in history`,\n * `integration not found` and every future message that happens to use the\n * phrase, turning real failures into a silent `false` from a delete. These two\n * strings are stable authz sentinels with test coverage on the server side;\n * anything else is a genuine error and must keep throwing.\n */\nexport function isNotFoundSentinel(error: unknown, sentinel: string): boolean {\n return (\n error instanceof UnfazedGraphQLError &&\n error.errors.some((e) => e.message.trim() === sentinel)\n );\n}\n\n/**\n * Message shape of the server's per-owner duplicate workspace name error:\n * `a workspace named \"x\" already exists; reuse it (id <uuid>) or pick a\n * different name`.\n *\n * Used only to turn a lost `ensureWorkspace` race into a re-read. Requiring\n * both \"workspace\" and \"already exists\" in one clause keeps it off unrelated\n * conflicts (a duplicate ALIAS, say), and a false negative is safe: the\n * caller just gets the server's error verbatim.\n */\nconst DUPLICATE_WORKSPACE_NAME = /\\bworkspace\\b[^.;]{0,80}\\balready exists\\b/i;\n\n/** Did `createWorkspace` fail because the name is already taken by this owner? */\nexport function isDuplicateWorkspaceName(error: unknown): boolean {\n return (\n error instanceof UnfazedGraphQLError &&\n error.errors.some((e) => DUPLICATE_WORKSPACE_NAME.test(e.message))\n );\n}\n\nfunction truncate(s: string, max = 500): string {\n return s.length <= max ? s : `${s.slice(0, max)}…`;\n}\n\n/**\n * Message shapes that mean \"the alias moved under you\".\n *\n * The first two are the platform's ACTUAL strings. A rejected rollback guard\n * returns `store.ErrAliasMoved` verbatim — every guard point in the rollback\n * and adopt paths returns that one sentinel unwrapped — and the staged-service\n * rollback wraps the same guard with its own prefix. The rest are defensive:\n * the guard ARGUMENTS named back at you, or the alias-revision concept in any\n * spelling, or `alias` sitting in the same clause as a conflict verb, so a\n * rewording on the server side does not silently disable the retry.\n *\n * Deliberately NOT matched: a bare \"conflict\", \"changed\" or \"stale\" anywhere in\n * any message. Those fire on errors that have nothing to do with the alias\n * compare-and-swap, and a false positive is not free — it spends a pointless\n * re-list AND rewraps an unrelated failure as {@link UnfazedConflictError},\n * whose message would then blame concurrent deploys for, say, a permission\n * problem.\n */\nconst CAS_PATTERNS: RegExp[] = [\n /alias was repointed by another change/i,\n /became stale before alias change/i,\n /expected[\\s_-]*current/i,\n /alias[\\s_-]*revision/i,\n /\\balias\\b[^.;]{0,80}\\b(?:moved|repointed|changed|conflicts?|stale|mismatch)\\b/i,\n /\\b(?:moved|repointed|changed|conflicts?|stale|mismatch)\\b[^.;]{0,80}\\balias\\b/i,\n];\n\n/**\n * Heuristic: does this GraphQL error set look like a rejected compare-and-swap?\n *\n * The server carries no machine-readable code in `extensions` — its error\n * presenter returns gqlgen's default untouched — so this has to match message\n * text. It is used ONLY to decide whether to re-list and retry once: a false\n * negative costs a surfaced error the caller can retry by hand, a false\n * positive costs an extra read and a misleading error class. Neither can\n * promote the wrong release, because the retry rebuilds the guard from a fresh\n * read and the server re-validates it.\n */\nexport function looksLikeCASConflict(errors: GraphQLResponseError[]): boolean {\n return errors.some((e) => CAS_PATTERNS.some((pattern) => pattern.test(e.message)));\n}\n"]}
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Error taxonomy.\n *\n * Everything the SDK throws is an {@link UnfazedError}, so a code-mode script\n * can `catch (e) { if (e instanceof UnfazedError) ... }` and still\n * discriminate on the subclass to decide whether to retry.\n */\n\n/** A single entry from a GraphQL response's `errors` array. */\nexport interface GraphQLResponseError {\n message: string;\n path?: (string | number)[];\n extensions?: Record<string, unknown>;\n}\n\n/** Base class for everything this SDK throws. */\nexport class UnfazedError extends Error {\n constructor(message: string, options?: { cause?: unknown }) {\n super(message, options);\n this.name = new.target.name;\n }\n}\n\n/** No token was passed and no token environment variable was set. */\nexport class UnfazedAuthError extends UnfazedError {}\n\n/** The runtime has no global `fetch` and none was injected. */\nexport class UnfazedConfigError extends UnfazedError {}\n\n/**\n * The endpoint answered with a non-2xx status. `status` is the HTTP status and\n * `body` the response text, which is where the platform reports non-GraphQL\n * failures such as an expired or wrong-scope bearer token.\n */\nexport class UnfazedHTTPError extends UnfazedError {\n readonly status: number;\n readonly body: string;\n\n constructor(status: number, body: string, url: string) {\n super(`unfazed: HTTP ${status} from ${url}${body ? `: ${truncate(body)}` : \"\"}`);\n this.status = status;\n this.body = body;\n }\n}\n\n/**\n * The endpoint answered 200 with a GraphQL `errors` array.\n *\n * Partial `data` is preserved deliberately: the batched writeFiles document\n * resolves each aliased field independently, so one path can fail while the\n * others commit, and the caller needs to see which landed.\n */\nexport class UnfazedGraphQLError extends UnfazedError {\n readonly errors: GraphQLResponseError[];\n readonly data: unknown;\n\n constructor(errors: GraphQLResponseError[], data: unknown) {\n const summary = errors.map((e) => e.message).join(\"; \") || \"unknown GraphQL error\";\n super(`unfazed: ${summary}`);\n this.errors = errors;\n this.data = data;\n }\n}\n\n/** A request exceeded `timeoutMs`, or the caller's AbortSignal fired. */\nexport class UnfazedTimeoutError extends UnfazedError {}\n\n/**\n * Waiting on a deploy ended badly.\n *\n * `reason` separates the two outcomes because they need different handling:\n * FAILED means the build finished and is broken (read `deployment.error` and\n * fix the source — retrying will not help), TIMEOUT means it is still BUILDING\n * and polling longer may still succeed.\n */\nexport class UnfazedDeployError extends UnfazedError {\n readonly reason: \"FAILED\" | \"TIMEOUT\";\n readonly deploymentId: string;\n /** Last deployment snapshot observed before giving up. */\n readonly deployment: unknown;\n\n constructor(\n reason: \"FAILED\" | \"TIMEOUT\",\n deploymentId: string,\n deployment: unknown,\n message: string,\n ) {\n super(message);\n this.reason = reason;\n this.deploymentId = deploymentId;\n this.deployment = deployment;\n }\n}\n\n/**\n * Waiting on a run timed out.\n *\n * Unlike {@link UnfazedDeployError}, a run's FAILED status is never thrown\n * here: {@link import(\"./unfazed.js\").Unfazed.waitForRun} resolves on\n * SUCCEEDED, FAILED and CANCELLED alike, because a run that executed and\n * exited non-zero is a completed answer — read `run.error`/`run.exitCode` —\n * not a broken build the way a FAILED deployment is. TIMEOUT is the only\n * outcome still worth an exception: the run is, as far as the last poll\n * showed, still QUEUED or RUNNING, and waiting longer may yet succeed.\n */\nexport class UnfazedRunError extends UnfazedError {\n readonly reason: \"TIMEOUT\";\n readonly runId: string;\n /** Last run snapshot observed before giving up. */\n readonly run: unknown;\n\n constructor(reason: \"TIMEOUT\", runId: string, run: unknown, message: string) {\n super(message);\n this.reason = reason;\n this.runId = runId;\n this.run = run;\n }\n}\n\n/**\n * A rollback's compare-and-swap guard was rejected: the alias moved between\n * reading the current release and swapping it.\n *\n * Recovery is always \"re-list releases and rebuild the guard\" — never \"reuse\n * the old tuple\", because the server checks the holder id, epoch AND opaque\n * revision to close same-id ABA. `Unfazed.rollback` does that re-list for you\n * once by default.\n */\nexport class UnfazedConflictError extends UnfazedError {\n readonly errors: GraphQLResponseError[];\n\n constructor(message: string, errors: GraphQLResponseError[]) {\n super(message);\n this.errors = errors;\n }\n}\n\n/** The requested alias, deployment or release does not exist (or was pruned). */\nexport class UnfazedNotFoundError extends UnfazedError {}\n\n/**\n * What the `unfazed` CLI backend's World (`createWorld()`) throws.\n *\n * Its `name` is the Workflow SDK error a World would throw in its place\n * (`WorkflowWorldError`, `WorkflowRunNotFoundError`, `HookNotFoundError`),\n * because Workflow SDK's CLI and web UI pick their message by `name`,\n * `status` and `code`: a missing run reads as a 404 there, an argument the\n * backend cannot serve (`code: \"INVALID_ARGUMENT\"`) prints as given.\n * `instanceof UnfazedError` still holds.\n */\nexport class UnfazedInspectError extends UnfazedError {\n /** HTTP-like status: 404 for a missing run, step, event, hook or app; 401/403 for a refused token. */\n readonly status: number | undefined;\n /** `INVALID_ARGUMENT`, `READ_ONLY`, or undefined. */\n readonly code: string | undefined;\n /** The run, for `WorkflowRunNotFoundError`. */\n readonly runId: string | undefined;\n\n constructor(\n message: string,\n options: {\n name?: \"WorkflowWorldError\" | \"WorkflowRunNotFoundError\" | \"HookNotFoundError\";\n status?: number;\n code?: string;\n runId?: string;\n cause?: unknown;\n } = {},\n ) {\n super(message, options.cause === undefined ? undefined : { cause: options.cause });\n this.name = options.name ?? \"WorkflowWorldError\";\n this.status = options.status;\n this.code = options.code;\n this.runId = options.runId;\n }\n}\n\n/**\n * The server's exact \"no such workspace\" sentinel.\n *\n * A missing workspace and a workspace belonging to another tenant return the\n * SAME message on purpose, so the API cannot be used to probe for someone\n * else's ids. Treat it as \"not yours to see\", not strictly as \"absent\".\n */\nexport const WORKSPACE_NOT_FOUND = \"workspace was not found\";\n\n/** The server's exact \"no such deployment\" sentinel. See {@link WORKSPACE_NOT_FOUND}. */\nexport const DEPLOYMENT_NOT_FOUND = \"deployment was not found\";\n\n/**\n * Does this error carry EXACTLY the given not-found sentinel?\n *\n * Exact equality, deliberately. The tempting version — \"does any message\n * contain 'not found'\" — also swallows `release not found in history`,\n * `integration not found` and every future message that happens to use the\n * phrase, turning real failures into a silent `false` from a delete. These two\n * strings are stable authz sentinels with test coverage on the server side;\n * anything else is a genuine error and must keep throwing.\n */\nexport function isNotFoundSentinel(error: unknown, sentinel: string): boolean {\n return (\n error instanceof UnfazedGraphQLError &&\n error.errors.some((e) => e.message.trim() === sentinel)\n );\n}\n\n/**\n * Message shape of the server's per-owner duplicate workspace name error:\n * `a workspace named \"x\" already exists; reuse it (id <uuid>) or pick a\n * different name`.\n *\n * Used only to turn a lost `ensureWorkspace` race into a re-read. Requiring\n * both \"workspace\" and \"already exists\" in one clause keeps it off unrelated\n * conflicts (a duplicate ALIAS, say), and a false negative is safe: the\n * caller just gets the server's error verbatim.\n */\nconst DUPLICATE_WORKSPACE_NAME = /\\bworkspace\\b[^.;]{0,80}\\balready exists\\b/i;\n\n/** Did `createWorkspace` fail because the name is already taken by this owner? */\nexport function isDuplicateWorkspaceName(error: unknown): boolean {\n return (\n error instanceof UnfazedGraphQLError &&\n error.errors.some((e) => DUPLICATE_WORKSPACE_NAME.test(e.message))\n );\n}\n\nfunction truncate(s: string, max = 500): string {\n return s.length <= max ? s : `${s.slice(0, max)}…`;\n}\n\n/**\n * Message shapes that mean \"the alias moved under you\".\n *\n * The first two are the platform's ACTUAL strings. A rejected rollback guard\n * returns `store.ErrAliasMoved` verbatim — every guard point in the rollback\n * and adopt paths returns that one sentinel unwrapped — and the staged-service\n * rollback wraps the same guard with its own prefix. The rest are defensive:\n * the guard ARGUMENTS named back at you, or the alias-revision concept in any\n * spelling, or `alias` sitting in the same clause as a conflict verb, so a\n * rewording on the server side does not silently disable the retry.\n *\n * Deliberately NOT matched: a bare \"conflict\", \"changed\" or \"stale\" anywhere in\n * any message. Those fire on errors that have nothing to do with the alias\n * compare-and-swap, and a false positive is not free — it spends a pointless\n * re-list AND rewraps an unrelated failure as {@link UnfazedConflictError},\n * whose message would then blame concurrent deploys for, say, a permission\n * problem.\n */\nconst CAS_PATTERNS: RegExp[] = [\n /alias was repointed by another change/i,\n /became stale before alias change/i,\n /expected[\\s_-]*current/i,\n /alias[\\s_-]*revision/i,\n /\\balias\\b[^.;]{0,80}\\b(?:moved|repointed|changed|conflicts?|stale|mismatch)\\b/i,\n /\\b(?:moved|repointed|changed|conflicts?|stale|mismatch)\\b[^.;]{0,80}\\balias\\b/i,\n];\n\n/**\n * Heuristic: does this GraphQL error set look like a rejected compare-and-swap?\n *\n * The server carries no machine-readable code in `extensions` — its error\n * presenter returns gqlgen's default untouched — so this has to match message\n * text. It is used ONLY to decide whether to re-list and retry once: a false\n * negative costs a surfaced error the caller can retry by hand, a false\n * positive costs an extra read and a misleading error class. Neither can\n * promote the wrong release, because the retry rebuilds the guard from a fresh\n * read and the server re-validates it.\n */\nexport function looksLikeCASConflict(errors: GraphQLResponseError[]): boolean {\n return errors.some((e) => CAS_PATTERNS.some((pattern) => pattern.test(e.message)));\n}\n"]}
@@ -133,7 +133,7 @@ export declare function graphql(source: "\n query SdkListAppEnv($workspaceID: I
133
133
  /**
134
134
  * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
135
135
  */
136
- export declare function graphql(source: "\n fragment RunFields on Run {\n id\n workspaceID\n ownerId\n status\n rawStatus\n command\n harness\n prompt\n setup\n skills\n verify\n approval\n mode\n parentRunID\n depth\n question {\n id\n text\n options\n askedAt\n }\n memoryMiB\n maxSeconds\n metadata\n error\n exitCode\n result {\n status\n detail\n text\n }\n verifyExitCode\n vmID\n transcriptAvailable\n usage {\n vmSeconds\n memoryMiB\n mibSeconds\n }\n createdAt\n startedAt\n settledAt\n }\n"): typeof import('./graphql.js').RunFieldsFragmentDoc;
136
+ export declare function graphql(source: "\n fragment RunFields on Run {\n id\n workspaceID\n ownerId\n status\n rawStatus\n command\n harness\n prompt\n setup\n skills\n verify\n approval\n mode\n access\n alias\n integrations {\n kind\n bindingID\n env\n skills\n mcpServers {\n name\n transport\n readOnly\n }\n }\n parentRunID\n depth\n question {\n id\n text\n options\n askedAt\n }\n memoryMiB\n maxSeconds\n metadata\n error\n exitCode\n result {\n status\n detail\n text\n }\n verifyExitCode\n vmID\n transcriptAvailable\n usage {\n vmSeconds\n memoryMiB\n mibSeconds\n }\n createdAt\n startedAt\n settledAt\n }\n"): typeof import('./graphql.js').RunFieldsFragmentDoc;
137
137
  /**
138
138
  * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
139
139
  */
@@ -174,3 +174,7 @@ export declare function graphql(source: "\n mutation SdkAnswerRun($id: ID!, $an
174
174
  * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
175
175
  */
176
176
  export declare function graphql(source: "\n mutation SdkFollowUpRun($input: FollowUpRunInput!) {\n followUpRun(input: $input) {\n ...RunFields\n }\n }\n"): typeof import('./graphql.js').SdkFollowUpRunDocument;
177
+ /**
178
+ * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
179
+ */
180
+ export declare function graphql(source: "\n query SdkRunHarnesses {\n runHarnesses {\n name\n title\n summary\n requires\n }\n }\n"): typeof import('./graphql.js').SdkRunHarnessesDocument;