@norskvideo/ctl-product-template-schema 0.1.34 → 0.1.36

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.d.ts CHANGED
@@ -317,15 +317,15 @@ export declare const ProductTemplateManifestSchema: z.ZodObject<{
317
317
  }, z.core.$strip>>>;
318
318
  logs: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
319
319
  stdout: z.ZodOptional<z.ZodEnum<{
320
- json: "json";
321
- text: "text";
322
320
  none: "none";
321
+ text: "text";
322
+ json: "json";
323
323
  }>>;
324
324
  files: z.ZodOptional<z.ZodArray<z.ZodObject<{
325
325
  path: z.ZodString;
326
326
  format: z.ZodEnum<{
327
- json: "json";
328
327
  text: "text";
328
+ json: "json";
329
329
  }>;
330
330
  }, z.core.$strict>>>;
331
331
  fluentBit: z.ZodOptional<z.ZodUnion<readonly [z.ZodObject<{
@@ -348,6 +348,10 @@ export declare const ProductTemplateManifestSchema: z.ZodObject<{
348
348
  }>;
349
349
  on: z.ZodLiteral<"product">;
350
350
  }, z.core.$strip>>;
351
+ interfaces: z.ZodOptional<z.ZodArray<z.ZodObject<{
352
+ name: z.ZodString;
353
+ version: z.ZodNumber;
354
+ }, z.core.$catchall<z.ZodUnknown>>>>;
351
355
  migration: z.ZodOptional<z.ZodObject<{
352
356
  strategy: z.ZodEnum<{
353
357
  overlap: "overlap";
@@ -381,6 +385,8 @@ export declare const ProductTemplateParameterSchema: z.ZodObject<{
381
385
  min: z.ZodOptional<z.ZodNumber>;
382
386
  max: z.ZodOptional<z.ZodNumber>;
383
387
  values: z.ZodOptional<z.ZodArray<z.ZodString>>;
388
+ label: z.ZodOptional<z.ZodString>;
389
+ group: z.ZodOptional<z.ZodString>;
384
390
  }, z.core.$strip>;
385
391
  export type ProductTemplateParameter = z.infer<typeof ProductTemplateParameterSchema>;
386
392
  export declare const ParametersFileSchema: z.ZodObject<{
@@ -400,6 +406,8 @@ export declare const ParametersFileSchema: z.ZodObject<{
400
406
  min: z.ZodOptional<z.ZodNumber>;
401
407
  max: z.ZodOptional<z.ZodNumber>;
402
408
  values: z.ZodOptional<z.ZodArray<z.ZodString>>;
409
+ label: z.ZodOptional<z.ZodString>;
410
+ group: z.ZodOptional<z.ZodString>;
403
411
  }, z.core.$strip>>>;
404
412
  }, z.core.$strip>;
405
413
  export type ParametersFile = z.infer<typeof ParametersFileSchema>;
@@ -566,15 +574,15 @@ export declare const ProductTemplateMaterialsSchema: z.ZodObject<{
566
574
  }, z.core.$strip>>>;
567
575
  logs: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
568
576
  stdout: z.ZodOptional<z.ZodEnum<{
569
- json: "json";
570
- text: "text";
571
577
  none: "none";
578
+ text: "text";
579
+ json: "json";
572
580
  }>>;
573
581
  files: z.ZodOptional<z.ZodArray<z.ZodObject<{
574
582
  path: z.ZodString;
575
583
  format: z.ZodEnum<{
576
- json: "json";
577
584
  text: "text";
585
+ json: "json";
578
586
  }>;
579
587
  }, z.core.$strict>>>;
580
588
  fluentBit: z.ZodOptional<z.ZodUnion<readonly [z.ZodObject<{
@@ -597,6 +605,10 @@ export declare const ProductTemplateMaterialsSchema: z.ZodObject<{
597
605
  }>;
598
606
  on: z.ZodLiteral<"product">;
599
607
  }, z.core.$strip>>;
608
+ interfaces: z.ZodOptional<z.ZodArray<z.ZodObject<{
609
+ name: z.ZodString;
610
+ version: z.ZodNumber;
611
+ }, z.core.$catchall<z.ZodUnknown>>>>;
600
612
  migration: z.ZodOptional<z.ZodObject<{
601
613
  strategy: z.ZodEnum<{
602
614
  overlap: "overlap";
@@ -623,6 +635,8 @@ export declare const ProductTemplateMaterialsSchema: z.ZodObject<{
623
635
  min: z.ZodOptional<z.ZodNumber>;
624
636
  max: z.ZodOptional<z.ZodNumber>;
625
637
  values: z.ZodOptional<z.ZodArray<z.ZodString>>;
638
+ label: z.ZodOptional<z.ZodString>;
639
+ group: z.ZodOptional<z.ZodString>;
626
640
  }, z.core.$strip>>>;
627
641
  }, z.core.$strip>>;
628
642
  workflow: z.ZodOptional<z.ZodString>;
@@ -644,4 +658,5 @@ export declare const ProductTemplateMaterialsSchema: z.ZodObject<{
644
658
  }, z.core.$strip>>>;
645
659
  }, z.core.$strip>;
646
660
  export type ProductTemplateMaterials = z.infer<typeof ProductTemplateMaterialsSchema>;
661
+ export { declareMediaConversion, interfaceDeclarationProblems, MEDIA_CONVERSION_INTERFACE, type MediaConversionV1, MediaConversionV1Schema, mediaConversionOf, type ProductTemplateInterface, ProductTemplateInterfaceSchema, } from "./interfaces.js";
647
662
  export { composeServiceNamesOf, logsDeclarationProblems, NORSK_MEDIA_SERVICE_LOGS, type ProductTemplateLogs, ProductTemplateLogsSchema, productTemplateLogsProblems, type ServiceLogs, ServiceLogsSchema, STANDARD_LOGS, STUDIO_SERVICE_LOGS, servicesWithoutLogs, } from "./logs.js";
package/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import { ProductTemplateInterfaceSchema } from "./interfaces.js";
2
3
  import { ProductTemplateLogsSchema } from "./logs.js";
3
4
  // ProductTemplate format v1. A product template is a tar file at the wire; the schemas
4
5
  // below describe its parts before packing and how the runner validates
@@ -384,6 +385,9 @@ export const ProductTemplateManifestSchema = z
384
385
  workload: ProductTemplateWorkloadSchema.optional().meta({
385
386
  description: "The role each instance's own workload (`urn:norsk:workload:instance:<id>`) holds on its product (`product/<productName>`), granted when it launches, whoever launches it — for an instance that creates or manages its product's other instances (a capture launching recorders). At most `builder`: registering a template takes builder, and that registration is the consent. Absent = the workload holds nothing it was not passed at launch. An older ctl ignores it, and its instances hold nothing.",
386
387
  }),
388
+ interfaces: z.array(ProductTemplateInterfaceSchema).optional().meta({
389
+ description: "Named, versioned contracts this template implements (e.g. norsk.io/media-conversion), so a client can find the templates that implement one whatever product built them. Each entry is carried verbatim; the runner checks only contracts it knows, so a template declaring one an older ctl does not know still launches. Absent = implements none.",
390
+ }),
387
391
  migration: ProductTemplateMigrationSchema.optional().meta({
388
392
  description: "How a running instance may be moved between nodes. `strategy` is the strongest promise the product keeps: `overlap` (target started, then source stopped — both may produce briefly), `stopThenStart` (the default when absent — target prepared but not started, source stopped, target started), or `handover` (the job speaks the worker's migration socket and hands over at an exact position). A migrate request may down-grade to a safer strategy but never up-grade past the declaration. Only the manager reads this; the worker never does.",
389
393
  }),
@@ -421,6 +425,14 @@ export const ProductTemplateParameterSchema = z
421
425
  max: z.number().optional(),
422
426
  /** Allowed values for `enum` parameters (rendered as a dropdown). */
423
427
  values: z.array(z.string()).optional(),
428
+ // Display metadata for a form built from the template. Declaration order is
429
+ // display order, so there is no `order` field to disagree with it.
430
+ label: z.string().min(1).optional().meta({
431
+ description: "Short name to show for the parameter in a form, e.g. `Width`. Absent = the form shows the parameter name. `purpose` stays the longer help text.",
432
+ }),
433
+ group: z.string().min(1).optional().meta({
434
+ description: "Section of a form the parameter belongs in, e.g. `Output`. Groups appear in the order their first parameter is declared; ungrouped parameters come first. Absent = ungrouped.",
435
+ }),
424
436
  })
425
437
  .superRefine((p, ctx) => {
426
438
  if (p.type === "enum" && (!p.values || p.values.length === 0)) {
@@ -491,4 +503,5 @@ export const ProductTemplateMaterialsSchema = z
491
503
  }),
492
504
  })
493
505
  .meta({ id: "ProductTemplateMaterials" });
506
+ export { declareMediaConversion, interfaceDeclarationProblems, MEDIA_CONVERSION_INTERFACE, MediaConversionV1Schema, mediaConversionOf, ProductTemplateInterfaceSchema, } from "./interfaces.js";
494
507
  export { composeServiceNamesOf, logsDeclarationProblems, NORSK_MEDIA_SERVICE_LOGS, ProductTemplateLogsSchema, productTemplateLogsProblems, ServiceLogsSchema, STANDARD_LOGS, STUDIO_SERVICE_LOGS, servicesWithoutLogs, } from "./logs.js";
@@ -0,0 +1,67 @@
1
+ import { z } from "zod";
2
+ import type { ProductTemplateManifest, ProductTemplateParameter } from "./index.js";
3
+ export declare const ProductTemplateInterfaceSchema: z.ZodObject<{
4
+ name: z.ZodString;
5
+ version: z.ZodNumber;
6
+ }, z.core.$catchall<z.ZodUnknown>>;
7
+ export type ProductTemplateInterface = z.infer<typeof ProductTemplateInterfaceSchema>;
8
+ export declare const MEDIA_CONVERSION_INTERFACE = "norsk.io/media-conversion";
9
+ export declare const MediaConversionV1Schema: z.ZodObject<{
10
+ name: z.ZodLiteral<"norsk.io/media-conversion">;
11
+ version: z.ZodLiteral<1>;
12
+ inputs: z.ZodArray<z.ZodObject<{
13
+ name: z.ZodString;
14
+ kinds: z.ZodRecord<z.ZodEnum<{
15
+ video: "video";
16
+ audio: "audio";
17
+ subtitle: "subtitle";
18
+ ancillary: "ancillary";
19
+ }> & z.core.$partial, z.ZodUnion<readonly [z.ZodNumber, z.ZodObject<{
20
+ min: z.ZodNumber;
21
+ max: z.ZodNumber;
22
+ }, z.core.$strip>]>>;
23
+ }, z.core.$strip>>;
24
+ outputs: z.ZodArray<z.ZodObject<{
25
+ name: z.ZodString;
26
+ kind: z.ZodEnum<{
27
+ video: "video";
28
+ audio: "audio";
29
+ subtitle: "subtitle";
30
+ ancillary: "ancillary";
31
+ }>;
32
+ count: z.ZodOptional<z.ZodNumber>;
33
+ format: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
34
+ }, z.core.$strip>>;
35
+ transports: z.ZodArray<z.ZodString>;
36
+ timeline: z.ZodString;
37
+ bindings: z.ZodObject<{
38
+ sourceUrl: z.ZodString;
39
+ returnName: z.ZodString;
40
+ engineVersion: z.ZodOptional<z.ZodString>;
41
+ conversionId: z.ZodOptional<z.ZodString>;
42
+ }, z.core.$strip>;
43
+ }, z.core.$strip>;
44
+ export type MediaConversionV1 = z.infer<typeof MediaConversionV1Schema>;
45
+ /** Everything wrong with a template's interface declarations, given the
46
+ * parameters it declares. Only known interfaces are checked. Empty = fine. */
47
+ export declare function interfaceDeclarationProblems(template: {
48
+ manifest: Pick<ProductTemplateManifest, "interfaces">;
49
+ parameters: Pick<ProductTemplateParameter, "name">[];
50
+ }): string[];
51
+ /** The template's norsk.io/media-conversion v1 declaration, typed, if it
52
+ * declares a valid one. */
53
+ export declare function mediaConversionOf(manifest: Pick<ProductTemplateManifest, "interfaces">): MediaConversionV1 | undefined;
54
+ type BindingRole = keyof MediaConversionV1["bindings"];
55
+ /** A norsk.io/media-conversion v1 declaration and the parameters its
56
+ * bindings name, built together so a template cannot bind a parameter it
57
+ * never declared. Spread `parameters` into the template's parameters.yaml and
58
+ * `interface` into its manifest's `interfaces`. Each binding defaults to a
59
+ * conventional parameter name; pass a name to use the template's own, or
60
+ * null to leave an optional role out. */
61
+ export declare function declareMediaConversion(shape: Omit<MediaConversionV1, "name" | "version" | "bindings"> & {
62
+ bindings?: Partial<Record<BindingRole, string | null>>;
63
+ }): {
64
+ interface: MediaConversionV1;
65
+ parameters: ProductTemplateParameter[];
66
+ };
67
+ export {};
package/interfaces.js ADDED
@@ -0,0 +1,178 @@
1
+ import { z } from "zod";
2
+ // Interfaces: named, versioned contracts a product template says it implements,
3
+ // so a client can find every template that implements one without knowing which
4
+ // product built it. The first is norsk.io/media-conversion: the relay lists the
5
+ // production templates whose runtime implements it, and any product or turnkey
6
+ // can provide one.
7
+ //
8
+ // The manifest field accepts ANY well-formed declaration and carries it
9
+ // verbatim. Only the interfaces (and versions) this package knows are checked,
10
+ // by interfaceDeclarationProblems: a newer product declaring something this ctl
11
+ // has never heard of must still launch (RFC 0001), and a client that does know
12
+ // it must still see it.
13
+ export const ProductTemplateInterfaceSchema = z
14
+ .object({
15
+ name: z
16
+ .string()
17
+ .regex(/^[a-z0-9]+(\.[a-z0-9]+)+\/[a-z0-9]+(-[a-z0-9]+)*$/, "<domain>/<name>, e.g. norsk.io/media-conversion")
18
+ .meta({ description: "The contract, as <domain>/<name>, e.g. norsk.io/media-conversion." }),
19
+ version: z.number().int().min(1).meta({ description: "The version of the contract this declaration follows." }),
20
+ })
21
+ .catchall(z.unknown())
22
+ .meta({
23
+ id: "ProductTemplateInterface",
24
+ outputId: "ProductTemplateInterface",
25
+ description: "A contract the template implements: its name and version, and the body that contract defines. Carried verbatim; a runner checks only the contracts it knows.",
26
+ });
27
+ export const MEDIA_CONVERSION_INTERFACE = "norsk.io/media-conversion";
28
+ const PARAMETER_NAME = /^[A-Z][A-Z0-9_]*$/;
29
+ const parameterName = z.string().regex(PARAMETER_NAME, "a parameter name");
30
+ const MediaKindSchema = z.enum(["video", "audio", "subtitle", "ancillary"]);
31
+ const KindCountSchema = z.union([
32
+ z.number().int().min(0),
33
+ z
34
+ .object({ min: z.number().int().min(0), max: z.number().int().min(0) })
35
+ .refine((r) => r.min <= r.max, { message: "min must be <= max" }),
36
+ ]);
37
+ export const MediaConversionV1Schema = z.object({
38
+ name: z.literal(MEDIA_CONVERSION_INTERFACE),
39
+ version: z.literal(1),
40
+ // What the converter takes from the relay: per named input, how many tracks
41
+ // of each kind (a count, or a range).
42
+ inputs: z
43
+ .array(z.object({ name: z.string().min(1), kinds: z.partialRecord(MediaKindSchema, KindCountSchema) }))
44
+ .min(1),
45
+ // What it hands back. A format value may refer to a parameter as ${NAME}, so
46
+ // a client can show the result of a launch before it happens.
47
+ outputs: z
48
+ .array(z.object({
49
+ name: z.string().min(1),
50
+ kind: MediaKindSchema,
51
+ count: z.number().int().min(1).optional(),
52
+ format: z.record(z.string(), z.string()).optional(),
53
+ }))
54
+ .min(1),
55
+ // Free strings rather than an enum: a template offering a transport this ctl
56
+ // does not know (srt, moq) must still validate; the client picks one it speaks.
57
+ transports: z.array(z.string().min(1)).min(1),
58
+ timeline: z.string().min(1),
59
+ // The template's own parameters the client fills, by role.
60
+ bindings: z.object({
61
+ sourceUrl: parameterName,
62
+ returnName: parameterName,
63
+ engineVersion: parameterName.optional(),
64
+ conversionId: parameterName.optional(),
65
+ }),
66
+ });
67
+ const PARAMETER_REFERENCE = /\$\{([^}]*)\}/g;
68
+ /** Everything wrong with a template's interface declarations, given the
69
+ * parameters it declares. Only known interfaces are checked. Empty = fine. */
70
+ export function interfaceDeclarationProblems(template) {
71
+ const declared = new Set(template.parameters.map((p) => p.name));
72
+ const seen = new Set();
73
+ const problems = [];
74
+ (template.manifest.interfaces ?? []).forEach((decl, i) => {
75
+ if (decl.name !== MEDIA_CONVERSION_INTERFACE || decl.version !== 1)
76
+ return;
77
+ const at = `interfaces[${i}] (${decl.name} v${decl.version})`;
78
+ if (seen.has(`${decl.name}@${decl.version}`)) {
79
+ problems.push(`${at}: declared more than once`);
80
+ return;
81
+ }
82
+ seen.add(`${decl.name}@${decl.version}`);
83
+ const parsed = MediaConversionV1Schema.safeParse(decl);
84
+ if (!parsed.success) {
85
+ for (const issue of parsed.error.issues) {
86
+ problems.push(`${at}: ${issue.path.join(".") || "(declaration)"}: ${issue.message}`);
87
+ }
88
+ return;
89
+ }
90
+ for (const [role, param] of Object.entries(parsed.data.bindings)) {
91
+ if (param !== undefined && !declared.has(param)) {
92
+ problems.push(`${at}: binding ${role} names ${param}, which is not a declared parameter`);
93
+ }
94
+ }
95
+ parsed.data.outputs.forEach((output, o) => {
96
+ for (const [key, value] of Object.entries(output.format ?? {})) {
97
+ for (const [, ref] of value.matchAll(PARAMETER_REFERENCE)) {
98
+ if (!declared.has(ref)) {
99
+ problems.push(`${at}: outputs[${o}].format.${key} refers to ${ref}, which is not a declared parameter`);
100
+ }
101
+ }
102
+ }
103
+ });
104
+ });
105
+ return problems;
106
+ }
107
+ /** The template's norsk.io/media-conversion v1 declaration, typed, if it
108
+ * declares a valid one. */
109
+ export function mediaConversionOf(manifest) {
110
+ for (const decl of manifest.interfaces ?? []) {
111
+ if (decl.name !== MEDIA_CONVERSION_INTERFACE || decl.version !== 1)
112
+ continue;
113
+ const parsed = MediaConversionV1Schema.safeParse(decl);
114
+ if (parsed.success)
115
+ return parsed.data;
116
+ }
117
+ return undefined;
118
+ }
119
+ const BINDING_DEFAULTS = {
120
+ sourceUrl: {
121
+ name: "SOURCE_URL",
122
+ label: "Source",
123
+ purpose: "Where to pull the media to convert from, e.g. link://host:port/name. Filled by the caller.",
124
+ required: true,
125
+ },
126
+ returnName: {
127
+ name: "RETURN_NAME",
128
+ label: "Return name",
129
+ purpose: "The name to publish the converted media under, for the caller to take it back. Filled by the caller.",
130
+ required: true,
131
+ },
132
+ engineVersion: {
133
+ name: "ENGINE_VERSION",
134
+ label: "Engine version",
135
+ purpose: "The caller's engine build, which this template's must match while norsk-link needs one build at both ends. Filled by the caller.",
136
+ required: false,
137
+ },
138
+ conversionId: {
139
+ name: "CONVERSION_ID",
140
+ label: "Conversion id",
141
+ purpose: "The caller's id for this conversion, for logs and correlation. Filled by the caller.",
142
+ required: false,
143
+ },
144
+ };
145
+ const BINDING_GROUP = "Conversion link";
146
+ /** A norsk.io/media-conversion v1 declaration and the parameters its
147
+ * bindings name, built together so a template cannot bind a parameter it
148
+ * never declared. Spread `parameters` into the template's parameters.yaml and
149
+ * `interface` into its manifest's `interfaces`. Each binding defaults to a
150
+ * conventional parameter name; pass a name to use the template's own, or
151
+ * null to leave an optional role out. */
152
+ export function declareMediaConversion(shape) {
153
+ const { bindings: chosen = {}, ...rest } = shape;
154
+ const bindings = {};
155
+ const parameters = [];
156
+ for (const role of Object.keys(BINDING_DEFAULTS)) {
157
+ const fallback = BINDING_DEFAULTS[role];
158
+ const name = chosen[role] === undefined ? fallback.name : chosen[role];
159
+ if (name === null)
160
+ continue;
161
+ bindings[role] = name;
162
+ parameters.push({
163
+ name,
164
+ purpose: fallback.purpose,
165
+ type: "string",
166
+ label: fallback.label,
167
+ group: BINDING_GROUP,
168
+ ...(fallback.required ? { required: true } : {}),
169
+ });
170
+ }
171
+ const declaration = MediaConversionV1Schema.parse({
172
+ name: MEDIA_CONVERSION_INTERFACE,
173
+ version: 1,
174
+ ...rest,
175
+ bindings,
176
+ });
177
+ return { interface: declaration, parameters };
178
+ }
package/logs.d.ts CHANGED
@@ -2,8 +2,8 @@ import { z } from "zod";
2
2
  export declare const ServiceLogFileSchema: z.ZodObject<{
3
3
  path: z.ZodString;
4
4
  format: z.ZodEnum<{
5
- json: "json";
6
5
  text: "text";
6
+ json: "json";
7
7
  }>;
8
8
  }, z.core.$strict>;
9
9
  export declare const ServiceFluentBitSchema: z.ZodUnion<readonly [z.ZodObject<{
@@ -13,15 +13,15 @@ export declare const ServiceFluentBitSchema: z.ZodUnion<readonly [z.ZodObject<{
13
13
  }, z.core.$strict>]>;
14
14
  export declare const ServiceLogsSchema: z.ZodObject<{
15
15
  stdout: z.ZodOptional<z.ZodEnum<{
16
- json: "json";
17
- text: "text";
18
16
  none: "none";
17
+ text: "text";
18
+ json: "json";
19
19
  }>>;
20
20
  files: z.ZodOptional<z.ZodArray<z.ZodObject<{
21
21
  path: z.ZodString;
22
22
  format: z.ZodEnum<{
23
- json: "json";
24
23
  text: "text";
24
+ json: "json";
25
25
  }>;
26
26
  }, z.core.$strict>>>;
27
27
  fluentBit: z.ZodOptional<z.ZodUnion<readonly [z.ZodObject<{
@@ -33,15 +33,15 @@ export declare const ServiceLogsSchema: z.ZodObject<{
33
33
  export type ServiceLogs = z.infer<typeof ServiceLogsSchema>;
34
34
  export declare const ProductTemplateLogsSchema: z.ZodRecord<z.ZodString, z.ZodObject<{
35
35
  stdout: z.ZodOptional<z.ZodEnum<{
36
- json: "json";
37
- text: "text";
38
36
  none: "none";
37
+ text: "text";
38
+ json: "json";
39
39
  }>>;
40
40
  files: z.ZodOptional<z.ZodArray<z.ZodObject<{
41
41
  path: z.ZodString;
42
42
  format: z.ZodEnum<{
43
- json: "json";
44
43
  text: "text";
44
+ json: "json";
45
45
  }>;
46
46
  }, z.core.$strict>>>;
47
47
  fluentBit: z.ZodOptional<z.ZodUnion<readonly [z.ZodObject<{
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@norskvideo/ctl-product-template-schema",
3
- "version": "0.1.34",
3
+ "version": "0.1.36",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {