@norskvideo/ctl-dev-kit 0.1.105 → 0.2.1
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.
|
@@ -102,14 +102,16 @@
|
|
|
102
102
|
accidental regressions to the things that matter most.
|
|
103
103
|
- **Know who reads it — name the persona before writing docs or a
|
|
104
104
|
persona-facing surface.** Every Norsk ctl product writes for the same four
|
|
105
|
-
reader personas — Evaluator, Builder, Integrator,
|
|
106
|
-
`@norskvideo/ctl-dev-kit/conventions/personas.md`, with this
|
|
107
|
-
audiences mapped onto them in its `docs/personas.md`. Before
|
|
108
|
-
restructuring docs, a configure/dashboard surface, onboarding
|
|
109
|
-
copy a persona will read, name that reader and honour the
|
|
110
|
-
(Evaluator/Builder -> product repo,
|
|
111
|
-
Runtime actors (the roles people play in the deployed
|
|
112
|
-
doc-reader personas
|
|
113
|
-
|
|
105
|
+
reader personas — Evaluator, Builder, Integrator, Administrator (SRE) —
|
|
106
|
+
defined in `@norskvideo/ctl-dev-kit/conventions/personas.md`, with this
|
|
107
|
+
product's own audiences mapped onto them in its `docs/personas.md`. Before
|
|
108
|
+
writing or restructuring docs, a configure/dashboard surface, onboarding
|
|
109
|
+
material, or any copy a persona will read, name that reader and honour the
|
|
110
|
+
ownership handoff (Evaluator/Builder -> product repo, Administrator -> ctl,
|
|
111
|
+
Integrator -> both). Runtime actors (the roles people play in the deployed
|
|
112
|
+
product) are NOT doc-reader personas, and neither are norsk-ctl's access
|
|
113
|
+
roles, whose names collide with two of these by coincidence: whoever stands
|
|
114
|
+
the deployment up and runs it reads as the Builder; guests who are invited in receive
|
|
115
|
+
onboarding kit, they don't read the manual.
|
|
114
116
|
|
|
115
117
|
<!-- END ctl-shared-conventions v1 -->
|
package/conventions/docs.md
CHANGED
|
@@ -39,9 +39,10 @@ Name the reader before writing a page (see `personas.md` and the product's own
|
|
|
39
39
|
- **Builder** — the illustrated task pages. The domain-fluent operator who stands
|
|
40
40
|
the show up and runs it.
|
|
41
41
|
|
|
42
|
-
Integrator and
|
|
43
|
-
(a commentator, a contributor, a producer) are NOT readers of the
|
|
44
|
-
|
|
42
|
+
Integrator and Administrator are served elsewhere (reference / ctl). Runtime
|
|
43
|
+
actors (a commentator, a contributor, a producer) are NOT readers of the
|
|
44
|
+
manual — whoever runs the show reads as the Builder; the others receive join
|
|
45
|
+
links and onboarding kit.
|
|
45
46
|
|
|
46
47
|
## Voice
|
|
47
48
|
|
package/conventions/personas.md
CHANGED
|
@@ -4,7 +4,7 @@ The shared reader vocabulary. Every Norsk ctl product — probe, commentary,
|
|
|
4
4
|
playout, the turnkeys — writes docs for the same four personas, so a reader who
|
|
5
5
|
learns one product's docs knows how to navigate the next. Read this before
|
|
6
6
|
writing or restructuring a product's `docs/`; then keep the product's own
|
|
7
|
-
`docs/personas.md`, which maps
|
|
7
|
+
`docs/personas.md`, which maps _this_ product's named audiences and runtime
|
|
8
8
|
roles onto the four below.
|
|
9
9
|
|
|
10
10
|
This is a **shipped reference**, not a drift-gated copy: you consume it from
|
|
@@ -36,12 +36,12 @@ founder — or a **sales engineer demoing it on their behalf**. Wants to know
|
|
|
36
36
|
|
|
37
37
|
### 2. Builder
|
|
38
38
|
|
|
39
|
-
Building or standing up a real deployment
|
|
39
|
+
Building or standing up a real deployment _with_ the product. A developer or a
|
|
40
40
|
technical producer/operator. Wants it to work; does not want to become a DevOps
|
|
41
41
|
expert, and does not want to hand-write config/graph YAML.
|
|
42
42
|
|
|
43
43
|
- **Skills**: can run commands, knows what a container is. May be deeply fluent
|
|
44
|
-
in the product's
|
|
44
|
+
in the product's _domain_ (broadcast transports, say) while still not wanting
|
|
45
45
|
ctl/Docker internals — a product refines this in its own mapping.
|
|
46
46
|
- **Wants**: task-oriented hand-holding; a UI that exposes everything; good
|
|
47
47
|
errors with fix hints; sensible defaults.
|
|
@@ -51,7 +51,7 @@ expert, and does not want to hand-write config/graph YAML.
|
|
|
51
51
|
gives up.
|
|
52
52
|
- **Usually the most underserved persona**, and usually the product's sweet
|
|
53
53
|
spot — the Integrator has generated reference, the Evaluator has zero-to-hero,
|
|
54
|
-
the
|
|
54
|
+
the Administrator is largely ctl's; the Builder's task guides are the product's own
|
|
55
55
|
work and the thing most worth investing in.
|
|
56
56
|
|
|
57
57
|
### 3. Integrator
|
|
@@ -67,7 +67,7 @@ launches, CI/CD, programmatic control.
|
|
|
67
67
|
- **Failure mode**: the reference has drifted from the tool; trust is lost; they
|
|
68
68
|
read the source instead.
|
|
69
69
|
|
|
70
|
-
### 4.
|
|
70
|
+
### 4. Administrator (SRE)
|
|
71
71
|
|
|
72
72
|
Running the product in production. Responsible for uptime, cost, security.
|
|
73
73
|
|
|
@@ -91,12 +91,12 @@ no marketing copy. Don't build a separate doc track for it.
|
|
|
91
91
|
The persona a page serves decides which repo owns it, once products are split
|
|
92
92
|
out of ctl:
|
|
93
93
|
|
|
94
|
-
| Persona
|
|
95
|
-
|
|
|
96
|
-
| Evaluator
|
|
97
|
-
| Builder
|
|
98
|
-
| Integrator
|
|
99
|
-
|
|
|
94
|
+
| Persona | Owner repo |
|
|
95
|
+
| ------------- | ------------------------------------------------------------------------------------------------------------------------ |
|
|
96
|
+
| Evaluator | **product** |
|
|
97
|
+
| Builder | **product** |
|
|
98
|
+
| Integrator | **both** — the product owns its config schema + CLI subtree + API fragment; ctl owns the daemon verbs and the shared API |
|
|
99
|
+
| Administrator | **ctl** — host footprint, ports, TLS, upgrade/DR are the platform's story, not any one product's |
|
|
100
100
|
|
|
101
101
|
The test: **if the answer changes when you swap products, it's product docs; if
|
|
102
102
|
it changes when you upgrade ctl, it's ctl docs.** Ownership is not presentation —
|
|
@@ -107,17 +107,23 @@ a single rendered site can still interleave both (see norsk-ctl's
|
|
|
107
107
|
|
|
108
108
|
A deployed product has **runtime actors** — the roles people play in the running
|
|
109
109
|
system (an operator at a console, a guest who joins, an analyst who reads a
|
|
110
|
-
report). These are
|
|
110
|
+
report). These are _not_ a fifth, sixth, seventh persona, and indexing docs by
|
|
111
111
|
them is the most common way a product's docs sprawl.
|
|
112
112
|
|
|
113
113
|
Reconcile them like this:
|
|
114
114
|
|
|
115
115
|
- The actor who **sets the deployment up and runs it** is the **Builder** (and,
|
|
116
|
-
live, the
|
|
117
|
-
material is the Builder task guides.
|
|
116
|
+
live, the Administrator in the ctl sense) — the same human, wearing hats.
|
|
117
|
+
Their material is the Builder task guides. Neither hat is an **access role**:
|
|
118
|
+
norsk-ctl's platform roles (RFC 0003 §2.6, ADR-0013) say what a signed-in
|
|
119
|
+
principal may _do_, and two of their names collide with persona names —
|
|
120
|
+
Builder/`builder` and Administrator/`admin` — by coincidence, not by design.
|
|
121
|
+
A persona is who you write for; a role is what an enforcement point checks.
|
|
122
|
+
The roles are not listed here: this file ships to every product repo with no
|
|
123
|
+
gate tying it to the ladder, so a copy of the list would rot unnoticed.
|
|
118
124
|
- Actors who are **invited into** the running deployment (a guest, a
|
|
119
125
|
contributor) rarely read the manual at all — they receive a link, and maybe an
|
|
120
|
-
onboarding message. Material authored
|
|
126
|
+
onboarding message. Material authored _for_ them but _handed out by_ the
|
|
121
127
|
Builder — join-page guides, onboarding email templates — is **distribution /
|
|
122
128
|
onboarding kit**, indexed separately from the Builder's own task guides, not
|
|
123
129
|
as manual chapters.
|
|
@@ -311,7 +311,7 @@ function rootPackageJson(ctx: ShapeContext): string {
|
|
|
311
311
|
},
|
|
312
312
|
devDependencies: {
|
|
313
313
|
"@biomejs/biome": "2.5.5",
|
|
314
|
-
"@norskvideo/ctl-dev-kit": "^0.
|
|
314
|
+
"@norskvideo/ctl-dev-kit": "^0.2.0",
|
|
315
315
|
// The demo driver (`ctl-demo`, tests/demo.spec.ts) — hoisted here so the
|
|
316
316
|
// root `demo` script resolves the bin.
|
|
317
317
|
"@norskvideo/ctl-test-harness": "^0.1.23",
|
|
@@ -1571,7 +1571,7 @@ function testsPackageJson(ctx: ShapeContext): string {
|
|
|
1571
1571
|
private: true,
|
|
1572
1572
|
type: "module",
|
|
1573
1573
|
devDependencies: {
|
|
1574
|
-
"@norskvideo/ctl-dev-kit": "^0.
|
|
1574
|
+
"@norskvideo/ctl-dev-kit": "^0.2.0",
|
|
1575
1575
|
// Same range the backend declares: the tests tree imports the schema
|
|
1576
1576
|
// directly, and bun links a member's deps into its OWN node_modules —
|
|
1577
1577
|
// the backend's copy is not reachable from here.
|
|
@@ -6,12 +6,12 @@ export interface InstanceProxy {
|
|
|
6
6
|
stop: () => void;
|
|
7
7
|
}
|
|
8
8
|
/** Start a reverse proxy that serves `${prefix}/…` by forwarding `/…` to
|
|
9
|
-
* studioHostPort
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* studioHostPort, where `prefix` is the `studioUrlPrefix` Studio advertises.
|
|
10
|
+
* `dashboardKey` is the product's baked dashboard path segment (the dashboard
|
|
11
|
+
* is served at `/dashboard/<dashboardKey>/`) and so also names the `env`
|
|
12
|
+
* document the prefix is read from. */
|
|
12
13
|
export declare function startInstanceProxy(opts: {
|
|
13
14
|
studioHostPort: number;
|
|
14
|
-
instanceId: string;
|
|
15
15
|
dashboardKey: string;
|
|
16
16
|
/** Explicit upstream studio endpoint. When set, the proxy forwards here
|
|
17
17
|
* instead of `NORSK_TEST_HOST:studioHostPort` — a `noPublish` instance binds
|
|
@@ -27,4 +27,4 @@ export declare function startInstanceProxy(opts: {
|
|
|
27
27
|
host: string;
|
|
28
28
|
port: number;
|
|
29
29
|
};
|
|
30
|
-
}): InstanceProxy
|
|
30
|
+
}): Promise<InstanceProxy>;
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
//
|
|
3
3
|
// A product's baked operator dashboard is built to run behind the runner's oauth2
|
|
4
4
|
// proxy: its `env` endpoint advertises instance-scoped paths
|
|
5
|
-
// (`apiBasePath: /instance/<id>/live/api`, `wsBasePath: /
|
|
5
|
+
// (`apiBasePath: /instance/<id>/studio/live/api`, `wsBasePath: …/studio/live`).
|
|
6
6
|
// The harness, though, publishes Studio DIRECTLY on studioHostPort with no
|
|
7
|
-
//
|
|
8
|
-
//
|
|
7
|
+
// prefix at all — so those advertised paths 404 and the console never leaves
|
|
8
|
+
// its "workflow starting up" splash.
|
|
9
9
|
//
|
|
10
10
|
// Rather than drag the whole oauth2 proxy + TLS + port 443 into a doc run, this
|
|
11
11
|
// serves the dashboard under exactly the prefix `env` advertises and strips it
|
|
@@ -15,12 +15,48 @@
|
|
|
15
15
|
// Response headers that describe the upstream transfer encoding; fetch() has
|
|
16
16
|
// already decoded the body, so forwarding these would misdescribe what we send.
|
|
17
17
|
const STRIP_RESPONSE_HEADERS = ["content-encoding", "content-length", "transfer-encoding"];
|
|
18
|
+
// Studio is long up by the time a guide starts its proxy (the guide has already
|
|
19
|
+
// waited on streams), so this window only covers a slow first byte — it is not
|
|
20
|
+
// a readiness wait.
|
|
21
|
+
const ENV_DISCOVERY_TIMEOUT_MS = 10_000;
|
|
22
|
+
const ENV_DISCOVERY_INTERVAL_MS = 250;
|
|
23
|
+
/** The prefix ctl told Studio to emit its own absolute paths under. Never
|
|
24
|
+
* reconstruct it: ctl anchors it on the studio ROUTE
|
|
25
|
+
* (`/instance/<id>/studio`), deliberately leaving the `/instance/<id>/`
|
|
26
|
+
* catch-all free for a product's sidecar UI, and that choice is ctl's to
|
|
27
|
+
* change. Studio hands it back verbatim on the `env` document it serves beside
|
|
28
|
+
* every dashboard, which is the same document the dashboard itself trusts. */
|
|
29
|
+
async function discoverStudioUrlPrefix(httpUpstream, dashboardKey) {
|
|
30
|
+
const url = `${httpUpstream}/dashboard/${dashboardKey}/env`;
|
|
31
|
+
const deadline = Date.now() + ENV_DISCOVERY_TIMEOUT_MS;
|
|
32
|
+
let lastFailure = "never reached";
|
|
33
|
+
for (;;) {
|
|
34
|
+
const res = await fetch(url).catch((e) => {
|
|
35
|
+
lastFailure = `fetch failed: ${String(e)}`;
|
|
36
|
+
return null;
|
|
37
|
+
});
|
|
38
|
+
if (res?.ok) {
|
|
39
|
+
const body = (await res.json().catch(() => null));
|
|
40
|
+
const prefix = body?.studioUrlPrefix;
|
|
41
|
+
// A prefix-less studio (bare `bun run dev`) advertises "", which is a
|
|
42
|
+
// legitimate answer: serve at the proxy root and strip nothing.
|
|
43
|
+
if (typeof prefix === "string")
|
|
44
|
+
return prefix;
|
|
45
|
+
throw new Error(`${url} is not a studio env document (no studioUrlPrefix)`);
|
|
46
|
+
}
|
|
47
|
+
if (res)
|
|
48
|
+
lastFailure = `HTTP ${res.status}`;
|
|
49
|
+
if (Date.now() >= deadline)
|
|
50
|
+
throw new Error(`could not read the advertised prefix from ${url} (${lastFailure})`);
|
|
51
|
+
await Bun.sleep(ENV_DISCOVERY_INTERVAL_MS);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
18
54
|
/** Start a reverse proxy that serves `${prefix}/…` by forwarding `/…` to
|
|
19
|
-
* studioHostPort
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
|
|
23
|
-
|
|
55
|
+
* studioHostPort, where `prefix` is the `studioUrlPrefix` Studio advertises.
|
|
56
|
+
* `dashboardKey` is the product's baked dashboard path segment (the dashboard
|
|
57
|
+
* is served at `/dashboard/<dashboardKey>/`) and so also names the `env`
|
|
58
|
+
* document the prefix is read from. */
|
|
59
|
+
export async function startInstanceProxy(opts) {
|
|
24
60
|
// Studio's published port is on localhost for a local run, but in the
|
|
25
61
|
// docker-outside-of-docker CI the test process runs in a container and the
|
|
26
62
|
// port lives on the host gateway — reach it via NORSK_TEST_HOST
|
|
@@ -31,7 +67,14 @@ export function startInstanceProxy(opts) {
|
|
|
31
67
|
const upstreamHost = opts.upstream?.host ?? process.env.NORSK_TEST_HOST ?? "localhost";
|
|
32
68
|
const upstreamPort = opts.upstream?.port ?? opts.studioHostPort;
|
|
33
69
|
const httpUpstream = `http://${upstreamHost}:${upstreamPort}`;
|
|
34
|
-
const
|
|
70
|
+
const prefix = await discoverStudioUrlPrefix(httpUpstream, opts.dashboardKey);
|
|
71
|
+
// An empty prefix (a studio with no studio_url_prefix) makes every path its
|
|
72
|
+
// own strip result, which the startsWith arm already gives.
|
|
73
|
+
const strip = (pathname) => {
|
|
74
|
+
if (prefix !== "" && pathname === prefix)
|
|
75
|
+
return "/";
|
|
76
|
+
return pathname.startsWith(`${prefix}/`) ? pathname.slice(prefix.length) : pathname;
|
|
77
|
+
};
|
|
35
78
|
const server = Bun.serve({
|
|
36
79
|
port: 0,
|
|
37
80
|
async fetch(req, srv) {
|