unfazed 0.6.0 → 0.6.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/README.md CHANGED
@@ -20,8 +20,11 @@ SDK: one script. Three requests. You read the four lines you cared about.
20
20
  - Zero runtime dependencies. Installing `unfazed` installs exactly one package.
21
21
  (The optional [`unfazed/workflow`](#durable-workflows-unfazedworkflow) subpath
22
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)).
23
+ - `npx unfazed login` signs you in from a terminal in the browser; the SDK then
24
+ acts as you, with no token to copy ([details](#sign-in-from-a-terminal-npx-unfazed)).
25
+ - `npx unfazed workflow inspect runs --app <alias>`: Workflow SDK's CLI and web
26
+ UI read your app's runs through the platform, from any directory
27
+ ([details](#inspect-runs-from-your-terminal)).
25
28
  - Strict TypeScript, generated from the platform's own GraphQL schema.
26
29
  - Bun-first, runs unchanged on Node >= 20 and Deno.
27
30
 
@@ -33,27 +36,79 @@ bun add unfazed # or: npm i unfazed
33
36
 
34
37
  ## Auth
35
38
 
36
- Get a token from <https://unfazed.dev/auth.md> (or `POST
37
- https://unfazed.dev/agents/register`). The SDK reads `UNFZ_API_TOKEN`:
39
+ **A person** signs in once from a terminal, in the browser
40
+ ([`npx unfazed login`](#sign-in-from-a-terminal-npx-unfazed)); the SDK then acts
41
+ as them, with their apps, teams and claimed agents:
42
+
43
+ ```sh
44
+ npx unfazed login
45
+ ```
46
+
47
+ **An agent or a CI job** uses a bearer token instead: an agent registers itself
48
+ at <https://unfazed.dev/auth.md> (`POST https://unfazed.dev/agents/register`).
49
+ The SDK reads `UNFZ_API_TOKEN`, which wins over a stored login:
38
50
 
39
51
  ```sh
40
52
  export UNFZ_API_TOKEN="..."
41
53
  ```
42
54
 
43
55
  ```ts
44
- const unfazed = new Unfazed(); // reads UNFZ_API_TOKEN
45
- const unfazed = new Unfazed({ token }); // or pass it explicitly
56
+ const unfazed = new Unfazed(); // UNFZ_API_TOKEN, else your `npx unfazed login`
57
+ const unfazed = new Unfazed({ token }); // or pass a token explicitly
46
58
  ```
47
59
 
48
60
  Both endpoint and token are configurable. `UNFZ_API_URL` overrides the endpoint
49
- (default `https://unfazed.dev/query`); `baseUrl` is a convenience for
50
- self-hosted control planes where only the host differs.
61
+ (default `https://unfazed.dev/query`, or the stored login's); `baseUrl` is a
62
+ convenience for self-hosted control planes where only the host differs.
51
63
 
52
64
  ```ts
53
65
  new Unfazed({ token, baseUrl: "https://staging.example.com" }); // → /query
54
66
  new Unfazed({ token, endpoint: "https://example.com/graphql" }); // exact
55
67
  ```
56
68
 
69
+ ## Sign in from a terminal: `npx unfazed`
70
+
71
+ The package's `unfazed` bin signs a person in, the way `vercel login` does. It
72
+ runs from any directory (`npx` fetches the package when the directory has no
73
+ `unfazed`), on Node >= 20.16 or Bun, with no dependencies.
74
+
75
+ ```sh
76
+ npx unfazed login # opens the browser; you sign in and allow access
77
+ npx unfazed whoami # who the SDK acts as here
78
+ npx unfazed logout # revokes the login where the server offers it, and deletes it
79
+ ```
80
+
81
+ ```
82
+ $ npx unfazed login
83
+ Opening your browser to sign in to https://unfazed.dev.
84
+ …
85
+ Signed in to https://unfazed.dev as ada (user).
86
+ The login is stored in /home/ada/.config/unfazed/credentials.json.
87
+ ```
88
+
89
+ - **How:** OAuth 2.1 with PKCE against the platform's own authorization server,
90
+ the one MCP clients sign in through. The CLI registers itself once as a
91
+ public client (cached in `oauth-clients.json` beside the login), listens on
92
+ `http://127.0.0.1:<random port>/callback` for the answer, and asks for
93
+ `unfz:read unfz:deploy` (`--scope` narrows it).
94
+ - **The login** is `${XDG_CONFIG_HOME:-~/.config}/unfazed/credentials.json`,
95
+ mode 0600: the access token (an hour long), the refresh token (30 days, rotated
96
+ on every refresh), the expiry, the endpoint and who signed in. The SDK uses it
97
+ whenever no token is passed and `UNFZ_API_TOKEN` is unset, refreshes the access
98
+ token before it expires (and once when the platform refuses it), and writes
99
+ the rotated pair back for the next process. It is only ever sent to the
100
+ endpoint it was issued by; another `UNFZ_API_URL` needs its own
101
+ `UNFZ_API_URL=… npx unfazed login`. `new UnfazedClient({ login: false })`
102
+ ignores it.
103
+ - **A browser on another machine** (an SSH session, a box): the URL is printed
104
+ instead of opened. Sign in there, then paste the address the browser ends up
105
+ on (`http://127.0.0.1:…/callback?code=…`, which does not load there) into the
106
+ terminal. `--no-browser` prints the URL on your own machine too.
107
+ - **Another control plane:** `--endpoint https://staging.example.com` (or
108
+ `UNFZ_API_URL`).
109
+ - `whoami --json` prints the same as JSON; nothing the CLI prints ever carries
110
+ a token.
111
+
57
112
  ## Code mode: deploy, wait, tail logs, in one script
58
113
 
59
114
  ```ts
@@ -499,8 +554,12 @@ production build the module:
499
554
  A complete app — steps, a retried step, `sleep`, a webhook (plain
500
555
  `createWebhook()` and `webhook.url`), a progress stream — is in
501
556
  [`examples/workflow-nitro`](./examples/workflow-nitro). Once it is deployed,
502
- `UNFZ_APP=<alias> npx workflow inspect runs --backend unfazed` lists its runs
557
+ `npx unfazed workflow inspect runs --app <alias>` lists its runs
503
558
  ([Inspect runs from your terminal](#inspect-runs-from-your-terminal)).
559
+ [`examples/birthday-card-generator`](./examples/birthday-card-generator) is
560
+ Vercel's Workflow launch demo on unfazed: a React UI built by Vite and served
561
+ from the same bundle, AI steps, webhooks resumed from emails, `sleep` until a
562
+ date, and a stream the UI reconnects to; it runs without any provider keys.
504
563
 
505
564
  ### Without Nitro
506
565
 
@@ -569,12 +628,11 @@ database connection afterwards; `wake` and `cancel` are Workflow SDK's own
569
628
  ### Inspect runs from your terminal
570
629
 
571
630
  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):
631
+ `--backend vercel` reads a Vercel project. From any directory:
574
632
 
575
633
  ```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
634
+ npx unfazed login # once
635
+ npx unfazed workflow inspect runs --app birthday-card
578
636
  ```
579
637
 
580
638
  ```
@@ -586,25 +644,38 @@ wrun_01KPBDAY00000000000000RN03 generateBirthdayCard failed 2026-10-15 09:
586
644
  ```
587
645
 
588
646
  ```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
647
+ npx unfazed workflow inspect run <run-id> --app birthday-card # input, output, error
648
+ npx unfazed workflow inspect steps --runId <run-id> --app birthday-card # also: events, hooks, sleeps, streams
649
+ npx unfazed workflow inspect step <step-id> --runId <run-id> --app birthday-card
650
+ npx unfazed workflow inspect stream <stream-id> --runId <run-id> --app birthday-card # follows it while the run writes
651
+ npx unfazed workflow cancel <run-id> --app birthday-card
652
+ npx unfazed workflow web --app birthday-card # the local web UI: trace, events, streams,
653
+ # Wake Up Sleep and Cancel
654
+ ```
655
+
656
+ `npx unfazed workflow …` is Workflow SDK's CLI with this package as its
657
+ backend: everything after `workflow` but `--app` is passed to it, and its
658
+ output and exit code come back unchanged. It runs the project's own
659
+ `workflow` (major 5) when the directory has one, else `npx -y workflow@5`.
660
+
661
+ **Inside a project** that has `unfazed` installed, the CLI's native form works
662
+ too, since the CLI resolves `--backend unfazed` from the project:
663
+
664
+ ```sh
665
+ UNFZ_APP=birthday-card npx workflow inspect runs --backend unfazed
596
666
  ```
597
667
 
598
- The usual options work (`--json`, `--limit`, `--cursor`, `--all`, `--status`,
668
+ `export WORKFLOW_TARGET_WORLD=unfazed` saves typing `--backend` there. The
669
+ usual options work (`--json`, `--limit`, `--cursor`, `--all`, `--status`,
599
670
  `--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).
671
+
672
+ - **Which app:** `--app` (`npx unfazed workflow` only), else `UNFZ_APP` (its
673
+ alias, the `<alias>` of `https://<alias>.unfz.to`), else the `alias` in
674
+ `./unfazed.json`, or its only `apps[]` entry. `UNFZ_API_URL` points at
675
+ another control plane.
676
+ - **The token** is your `npx unfazed login`, or `UNFZ_API_TOKEN` when set. It
677
+ needs `unfz:read`; cancel and Wake Up Sleep need `unfz:deploy` too, and the
678
+ caller must be allowed to restart the app (owner, team member, operator).
608
679
  - **Every command is one platform call relayed to the app**, through the same
609
680
  inspection route the dashboard reads; an `on_demand` app cold-starts for it,
610
681
  as for a visitor.
@@ -726,6 +797,7 @@ WORKFLOW_POSTGRES_URL=$UNFAZED_TEST_DATABASE_URL bun run test:conformance
726
797
  # delivery cold-started in a fresh process
727
798
  bun run test:example # the example, bundled as unfazed builds it,
728
799
  # one fresh process per delivery and request
800
+ bun run test:birthday # the same for examples/birthday-card-generator
729
801
  bun run test:cli # Workflow SDK's CLI with --backend unfazed (and --web)
730
802
  # against a fake of the platform's inspection API
731
803
  ```
@@ -0,0 +1,129 @@
1
+ import { type StoredCredentials } from "../credentials.js";
2
+ import { UnfazedError } from "../errors.js";
3
+ export declare const USER_AGENT = "unfazed-cli/0.6.1 (+https://unfazed.dev/auth.md)";
4
+ /** What a login asks for: reading and deploying, like an MCP client. */
5
+ export declare const DEFAULT_SCOPE = "unfz:read unfz:deploy";
6
+ export declare const CLIENT_NAME = "unfazed CLI";
7
+ export declare const CALLBACK_PATH = "/callback";
8
+ /** The redirect registered once; each login uses it with its own port. */
9
+ export declare const REGISTERED_REDIRECT_URI = "http://127.0.0.1/callback";
10
+ /** Registered clients, per issuer, beside the login. */
11
+ export declare const CLIENTS_FILE = "oauth-clients.json";
12
+ /** How long a login waits for the browser. */
13
+ export declare const LOGIN_TIMEOUT_MS: number;
14
+ export type Env = Record<string, string | undefined>;
15
+ /** The `fetch` the CLI needs; the global one fits. */
16
+ export type Fetch = (input: string, init: {
17
+ method: string;
18
+ headers: Record<string, string>;
19
+ body?: string;
20
+ redirect?: "manual" | "follow";
21
+ signal?: AbortSignal;
22
+ }) => Promise<{
23
+ ok: boolean;
24
+ status: number;
25
+ text(): Promise<string>;
26
+ }>;
27
+ /** A login that could not be completed; the message says why and what to do. */
28
+ export declare class UnfazedLoginError extends UnfazedError {
29
+ }
30
+ /** The GraphQL endpoint: given, else `UNFZ_API_URL`, else unfazed.dev's; a bare origin gets `/query`. */
31
+ export declare function resolveEndpoint(raw: string | undefined, env: Env): string;
32
+ /** Whether a URL may carry tokens: https, or plain http to this machine only. */
33
+ export declare function safeURL(raw: unknown): raw is string;
34
+ /** RFC 8414 authorization server metadata: the fields the CLI uses. */
35
+ export interface AuthServerMetadata {
36
+ issuer: string;
37
+ authorization_endpoint: string;
38
+ token_endpoint: string;
39
+ registration_endpoint?: string;
40
+ revocation_endpoint?: string;
41
+ code_challenge_methods_supported?: string[];
42
+ response_types_supported?: string[];
43
+ authorization_response_iss_parameter_supported?: boolean;
44
+ }
45
+ /** Reads and checks the metadata of the server at `origin` (e.g. https://unfazed.dev). */
46
+ export declare function discover(origin: string, fetch: Fetch): Promise<AuthServerMetadata>;
47
+ /** A PKCE verifier and its S256 challenge. */
48
+ export declare function pkcePair(): {
49
+ verifier: string;
50
+ challenge: string;
51
+ };
52
+ /** A client the CLI registered at one issuer. */
53
+ export interface ClientRecord {
54
+ client_id: string;
55
+ redirect_uri: string;
56
+ scope: string;
57
+ registered_at: string;
58
+ }
59
+ /** The cached client for `issuer` that may ask for `scope`, if any. */
60
+ export declare function cachedClient(env: Env, issuer: string, scope: string): ClientRecord | undefined;
61
+ export interface LoginOptions {
62
+ /** GraphQL endpoint or platform origin. Default: `UNFZ_API_URL`, else https://unfazed.dev. */
63
+ endpoint?: string;
64
+ /** Space-separated scopes. Default: `unfz:read unfz:deploy`. */
65
+ scope?: string;
66
+ /** Default `process.env`: where `UNFZ_API_URL` and the login's location come from. */
67
+ env?: Env;
68
+ fetch?: Fetch;
69
+ /** Opens the sign-in URL; resolves to whether a browser started. Default: print only. */
70
+ openBrowser?: (url: string) => boolean | Promise<boolean>;
71
+ /** Where progress goes (never a token). Default: stderr. */
72
+ log?: (line: string) => void;
73
+ /** Where a pasted answer is read from (a browser on another machine). Default: none. */
74
+ input?: NodeJS.ReadableStream;
75
+ /** How long to wait for the browser. Default {@link LOGIN_TIMEOUT_MS}. */
76
+ timeoutMs?: number;
77
+ }
78
+ /** Signs in through the browser and stores the login. Resolves to it and where it was stored. */
79
+ export declare function login(options?: LoginOptions): Promise<{
80
+ credentials: StoredCredentials;
81
+ path: string;
82
+ }>;
83
+ /** The fields `whoami` shows. */
84
+ export interface Actor {
85
+ kind: string;
86
+ userID: string;
87
+ name: string;
88
+ scopes: string[];
89
+ }
90
+ export interface WhoamiResult {
91
+ actor: Actor;
92
+ endpoint: string;
93
+ /** `token`: UNFZ_API_TOKEN; `login`: the stored login at `path`. */
94
+ source: "token" | "login";
95
+ path?: string;
96
+ }
97
+ /** Who the SDK acts as here: `UNFZ_API_TOKEN` when set, else the stored login (refreshed as needed). */
98
+ export declare function whoami(options?: {
99
+ env?: Env;
100
+ fetch?: Fetch;
101
+ }): Promise<WhoamiResult>;
102
+ export interface LogoutResult {
103
+ /** Whether there was a login to remove. */
104
+ signedOut: boolean;
105
+ issuer?: string;
106
+ /** Whether the server revoked the tokens. */
107
+ revoked: boolean;
108
+ /** Whether the server offers revocation (RFC 7009). */
109
+ revocationSupported: boolean;
110
+ }
111
+ /**
112
+ * Revokes the stored tokens when the server advertises revocation, then
113
+ * deletes the login. The endpoint is read from the server's metadata again,
114
+ * so one the server gained since the login is used.
115
+ */
116
+ export declare function logout(options?: {
117
+ env?: Env;
118
+ fetch?: Fetch;
119
+ }): Promise<LogoutResult>;
120
+ export declare function accountName(credentials: StoredCredentials): string;
121
+ /**
122
+ * Whether a browser can be expected here. Over SSH, or on Linux without a
123
+ * display, the login prints the URL instead (and takes the pasted answer).
124
+ */
125
+ export declare function canOpenBrowser(env: Env, platform?: NodeJS.Platform): boolean;
126
+ /** The command that opens `url` — `$BROWSER`, `open`, `xdg-open` or the Windows URL handler — as argv, never a shell line. */
127
+ export declare function browserCommand(url: string, env: Env, platform?: NodeJS.Platform): string[];
128
+ /** Starts the browser on `url`. Resolves to whether it could be started. */
129
+ export declare function openBrowser(url: string, env?: Env, platform?: NodeJS.Platform): Promise<boolean>;