@norskvideo/ctl-product-template-schema 0.1.15 → 0.1.17
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 +21 -0
- package/index.js +51 -0
- package/package.json +9 -1
- package/workdir-fetch.d.ts +30 -0
- package/workdir-fetch.js +82 -0
- package/workdir-path.d.ts +17 -0
- package/workdir-path.js +67 -0
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<{
|
|
@@ -123,7 +124,17 @@ export declare const ProductTemplateManifestSchema: z.ZodObject<{
|
|
|
123
124
|
}, z.core.$strip>>;
|
|
124
125
|
proxy: z.ZodOptional<z.ZodObject<{
|
|
125
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>>>;
|
|
126
134
|
}, z.core.$strip>>;
|
|
135
|
+
allowWorkflowUploads: z.ZodOptional<z.ZodBoolean>;
|
|
136
|
+
isStudio: z.ZodOptional<z.ZodBoolean>;
|
|
137
|
+
supportsCustomAssets: z.ZodOptional<z.ZodBoolean>;
|
|
127
138
|
requiresWorkingDirectory: z.ZodOptional<z.ZodBoolean>;
|
|
128
139
|
sideload: z.ZodOptional<z.ZodObject<{
|
|
129
140
|
gpuInference: z.ZodOptional<z.ZodBoolean>;
|
|
@@ -291,7 +302,17 @@ export declare const ProductTemplateMaterialsSchema: z.ZodObject<{
|
|
|
291
302
|
}, z.core.$strip>>;
|
|
292
303
|
proxy: z.ZodOptional<z.ZodObject<{
|
|
293
304
|
expose: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
305
|
+
routes: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
306
|
+
segment: z.ZodString;
|
|
307
|
+
service: z.ZodString;
|
|
308
|
+
port: z.ZodOptional<z.ZodNumber>;
|
|
309
|
+
websocket: z.ZodOptional<z.ZodBoolean>;
|
|
310
|
+
auth: z.ZodOptional<z.ZodBoolean>;
|
|
311
|
+
}, z.core.$strip>>>;
|
|
294
312
|
}, z.core.$strip>>;
|
|
313
|
+
allowWorkflowUploads: z.ZodOptional<z.ZodBoolean>;
|
|
314
|
+
isStudio: z.ZodOptional<z.ZodBoolean>;
|
|
315
|
+
supportsCustomAssets: z.ZodOptional<z.ZodBoolean>;
|
|
295
316
|
requiresWorkingDirectory: z.ZodOptional<z.ZodBoolean>;
|
|
296
317
|
sideload: z.ZodOptional<z.ZodObject<{
|
|
297
318
|
gpuInference: z.ZodOptional<z.ZodBoolean>;
|
package/index.js
CHANGED
|
@@ -50,10 +50,48 @@ const ProductTemplateDebugSchema = z.object({
|
|
|
50
50
|
.optional()
|
|
51
51
|
.meta({ description: "Show the Debug menu's 'Open Visualiser' link. Default true." }),
|
|
52
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
|
+
});
|
|
53
88
|
const ProductTemplateProxySchema = z.object({
|
|
54
89
|
expose: z.array(z.string()).default([]).meta({
|
|
55
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.",
|
|
56
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
|
+
}),
|
|
57
95
|
});
|
|
58
96
|
// product-template-schema cannot import @norsk-ctl/shared (dependency direction), so
|
|
59
97
|
// this is a literal copy of the runner's network-mode enum — and the runner
|
|
@@ -218,6 +256,19 @@ export const ProductTemplateManifestSchema = z
|
|
|
218
256
|
description: "Per-instance Debug menu toggles. Each of `studio`/`visualiser` defaults to shown; set false to hide a redundant link (e.g. Studio hides its own 'Open Studio').",
|
|
219
257
|
}),
|
|
220
258
|
proxy: ProductTemplateProxySchema.optional(),
|
|
259
|
+
allowWorkflowUploads: z.boolean().optional().meta({
|
|
260
|
+
description: "Whether the operator may upload workflow documents for this product. They land in the working directory's studio-save-files/, where Studio lists them for the operator to open — so this is for Norsk Studio itself (the editor), not for appliances built on it: an appliance (probe, playout) generates its one workflow from its config screen and has nothing to select. Absent = false. Assets are governed separately by `supportsCustomAssets`.",
|
|
261
|
+
}),
|
|
262
|
+
/** @deprecated Renamed to `allowWorkflowUploads`, which says what it
|
|
263
|
+
* gates. Read-side only: existing stored product templates carry it, and
|
|
264
|
+
* dropping it would silently un-set the flag on a real install. Nothing
|
|
265
|
+
* writes it. */
|
|
266
|
+
isStudio: z.boolean().optional().meta({
|
|
267
|
+
description: "DEPRECATED — renamed to `allowWorkflowUploads`. Accepted on read so product templates built before the rename keep working; nothing writes it.",
|
|
268
|
+
}),
|
|
269
|
+
supportsCustomAssets: z.boolean().optional().meta({
|
|
270
|
+
description: "Whether the operator may attach their own files (media, overlays, rundowns) to the working directory. Absent = true: every product gets a working directory, so the asset surface is universal and a product opts OUT rather than in. Assets land at operator-named workdir-relative paths and are also surfaced as NORSK_WORKDIR_ASSETS.",
|
|
271
|
+
}),
|
|
221
272
|
requiresWorkingDirectory: z.boolean().optional().meta({
|
|
222
273
|
description: "When true, the operator must choose a working directory at launch (a dev product template they own and keep), rather than getting the auto-assigned per-instance default. The runner's launch UI makes the working-directory field mandatory.",
|
|
223
274
|
}),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@norskvideo/ctl-product-template-schema",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.17",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -18,6 +18,14 @@
|
|
|
18
18
|
"./read": {
|
|
19
19
|
"types": "./read.d.ts",
|
|
20
20
|
"default": "./read.js"
|
|
21
|
+
},
|
|
22
|
+
"./workdir-fetch": {
|
|
23
|
+
"types": "./workdir-fetch.d.ts",
|
|
24
|
+
"default": "./workdir-fetch.js"
|
|
25
|
+
},
|
|
26
|
+
"./workdir-path": {
|
|
27
|
+
"types": "./workdir-path.d.ts",
|
|
28
|
+
"default": "./workdir-path.js"
|
|
21
29
|
}
|
|
22
30
|
},
|
|
23
31
|
"main": "./index.js",
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/** Reserved parameter carrying the JSON-encoded fetch list. Infra-injected —
|
|
3
|
+
* `invalidParameterNames` rejects operator entries in the NORSK_ namespace. */
|
|
4
|
+
export declare const WORKDIR_FETCH_PARAM = "NORSK_WORKDIR_FETCH";
|
|
5
|
+
export declare const WorkdirFetchEntrySchema: z.ZodObject<{
|
|
6
|
+
path: z.ZodString;
|
|
7
|
+
url: z.ZodString;
|
|
8
|
+
name: z.ZodOptional<z.ZodString>;
|
|
9
|
+
sha256: z.ZodOptional<z.ZodString>;
|
|
10
|
+
optional: z.ZodOptional<z.ZodBoolean>;
|
|
11
|
+
}, z.core.$strip>;
|
|
12
|
+
export type WorkdirFetchEntry = z.infer<typeof WorkdirFetchEntrySchema>;
|
|
13
|
+
export declare const WorkdirFetchListSchema: z.ZodArray<z.ZodObject<{
|
|
14
|
+
path: z.ZodString;
|
|
15
|
+
url: z.ZodString;
|
|
16
|
+
name: z.ZodOptional<z.ZodString>;
|
|
17
|
+
sha256: z.ZodOptional<z.ZodString>;
|
|
18
|
+
optional: z.ZodOptional<z.ZodBoolean>;
|
|
19
|
+
}, z.core.$strip>>;
|
|
20
|
+
export type WorkdirFetchList = z.infer<typeof WorkdirFetchListSchema>;
|
|
21
|
+
/** Parse the reserved parameter's value. Returns [] for an absent/blank
|
|
22
|
+
* value; a malformed one is an error the caller reports rather than a
|
|
23
|
+
* silent empty list — a typo'd list must not look like "no assets". */
|
|
24
|
+
export declare function parseWorkdirFetchParam(raw: string | undefined): WorkdirFetchList;
|
|
25
|
+
export declare function encodeWorkdirFetchParam(list: WorkdirFetchList): string;
|
|
26
|
+
export { assertSafeWorkdirPath, checkWorkdirPath, type WorkdirPathProblem, } from "./workdir-path.js";
|
|
27
|
+
/** Stack the tiers — product template < production template < launch — the
|
|
28
|
+
* same way the parameter tiers stack. Later layers win by `path`, and the
|
|
29
|
+
* winner keeps its position so the resolved order stays readable in the UI. */
|
|
30
|
+
export declare function mergeWorkdirFetchLayers(...layers: (WorkdirFetchList | undefined)[]): WorkdirFetchList;
|
package/workdir-fetch.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// Working-directory fetch list — the per-JOB channel for operator content
|
|
2
|
+
// (assets, media, rundowns) that must land in an instance's working
|
|
3
|
+
// directory on an ephemeral cloud node.
|
|
4
|
+
//
|
|
5
|
+
// Design: norsk-mgr-deploy/docs/design/working-directory-and-assets.md.
|
|
6
|
+
// The list rides the job rather than the product template's tar, which is
|
|
7
|
+
// what keeps the tar immutable: `ProductTemplateRef.sha256` names the on-disk
|
|
8
|
+
// extract on the worker (`worker-<sha12>`) and a repeat import is skipped as
|
|
9
|
+
// NAME_CONFLICT, so mutating a template's bytes would silently give two nodes
|
|
10
|
+
// different content for the same production.
|
|
11
|
+
//
|
|
12
|
+
// Transport is the existing `product_template_parameters` map<string,string>
|
|
13
|
+
// under the reserved name below — no proto change. mgr builds and interpolates
|
|
14
|
+
// the list at launch (where ${production}/${role}/${index} are in scope) and
|
|
15
|
+
// the worker resolves it after seeding, before compose up.
|
|
16
|
+
//
|
|
17
|
+
// This module is on the browser side of the barrel (mgr's composer imports the
|
|
18
|
+
// schema to validate operator input), so: zod only, no node builtins.
|
|
19
|
+
import { z } from "zod";
|
|
20
|
+
import { checkWorkdirPath } from "./workdir-path.js";
|
|
21
|
+
/** Reserved parameter carrying the JSON-encoded fetch list. Infra-injected —
|
|
22
|
+
* `invalidParameterNames` rejects operator entries in the NORSK_ namespace. */
|
|
23
|
+
export const WORKDIR_FETCH_PARAM = "NORSK_WORKDIR_FETCH";
|
|
24
|
+
const SHA256 = z.string().regex(/^[0-9a-f]{64}$/, "lowercase-hex sha256");
|
|
25
|
+
export const WorkdirFetchEntrySchema = z
|
|
26
|
+
.object({
|
|
27
|
+
path: z.string().meta({
|
|
28
|
+
description: "Destination relative to the instance working directory. Must survive assertSafeWorkdirPath: no absolute paths, no '..', and nothing landing under a runner-managed read-only mount point.",
|
|
29
|
+
}),
|
|
30
|
+
url: z.string().meta({
|
|
31
|
+
description: "Where the bytes come from: https:// (plain fetch), s3:// (resolved with the worker's instance role — never credentials in a template), or an absolute mgr URL for operator-uploaded bytes. May carry interpolation tokens (production, runtime, index, role); mgr resolves them before this reaches the wire.",
|
|
32
|
+
}),
|
|
33
|
+
name: z.string().optional().meta({
|
|
34
|
+
description: "Display label for the manager UI. Never a lookup key: assets simply land in the working directory (mounted at /data), and a product may create or rename them at runtime, so no name→path map is meaningful at template time.",
|
|
35
|
+
}),
|
|
36
|
+
sha256: SHA256.optional().meta({
|
|
37
|
+
description: "Content hash. When present the worker verifies the download and caches it content-addressed, so N nodes of a count-launch share one fetch.",
|
|
38
|
+
}),
|
|
39
|
+
optional: z.boolean().optional().meta({
|
|
40
|
+
description: "When true a missing object (404 / NoSuchKey) is not a launch failure — the entry is skipped. For interpolated per-event URLs that may legitimately not exist yet.",
|
|
41
|
+
}),
|
|
42
|
+
})
|
|
43
|
+
.meta({ id: "WorkdirFetchEntry", outputId: "WorkdirFetchEntry" });
|
|
44
|
+
export const WorkdirFetchListSchema = z.array(WorkdirFetchEntrySchema);
|
|
45
|
+
/** Parse the reserved parameter's value. Returns [] for an absent/blank
|
|
46
|
+
* value; a malformed one is an error the caller reports rather than a
|
|
47
|
+
* silent empty list — a typo'd list must not look like "no assets". */
|
|
48
|
+
export function parseWorkdirFetchParam(raw) {
|
|
49
|
+
if (raw === undefined || raw.trim() === "")
|
|
50
|
+
return [];
|
|
51
|
+
let json;
|
|
52
|
+
try {
|
|
53
|
+
json = JSON.parse(raw);
|
|
54
|
+
}
|
|
55
|
+
catch (e) {
|
|
56
|
+
throw new Error(`${WORKDIR_FETCH_PARAM} is not valid JSON: ${e instanceof Error ? e.message : String(e)}`);
|
|
57
|
+
}
|
|
58
|
+
const parsed = WorkdirFetchListSchema.safeParse(json);
|
|
59
|
+
if (!parsed.success)
|
|
60
|
+
throw new Error(`${WORKDIR_FETCH_PARAM} is not a valid fetch list: ${parsed.error.message}`);
|
|
61
|
+
return parsed.data;
|
|
62
|
+
}
|
|
63
|
+
export function encodeWorkdirFetchParam(list) {
|
|
64
|
+
return JSON.stringify(list);
|
|
65
|
+
}
|
|
66
|
+
// Destination safety lives in ./workdir-path.ts — zod-free, so browsers can
|
|
67
|
+
// import the same rules the server enforces. Re-exported here because callers
|
|
68
|
+
// reach for the fetch-list contract as one module.
|
|
69
|
+
export { assertSafeWorkdirPath, checkWorkdirPath, } from "./workdir-path.js";
|
|
70
|
+
/** Stack the tiers — product template < production template < launch — the
|
|
71
|
+
* same way the parameter tiers stack. Later layers win by `path`, and the
|
|
72
|
+
* winner keeps its position so the resolved order stays readable in the UI. */
|
|
73
|
+
export function mergeWorkdirFetchLayers(...layers) {
|
|
74
|
+
const byPath = new Map();
|
|
75
|
+
for (const layer of layers) {
|
|
76
|
+
for (const entry of layer ?? []) {
|
|
77
|
+
const checked = checkWorkdirPath(entry.path);
|
|
78
|
+
byPath.set(checked.ok ? checked.path : entry.path, entry);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return [...byPath.values()];
|
|
82
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type WorkdirPathProblem = {
|
|
2
|
+
kind: "escape" | "shadowed";
|
|
3
|
+
message: string;
|
|
4
|
+
};
|
|
5
|
+
/** The normalised workdir-relative path, or why it is not usable. Pure string
|
|
6
|
+
* logic — no node:path — because this runs in the composer as well as on the
|
|
7
|
+
* worker, and both must agree. */
|
|
8
|
+
export declare function checkWorkdirPath(raw: string): {
|
|
9
|
+
ok: true;
|
|
10
|
+
path: string;
|
|
11
|
+
} | {
|
|
12
|
+
ok: false;
|
|
13
|
+
problem: WorkdirPathProblem;
|
|
14
|
+
};
|
|
15
|
+
/** checkWorkdirPath, throwing. For the worker, where a bad path is a launch
|
|
16
|
+
* failure rather than a form validation message. */
|
|
17
|
+
export declare function assertSafeWorkdirPath(raw: string): string;
|
package/workdir-path.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// Destination safety for working-directory assets — where a file may and may
|
|
2
|
+
// not land inside an instance's working directory.
|
|
3
|
+
//
|
|
4
|
+
// Deliberately ZOD-FREE and dependency-free so browsers can import it: the
|
|
5
|
+
// manager's composer, the product-template forms and (soon) norsk-ctl's UI all
|
|
6
|
+
// validate destinations as the operator types, and none of them should pull a
|
|
7
|
+
// schema library into their bundle to do it. The server validates the same list
|
|
8
|
+
// with the zod schema in ./workdir-fetch.ts, which re-exports everything here —
|
|
9
|
+
// so form and server can never disagree about what a legal destination is.
|
|
10
|
+
// Two separate hazards, deliberately not conflated:
|
|
11
|
+
// escape — `../../etc/x` writes outside the workdir. A hole.
|
|
12
|
+
// shadowing — a file written UNDER one of the runner's read-only bind
|
|
13
|
+
// mounts is on disk and invisible to the container. Not a
|
|
14
|
+
// security problem, but a miserable one to debug, so it is
|
|
15
|
+
// refused at upload time rather than discovered at runtime.
|
|
16
|
+
/** Runner-managed read-only mount points inside the working directory
|
|
17
|
+
* (override-generator.ts). `workflow.yml` is a file mount; the other two are
|
|
18
|
+
* directory mounts, so anything beneath them is shadowed too. */
|
|
19
|
+
const READONLY_MOUNTS = {
|
|
20
|
+
file: ["studio-save-files/workflow.yml"],
|
|
21
|
+
dir: [".norsk"],
|
|
22
|
+
/** `studio-save-files/<anything>/dashboards` — the per-workflow dashboard mount. */
|
|
23
|
+
pattern: [/^studio-save-files\/[^/]+\/dashboards(\/|$)/],
|
|
24
|
+
};
|
|
25
|
+
/** The normalised workdir-relative path, or why it is not usable. Pure string
|
|
26
|
+
* logic — no node:path — because this runs in the composer as well as on the
|
|
27
|
+
* worker, and both must agree. */
|
|
28
|
+
export function checkWorkdirPath(raw) {
|
|
29
|
+
const escaped = (message) => ({ ok: false, problem: { kind: "escape", message } });
|
|
30
|
+
if (raw.includes("\\"))
|
|
31
|
+
return escaped(`'${raw}' must use '/' separators`);
|
|
32
|
+
if (raw.startsWith("/") || /^[a-zA-Z]:/.test(raw))
|
|
33
|
+
return escaped(`'${raw}' must be relative to the working directory`);
|
|
34
|
+
if (raw.startsWith("~"))
|
|
35
|
+
return escaped(`'${raw}' must not start with '~'`);
|
|
36
|
+
const segments = raw.split("/").filter((s) => s !== "" && s !== ".");
|
|
37
|
+
if (segments.length === 0)
|
|
38
|
+
return escaped(`'${raw}' names no file`);
|
|
39
|
+
if (segments.some((s) => s === ".."))
|
|
40
|
+
return escaped(`'${raw}' escapes the working directory`);
|
|
41
|
+
const path = segments.join("/");
|
|
42
|
+
const shadowed = (mount) => ({
|
|
43
|
+
ok: false,
|
|
44
|
+
problem: {
|
|
45
|
+
kind: "shadowed",
|
|
46
|
+
message: `'${path}' lands under the read-only mount '${mount}', where it would be on disk but invisible to the container`,
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
for (const file of READONLY_MOUNTS.file)
|
|
50
|
+
if (path === file)
|
|
51
|
+
return shadowed(file);
|
|
52
|
+
for (const dir of READONLY_MOUNTS.dir)
|
|
53
|
+
if (path === dir || path.startsWith(`${dir}/`))
|
|
54
|
+
return shadowed(dir);
|
|
55
|
+
for (const re of READONLY_MOUNTS.pattern)
|
|
56
|
+
if (re.test(path))
|
|
57
|
+
return shadowed(path.split("/").slice(0, 3).join("/"));
|
|
58
|
+
return { ok: true, path };
|
|
59
|
+
}
|
|
60
|
+
/** checkWorkdirPath, throwing. For the worker, where a bad path is a launch
|
|
61
|
+
* failure rather than a form validation message. */
|
|
62
|
+
export function assertSafeWorkdirPath(raw) {
|
|
63
|
+
const checked = checkWorkdirPath(raw);
|
|
64
|
+
if (!checked.ok)
|
|
65
|
+
throw new Error(checked.problem.message);
|
|
66
|
+
return checked.path;
|
|
67
|
+
}
|