@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/dist/types.d.ts
ADDED
|
@@ -0,0 +1,1312 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The admin API's JSON, as types.
|
|
3
|
+
*
|
|
4
|
+
* Every field is optional in practice — app-lb omits what is absent and adds
|
|
5
|
+
* fields over time — so these describe what you *may* find rather than what is
|
|
6
|
+
* guaranteed. The runtime never validates them: an unknown field is carried
|
|
7
|
+
* through untouched, which is what makes a client one version behind still work.
|
|
8
|
+
*
|
|
9
|
+
* `test/wire-contract.test.ts` reads `testdata/wire/*.json`, written by app-lb's
|
|
10
|
+
* own response types, and asserts every key in them is declared here. A field
|
|
11
|
+
* app-lb starts sending fails that test instead of silently going unread.
|
|
12
|
+
*/
|
|
13
|
+
export type DeploymentKind = "vm" | "static" | "site";
|
|
14
|
+
export interface RouteRule {
|
|
15
|
+
host?: string;
|
|
16
|
+
host_suffix?: string;
|
|
17
|
+
path_prefix?: string;
|
|
18
|
+
strip_prefix?: boolean;
|
|
19
|
+
/** Answer matched requests with a redirect instead of serving them. */
|
|
20
|
+
redirect?: RouteRedirect;
|
|
21
|
+
}
|
|
22
|
+
export interface RouteRedirect {
|
|
23
|
+
/** Absolute http(s) URL. */
|
|
24
|
+
to: string;
|
|
25
|
+
/** 301 (default), 302, 303, 307 or 308. */
|
|
26
|
+
status?: number;
|
|
27
|
+
/** Append the request path and query to `to`. Defaults to true. */
|
|
28
|
+
keep_path?: boolean;
|
|
29
|
+
}
|
|
30
|
+
export interface ScalingPolicy {
|
|
31
|
+
min_replicas?: number;
|
|
32
|
+
max_replicas?: number;
|
|
33
|
+
warm_pool?: number;
|
|
34
|
+
target_concurrency?: number;
|
|
35
|
+
scale_to_zero_after_secs?: number;
|
|
36
|
+
cold_start_timeout_secs?: number;
|
|
37
|
+
drain_timeout_secs?: number;
|
|
38
|
+
/** `destroy` kills an idle VM; `retain` stops it, keeping its data disk. */
|
|
39
|
+
idle_action?: "destroy" | "retain";
|
|
40
|
+
boot_timeout_secs?: number;
|
|
41
|
+
}
|
|
42
|
+
export interface ExpectedHeader {
|
|
43
|
+
name: string;
|
|
44
|
+
value: string;
|
|
45
|
+
}
|
|
46
|
+
export interface HealthCheck {
|
|
47
|
+
/** Require 2xx and one exact response header; rollout requires x-heyo-revision. */
|
|
48
|
+
expected_header?: ExpectedHeader;
|
|
49
|
+
/** `null` means a bare TCP connect rather than an HTTP probe. */
|
|
50
|
+
path?: string | null;
|
|
51
|
+
port?: number;
|
|
52
|
+
timeout_secs?: number;
|
|
53
|
+
}
|
|
54
|
+
export interface VmSpec {
|
|
55
|
+
driver: "firecracker" | "kvm" | "libvirt";
|
|
56
|
+
image?: string;
|
|
57
|
+
port: number;
|
|
58
|
+
start_command?: string;
|
|
59
|
+
size_class?: "micro" | "mini" | "small" | "medium" | "large" | "xlarge";
|
|
60
|
+
disk_size_gb?: number;
|
|
61
|
+
working_directory?: string;
|
|
62
|
+
env_vars?: Record<string, string>;
|
|
63
|
+
setup_hooks?: string[];
|
|
64
|
+
open_ports?: number[];
|
|
65
|
+
/**
|
|
66
|
+
* Directories every replica boots with, unpacked from tarballs in an artifact
|
|
67
|
+
* store. Attached at boot, so editing this list recycles the pool.
|
|
68
|
+
*/
|
|
69
|
+
mounts?: MountSpec[];
|
|
70
|
+
/**
|
|
71
|
+
* A writable directory owned by the deployment rather than by any one VM:
|
|
72
|
+
* captured when a replica retires (rollout, restart, eviction, idle
|
|
73
|
+
* suspend) and seeded into its replacement, with every snapshot pushed to
|
|
74
|
+
* `store`. Requires `scaling.max_replicas: 1`, `warm_pool: 0` and the
|
|
75
|
+
* firecracker driver.
|
|
76
|
+
*/
|
|
77
|
+
workspace?: WorkspaceSpec;
|
|
78
|
+
/**
|
|
79
|
+
* Secret values exported to every replica as environment variables,
|
|
80
|
+
* resolved when the VM is created. The spec carries the reference and the
|
|
81
|
+
* store the value — the reason `env_vars` is the wrong place for a token.
|
|
82
|
+
* Resolved in the deployment's own namespace.
|
|
83
|
+
*/
|
|
84
|
+
env_from?: SecretEnv[];
|
|
85
|
+
/**
|
|
86
|
+
* A gzipped tarball every replica's `/workspace` is unpacked from at boot.
|
|
87
|
+
* Name it by `archive_id` through the Heyo cloud API, which checks the
|
|
88
|
+
* caller owns it and fills in `s3_key`; app-lb refuses the id alone.
|
|
89
|
+
* Unlike `workspace` nothing is captured back, so it composes with a pool of
|
|
90
|
+
* any size. Mutually exclusive with `workspace`.
|
|
91
|
+
*/
|
|
92
|
+
workspace_archive?: WorkspaceArchive;
|
|
93
|
+
/**
|
|
94
|
+
* Where the daemon may fetch `image` from when it does not hold it — a
|
|
95
|
+
* public-image catalog URL with the size and digest the download is
|
|
96
|
+
* verified against. Filled in by cloud from its catalog; leave unset when
|
|
97
|
+
* posting to app-lb directly against a daemon that already has the image.
|
|
98
|
+
*/
|
|
99
|
+
image_download_url?: string;
|
|
100
|
+
image_size_bytes?: number;
|
|
101
|
+
image_sha256?: string;
|
|
102
|
+
ttl_seconds?: number;
|
|
103
|
+
}
|
|
104
|
+
export interface WorkspaceArchive {
|
|
105
|
+
/** The cloud archive id (`ar-…`). */
|
|
106
|
+
archive_id?: string;
|
|
107
|
+
/** The object key the daemon fetches; resolved by cloud, never guessed. */
|
|
108
|
+
s3_key?: string;
|
|
109
|
+
size_bytes?: number;
|
|
110
|
+
}
|
|
111
|
+
/** `GET /ingress` — where DNS should point a deployment's hostname. Empty until `APP_LB_PUBLIC_IPS` is set. */
|
|
112
|
+
export interface Ingress {
|
|
113
|
+
ipv4: string[];
|
|
114
|
+
ipv6: string[];
|
|
115
|
+
}
|
|
116
|
+
export interface WorkspaceSpec {
|
|
117
|
+
/** Guest path. Defaults to `/workspace`, which `disk_size_gb` then sizes. */
|
|
118
|
+
path?: string;
|
|
119
|
+
/** `s3://bucket[/prefix]`, an `http(s)://` artifact store, or a local store root. */
|
|
120
|
+
store: string;
|
|
121
|
+
/** Tag the newest snapshot is published under (artifact stores). Defaults to `workspace-<id>`. */
|
|
122
|
+
ref?: string;
|
|
123
|
+
auth?: SecretRef;
|
|
124
|
+
}
|
|
125
|
+
export interface WorkspaceStatus {
|
|
126
|
+
path: string;
|
|
127
|
+
store: string;
|
|
128
|
+
/** The snapshot the pool runs from; `null` is the empty workspace. */
|
|
129
|
+
digest: string | null;
|
|
130
|
+
captured_at: number | null;
|
|
131
|
+
captured_from: string | null;
|
|
132
|
+
files: number;
|
|
133
|
+
bytes: number;
|
|
134
|
+
/** The snapshot the store is known to hold. */
|
|
135
|
+
pushed: string | null;
|
|
136
|
+
pushed_at: number | null;
|
|
137
|
+
push_pending: boolean;
|
|
138
|
+
phase: "idle" | "restoring" | "capturing" | "pushing" | "blocked";
|
|
139
|
+
/** Why no replica can be created right now, when none can. */
|
|
140
|
+
blocked?: string;
|
|
141
|
+
pending?: {
|
|
142
|
+
sandbox_id: string;
|
|
143
|
+
then: "kill" | "suspend";
|
|
144
|
+
queued_at: number;
|
|
145
|
+
attempts: number;
|
|
146
|
+
}[];
|
|
147
|
+
last_error?: string;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* One directory handed to every replica of a managed deployment, unpacked from a
|
|
151
|
+
* tarball in an artifact store.
|
|
152
|
+
*
|
|
153
|
+
* The counterpart of `ArtifactSpec` for *data* rather than for the image: the
|
|
154
|
+
* rootfs decides what the guests run, this decides what they hold. A mount whose
|
|
155
|
+
* `digest` is absent has not been pulled on the target host, and a deployment
|
|
156
|
+
* with one has no pool at all — app-lb refuses to boot a guest that would be
|
|
157
|
+
* missing its data. `POST /deployments/:id/mounts/pull` is what fills it in, and
|
|
158
|
+
* registering or editing the deployment starts one of those automatically.
|
|
159
|
+
*/
|
|
160
|
+
export interface MountSpec {
|
|
161
|
+
/** Absolute path inside the guest. Unique within the deployment, and never
|
|
162
|
+
* nested inside another mount. */
|
|
163
|
+
path: string;
|
|
164
|
+
/** An `art serve` URL, or an absolute store root on the app-lb host. */
|
|
165
|
+
store: string;
|
|
166
|
+
/** A tag or a 64-hex digest naming a `tar`/`tar.gz` of the directory. */
|
|
167
|
+
ref: string;
|
|
168
|
+
auth?: SecretRef;
|
|
169
|
+
/** Leading path components to drop while unpacking, as
|
|
170
|
+
* `tar --strip-components` does. */
|
|
171
|
+
strip_components?: number;
|
|
172
|
+
/**
|
|
173
|
+
* Whether the guest mounts it read-only. Defaults to `true`, and is **refused
|
|
174
|
+
* on the `kvm` driver** when false: that driver syncs a writable mount back
|
|
175
|
+
* into the host tree every other replica boots from.
|
|
176
|
+
*/
|
|
177
|
+
read_only?: boolean;
|
|
178
|
+
/** What `ref` resolved to on the last pull — which bytes the guests hold. */
|
|
179
|
+
digest?: string;
|
|
180
|
+
}
|
|
181
|
+
export interface SecretRef {
|
|
182
|
+
secret: string;
|
|
183
|
+
key?: string;
|
|
184
|
+
username?: string;
|
|
185
|
+
/**
|
|
186
|
+
* The namespace the secret lives in. Stamped by app-lb from the
|
|
187
|
+
* deployment's own namespace on register/edit; a value sent by a client is
|
|
188
|
+
* overwritten, so a spec can only ever name secrets behind its own wall.
|
|
189
|
+
*/
|
|
190
|
+
namespace?: string;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Where a managed deployment's guest image is built from. Exactly one of `repo`
|
|
194
|
+
* and `store` is set — both name the Dockerfile, and a build with two recipes
|
|
195
|
+
* has no answer to which one produced the image.
|
|
196
|
+
*/
|
|
197
|
+
export interface BuildSpec {
|
|
198
|
+
/** Git remote. Absent when the recipe comes from `store`. */
|
|
199
|
+
repo?: string;
|
|
200
|
+
/**
|
|
201
|
+
* An artifact store holding a Dockerfile manifest (`heyvm.dockerfile.v1`):
|
|
202
|
+
* an `art serve` URL, or an absolute store root on the app-lb host.
|
|
203
|
+
*/
|
|
204
|
+
store?: string;
|
|
205
|
+
/**
|
|
206
|
+
* Which version of the source to build: a branch, tag or commit for `repo`
|
|
207
|
+
* (absent follows the remote's default branch), or the tag or digest of a
|
|
208
|
+
* Dockerfile manifest for `store`, where it is required.
|
|
209
|
+
*/
|
|
210
|
+
ref?: string;
|
|
211
|
+
/** Git source only — a Dockerfile manifest already names its own recipe. */
|
|
212
|
+
dockerfile?: string;
|
|
213
|
+
/** Git source only. */
|
|
214
|
+
context?: string;
|
|
215
|
+
image_name?: string;
|
|
216
|
+
image_size_mb?: number;
|
|
217
|
+
/** A git token for `repo`, or the store's API key for `store`. */
|
|
218
|
+
auth?: SecretRef;
|
|
219
|
+
}
|
|
220
|
+
export interface ArtifactSpec {
|
|
221
|
+
store: string;
|
|
222
|
+
ref: string;
|
|
223
|
+
auth?: SecretRef;
|
|
224
|
+
grow_gb?: number;
|
|
225
|
+
image_name?: string;
|
|
226
|
+
/** Site bundles only: leading path components to drop while unpacking. */
|
|
227
|
+
strip_components?: number;
|
|
228
|
+
}
|
|
229
|
+
export interface SiteSpec {
|
|
230
|
+
root: string;
|
|
231
|
+
index?: string;
|
|
232
|
+
not_found?: string;
|
|
233
|
+
spa?: boolean;
|
|
234
|
+
cache_control?: string;
|
|
235
|
+
}
|
|
236
|
+
export interface SecretEnv {
|
|
237
|
+
secret: string;
|
|
238
|
+
key?: string;
|
|
239
|
+
as?: string;
|
|
240
|
+
/** Stamped from the deployment's namespace, as on {@link SecretRef}. */
|
|
241
|
+
namespace?: string;
|
|
242
|
+
}
|
|
243
|
+
export interface UpdateSpec {
|
|
244
|
+
working_dir: string;
|
|
245
|
+
commands: string[];
|
|
246
|
+
env?: Record<string, string>;
|
|
247
|
+
env_from?: SecretEnv[];
|
|
248
|
+
auth?: SecretRef;
|
|
249
|
+
timeout_secs?: number;
|
|
250
|
+
verify_timeout_secs?: number;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* `provider` is a bare string for one and an array for several — they are
|
|
254
|
+
* alternatives, so any one of them admits a request. A gate written before
|
|
255
|
+
* app-tokens existed omits it entirely and means `"google"`.
|
|
256
|
+
*/
|
|
257
|
+
/** One entry in {@link AuthGate.public_paths}. */
|
|
258
|
+
export interface PublicPath {
|
|
259
|
+
path: string;
|
|
260
|
+
/**
|
|
261
|
+
* `public` | `none` | `view` | `admin`. The lower three mirror an app-token's
|
|
262
|
+
* `admin` scope; `public` is the only one that needs no credential.
|
|
263
|
+
*/
|
|
264
|
+
scope: "public" | "none" | "view" | "admin";
|
|
265
|
+
}
|
|
266
|
+
export interface AuthGate {
|
|
267
|
+
provider?: AuthProvider | AuthProvider[];
|
|
268
|
+
/** Required for `google`, meaningless without it. */
|
|
269
|
+
client_id?: string;
|
|
270
|
+
client_secret?: SecretRef;
|
|
271
|
+
allowed_domains?: string[];
|
|
272
|
+
allowed_emails?: string[];
|
|
273
|
+
/**
|
|
274
|
+
* Path prefixes the *sign-in* gate does not sit in front of, and what app-lb
|
|
275
|
+
* requires instead. `public` is the only scope that admits a request
|
|
276
|
+
* presenting no credential; a bare string written by hand means `admin`.
|
|
277
|
+
*/
|
|
278
|
+
public_paths?: PublicPath[];
|
|
279
|
+
/**
|
|
280
|
+
* When set, signing in at this gate mints an app-token with this scope and
|
|
281
|
+
* app-lb presents it upstream for the life of the session — so an upstream
|
|
282
|
+
* that authenticates for itself can accept a signed-in person without its
|
|
283
|
+
* own authentication being turned off. Absent means no token is minted.
|
|
284
|
+
*/
|
|
285
|
+
session_scope?: "none" | "view" | "admin";
|
|
286
|
+
base_path?: string;
|
|
287
|
+
session_ttl_secs?: number;
|
|
288
|
+
cookie_name?: string;
|
|
289
|
+
cookie_domain?: string;
|
|
290
|
+
redirect_url?: string;
|
|
291
|
+
forward_identity?: boolean;
|
|
292
|
+
/** How to verify a JWT, when `jwt` is among the providers. */
|
|
293
|
+
jwt?: JwtSpec;
|
|
294
|
+
/**
|
|
295
|
+
* Inherit the identity half of this gate — `provider`, the OAuth credentials
|
|
296
|
+
* and allow-lists, `jwt`, `cookie_domain` — from a named provider declared on
|
|
297
|
+
* the deployment's namespace, resolved live on every request. When set, this
|
|
298
|
+
* gate carries only the route-scoped fields (`public_paths`, `session_scope`,
|
|
299
|
+
* `base_path`, `cookie_name`, `redirect_url`, `forward_identity`,
|
|
300
|
+
* `session_ttl_secs`); setting an identity field alongside it is refused.
|
|
301
|
+
*/
|
|
302
|
+
provider_ref?: string;
|
|
303
|
+
}
|
|
304
|
+
export type AuthProvider = "google" | "app-token" | "jwt";
|
|
305
|
+
/**
|
|
306
|
+
* How a gate verifies a JWT somebody else issued, and which ones it lets past.
|
|
307
|
+
*
|
|
308
|
+
* The `jwt` provider holds no state: there is no session cookie and no token
|
|
309
|
+
* table, because the credential carries its own proof. Everything the gate needs
|
|
310
|
+
* is therefore configuration — which key, which algorithm, which issuer, which
|
|
311
|
+
* claim is the user, which claims must hold — which is also what makes one gate
|
|
312
|
+
* front the Heyo auth API, an Auth0 tenant or a Keycloak realm.
|
|
313
|
+
*
|
|
314
|
+
* Exactly one of `secret`, `public_key` and `jwks_url` is set.
|
|
315
|
+
*/
|
|
316
|
+
export interface JwtSpec {
|
|
317
|
+
/** HMAC shared secret (the `HS*` algorithms), as a secret-store reference. */
|
|
318
|
+
secret?: SecretRef;
|
|
319
|
+
/** An inline PEM public key or certificate, for `RS*`/`PS*`/`ES*`. */
|
|
320
|
+
public_key?: string;
|
|
321
|
+
/** The issuer's JWKS endpoint, for a provider that rotates keys. */
|
|
322
|
+
jwks_url?: string;
|
|
323
|
+
/**
|
|
324
|
+
* The signature algorithms this gate accepts, e.g. `["HS256"]`.
|
|
325
|
+
*
|
|
326
|
+
* Required, with no default: the algorithm is named in the token's own header,
|
|
327
|
+
* and a verifier that trusted that would accept an unsigned token.
|
|
328
|
+
*/
|
|
329
|
+
algorithms: string[];
|
|
330
|
+
/** The `iss` a token must carry, exactly. Required. */
|
|
331
|
+
issuer: string;
|
|
332
|
+
/** The `aud` a token must carry, if the issuer sets one. */
|
|
333
|
+
audience?: string;
|
|
334
|
+
/**
|
|
335
|
+
* Claims a token must satisfy on top of verifying. A value or a list of them
|
|
336
|
+
* per claim: a list is an OR within that claim, and the map is an AND across
|
|
337
|
+
* claims. A claim that is itself a list — scopes, roles, groups — is satisfied
|
|
338
|
+
* by containing one of the wanted values.
|
|
339
|
+
*/
|
|
340
|
+
require?: Record<string, unknown>;
|
|
341
|
+
/** Which claim is forwarded as `x-auth-request-user`. Defaults to `sub`. */
|
|
342
|
+
subject_claim?: string;
|
|
343
|
+
/** Which claim is forwarded as `x-auth-request-email`. Defaults to `email`. */
|
|
344
|
+
email_claim?: string;
|
|
345
|
+
/** Which claim is forwarded as `x-auth-request-name`. Defaults to `name`. */
|
|
346
|
+
name_claim?: string;
|
|
347
|
+
/** Clock skew allowed on `exp`/`nbf`, in seconds. Capped at 300. */
|
|
348
|
+
leeway_secs?: number;
|
|
349
|
+
/**
|
|
350
|
+
* A cookie to read the token from when there is no `Authorization` header —
|
|
351
|
+
* the only way a browser page navigation can carry one. The header wins when
|
|
352
|
+
* both are present.
|
|
353
|
+
*/
|
|
354
|
+
cookie?: string;
|
|
355
|
+
/**
|
|
356
|
+
* Heyo Auth `/api/auth/login` endpoint for browser email/password sign-in.
|
|
357
|
+
* Requires `cookie`; app-lb stores neither passwords nor refresh tokens.
|
|
358
|
+
*/
|
|
359
|
+
login_endpoint?: string;
|
|
360
|
+
/**
|
|
361
|
+
* Hosted sign-in: where to redirect a token-less *browser* (a request that
|
|
362
|
+
* accepts HTML). The issuer signs the person in, sets the JWT in `cookie`, and
|
|
363
|
+
* redirects back to the URL passed in `login_redirect_param`; app-lb keeps no
|
|
364
|
+
* session of its own. A program still gets a 401. Requires `cookie`, and must
|
|
365
|
+
* be `https://` (loopback `http://` aside).
|
|
366
|
+
*/
|
|
367
|
+
login_url?: string;
|
|
368
|
+
/** The query parameter the hosted sign-in reads the return URL from. Only with `login_url`; defaults to `redirect_uri`. */
|
|
369
|
+
login_redirect_param?: string;
|
|
370
|
+
/**
|
|
371
|
+
* Scoped sign-in: the issuer's OAuth authorization endpoint. With
|
|
372
|
+
* `token_url`, a token-less browser is sent through an authorization-code
|
|
373
|
+
* flow (PKCE S256) asking for this deployment's namespace; the token that
|
|
374
|
+
* comes back must name this host (`gateHost`) and namespace, and app-lb keeps
|
|
375
|
+
* a host-only session. Cannot be combined with a `cookie_domain` realm.
|
|
376
|
+
*/
|
|
377
|
+
authorize_url?: string;
|
|
378
|
+
/** Scoped sign-in: the issuer's token endpoint, called server to server. */
|
|
379
|
+
token_url?: string;
|
|
380
|
+
}
|
|
381
|
+
export interface DeploymentSpec {
|
|
382
|
+
id: string;
|
|
383
|
+
/**
|
|
384
|
+
* The namespace this deployment belongs to. Absent means `"default"`.
|
|
385
|
+
* Namespaces segregate use: a namespace-confined token reaches only the
|
|
386
|
+
* deployments in it, and the event feed is kept per namespace.
|
|
387
|
+
*/
|
|
388
|
+
namespace?: string;
|
|
389
|
+
/**
|
|
390
|
+
* The heyo account this deployment's VMs are metered to, and the user who
|
|
391
|
+
* registered it. The managed service stamps both from the caller's
|
|
392
|
+
* credential (the namespace's owning account) and ignores what the body
|
|
393
|
+
* says; a self-hosted app-lb keeps what it was sent, usually nothing.
|
|
394
|
+
*/
|
|
395
|
+
account_id?: string;
|
|
396
|
+
user_id?: string;
|
|
397
|
+
routes: RouteRule[];
|
|
398
|
+
/** Return HTTP 503 for routed proxy traffic while admin management remains available. */
|
|
399
|
+
maintenance?: boolean;
|
|
400
|
+
vm?: VmSpec;
|
|
401
|
+
scaling?: ScalingPolicy;
|
|
402
|
+
health?: HealthCheck;
|
|
403
|
+
upstreams?: string[];
|
|
404
|
+
discovery?: DiscoverySpec;
|
|
405
|
+
build?: BuildSpec;
|
|
406
|
+
artifact?: ArtifactSpec;
|
|
407
|
+
site?: SiteSpec;
|
|
408
|
+
update?: UpdateSpec;
|
|
409
|
+
auth?: AuthGate;
|
|
410
|
+
feed?: FeedSpec;
|
|
411
|
+
/** Anything app-lb sent that this build has no name for. */
|
|
412
|
+
[extra: string]: unknown;
|
|
413
|
+
}
|
|
414
|
+
/** Orchestrator-owned endpoint membership for a static deployment. */
|
|
415
|
+
export interface DiscoverySpec {
|
|
416
|
+
service_id: string;
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* A deployment's opt-in hooks into its namespace's event feed. Everything
|
|
420
|
+
* defaults to off — a deployment publishes nothing its spec did not ask for.
|
|
421
|
+
*/
|
|
422
|
+
export interface FeedSpec {
|
|
423
|
+
/** Publish lifecycle events (registered, updated, removed). */
|
|
424
|
+
announce?: boolean;
|
|
425
|
+
/** Publish operational issues (boot failures, cold-start timeouts, …). */
|
|
426
|
+
issues?: boolean;
|
|
427
|
+
/**
|
|
428
|
+
* Serve the namespace's feed as RSS at this path on this deployment's own
|
|
429
|
+
* routes — the only way a feed becomes reachable outside the admin listener.
|
|
430
|
+
* Runs behind the deployment's `auth` gate, if it has one.
|
|
431
|
+
*/
|
|
432
|
+
expose?: string;
|
|
433
|
+
}
|
|
434
|
+
export interface VmStatus {
|
|
435
|
+
sandbox_id: string;
|
|
436
|
+
addr: string;
|
|
437
|
+
in_flight: number;
|
|
438
|
+
healthy: boolean;
|
|
439
|
+
draining: boolean;
|
|
440
|
+
}
|
|
441
|
+
export interface DeploymentStatus {
|
|
442
|
+
/** Opaque persisted token for conditional service rollout; absent on older servers. */
|
|
443
|
+
rollout_revision?: string;
|
|
444
|
+
spec: DeploymentSpec;
|
|
445
|
+
kind: DeploymentKind;
|
|
446
|
+
desired_replicas: number;
|
|
447
|
+
ready: number;
|
|
448
|
+
pending: number;
|
|
449
|
+
total_in_flight: number;
|
|
450
|
+
vms: VmStatus[];
|
|
451
|
+
/** Present when the spec declares `vm.workspace`. */
|
|
452
|
+
workspace?: WorkspaceStatus;
|
|
453
|
+
/** Present for a site: whether its root on the LB host can serve. */
|
|
454
|
+
site?: SiteRootStatus;
|
|
455
|
+
}
|
|
456
|
+
/** A site's root as the LB host sees it. */
|
|
457
|
+
export interface SiteRootStatus {
|
|
458
|
+
root: string;
|
|
459
|
+
status: "ok" | "missing" | "not_a_directory" | "unreadable" | "empty";
|
|
460
|
+
index_present?: boolean;
|
|
461
|
+
/** Why it cannot serve, and how to fill it. */
|
|
462
|
+
hint?: string;
|
|
463
|
+
}
|
|
464
|
+
export interface UpstreamTrafficStatus {
|
|
465
|
+
deployment_id: string;
|
|
466
|
+
upstream: string;
|
|
467
|
+
state: "accepting" | "draining" | "drained";
|
|
468
|
+
healthy: boolean;
|
|
469
|
+
in_flight: number;
|
|
470
|
+
reason: string | null;
|
|
471
|
+
started_at: number | null;
|
|
472
|
+
}
|
|
473
|
+
export interface StatusCounts {
|
|
474
|
+
total: number;
|
|
475
|
+
c2xx: number;
|
|
476
|
+
c3xx: number;
|
|
477
|
+
c4xx: number;
|
|
478
|
+
c5xx: number;
|
|
479
|
+
errors: number;
|
|
480
|
+
}
|
|
481
|
+
export interface Bucket {
|
|
482
|
+
/** `null` is the `+Inf` bucket. */
|
|
483
|
+
le: number | null;
|
|
484
|
+
count: number;
|
|
485
|
+
}
|
|
486
|
+
export interface Histogram {
|
|
487
|
+
count: number;
|
|
488
|
+
sum: number;
|
|
489
|
+
mean: number;
|
|
490
|
+
p50: number;
|
|
491
|
+
p90: number;
|
|
492
|
+
p99: number;
|
|
493
|
+
buckets: Bucket[];
|
|
494
|
+
}
|
|
495
|
+
export interface AutoscaleCounts {
|
|
496
|
+
vms_created: number;
|
|
497
|
+
vms_drained: number;
|
|
498
|
+
vms_reaped: number;
|
|
499
|
+
scale_up_events: number;
|
|
500
|
+
scale_down_events: number;
|
|
501
|
+
cold_start_waits: number;
|
|
502
|
+
cold_start_hits: number;
|
|
503
|
+
cold_start_timeouts: number;
|
|
504
|
+
boot_timeouts: number;
|
|
505
|
+
/** Creates the daemon refused or that failed client-side, since app-lb started. */
|
|
506
|
+
create_failures: number;
|
|
507
|
+
/** What the most recent failed create said; absent until one has failed. */
|
|
508
|
+
last_create_error?: string;
|
|
509
|
+
}
|
|
510
|
+
export interface DeploymentMetrics {
|
|
511
|
+
requests: StatusCounts;
|
|
512
|
+
latency_ms: Histogram;
|
|
513
|
+
cold_start_s: Histogram;
|
|
514
|
+
autoscale: AutoscaleCounts;
|
|
515
|
+
}
|
|
516
|
+
export interface HostUsage {
|
|
517
|
+
available: boolean;
|
|
518
|
+
cpu_count: number;
|
|
519
|
+
cpu_percent: number;
|
|
520
|
+
memory_total_bytes: number;
|
|
521
|
+
memory_used_bytes: number;
|
|
522
|
+
sampled_at_ms: number;
|
|
523
|
+
}
|
|
524
|
+
export interface FleetPool {
|
|
525
|
+
deployments: number;
|
|
526
|
+
ready: number;
|
|
527
|
+
draining: number;
|
|
528
|
+
pending: number;
|
|
529
|
+
total_in_flight: number;
|
|
530
|
+
}
|
|
531
|
+
export interface PoolStatus {
|
|
532
|
+
desired_replicas: number;
|
|
533
|
+
ready: number;
|
|
534
|
+
draining: number;
|
|
535
|
+
pending: number;
|
|
536
|
+
total_in_flight: number;
|
|
537
|
+
target_concurrency: number;
|
|
538
|
+
min_replicas: number;
|
|
539
|
+
max_replicas: number;
|
|
540
|
+
warm_pool: number;
|
|
541
|
+
/** `null` means no capacity to divide by — not zero load. */
|
|
542
|
+
utilization: number | null;
|
|
543
|
+
cpu_percent: number | null;
|
|
544
|
+
memory_bytes: number | null;
|
|
545
|
+
boot_timeout_secs: number;
|
|
546
|
+
cold_start_timeout_secs: number;
|
|
547
|
+
/** Failed boots in a row since the last VM that passed its health check. */
|
|
548
|
+
boot_failures: number;
|
|
549
|
+
/**
|
|
550
|
+
* Seconds until the autoscaler may create a VM again while the boot-failure
|
|
551
|
+
* backoff holds it off; `null` when it may create one now. Any spec write
|
|
552
|
+
* clears it.
|
|
553
|
+
*/
|
|
554
|
+
boot_backoff_secs: number | null;
|
|
555
|
+
}
|
|
556
|
+
export interface VmView {
|
|
557
|
+
sandbox_id: string;
|
|
558
|
+
addr: string;
|
|
559
|
+
in_flight: number;
|
|
560
|
+
healthy: boolean;
|
|
561
|
+
draining: boolean;
|
|
562
|
+
uptime_secs: number;
|
|
563
|
+
cpu_percent: number | null;
|
|
564
|
+
memory_bytes: number | null;
|
|
565
|
+
}
|
|
566
|
+
export interface PendingVmView {
|
|
567
|
+
sandbox_id: string;
|
|
568
|
+
age_secs: number;
|
|
569
|
+
status?: string;
|
|
570
|
+
}
|
|
571
|
+
export interface DeploymentView {
|
|
572
|
+
id: string;
|
|
573
|
+
/** Absent for the default namespace. */
|
|
574
|
+
namespace?: string;
|
|
575
|
+
/** The account this deployment's VMs are metered to, when app-lb knows it. */
|
|
576
|
+
account_id?: string;
|
|
577
|
+
kind: DeploymentKind;
|
|
578
|
+
upstreams: string[];
|
|
579
|
+
/** Whether at least one data-plane route points at this deployment. */
|
|
580
|
+
routed: boolean;
|
|
581
|
+
hosts: string[];
|
|
582
|
+
urls?: string[];
|
|
583
|
+
site_root?: string;
|
|
584
|
+
site_spa?: boolean;
|
|
585
|
+
job_kind?: "build" | "pull" | "update";
|
|
586
|
+
pool: PoolStatus;
|
|
587
|
+
vms: VmView[];
|
|
588
|
+
pending_vms: PendingVmView[];
|
|
589
|
+
metrics: DeploymentMetrics;
|
|
590
|
+
}
|
|
591
|
+
export interface ObsStats {
|
|
592
|
+
queued: number;
|
|
593
|
+
dropped: number;
|
|
594
|
+
shipped: number;
|
|
595
|
+
failed: number;
|
|
596
|
+
healthy: boolean;
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* Whether app-lb can reach the VM daemon. When it cannot, the autoscaler
|
|
600
|
+
* abandons every tick, so nothing scales or boots and every other number in
|
|
601
|
+
* the metrics response is frozen at whatever it was when the daemon went away.
|
|
602
|
+
*/
|
|
603
|
+
export interface DaemonSnapshot {
|
|
604
|
+
reachable: boolean;
|
|
605
|
+
/** What the last failed listing said; absent while it is reachable. */
|
|
606
|
+
last_error?: string;
|
|
607
|
+
}
|
|
608
|
+
export interface MetricsResponse {
|
|
609
|
+
generated_at: number;
|
|
610
|
+
uptime_secs: number;
|
|
611
|
+
host: HostUsage;
|
|
612
|
+
fleet: FleetPool;
|
|
613
|
+
global: DeploymentMetrics;
|
|
614
|
+
obs?: ObsStats;
|
|
615
|
+
/** Absent when `APP_LB_SIEM=0`. */
|
|
616
|
+
security?: SecuritySummary;
|
|
617
|
+
daemon: DaemonSnapshot;
|
|
618
|
+
deployments: DeploymentView[];
|
|
619
|
+
/** How many matched before `limit`/`offset`, so you can page. */
|
|
620
|
+
matched: number;
|
|
621
|
+
tracked_deployments: number;
|
|
622
|
+
/**
|
|
623
|
+
* Sandboxes on the host that no deployment owns — created through the heyvm
|
|
624
|
+
* CLI, the cloud API or the desktop rather than by app-lb. They share the
|
|
625
|
+
* host with every pool. Absent from an older app-lb, empty under
|
|
626
|
+
* `summary=true`, and narrowed to the caller's own accounts for a namespace
|
|
627
|
+
* caller.
|
|
628
|
+
*/
|
|
629
|
+
host_sandboxes?: HostSandboxView[];
|
|
630
|
+
}
|
|
631
|
+
/** One sandbox on the host that app-lb reports but does not manage. */
|
|
632
|
+
export interface HostSandboxView {
|
|
633
|
+
sandbox_id: string;
|
|
634
|
+
name: string;
|
|
635
|
+
/** The daemon's status string: `running`, `stopped`, `provisioning`, …. */
|
|
636
|
+
status: string;
|
|
637
|
+
image: string;
|
|
638
|
+
size_class?: string;
|
|
639
|
+
guest_ip?: string;
|
|
640
|
+
uptime_secs: number;
|
|
641
|
+
cpu_percent: number | null;
|
|
642
|
+
memory_bytes: number | null;
|
|
643
|
+
/** The heyo account the sandbox is billed to, when the daemon knows. */
|
|
644
|
+
account_id?: string;
|
|
645
|
+
/** RFC 3339, when the daemon reports it. */
|
|
646
|
+
created_at?: string;
|
|
647
|
+
}
|
|
648
|
+
export type DiskState = "running" | "stopped" | "orphan" | "unknown";
|
|
649
|
+
export type DiskPartKind = "data" | "rootfs" | "mount" | "snapshot" | "other";
|
|
650
|
+
export type ArchiveStatus = "running" | "succeeded" | "failed";
|
|
651
|
+
export interface DiskPart {
|
|
652
|
+
kind: DiskPartKind;
|
|
653
|
+
path: string;
|
|
654
|
+
bytes: number;
|
|
655
|
+
apparent_bytes: number;
|
|
656
|
+
modified_at: number;
|
|
657
|
+
}
|
|
658
|
+
export interface ArchiveRecord {
|
|
659
|
+
uri: string;
|
|
660
|
+
at: number;
|
|
661
|
+
bytes: number;
|
|
662
|
+
}
|
|
663
|
+
export interface DiskInfo {
|
|
664
|
+
sandbox_id: string;
|
|
665
|
+
name?: string;
|
|
666
|
+
deployment?: string;
|
|
667
|
+
state: DiskState;
|
|
668
|
+
claimed: boolean;
|
|
669
|
+
retain: boolean;
|
|
670
|
+
note?: string;
|
|
671
|
+
bytes: number;
|
|
672
|
+
apparent_bytes: number;
|
|
673
|
+
modified_at: number;
|
|
674
|
+
expires_at?: number;
|
|
675
|
+
held_by?: string;
|
|
676
|
+
archived?: ArchiveRecord;
|
|
677
|
+
parts: DiskPart[];
|
|
678
|
+
roots: string[];
|
|
679
|
+
}
|
|
680
|
+
export interface ArchiveView {
|
|
681
|
+
id: string;
|
|
682
|
+
sandbox_id: string;
|
|
683
|
+
uri: string;
|
|
684
|
+
started_at: number;
|
|
685
|
+
finished_at?: number;
|
|
686
|
+
status: ArchiveStatus;
|
|
687
|
+
bytes: number;
|
|
688
|
+
expected_bytes: number;
|
|
689
|
+
error?: string;
|
|
690
|
+
purged?: boolean;
|
|
691
|
+
}
|
|
692
|
+
export interface DiskTotals {
|
|
693
|
+
disks: number;
|
|
694
|
+
bytes: number;
|
|
695
|
+
apparent_bytes: number;
|
|
696
|
+
running: number;
|
|
697
|
+
stopped: number;
|
|
698
|
+
orphan: number;
|
|
699
|
+
retained: number;
|
|
700
|
+
expiring_now: number;
|
|
701
|
+
reclaimable_bytes: number;
|
|
702
|
+
}
|
|
703
|
+
export interface DiskInventory {
|
|
704
|
+
complete: boolean;
|
|
705
|
+
incomplete_reason?: string;
|
|
706
|
+
data_dir: string;
|
|
707
|
+
tmp_dir: string;
|
|
708
|
+
ttl_secs: number;
|
|
709
|
+
sweep_secs: number;
|
|
710
|
+
archive_enabled: boolean;
|
|
711
|
+
archive_on_expire: boolean;
|
|
712
|
+
archive_target?: string;
|
|
713
|
+
free_bytes?: number;
|
|
714
|
+
filesystem_bytes?: number;
|
|
715
|
+
orphan_ttl_secs: number;
|
|
716
|
+
totals: DiskTotals;
|
|
717
|
+
disks: DiskInfo[];
|
|
718
|
+
archives: ArchiveView[];
|
|
719
|
+
}
|
|
720
|
+
/** The alert counts carried on `/metrics`, for a status tile. */
|
|
721
|
+
export interface SecuritySummary {
|
|
722
|
+
/** Alerts currently held in app-lb's in-memory ring. */
|
|
723
|
+
open: number;
|
|
724
|
+
/** How many of those are `high` or `critical`. */
|
|
725
|
+
urgent: number;
|
|
726
|
+
/**
|
|
727
|
+
* Observations dropped because the analysis queue was full. Non-zero means
|
|
728
|
+
* detection is sampling rather than complete — the figure to watch, since a
|
|
729
|
+
* SIEM that has stopped looking is indistinguishable from a quiet network.
|
|
730
|
+
*/
|
|
731
|
+
dropped: number;
|
|
732
|
+
/** Whether the per-source table is full, which means the same for addresses. */
|
|
733
|
+
clients_at_capacity: boolean;
|
|
734
|
+
/** Guard rules currently in force. */
|
|
735
|
+
rules: number;
|
|
736
|
+
/** Requests those rules have refused since app-lb started. */
|
|
737
|
+
blocked: number;
|
|
738
|
+
}
|
|
739
|
+
export type Severity = "info" | "low" | "medium" | "high" | "critical";
|
|
740
|
+
export interface SecurityAlert {
|
|
741
|
+
id: number;
|
|
742
|
+
/** Epoch millis of the first occurrence folded into this alert. */
|
|
743
|
+
ts: number;
|
|
744
|
+
last_ts: number;
|
|
745
|
+
/** e.g. `auth.brute-force`, `web.sqli`, `traffic.scanner`. */
|
|
746
|
+
rule: string;
|
|
747
|
+
severity: Severity;
|
|
748
|
+
title: string;
|
|
749
|
+
client?: string;
|
|
750
|
+
/** Absent for admin-plane and unrouted findings. */
|
|
751
|
+
deployment?: string;
|
|
752
|
+
/** Never carries a query string. */
|
|
753
|
+
path?: string;
|
|
754
|
+
/** MITRE ATT&CK technique id, e.g. `T1110`. */
|
|
755
|
+
technique?: string;
|
|
756
|
+
/**
|
|
757
|
+
* Occurrences folded into this alert. A scanner produces one alert whose
|
|
758
|
+
* count climbs, not ten thousand alerts.
|
|
759
|
+
*/
|
|
760
|
+
count: number;
|
|
761
|
+
/**
|
|
762
|
+
* The triggering event in ECS field names (`source.ip`, `url.path`, …). A
|
|
763
|
+
* free-form map: app-lb may add fields, and `url.query` is never among them.
|
|
764
|
+
*/
|
|
765
|
+
ecs?: Record<string, unknown>;
|
|
766
|
+
/** What to do about it. Derived server-side, so a client renders rather than
|
|
767
|
+
* reasons. */
|
|
768
|
+
response: AlertResponse;
|
|
769
|
+
}
|
|
770
|
+
/** The runbook half of an alert: what to check, and what can be applied. */
|
|
771
|
+
export interface AlertResponse {
|
|
772
|
+
/** Ordered, short. What to check before refusing anyone's traffic. */
|
|
773
|
+
investigate: string[];
|
|
774
|
+
/** Rules that would mitigate this, ready to `POST /security/rules`. */
|
|
775
|
+
actions: SuggestedAction[];
|
|
776
|
+
/** Present where the obvious action does not do what it looks like it does —
|
|
777
|
+
* notably that guard rules are never applied to app-lb's own admin API. */
|
|
778
|
+
caveat?: string;
|
|
779
|
+
}
|
|
780
|
+
export interface SuggestedAction {
|
|
781
|
+
/** `block-client`, `block-client-deployment`, `block-path`, `exempt-client`. */
|
|
782
|
+
kind: string;
|
|
783
|
+
label: string;
|
|
784
|
+
/** What it stops and what it leaves alone. Show this next to the button. */
|
|
785
|
+
effect: string;
|
|
786
|
+
/** Post verbatim to `/security/rules`. */
|
|
787
|
+
rule: RuleSpec;
|
|
788
|
+
}
|
|
789
|
+
export type RuleAction = "block" | "allow";
|
|
790
|
+
/**
|
|
791
|
+
* Conditions on a request. Every field present must match; absent fields are
|
|
792
|
+
* not checked. All literal — app-lb runs no regular expressions on the request
|
|
793
|
+
* path.
|
|
794
|
+
*/
|
|
795
|
+
export interface RuleMatch {
|
|
796
|
+
/** An address or CIDR: `203.0.113.9`, `203.0.113.0/24`, `2001:db8::/32`. */
|
|
797
|
+
client?: string;
|
|
798
|
+
host?: string;
|
|
799
|
+
deployment?: string;
|
|
800
|
+
path_prefix?: string;
|
|
801
|
+
path_contains?: string;
|
|
802
|
+
method?: string;
|
|
803
|
+
user_agent_contains?: string;
|
|
804
|
+
}
|
|
805
|
+
/** The body of `POST /security/rules`. */
|
|
806
|
+
export interface RuleSpec {
|
|
807
|
+
action: RuleAction;
|
|
808
|
+
/** At least one condition — an empty match is refused, since it would apply
|
|
809
|
+
* to every request to the data plane. */
|
|
810
|
+
match: RuleMatch;
|
|
811
|
+
/** Seconds from now. Omitted means permanent. */
|
|
812
|
+
expires_in_secs?: number;
|
|
813
|
+
note?: string;
|
|
814
|
+
}
|
|
815
|
+
/** One rule in force. */
|
|
816
|
+
export interface RuleView {
|
|
817
|
+
id: string;
|
|
818
|
+
action: RuleAction;
|
|
819
|
+
match: RuleMatch;
|
|
820
|
+
/** The conditions as a phrase, so a client need not render them. */
|
|
821
|
+
summary: string;
|
|
822
|
+
note?: string;
|
|
823
|
+
created_at: number;
|
|
824
|
+
expires_at?: number;
|
|
825
|
+
/** Cumulative since this rule was created. The number to trust. */
|
|
826
|
+
hits: number;
|
|
827
|
+
last_hit?: number;
|
|
828
|
+
/** `false` under `APP_LB_GUARD_ENFORCE=0`: matched and counted, not refused. */
|
|
829
|
+
enforcing: boolean;
|
|
830
|
+
/**
|
|
831
|
+
* Hits per bucket over the last window, oldest first — see
|
|
832
|
+
* {@link GuardStats.hits_bucket_secs}. In-memory on the LB, so it is absent
|
|
833
|
+
* from the persisted rule file and starts empty after a restart. All-zero
|
|
834
|
+
* means "no hits", not "no data": a rule that has never fired is exactly what
|
|
835
|
+
* this is for spotting.
|
|
836
|
+
*
|
|
837
|
+
* Approximate at bucket boundaries by design — the LB will not take a lock on
|
|
838
|
+
* the request path to make a chart exact. Use `hits` for anything that has to
|
|
839
|
+
* add up.
|
|
840
|
+
*/
|
|
841
|
+
hits_recent?: number[];
|
|
842
|
+
}
|
|
843
|
+
export interface GuardStats {
|
|
844
|
+
rules: number;
|
|
845
|
+
blocked: number;
|
|
846
|
+
/** Requests an `allow` rule exempted from a block. */
|
|
847
|
+
exempted: number;
|
|
848
|
+
enforcing: boolean;
|
|
849
|
+
/** Requests refused per bucket over the last window, oldest first. */
|
|
850
|
+
blocked_recent?: number[];
|
|
851
|
+
/** The same for requests an `allow` rule exempted. */
|
|
852
|
+
exempted_recent?: number[];
|
|
853
|
+
/** Seconds each entry of the `*_recent` series covers. */
|
|
854
|
+
hits_bucket_secs: number;
|
|
855
|
+
/** Total seconds the `*_recent` series spans. */
|
|
856
|
+
hits_window_secs: number;
|
|
857
|
+
}
|
|
858
|
+
export interface SeverityTotals {
|
|
859
|
+
info: number;
|
|
860
|
+
low: number;
|
|
861
|
+
medium: number;
|
|
862
|
+
high: number;
|
|
863
|
+
critical: number;
|
|
864
|
+
}
|
|
865
|
+
export interface SiemStats {
|
|
866
|
+
observed: number;
|
|
867
|
+
dropped: number;
|
|
868
|
+
analyzed: number;
|
|
869
|
+
raised: number;
|
|
870
|
+
suppressed: number;
|
|
871
|
+
tracked_clients: number;
|
|
872
|
+
clients_at_capacity: boolean;
|
|
873
|
+
}
|
|
874
|
+
/** `GET /security`. */
|
|
875
|
+
export interface SecurityResponse {
|
|
876
|
+
generated_at: number;
|
|
877
|
+
/** `false` with an empty list when `APP_LB_SIEM=0` — not a 404. */
|
|
878
|
+
enabled: boolean;
|
|
879
|
+
/** Seconds each rate-based rule counts over. */
|
|
880
|
+
window_secs: number;
|
|
881
|
+
/** Newest first. */
|
|
882
|
+
alerts: SecurityAlert[];
|
|
883
|
+
totals: SeverityTotals;
|
|
884
|
+
/** The block rules in force. Served here so a console renders findings and
|
|
885
|
+
* interventions from one fetch. Narrowed for a deployment-scoped token. */
|
|
886
|
+
rules: RuleView[];
|
|
887
|
+
guard: GuardStats;
|
|
888
|
+
/** Withheld from a deployment-scoped token, for whom it would describe
|
|
889
|
+
* traffic it cannot see. */
|
|
890
|
+
stats?: SiemStats;
|
|
891
|
+
}
|
|
892
|
+
/** A repository workflow registered with app-lb. */
|
|
893
|
+
export interface WorkflowSpec {
|
|
894
|
+
id: string;
|
|
895
|
+
repo: string;
|
|
896
|
+
ref: string;
|
|
897
|
+
path: string;
|
|
898
|
+
network: string;
|
|
899
|
+
/** A stored credential reference, never the credential value. */
|
|
900
|
+
auth?: SecretRef;
|
|
901
|
+
secrets_prefix?: string;
|
|
902
|
+
enabled: boolean;
|
|
903
|
+
}
|
|
904
|
+
export interface WorkflowList {
|
|
905
|
+
workflows: WorkflowSpec[];
|
|
906
|
+
}
|
|
907
|
+
export type JobKind = "image-build" | "artifact-pull" | "mount-pull" | "host-update";
|
|
908
|
+
export type JobStatus = "running" | "succeeded" | "failed";
|
|
909
|
+
export interface JobRecord {
|
|
910
|
+
id: string;
|
|
911
|
+
deployment: string;
|
|
912
|
+
kind: JobKind;
|
|
913
|
+
status: JobStatus;
|
|
914
|
+
started_at: number;
|
|
915
|
+
finished_at?: number | null;
|
|
916
|
+
error?: string | null;
|
|
917
|
+
log: string[];
|
|
918
|
+
repo?: string;
|
|
919
|
+
ref?: string;
|
|
920
|
+
commit?: string;
|
|
921
|
+
dockerfile?: string;
|
|
922
|
+
image?: string;
|
|
923
|
+
rolled_out?: boolean;
|
|
924
|
+
store?: string;
|
|
925
|
+
artifact?: string;
|
|
926
|
+
digest?: string;
|
|
927
|
+
bytes?: number;
|
|
928
|
+
reused?: boolean;
|
|
929
|
+
/** Site pulls only: the directory the bundle was unpacked into. */
|
|
930
|
+
site_root?: string;
|
|
931
|
+
/** Site pulls only: regular files unpacked. */
|
|
932
|
+
files?: number;
|
|
933
|
+
/**
|
|
934
|
+
* Mount pulls only: one entry per guest mount, in spec order. A mount pull
|
|
935
|
+
* covers every mount on the deployment, so the single `store`/`digest` fields
|
|
936
|
+
* above stay empty on this kind.
|
|
937
|
+
*/
|
|
938
|
+
mounts?: MountOutcome[];
|
|
939
|
+
working_dir?: string;
|
|
940
|
+
commands_total?: number;
|
|
941
|
+
commands_run?: number;
|
|
942
|
+
verified?: boolean;
|
|
943
|
+
}
|
|
944
|
+
/** What one guest mount's pull did. */
|
|
945
|
+
export interface MountOutcome {
|
|
946
|
+
/** The guest path, which identifies the mount within the deployment. */
|
|
947
|
+
path: string;
|
|
948
|
+
store: string;
|
|
949
|
+
ref: string;
|
|
950
|
+
digest?: string;
|
|
951
|
+
/** The tree on the app-lb host the guests mount. */
|
|
952
|
+
tree?: string;
|
|
953
|
+
/** Regular files unpacked. Absent on a reuse, where nothing was unpacked. */
|
|
954
|
+
files?: number;
|
|
955
|
+
/** Bytes transferred. `0` with `reused` means the tree was already there. */
|
|
956
|
+
bytes?: number;
|
|
957
|
+
/** Uncompressed size of the tree — what the mount costs the host's disk. */
|
|
958
|
+
unpacked?: number;
|
|
959
|
+
reused?: boolean;
|
|
960
|
+
/** Whether this mount's digest changed, which is what recycles the pool. */
|
|
961
|
+
changed?: boolean;
|
|
962
|
+
}
|
|
963
|
+
export interface SecretSummary {
|
|
964
|
+
id: string;
|
|
965
|
+
/** The namespace wall the secret sits behind; `default` when unset. */
|
|
966
|
+
namespace: string;
|
|
967
|
+
description: string | null;
|
|
968
|
+
/** Key *names*. Values never leave app-lb. */
|
|
969
|
+
keys: string[];
|
|
970
|
+
updated_at: number;
|
|
971
|
+
encrypted_at_rest: boolean;
|
|
972
|
+
}
|
|
973
|
+
export interface CertStatus {
|
|
974
|
+
host: string;
|
|
975
|
+
not_after: string;
|
|
976
|
+
issuer: string;
|
|
977
|
+
needs_renewal: boolean;
|
|
978
|
+
}
|
|
979
|
+
export interface ExecOutput {
|
|
980
|
+
sandbox_id: string;
|
|
981
|
+
exit_code: number;
|
|
982
|
+
stdout: string;
|
|
983
|
+
stderr: string;
|
|
984
|
+
/** stdout and stderr interleaved as the guest wrote them. */
|
|
985
|
+
output: string;
|
|
986
|
+
}
|
|
987
|
+
export interface EvictOutcome {
|
|
988
|
+
sandbox_id: string;
|
|
989
|
+
/** `killed` (immediate) or `draining` (still serving what it has). */
|
|
990
|
+
outcome: "killed" | "draining";
|
|
991
|
+
}
|
|
992
|
+
/** What a token may do on the admin API. */
|
|
993
|
+
export type AdminScope = "none" | "view" | "admin";
|
|
994
|
+
export interface TokenSummary {
|
|
995
|
+
id: string;
|
|
996
|
+
name: string;
|
|
997
|
+
admin: AdminScope;
|
|
998
|
+
/**
|
|
999
|
+
* The namespace this token is confined to, if any. Inside it, an empty
|
|
1000
|
+
* `deployments` list means every deployment there; outside it the token
|
|
1001
|
+
* reaches nothing.
|
|
1002
|
+
*/
|
|
1003
|
+
namespace?: string;
|
|
1004
|
+
/** Deployment ids, or `["*"]` for all of them. */
|
|
1005
|
+
deployments: string[];
|
|
1006
|
+
created_at: number;
|
|
1007
|
+
expires_at?: number;
|
|
1008
|
+
/**
|
|
1009
|
+
* `undefined` also means "not used since the store was last written" — the
|
|
1010
|
+
* stamp is flushed opportunistically, not per request.
|
|
1011
|
+
*/
|
|
1012
|
+
last_used_at?: number;
|
|
1013
|
+
/** Minted at a control plane and valid on every server that mirrors it. */
|
|
1014
|
+
fleet?: boolean;
|
|
1015
|
+
/**
|
|
1016
|
+
* Set when this server holds the token only as a mirror: the control plane
|
|
1017
|
+
* it came from, which is where it is changed or revoked.
|
|
1018
|
+
*/
|
|
1019
|
+
mirrored_from?: string;
|
|
1020
|
+
}
|
|
1021
|
+
/** One namespace that has feed events, from `GET /feeds`. */
|
|
1022
|
+
export interface FeedIndexEntry {
|
|
1023
|
+
namespace: string;
|
|
1024
|
+
events: number;
|
|
1025
|
+
}
|
|
1026
|
+
/**
|
|
1027
|
+
* One event from a namespace's feed (`GET /feeds/:ns?format=json`) — the same
|
|
1028
|
+
* entries the RSS document carries; `id` is the RSS `<guid>`.
|
|
1029
|
+
*/
|
|
1030
|
+
export interface FeedEvent {
|
|
1031
|
+
id: number;
|
|
1032
|
+
/** When the event first happened (unix seconds). */
|
|
1033
|
+
ts: number;
|
|
1034
|
+
/** When it last happened — repeats of the same issue fold into one entry. */
|
|
1035
|
+
last_ts: number;
|
|
1036
|
+
count: number;
|
|
1037
|
+
namespace: string;
|
|
1038
|
+
deployment: string;
|
|
1039
|
+
kind: "deployed" | "updated" | "removed" | "issue";
|
|
1040
|
+
title: string;
|
|
1041
|
+
detail: string;
|
|
1042
|
+
}
|
|
1043
|
+
/**
|
|
1044
|
+
* The reply to a mint. **`token` is the only time the secret is returned** —
|
|
1045
|
+
* app-lb stores only its hash and no endpoint reads it back.
|
|
1046
|
+
*/
|
|
1047
|
+
export interface MintedToken extends TokenSummary {
|
|
1048
|
+
token: string;
|
|
1049
|
+
}
|
|
1050
|
+
/** `GET /api/plugins` — a built-in plugin, its stored record and its live status. */
|
|
1051
|
+
export interface PluginView {
|
|
1052
|
+
id: string;
|
|
1053
|
+
name: string;
|
|
1054
|
+
description: string;
|
|
1055
|
+
/** JSON Schema for `config`. Advisory; app-lb validates on write. */
|
|
1056
|
+
config_schema: unknown;
|
|
1057
|
+
enabled: boolean;
|
|
1058
|
+
/** Kept while disabled, so re-enabling does not lose it. */
|
|
1059
|
+
config: unknown;
|
|
1060
|
+
updated_at: number;
|
|
1061
|
+
/** Why the last apply failed. A plugin can be enabled *and* failing. */
|
|
1062
|
+
last_error?: string;
|
|
1063
|
+
/** Whatever the plugin reports about itself; the shape is per plugin. */
|
|
1064
|
+
status: unknown;
|
|
1065
|
+
/** Whether namespaces install this plugin themselves. */
|
|
1066
|
+
per_namespace?: boolean;
|
|
1067
|
+
/** The namespaces that have, for a per-namespace plugin. */
|
|
1068
|
+
installed_in?: string[];
|
|
1069
|
+
}
|
|
1070
|
+
/**
|
|
1071
|
+
* `GET /whoami` — what the server makes of the credential presented. Needs no
|
|
1072
|
+
* tier, so it answers even for a token every other route refuses.
|
|
1073
|
+
*/
|
|
1074
|
+
export interface WhoAmI {
|
|
1075
|
+
/** `ungated`, `operator`, `app-token` or `federated`. */
|
|
1076
|
+
caller: string;
|
|
1077
|
+
/** `none`, `view`, `admin` — or `unchecked` on an ungated listener. */
|
|
1078
|
+
admin_scope: string;
|
|
1079
|
+
/** Whether this credential may use the fleet-wide routes. */
|
|
1080
|
+
fleet: boolean;
|
|
1081
|
+
/** Whether it is behind a namespace wall. */
|
|
1082
|
+
confined: boolean;
|
|
1083
|
+
may: {
|
|
1084
|
+
read_view_routes: boolean;
|
|
1085
|
+
use_admin_routes: boolean;
|
|
1086
|
+
[extra: string]: unknown;
|
|
1087
|
+
};
|
|
1088
|
+
token?: {
|
|
1089
|
+
id: string;
|
|
1090
|
+
name: string;
|
|
1091
|
+
[extra: string]: unknown;
|
|
1092
|
+
};
|
|
1093
|
+
/** An app-token's namespace, when it is confined to one. */
|
|
1094
|
+
namespace?: string;
|
|
1095
|
+
/** `["*"]` is every deployment; empty on a namespace token is all of it. */
|
|
1096
|
+
deployments?: string[];
|
|
1097
|
+
expires_at?: number;
|
|
1098
|
+
expires_in_secs?: number;
|
|
1099
|
+
/** A federated caller's identity, as the auth service reported it. */
|
|
1100
|
+
subject?: unknown;
|
|
1101
|
+
/** A federated caller's namespaces and its tier in each. */
|
|
1102
|
+
namespaces?: Record<string, string>;
|
|
1103
|
+
detail?: string;
|
|
1104
|
+
note?: string;
|
|
1105
|
+
[extra: string]: unknown;
|
|
1106
|
+
}
|
|
1107
|
+
/** `POST /deployments/:id/rollouts` and `GET …/rollouts/:operation`. */
|
|
1108
|
+
export interface RolloutOperation {
|
|
1109
|
+
operation_id: string;
|
|
1110
|
+
deployment: string;
|
|
1111
|
+
/** The `rollout_revision` the operation was admitted against. */
|
|
1112
|
+
source_revision: string;
|
|
1113
|
+
target_spec_sha256: string;
|
|
1114
|
+
/** `running`, `succeeded`, `failed` or `reconciliation_required`. */
|
|
1115
|
+
status: string;
|
|
1116
|
+
phase: string;
|
|
1117
|
+
readiness_verified: boolean;
|
|
1118
|
+
previous_stopped: boolean;
|
|
1119
|
+
error?: string | null;
|
|
1120
|
+
preparation_stage?: string | null;
|
|
1121
|
+
/** Present once a failed rollout's candidates have been reclaimed. */
|
|
1122
|
+
failure_settlement?: unknown;
|
|
1123
|
+
[extra: string]: unknown;
|
|
1124
|
+
}
|
|
1125
|
+
/** `GET /deployments/:id/discovery-status`. camelCase on the wire. */
|
|
1126
|
+
export interface DiscoveryStatus {
|
|
1127
|
+
serviceId: string;
|
|
1128
|
+
sourceUrl?: string | null;
|
|
1129
|
+
version?: number | null;
|
|
1130
|
+
/** The regional discovery state; its shape belongs to the regional protocol. */
|
|
1131
|
+
regional?: unknown;
|
|
1132
|
+
upstreams: {
|
|
1133
|
+
peer: string;
|
|
1134
|
+
draining: boolean;
|
|
1135
|
+
inFlight: number;
|
|
1136
|
+
[extra: string]: unknown;
|
|
1137
|
+
}[];
|
|
1138
|
+
[extra: string]: unknown;
|
|
1139
|
+
}
|
|
1140
|
+
/** `GET /namespaces` — one namespace the credential can see. */
|
|
1141
|
+
export interface NamespaceEntry {
|
|
1142
|
+
namespace: string;
|
|
1143
|
+
/** How many of its deployments this credential may view. */
|
|
1144
|
+
deployments: number;
|
|
1145
|
+
/** Whether it was declared, rather than only named by a deployment. */
|
|
1146
|
+
declared: boolean;
|
|
1147
|
+
description?: string;
|
|
1148
|
+
created_at?: number;
|
|
1149
|
+
[extra: string]: unknown;
|
|
1150
|
+
}
|
|
1151
|
+
/** `GET /auth-providers` — a declared identity provider. */
|
|
1152
|
+
export interface AuthProviderView {
|
|
1153
|
+
name: string;
|
|
1154
|
+
namespace: string;
|
|
1155
|
+
description?: string;
|
|
1156
|
+
created_at: number;
|
|
1157
|
+
provider: AuthProvider | AuthProvider[];
|
|
1158
|
+
client_id?: string;
|
|
1159
|
+
client_secret?: SecretRef;
|
|
1160
|
+
allowed_domains?: string[];
|
|
1161
|
+
allowed_emails?: string[];
|
|
1162
|
+
jwt?: JwtSpec;
|
|
1163
|
+
cookie_domain?: string;
|
|
1164
|
+
[extra: string]: unknown;
|
|
1165
|
+
}
|
|
1166
|
+
/**
|
|
1167
|
+
* `GET /namespaces/:ns/plugins` — a plugin as one namespace sees it. `enabled`
|
|
1168
|
+
* is the operator's fleet switch; `installed` is this namespace's. A plugin
|
|
1169
|
+
* does nothing for a namespace unless both are true.
|
|
1170
|
+
*/
|
|
1171
|
+
export interface NamespacePlugin {
|
|
1172
|
+
id: string;
|
|
1173
|
+
name: string;
|
|
1174
|
+
description: string;
|
|
1175
|
+
enabled: boolean;
|
|
1176
|
+
installed: boolean;
|
|
1177
|
+
installed_at?: number;
|
|
1178
|
+
/** `token:<id>` or `user:<id>`; absent for the operator. */
|
|
1179
|
+
installed_by?: string;
|
|
1180
|
+
config?: unknown;
|
|
1181
|
+
[extra: string]: unknown;
|
|
1182
|
+
}
|
|
1183
|
+
export interface NamespaceInstall {
|
|
1184
|
+
installed_at: number;
|
|
1185
|
+
installed_by?: string | null;
|
|
1186
|
+
config: unknown;
|
|
1187
|
+
[extra: string]: unknown;
|
|
1188
|
+
}
|
|
1189
|
+
/** `GET /api/plugins/:id/installs` — fleet scope only. */
|
|
1190
|
+
export interface PluginInstalls {
|
|
1191
|
+
plugin: string;
|
|
1192
|
+
enabled: boolean;
|
|
1193
|
+
namespaces: string[];
|
|
1194
|
+
installs: Record<string, NamespaceInstall>;
|
|
1195
|
+
[extra: string]: unknown;
|
|
1196
|
+
}
|
|
1197
|
+
/** Whether recently ingested telemetry is queryable yet. */
|
|
1198
|
+
export interface ObsFreshness {
|
|
1199
|
+
buffered_rows: number;
|
|
1200
|
+
flush_secs: number;
|
|
1201
|
+
dropped: number;
|
|
1202
|
+
[extra: string]: unknown;
|
|
1203
|
+
}
|
|
1204
|
+
/** One time bucket. A `null` measure means nothing was sampled, not zero. */
|
|
1205
|
+
export interface ObsMetricBucket {
|
|
1206
|
+
/** Bucket start, epoch milliseconds UTC. */
|
|
1207
|
+
t: number;
|
|
1208
|
+
requests_per_sec?: number | null;
|
|
1209
|
+
errors_per_sec?: number | null;
|
|
1210
|
+
mean_latency_ms?: number | null;
|
|
1211
|
+
p50_ms?: number | null;
|
|
1212
|
+
p90_ms?: number | null;
|
|
1213
|
+
p99_ms?: number | null;
|
|
1214
|
+
cpu_percent?: number | null;
|
|
1215
|
+
memory_bytes?: number | null;
|
|
1216
|
+
in_flight?: number | null;
|
|
1217
|
+
ready?: number | null;
|
|
1218
|
+
pending?: number | null;
|
|
1219
|
+
draining?: number | null;
|
|
1220
|
+
[extra: string]: unknown;
|
|
1221
|
+
}
|
|
1222
|
+
export interface ObsLogBucket {
|
|
1223
|
+
t: number;
|
|
1224
|
+
lines: number;
|
|
1225
|
+
errors: number;
|
|
1226
|
+
[extra: string]: unknown;
|
|
1227
|
+
}
|
|
1228
|
+
export interface ObsFleetRow {
|
|
1229
|
+
id: string;
|
|
1230
|
+
buckets: ObsMetricBucket[];
|
|
1231
|
+
log_buckets: ObsLogBucket[];
|
|
1232
|
+
latest: ObsMetricBucket;
|
|
1233
|
+
log_lines: number;
|
|
1234
|
+
error_logs: number;
|
|
1235
|
+
[extra: string]: unknown;
|
|
1236
|
+
}
|
|
1237
|
+
/** `…/plugins/obs/api/fleet` — a namespace's telemetry overview. */
|
|
1238
|
+
export interface ObsFleet {
|
|
1239
|
+
generated_at_ms: number;
|
|
1240
|
+
from_ms: number;
|
|
1241
|
+
to_ms: number;
|
|
1242
|
+
step_secs: number;
|
|
1243
|
+
window: string;
|
|
1244
|
+
windows: string[];
|
|
1245
|
+
retain_days: number;
|
|
1246
|
+
freshness: ObsFreshness;
|
|
1247
|
+
/** Operator view only; absent in a namespace's. */
|
|
1248
|
+
host?: ObsMetricBucket[];
|
|
1249
|
+
deployments: ObsFleetRow[];
|
|
1250
|
+
/** Operator view only. */
|
|
1251
|
+
host_sandboxes?: unknown;
|
|
1252
|
+
[extra: string]: unknown;
|
|
1253
|
+
}
|
|
1254
|
+
/** `…/plugins/obs/api/deployments/:id`. */
|
|
1255
|
+
export interface ObsDeployment {
|
|
1256
|
+
id: string;
|
|
1257
|
+
generated_at_ms: number;
|
|
1258
|
+
from_ms: number;
|
|
1259
|
+
to_ms: number;
|
|
1260
|
+
step_secs: number;
|
|
1261
|
+
window: string;
|
|
1262
|
+
windows: string[];
|
|
1263
|
+
retain_days: number;
|
|
1264
|
+
freshness: ObsFreshness;
|
|
1265
|
+
buckets: ObsMetricBucket[];
|
|
1266
|
+
log_buckets: ObsLogBucket[];
|
|
1267
|
+
latest: ObsMetricBucket;
|
|
1268
|
+
log_lines: number;
|
|
1269
|
+
error_logs: number;
|
|
1270
|
+
/** Backends that logged in the window. */
|
|
1271
|
+
backends: string[];
|
|
1272
|
+
[extra: string]: unknown;
|
|
1273
|
+
}
|
|
1274
|
+
/** One stored log line. */
|
|
1275
|
+
export interface ObsLogRow {
|
|
1276
|
+
/** Epoch milliseconds UTC. */
|
|
1277
|
+
ts: number;
|
|
1278
|
+
level?: string | null;
|
|
1279
|
+
/** `stdout`, `stderr`, `console`, `access`, `security`, … */
|
|
1280
|
+
source: string;
|
|
1281
|
+
message: string;
|
|
1282
|
+
backend?: string | null;
|
|
1283
|
+
host?: string | null;
|
|
1284
|
+
/** The structured payload, still a JSON string. */
|
|
1285
|
+
fields?: string | null;
|
|
1286
|
+
[extra: string]: unknown;
|
|
1287
|
+
}
|
|
1288
|
+
/** `…/plugins/obs/api/deployments/:id/logs` — one page, newest first. */
|
|
1289
|
+
export interface ObsLogs {
|
|
1290
|
+
id: string;
|
|
1291
|
+
from_ms: number;
|
|
1292
|
+
to_ms: number;
|
|
1293
|
+
rows: ObsLogRow[];
|
|
1294
|
+
/**
|
|
1295
|
+
* Pass back as `before` for the next page; `null` at the end. Inclusive, so
|
|
1296
|
+
* the next page may repeat lines from the same millisecond.
|
|
1297
|
+
*/
|
|
1298
|
+
next_before_ms: number | null;
|
|
1299
|
+
limit: number;
|
|
1300
|
+
[extra: string]: unknown;
|
|
1301
|
+
}
|
|
1302
|
+
/** An alert rule, from `…/plugins/obs/api/alerts`. */
|
|
1303
|
+
export interface ObsAlert {
|
|
1304
|
+
id: string;
|
|
1305
|
+
deployment: string;
|
|
1306
|
+
namespace?: string | null;
|
|
1307
|
+
/** `errors` — errors over the trailing minute. */
|
|
1308
|
+
metric: string;
|
|
1309
|
+
threshold: number;
|
|
1310
|
+
webhook_url: string;
|
|
1311
|
+
[extra: string]: unknown;
|
|
1312
|
+
}
|