@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.
@@ -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
+ }