@norskvideo/ctl-product-template-schema 0.1.56 → 0.1.58

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
@@ -209,6 +209,10 @@ export declare const ProductTemplateManifestSchema: z.ZodObject<{
209
209
  path: z.ZodString;
210
210
  openapi: z.ZodString;
211
211
  }, z.core.$strip>>>;
212
+ openPaths: z.ZodOptional<z.ZodArray<z.ZodObject<{
213
+ path: z.ZodString;
214
+ prefix: z.ZodOptional<z.ZodBoolean>;
215
+ }, z.core.$strip>>>;
212
216
  }, z.core.$strip>>;
213
217
  dashboard: z.ZodOptional<z.ZodObject<{
214
218
  kind: z.ZodString;
@@ -481,6 +485,10 @@ export declare const ProductTemplateMaterialsSchema: z.ZodObject<{
481
485
  path: z.ZodString;
482
486
  openapi: z.ZodString;
483
487
  }, z.core.$strip>>>;
488
+ openPaths: z.ZodOptional<z.ZodArray<z.ZodObject<{
489
+ path: z.ZodString;
490
+ prefix: z.ZodOptional<z.ZodBoolean>;
491
+ }, z.core.$strip>>>;
484
492
  }, z.core.$strip>>;
485
493
  dashboard: z.ZodOptional<z.ZodObject<{
486
494
  kind: z.ZodString;
package/index.js CHANGED
@@ -117,6 +117,64 @@ export const ProductTemplateDashboardSchema = z.object({
117
117
  description: "The roles of the MoQ previews it offers, e.g. main.",
118
118
  }),
119
119
  });
120
+ // One Studio path a guest reaches without the operator sign-in (RFC 0010): a
121
+ // join page, its assets, the scoped routes it calls. The path carries its own
122
+ // authority (a signed join token), so the sign-in protects nothing there and
123
+ // only stops the guest.
124
+ //
125
+ // Studio's editor API (uploads included) and MCP are never open, and a prefix
126
+ // may not reach into /live/, which holds every component's API and every live
127
+ // socket. Exact paths under /live/ are how a product opens one route and not
128
+ // its neighbours (`join` but not `join-link`).
129
+ //
130
+ // A `{name}` segment stands for any one segment, for guests named at runtime
131
+ // (`commentators/{commentator}/join`). It never stands for the first segment,
132
+ // which decides /api, /mcp and /live, nor for the component under /live/: the
133
+ // component is what vouches for its own routes, so "this route on every
134
+ // component" would open routes no component offered. A prefix is literal.
135
+ const OPEN_PATH = /^\/[A-Za-z0-9._~-]+(\/([A-Za-z0-9._~-]+|\{[A-Za-z][A-Za-z0-9_]*\}))*\/?$/;
136
+ const PLACEHOLDER = /^\{.*\}$/;
137
+ const NEVER_OPEN = ["/api", "/mcp"];
138
+ const under = (path, root) => path === root || path.startsWith(`${root}/`);
139
+ /** The component a /live path addresses: `/live/<c>/ws`, `/live/api/<c>/...`. */
140
+ const liveComponentSegment = (path) => {
141
+ const [, first, second, third] = path.split("/");
142
+ if (first !== "live")
143
+ return undefined;
144
+ return second === "api" ? third : second;
145
+ };
146
+ const ProductTemplateOpenPathSchema = z
147
+ .object({
148
+ path: z
149
+ .string()
150
+ .regex(OPEN_PATH, "an absolute path under the Studio route: letters, digits and . _ ~ - between slashes, or a whole-segment {name} after the first")
151
+ .refine(SAFE_SEGMENTS, { message: "no . or .. segments" })
152
+ .refine((p) => !NEVER_OPEN.some((root) => under(p, root)), {
153
+ message: `Studio's ${NEVER_OPEN.join(" and ")} are never open`,
154
+ })
155
+ .meta({
156
+ description: "Path relative to the instance's Studio route: /dashboard/x is served at /instance/<id>/studio/dashboard/x. Never under /api or /mcp. A whole segment written {name} matches any one segment, except the first and the component under /live/.",
157
+ }),
158
+ prefix: z.boolean().optional().meta({
159
+ description: "Open everything below `path`, which must then end in `/`. For static assets; a prefix may not reach into /live/. Omit for an exact path.",
160
+ }),
161
+ })
162
+ .refine((o) => !o.prefix || o.path.endsWith("/"), {
163
+ message: "a prefix ends in /",
164
+ path: ["path"],
165
+ })
166
+ .refine((o) => !o.prefix || !under(o.path.replace(/\/$/, ""), "/live"), {
167
+ message: "a prefix may not reach into /live/: open each route there by its exact path",
168
+ path: ["path"],
169
+ })
170
+ .refine((o) => !o.prefix || !o.path.includes("{"), {
171
+ message: "a prefix is literal: placeholders are for exact paths",
172
+ path: ["path"],
173
+ })
174
+ .refine((o) => !PLACEHOLDER.test(liveComponentSegment(o.path) ?? ""), {
175
+ message: "a placeholder may not stand for the component under /live/: name the component",
176
+ path: ["path"],
177
+ });
120
178
  const ProductTemplateProxySchema = z.object({
121
179
  expose: z.array(z.string()).default([]).meta({
122
180
  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.",
@@ -127,6 +185,15 @@ const ProductTemplateProxySchema = z.object({
127
185
  operations: z.array(ProductTemplateOperationsSchema).optional().meta({
128
186
  description: "The operations this template's runtimes serve, each set described by an OpenAPI document in the tar; the ones marked x-norsk-dashboard are what fleet dashboards may call, each decided per user by the platform. Additive: a runner that predates it ignores it.",
129
187
  }),
188
+ openPaths: z
189
+ .array(ProductTemplateOpenPathSchema)
190
+ .refine((paths) => new Set(paths.map((p) => `${p.prefix ? "prefix" : "exact"} ${p.path}`)).size === paths.length, {
191
+ message: "each open path once: nginx refuses a duplicate location, and that fails the reload for every instance",
192
+ })
193
+ .optional()
194
+ .meta({
195
+ description: "Studio paths served without the operator sign-in (RFC 0010): a guest page, its assets and the routes it calls, each of which carries its own authority. Additive: a runner that predates it leaves them signed in.",
196
+ }),
130
197
  });
131
198
  // product-template-schema cannot import @norsk-ctl/shared (dependency direction), so
132
199
  // 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.56",
3
+ "version": "0.1.58",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
package/read.d.ts CHANGED
@@ -1,5 +1,17 @@
1
1
  import type { ProductTemplateParameter } from "./index.js";
2
2
  import { type ProductTemplateManifest } from "./index.js";
3
+ /** The NORSK_ prefix is platform-injected (runtime identity, allocated ports,
4
+ * the licence path...); mgr already refuses an operator value for one, so a
5
+ * product declaring one asks for a value nobody may give it.
6
+ *
7
+ * WARN-ONLY FOR ONE RELEASE, then a refusal (a superRefine on
8
+ * ProductTemplateParameterSchema.name). Release order: relay (its NORSK_IMAGE
9
+ * parameter became MEDIA_IMAGE) and db-chanel (NORSK_CTL_URL became CTL_URL)
10
+ * ship first, then ctl and mgr with this warning, then the refusal. The
11
+ * callers log what this returns at import, refresh, upload and metadata
12
+ * update; nothing re-validates stored templates at startup. */
13
+ export declare const RESERVED_PARAMETER_PREFIX = "NORSK_";
14
+ export declare function reservedParameterNameWarnings(parameters: readonly ProductTemplateParameter[]): string[];
3
15
  /**
4
16
  * Error taxonomy for product-template lifecycle issues. Codes cover both the
5
17
  * "store + extract" path (ctl's ProductTemplateService — extraction, manifest
package/read.js CHANGED
@@ -9,6 +9,23 @@
9
9
  // mgr (which stores raw bytes for relay) does not — so compose presence is a
10
10
  // reported fact here, not a verdict.
11
11
  import { ParametersFileSchema, ProductTemplateManifestSchema } from "./index.js";
12
+ /** The NORSK_ prefix is platform-injected (runtime identity, allocated ports,
13
+ * the licence path...); mgr already refuses an operator value for one, so a
14
+ * product declaring one asks for a value nobody may give it.
15
+ *
16
+ * WARN-ONLY FOR ONE RELEASE, then a refusal (a superRefine on
17
+ * ProductTemplateParameterSchema.name). Release order: relay (its NORSK_IMAGE
18
+ * parameter became MEDIA_IMAGE) and db-chanel (NORSK_CTL_URL became CTL_URL)
19
+ * ship first, then ctl and mgr with this warning, then the refusal. The
20
+ * callers log what this returns at import, refresh, upload and metadata
21
+ * update; nothing re-validates stored templates at startup. */
22
+ export const RESERVED_PARAMETER_PREFIX = "NORSK_";
23
+ export function reservedParameterNameWarnings(parameters) {
24
+ return parameters
25
+ .filter((p) => p.name.startsWith(RESERVED_PARAMETER_PREFIX))
26
+ .map((p) => `parameter '${p.name}' uses the reserved ${RESERVED_PARAMETER_PREFIX} prefix, which the platform sets; ` +
27
+ "rename it without the prefix. A later release refuses such templates");
28
+ }
12
29
  /**
13
30
  * Error taxonomy for product-template lifecycle issues. Codes cover both the
14
31
  * "store + extract" path (ctl's ProductTemplateService — extraction, manifest