@heyocomputer/hws 0.0.0-stage → 0.2.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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,55 @@
1
+ # Changelog
2
+
3
+ Versioned with the [`hws`](https://crates.io/crates/hws) crate; the two
4
+ releases share a wire contract and are checked against the same app-lb fixtures.
5
+
6
+ ## 0.2.0
7
+
8
+ First published release.
9
+
10
+ ### Renamed
11
+
12
+ - The package is **`@heyocomputer/hws`**, the TypeScript twin of the `hws`
13
+ crate. It was `heyctl` in-tree and never published under that name.
14
+ - `Hws` is the client class. `Heyctl` remains exported as the same class, and
15
+ `HwsOptions` as an alias of `HeyctlOptions`, so only the import changes.
16
+ - Licensed Apache-2.0, like the rest of the repository.
17
+
18
+ ### Added — parity with hws 0.2.0
19
+
20
+ - `whoami()` — the credential's tier, confinement and namespace.
21
+ - `startRollout(id, { operationId, expectedRevision, spec })` and
22
+ `rollout(id, operationId)` — replace a spec by rolling a verified pool beside
23
+ the old one.
24
+ - `discoveryStatus(id, { staged })`.
25
+ - `cordonUpstream` / `uncordonUpstream` for static upstreams.
26
+ - Namespace plugins: `namespacePlugins(ns)`, `installPlugin(ns, id, config?)`,
27
+ `uninstallPlugin(ns, id)`, `pluginInstalls(id)`.
28
+ - Telemetry through the `obs` plugin: `obs(ns)` returns an `ObsClient` with
29
+ `fleet`, `deployment`, `logs` (with the `before` page cursor), `alerts`,
30
+ `createAlert` and `deleteAlert`.
31
+ - Namespaces: `namespaces()`, `createNamespace`, `deleteNamespace`.
32
+ - Workflows: `workflows`, `workflow`, `createWorkflow`, `replaceWorkflow`,
33
+ `deleteWorkflow`.
34
+ - Auth providers: `authProviders(ns?)`, `authProvider`, `authProviderExists`,
35
+ `createAuthProvider`, `deleteAuthProvider`.
36
+ - `startMountPull(id, force)`, `disks()`, `feedRss(ns)`, `probe(path)`.
37
+ - Errors carry app-lb's machine-readable `code` when it sends one, e.g.
38
+ `plugin_not_installed` / `plugin_disabled` on a `ConflictError`.
39
+ - Types: `WhoAmI`, `RolloutOperation`, `DiscoveryStatus`, `NamespaceEntry`,
40
+ `AuthProviderView`, `NamespacePlugin`, `PluginInstalls`, `NamespaceInstall`,
41
+ `ObsFleet`, `ObsFleetRow`, `ObsDeployment`, `ObsLogs`, `ObsLogRow`,
42
+ `ObsAlert`, `ObsMetricBucket`, `ObsLogBucket`, `ObsFreshness`, `LogQuery`,
43
+ `NewAlert`; `PluginView` gains `per_namespace` and `installed_in`.
44
+
45
+ ### Fixed
46
+
47
+ - The wire-contract test now covers every fixture app-lb writes, including the
48
+ auth-provider and inherited-gate fixtures, a status's `site`, a gate's
49
+ `provider_ref` and the JWT login fields — declarations the types already had
50
+ but the test did not check.
51
+
52
+ ## 0.1.0
53
+
54
+ The in-tree client: deployments, scaling, exec and shells, secrets, tokens,
55
+ feeds, jobs, metrics, certificates and fleet plugins.
package/README.md CHANGED
@@ -1,3 +1,239 @@
1
- # Temporary Holding Version
1
+ # @heyocomputer/hws
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The TypeScript twin of [`hws`](https://crates.io/crates/hws), the app-lb SDK:
4
+ register deployments, scale pools, run commands inside a microVM, attach an
5
+ interactive shell, roll out new specs, and — through a namespace's installed
6
+ plugins — read its telemetry.
7
+
8
+ Same wire contract and the same version as the [Rust crate](../../heyctl)
9
+ (source in this repo, published as `hws` on crates.io), which also ships the
10
+ `heyctl` CLI. Both are checked against the golden fixtures app-lb's own tests
11
+ write.
12
+
13
+ ```sh
14
+ npm install @heyocomputer/hws
15
+ ```
16
+
17
+ ```ts
18
+ import { Hws } from "@heyocomputer/hws";
19
+
20
+ const lb = new Hws({
21
+ server: "127.0.0.1:9090",
22
+ token: process.env.APP_LB_TOKEN,
23
+ });
24
+
25
+ const { stdout, exit_code } = await lb.exec("sb-7f3a9c", "uname -a");
26
+ ```
27
+
28
+ Node 18+, Bun, Deno and browsers. Everything uses `fetch`; shells use
29
+ [`ws`](https://www.npmjs.com/package/ws) on the server and the native
30
+ `WebSocket` in a browser.
31
+
32
+ ## Authenticating
33
+
34
+ Prefer an **app-token**: scoped to particular deployments, revocable without
35
+ restarting app-lb, optionally expiring. Basic auth also works and is unscoped —
36
+ it is the operator credential, and the one that mints tokens.
37
+
38
+ ```ts
39
+ const admin = new Hws({ server, user: "admin", password: process.env.PW! });
40
+
41
+ const minted = await admin.mintToken({
42
+ name: "agent-runner",
43
+ admin: "admin",
44
+ deployments: ["sb-7f3a9c"],
45
+ expiresInSecs: 86_400,
46
+ });
47
+
48
+ // The secret is in the reply and nowhere else, ever — app-lb stores only its
49
+ // hash and no endpoint reads it back.
50
+ console.log(minted.token);
51
+ ```
52
+
53
+ A token scoped to specific deployments is refused the fleet-wide routes —
54
+ listing every deployment, the secret store, and minting — so it cannot widen
55
+ itself. `/metrics` is the exception: rather than refusing a scoped token it
56
+ narrows the answer to what that token can see.
57
+
58
+ ## Sandboxes
59
+
60
+ ```ts
61
+ await lb.createDeployment({
62
+ id: "sb-7f3a9c",
63
+ routes: [], // unrouted: reached by exec and shell only
64
+ vm: { driver: "firecracker", port: 8080, disk_size_gb: 20 },
65
+ scaling: { min_replicas: 0, max_replicas: 1, idle_action: "retain" },
66
+ });
67
+
68
+ await lb.waitForReady("sb-7f3a9c", {
69
+ onProgress: (p) => console.log(`${p.healthy}/${p.desired} healthy, ${p.pending} booting`),
70
+ });
71
+ ```
72
+
73
+ `waitForReady` counts **healthy** backends, not `ready` — `ready` is the size of
74
+ the pool, including a VM that is failing its health check, so waiting on it
75
+ reports success for a deployment that cannot serve a request.
76
+
77
+ ## Shells
78
+
79
+ ```ts
80
+ const shell = await lb.shell("sb-7f3a9c", { cols: 120, rows: 40 });
81
+ shell.onData((bytes) => process.stdout.write(bytes));
82
+
83
+ process.stdin.on("data", (d) => shell.write(d));
84
+ process.stdout.on("resize", () =>
85
+ shell.resize(process.stdout.columns, process.stdout.rows));
86
+
87
+ const { code, clean, error } = await shell.exit;
88
+ ```
89
+
90
+ **Check `clean`, not `code === 0`.** app-lb reports an *unknown* exit code as
91
+ `0`, which is what a VM dying under a live session looks like — so a crash and a
92
+ logout are the same number. `clean` is false when an error preceded the exit.
93
+
94
+ **There is no resume.** If the socket drops the session is gone; reconnecting
95
+ gives a *new* shell. This package will not silently retry, because a retry that
96
+ quietly discards a session is worse than an error.
97
+
98
+ ### In a browser
99
+
100
+ A browser's `WebSocket` constructor cannot set headers, so a browser shell needs
101
+ an **app-token**, which travels in the query string. app-lb accepts
102
+ `?app_token=` on the shell route and nowhere else, for exactly that reason.
103
+
104
+ A credential in a URL lands in access logs, proxy logs and browser history, so
105
+ mint a short-lived one:
106
+
107
+ ```ts
108
+ const ticket = await admin.mintToken({
109
+ name: `terminal ${user}`,
110
+ admin: "admin",
111
+ deployments: [sandboxId],
112
+ expiresInSecs: 120,
113
+ });
114
+ // hand `ticket.token` to the page, which opens the shell with it
115
+ ```
116
+
117
+ Basic credentials cannot be used for a browser shell at all, and this package
118
+ throws rather than silently failing the upgrade.
119
+
120
+ ## Errors
121
+
122
+ Every failure is a subclass of `HeyctlError` carrying `status`, `retryable`
123
+ and `isAuth`:
124
+
125
+ | | |
126
+ |---|---|
127
+ | `UnauthorizedError` | 401 — missing, wrong, revoked or expired. app-lb does not distinguish those, so token ids cannot be enumerated. |
128
+ | `ForbiddenError` | 403 — the credential was good, the scope was not. Re-presenting it will not help. |
129
+ | `NotFoundError` | 404, carrying `kind` and the name |
130
+ | `NoRunningVmError` | 409 from `exec`/`shell` with `wake: false` |
131
+ | `ConflictError` | 409 — a job is already running, a secret is still referenced, a rollout's revision moved. `code` carries app-lb's reason when it gives one, e.g. `plugin_not_installed` / `plugin_disabled` |
132
+ | `ColdStartTimeoutError` | 503 — no VM appeared in time. `retryable`. |
133
+ | `UpstreamError` | 502 — the daemon failed. `retryable`. |
134
+ | `MalformedResponseError` | a response that could not be interpreted |
135
+
136
+ ```ts
137
+ try {
138
+ await lb.exec("sb-1", "make test", { wake: false });
139
+ } catch (e) {
140
+ if (e instanceof NoRunningVmError) {
141
+ await lb.exec("sb-1", "make test"); // wake it after all
142
+ } else if (e.retryable) {
143
+ // ColdStartTimeout, Upstream, Transport
144
+ } else throw e;
145
+ }
146
+ ```
147
+
148
+ Note app-lb answers a failed request in **two** shapes: `{"error": "…"}` from
149
+ its own handlers, and plain text for the 401 and every framework-level rejection
150
+ (415 for a missing content-type, 422 for well-formed JSON of the wrong shape).
151
+ This package parses both, so a caller never sees a JSON syntax error where an
152
+ HTTP status was the actual answer.
153
+
154
+ ## Three things this package does not smooth over
155
+
156
+ **A non-zero exit resolves.** `exec` rejects only when the command could not be
157
+ *run*. Check `exit_code`.
158
+
159
+ **`exec`'s timeout does not kill anything.** `timeoutSecs` bounds app-lb's call
160
+ to the daemon; when it expires you get an `UpstreamError` and **the command
161
+ keeps running in the guest**. The daemon offers no streaming and no
162
+ cancellation, so output is buffered until it exits. The client-side deadline is
163
+ sized to outlast the server's worst case — the command timeout plus a possible
164
+ cold start — because abandoning a request app-lb is still serving turns a
165
+ well-defined answer into an unexplained transport error.
166
+
167
+ **Registering a deployment does not mean it has a certificate.** ACME issuance
168
+ is asynchronous; poll `certs()`.
169
+
170
+ ## Rollouts
171
+
172
+ `replaceDeployment` recycles the pool in place. To roll a new spec beside the
173
+ old pool, verify it, and only then drain the old one:
174
+
175
+ ```ts
176
+ const { rollout_revision, spec } = await lb.deployment("api");
177
+ spec.vm!.image = "api-v2";
178
+ const op = await lb.startRollout("api", {
179
+ operationId: crypto.randomUUID(), // retry a lost reply with the same id
180
+ expectedRevision: rollout_revision!, // 409 if anything changed it since
181
+ spec,
182
+ });
183
+ let now = op;
184
+ while (now.status === "running") now = await lb.rollout("api", op.operation_id);
185
+ ```
186
+
187
+ ## Namespace plugins and telemetry
188
+
189
+ Some plugins install per namespace. The operator enables one for the fleet; a
190
+ namespace administrator installs it. Installing `obs` makes app-obs collect
191
+ every deployment in the namespace, readable with the namespace's own token:
192
+
193
+ ```ts
194
+ await lb.installPlugin("team-a", "obs");
195
+ const obs = lb.obs("team-a");
196
+ const fleet = await obs.fleet({ window: "1h" });
197
+ let page = await obs.logs("web", { level: "error", limit: 100 });
198
+ while (page.next_before_ms !== null) {
199
+ page = await obs.logs("web", { level: "error", limit: 100, before: page.next_before_ms });
200
+ }
201
+ ```
202
+
203
+ Before it is installed every `obs` call rejects with a `ConflictError` whose
204
+ `code` is `plugin_not_installed` (or `plugin_disabled` while the operator has it
205
+ off).
206
+
207
+ `whoami()` says what the server makes of the credential: its tier, whether it is
208
+ confined, and to which namespace.
209
+
210
+ ## Building deployments
211
+
212
+ Writes take the spec as an object and send it verbatim. Read with
213
+ `deployment()`, edit `status.spec`, and pass that back — `PUT` replaces the
214
+ *whole* spec, so anything dropped in between is genuinely dropped.
215
+
216
+ ```ts
217
+ const { spec } = await lb.deployment("api");
218
+ spec.vm!.image = "api-v2";
219
+ await lb.replaceDeployment("api", spec);
220
+ ```
221
+
222
+ ## Development
223
+
224
+ ```sh
225
+ npm install
226
+ npm test # builds, then unit tests + the wire-contract check
227
+ ```
228
+
229
+ The wire-contract test reads `testdata/wire/*.json` — fixtures written by
230
+ app-lb's own response types — and asserts every key in them is declared in
231
+ `src/types.ts`. To a JS client an unknown field and an absent one look
232
+ identical, which is exactly how five fields once went missing from the Rust
233
+ client without a test failing. A field app-lb starts sending now fails this test
234
+ instead of going silently unread.
235
+
236
+ `examples/e2e.mjs` runs the whole surface against a live app-lb.
237
+
238
+ `Heyctl` is still exported as an alias of `Hws`, so code written against the
239
+ pre-0.2 package name keeps compiling after changing its import.
@@ -0,0 +1,371 @@
1
+ import { type Credential } from "./errors.js";
2
+ import { ObsClient } from "./obs.js";
3
+ import type { AdminScope, AuthProviderView, CertStatus, DiscoveryStatus, DiskInventory, NamespaceEntry, NamespacePlugin, PluginInstalls, RolloutOperation, UpstreamTrafficStatus, WhoAmI, WorkflowSpec, DeploymentSpec, DeploymentStatus, EvictOutcome, ExecOutput, JobRecord, MetricsResponse, MintedToken, PluginView, Ingress, SecretSummary, TokenSummary, FeedEvent, FeedIndexEntry } from "./types.js";
4
+ import type { Shell, ShellOptions } from "./shell.js";
5
+ import type { WaitForJobOptions, WaitForReadyOptions } from "./wait.js";
6
+ /** app-lb's own default `cold_start_timeout_secs`. */
7
+ export declare const ASSUMED_COLD_START_MS = 120000;
8
+ /** How to authenticate. */
9
+ export type Auth = {
10
+ kind: "none";
11
+ }
12
+ /** The operator credential. Unscoped, and the one that mints tokens. */
13
+ | {
14
+ kind: "basic";
15
+ user: string;
16
+ password: string;
17
+ }
18
+ /** An app-token. Scoped, revocable — the normal choice for a program. */
19
+ | {
20
+ kind: "token";
21
+ token: string;
22
+ };
23
+ export interface HeyctlOptions {
24
+ /** A URL, or a bare `host:port`. */
25
+ server: string;
26
+ /** Shorthand for `auth: { kind: "token", token }`. */
27
+ token?: string;
28
+ user?: string;
29
+ password?: string;
30
+ auth?: Auth;
31
+ /** Per-request deadline. `exec` computes its own, larger, one. */
32
+ timeoutMs?: number;
33
+ /** Swapped in for tests. Defaults to the global `fetch`. */
34
+ fetch?: typeof globalThis.fetch;
35
+ }
36
+ /** Which admin routes a server is gating. */
37
+ export interface Gates {
38
+ view: boolean;
39
+ crud: boolean;
40
+ }
41
+ export interface ExecOptions {
42
+ cwd?: string;
43
+ env?: Record<string, string>;
44
+ /** Bounds app-lb's call to the daemon. Clamped server-side to 1..3600. */
45
+ timeoutSecs?: number;
46
+ /** Boot or resume a VM if none is running. Default true. */
47
+ wake?: boolean;
48
+ /** Override the client-side deadline. */
49
+ patienceMs?: number;
50
+ signal?: AbortSignal;
51
+ }
52
+ export interface MetricsQuery {
53
+ deployment?: string;
54
+ prefix?: string;
55
+ /** Drop per-VM detail, which is most of the payload. */
56
+ summary?: boolean;
57
+ limit?: number;
58
+ offset?: number;
59
+ }
60
+ export interface NewToken {
61
+ name: string;
62
+ admin?: AdminScope;
63
+ /**
64
+ * Confine the token to one namespace. With no `deployments` it reaches
65
+ * every deployment in the namespace — and nothing outside it, ever.
66
+ */
67
+ namespace?: string;
68
+ /** Deployment ids, or `["*"]`. Defaults to none, which can reach nothing. */
69
+ deployments?: string[];
70
+ expiresInSecs?: number;
71
+ /**
72
+ * Valid on every server that mirrors this control plane's tokens, not only
73
+ * the one minting it. Only a control-plane app-lb (one with gateways
74
+ * configured) accepts it; anywhere else the mint is refused with 409.
75
+ */
76
+ allServers?: boolean;
77
+ }
78
+ /** Accept `host:port` as well as a URL, and drop a trailing slash. */
79
+ export declare function normalizeServer(server: string): string;
80
+ /**
81
+ * A client for one app-lb.
82
+ *
83
+ * ```ts
84
+ * const lb = new Heyctl({ server: "127.0.0.1:9090", token: process.env.APP_LB_TOKEN });
85
+ * const { stdout } = await lb.exec("sb-7f3a9c", "uname -a");
86
+ * ```
87
+ */
88
+ export declare class Heyctl {
89
+ readonly server: string;
90
+ /** @internal */ readonly auth: Auth;
91
+ private readonly timeoutMs;
92
+ private readonly doFetch;
93
+ constructor(opts: HeyctlOptions);
94
+ /**
95
+ * The exact `Authorization` header, or undefined.
96
+ *
97
+ * @internal app-lb compares the Basic header **byte for byte** against a
98
+ * string it precomputed at startup — it never base64-decodes it. So this must
99
+ * be standard base64 with padding, one space after `Basic`, that
100
+ * capitalisation. A re-encoded-but-equivalent header is rejected.
101
+ */
102
+ authHeader(): string | undefined;
103
+ /** @internal */
104
+ credential(): Credential;
105
+ private send;
106
+ /** @internal Raw JSON for a route, with failures raised as typed errors. */
107
+ request<T>(method: string, path: string, opts?: {
108
+ body?: unknown;
109
+ kind?: string;
110
+ name?: string;
111
+ timeoutMs?: number;
112
+ signal?: AbortSignal;
113
+ /**
114
+ * Whether a success carries JSON. Not every route does: `/healthz`
115
+ * answers `ok\n` as plain text, and a `204` has no body at all — parsing
116
+ * those would turn a perfectly good response into a syntax error.
117
+ */
118
+ expect?: "json" | "nothing";
119
+ }): Promise<T>;
120
+ /** Never gated, so this proves reachability without a credential. */
121
+ healthz(): Promise<void>;
122
+ /**
123
+ * Which tiers this server gates, probed **anonymously** — the question is
124
+ * what an unauthenticated caller is refused, which is what says whether the
125
+ * gate is on at all.
126
+ */
127
+ gates(): Promise<Gates>;
128
+ /**
129
+ * What the server makes of this client's credential: its tier, whether it is
130
+ * confined, and to which namespace. Needs no tier, so it answers even for a
131
+ * token every other route refuses.
132
+ */
133
+ whoami(signal?: AbortSignal): Promise<WhoAmI>;
134
+ /**
135
+ * The status a `GET` answers with, treating a 4xx as an answer rather than an
136
+ * error, plus whatever the server said about it (`error — detail`).
137
+ */
138
+ probe(path: string, signal?: AbortSignal): Promise<{
139
+ status: number;
140
+ detail?: string;
141
+ }>;
142
+ deployments(signal?: AbortSignal): Promise<DeploymentStatus[]>;
143
+ deployment(id: string, signal?: AbortSignal): Promise<DeploymentStatus>;
144
+ /** Whether a deployment exists, without treating absence as an error. */
145
+ deploymentExists(id: string): Promise<boolean>;
146
+ /**
147
+ * Register or replace a deployment.
148
+ *
149
+ * app-lb answers `201` even when this replaced an existing one, so the status
150
+ * does not distinguish create from update. Certificate issuance is
151
+ * asynchronous: success here does not mean a certificate exists yet.
152
+ */
153
+ createDeployment(spec: DeploymentSpec, signal?: AbortSignal): Promise<DeploymentStatus>;
154
+ /**
155
+ * Replace a whole spec.
156
+ *
157
+ * `PUT` replaces everything, so read with {@link deployment}, edit
158
+ * `status.spec`, and pass that back — anything dropped in between is
159
+ * genuinely dropped.
160
+ */
161
+ replaceDeployment(id: string, spec: DeploymentSpec, signal?: AbortSignal): Promise<DeploymentStatus>;
162
+ /** A shallow merge onto the current scaling policy. Managed pools only. */
163
+ patchScaling(id: string, patch: Record<string, unknown>, signal?: AbortSignal): Promise<DeploymentStatus>;
164
+ deleteDeployment(id: string, signal?: AbortSignal): Promise<void>;
165
+ /** Evict one VM. `force` kills immediately; otherwise it drains. */
166
+ evictVm(id: string, sandboxId: string, force?: boolean, signal?: AbortSignal): Promise<EvictOutcome>;
167
+ /**
168
+ * Stop new requests to one static upstream. Existing requests finish; watch
169
+ * `in_flight` on the result to observe the drain.
170
+ */
171
+ cordonUpstream(id: string, upstream: string, opts?: {
172
+ force?: boolean;
173
+ reason?: string;
174
+ signal?: AbortSignal;
175
+ }): Promise<UpstreamTrafficStatus>;
176
+ /** Remove an administrative drain. An unhealthy upstream stays excluded until it recovers. */
177
+ uncordonUpstream(id: string, upstream: string, signal?: AbortSignal): Promise<UpstreamTrafficStatus>;
178
+ /**
179
+ * Replace a managed deployment's spec by rolling a fresh pool beside the old
180
+ * one, verifying it, and only then draining the old one.
181
+ *
182
+ * `expectedRevision` is the deployment's `rollout_revision` as last read; the
183
+ * rollout is refused (409) if anything changed it since. `operationId` makes
184
+ * the call idempotent — retry a lost reply with the same id. Answers `202`;
185
+ * poll {@link rollout} until `status` is no longer `running`.
186
+ */
187
+ startRollout(id: string, req: {
188
+ operationId: string;
189
+ expectedRevision: string;
190
+ spec: DeploymentSpec | Record<string, unknown>;
191
+ }, signal?: AbortSignal): Promise<RolloutOperation>;
192
+ rollout(id: string, operationId: string, signal?: AbortSignal): Promise<RolloutOperation>;
193
+ /**
194
+ * What this gateway publishes for the deployment to discovery. `staged` asks
195
+ * about the spec a pending change would publish instead of the live one.
196
+ */
197
+ discoveryStatus(id: string, opts?: {
198
+ staged?: boolean;
199
+ signal?: AbortSignal;
200
+ }): Promise<DiscoveryStatus>;
201
+ /**
202
+ * Run a command in the deployment's VM and wait for it to finish.
203
+ *
204
+ * Two things to know:
205
+ *
206
+ * - **A non-zero exit resolves.** The command ran; it failed. Only an
207
+ * inability to run it rejects. Check `exitCode`.
208
+ * - **The timeout does not kill anything.** `timeoutSecs` bounds app-lb's own
209
+ * call to the daemon; when it expires you get an {@link UpstreamError} and
210
+ * the command **keeps running in the guest**. The daemon offers no
211
+ * streaming and no cancellation, so output is buffered until it exits.
212
+ */
213
+ exec(id: string, command: string, opts?: ExecOptions): Promise<ExecOutput>;
214
+ /** Every CI workflow object. */
215
+ workflows(signal?: AbortSignal): Promise<WorkflowSpec[]>;
216
+ workflow(id: string, signal?: AbortSignal): Promise<WorkflowSpec>;
217
+ /** Create or replace. Sends the object as given, unknown fields included. */
218
+ createWorkflow(spec: WorkflowSpec | Record<string, unknown>, signal?: AbortSignal): Promise<WorkflowSpec>;
219
+ replaceWorkflow(id: string, spec: WorkflowSpec | Record<string, unknown>, signal?: AbortSignal): Promise<WorkflowSpec>;
220
+ deleteWorkflow(id: string, signal?: AbortSignal): Promise<void>;
221
+ /**
222
+ * The declared auth providers this credential can see, or those of one
223
+ * namespace. Narrows itself server-side rather than refusing.
224
+ */
225
+ authProviders(namespace?: string, signal?: AbortSignal): Promise<AuthProviderView[]>;
226
+ /** One provider. Unique within its namespace, so both halves are required. */
227
+ authProvider(namespace: string, name: string, signal?: AbortSignal): Promise<AuthProviderView>;
228
+ authProviderExists(namespace: string, name: string): Promise<boolean>;
229
+ /**
230
+ * Declare or replace a provider (upserts, keeping `created_at`). The body may
231
+ * carry request-only `preset`/`secret` conveniences the server expands.
232
+ */
233
+ createAuthProvider(spec: Record<string, unknown>, signal?: AbortSignal): Promise<AuthProviderView>;
234
+ /** Refused (409) while a deployment's gate still inherits it. */
235
+ deleteAuthProvider(namespace: string, name: string, signal?: AbortSignal): Promise<void>;
236
+ /** The namespaces this credential can see, narrowed server-side. */
237
+ namespaces(signal?: AbortSignal): Promise<NamespaceEntry[]>;
238
+ /** Declare a namespace. Idempotent; fleet scope and `admin` server-side. */
239
+ createNamespace(spec: {
240
+ name: string;
241
+ description?: string;
242
+ [extra: string]: unknown;
243
+ }, signal?: AbortSignal): Promise<unknown>;
244
+ /** Undeclare a namespace. Refused while deployments are still in it. */
245
+ deleteNamespace(name: string, signal?: AbortSignal): Promise<void>;
246
+ /**
247
+ * The plugins that install per namespace, and whether each is switched on for
248
+ * the fleet (`enabled`) and installed here (`installed`).
249
+ */
250
+ namespacePlugins(namespace: string, signal?: AbortSignal): Promise<NamespacePlugin[]>;
251
+ /**
252
+ * Install a plugin into a namespace, or replace its per-namespace config.
253
+ * Idempotent. Needs `admin` over the whole namespace; a `ConflictError` with
254
+ * `code: "plugin_disabled"` while the operator has it off for the fleet.
255
+ */
256
+ installPlugin(namespace: string, id: string, config?: unknown, signal?: AbortSignal): Promise<NamespacePlugin>;
257
+ /** Uninstall. For `obs` this stops collection; what was collected ages out. */
258
+ uninstallPlugin(namespace: string, id: string, signal?: AbortSignal): Promise<NamespacePlugin>;
259
+ /** Every namespace a plugin is installed in. Fleet scope only. */
260
+ pluginInstalls(id: string, signal?: AbortSignal): Promise<PluginInstalls>;
261
+ /** One namespace's telemetry through the `obs` plugin. Makes no request. */
262
+ obs(namespace: string): ObsClient;
263
+ /**
264
+ * The secrets the credential may see: one namespace when `namespace` is
265
+ * given, else every namespace within its reach. Secrets are walled by
266
+ * namespace exactly as deployments are.
267
+ */
268
+ secrets(namespace?: string, signal?: AbortSignal): Promise<SecretSummary[]>;
269
+ secret(id: string, namespace?: string, signal?: AbortSignal): Promise<SecretSummary>;
270
+ /**
271
+ * Values enter here and are never readable again. `namespace` defaults to
272
+ * `default`; a confined credential must reach it as an admin.
273
+ */
274
+ putSecret(spec: {
275
+ id: string;
276
+ namespace?: string;
277
+ description?: string;
278
+ data: Record<string, string>;
279
+ }, signal?: AbortSignal): Promise<SecretSummary>;
280
+ /** `null` for a value deletes that key; absent keys are left alone. */
281
+ patchSecret(id: string, patch: {
282
+ data?: Record<string, string | null>;
283
+ description?: string;
284
+ }, namespace?: string, signal?: AbortSignal): Promise<SecretSummary>;
285
+ deleteSecret(id: string, force?: boolean, namespace?: string, signal?: AbortSignal): Promise<void>;
286
+ /** Where DNS should point a deployment's hostname — this LB's public addresses. */
287
+ ingress(signal?: AbortSignal): Promise<Ingress>;
288
+ /**
289
+ * Mint a token.
290
+ *
291
+ * **The secret in the reply is shown once** — app-lb keeps only its hash and
292
+ * no endpoint reads it back. Store it now or mint another.
293
+ *
294
+ * Both scope fields default to nothing: a token minted with no scope can do
295
+ * nothing, which is a harmless mistake. The other default would turn a
296
+ * forgotten field into fleet-wide credentials.
297
+ */
298
+ mintToken(req: NewToken, signal?: AbortSignal): Promise<MintedToken>;
299
+ plugins(signal?: AbortSignal): Promise<PluginView[]>;
300
+ plugin(id: string, signal?: AbortSignal): Promise<PluginView>;
301
+ /**
302
+ * Write a plugin's record. Omit `config` to keep the stored one. Resolves
303
+ * when the record is saved even if applying it failed — check `last_error`.
304
+ */
305
+ setPlugin(id: string, enabled: boolean, config?: unknown, signal?: AbortSignal): Promise<PluginView>;
306
+ tokens(signal?: AbortSignal): Promise<TokenSummary[]>;
307
+ token(id: string, signal?: AbortSignal): Promise<TokenSummary>;
308
+ /**
309
+ * Re-scope a token **without changing its secret**, so narrowing a credential
310
+ * does not mean redistributing it. Pass `expires_at: null` to clear an expiry.
311
+ */
312
+ patchToken(id: string, patch: {
313
+ name?: string;
314
+ admin?: AdminScope;
315
+ /** `null` lifts the namespace wall; a string moves it. */
316
+ namespace?: string | null;
317
+ deployments?: string[];
318
+ expires_at?: number | null;
319
+ }, signal?: AbortSignal): Promise<TokenSummary>;
320
+ /** Effective on the next request — verification is a lookup, not a signature. */
321
+ revokeToken(id: string, signal?: AbortSignal): Promise<void>;
322
+ /** The namespaces that have feed events, narrowed to this credential. */
323
+ feeds(signal?: AbortSignal): Promise<FeedIndexEntry[]>;
324
+ /** A namespace's feed events as structured data, newest first. */
325
+ feedEvents(namespace: string, signal?: AbortSignal): Promise<FeedEvent[]>;
326
+ /** A namespace's feed as the RSS document a reader would fetch, verbatim. */
327
+ feedRss(namespace: string, signal?: AbortSignal): Promise<string>;
328
+ startBuild(id: string, ref?: string, signal?: AbortSignal): Promise<JobRecord>;
329
+ startPull(id: string, ref?: string, force?: boolean, signal?: AbortSignal): Promise<JobRecord>;
330
+ /**
331
+ * Unpack a managed deployment's guest mounts and roll the pool onto them.
332
+ * One job covers every mount, so there is no `ref` override.
333
+ */
334
+ startMountPull(id: string, force?: boolean, signal?: AbortSignal): Promise<JobRecord>;
335
+ startUpdate(id: string, signal?: AbortSignal): Promise<JobRecord>;
336
+ jobs(signal?: AbortSignal): Promise<JobRecord[]>;
337
+ deploymentJobs(id: string, signal?: AbortSignal): Promise<JobRecord[]>;
338
+ /** A 404 can mean the job aged out of the bounded history, not that it never existed. */
339
+ job(jobId: string, signal?: AbortSignal): Promise<JobRecord>;
340
+ /**
341
+ * The unfiltered response is megabytes at fleet scale, so prefer `summary`
342
+ * and paging. `fleet`, `global` and `host` always describe everything the
343
+ * credential can see, never the page.
344
+ */
345
+ metrics(query?: MetricsQuery, signal?: AbortSignal): Promise<MetricsResponse>;
346
+ /**
347
+ * Attach an interactive shell.
348
+ *
349
+ * Everything that can fail with a status does so *before* the upgrade, so a
350
+ * socket that opens is a shell that attached: a 404, 403, 409 or 503 arrives
351
+ * as a typed error, never as an unexplained close.
352
+ */
353
+ shell(id: string, opts?: ShellOptions): Promise<Shell>;
354
+ /** Poll a job until it finishes. See {@link waitForJob}. */
355
+ waitForJob(jobId: string, opts?: WaitForJobOptions): Promise<JobRecord>;
356
+ /** Poll a deployment until its pool has converged. See {@link waitForReady}. */
357
+ waitForReady(id: string, opts?: WaitForReadyOptions): Promise<DeploymentStatus>;
358
+ certs(signal?: AbortSignal): Promise<CertStatus[]>;
359
+ /**
360
+ * The host's disk inventory. Check `complete` before acting on it: an
361
+ * incomplete inventory classified nothing.
362
+ */
363
+ disks(signal?: AbortSignal): Promise<DiskInventory>;
364
+ /**
365
+ * Every deployment id, a page at a time.
366
+ *
367
+ * Walks `/metrics` rather than `GET /deployments`, which is unpaged and
368
+ * returns whole specs — megabytes at fleet scale.
369
+ */
370
+ deploymentIds(pageSize?: number, signal?: AbortSignal): Promise<string[]>;
371
+ }