@norskvideo/ctl-dev-kit 0.1.73 → 0.1.75

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.
@@ -172,6 +172,24 @@ jobs:
172
172
  # runner the first pull overruns the per-test timeout. Pre-pull them (tags
173
173
  # from manifest.seed.json), plus whatever the repo lists in the optional
174
174
  # scripts/smoke/prepull-extra.sh hook (a driver image, a sidecar).
175
+ # Before anything is built or launched: does this licence actually name
176
+ # this product? `product add` refuses one it does not, and that refusal
177
+ # reaches the log as a 60-second readiness timeout indistinguishable from
178
+ # a product that failed to start. Turnkeys make this routine — each ships
179
+ # under a licence naming its own product, so a newly created one whose
180
+ # licence was never minted should say so in seconds, by name.
181
+ #
182
+ # The product name is the repo's own PRODUCT_NAME, not the short board key
183
+ # above (`funke` vs `funke-pegasus`): the licence names the product.
184
+ - name: Check the licence entitles this product
185
+ run: |
186
+ set -euo pipefail
187
+ product="$(sed -nE 's/.*PRODUCT_NAME[[:space:]]*=[[:space:]]*"([^"]+)".*/\1/p' shared/src/version.ts | head -1)"
188
+ [ -n "$product" ] || { echo "::error::could not read PRODUCT_NAME from shared/src/version.ts"; exit 1; }
189
+ nix develop .#build --command bun \
190
+ node_modules/@norskvideo/ctl-dev-kit/licence/check-entitlement.ts \
191
+ "$RUNNER_TEMP/norsk-license.json" "$product"
192
+
175
193
  - name: Pre-pull the media + studio images
176
194
  run: |
177
195
  set -euo pipefail
@@ -104,7 +104,14 @@ jobs:
104
104
  run: |
105
105
  nix develop .#build --command bash -c '
106
106
  set -euo pipefail
107
- bun install
107
+ # Per INSTALL ROOT, not per repo. The rewrite above walks every
108
+ # tracked package.json, and a manifest outside the root workspace
109
+ # list (funke proxy/ui) owns its own lockfile that a root-only
110
+ # install never regenerates -- leaving a bumped pin its own
111
+ # --frozen-lockfile then rejects.
112
+ for lock in $(git ls-files "*bun.lock"); do
113
+ (cd "$(dirname "$lock")" && bun install)
114
+ done
108
115
  bun run check:drift
109
116
  bun run lint
110
117
  bun run typecheck
@@ -191,7 +191,16 @@ jobs:
191
191
 
192
192
  - name: Refresh the lockfile against the new pins
193
193
  if: steps.bump.outputs.changed == '1'
194
- run: nix develop .#build --command bash -c 'set -euo pipefail; bun install'
194
+ # Per INSTALL ROOT: the rewrites above walk every tracked package.json,
195
+ # and a manifest outside the root workspace list owns a lockfile a
196
+ # root-only install never regenerates. See lockfile-reinstall.test.ts.
197
+ run: |
198
+ nix develop .#build --command bash -c '
199
+ set -euo pipefail
200
+ for lock in $(git ls-files "*bun.lock"); do
201
+ (cd "$(dirname "$lock")" && bun install)
202
+ done
203
+ '
195
204
 
196
205
  - name: Commit the bump to main
197
206
  if: steps.bump.outputs.changed == '1'
@@ -0,0 +1,78 @@
1
+ // Does this licence entitle this product? Asked before anything is built or
2
+ // launched, because the failure it prevents is otherwise invisible.
3
+ //
4
+ // A V2 licence names the products it covers, and `product add` refuses one it
5
+ // does not name. Discovered late, that surfaces as
6
+ // `product did not become ready within 60000ms` — the same 60 seconds of
7
+ // silence as a product that could not start, or one on an address the daemon
8
+ // could not dial. Reuters' smoke tier sat behind exactly that ambiguity.
9
+ //
10
+ // Turnkeys make this routine rather than rare: each ships to one customer under
11
+ // a licence naming their product, so every new turnkey needs its own, and the
12
+ // moment one is missing should be a sentence rather than a timeout.
13
+ import { readFileSync } from "node:fs";
14
+
15
+ interface LicensedProduct {
16
+ product?: unknown;
17
+ }
18
+
19
+ /** The product names a licence covers, or null when the shape is unreadable —
20
+ * deliberately distinct from "covers nothing", which is a real answer. */
21
+ export function licensedProducts(licenceJson: string): string[] | null {
22
+ let payload: string;
23
+ try {
24
+ const outer = JSON.parse(licenceJson) as { payload?: unknown };
25
+ if (typeof outer.payload !== "string") return null;
26
+ payload = Buffer.from(outer.payload, "base64").toString("utf8");
27
+ } catch {
28
+ return null;
29
+ }
30
+ try {
31
+ const inner = JSON.parse(payload) as { products?: unknown };
32
+ if (!Array.isArray(inner.products)) return null;
33
+ return inner.products.map((p) => (p as LicensedProduct)?.product).filter((p): p is string => typeof p === "string");
34
+ } catch {
35
+ return null;
36
+ }
37
+ }
38
+
39
+ /** The message to fail with, or null to proceed. Null covers two very different
40
+ * cases on purpose: the licence entitles the product, or the licence is in a
41
+ * shape this cannot read. Refusing on the second would assert something never
42
+ * determined, and would break the fleet on the first licence format change. */
43
+ export function entitlementProblem(licenceJson: string, product: string): string | null {
44
+ if (licenceJson.trim() === "") {
45
+ return `licence is empty — the NORSK_LICENSE_V2 secret is unset or unreadable, so ${product} cannot be launched`;
46
+ }
47
+ const products = licensedProducts(licenceJson);
48
+ if (products === null) return null;
49
+ if (products.includes(product)) return null;
50
+ const covers = products.length ? products.join(", ") : "nothing";
51
+ return `licence does not entitle ${product} (covers: ${covers})`;
52
+ }
53
+
54
+ if (import.meta.main) {
55
+ const [file, product] = process.argv.slice(2);
56
+ if (!file || !product) {
57
+ console.error("usage: check-entitlement.ts <licence.json> <product-name>");
58
+ process.exit(2);
59
+ }
60
+ let contents: string;
61
+ try {
62
+ contents = readFileSync(file, "utf8");
63
+ } catch (e) {
64
+ console.error(`licence-check: cannot read ${file}: ${String(e)}`);
65
+ process.exit(2);
66
+ }
67
+ const problem = entitlementProblem(contents, product);
68
+ if (problem) {
69
+ console.error(`licence-check: ${problem}`);
70
+ process.exit(1);
71
+ }
72
+ const products = licensedProducts(contents);
73
+ console.log(
74
+ products === null
75
+ ? `licence-check: licence shape not recognised; entitlement for ${product} not asserted`
76
+ : `licence-check: ${product} is entitled (licence covers: ${products.join(", ")})`,
77
+ );
78
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@norskvideo/ctl-dev-kit",
3
- "version": "0.1.73",
3
+ "version": "0.1.75",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  "./create-product": "./create-product/create-product.ts",
@@ -13,6 +13,7 @@
13
13
  "./doc-guide/prose-bundle": "./doc-guide/prose-bundle.js",
14
14
  "./docs/path-existence": "./docs/path-existence.ts",
15
15
  "./docs/splice-generated": "./docs/splice-generated.ts",
16
+ "./licence/check-entitlement": "./licence/check-entitlement.ts",
16
17
  "./licence/check-product-name": "./licence/check-product-name.ts",
17
18
  "./licence/check-template": "./licence/check-template.ts",
18
19
  "./package.json": "./package.json",