Files
Nathan Rajlich e8bc7d6aad feat: decrypt sealed payloads in the dashboard and CLI (#3146)
* feat: decrypt sealed payloads in the dashboard and CLI

Without this, any payload another run sealed to this one renders as a lock
icon with no way to open it — a visible regression for anyone debugging a
run that received a cross-deployment hook resumption. The user is entitled
to read the data and has already supplied the key; only the plumbing was
missing.

`hydrateDataWithKey` now delegates to the envelope layer, which dispatches
on the format prefix, instead of unconditionally running AES-GCM. All four
o11y key-resolution sites (web-shared hydration, the web stream reader,
and both CLI `--decrypt` paths) resolve the full capability rather than
just the symmetric key. Each already had the raw 32 bytes in hand, so this
costs one extra derivation and no additional requests.

A caller that supplies only a symmetric key still gets the ciphertext
placeholder for sealed payloads rather than a decryption error, since that
key never could have opened them.

**Browser bundling.** The obvious import for the new helper is
`@workflow/core/serialization`, but that module graph reaches `node:util`
and `node:async_hooks` and cannot be bundled for the browser — which is
what `@workflow/core/serialization-format` exists to avoid. The key
helpers are re-exported from that browser-safe entrypoint instead, and the
two browser consumers import from there; the CLI keeps the direct import
since it runs on Node. Verified by walking the built import graph: the
entrypoint reaches 6 modules and zero Node built-ins.

Unrelated: `pnpm --filter @workflow/web build` currently fails on `main`
too (`reducers/common.js` importing `node:util`). Turbo caching had been
hiding it; touching core caused a cache miss that surfaced it. Not
addressed here.

* review: narrow the o11y decrypt key type and dedupe an import

- `hydrateDataWithKey` accepted `PayloadKey`, which includes `SealTarget`.
  A seal target holds only a public key, so it can open neither scheme —
  passing one compiled fine and then always failed at runtime. Added a
  `DecryptionKey` alias (`CryptoKey | RunPayloadKeys`) and narrowed the
  signature, so that misuse is now a compile error. A `@ts-expect-error`
  test pins the guarantee.
- `hydrateResourceIOAsync` dynamically imported
  `@workflow/core/serialization-format` twice. Destructure both bindings
  from the single existing import instead.

* review: record @workflow/web in the changeset

This PR changes the dashboard's stream reader
(`packages/web/app/lib/hooks/use-stream-reader.ts`) so it dispatches on the
envelope format and can read sealed (`encp`) frames, but the changeset listed
only core, web-shared and cli.

`@workflow/web` is published, so without an entry the change would still ship —
just as an incidental dependency bump, with nothing in that package's release
notes explaining that sealed-stream decryption landed.
2026-07-28 00:36:15 +00:00
..
2026-07-22 09:56:48 -07:00
2025-10-23 12:07:52 +03:00
2026-07-22 09:56:48 -07:00

@workflow/web

Observability Web UI Package bundled in the Workflow SDK.

Self-hosting

Security notice: The @workflow/web package does not include authentication or authorization. All users who can reach the deployment share the same backend credentials configured via environment variables. If you self-host this UI, you must place it behind your own authentication layer (e.g. VPN, reverse proxy with auth, OAuth). Exposing it to untrusted users without authentication is at your own risk and may allow unauthorized access to your workflow data.

While this UI is bundled with the Workflow CLI, you can also self-host it.

There are multiple approaches:

  1. Run npx workflow web --noBrowser on a server
  2. Clone this repo and deploy as a normal Next.js app
  3. Deploy the published @workflow/web package

All options require the environment to be configured with the right environment variables for the World you're using.

Option 1: Run with the CLI

npx workflow web --noBrowser

This will start the web UI on the default port 3456.

Option 2: Clone and deploy

  • Build and deploy like any Next.js app.
  • Configure the backend via environment variables (same variables the CLI writes).

Option 3: Deploy the published @workflow/web package

The published @workflow/web package contains a prebuilt .next directory and package.json. You can install it and run next start from the package directory.

Example (Node server / container):

mkdir workflow-observability-ui
cd workflow-observability-ui

npm init -y
# You must provide React runtime dependencies in your host project.
npm i @workflow/web react react-dom

# Run Next.js from the installed package (it contains the .next output)
cd node_modules/@workflow/web
npx --yes next start -p "${PORT:-3456}"

If you prefer, you can set a start script in your host package.json like:

{
  "scripts": {
    "start": "cd node_modules/@workflow/web && next start -p $PORT"
  }
}

Configuration (environment variables)

The UI reads configuration on the server via environment variables.

  • Vercel (remote observability):

    • WORKFLOW_TARGET_WORLD=vercel
    • WORKFLOW_VERCEL_TEAM
    • WORKFLOW_VERCEL_PROJECT
    • WORKFLOW_VERCEL_ENV (optional; defaults to production)
  • Local (filesystem-backed observability):

    • WORKFLOW_TARGET_WORLD=local
    • WORKFLOW_LOCAL_DATA_DIR (absolute path to the workflow data dir, e.g. /path/to/project/.workflow-data)
    • WORKFLOW_MANIFEST_PATH (optional; enables the Graph tab)
  • Postgres:

    • WORKFLOW_TARGET_WORLD=postgres
    • WORKFLOW_POSTGRES_URL

For a complete list and CLI flags, see npx workflow inspect --help and npx workflow web --help.

If you're deploying this to Vercel, setting WORKFLOW_TARGET_WORLD to vercel is enough for the server to infer your other project details at runtime. Note that observability will be scoped to the project and environment you're deploying to.