@norskvideo/ctl-product-template-schema 0.1.60 → 0.1.62

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/index.js CHANGED
@@ -601,11 +601,14 @@ const ProductTemplateFileSchema = z.object({
601
601
  // subpath) turns this into tar bytes at the paths below.
602
602
  //
603
603
  // `workflow`, `components` and `dashboards` are known top-level paths in the
604
- // v1 tar format. The runner detects studio in the compose stack and mounts
605
- // all three into the studio container automatically — products don't need
606
- // to declare the mounts.
604
+ // v1 tar format. The runner detects studio in the compose stack and places
605
+ // all three in the studio container automatically — products don't need
606
+ // to declare the mounts. workflow.yml is COPIED into the working directory on
607
+ // every launch (a single-file bind under /data is refused by OrbStack and
608
+ // Docker Desktop); the template stays authoritative, so a Studio save of it
609
+ // lasts only until the next launch.
607
610
  //
608
- // workflow.yml → /data/studio-save-files/workflow.yml (RO)
611
+ // workflow.yml → /data/studio-save-files/workflow.yml (copied)
609
612
  // components/<name>/ → /usr/src/app/plugins/<name>/ (RO)
610
613
  // dashboards/<name>/ → /data/studio-save-files/<name>/dashboards/ (RO)
611
614
  //
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@norskvideo/ctl-product-template-schema",
3
- "version": "0.1.60",
3
+ "version": "0.1.62",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -36,6 +36,10 @@
36
36
  "types": "./migration-socket.d.ts",
37
37
  "default": "./migration-socket.js"
38
38
  },
39
+ "./secret-references": {
40
+ "types": "./secret-references.d.ts",
41
+ "default": "./secret-references.js"
42
+ },
39
43
  "./product-channels": {
40
44
  "types": "./product-channels.d.ts",
41
45
  "default": "./product-channels.js"
@@ -0,0 +1,27 @@
1
+ import { type Result } from "@norskvideo/ctl-foundation";
2
+ export declare const SECRET_REFERENCE_PREFIX = "secretref:";
3
+ /** The NodeInventory capability a worker advertises when it resolves secret
4
+ * references, so placement can keep a job whose parameters hold one off an
5
+ * older worker — which would hand the container the reference itself. */
6
+ export declare const SECRET_REFERENCES_CAPABILITY = "norsk.io/secret-references";
7
+ /** What a secret parameter's literal value is shown as wherever ctl prints
8
+ * launch parameters back. */
9
+ export declare const SECRET_PLACEHOLDER = "<secret>";
10
+ export interface SecretReference {
11
+ provider: string;
12
+ key: string;
13
+ }
14
+ /** A value is a reference exactly when it carries the prefix. A malformed
15
+ * reference is still a reference: it fails resolution rather than reaching a
16
+ * container as a literal. */
17
+ export declare function isSecretReference(value: string): boolean;
18
+ export declare function parseSecretReference(value: string): Result<SecretReference, string>;
19
+ export declare function secretReference(provider: string, key: string): string;
20
+ /** Launch parameters as they may be shown: a parameter the template declares
21
+ * `secret` has its literal value masked. A reference is shown as it is — it
22
+ * is the point of a reference that it reveals nothing — and an empty value
23
+ * stays visibly empty. */
24
+ export declare function redactSecretParams(params: Record<string, string>, declared: readonly {
25
+ name: string;
26
+ secret?: boolean;
27
+ }[]): Record<string, string>;
@@ -0,0 +1,56 @@
1
+ // Secret references: a launch parameter's value may name a secret instead of
2
+ // carrying it, as `secretref:<provider>:<key>`. The worker that runs the
3
+ // instance resolves the reference immediately before it writes the
4
+ // container's environment, so a scheduler, its job store, its templates and
5
+ // its backups only ever hold the reference (norsk-ctl ADR-0017).
6
+ //
7
+ // Shared by both ends: whoever builds launch parameters (a Manager front end,
8
+ // a script) and the worker that resolves them. The provider names a resolver
9
+ // configured on the worker; the key is that provider's business and may hold
10
+ // colons, slashes or ciphertext.
11
+ import { err, ok } from "@norskvideo/ctl-foundation";
12
+ export const SECRET_REFERENCE_PREFIX = "secretref:";
13
+ /** The NodeInventory capability a worker advertises when it resolves secret
14
+ * references, so placement can keep a job whose parameters hold one off an
15
+ * older worker — which would hand the container the reference itself. */
16
+ export const SECRET_REFERENCES_CAPABILITY = "norsk.io/secret-references";
17
+ /** What a secret parameter's literal value is shown as wherever ctl prints
18
+ * launch parameters back. */
19
+ export const SECRET_PLACEHOLDER = "<secret>";
20
+ const PROVIDER_NAME = /^[a-z][a-z0-9-]*$/;
21
+ /** A value is a reference exactly when it carries the prefix. A malformed
22
+ * reference is still a reference: it fails resolution rather than reaching a
23
+ * container as a literal. */
24
+ export function isSecretReference(value) {
25
+ return value.startsWith(SECRET_REFERENCE_PREFIX);
26
+ }
27
+ export function parseSecretReference(value) {
28
+ if (!isSecretReference(value))
29
+ return err("not a secret reference");
30
+ const rest = value.slice(SECRET_REFERENCE_PREFIX.length);
31
+ const colon = rest.indexOf(":");
32
+ if (colon < 0)
33
+ return err(`a secret reference is ${SECRET_REFERENCE_PREFIX}<provider>:<key>`);
34
+ const provider = rest.slice(0, colon);
35
+ const key = rest.slice(colon + 1);
36
+ if (!PROVIDER_NAME.test(provider)) {
37
+ return err(`secret provider name '${provider}' must be lowercase letters, digits and dashes`);
38
+ }
39
+ if (key === "")
40
+ return err(`the secret reference for provider '${provider}' names no key`);
41
+ return ok({ provider, key });
42
+ }
43
+ export function secretReference(provider, key) {
44
+ return `${SECRET_REFERENCE_PREFIX}${provider}:${key}`;
45
+ }
46
+ /** Launch parameters as they may be shown: a parameter the template declares
47
+ * `secret` has its literal value masked. A reference is shown as it is — it
48
+ * is the point of a reference that it reveals nothing — and an empty value
49
+ * stays visibly empty. */
50
+ export function redactSecretParams(params, declared) {
51
+ const secret = new Set(declared.filter((p) => p.secret === true).map((p) => p.name));
52
+ return Object.fromEntries(Object.entries(params).map(([k, v]) => [
53
+ k,
54
+ secret.has(k) && v !== "" && !isSecretReference(v) ? SECRET_PLACEHOLDER : v,
55
+ ]));
56
+ }
package/workdir-path.js CHANGED
@@ -10,14 +10,19 @@
10
10
  // Two separate hazards, deliberately not conflated:
11
11
  // escape — `../../etc/x` writes outside the workdir. A hole.
12
12
  // shadowing — a file written UNDER one of the runner's read-only bind
13
- // mounts is on disk and invisible to the container. Not a
14
- // security problem, but a miserable one to debug, so it is
15
- // refused at upload time rather than discovered at runtime.
16
- /** Runner-managed read-only mount points inside the working directory
17
- * (override-generator.ts). `workflow.yml` is a file mount; the other two are
18
- * directory mounts, so anything beneath them is shadowed too. */
13
+ // mounts is on disk and invisible to the container, and a file
14
+ // at a path the launch writes itself is never the one used.
15
+ // Not a security problem, but a miserable one to debug, so it
16
+ // is refused at upload time rather than discovered at runtime.
17
+ /** Files the launch writes into the working directory itself. The product
18
+ * template's workflow.yml is copied to the path Studio's `server.document`
19
+ * names on every launch (norsk-ctl's workflow-sync.ts; it used to be a
20
+ * read-only single-file bind, which OrbStack and Docker Desktop refuse), so an
21
+ * operator asset there is replaced or skipped and never opened. */
22
+ const RUNNER_WRITTEN_FILES = ["studio-save-files/workflow.yml"];
23
+ /** Runner-managed read-only directory mount points inside the working
24
+ * directory (override-generator.ts); anything beneath them is shadowed. */
19
25
  const READONLY_MOUNTS = {
20
- file: ["studio-save-files/workflow.yml"],
21
26
  dir: [".norsk"],
22
27
  /** `studio-save-files/<anything>/dashboards` — the per-workflow dashboard mount. */
23
28
  pattern: [/^studio-save-files\/[^/]+\/dashboards(\/|$)/],
@@ -46,9 +51,17 @@ export function checkWorkdirPath(raw) {
46
51
  message: `'${path}' lands under the read-only mount '${mount}', where it would be on disk but invisible to the container`,
47
52
  },
48
53
  });
49
- for (const file of READONLY_MOUNTS.file)
50
- if (path === file)
51
- return shadowed(file);
54
+ for (const file of RUNNER_WRITTEN_FILES) {
55
+ if (path === file) {
56
+ return {
57
+ ok: false,
58
+ problem: {
59
+ kind: "shadowed",
60
+ message: `'${path}' is where each launch copies the product template's workflow.yml, so an asset there would never be the one Studio opens`,
61
+ },
62
+ };
63
+ }
64
+ }
52
65
  for (const dir of READONLY_MOUNTS.dir)
53
66
  if (path === dir || path.startsWith(`${dir}/`))
54
67
  return shadowed(dir);