@norskvideo/ctl-test-harness 0.1.57 → 0.1.58
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-stores.d.ts +5 -0
- package/daemon-stores.js +31 -0
- package/daemon.js +4 -0
- package/package.json +5 -1
- package/source-pump.d.ts +10 -9
- package/source-pump.js +16 -2
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Called by startDaemon. Last writer wins: a port reused by a later daemon in
|
|
2
|
+
* the same process belongs to that daemon. */
|
|
3
|
+
export declare function rememberDaemonStore(port: number, storeDir: string): void;
|
|
4
|
+
export declare function forgetDaemonStore(port: number): void;
|
|
5
|
+
export declare function storeDirForDaemonPort(port: number): string | undefined;
|
package/daemon-stores.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Which store dir the daemon on a given port was started with.
|
|
2
|
+
//
|
|
3
|
+
// The daemon's ingress secret lives in its store (`<storeDir>/proxy-secret`),
|
|
4
|
+
// and RFC 0003 D10b guards /api/* for loopback callers too — so anything
|
|
5
|
+
// driving a test daemon's API needs that file. Every caller already has the
|
|
6
|
+
// PORT; almost none of them has the store dir, because a test holds a harness
|
|
7
|
+
// that started the daemon for it. That gap is why `startCameraSource` shipped
|
|
8
|
+
// with a `proxySecret` option that nearly nobody passed, and why five product
|
|
9
|
+
// repos' smoke tiers were 307'd for days.
|
|
10
|
+
//
|
|
11
|
+
// `startDaemon` knows both, so it records the pairing here and the pump looks
|
|
12
|
+
// it up. Deliberately a process-global: the two are one-to-one within a test
|
|
13
|
+
// process (one daemon per port), and the alternative — threading a store dir
|
|
14
|
+
// through every helper signature and every call site — is the arrangement that
|
|
15
|
+
// already failed.
|
|
16
|
+
//
|
|
17
|
+
// A daemon this process did not start (the demo driver's `reuse` policy, a
|
|
18
|
+
// developer's own `norsk-ctl serve`) is absent, and callers that know their
|
|
19
|
+
// store read it themselves.
|
|
20
|
+
const storeByPort = new Map();
|
|
21
|
+
/** Called by startDaemon. Last writer wins: a port reused by a later daemon in
|
|
22
|
+
* the same process belongs to that daemon. */
|
|
23
|
+
export function rememberDaemonStore(port, storeDir) {
|
|
24
|
+
storeByPort.set(port, storeDir);
|
|
25
|
+
}
|
|
26
|
+
export function forgetDaemonStore(port) {
|
|
27
|
+
storeByPort.delete(port);
|
|
28
|
+
}
|
|
29
|
+
export function storeDirForDaemonPort(port) {
|
|
30
|
+
return storeByPort.get(port);
|
|
31
|
+
}
|
package/daemon.js
CHANGED
|
@@ -12,6 +12,7 @@ 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";
|
|
15
|
+
import { rememberDaemonStore } from "./daemon-stores.js";
|
|
15
16
|
import { pollUntil } from "./poll.js";
|
|
16
17
|
import { makeStoreDir, makeTempDir, TEST_TMP_BASE } from "./temp-dir.js";
|
|
17
18
|
export const HEALTH_TIMEOUT_MS = 120_000;
|
|
@@ -146,6 +147,9 @@ export function seedDaemonConfig(storeDir) {
|
|
|
146
147
|
}
|
|
147
148
|
export function startDaemon(storeDir, options) {
|
|
148
149
|
const port = options?.port ?? 8333;
|
|
150
|
+
// Before the spawn, so anything reaching for this daemon's ingress secret can
|
|
151
|
+
// find its store by the port it will answer on (daemon-stores.ts).
|
|
152
|
+
rememberDaemonStore(port, storeDir);
|
|
149
153
|
killProcessOnPort(port);
|
|
150
154
|
if (options?.seedConfig !== false)
|
|
151
155
|
seedDaemonConfig(storeDir);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@norskvideo/ctl-test-harness",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.58",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -15,6 +15,10 @@
|
|
|
15
15
|
"types": "./daemon.d.ts",
|
|
16
16
|
"default": "./daemon.js"
|
|
17
17
|
},
|
|
18
|
+
"./daemon-stores": {
|
|
19
|
+
"types": "./daemon-stores.d.ts",
|
|
20
|
+
"default": "./daemon-stores.js"
|
|
21
|
+
},
|
|
18
22
|
"./image-surface": {
|
|
19
23
|
"types": "./image-surface.d.ts",
|
|
20
24
|
"default": "./image-surface.js"
|
package/source-pump.d.ts
CHANGED
|
@@ -42,16 +42,17 @@ export interface StartCameraSourceOptions {
|
|
|
42
42
|
* (kept even when the fallback is used, so the handle is stable). Required
|
|
43
43
|
* when the same asset must run multiple times within an instance. */
|
|
44
44
|
name?: string;
|
|
45
|
-
/** Sent as `x-norsk-proxy`. **
|
|
46
|
-
*
|
|
47
|
-
*
|
|
45
|
+
/** Sent as `x-norsk-proxy`. **Usually leave this out**: when the daemon on
|
|
46
|
+
* `daemonPort` was started by this process's harness, the secret is resolved
|
|
47
|
+
* from its store automatically. Pass it only for a daemon the harness did
|
|
48
|
+
* not start — the demo driver's `reuse` policy, or a developer's own
|
|
49
|
+
* `norsk-ctl serve`.
|
|
48
50
|
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
* names rather than following. */
|
|
51
|
+
* RFC 0003 D10b: the daemon used to admit any loopback caller without a
|
|
52
|
+
* secret while TLS was off, and that bypass is gone — a loopback caller is a
|
|
53
|
+
* host admin, and a host admin can read the secret file. When neither this
|
|
54
|
+
* option nor the registry supplies one, the call meets the guard's 307,
|
|
55
|
+
* which `guardRedirectError` names rather than following. */
|
|
55
56
|
proxySecret?: string;
|
|
56
57
|
timeoutMs?: number;
|
|
57
58
|
/** Test seams. */
|
package/source-pump.js
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
// file the daemon writes.
|
|
19
19
|
import { existsSync, readFileSync } from "node:fs";
|
|
20
20
|
import { basename, extname, join } from "node:path";
|
|
21
|
+
import { storeDirForDaemonPort } from "./daemon-stores.js";
|
|
21
22
|
import { pollUntil } from "./poll.js";
|
|
22
23
|
/** Where every daemon the harness starts writes its ingress secret: the store
|
|
23
24
|
* root (the guard's stateDir is `<storeDir>` under NORSK_CTL_STORE_DIR). One
|
|
@@ -58,6 +59,12 @@ function assetFields(asset, fileExists) {
|
|
|
58
59
|
}
|
|
59
60
|
return { ...asset };
|
|
60
61
|
}
|
|
62
|
+
/** The secret of whichever daemon this process started on `port`, or undefined
|
|
63
|
+
* for a daemon it did not start. */
|
|
64
|
+
function secretForDaemonPort(port) {
|
|
65
|
+
const storeDir = storeDirForDaemonPort(port);
|
|
66
|
+
return storeDir === undefined ? undefined : readProxySecret(storeDir);
|
|
67
|
+
}
|
|
61
68
|
/** A 307/308 off `/api/*` is the daemon's guard, not a route that moved: the
|
|
62
69
|
* API itself never redirects. */
|
|
63
70
|
function isGuardRedirect(res) {
|
|
@@ -70,7 +77,9 @@ function guardRedirectError(method, res, sentSecret) {
|
|
|
70
77
|
? "The x-norsk-proxy secret that was sent is not the one this daemon holds — it is re-issued on start, so a " +
|
|
71
78
|
"secret read before the daemon booted is stale (read it after `ready`)."
|
|
72
79
|
: "No x-norsk-proxy secret was sent, and RFC 0003 D10b removed the loopback bypass, so a local caller needs " +
|
|
73
|
-
"one too.
|
|
80
|
+
"one too. This process started no daemon on that port, so there was no store to read one from: either start " +
|
|
81
|
+
"it through the harness's startDaemon, or read the secret yourself with `readProxySecret(storeDir)` and pass " +
|
|
82
|
+
"it as `proxySecret`.") +
|
|
74
83
|
" Following this redirect instead is what turns a missing secret into an unrelated-looking ConnectionRefused or " +
|
|
75
84
|
"a `Failed to parse JSON` on nginx's HTML.");
|
|
76
85
|
}
|
|
@@ -81,7 +90,12 @@ export async function startCameraSource(opts) {
|
|
|
81
90
|
const asset = opts.asset ?? { preset: opts.preset ?? "camera1" };
|
|
82
91
|
const name = opts.name ?? defaultSourceName(asset);
|
|
83
92
|
const fetchImpl = opts.fetch ?? ((url, init) => fetch(url, init));
|
|
84
|
-
|
|
93
|
+
// Resolved here, not at the call site: every caller has the port and almost
|
|
94
|
+
// none has the store dir, which is exactly why the explicit option went
|
|
95
|
+
// unpassed. Read now rather than when the daemon was registered — the guard
|
|
96
|
+
// issues the secret as the proxy comes up, after startDaemon returns.
|
|
97
|
+
const secret = opts.proxySecret ?? secretForDaemonPort(opts.daemonPort);
|
|
98
|
+
const headers = secret ? { "x-norsk-proxy": secret } : {};
|
|
85
99
|
const base = `http://localhost:${opts.daemonPort}`;
|
|
86
100
|
const body = {
|
|
87
101
|
instanceId: opts.instanceId,
|