@norskvideo/ctl-product-template-schema 0.1.14 → 0.1.16

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.
Files changed (3) hide show
  1. package/index.d.ts +17 -0
  2. package/index.js +41 -0
  3. package/package.json +1 -1
package/index.d.ts CHANGED
@@ -3,6 +3,7 @@ export declare const ProductTemplateTargetSchema: z.ZodEnum<{
3
3
  "docker-compose": "docker-compose";
4
4
  }>;
5
5
  export type ProductTemplateTarget = z.infer<typeof ProductTemplateTargetSchema>;
6
+ export declare const RESERVED_INSTANCE_ROUTE_SEGMENTS: readonly ["studio", "media", "visualiser", "uvis", "status", "metrics"];
6
7
  export declare const PRODUCT_TEMPLATE_ADVANCED_NETWORK_MODES: readonly ["docker", "hybrid"];
7
8
  export declare const ProductTemplateAdvancedSchema: z.ZodObject<{
8
9
  networkMode: z.ZodOptional<z.ZodObject<{
@@ -115,6 +116,7 @@ export declare const ProductTemplateManifestSchema: z.ZodObject<{
115
116
  healthy: "healthy";
116
117
  never: "never";
117
118
  }>>;
119
+ runtimeReadyPath: z.ZodOptional<z.ZodString>;
118
120
  }, z.core.$strip>>;
119
121
  debug: z.ZodOptional<z.ZodObject<{
120
122
  studio: z.ZodOptional<z.ZodBoolean>;
@@ -122,6 +124,13 @@ export declare const ProductTemplateManifestSchema: z.ZodObject<{
122
124
  }, z.core.$strip>>;
123
125
  proxy: z.ZodOptional<z.ZodObject<{
124
126
  expose: z.ZodDefault<z.ZodArray<z.ZodString>>;
127
+ routes: z.ZodOptional<z.ZodArray<z.ZodObject<{
128
+ segment: z.ZodString;
129
+ service: z.ZodString;
130
+ port: z.ZodOptional<z.ZodNumber>;
131
+ websocket: z.ZodOptional<z.ZodBoolean>;
132
+ auth: z.ZodOptional<z.ZodBoolean>;
133
+ }, z.core.$strip>>>;
125
134
  }, z.core.$strip>>;
126
135
  requiresWorkingDirectory: z.ZodOptional<z.ZodBoolean>;
127
136
  sideload: z.ZodOptional<z.ZodObject<{
@@ -282,6 +291,7 @@ export declare const ProductTemplateMaterialsSchema: z.ZodObject<{
282
291
  healthy: "healthy";
283
292
  never: "never";
284
293
  }>>;
294
+ runtimeReadyPath: z.ZodOptional<z.ZodString>;
285
295
  }, z.core.$strip>>;
286
296
  debug: z.ZodOptional<z.ZodObject<{
287
297
  studio: z.ZodOptional<z.ZodBoolean>;
@@ -289,6 +299,13 @@ export declare const ProductTemplateMaterialsSchema: z.ZodObject<{
289
299
  }, z.core.$strip>>;
290
300
  proxy: z.ZodOptional<z.ZodObject<{
291
301
  expose: z.ZodDefault<z.ZodArray<z.ZodString>>;
302
+ routes: z.ZodOptional<z.ZodArray<z.ZodObject<{
303
+ segment: z.ZodString;
304
+ service: z.ZodString;
305
+ port: z.ZodOptional<z.ZodNumber>;
306
+ websocket: z.ZodOptional<z.ZodBoolean>;
307
+ auth: z.ZodOptional<z.ZodBoolean>;
308
+ }, z.core.$strip>>>;
292
309
  }, z.core.$strip>>;
293
310
  requiresWorkingDirectory: z.ZodOptional<z.ZodBoolean>;
294
311
  sideload: z.ZodOptional<z.ZodObject<{
package/index.js CHANGED
@@ -34,6 +34,9 @@ const ProductTemplateUiSchema = z.object({
34
34
  runtimeScreenReadyWhen: z.enum(["running", "healthy", "never"]).optional().meta({
35
35
  description: "When the runner may offer the runtime-screen link, judged on `runtimeScreenService`'s own container state. Omit for the default `running` — the link goes live as soon as the service is up, which is the only state a service without a healthcheck ever reaches. `healthy` holds it back until the service's healthcheck passes, for a product whose screen 404s or misbehaves before it is fully warm. `never` suppresses the link entirely.",
36
36
  }),
37
+ runtimeReadyPath: z.string().startsWith("/").optional().meta({
38
+ description: "HTTP path on `runtimeScreenService` that answers 2xx only once the product's workflow is actually running — for a Studio-hosted product, `/live/api/components`, which 503s until the runner is up. Container health cannot tell: Studio's web server stays healthy after its runner has exited, so a workflow refused at start reads as `healthy` without this. The runner polls the path into the instance's `workflow` facet (`starting` | `running` | `refused`). Omit and the facet is absent — never guessed.",
39
+ }),
37
40
  });
38
41
  // Per-instance Debug menu toggles. The runner always offers raw Studio +
39
42
  // Visualiser debug surfaces for an instance; a product opts a link out when
@@ -47,10 +50,48 @@ const ProductTemplateDebugSchema = z.object({
47
50
  .optional()
48
51
  .meta({ description: "Show the Debug menu's 'Open Visualiser' link. Default true." }),
49
52
  });
53
+ // Path segments the runner owns under /instance/<id>/ and a product may never
54
+ // claim. They are the six base routes instance-routes.ts always emits; a
55
+ // declared route colliding with one would be shadowed by nginx's more-specific
56
+ // match and silently never serve, so the schema rejects it instead.
57
+ export const RESERVED_INSTANCE_ROUTE_SEGMENTS = ["studio", "media", "visualiser", "uvis", "status", "metrics"];
58
+ // One additional HTTP service a product wants behind the runner's front door,
59
+ // beside the runtime-screen catch-all. WHY THIS EXISTS: a product's second UI
60
+ // otherwise needs a published host port, and a plain http host port is a secure
61
+ // context on localhost only — browser APIs like WebCodecs fail from any other
62
+ // machine. The catch-all is a single slot, so a product with two UIs used to
63
+ // have to reverse-proxy one behind the other itself, websocket bridging
64
+ // included.
65
+ const ProductTemplateRouteSchema = z.object({
66
+ segment: z
67
+ .string()
68
+ .regex(/^[a-z0-9][a-z0-9-]*$/, "one lowercase path segment: letters, digits and dashes")
69
+ .refine((s) => !RESERVED_INSTANCE_ROUTE_SEGMENTS.includes(s), {
70
+ message: `segment is reserved by the runner (${RESERVED_INSTANCE_ROUTE_SEGMENTS.join(", ")})`,
71
+ })
72
+ .meta({
73
+ description: "Single path segment under the instance root. The runner serves it at /instance/<id>/<segment>/. Must not be one of the runner's reserved segments (studio, media, visualiser, uvis, status, metrics).",
74
+ }),
75
+ service: z.string().min(1).meta({
76
+ description: "Compose service that serves this route. Must be a service in the template's own compose.yml; the runner dials it by its compose container name.",
77
+ }),
78
+ port: z.number().int().positive().optional().meta({
79
+ description: "Container port to dial. Omit for the conventional 8000.",
80
+ }),
81
+ websocket: z.boolean().optional().meta({
82
+ description: "Emit the Upgrade/Connection proxy headers so this route can carry websockets. Omit for a plain HTTP route. HTTP is always served; this only adds the upgrade path, so a route carrying both needs just this flag.",
83
+ }),
84
+ auth: z.boolean().optional().meta({
85
+ description: "Whether the route sits behind the runner's oauth2 auth_request. Omit for the default `true`. Set false only for a surface that must be reachable unauthenticated, as the media route is.",
86
+ }),
87
+ });
50
88
  const ProductTemplateProxySchema = z.object({
51
89
  expose: z.array(z.string()).default([]).meta({
52
90
  description: "Paths (or globs) the runner's nginx routes through to this instance. Anything not in the list returns 404 from the proxy. Glob-shaped strings (`/static/*`) are accepted as hints — current implementation treats the whole set as a signal to mount a catch-all on the instance root; per-path filtering can come later.",
53
91
  }),
92
+ routes: z.array(ProductTemplateRouteSchema).optional().meta({
93
+ description: "Extra named services to route under the instance root, beside the runtime-screen catch-all. Additive and optional: a template that omits it routes exactly as before, so an older runner ignoring the field still launches the product.",
94
+ }),
54
95
  });
55
96
  // product-template-schema cannot import @norsk-ctl/shared (dependency direction), so
56
97
  // this is a literal copy of the runner's network-mode enum — and the runner
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@norskvideo/ctl-product-template-schema",
3
- "version": "0.1.14",
3
+ "version": "0.1.16",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {