@telorun/runner-core 0.5.2

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.
Files changed (109) hide show
  1. package/LICENSE +17 -0
  2. package/README.md +38 -0
  3. package/dist/backend.d.ts +71 -0
  4. package/dist/backend.d.ts.map +1 -0
  5. package/dist/backend.js +2 -0
  6. package/dist/backend.js.map +1 -0
  7. package/dist/base-image-catalog.d.ts +110 -0
  8. package/dist/base-image-catalog.d.ts.map +1 -0
  9. package/dist/base-image-catalog.js +245 -0
  10. package/dist/base-image-catalog.js.map +1 -0
  11. package/dist/capabilities-schema.d.ts +33 -0
  12. package/dist/capabilities-schema.d.ts.map +1 -0
  13. package/dist/capabilities-schema.js +44 -0
  14. package/dist/capabilities-schema.js.map +1 -0
  15. package/dist/config.d.ts +37 -0
  16. package/dist/config.d.ts.map +1 -0
  17. package/dist/config.js +93 -0
  18. package/dist/config.js.map +1 -0
  19. package/dist/contract.d.ts +170 -0
  20. package/dist/contract.d.ts.map +1 -0
  21. package/dist/contract.js +24 -0
  22. package/dist/contract.js.map +1 -0
  23. package/dist/debug/relay.d.ts +25 -0
  24. package/dist/debug/relay.d.ts.map +1 -0
  25. package/dist/debug/relay.js +89 -0
  26. package/dist/debug/relay.js.map +1 -0
  27. package/dist/dependency-key.d.ts +38 -0
  28. package/dist/dependency-key.d.ts.map +1 -0
  29. package/dist/dependency-key.js +68 -0
  30. package/dist/dependency-key.js.map +1 -0
  31. package/dist/index.d.ts +20 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +19 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/routes/capabilities.d.ts +12 -0
  36. package/dist/routes/capabilities.d.ts.map +1 -0
  37. package/dist/routes/capabilities.js +15 -0
  38. package/dist/routes/capabilities.js.map +1 -0
  39. package/dist/routes/health.d.ts +5 -0
  40. package/dist/routes/health.d.ts.map +1 -0
  41. package/dist/routes/health.js +8 -0
  42. package/dist/routes/health.js.map +1 -0
  43. package/dist/routes/io.d.ts +8 -0
  44. package/dist/routes/io.d.ts.map +1 -0
  45. package/dist/routes/io.js +239 -0
  46. package/dist/routes/io.js.map +1 -0
  47. package/dist/routes/probe.d.ts +7 -0
  48. package/dist/routes/probe.d.ts.map +1 -0
  49. package/dist/routes/probe.js +22 -0
  50. package/dist/routes/probe.js.map +1 -0
  51. package/dist/routes/sessions.d.ts +22 -0
  52. package/dist/routes/sessions.d.ts.map +1 -0
  53. package/dist/routes/sessions.js +223 -0
  54. package/dist/routes/sessions.js.map +1 -0
  55. package/dist/server.d.ts +34 -0
  56. package/dist/server.d.ts.map +1 -0
  57. package/dist/server.js +67 -0
  58. package/dist/server.js.map +1 -0
  59. package/dist/session/bundle-path.d.ts +12 -0
  60. package/dist/session/bundle-path.d.ts.map +1 -0
  61. package/dist/session/bundle-path.js +27 -0
  62. package/dist/session/bundle-path.js.map +1 -0
  63. package/dist/session/byte-ring-buffer.d.ts +29 -0
  64. package/dist/session/byte-ring-buffer.d.ts.map +1 -0
  65. package/dist/session/byte-ring-buffer.js +54 -0
  66. package/dist/session/byte-ring-buffer.js.map +1 -0
  67. package/dist/session/registry.d.ts +62 -0
  68. package/dist/session/registry.d.ts.map +1 -0
  69. package/dist/session/registry.js +156 -0
  70. package/dist/session/registry.js.map +1 -0
  71. package/dist/session/ring-buffer.d.ts +37 -0
  72. package/dist/session/ring-buffer.d.ts.map +1 -0
  73. package/dist/session/ring-buffer.js +64 -0
  74. package/dist/session/ring-buffer.js.map +1 -0
  75. package/dist/session/session-id.d.ts +3 -0
  76. package/dist/session/session-id.d.ts.map +1 -0
  77. package/dist/session/session-id.js +19 -0
  78. package/dist/session/session-id.js.map +1 -0
  79. package/dist/sse/channel.d.ts +11 -0
  80. package/dist/sse/channel.d.ts.map +1 -0
  81. package/dist/sse/channel.js +129 -0
  82. package/dist/sse/channel.js.map +1 -0
  83. package/package.json +48 -0
  84. package/src/backend.ts +88 -0
  85. package/src/base-image-catalog.test.ts +209 -0
  86. package/src/base-image-catalog.ts +320 -0
  87. package/src/capabilities-schema.test.ts +54 -0
  88. package/src/capabilities-schema.ts +71 -0
  89. package/src/config.ts +122 -0
  90. package/src/contract.ts +170 -0
  91. package/src/debug/relay.ts +104 -0
  92. package/src/dependency-key.test.ts +64 -0
  93. package/src/dependency-key.ts +105 -0
  94. package/src/index.ts +33 -0
  95. package/src/routes/capabilities.ts +20 -0
  96. package/src/routes/health.ts +9 -0
  97. package/src/routes/io.ts +265 -0
  98. package/src/routes/probe.ts +35 -0
  99. package/src/routes/sessions.ts +270 -0
  100. package/src/server.ts +108 -0
  101. package/src/session/bundle-path.ts +27 -0
  102. package/src/session/byte-ring-buffer.ts +62 -0
  103. package/src/session/registry.test.ts +34 -0
  104. package/src/session/registry.ts +185 -0
  105. package/src/session/ring-buffer.test.ts +54 -0
  106. package/src/session/ring-buffer.ts +75 -0
  107. package/src/session/session-id.test.ts +17 -0
  108. package/src/session/session-id.ts +20 -0
  109. package/src/sse/channel.ts +154 -0
package/LICENSE ADDED
@@ -0,0 +1,17 @@
1
+ # SUSTAINABLE USE LICENSE (Fair-code)
2
+
3
+ Copyright (c) 2026 CodeNet Sp. z o.o.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to use, copy, modify, and distribute the Software for any purpose—including commercial purposes—subject to the following conditions:
6
+
7
+ 1. ANTI-COMPETITION RESTRICTION: The Software may not be provided to third parties as a managed service, commercial SaaS (Software-as-a-Service), PaaS (Platform-as-a-Service), BaaS (Backend-as-a-Service), or similar offering where the primary value provided to the user is the functionality of the Software itself, without a separate commercial license from the copyright holder.
8
+
9
+ 2. PERMITTED COMMERCIAL USE: You are free to use the Software to build, host, and monetize your own commercial applications, products, and services, provided such use does not violate Clause 1.
10
+
11
+ 3. ATTRIBUTION: This copyright notice and license must be included in all copies or substantial portions of the Software.
12
+
13
+ 4. CONTRIBUTIONS: Contributions to the Software are welcome and encouraged. By contributing, you agree that your contributions may be incorporated into the Software and distributed under this license.
14
+
15
+ 5. DISCLAIMER: The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose and noninfringement. In no event shall the authors or copyright holders be liable for any claim, damages or other liability, whether in an action of contract, tort or otherwise, arising from, out of or in connection with the Software or the use or other dealings in the Software.
16
+
17
+ For commercial licensing, managed hosting exemptions, or enterprise inquiries, please contact <contact@codenet.pl>.
package/README.md ADDED
@@ -0,0 +1,38 @@
1
+ # @telorun/runner-core
2
+
3
+ Backend-neutral core shared by the Telo runners ([`docker-runner`](../../apps/docker-runner)
4
+ and [`k8s-runner`](../../apps/k8s-runner)). It owns everything about a run
5
+ *except* how the workload is actually spawned:
6
+
7
+ - the `/v1` HTTP+SSE **session contract** and Fastify routes (`/v1/health`,
8
+ `/v1/probe`, `/v1/sessions`, `/v1/sessions/:id/events` SSE,
9
+ `/v1/sessions/:id/io` WebSocket PTY);
10
+ - the in-memory **session registry**, event/byte **ring buffers** (SSE + PTY
11
+ replay with gap detection), and bundle-path traversal guards;
12
+ - graceful-shutdown helpers and base config parsing.
13
+
14
+ ## The seam: `RunnerBackend`
15
+
16
+ Concrete runners implement a small abstract interface — a byte-stream out
17
+ (`onOutput`), a stdin writer, a resize signal, and a wait/exit (`done`) — *not*
18
+ any docker- or k8s-specific stream shape:
19
+
20
+ ```ts
21
+ interface RunnerBackend {
22
+ probe(config): Promise<AvailabilityReport>;
23
+ start(spec): Promise<BackendSession>; // writeStdin / resize / done / stop
24
+ reapOrphans?(): Promise<void>;
25
+ }
26
+ ```
27
+
28
+ Bundle delivery is the backend's responsibility (docker writes a shared-volume
29
+ workdir; k8s stages a tarball for an initContainer fetch), so `start` receives
30
+ the raw `bundle` rather than a pre-resolved path. `buildServer({ backend, config,
31
+ version })` wires a backend into the full `/v1` app.
32
+
33
+ ## Development
34
+
35
+ ```bash
36
+ pnpm --filter @telorun/runner-core build
37
+ pnpm --filter @telorun/runner-core test
38
+ ```
@@ -0,0 +1,71 @@
1
+ import type { DebugFrame } from "@telorun/debug-wire";
2
+ import type { AvailabilityReport, PortMapping, ProbeConfig, RunBundle, RunPhase, RunStatus, SessionConfig } from "./contract.js";
3
+ /**
4
+ * The seam between backend-neutral session machinery (routes, registry, SSE)
5
+ * and a concrete workload runtime (docker container, kubernetes pod).
6
+ *
7
+ * Deliberately abstract: a byte-stream out (`onOutput`), a stdin writer
8
+ * (`BackendSession.writeStdin`), a resize signal, and a wait/exit (`done`) —
9
+ * NOT docker's `ReadWriteStream` duplex. The k8s backend serves the same shape
10
+ * over the Pod `attach` subresource; the docker backend adapts its hijacked
11
+ * attach duplex onto it. Bundle delivery is the backend's responsibility too
12
+ * (docker writes a shared-volume workdir; k8s stages the bundle for an
13
+ * initContainer fetch), so the spec carries the raw `bundle` rather than a
14
+ * pre-resolved on-disk path.
15
+ */
16
+ export interface RunnerBackend {
17
+ /** Report readiness without starting a session (daemon/cluster reachable,
18
+ * image pullable, scaffolding present). Backs `POST /v1/probe`. */
19
+ probe(config: ProbeConfig): Promise<AvailabilityReport>;
20
+ /** Spawn the workload and wire it to the supplied callbacks. Resolves once
21
+ * the workload has started (after a `running` status is emitted); rejects
22
+ * with `SessionStartError` for any pre-start failure. */
23
+ start(spec: BackendStartSpec): Promise<BackendSession>;
24
+ /** Reap workloads orphaned by a prior runner process, matched by the
25
+ * backend's own labelling. Run once at boot — the session registry is
26
+ * in-memory, so a restart otherwise leaks running workloads. */
27
+ reapOrphans?(): Promise<void>;
28
+ }
29
+ export interface BackendStartSpec {
30
+ sessionId: string;
31
+ bundle: RunBundle;
32
+ /** Bundle-relative entry path, already traversal-normalized by core. */
33
+ entryRelativePath: string;
34
+ env: Record<string, string>;
35
+ ports: PortMapping[];
36
+ config: SessionConfig;
37
+ /** When true, launch the workload with `--inspect` and relay its kernel debug
38
+ * stream via `onDebug`. The inspect endpoint stays reachable only by the
39
+ * runner — never published outward. */
40
+ inspect: boolean;
41
+ /** Emit a lifecycle status. The backend drives `starting` → `running` →
42
+ * terminal (`exited`/`failed`/`stopped`). */
43
+ onStatus(status: RunStatus): void;
44
+ /** Emit a progress message for a coming-up phase (build / provision / boot).
45
+ * Additive to status — surfaces what's happening while the session is still
46
+ * `starting`, driving the editor's spinner + step feed. */
47
+ onProgress(phase: RunPhase, message: string, done?: boolean): void;
48
+ /** Raw merged stdout/stderr (PTY) bytes from the workload. */
49
+ onOutput(chunk: Buffer): void;
50
+ /** A frame relayed from the workload's kernel debug stream. Only called when
51
+ * `inspect` is true and the backend has connected to the inspect endpoint. */
52
+ onDebug(frame: DebugFrame): void;
53
+ /** True once a user stop / shutdown has been requested — lets the backend
54
+ * classify a kill as `stopped` rather than `failed`. */
55
+ isUserStopped(): boolean;
56
+ }
57
+ export interface BackendSession {
58
+ /** Write bytes to the workload's stdin (PTY). A no-op once the workload has
59
+ * terminated, so callers need not track liveness. */
60
+ writeStdin(bytes: Uint8Array): void;
61
+ /** Resize the workload's PTY. */
62
+ resize(cols: number, rows: number): void;
63
+ /** Resolves after the workload terminates and its terminal status has been
64
+ * emitted via `onStatus`. Never rejects — terminal failures surface as a
65
+ * `failed` status. */
66
+ readonly done: Promise<void>;
67
+ /** Force-stop the workload. Idempotent and safe to call after natural exit
68
+ * (a backend should treat an already-gone workload as a no-op). */
69
+ stop(): Promise<void>;
70
+ }
71
+ //# sourceMappingURL=backend.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backend.d.ts","sourceRoot":"","sources":["../src/backend.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAEtD,OAAO,KAAK,EACV,kBAAkB,EAClB,WAAW,EACX,WAAW,EACX,SAAS,EACT,QAAQ,EACR,SAAS,EACT,aAAa,EACd,MAAM,eAAe,CAAC;AAEvB;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,aAAa;IAC5B;wEACoE;IACpE,KAAK,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAAC;IAExD;;8DAE0D;IAC1D,KAAK,CAAC,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;IAEvD;;qEAEiE;IACjE,WAAW,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/B;AAED,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,SAAS,CAAC;IAClB,wEAAwE;IACxE,iBAAiB,EAAE,MAAM,CAAC;IAC1B,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5B,KAAK,EAAE,WAAW,EAAE,CAAC;IACrB,MAAM,EAAE,aAAa,CAAC;IACtB;;4CAEwC;IACxC,OAAO,EAAE,OAAO,CAAC;IAEjB;kDAC8C;IAC9C,QAAQ,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CAAC;IAClC;;gEAE4D;IAC5D,UAAU,CAAC,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;IACnE,8DAA8D;IAC9D,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B;mFAC+E;IAC/E,OAAO,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI,CAAC;IACjC;6DACyD;IACzD,aAAa,IAAI,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,cAAc;IAC7B;0DACsD;IACtD,UAAU,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI,CAAC;IAEpC,iCAAiC;IACjC,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAEzC;;2BAEuB;IACvB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IAE7B;wEACoE;IACpE,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACvB"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=backend.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backend.js","sourceRoot":"","sources":["../src/backend.ts"],"names":[],"mappings":""}
@@ -0,0 +1,110 @@
1
+ /**
2
+ * A base-image catalog backed by the Docker Hub repository-tags API. A runner
3
+ * advertises the resolved tag list as an `enum` on its session `image` field so
4
+ * the editor renders a base-image picker, and validates a chosen image against
5
+ * the same list server-side — the runner is the source of truth, so a non-editor
6
+ * client can't widen the allowed set.
7
+ *
8
+ * The list is fetched at startup and on a refresh interval, cached in memory,
9
+ * and degrades gracefully: when Docker Hub is unreachable the catalog still
10
+ * serves the configured default (always present), so the runner boots and a
11
+ * session can still start on the default image.
12
+ *
13
+ * Free of runner specifics — it takes only a repository, a default ref, and a
14
+ * tag filter — but the tag source is specifically Docker Hub's registry API
15
+ * (`hub.docker.com/v2`); a base on another registry (GHCR, a private registry)
16
+ * can't be enumerated or digest-resolved here. The Docker registry the kubelet
17
+ * pulls from is a separate concern; this resolves the *menu* of base images, not
18
+ * pull auth.
19
+ */
20
+ /** A composable predicate set narrowing a repository's tag list to the offered
21
+ * menu. Named flags cover the common cases; `include`/`exclude` are the regex
22
+ * escape hatch for everything else. */
23
+ export interface TagFilter {
24
+ /** Keep only `MAJOR.MINOR.PATCH[-variant]` tags — drops moving tags (`latest`,
25
+ * `0`, `0.30`) and their variants (`latest-slim`, `0-slim`). */
26
+ pinnedOnly?: boolean;
27
+ /** Drop commit-hash tags (`sha-<hex>` or a bare hex string). */
28
+ excludeSha?: boolean;
29
+ /** Drop semver prereleases (`0.30.1-rc.1`, `…-alpha`). Variant suffixes like
30
+ * `-slim` / `-rust-1.95.0` are NOT prereleases and are kept. */
31
+ excludePrerelease?: boolean;
32
+ /** A tag must match at least one of these to be kept. */
33
+ include?: RegExp[];
34
+ /** A tag matching any of these is dropped. */
35
+ exclude?: RegExp[];
36
+ }
37
+ export interface BaseImageCatalogOptions {
38
+ /** `namespace/repository`, e.g. `telorun/node`. */
39
+ repository: string;
40
+ /** Always-present default ref — the pre-selected option and the
41
+ * Docker-Hub-unreachable fallback, e.g. `telorun/node:latest-slim`. */
42
+ defaultRef: string;
43
+ filter?: TagFilter;
44
+ /** Cap the advertised list to this many tags (newest first). Default 20. */
45
+ limit?: number;
46
+ /** Background refresh cadence in ms. Default 1h. */
47
+ refreshIntervalMs?: number;
48
+ /** Upper bound on tags pulled from Docker Hub before filtering. Default 200. */
49
+ maxTagsScanned?: number;
50
+ /** Per-request timeout for a Docker Hub call, in ms. Default 10s. */
51
+ requestTimeoutMs?: number;
52
+ /** Injectable fetch for tests; defaults to the global `fetch`. */
53
+ fetchImpl?: typeof fetch;
54
+ }
55
+ /** Apply a {@link TagFilter} to a tag list, preserving input order. Pure — the
56
+ * unit-testable core of the catalog. */
57
+ export declare function filterTags(tags: string[], filter?: TagFilter): string[];
58
+ /**
59
+ * Parse a Docker image reference into the Docker Hub coordinates needed to look
60
+ * up its digest, or `null` when the ref points at a non-Docker-Hub registry.
61
+ * Handles the `library/` shorthand, optional `docker.io` host, and an already
62
+ * `@sha256:`-pinned digest (carried through verbatim).
63
+ */
64
+ export declare function parseDockerHubRef(ref: string): {
65
+ namespace: string;
66
+ repo: string;
67
+ tag: string;
68
+ digest?: string;
69
+ } | null;
70
+ /**
71
+ * Resolve a Docker Hub image ref's CURRENT manifest digest. A moving tag
72
+ * (`latest-slim`) maps to a new digest once a new version is published, so a
73
+ * build that folds this into its cache key detects that the base moved and
74
+ * rebuilds. An already `@sha256:`-pinned ref returns its digest without a
75
+ * network call. Returns `undefined` for a non-Docker-Hub ref (not an error —
76
+ * resolution simply doesn't apply) or when a Hub request fails. A genuine
77
+ * request failure is reported to `onError` rather than swallowed, so an operator
78
+ * whose Hub calls persistently fail (auth / network / rate-limit) gets a signal
79
+ * instead of an unexplained "always reuses the cached image".
80
+ */
81
+ export declare function resolveTagDigest(ref: string, opts?: {
82
+ fetchImpl?: typeof fetch;
83
+ requestTimeoutMs?: number;
84
+ onError?: (err: unknown) => void;
85
+ }): Promise<string | undefined>;
86
+ export declare class BaseImageCatalog {
87
+ private readonly repository;
88
+ private readonly defaultRef;
89
+ private readonly filter;
90
+ private readonly limit;
91
+ private readonly refreshIntervalMs;
92
+ private readonly maxTagsScanned;
93
+ private readonly requestTimeoutMs;
94
+ private readonly fetchImpl;
95
+ /** Filtered tags (bare, newest first); empty until the first refresh. */
96
+ private tags;
97
+ private timer?;
98
+ constructor(opts: BaseImageCatalogOptions);
99
+ /** The advertised image refs: the default first, then the filtered tags. */
100
+ current(): string[];
101
+ /** Whether `ref` is offered by this catalog (server-side allowlist check). */
102
+ isAllowed(ref: string): boolean;
103
+ /** Refetch and re-filter the tag list. Throws on a fetch/HTTP failure. */
104
+ refresh(): Promise<void>;
105
+ /** Begin periodic background refresh. Refresh failures are surfaced to
106
+ * `onError` rather than swallowed; the last good list keeps serving. */
107
+ start(onError?: (err: unknown) => void): void;
108
+ stop(): void;
109
+ }
110
+ //# sourceMappingURL=base-image-catalog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"base-image-catalog.d.ts","sourceRoot":"","sources":["../src/base-image-catalog.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH;;wCAEwC;AACxC,MAAM,WAAW,SAAS;IACxB;qEACiE;IACjE,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,gEAAgE;IAChE,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;qEACiE;IACjE,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,yDAAyD;IACzD,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,8CAA8C;IAC9C,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,MAAM,WAAW,uBAAuB;IACtC,mDAAmD;IACnD,UAAU,EAAE,MAAM,CAAC;IACnB;4EACwE;IACxE,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,4EAA4E;IAC5E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,oDAAoD;IACpD,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,gFAAgF;IAChF,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,qEAAqE;IACrE,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,kEAAkE;IAClE,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;CAC1B;AA+BD;yCACyC;AACzC,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,MAAM,GAAE,SAAc,GAAG,MAAM,EAAE,CAW3E;AAoED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,MAAM,GACV;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAoC1E;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,gBAAgB,CACpC,GAAG,EAAE,MAAM,EACX,IAAI,GAAE;IACJ,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,CAAC;CAC7B,GACL,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAwB7B;AAED,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAY;IACnC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAC/B,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAS;IAC3C,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;IACxC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAS;IAC1C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAe;IACzC,yEAAyE;IACzE,OAAO,CAAC,IAAI,CAAgB;IAC5B,OAAO,CAAC,KAAK,CAAC,CAAiC;gBAEnC,IAAI,EAAE,uBAAuB;IAWzC,4EAA4E;IAC5E,OAAO,IAAI,MAAM,EAAE;IAKnB,8EAA8E;IAC9E,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IAI/B,0EAA0E;IACpE,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAU9B;6EACyE;IACzE,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,GAAG,IAAI;IAQ7C,IAAI,IAAI,IAAI;CAMb"}
@@ -0,0 +1,245 @@
1
+ /**
2
+ * A base-image catalog backed by the Docker Hub repository-tags API. A runner
3
+ * advertises the resolved tag list as an `enum` on its session `image` field so
4
+ * the editor renders a base-image picker, and validates a chosen image against
5
+ * the same list server-side — the runner is the source of truth, so a non-editor
6
+ * client can't widen the allowed set.
7
+ *
8
+ * The list is fetched at startup and on a refresh interval, cached in memory,
9
+ * and degrades gracefully: when Docker Hub is unreachable the catalog still
10
+ * serves the configured default (always present), so the runner boots and a
11
+ * session can still start on the default image.
12
+ *
13
+ * Free of runner specifics — it takes only a repository, a default ref, and a
14
+ * tag filter — but the tag source is specifically Docker Hub's registry API
15
+ * (`hub.docker.com/v2`); a base on another registry (GHCR, a private registry)
16
+ * can't be enumerated or digest-resolved here. The Docker registry the kubelet
17
+ * pulls from is a separate concern; this resolves the *menu* of base images, not
18
+ * pull auth.
19
+ */
20
+ const SHA_TAG = /^(?:sha-)?[0-9a-f]{7,}$/i;
21
+ const PINNED_TAG = /^\d+\.\d+\.\d+(?:[-.].*)?$/;
22
+ const PRERELEASE_CORE = /^\d+\.\d+\.\d+-(.+)$/;
23
+ /** First `-` identifier on a pinned tag that marks it a prerelease rather than a
24
+ * build variant (`-slim`, `-rust-*`). */
25
+ const PRERELEASE_TOKENS = new Set([
26
+ "rc",
27
+ "alpha",
28
+ "beta",
29
+ "pre",
30
+ "preview",
31
+ "next",
32
+ "canary",
33
+ "dev",
34
+ "nightly",
35
+ "snapshot",
36
+ ]);
37
+ function isShaTag(tag) {
38
+ return SHA_TAG.test(tag);
39
+ }
40
+ function isPrerelease(tag) {
41
+ const m = PRERELEASE_CORE.exec(tag);
42
+ if (!m)
43
+ return false;
44
+ const firstId = m[1].split(/[-.]/, 1)[0].toLowerCase();
45
+ return PRERELEASE_TOKENS.has(firstId);
46
+ }
47
+ /** Apply a {@link TagFilter} to a tag list, preserving input order. Pure — the
48
+ * unit-testable core of the catalog. */
49
+ export function filterTags(tags, filter = {}) {
50
+ return tags.filter((tag) => {
51
+ if (filter.excludeSha && isShaTag(tag))
52
+ return false;
53
+ if (filter.pinnedOnly && !PINNED_TAG.test(tag))
54
+ return false;
55
+ if (filter.excludePrerelease && isPrerelease(tag))
56
+ return false;
57
+ if (filter.include && filter.include.length > 0 && !filter.include.some((re) => re.test(tag))) {
58
+ return false;
59
+ }
60
+ if (filter.exclude && filter.exclude.some((re) => re.test(tag)))
61
+ return false;
62
+ return true;
63
+ });
64
+ }
65
+ function splitRepository(repository) {
66
+ const slash = repository.indexOf("/");
67
+ // A bare name (no slash) is a Docker official image under `library/`.
68
+ if (slash < 0)
69
+ return ["library", repository];
70
+ return [repository.slice(0, slash), repository.slice(slash + 1)];
71
+ }
72
+ function unique(refs) {
73
+ const seen = new Set();
74
+ const out = [];
75
+ for (const ref of refs) {
76
+ if (!seen.has(ref)) {
77
+ seen.add(ref);
78
+ out.push(ref);
79
+ }
80
+ }
81
+ return out;
82
+ }
83
+ /**
84
+ * Fetch a repository's tags from Docker Hub, newest first, bounded by
85
+ * `maxTagsScanned`. Paginates the `/v2/repositories/{ns}/{repo}/tags` endpoint
86
+ * and sorts by `last_updated` descending so the cap keeps the most recent tags
87
+ * regardless of the API's page ordering. Throws on a network/HTTP error — the
88
+ * caller decides whether a failure degrades to default-only.
89
+ */
90
+ async function fetchRepoTags(repository, maxTagsScanned, requestTimeoutMs, fetchImpl) {
91
+ const [namespace, repo] = splitRepository(repository);
92
+ const collected = [];
93
+ let url = `https://hub.docker.com/v2/repositories/${encodeURIComponent(namespace)}/` +
94
+ `${encodeURIComponent(repo)}/tags?page_size=100`;
95
+ while (url && collected.length < maxTagsScanned) {
96
+ const res = await fetchImpl(url, {
97
+ headers: { Accept: "application/json" },
98
+ signal: AbortSignal.timeout(requestTimeoutMs),
99
+ });
100
+ if (!res.ok) {
101
+ throw new Error(`Docker Hub tags request for '${repository}' failed: ${res.status} ${res.statusText}`);
102
+ }
103
+ const body = (await res.json());
104
+ for (const r of body.results ?? []) {
105
+ if (typeof r.name === "string") {
106
+ collected.push({ name: r.name, lastUpdated: r.last_updated ?? "" });
107
+ }
108
+ }
109
+ url = body.next ?? null;
110
+ }
111
+ collected.sort((a, b) => a.lastUpdated < b.lastUpdated ? 1 : a.lastUpdated > b.lastUpdated ? -1 : 0);
112
+ return collected.map((c) => c.name);
113
+ }
114
+ /**
115
+ * Parse a Docker image reference into the Docker Hub coordinates needed to look
116
+ * up its digest, or `null` when the ref points at a non-Docker-Hub registry.
117
+ * Handles the `library/` shorthand, optional `docker.io` host, and an already
118
+ * `@sha256:`-pinned digest (carried through verbatim).
119
+ */
120
+ export function parseDockerHubRef(ref) {
121
+ let rest = ref;
122
+ let digest;
123
+ const at = ref.indexOf("@");
124
+ if (at >= 0) {
125
+ digest = ref.slice(at + 1);
126
+ rest = ref.slice(0, at);
127
+ }
128
+ // A first path segment with a `.`/`:` (or `localhost`) is a registry host.
129
+ const firstSlash = rest.indexOf("/");
130
+ const firstPart = firstSlash >= 0 ? rest.slice(0, firstSlash) : "";
131
+ if (firstPart.includes(".") || firstPart.includes(":") || firstPart === "localhost") {
132
+ const hubHosts = new Set(["docker.io", "index.docker.io", "registry-1.docker.io"]);
133
+ if (!hubHosts.has(firstPart))
134
+ return null;
135
+ rest = rest.slice(firstSlash + 1);
136
+ }
137
+ // `rest` is now `name[:tag]`; the tag is the segment after a colon that isn't
138
+ // inside the path (i.e. after the last `/`).
139
+ const lastColon = rest.lastIndexOf(":");
140
+ const lastSlash = rest.lastIndexOf("/");
141
+ let name = rest;
142
+ let tag = "latest";
143
+ if (lastColon > lastSlash) {
144
+ name = rest.slice(0, lastColon);
145
+ tag = rest.slice(lastColon + 1);
146
+ }
147
+ const slash = name.indexOf("/");
148
+ return {
149
+ namespace: slash >= 0 ? name.slice(0, slash) : "library",
150
+ repo: slash >= 0 ? name.slice(slash + 1) : name,
151
+ tag,
152
+ digest,
153
+ };
154
+ }
155
+ /**
156
+ * Resolve a Docker Hub image ref's CURRENT manifest digest. A moving tag
157
+ * (`latest-slim`) maps to a new digest once a new version is published, so a
158
+ * build that folds this into its cache key detects that the base moved and
159
+ * rebuilds. An already `@sha256:`-pinned ref returns its digest without a
160
+ * network call. Returns `undefined` for a non-Docker-Hub ref (not an error —
161
+ * resolution simply doesn't apply) or when a Hub request fails. A genuine
162
+ * request failure is reported to `onError` rather than swallowed, so an operator
163
+ * whose Hub calls persistently fail (auth / network / rate-limit) gets a signal
164
+ * instead of an unexplained "always reuses the cached image".
165
+ */
166
+ export async function resolveTagDigest(ref, opts = {}) {
167
+ const parsed = parseDockerHubRef(ref);
168
+ if (!parsed)
169
+ return undefined;
170
+ if (parsed.digest)
171
+ return parsed.digest;
172
+ const fetchImpl = opts.fetchImpl ?? fetch;
173
+ const url = `https://hub.docker.com/v2/repositories/${encodeURIComponent(parsed.namespace)}/` +
174
+ `${encodeURIComponent(parsed.repo)}/tags/${encodeURIComponent(parsed.tag)}`;
175
+ try {
176
+ const res = await fetchImpl(url, {
177
+ headers: { Accept: "application/json" },
178
+ signal: AbortSignal.timeout(opts.requestTimeoutMs ?? 10_000),
179
+ });
180
+ if (!res.ok) {
181
+ opts.onError?.(new Error(`Docker Hub tag request for '${ref}' failed: ${res.status} ${res.statusText}`));
182
+ return undefined;
183
+ }
184
+ const body = (await res.json());
185
+ return body.digest ?? body.images?.find((i) => i.digest)?.digest ?? undefined;
186
+ }
187
+ catch (err) {
188
+ opts.onError?.(err);
189
+ return undefined;
190
+ }
191
+ }
192
+ export class BaseImageCatalog {
193
+ repository;
194
+ defaultRef;
195
+ filter;
196
+ limit;
197
+ refreshIntervalMs;
198
+ maxTagsScanned;
199
+ requestTimeoutMs;
200
+ fetchImpl;
201
+ /** Filtered tags (bare, newest first); empty until the first refresh. */
202
+ tags = [];
203
+ timer;
204
+ constructor(opts) {
205
+ this.repository = opts.repository;
206
+ this.defaultRef = opts.defaultRef;
207
+ this.filter = opts.filter ?? {};
208
+ this.limit = opts.limit ?? 20;
209
+ this.refreshIntervalMs = opts.refreshIntervalMs ?? 60 * 60 * 1000;
210
+ this.maxTagsScanned = opts.maxTagsScanned ?? 200;
211
+ this.requestTimeoutMs = opts.requestTimeoutMs ?? 10_000;
212
+ this.fetchImpl = opts.fetchImpl ?? fetch;
213
+ }
214
+ /** The advertised image refs: the default first, then the filtered tags. */
215
+ current() {
216
+ const refs = this.tags.map((tag) => `${this.repository}:${tag}`);
217
+ return unique([this.defaultRef, ...refs]);
218
+ }
219
+ /** Whether `ref` is offered by this catalog (server-side allowlist check). */
220
+ isAllowed(ref) {
221
+ return this.current().includes(ref);
222
+ }
223
+ /** Refetch and re-filter the tag list. Throws on a fetch/HTTP failure. */
224
+ async refresh() {
225
+ const tags = await fetchRepoTags(this.repository, this.maxTagsScanned, this.requestTimeoutMs, this.fetchImpl);
226
+ this.tags = filterTags(tags, this.filter).slice(0, this.limit);
227
+ }
228
+ /** Begin periodic background refresh. Refresh failures are surfaced to
229
+ * `onError` rather than swallowed; the last good list keeps serving. */
230
+ start(onError) {
231
+ if (this.timer)
232
+ return;
233
+ this.timer = setInterval(() => {
234
+ this.refresh().catch((err) => onError?.(err));
235
+ }, this.refreshIntervalMs);
236
+ this.timer.unref?.();
237
+ }
238
+ stop() {
239
+ if (this.timer) {
240
+ clearInterval(this.timer);
241
+ this.timer = undefined;
242
+ }
243
+ }
244
+ }
245
+ //# sourceMappingURL=base-image-catalog.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"base-image-catalog.js","sourceRoot":"","sources":["../src/base-image-catalog.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,33 @@
1
+ import type { JsonSchema, PullPolicy } from "./contract.js";
2
+ export interface SessionConfigSchemaOptions {
3
+ /** Default container image; becomes the field's advertised `default`. */
4
+ imageDefault: string;
5
+ /** When set (and non-empty), constrains `image` to this allowlist — advertised
6
+ * as a JSON Schema `enum` so the editor renders a base-image picker. The
7
+ * picker is editable within the list, so an `imageEnum` overrides `enforced`
8
+ * for the `image` field (a one-entry enum is effectively locked anyway). */
9
+ imageEnum?: string[];
10
+ /** Default pull policy (defaults to `missing`). */
11
+ pullPolicyDefault?: PullPolicy;
12
+ /** Override the `pullPolicy` field description. `pullPolicy` semantics differ
13
+ * per backend (docker: container pull policy; k8s: base-image build
14
+ * freshness), so each runner can supply wording that matches its behaviour. */
15
+ pullPolicyDescription?: string;
16
+ /** When true, `image` is server-enforced — advertised `readOnly` (locked to
17
+ * `imageDefault`) so the editor renders it disabled but still sends it. No
18
+ * effect once `imageEnum` is set (the picker is the constraint). `pullPolicy`
19
+ * is always client-editable. */
20
+ enforced?: boolean;
21
+ /** Include the optional `registryUrl` field (a runner that forwards a module
22
+ * registry URL to its workloads). */
23
+ registryUrl?: boolean;
24
+ }
25
+ /**
26
+ * Builds the JSON Schema a runner advertises on `/v1/capabilities` for the
27
+ * editable `SessionConfig` surface. `baseUrl` is deliberately absent — the
28
+ * client owns it. Server-enforced runners pass `enforced: true` to lock `image`
29
+ * as `readOnly` (the value still travels on the wire); `pullPolicy` is always
30
+ * client-editable.
31
+ */
32
+ export declare function sessionConfigSchema(opts: SessionConfigSchemaOptions): JsonSchema;
33
+ //# sourceMappingURL=capabilities-schema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capabilities-schema.d.ts","sourceRoot":"","sources":["../src/capabilities-schema.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAE5D,MAAM,WAAW,0BAA0B;IACzC,yEAAyE;IACzE,YAAY,EAAE,MAAM,CAAC;IACrB;;;iFAG6E;IAC7E,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,mDAAmD;IACnD,iBAAiB,CAAC,EAAE,UAAU,CAAC;IAC/B;;oFAEgF;IAChF,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B;;;qCAGiC;IACjC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;0CACsC;IACtC,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,0BAA0B,GAAG,UAAU,CAqChF"}
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Builds the JSON Schema a runner advertises on `/v1/capabilities` for the
3
+ * editable `SessionConfig` surface. `baseUrl` is deliberately absent — the
4
+ * client owns it. Server-enforced runners pass `enforced: true` to lock `image`
5
+ * as `readOnly` (the value still travels on the wire); `pullPolicy` is always
6
+ * client-editable.
7
+ */
8
+ export function sessionConfigSchema(opts) {
9
+ const readOnly = opts.enforced === true;
10
+ const hasEnum = Array.isArray(opts.imageEnum) && opts.imageEnum.length > 0;
11
+ const properties = {
12
+ image: {
13
+ type: "string",
14
+ minLength: 1,
15
+ default: opts.imageDefault,
16
+ title: "Image",
17
+ description: "Container image the runner spawns for each run.",
18
+ // An allowlist renders as an editable picker; otherwise fall back to the
19
+ // enforced (readOnly) single value.
20
+ ...(hasEnum ? { enum: opts.imageEnum } : readOnly ? { readOnly: true } : {}),
21
+ },
22
+ pullPolicy: {
23
+ type: "string",
24
+ enum: ["missing", "always", "never"],
25
+ default: opts.pullPolicyDefault ?? "missing",
26
+ title: "Pull policy",
27
+ description: opts.pullPolicyDescription ??
28
+ "`missing` pulls on first use; `always` forces a pull every run; `never` fails if the image isn't present.",
29
+ },
30
+ };
31
+ if (opts.registryUrl) {
32
+ properties.registryUrl = {
33
+ type: "string",
34
+ title: "Registry URL",
35
+ description: "Optional base URL for the telo module registry, forwarded to the runner as TELO_REGISTRY_URL. Leave blank for the default registry.",
36
+ };
37
+ }
38
+ return {
39
+ type: "object",
40
+ required: ["image", "pullPolicy"],
41
+ properties,
42
+ };
43
+ }
44
+ //# sourceMappingURL=capabilities-schema.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capabilities-schema.js","sourceRoot":"","sources":["../src/capabilities-schema.ts"],"names":[],"mappings":"AA0BA;;;;;;GAMG;AACH,MAAM,UAAU,mBAAmB,CAAC,IAAgC;IAClE,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,KAAK,IAAI,CAAC;IACxC,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,IAAI,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,CAAC;IAC3E,MAAM,UAAU,GAA+B;QAC7C,KAAK,EAAE;YACL,IAAI,EAAE,QAAQ;YACd,SAAS,EAAE,CAAC;YACZ,OAAO,EAAE,IAAI,CAAC,YAAY;YAC1B,KAAK,EAAE,OAAO;YACd,WAAW,EAAE,iDAAiD;YAC9D,yEAAyE;YACzE,oCAAoC;YACpC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SAC7E;QACD,UAAU,EAAE;YACV,IAAI,EAAE,QAAQ;YACd,IAAI,EAAE,CAAC,SAAS,EAAE,QAAQ,EAAE,OAAO,CAAC;YACpC,OAAO,EAAE,IAAI,CAAC,iBAAiB,IAAI,SAAS;YAC5C,KAAK,EAAE,aAAa;YACpB,WAAW,EACT,IAAI,CAAC,qBAAqB;gBAC1B,2GAA2G;SAC9G;KACF,CAAC;IACF,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;QACrB,UAAU,CAAC,WAAW,GAAG;YACvB,IAAI,EAAE,QAAQ;YACd,KAAK,EAAE,cAAc;YACrB,WAAW,EACT,qIAAqI;SACxI,CAAC;IACJ,CAAC;IACD,OAAO;QACL,IAAI,EAAE,QAAQ;QACd,QAAQ,EAAE,CAAC,OAAO,EAAE,YAAY,CAAC;QACjC,UAAU;KACX,CAAC;AACJ,CAAC"}
@@ -0,0 +1,37 @@
1
+ import type { RunnerTerms } from "./contract.js";
2
+ /**
3
+ * Backend-neutral runner configuration. Concrete runners (docker, k8s) extend
4
+ * `RunnerCoreConfig` with their own fields and reuse these parse helpers so
5
+ * config validation and error reporting stay identical across backends.
6
+ */
7
+ export interface RunnerCoreConfig {
8
+ port: number;
9
+ logLevel: string;
10
+ maxSessions: number;
11
+ exitTtlMs: number;
12
+ replayBufferBytes: number;
13
+ corsOrigins: string[] | "*";
14
+ }
15
+ export declare class RunnerConfigError extends Error {
16
+ }
17
+ export declare function parsePositiveInt(raw: string | undefined, fallback: number, field: string): number;
18
+ export declare function parseBool(raw: string | undefined, fallback: boolean, field: string): boolean;
19
+ export declare function parsePort(raw: string | undefined, fallback: number): number;
20
+ /**
21
+ * Build the runner's usage terms from the environment. Terms are opt-in: returns
22
+ * `undefined` (no gate) unless a non-empty body is provided.
23
+ *
24
+ * The body comes from `RUNNER_TERMS_FILE` (a path read at startup — the natural
25
+ * fit for a k8s ConfigMap mount or a bind-mounted file) or, for short notes,
26
+ * inline `RUNNER_TERMS_BODY`. `RUNNER_TERMS_VERSION` is optional and defaults to
27
+ * a short hash of the body, so editing the agreement automatically re-prompts
28
+ * every client; set it explicitly only to control "material change vs. typo".
29
+ * A configured-but-unreadable `RUNNER_TERMS_FILE` is a hard error rather than a
30
+ * silent "no terms", so a misconfiguration can't quietly drop the gate.
31
+ */
32
+ export declare function loadTermsFromEnv(env: NodeJS.ProcessEnv): RunnerTerms | undefined;
33
+ export declare function parseCorsOrigins(raw: string | undefined): string[] | "*";
34
+ export declare function loadCoreConfig(env: NodeJS.ProcessEnv, defaults?: {
35
+ port?: number;
36
+ }): RunnerCoreConfig;
37
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAEjD;;;;GAIG;AACH,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,WAAW,EAAE,MAAM,EAAE,GAAG,GAAG,CAAC;CAC7B;AAED,qBAAa,iBAAkB,SAAQ,KAAK;CAAG;AAE/C,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,MAAM,GAAG,SAAS,EACvB,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,GACZ,MAAM,CAOR;AAED,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,EAAE,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAM5F;AAED,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAO3E;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,CAAC,UAAU,GAAG,WAAW,GAAG,SAAS,CAQhF;AAsBD,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,EAAE,GAAG,GAAG,CAMxE;AAED,wBAAgB,cAAc,CAC5B,GAAG,EAAE,MAAM,CAAC,UAAU,EACtB,QAAQ,GAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAO,GAC/B,gBAAgB,CAiBlB"}