@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 +55 -0
- package/README.md +238 -2
- package/dist/client.d.ts +371 -0
- package/dist/client.js +761 -0
- package/dist/errors.d.ts +113 -0
- package/dist/errors.js +228 -0
- package/dist/index.d.ts +54 -0
- package/dist/index.js +48 -0
- package/dist/obs.d.ts +86 -0
- package/dist/obs.js +115 -0
- package/dist/shell.d.ts +123 -0
- package/dist/shell.js +340 -0
- package/dist/types.d.ts +1312 -0
- package/dist/types.js +13 -0
- package/dist/wait.d.ts +70 -0
- package/dist/wait.js +99 -0
- package/package.json +58 -4
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
|
-
#
|
|
1
|
+
# @heyocomputer/hws
|
|
2
2
|
|
|
3
|
-
|
|
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.
|
package/dist/client.d.ts
ADDED
|
@@ -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
|
+
}
|