@norskvideo/ctl-product-template-schema 0.1.51 → 0.1.53

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
@@ -150,6 +150,11 @@ export declare const ProductTemplateMigrationSchema: z.ZodObject<{
150
150
  }>;
151
151
  }, z.core.$strip>;
152
152
  export type ProductTemplateMigration = z.infer<typeof ProductTemplateMigrationSchema>;
153
+ export declare const ProductTemplateLeaseSchema: z.ZodObject<{
154
+ protected: z.ZodLiteral<true>;
155
+ graceMs: z.ZodOptional<z.ZodNumber>;
156
+ }, z.core.$strip>;
157
+ export type ProductTemplateLease = z.infer<typeof ProductTemplateLeaseSchema>;
153
158
  export declare const PRODUCT_TEMPLATE_WORKLOAD_ROLES: readonly ["viewer", "operator", "builder"];
154
159
  export declare const ProductTemplateWorkloadSchema: z.ZodObject<{
155
160
  role: z.ZodEnum<{
@@ -352,6 +357,10 @@ export declare const ProductTemplateManifestSchema: z.ZodObject<{
352
357
  name: z.ZodString;
353
358
  version: z.ZodNumber;
354
359
  }, z.core.$catchall<z.ZodUnknown>>>>;
360
+ lease: z.ZodOptional<z.ZodObject<{
361
+ protected: z.ZodLiteral<true>;
362
+ graceMs: z.ZodOptional<z.ZodNumber>;
363
+ }, z.core.$strip>>;
355
364
  migration: z.ZodOptional<z.ZodObject<{
356
365
  strategy: z.ZodEnum<{
357
366
  overlap: "overlap";
@@ -609,6 +618,10 @@ export declare const ProductTemplateMaterialsSchema: z.ZodObject<{
609
618
  name: z.ZodString;
610
619
  version: z.ZodNumber;
611
620
  }, z.core.$catchall<z.ZodUnknown>>>>;
621
+ lease: z.ZodOptional<z.ZodObject<{
622
+ protected: z.ZodLiteral<true>;
623
+ graceMs: z.ZodOptional<z.ZodNumber>;
624
+ }, z.core.$strip>>;
612
625
  migration: z.ZodOptional<z.ZodObject<{
613
626
  strategy: z.ZodEnum<{
614
627
  overlap: "overlap";
@@ -658,5 +671,5 @@ export declare const ProductTemplateMaterialsSchema: z.ZodObject<{
658
671
  }, z.core.$strip>>>;
659
672
  }, z.core.$strip>;
660
673
  export type ProductTemplateMaterials = z.infer<typeof ProductTemplateMaterialsSchema>;
661
- export { declareMediaConversion, interfaceDeclarationProblems, MEDIA_CONVERSION_INTERFACE, type MediaConversionV1, MediaConversionV1Schema, mediaConversionOf, type ProductTemplateInterface, ProductTemplateInterfaceSchema, } from "./interfaces.js";
674
+ export { CONVERSION_TARGET_FORMAT, declareMediaConversion, interfaceDeclarationProblems, MEDIA_CONVERSION_INTERFACE, MEDIA_CONVERSION_VERSION, type MediaConversion, MediaConversionSchema, mediaConversionOf, type ProductTemplateInterface, ProductTemplateInterfaceSchema, } from "./interfaces.js";
662
675
  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
@@ -320,6 +320,24 @@ export const ProductTemplateMigrationSchema = z
320
320
  }),
321
321
  })
322
322
  .meta({ id: "ProductTemplateMigration", outputId: "ProductTemplateMigration" });
323
+ // Lease-protected instances (norsk task 115 Phase 6; protocol 1.6). A product
324
+ // whose instance holds something that must never be in use by two encoders
325
+ // at once — an Akamai entry point, a multicast address, a SIP number —
326
+ // declares it here; the Manager does not need to know what the thing is.
327
+ // An instance lost uncleanly (its worker dropped off the network) is then
328
+ // not started again until `graceMs` has passed (absent: the deployment's,
329
+ // 60 s by default), and its worker stops it when it can no longer reach a
330
+ // live Manager. A clean stop needs no grace. Pair it with the default
331
+ // `stopThenStart` migration: an `overlap` migration would run two at once,
332
+ // which the Manager refuses for a lease-protected instance.
333
+ export const ProductTemplateLeaseSchema = z
334
+ .object({
335
+ protected: z.literal(true).meta({ description: "This product's instances are lease-protected." }),
336
+ graceMs: z.number().int().positive().max(3_600_000).optional().meta({
337
+ description: "How long an instance whose worker was lost uncleanly waits before it may start elsewhere, in ms. Must exceed the workers' lease deadline (30 s by default) by a margin. Absent: the deployment's (60 s by default).",
338
+ }),
339
+ })
340
+ .meta({ id: "ProductTemplateLease", outputId: "ProductTemplateLease" });
323
341
  // The roles a template may declare for its instances' workload: at most
324
342
  // builder, since registering a template takes builder and that registration is
325
343
  // the consent. Built-in names only, whose meaning every host agrees on.
@@ -388,6 +406,9 @@ export const ProductTemplateManifestSchema = z
388
406
  interfaces: z.array(ProductTemplateInterfaceSchema).optional().meta({
389
407
  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
408
  }),
409
+ lease: ProductTemplateLeaseSchema.optional().meta({
410
+ description: "Lease-protected instances: each holds something that must never be in use by two encoders at once. An instance lost uncleanly is not started again until its grace has passed; a clean stop needs none.",
411
+ }),
391
412
  migration: ProductTemplateMigrationSchema.optional().meta({
392
413
  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.",
393
414
  }),
@@ -503,5 +524,5 @@ export const ProductTemplateMaterialsSchema = z
503
524
  }),
504
525
  })
505
526
  .meta({ id: "ProductTemplateMaterials" });
506
- export { declareMediaConversion, interfaceDeclarationProblems, MEDIA_CONVERSION_INTERFACE, MediaConversionV1Schema, mediaConversionOf, ProductTemplateInterfaceSchema, } from "./interfaces.js";
527
+ export { CONVERSION_TARGET_FORMAT, declareMediaConversion, interfaceDeclarationProblems, MEDIA_CONVERSION_INTERFACE, MEDIA_CONVERSION_VERSION, MediaConversionSchema, mediaConversionOf, ProductTemplateInterfaceSchema, } from "./interfaces.js";
507
528
  export { composeServiceNamesOf, logsDeclarationProblems, NORSK_MEDIA_SERVICE_LOGS, ProductTemplateLogsSchema, productTemplateLogsProblems, ServiceLogsSchema, STANDARD_LOGS, STUDIO_SERVICE_LOGS, servicesWithoutLogs, } from "./logs.js";
package/interfaces.d.ts CHANGED
@@ -6,9 +6,17 @@ export declare const ProductTemplateInterfaceSchema: z.ZodObject<{
6
6
  }, z.core.$catchall<z.ZodUnknown>>;
7
7
  export type ProductTemplateInterface = z.infer<typeof ProductTemplateInterfaceSchema>;
8
8
  export declare const MEDIA_CONVERSION_INTERFACE = "norsk.io/media-conversion";
9
- export declare const MediaConversionV1Schema: z.ZodObject<{
9
+ /** The version of norsk.io/media-conversion this package knows. Version 1 (a
10
+ * fixed list of outputs, flat parameters) was retired before any caller used
11
+ * it; its number is not reused, so a reader of the old v1 never misreads this. */
12
+ export declare const MEDIA_CONVERSION_VERSION = 2;
13
+ /** The format a conversion's target follows: the conversion core's
14
+ * ConversionTarget (@norskvideo/norsk-studio-standards-conversion). Naming it
15
+ * tells a caller it can plan the conversion itself, with the same core. */
16
+ export declare const CONVERSION_TARGET_FORMAT = "norsk.io/conversion-target";
17
+ export declare const MediaConversionSchema: z.ZodObject<{
10
18
  name: z.ZodLiteral<"norsk.io/media-conversion">;
11
- version: z.ZodLiteral<1>;
19
+ version: z.ZodLiteral<2>;
12
20
  inputs: z.ZodArray<z.ZodObject<{
13
21
  name: z.ZodString;
14
22
  kinds: z.ZodRecord<z.ZodEnum<{
@@ -21,47 +29,50 @@ export declare const MediaConversionV1Schema: z.ZodObject<{
21
29
  max: z.ZodNumber;
22
30
  }, z.core.$strip>]>>;
23
31
  }, 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>>;
32
+ returns: z.ZodLiteral<"programme">;
35
33
  transports: z.ZodArray<z.ZodString>;
36
34
  timeline: z.ZodString;
35
+ target: z.ZodObject<{
36
+ format: z.ZodString;
37
+ version: z.ZodNumber;
38
+ }, z.core.$strip>;
39
+ profiles: z.ZodOptional<z.ZodArray<z.ZodObject<{
40
+ name: z.ZodString;
41
+ label: z.ZodString;
42
+ description: z.ZodOptional<z.ZodString>;
43
+ target: z.ZodRecord<z.ZodString, z.ZodUnknown>;
44
+ }, z.core.$strip>>>;
37
45
  bindings: z.ZodObject<{
38
46
  sourceUrl: z.ZodString;
39
47
  returnName: z.ZodString;
48
+ target: z.ZodString;
40
49
  engineVersion: z.ZodOptional<z.ZodString>;
41
50
  conversionId: z.ZodOptional<z.ZodString>;
42
51
  }, z.core.$strip>;
43
52
  }, z.core.$strip>;
44
- export type MediaConversionV1 = z.infer<typeof MediaConversionV1Schema>;
53
+ export type MediaConversion = z.infer<typeof MediaConversionSchema>;
45
54
  /** Everything wrong with a template's interface declarations, given the
46
55
  * parameters it declares. Only known interfaces are checked. Empty = fine. */
47
56
  export declare function interfaceDeclarationProblems(template: {
48
57
  manifest: Pick<ProductTemplateManifest, "interfaces">;
49
58
  parameters: Pick<ProductTemplateParameter, "name">[];
50
59
  }): string[];
51
- /** The template's norsk.io/media-conversion v1 declaration, typed, if it
60
+ /** The template's norsk.io/media-conversion declaration, typed, if it
52
61
  * 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
62
+ export declare function mediaConversionOf(manifest: Pick<ProductTemplateManifest, "interfaces">): MediaConversion | undefined;
63
+ type BindingRole = keyof MediaConversion["bindings"];
64
+ /** A norsk.io/media-conversion declaration and the parameters its bindings
65
+ * name, built together so a template cannot bind a parameter it never
66
+ * declared. Spread `parameters` into the template's parameters.yaml and
58
67
  * `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"> & {
68
+ * conventional parameter name; pass a name to use the template's own, or null
69
+ * to leave an optional role out. The target format defaults to the
70
+ * conversion core's. */
71
+ export declare function declareMediaConversion(shape: Omit<MediaConversion, "name" | "version" | "bindings" | "returns" | "target"> & {
72
+ target?: MediaConversion["target"];
62
73
  bindings?: Partial<Record<BindingRole, string | null>>;
63
74
  }): {
64
- interface: MediaConversionV1;
75
+ interface: MediaConversion;
65
76
  parameters: ProductTemplateParameter[];
66
77
  };
67
78
  export {};
package/interfaces.js CHANGED
@@ -25,6 +25,10 @@ export const ProductTemplateInterfaceSchema = z
25
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
26
  });
27
27
  export const MEDIA_CONVERSION_INTERFACE = "norsk.io/media-conversion";
28
+ /** The version of norsk.io/media-conversion this package knows. Version 1 (a
29
+ * fixed list of outputs, flat parameters) was retired before any caller used
30
+ * it; its number is not reused, so a reader of the old v1 never misreads this. */
31
+ export const MEDIA_CONVERSION_VERSION = 2;
28
32
  const PARAMETER_NAME = /^[A-Z][A-Z0-9_]*$/;
29
33
  const parameterName = z.string().regex(PARAMETER_NAME, "a parameter name");
30
34
  const MediaKindSchema = z.enum(["video", "audio", "subtitle", "ancillary"]);
@@ -34,53 +38,62 @@ const KindCountSchema = z.union([
34
38
  .object({ min: z.number().int().min(0), max: z.number().int().min(0) })
35
39
  .refine((r) => r.min <= r.max, { message: "min must be <= max" }),
36
40
  ]);
37
- export const MediaConversionV1Schema = z.object({
41
+ /** The format a conversion's target follows: the conversion core's
42
+ * ConversionTarget (@norskvideo/norsk-studio-standards-conversion). Naming it
43
+ * tells a caller it can plan the conversion itself, with the same core. */
44
+ export const CONVERSION_TARGET_FORMAT = "norsk.io/conversion-target";
45
+ export const MediaConversionSchema = z.object({
38
46
  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
47
+ version: z.literal(MEDIA_CONVERSION_VERSION),
48
+ // What the converter takes from the caller: per named input, how many tracks
41
49
  // of each kind (a count, or a range).
42
50
  inputs: z
43
51
  .array(z.object({ name: z.string().min(1), kinds: z.partialRecord(MediaKindSchema, KindCountSchema) }))
44
52
  .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),
53
+ // What comes back: the input programme, each track converted as the target
54
+ // says or passed, under its own stream key.
55
+ returns: z.literal("programme"),
55
56
  // Free strings rather than an enum: a template offering a transport this ctl
56
57
  // does not know (srt, moq) must still validate; the client picks one it speaks.
57
58
  transports: z.array(z.string().min(1)).min(1),
58
59
  timeline: z.string().min(1),
60
+ // The format of the value the target binding carries, and its version.
61
+ target: z.object({ format: z.string().min(1), version: z.number().int().min(1) }),
62
+ // Named targets for the common jobs, which a client offers before the
63
+ // target's own fields. A profile's target is in the target's format.
64
+ profiles: z
65
+ .array(z.object({
66
+ name: z.string().regex(/^[a-z0-9]+(-[a-z0-9]+)*$/, "a lower-case slug"),
67
+ label: z.string().min(1),
68
+ description: z.string().optional(),
69
+ target: z.record(z.string(), z.unknown()),
70
+ }))
71
+ .optional(),
59
72
  // The template's own parameters the client fills, by role.
60
73
  bindings: z.object({
61
74
  sourceUrl: parameterName,
62
75
  returnName: parameterName,
76
+ target: parameterName,
63
77
  engineVersion: parameterName.optional(),
64
78
  conversionId: parameterName.optional(),
65
79
  }),
66
80
  });
67
- const PARAMETER_REFERENCE = /\$\{([^}]*)\}/g;
68
81
  /** Everything wrong with a template's interface declarations, given the
69
82
  * parameters it declares. Only known interfaces are checked. Empty = fine. */
70
83
  export function interfaceDeclarationProblems(template) {
71
84
  const declared = new Set(template.parameters.map((p) => p.name));
72
- const seen = new Set();
85
+ let seen = false;
73
86
  const problems = [];
74
87
  (template.manifest.interfaces ?? []).forEach((decl, i) => {
75
- if (decl.name !== MEDIA_CONVERSION_INTERFACE || decl.version !== 1)
88
+ if (decl.name !== MEDIA_CONVERSION_INTERFACE || decl.version !== MEDIA_CONVERSION_VERSION)
76
89
  return;
77
90
  const at = `interfaces[${i}] (${decl.name} v${decl.version})`;
78
- if (seen.has(`${decl.name}@${decl.version}`)) {
91
+ if (seen) {
79
92
  problems.push(`${at}: declared more than once`);
80
93
  return;
81
94
  }
82
- seen.add(`${decl.name}@${decl.version}`);
83
- const parsed = MediaConversionV1Schema.safeParse(decl);
95
+ seen = true;
96
+ const parsed = MediaConversionSchema.safeParse(decl);
84
97
  if (!parsed.success) {
85
98
  for (const issue of parsed.error.issues) {
86
99
  problems.push(`${at}: ${issue.path.join(".") || "(declaration)"}: ${issue.message}`);
@@ -92,25 +105,16 @@ export function interfaceDeclarationProblems(template) {
92
105
  problems.push(`${at}: binding ${role} names ${param}, which is not a declared parameter`);
93
106
  }
94
107
  }
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
108
  });
105
109
  return problems;
106
110
  }
107
- /** The template's norsk.io/media-conversion v1 declaration, typed, if it
111
+ /** The template's norsk.io/media-conversion declaration, typed, if it
108
112
  * declares a valid one. */
109
113
  export function mediaConversionOf(manifest) {
110
114
  for (const decl of manifest.interfaces ?? []) {
111
- if (decl.name !== MEDIA_CONVERSION_INTERFACE || decl.version !== 1)
115
+ if (decl.name !== MEDIA_CONVERSION_INTERFACE || decl.version !== MEDIA_CONVERSION_VERSION)
112
116
  continue;
113
- const parsed = MediaConversionV1Schema.safeParse(decl);
117
+ const parsed = MediaConversionSchema.safeParse(decl);
114
118
  if (parsed.success)
115
119
  return parsed.data;
116
120
  }
@@ -129,6 +133,12 @@ const BINDING_DEFAULTS = {
129
133
  purpose: "The name to publish the converted media under, for the caller to take it back. Filled by the caller.",
130
134
  required: true,
131
135
  },
136
+ target: {
137
+ name: "CONVERSION_TARGET",
138
+ label: "Conversion target",
139
+ purpose: "What the converted programme should be, as JSON in the declared target format. Filled by the caller.",
140
+ required: true,
141
+ },
132
142
  engineVersion: {
133
143
  name: "ENGINE_VERSION",
134
144
  label: "Engine version",
@@ -143,14 +153,15 @@ const BINDING_DEFAULTS = {
143
153
  },
144
154
  };
145
155
  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
156
+ /** A norsk.io/media-conversion declaration and the parameters its bindings
157
+ * name, built together so a template cannot bind a parameter it never
158
+ * declared. Spread `parameters` into the template's parameters.yaml and
149
159
  * `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. */
160
+ * conventional parameter name; pass a name to use the template's own, or null
161
+ * to leave an optional role out. The target format defaults to the
162
+ * conversion core's. */
152
163
  export function declareMediaConversion(shape) {
153
- const { bindings: chosen = {}, ...rest } = shape;
164
+ const { bindings: chosen = {}, target = { format: CONVERSION_TARGET_FORMAT, version: 1 }, ...rest } = shape;
154
165
  const bindings = {};
155
166
  const parameters = [];
156
167
  for (const role of Object.keys(BINDING_DEFAULTS)) {
@@ -168,9 +179,11 @@ export function declareMediaConversion(shape) {
168
179
  ...(fallback.required ? { required: true } : {}),
169
180
  });
170
181
  }
171
- const declaration = MediaConversionV1Schema.parse({
182
+ const declaration = MediaConversionSchema.parse({
172
183
  name: MEDIA_CONVERSION_INTERFACE,
173
- version: 1,
184
+ version: MEDIA_CONVERSION_VERSION,
185
+ returns: "programme",
186
+ target,
174
187
  ...rest,
175
188
  bindings,
176
189
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@norskvideo/ctl-product-template-schema",
3
- "version": "0.1.51",
3
+ "version": "0.1.53",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {