@norskvideo/ctl-product-template-schema 0.1.57 → 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.
Files changed (3) hide show
  1. package/index.d.ts +8 -0
  2. package/index.js +67 -0
  3. package/package.json +1 -1
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.57",
3
+ "version": "0.1.58",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {