@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, Operator (SRE) — defined in
106
- `@norskvideo/ctl-dev-kit/conventions/personas.md`, with this product's own
107
- audiences mapped onto them in its `docs/personas.md`. Before writing or
108
- restructuring docs, a configure/dashboard surface, onboarding material, or any
109
- copy a persona will read, name that reader and honour the ownership handoff
110
- (Evaluator/Builder -> product repo, Operator -> ctl, Integrator -> both).
111
- Runtime actors (the roles people play in the deployed product) are NOT
112
- doc-reader personas: the operator who runs it is the Builder; guests who are
113
- invited in receive onboarding kit, they don't read the manual.
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 -->
@@ -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 Operator are served elsewhere (reference / ctl). Runtime actors
43
- (a commentator, a contributor, a producer) are NOT readers of the manual — the
44
- operator is the Builder; the others receive join links and onboarding kit.
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
 
@@ -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 *this* product's named audiences and runtime
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 *with* the product. A developer or a
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 *domain* (broadcast transports, say) while still not wanting
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 Operator is largely ctl's; the Builder's task guides are the product's own
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. Operator (SRE)
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 | 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
- | Operator | **ctl** — host footprint, ports, TLS, upgrade/DR are the platform's story, not any one product's |
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 *not* a fifth, sixth, seventh persona, and indexing docs by
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 Operator in the ctl sense) — the same human, wearing hats. Their
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 *for* them but *handed out by* the
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.1.10",
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.1.10",
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. `prefix` equals the instance's advertised `studioUrlPrefix`
10
- * (`/instance/<instanceId>`); `dashboardKey` is the product's baked dashboard
11
- * path segment (the dashboard is served at `/dashboard/<dashboardKey>/`). */
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: /instance/<id>/live`).
5
+ // (`apiBasePath: /instance/<id>/studio/live/api`, `wsBasePath: …/studio/live`).
6
6
  // The harness, though, publishes Studio DIRECTLY on studioHostPort with no
7
- // `/instance/<id>` prefix — so those advertised paths 404 and the console never
8
- // leaves its "workflow starting up" splash.
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. `prefix` equals the instance's advertised `studioUrlPrefix`
20
- * (`/instance/<instanceId>`); `dashboardKey` is the product's baked dashboard
21
- * path segment (the dashboard is served at `/dashboard/<dashboardKey>/`). */
22
- export function startInstanceProxy(opts) {
23
- const prefix = `/instance/${opts.instanceId}`;
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 strip = (pathname) => pathname === prefix ? "/" : pathname.startsWith(`${prefix}/`) ? pathname.slice(prefix.length) : pathname;
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@norskvideo/ctl-dev-kit",
3
- "version": "0.1.105",
3
+ "version": "0.2.1",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  "./create-product": "./create-product/create-product.ts",