@norskvideo/ctl-test-harness 0.1.51 → 0.1.53

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/daemon.d.ts CHANGED
@@ -42,6 +42,20 @@ export interface StartDaemonOptions {
42
42
  seedConfig?: boolean;
43
43
  readyTimeoutMs?: number;
44
44
  }
45
+ /** Root the instance working directory under the store dir (itself under
46
+ * TEST_TMP_BASE) rather than the daemon default ~/norsk-runtime. The launcher
47
+ * bind-mounts the workdir as /data and then mounts the stored template's
48
+ * workflow.yml (which lives under the store dir) *inside* it — Docker Desktop
49
+ * rejects a nested bind mount whose inner source resolves to a different host
50
+ * share-root than the outer, so both mounts must share the TEST_TMP_BASE root.
51
+ * With NORSK_CTL_STORE_DIR set, config.yaml is read from <storeDir>/config.yaml.
52
+ * networkMode is the one required config.yaml field — omitting it makes the
53
+ * whole file fail to parse (ConfigParseError), silently dropping the workdir
54
+ * override. Seed both.
55
+ *
56
+ * A suite that writes its own config.yaml (to pick a networkMode, a proxy
57
+ * port) still gets the working directory added, unless it states one. */
58
+ export declare function seedDaemonConfig(storeDir: string): void;
45
59
  export declare function startDaemon(storeDir: string, options?: StartDaemonOptions): {
46
60
  daemon: DaemonProcess;
47
61
  ready: Promise<void>;
package/daemon.js CHANGED
@@ -8,7 +8,7 @@
8
8
  // works under bun AND under Playwright's Node workers — so the doc-guide tier
9
9
  // consumes these same bodies instead of keeping a third fork.
10
10
  import { spawn, spawnSync } from "node:child_process";
11
- import { existsSync, rmSync, writeFileSync } from "node:fs";
11
+ import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
12
12
  import { createServer } from "node:net";
13
13
  import { join } from "node:path";
14
14
  import { cliCommand } from "./cli-command.js";
@@ -118,30 +118,47 @@ function daemonHandle(proc) {
118
118
  exited,
119
119
  };
120
120
  }
121
+ /** Root the instance working directory under the store dir (itself under
122
+ * TEST_TMP_BASE) rather than the daemon default ~/norsk-runtime. The launcher
123
+ * bind-mounts the workdir as /data and then mounts the stored template's
124
+ * workflow.yml (which lives under the store dir) *inside* it — Docker Desktop
125
+ * rejects a nested bind mount whose inner source resolves to a different host
126
+ * share-root than the outer, so both mounts must share the TEST_TMP_BASE root.
127
+ * With NORSK_CTL_STORE_DIR set, config.yaml is read from <storeDir>/config.yaml.
128
+ * networkMode is the one required config.yaml field — omitting it makes the
129
+ * whole file fail to parse (ConfigParseError), silently dropping the workdir
130
+ * override. Seed both.
131
+ *
132
+ * A suite that writes its own config.yaml (to pick a networkMode, a proxy
133
+ * port) still gets the working directory added, unless it states one. */
134
+ export function seedDaemonConfig(storeDir) {
135
+ const configPath = join(storeDir, "config.yaml");
136
+ const workdirLine = `defaultWorkingDirectory: ${JSON.stringify(join(storeDir, "norsk-runtime"))}\n`;
137
+ if (!existsSync(configPath)) {
138
+ writeFileSync(configPath, `networkMode: docker\n${workdirLine}`);
139
+ return;
140
+ }
141
+ const existing = readFileSync(configPath, "utf-8");
142
+ if (/^defaultWorkingDirectory\s*:/m.test(existing))
143
+ return;
144
+ const separator = existing === "" || existing.endsWith("\n") ? "" : "\n";
145
+ writeFileSync(configPath, `${existing}${separator}${workdirLine}`);
146
+ }
121
147
  export function startDaemon(storeDir, options) {
122
148
  const port = options?.port ?? 8333;
123
149
  killProcessOnPort(port);
124
- if (options?.seedConfig !== false) {
125
- // Root the instance working directory under the store dir (itself under
126
- // TEST_TMP_BASE) rather than the daemon default ~/norsk-runtime. The launcher
127
- // bind-mounts the workdir as /data and then mounts the stored template's
128
- // workflow.yml (which lives under the store dir) *inside* it — Docker Desktop
129
- // rejects a nested bind mount whose inner source resolves to a different host
130
- // share-root than the outer, so both mounts must share the TEST_TMP_BASE root.
131
- // With NORSK_CTL_STORE_DIR set, config.yaml is read from <storeDir>/config.yaml.
132
- // networkMode is the one required config.yaml field — omitting it makes the
133
- // whole file fail to parse (ConfigParseError), silently dropping the workdir
134
- // override. Seed both.
135
- const configPath = join(storeDir, "config.yaml");
136
- if (!existsSync(configPath)) {
137
- writeFileSync(configPath, `networkMode: docker\ndefaultWorkingDirectory: ${JSON.stringify(join(storeDir, "norsk-runtime"))}\n`);
138
- }
139
- }
150
+ if (options?.seedConfig !== false)
151
+ seedDaemonConfig(storeDir);
140
152
  const [cmd, ...cmdArgs] = cliCommand;
153
+ // A spawned daemon must boot GUARDED: exercising the real ingress secret is
154
+ // the point of the tiers that spawn one. The unit tier's marker would
155
+ // otherwise be inherited straight through this env spread and quietly open
156
+ // it (RFC 0003 D10b).
157
+ const { NORSK_CTL_UNGUARDED_TEST_DAEMON: _unguarded, ...inheritedEnv } = process.env;
141
158
  const proc = spawn(cmd, [...cmdArgs, "serve"], {
142
159
  stdio: "inherit",
143
160
  detached: options?.detached === true,
144
- env: { ...process.env, NORSK_CTL_STORE_DIR: storeDir, NORSK_CTL_PORT: String(port), ...options?.env },
161
+ env: { ...inheritedEnv, NORSK_CTL_STORE_DIR: storeDir, NORSK_CTL_PORT: String(port), ...options?.env },
145
162
  });
146
163
  const spawnFailed = new Promise((_, reject) => {
147
164
  proc.once("error", (e) => reject(new Error(cliUnavailableMessage(cliCommand, e.message))));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@norskvideo/ctl-test-harness",
3
- "version": "0.1.51",
3
+ "version": "0.1.53",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
package/source-pump.d.ts CHANGED
@@ -36,10 +36,15 @@ export interface StartCameraSourceOptions {
36
36
  * (kept even when the fallback is used, so the handle is stable). Required
37
37
  * when the same asset must run multiple times within an instance. */
38
38
  name?: string;
39
- /** Sent as `x-norsk-proxy`; needed when the daemon sits behind the oauth2
40
- * proxy, which 307s `/api/*` without it. The daemon writes it to
41
- * `<stateDir>/proxy-secret` (`<storeDir>/proxy-secret` under
42
- * NORSK_CTL_STORE_DIR). */
39
+ /** Sent as `x-norsk-proxy`. **Effectively required.** The daemon writes it
40
+ * to `<stateDir>/proxy-secret` (`<storeDir>/proxy-secret` under
41
+ * NORSK_CTL_STORE_DIR); read it from there and pass it.
42
+ *
43
+ * BREAKING for callers that omitted it (RFC 0003 D10b, ctl >= this
44
+ * release): the daemon used to admit any loopback caller without a secret
45
+ * while TLS was off, and that bypass is gone — a loopback caller is a host
46
+ * admin, and a host admin can read the secret file. A harness or guide that
47
+ * relied on it now fails with `POST /api/sources failed (307)`. */
43
48
  proxySecret?: string;
44
49
  timeoutMs?: number;
45
50
  /** Test seams. */
package/source-pump.js CHANGED
@@ -10,6 +10,10 @@
10
10
  // `name` overrides the source identity (container name + management key) so
11
11
  // multiple sources on the same asset can co-exist within one instance. See
12
12
  // `startSrtSources` for the multi-port fan-out helper that uses it.
13
+ //
14
+ // PASS `proxySecret`. RFC 0003 D10b removed the daemon's loopback bypass, so
15
+ // a call without it now 307s where it used to be admitted. See the field's
16
+ // docblock for where to read the secret from.
13
17
  import { existsSync } from "node:fs";
14
18
  import { basename, extname } from "node:path";
15
19
  import { pollUntil } from "./poll.js";