@molecule/api-code-sandbox-docker 1.0.1 → 1.1.0
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/README.md +92 -2
- package/dist/archive.d.ts +49 -0
- package/dist/archive.d.ts.map +1 -0
- package/dist/archive.js +61 -0
- package/dist/capacity.d.ts +71 -0
- package/dist/capacity.d.ts.map +1 -0
- package/dist/capacity.js +98 -0
- package/dist/egress.d.ts +49 -0
- package/dist/egress.d.ts.map +1 -0
- package/dist/egress.js +424 -0
- package/dist/hibernate.d.ts +79 -0
- package/dist/hibernate.d.ts.map +1 -0
- package/dist/hibernate.js +142 -0
- package/dist/index.d.ts +44 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +44 -1
- package/dist/inspect.d.ts +131 -0
- package/dist/inspect.d.ts.map +1 -0
- package/dist/inspect.js +212 -0
- package/dist/provider.d.ts +14 -0
- package/dist/provider.d.ts.map +1 -1
- package/dist/provider.js +496 -19
- package/dist/request.d.ts +66 -0
- package/dist/request.d.ts.map +1 -0
- package/dist/request.js +56 -0
- package/dist/templates.d.ts +109 -0
- package/dist/templates.d.ts.map +1 -0
- package/dist/templates.js +381 -0
- package/dist/types.d.ts +29 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Docker Engine API transports every capability module is given.
|
|
3
|
+
*
|
|
4
|
+
* Each capability lives in its own module and receives its transport as a
|
|
5
|
+
* parameter rather than reaching for `http` itself. That is not decoration: a
|
|
6
|
+
* module that opens its own socket can only be tested by intercepting HTTP, and
|
|
7
|
+
* the things worth testing here — a push that reports failure inside a 200
|
|
8
|
+
* response body, an eviction that must refuse when it cannot see what is in use —
|
|
9
|
+
* are decisions about response CONTENT, not about wire mechanics. Injecting the
|
|
10
|
+
* request function makes each of those a plain function call in a test.
|
|
11
|
+
*
|
|
12
|
+
* Three transports rather than one, because the Engine API genuinely has three
|
|
13
|
+
* shapes: JSON in/JSON out, a tar stream out, and a tar stream in. Forcing the
|
|
14
|
+
* streaming pair through the JSON transport would mean buffering a whole project
|
|
15
|
+
* tree in memory, which is the thing bulk transfer exists to avoid.
|
|
16
|
+
*
|
|
17
|
+
* @module
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* Issues a Docker Engine API request and returns the parsed JSON response (or
|
|
21
|
+
* the raw text when the body is not JSON — several endpoints stream JSON lines).
|
|
22
|
+
*
|
|
23
|
+
* Implementations MUST reject on a 4xx/5xx status. Several Engine endpoints also
|
|
24
|
+
* report failure INSIDE a 200 body; see {@link assertNoStreamError}.
|
|
25
|
+
*
|
|
26
|
+
* @param path - API path below the version prefix, e.g. `/containers/create`.
|
|
27
|
+
* @param method - HTTP method; defaults to `GET`.
|
|
28
|
+
* @param body - Optional JSON request body.
|
|
29
|
+
* @param timeoutMs - Request timeout.
|
|
30
|
+
* @param headers - Extra request headers (e.g. `X-Registry-Auth`).
|
|
31
|
+
* @returns The parsed JSON response, or raw text for non-JSON bodies.
|
|
32
|
+
*/
|
|
33
|
+
export type DockerRequest = (path: string, method?: string, body?: unknown, timeoutMs?: number, headers?: Record<string, string>) => Promise<unknown>;
|
|
34
|
+
/**
|
|
35
|
+
* Issues a Docker Engine API GET and hands back the response body as a byte
|
|
36
|
+
* stream, unbuffered.
|
|
37
|
+
*
|
|
38
|
+
* @param path - API path below the version prefix.
|
|
39
|
+
* @param timeoutMs - Request timeout.
|
|
40
|
+
* @returns The response body as an async byte stream.
|
|
41
|
+
*/
|
|
42
|
+
export type DockerDownload = (path: string, timeoutMs?: number) => Promise<AsyncIterable<Uint8Array>>;
|
|
43
|
+
/**
|
|
44
|
+
* Issues a Docker Engine API PUT whose request body is a byte stream,
|
|
45
|
+
* unbuffered and with backpressure.
|
|
46
|
+
*
|
|
47
|
+
* @param path - API path below the version prefix.
|
|
48
|
+
* @param body - The request body as an async byte stream.
|
|
49
|
+
* @param timeoutMs - Request timeout.
|
|
50
|
+
*/
|
|
51
|
+
export type DockerUpload = (path: string, body: AsyncIterable<Uint8Array>, timeoutMs?: number) => Promise<void>;
|
|
52
|
+
/**
|
|
53
|
+
* Fail on an error reported inside an otherwise-successful streaming response.
|
|
54
|
+
*
|
|
55
|
+
* `/images/create` (pull), `/images/{name}/push` and friends answer `200 OK` and
|
|
56
|
+
* then stream newline-delimited JSON progress objects. A failure — no such
|
|
57
|
+
* manifest, denied credentials, a broken layer — arrives as an `error` field in
|
|
58
|
+
* that stream, LONG after the status line. Treating the 200 as success is how a
|
|
59
|
+
* fleet ends up believing every host has a template that only one host has.
|
|
60
|
+
*
|
|
61
|
+
* @param response - The value returned by a {@link DockerRequest} for a streaming endpoint.
|
|
62
|
+
* @param operation - Short description used in the thrown message.
|
|
63
|
+
* @throws {Error} When the stream carried an error object.
|
|
64
|
+
*/
|
|
65
|
+
export declare function assertNoStreamError(response: unknown, operation: string): void;
|
|
66
|
+
//# sourceMappingURL=request.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"request.d.ts","sourceRoot":"","sources":["../src/request.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,aAAa,GAAG,CAC1B,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,MAAM,EACf,IAAI,CAAC,EAAE,OAAO,EACd,SAAS,CAAC,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAC7B,OAAO,CAAC,OAAO,CAAC,CAAA;AAErB;;;;;;;GAOG;AACH,MAAM,MAAM,cAAc,GAAG,CAC3B,IAAI,EAAE,MAAM,EACZ,SAAS,CAAC,EAAE,MAAM,KACf,OAAO,CAAC,aAAa,CAAC,UAAU,CAAC,CAAC,CAAA;AAEvC;;;;;;;GAOG;AACH,MAAM,MAAM,YAAY,GAAG,CACzB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,aAAa,CAAC,UAAU,CAAC,EAC/B,SAAS,CAAC,EAAE,MAAM,KACf,OAAO,CAAC,IAAI,CAAC,CAAA;AAElB;;;;;;;;;;;;GAYG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CAsB9E"}
|
package/dist/request.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Docker Engine API transports every capability module is given.
|
|
3
|
+
*
|
|
4
|
+
* Each capability lives in its own module and receives its transport as a
|
|
5
|
+
* parameter rather than reaching for `http` itself. That is not decoration: a
|
|
6
|
+
* module that opens its own socket can only be tested by intercepting HTTP, and
|
|
7
|
+
* the things worth testing here — a push that reports failure inside a 200
|
|
8
|
+
* response body, an eviction that must refuse when it cannot see what is in use —
|
|
9
|
+
* are decisions about response CONTENT, not about wire mechanics. Injecting the
|
|
10
|
+
* request function makes each of those a plain function call in a test.
|
|
11
|
+
*
|
|
12
|
+
* Three transports rather than one, because the Engine API genuinely has three
|
|
13
|
+
* shapes: JSON in/JSON out, a tar stream out, and a tar stream in. Forcing the
|
|
14
|
+
* streaming pair through the JSON transport would mean buffering a whole project
|
|
15
|
+
* tree in memory, which is the thing bulk transfer exists to avoid.
|
|
16
|
+
*
|
|
17
|
+
* @module
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* Fail on an error reported inside an otherwise-successful streaming response.
|
|
21
|
+
*
|
|
22
|
+
* `/images/create` (pull), `/images/{name}/push` and friends answer `200 OK` and
|
|
23
|
+
* then stream newline-delimited JSON progress objects. A failure — no such
|
|
24
|
+
* manifest, denied credentials, a broken layer — arrives as an `error` field in
|
|
25
|
+
* that stream, LONG after the status line. Treating the 200 as success is how a
|
|
26
|
+
* fleet ends up believing every host has a template that only one host has.
|
|
27
|
+
*
|
|
28
|
+
* @param response - The value returned by a {@link DockerRequest} for a streaming endpoint.
|
|
29
|
+
* @param operation - Short description used in the thrown message.
|
|
30
|
+
* @throws {Error} When the stream carried an error object.
|
|
31
|
+
*/
|
|
32
|
+
export function assertNoStreamError(response, operation) {
|
|
33
|
+
if (typeof response !== 'string') {
|
|
34
|
+
// A parsed object comes from a single-JSON-document endpoint; those already
|
|
35
|
+
// surfaced failure through the status code.
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
for (const line of response.split('\n')) {
|
|
39
|
+
const trimmed = line.trim();
|
|
40
|
+
if (!trimmed.startsWith('{'))
|
|
41
|
+
continue;
|
|
42
|
+
let parsed;
|
|
43
|
+
try {
|
|
44
|
+
parsed = JSON.parse(trimmed);
|
|
45
|
+
}
|
|
46
|
+
catch (_error) {
|
|
47
|
+
// A partial line from a chunked stream is not an error report; the error
|
|
48
|
+
// object, if there is one, is a complete line of its own.
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
const message = parsed.errorDetail?.message ?? parsed.error;
|
|
52
|
+
if (message !== undefined && message !== null && message !== '') {
|
|
53
|
+
throw new Error(`Docker ${operation} failed: ${String(message)}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sandbox templates on Docker — capture a sandbox's filesystem, boot new
|
|
3
|
+
* sandboxes from it, share it between hosts, and reclaim it on a budget.
|
|
4
|
+
*
|
|
5
|
+
* ## Why the capture is not just `commit`
|
|
6
|
+
*
|
|
7
|
+
* `POST /commit` snapshots a container's WRITABLE LAYER, and a mounted volume is
|
|
8
|
+
* not part of it. A sandbox keeps the project on a volume — that is the whole
|
|
9
|
+
* point of the volume — so committing a sandbox directly produces an image that
|
|
10
|
+
* builds, tags, boots, and contains no project. Everything succeeds; the template
|
|
11
|
+
* is empty. So the capture copies the caller-named paths into a throwaway
|
|
12
|
+
* container that has NO volume, and commits that instead.
|
|
13
|
+
*
|
|
14
|
+
* The paths are named by the CALLER (`capturePaths`). They are not discovered,
|
|
15
|
+
* because the two answers a discovery would produce — "this sandbox has no
|
|
16
|
+
* project files" and "I looked in the wrong place" — are indistinguishable, and
|
|
17
|
+
* the wrong one silently ships an empty template that later boots as a working
|
|
18
|
+
* sandbox with an empty workspace.
|
|
19
|
+
*
|
|
20
|
+
* ## The throwaway container is a tenant boundary
|
|
21
|
+
*
|
|
22
|
+
* It extracts a TENANT-AUTHORED archive and is then committed into a template
|
|
23
|
+
* that OTHER tenants boot. So it runs with no network, no capabilities beyond the
|
|
24
|
+
* three the extraction needs, a memory cap and a process cap — and setuid/setgid
|
|
25
|
+
* bits are stripped after extraction, so a tenant cannot bake a privilege
|
|
26
|
+
* escalation into an image somebody else boots. A previous unhardened version of
|
|
27
|
+
* this step ran on the default bridge, outside every firewall rule, holding
|
|
28
|
+
* Docker's full default capability set.
|
|
29
|
+
*
|
|
30
|
+
* @module
|
|
31
|
+
*/
|
|
32
|
+
import type { CommitTemplateOptions, ListTemplatesOptions, SandboxTemplate } from '@molecule/api-code-sandbox';
|
|
33
|
+
import { type DockerDownload, type DockerRequest, type DockerUpload } from './request.js';
|
|
34
|
+
/** What the template capability needs from the provider. */
|
|
35
|
+
export interface TemplateContext {
|
|
36
|
+
request: DockerRequest;
|
|
37
|
+
download: DockerDownload;
|
|
38
|
+
upload: DockerUpload;
|
|
39
|
+
/** Image the throwaway capture container runs. It only needs a shell. */
|
|
40
|
+
baseImage: string;
|
|
41
|
+
/** Local Docker repository that holds templates. One tag per template id. */
|
|
42
|
+
repository: string;
|
|
43
|
+
/** Remote registry host for publish/fetch, or `''` when none is configured. */
|
|
44
|
+
registry: string;
|
|
45
|
+
/** Base64 `X-Registry-Auth` value for the remote registry. */
|
|
46
|
+
registryAuth: string;
|
|
47
|
+
/** Label namespace for the template's own metadata labels. */
|
|
48
|
+
labelPrefix: string;
|
|
49
|
+
warn?: (message: string, meta?: Record<string, unknown>) => void;
|
|
50
|
+
debug?: (message: string, meta?: Record<string, unknown>) => void;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Validate a caller-supplied template id.
|
|
54
|
+
*
|
|
55
|
+
* REJECTS rather than sanitizes. Sanitizing would map two distinct ids onto one
|
|
56
|
+
* tag, and the second commit would silently overwrite the first — a project
|
|
57
|
+
* restoring its own template and receiving somebody else's filesystem.
|
|
58
|
+
*
|
|
59
|
+
* @param templateId - The caller's identifier.
|
|
60
|
+
* @throws {Error} When the id cannot be used as a Docker tag.
|
|
61
|
+
*/
|
|
62
|
+
export declare function assertTemplateId(templateId: string): void;
|
|
63
|
+
/**
|
|
64
|
+
* Capture a sandbox's filesystem into a reusable template.
|
|
65
|
+
*
|
|
66
|
+
* @param ctx - Template context.
|
|
67
|
+
* @param options - What to capture and what to call it.
|
|
68
|
+
* @returns The template as it now exists.
|
|
69
|
+
*/
|
|
70
|
+
export declare function commitTemplate(ctx: TemplateContext, options: CommitTemplateOptions): Promise<SandboxTemplate>;
|
|
71
|
+
/**
|
|
72
|
+
* Read one template by the caller's identifier.
|
|
73
|
+
*
|
|
74
|
+
* @param ctx - Template context.
|
|
75
|
+
* @param templateId - The caller's identifier.
|
|
76
|
+
* @returns The template, or `null` when no image carries that tag.
|
|
77
|
+
*/
|
|
78
|
+
export declare function getTemplate(ctx: TemplateContext, templateId: string): Promise<SandboxTemplate | null>;
|
|
79
|
+
/**
|
|
80
|
+
* Enumerate templates so the caller can apply its retention policy.
|
|
81
|
+
*
|
|
82
|
+
* @param ctx - Template context.
|
|
83
|
+
* @param options - Narrowing by id prefix.
|
|
84
|
+
* @returns Every matching template.
|
|
85
|
+
*/
|
|
86
|
+
export declare function listTemplates(ctx: TemplateContext, options?: ListTemplatesOptions): Promise<SandboxTemplate[]>;
|
|
87
|
+
/**
|
|
88
|
+
* Delete a template, refusing while anything still boots from it.
|
|
89
|
+
*
|
|
90
|
+
* @param ctx - Template context.
|
|
91
|
+
* @param templateId - The caller's identifier.
|
|
92
|
+
*/
|
|
93
|
+
export declare function removeTemplate(ctx: TemplateContext, templateId: string): Promise<void>;
|
|
94
|
+
/**
|
|
95
|
+
* Copy a local template into the configured registry.
|
|
96
|
+
*
|
|
97
|
+
* @param ctx - Template context.
|
|
98
|
+
* @param templateId - The caller's identifier.
|
|
99
|
+
*/
|
|
100
|
+
export declare function publishTemplate(ctx: TemplateContext, templateId: string): Promise<void>;
|
|
101
|
+
/**
|
|
102
|
+
* Pull a template from the configured registry onto this host.
|
|
103
|
+
*
|
|
104
|
+
* @param ctx - Template context.
|
|
105
|
+
* @param templateId - The caller's identifier.
|
|
106
|
+
* @returns The now-local template, or `null` when the registry does not have it.
|
|
107
|
+
*/
|
|
108
|
+
export declare function fetchTemplate(ctx: TemplateContext, templateId: string): Promise<SandboxTemplate | null>;
|
|
109
|
+
//# sourceMappingURL=templates.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"templates.d.ts","sourceRoot":"","sources":["../src/templates.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,OAAO,KAAK,EACV,qBAAqB,EACrB,oBAAoB,EACpB,eAAe,EAChB,MAAM,4BAA4B,CAAA;AAEnC,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,aAAa,EAClB,KAAK,YAAY,EAClB,MAAM,cAAc,CAAA;AAErB,4DAA4D;AAC5D,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,aAAa,CAAA;IACtB,QAAQ,EAAE,cAAc,CAAA;IACxB,MAAM,EAAE,YAAY,CAAA;IACpB,yEAAyE;IACzE,SAAS,EAAE,MAAM,CAAA;IACjB,6EAA6E;IAC7E,UAAU,EAAE,MAAM,CAAA;IAClB,+EAA+E;IAC/E,QAAQ,EAAE,MAAM,CAAA;IAChB,8DAA8D;IAC9D,YAAY,EAAE,MAAM,CAAA;IACpB,8DAA8D;IAC9D,WAAW,EAAE,MAAM,CAAA;IACnB,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAA;IAChE,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAA;CAClE;AAaD;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CASzD;AAiID;;;;;;GAMG;AACH,wBAAsB,cAAc,CAClC,GAAG,EAAE,eAAe,EACpB,OAAO,EAAE,qBAAqB,GAC7B,OAAO,CAAC,eAAe,CAAC,CAoF1B;AAED;;;;;;GAMG;AACH,wBAAsB,WAAW,CAC/B,GAAG,EAAE,eAAe,EACpB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,CAiCjC;AAED;;;;;;GAMG;AACH,wBAAsB,aAAa,CACjC,GAAG,EAAE,eAAe,EACpB,OAAO,CAAC,EAAE,oBAAoB,GAC7B,OAAO,CAAC,eAAe,EAAE,CAAC,CAmC5B;AAED;;;;;GAKG;AACH,wBAAsB,cAAc,CAAC,GAAG,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAc5F;AAED;;;;;GAKG;AACH,wBAAsB,eAAe,CAAC,GAAG,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAoB7F;AAED;;;;;;GAMG;AACH,wBAAsB,aAAa,CACjC,GAAG,EAAE,eAAe,EACpB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,CA6BjC"}
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sandbox templates on Docker — capture a sandbox's filesystem, boot new
|
|
3
|
+
* sandboxes from it, share it between hosts, and reclaim it on a budget.
|
|
4
|
+
*
|
|
5
|
+
* ## Why the capture is not just `commit`
|
|
6
|
+
*
|
|
7
|
+
* `POST /commit` snapshots a container's WRITABLE LAYER, and a mounted volume is
|
|
8
|
+
* not part of it. A sandbox keeps the project on a volume — that is the whole
|
|
9
|
+
* point of the volume — so committing a sandbox directly produces an image that
|
|
10
|
+
* builds, tags, boots, and contains no project. Everything succeeds; the template
|
|
11
|
+
* is empty. So the capture copies the caller-named paths into a throwaway
|
|
12
|
+
* container that has NO volume, and commits that instead.
|
|
13
|
+
*
|
|
14
|
+
* The paths are named by the CALLER (`capturePaths`). They are not discovered,
|
|
15
|
+
* because the two answers a discovery would produce — "this sandbox has no
|
|
16
|
+
* project files" and "I looked in the wrong place" — are indistinguishable, and
|
|
17
|
+
* the wrong one silently ships an empty template that later boots as a working
|
|
18
|
+
* sandbox with an empty workspace.
|
|
19
|
+
*
|
|
20
|
+
* ## The throwaway container is a tenant boundary
|
|
21
|
+
*
|
|
22
|
+
* It extracts a TENANT-AUTHORED archive and is then committed into a template
|
|
23
|
+
* that OTHER tenants boot. So it runs with no network, no capabilities beyond the
|
|
24
|
+
* three the extraction needs, a memory cap and a process cap — and setuid/setgid
|
|
25
|
+
* bits are stripped after extraction, so a tenant cannot bake a privilege
|
|
26
|
+
* escalation into an image somebody else boots. A previous unhardened version of
|
|
27
|
+
* this step ran on the default bridge, outside every firewall rule, holding
|
|
28
|
+
* Docker's full default capability set.
|
|
29
|
+
*
|
|
30
|
+
* @module
|
|
31
|
+
*/
|
|
32
|
+
import { assertNoStreamError, } from './request.js';
|
|
33
|
+
/**
|
|
34
|
+
* Legal Docker tag component, which is also a legal container-name component.
|
|
35
|
+
* A template id becomes both, so it must satisfy the stricter of the two.
|
|
36
|
+
*/
|
|
37
|
+
const VALID_TEMPLATE_ID = /^[A-Za-z0-9_][A-Za-z0-9._-]{0,110}$/;
|
|
38
|
+
/** Timeouts sized for operations that move gigabytes rather than metadata. */
|
|
39
|
+
const COMMIT_TIMEOUT_MS = 300_000;
|
|
40
|
+
const TRANSFER_TIMEOUT_MS = 600_000;
|
|
41
|
+
const REGISTRY_TIMEOUT_MS = 300_000;
|
|
42
|
+
/**
|
|
43
|
+
* Validate a caller-supplied template id.
|
|
44
|
+
*
|
|
45
|
+
* REJECTS rather than sanitizes. Sanitizing would map two distinct ids onto one
|
|
46
|
+
* tag, and the second commit would silently overwrite the first — a project
|
|
47
|
+
* restoring its own template and receiving somebody else's filesystem.
|
|
48
|
+
*
|
|
49
|
+
* @param templateId - The caller's identifier.
|
|
50
|
+
* @throws {Error} When the id cannot be used as a Docker tag.
|
|
51
|
+
*/
|
|
52
|
+
export function assertTemplateId(templateId) {
|
|
53
|
+
if (!VALID_TEMPLATE_ID.test(templateId)) {
|
|
54
|
+
throw new Error(`Invalid template id ${JSON.stringify(templateId.slice(0, 32))}: must match ` +
|
|
55
|
+
`${String(VALID_TEMPLATE_ID)} so it can be used verbatim as a Docker tag. ` +
|
|
56
|
+
'Ids are rejected rather than sanitized, because sanitizing would let two ' +
|
|
57
|
+
'distinct ids collide onto one template.');
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Absolute, traversal-free path inside a sandbox. Interpolated into a `sh -c`
|
|
62
|
+
* argument and into a URL query, so both shapes are checked here.
|
|
63
|
+
*
|
|
64
|
+
* @param path - Candidate path.
|
|
65
|
+
* @throws {Error} When the path is not a safe absolute path.
|
|
66
|
+
*/
|
|
67
|
+
function assertCapturePath(path) {
|
|
68
|
+
if (!/^\/[^\0'"`$\\\n]*$/.test(path) || path.includes('..')) {
|
|
69
|
+
throw new Error(`Invalid capture path ${JSON.stringify(path.slice(0, 64))}: must be absolute`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/** The provider-native reference for a template id. */
|
|
73
|
+
function templateRef(ctx, templateId) {
|
|
74
|
+
return `${ctx.repository}:${templateId}`;
|
|
75
|
+
}
|
|
76
|
+
/** The remote reference for a template id, in the configured registry. */
|
|
77
|
+
function remoteRef(ctx, templateId) {
|
|
78
|
+
return `${ctx.registry}/${ctx.repository}:${templateId}`;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Require a configured shared store, naming the setting that turns it on.
|
|
82
|
+
*
|
|
83
|
+
* A Docker daemon can always talk to a registry, so the capability is present;
|
|
84
|
+
* what is missing is the address. Throwing distinguishes that from "this
|
|
85
|
+
* template is not in the shared store", which is what a `null` return would say.
|
|
86
|
+
*
|
|
87
|
+
* @param ctx - Template context.
|
|
88
|
+
* @throws {Error} When no registry is configured.
|
|
89
|
+
*/
|
|
90
|
+
function assertRegistry(ctx) {
|
|
91
|
+
if (!ctx.registry) {
|
|
92
|
+
throw new Error('No shared template store is configured for the Docker sandbox provider. ' +
|
|
93
|
+
'Set `templateRegistry` (or SANDBOX_TEMPLATE_REGISTRY) to a Docker registry ' +
|
|
94
|
+
'host to publish and fetch templates across hosts.');
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/** The parent directory an archive of `path` must be extracted into. */
|
|
98
|
+
function parentOf(path) {
|
|
99
|
+
const trimmed = path.endsWith('/') && path.length > 1 ? path.slice(0, -1) : path;
|
|
100
|
+
const index = trimmed.lastIndexOf('/');
|
|
101
|
+
return index <= 0 ? '/' : trimmed.slice(0, index);
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Run a command in a container and fail loudly if it did not run.
|
|
105
|
+
*
|
|
106
|
+
* Best-effort would be wrong here: every exec in the capture path is a
|
|
107
|
+
* correctness step (make the destination exist, strip setuid bits), and a
|
|
108
|
+
* capture that skipped one produces a template that looks fine.
|
|
109
|
+
*
|
|
110
|
+
* @param ctx - Template context.
|
|
111
|
+
* @param containerId - Container to run in.
|
|
112
|
+
* @param command - Shell command.
|
|
113
|
+
*/
|
|
114
|
+
async function execInContainer(ctx, containerId, command) {
|
|
115
|
+
const created = (await ctx.request(`/containers/${containerId}/exec`, 'POST', {
|
|
116
|
+
Cmd: ['sh', '-c', command],
|
|
117
|
+
AttachStdout: true,
|
|
118
|
+
AttachStderr: true,
|
|
119
|
+
}));
|
|
120
|
+
await ctx.request(`/exec/${created.Id}/start`, 'POST', { Detach: false, Tty: false });
|
|
121
|
+
const inspected = (await ctx.request(`/exec/${created.Id}/json`));
|
|
122
|
+
if (inspected?.ExitCode !== 0) {
|
|
123
|
+
throw new Error(`Command failed inside capture container (exit ${String(inspected?.ExitCode)}): ${command}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/** Remove a container by id or name, tolerating one that is already gone. */
|
|
127
|
+
async function removeContainer(ctx, ref) {
|
|
128
|
+
await ctx.request(`/containers/${ref}?force=true`, 'DELETE').catch((error) => {
|
|
129
|
+
ctx.debug?.('Capture container removal failed (it may already be gone)', { ref, error });
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Every image reference currently backing a container, running or not.
|
|
134
|
+
*
|
|
135
|
+
* A stopped container still owns its filesystem, so it counts. The fallback on
|
|
136
|
+
* failure is deliberately `null` — the caller turns that into `inUse: true`,
|
|
137
|
+
* because a wrong `false` here authorizes deleting the image a live sandbox
|
|
138
|
+
* boots from.
|
|
139
|
+
*
|
|
140
|
+
* @param ctx - Template context.
|
|
141
|
+
* @returns The set of in-use image refs and ids, or `null` when it could not be read.
|
|
142
|
+
*/
|
|
143
|
+
async function inUseImageRefs(ctx) {
|
|
144
|
+
try {
|
|
145
|
+
const containers = (await ctx.request('/containers/json?all=true'));
|
|
146
|
+
const refs = new Set();
|
|
147
|
+
for (const container of Array.isArray(containers) ? containers : []) {
|
|
148
|
+
if (container.Image)
|
|
149
|
+
refs.add(container.Image);
|
|
150
|
+
if (container.ImageID)
|
|
151
|
+
refs.add(container.ImageID.replace(/^sha256:/, ''));
|
|
152
|
+
}
|
|
153
|
+
return refs;
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
ctx.warn?.('Could not list containers to determine template usage — assuming in use', { error });
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
/** Whether `imageId`/`ref` appears in the in-use set. `null` means "assume yes". */
|
|
161
|
+
function isInUse(inUse, ref, imageId) {
|
|
162
|
+
if (inUse === null)
|
|
163
|
+
return true;
|
|
164
|
+
if (inUse.has(ref))
|
|
165
|
+
return true;
|
|
166
|
+
const bare = imageId.replace(/^sha256:/, '');
|
|
167
|
+
for (const used of inUse) {
|
|
168
|
+
if (used === bare || bare.startsWith(used) || used.startsWith(bare))
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Capture a sandbox's filesystem into a reusable template.
|
|
175
|
+
*
|
|
176
|
+
* @param ctx - Template context.
|
|
177
|
+
* @param options - What to capture and what to call it.
|
|
178
|
+
* @returns The template as it now exists.
|
|
179
|
+
*/
|
|
180
|
+
export async function commitTemplate(ctx, options) {
|
|
181
|
+
assertTemplateId(options.templateId);
|
|
182
|
+
const capturePaths = options.capturePaths ?? [];
|
|
183
|
+
for (const path of capturePaths)
|
|
184
|
+
assertCapturePath(path);
|
|
185
|
+
const ref = templateRef(ctx, options.templateId);
|
|
186
|
+
const captureName = `mol-capture-${options.templateId}`;
|
|
187
|
+
// A previous run that died before its cleanup leaves this name taken, and the
|
|
188
|
+
// create below would fail on the conflict rather than on anything real.
|
|
189
|
+
await removeContainer(ctx, captureName);
|
|
190
|
+
const created = (await ctx.request(`/containers/create?name=${captureName}`, 'POST', {
|
|
191
|
+
Image: ctx.baseImage,
|
|
192
|
+
Cmd: ['tail', '-f', '/dev/null'],
|
|
193
|
+
Labels: {
|
|
194
|
+
[`${ctx.labelPrefix}.template.id`]: options.templateId,
|
|
195
|
+
...(options.label ? { [`${ctx.labelPrefix}.template.label`]: options.label } : {}),
|
|
196
|
+
},
|
|
197
|
+
HostConfig: {
|
|
198
|
+
// It is driven entirely over the Docker socket by this process. It needs
|
|
199
|
+
// no network, and having one puts it outside the sandbox firewall's
|
|
200
|
+
// subnet match while it handles tenant bytes.
|
|
201
|
+
NetworkMode: 'none',
|
|
202
|
+
AutoRemove: false,
|
|
203
|
+
RestartPolicy: { Name: '' },
|
|
204
|
+
CapDrop: ['ALL'],
|
|
205
|
+
CapAdd: ['CHOWN', 'SETGID', 'SETUID'],
|
|
206
|
+
SecurityOpt: ['no-new-privileges'],
|
|
207
|
+
Memory: 2 * 1024 * 1024 * 1024,
|
|
208
|
+
MemorySwap: 2 * 1024 * 1024 * 1024,
|
|
209
|
+
PidsLimit: 256,
|
|
210
|
+
},
|
|
211
|
+
}));
|
|
212
|
+
try {
|
|
213
|
+
await ctx.request(`/containers/${created.Id}/start`, 'POST');
|
|
214
|
+
for (const path of capturePaths) {
|
|
215
|
+
const parent = parentOf(path);
|
|
216
|
+
await execInContainer(ctx, created.Id, `mkdir -p '${parent}'`);
|
|
217
|
+
// Streamed straight through: a project tree is gigabytes, and buffering it
|
|
218
|
+
// in this process would trade a slow capture for a dead API.
|
|
219
|
+
const archive = await ctx.download(`/containers/${options.sandboxId}/archive?path=${encodeURIComponent(path)}`, TRANSFER_TIMEOUT_MS);
|
|
220
|
+
await ctx.upload(`/containers/${created.Id}/archive?path=${encodeURIComponent(parent)}`, archive, TRANSFER_TIMEOUT_MS);
|
|
221
|
+
// Strip setuid/setgid from the extracted tree. Docker's archive extraction
|
|
222
|
+
// honors the mode bits in the archive, and the archive came from a
|
|
223
|
+
// container the tenant controls — so without this a tenant can plant a
|
|
224
|
+
// setuid-root binary in an image OTHER tenants boot. Scoped by `-perm` so
|
|
225
|
+
// it touches only the handful of files that have those bits, not the whole
|
|
226
|
+
// tree.
|
|
227
|
+
await execInContainer(ctx, created.Id, `find '${path}' -type f -perm /6000 -exec chmod a-s {} + 2>/dev/null; exit 0`);
|
|
228
|
+
}
|
|
229
|
+
const params = new URLSearchParams({
|
|
230
|
+
container: created.Id,
|
|
231
|
+
repo: ctx.repository,
|
|
232
|
+
tag: options.templateId,
|
|
233
|
+
pause: 'false',
|
|
234
|
+
});
|
|
235
|
+
if (options.label)
|
|
236
|
+
params.set('comment', options.label);
|
|
237
|
+
await ctx.request(`/commit?${params.toString()}`, 'POST', undefined, COMMIT_TIMEOUT_MS);
|
|
238
|
+
}
|
|
239
|
+
finally {
|
|
240
|
+
await removeContainer(ctx, created.Id);
|
|
241
|
+
}
|
|
242
|
+
const template = await getTemplate(ctx, options.templateId);
|
|
243
|
+
if (!template) {
|
|
244
|
+
throw new Error(`Template "${options.templateId}" was committed as ${ref} but cannot be read back`);
|
|
245
|
+
}
|
|
246
|
+
return template;
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Read one template by the caller's identifier.
|
|
250
|
+
*
|
|
251
|
+
* @param ctx - Template context.
|
|
252
|
+
* @param templateId - The caller's identifier.
|
|
253
|
+
* @returns The template, or `null` when no image carries that tag.
|
|
254
|
+
*/
|
|
255
|
+
export async function getTemplate(ctx, templateId) {
|
|
256
|
+
assertTemplateId(templateId);
|
|
257
|
+
const ref = templateRef(ctx, templateId);
|
|
258
|
+
let image;
|
|
259
|
+
try {
|
|
260
|
+
image = (await ctx.request(`/images/${encodeURIComponent(ref)}/json`));
|
|
261
|
+
}
|
|
262
|
+
catch (error) {
|
|
263
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
264
|
+
// A 404 is the answer "no such template". Anything else is a failure to
|
|
265
|
+
// look, and reporting it as absence would turn a transient daemon hiccup
|
|
266
|
+
// into a full cold rebuild on every boot.
|
|
267
|
+
if (/\b404\b|no such image/i.test(message))
|
|
268
|
+
return null;
|
|
269
|
+
throw error;
|
|
270
|
+
}
|
|
271
|
+
const inUse = await inUseImageRefs(ctx);
|
|
272
|
+
const labels = image.Config?.Labels ?? {};
|
|
273
|
+
return {
|
|
274
|
+
id: labels[`${ctx.labelPrefix}.template.id`] ?? templateId,
|
|
275
|
+
ref,
|
|
276
|
+
createdAt: image.Created ?? '',
|
|
277
|
+
sizeBytes: typeof image.Size === 'number' ? image.Size : null,
|
|
278
|
+
...(labels[`${ctx.labelPrefix}.template.label`]
|
|
279
|
+
? { label: labels[`${ctx.labelPrefix}.template.label`] }
|
|
280
|
+
: {}),
|
|
281
|
+
inUse: isInUse(inUse, ref, image.Id ?? ''),
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Enumerate templates so the caller can apply its retention policy.
|
|
286
|
+
*
|
|
287
|
+
* @param ctx - Template context.
|
|
288
|
+
* @param options - Narrowing by id prefix.
|
|
289
|
+
* @returns Every matching template.
|
|
290
|
+
*/
|
|
291
|
+
export async function listTemplates(ctx, options) {
|
|
292
|
+
const filters = encodeURIComponent(JSON.stringify({ reference: [`${ctx.repository}:*`] }));
|
|
293
|
+
// No try/catch: a failed listing must reach the caller. Returning `[]` would
|
|
294
|
+
// tell a caller whose next step is deletion that nothing is over budget, and
|
|
295
|
+
// it would believe it.
|
|
296
|
+
const images = (await ctx.request(`/images/json?filters=${filters}`));
|
|
297
|
+
const inUse = await inUseImageRefs(ctx);
|
|
298
|
+
const templates = [];
|
|
299
|
+
for (const image of Array.isArray(images) ? images : []) {
|
|
300
|
+
for (const tag of image.RepoTags ?? []) {
|
|
301
|
+
if (!tag.startsWith(`${ctx.repository}:`))
|
|
302
|
+
continue;
|
|
303
|
+
const labels = image.Labels ?? {};
|
|
304
|
+
const id = labels[`${ctx.labelPrefix}.template.id`] ?? tag.slice(ctx.repository.length + 1);
|
|
305
|
+
if (options?.idPrefix && !id.startsWith(options.idPrefix))
|
|
306
|
+
continue;
|
|
307
|
+
templates.push({
|
|
308
|
+
id,
|
|
309
|
+
ref: tag,
|
|
310
|
+
createdAt: typeof image.Created === 'number' ? new Date(image.Created * 1000).toISOString() : '',
|
|
311
|
+
sizeBytes: typeof image.Size === 'number' ? image.Size : null,
|
|
312
|
+
...(labels[`${ctx.labelPrefix}.template.label`]
|
|
313
|
+
? { label: labels[`${ctx.labelPrefix}.template.label`] }
|
|
314
|
+
: {}),
|
|
315
|
+
inUse: isInUse(inUse, tag, image.Id ?? ''),
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return templates;
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Delete a template, refusing while anything still boots from it.
|
|
323
|
+
*
|
|
324
|
+
* @param ctx - Template context.
|
|
325
|
+
* @param templateId - The caller's identifier.
|
|
326
|
+
*/
|
|
327
|
+
export async function removeTemplate(ctx, templateId) {
|
|
328
|
+
const template = await getTemplate(ctx, templateId);
|
|
329
|
+
// Already gone is a success — callers reconcile, and reconciliation re-runs.
|
|
330
|
+
if (!template)
|
|
331
|
+
return;
|
|
332
|
+
if (template.inUse) {
|
|
333
|
+
throw new Error(`Refusing to remove template "${templateId}" (${template.ref}): a sandbox is still ` +
|
|
334
|
+
'backed by it, and removing it would destroy that sandbox.');
|
|
335
|
+
}
|
|
336
|
+
// Deliberately no `force`: the daemon independently refuses to remove an image
|
|
337
|
+
// a container references, which is a second net behind the check above for the
|
|
338
|
+
// sandbox that was created in between.
|
|
339
|
+
await ctx.request(`/images/${encodeURIComponent(template.ref)}`, 'DELETE');
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Copy a local template into the configured registry.
|
|
343
|
+
*
|
|
344
|
+
* @param ctx - Template context.
|
|
345
|
+
* @param templateId - The caller's identifier.
|
|
346
|
+
*/
|
|
347
|
+
export async function publishTemplate(ctx, templateId) {
|
|
348
|
+
assertRegistry(ctx);
|
|
349
|
+
const template = await getTemplate(ctx, templateId);
|
|
350
|
+
if (!template)
|
|
351
|
+
throw new Error(`Cannot publish template "${templateId}": it does not exist locally`);
|
|
352
|
+
await ctx.request(`/images/${encodeURIComponent(template.ref)}/tag?repo=${encodeURIComponent(`${ctx.registry}/${ctx.repository}`)}&tag=${encodeURIComponent(templateId)}`, 'POST');
|
|
353
|
+
const pushed = await ctx.request(`/images/${encodeURIComponent(`${ctx.registry}/${ctx.repository}`)}/push?tag=${encodeURIComponent(templateId)}`, 'POST', undefined, REGISTRY_TIMEOUT_MS, { 'X-Registry-Auth': ctx.registryAuth });
|
|
354
|
+
assertNoStreamError(pushed, `push of ${remoteRef(ctx, templateId)}`);
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Pull a template from the configured registry onto this host.
|
|
358
|
+
*
|
|
359
|
+
* @param ctx - Template context.
|
|
360
|
+
* @param templateId - The caller's identifier.
|
|
361
|
+
* @returns The now-local template, or `null` when the registry does not have it.
|
|
362
|
+
*/
|
|
363
|
+
export async function fetchTemplate(ctx, templateId) {
|
|
364
|
+
assertRegistry(ctx);
|
|
365
|
+
const remote = `${ctx.registry}/${ctx.repository}`;
|
|
366
|
+
try {
|
|
367
|
+
const pulled = await ctx.request(`/images/create?fromImage=${encodeURIComponent(remote)}&tag=${encodeURIComponent(templateId)}`, 'POST', undefined, REGISTRY_TIMEOUT_MS, { 'X-Registry-Auth': ctx.registryAuth });
|
|
368
|
+
assertNoStreamError(pulled, `pull of ${remoteRef(ctx, templateId)}`);
|
|
369
|
+
}
|
|
370
|
+
catch (error) {
|
|
371
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
372
|
+
// "The registry does not have it" is an answer, and the caller's next move is
|
|
373
|
+
// to build the template. Anything else is a failure to look, and must not be
|
|
374
|
+
// reported as absence.
|
|
375
|
+
if (/manifest unknown|not found|\b404\b/i.test(message))
|
|
376
|
+
return null;
|
|
377
|
+
throw error;
|
|
378
|
+
}
|
|
379
|
+
await ctx.request(`/images/${encodeURIComponent(`${remote}:${templateId}`)}/tag?repo=${encodeURIComponent(ctx.repository)}&tag=${encodeURIComponent(templateId)}`, 'POST');
|
|
380
|
+
return getTemplate(ctx, templateId);
|
|
381
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -42,6 +42,29 @@ export interface DockerConfig {
|
|
|
42
42
|
labelPrefix?: string;
|
|
43
43
|
/** Preview URL template. Use {port} placeholder. */
|
|
44
44
|
previewUrlTemplate?: string;
|
|
45
|
+
/**
|
|
46
|
+
* Docker repository holding sandbox TEMPLATES — one tag per template id.
|
|
47
|
+
* Defaults to `molecule-sandbox-template` (or `SANDBOX_TEMPLATE_REPOSITORY`).
|
|
48
|
+
*
|
|
49
|
+
* Deliberately NOT the base image's repository. Sharing one repository between
|
|
50
|
+
* the base image and every template makes `docker images <repo>` and any
|
|
51
|
+
* cleanup written against it operate on both, and the only thing standing
|
|
52
|
+
* between a template sweep and the base image is a tag-prefix convention.
|
|
53
|
+
*/
|
|
54
|
+
templateRepository?: string;
|
|
55
|
+
/**
|
|
56
|
+
* Registry host for sharing templates across hosts, e.g. `registry.example.com`
|
|
57
|
+
* (or `SANDBOX_TEMPLATE_REGISTRY`). When unset, `publishTemplate`/`fetchTemplate`
|
|
58
|
+
* throw with an actionable message: the daemon CAN talk to a registry, so the
|
|
59
|
+
* capability is present and only the address is missing.
|
|
60
|
+
*/
|
|
61
|
+
templateRegistry?: string;
|
|
62
|
+
/**
|
|
63
|
+
* Base64-encoded Docker `X-Registry-Auth` value for {@link DockerConfig.templateRegistry}
|
|
64
|
+
* (or `SANDBOX_TEMPLATE_REGISTRY_AUTH`). Defaults to an empty credential set,
|
|
65
|
+
* which is what an anonymous or already-logged-in daemon needs.
|
|
66
|
+
*/
|
|
67
|
+
templateRegistryAuth?: string;
|
|
45
68
|
}
|
|
46
69
|
/**
|
|
47
70
|
* Container Info interface.
|
|
@@ -65,5 +88,11 @@ export interface ProcessEnv {
|
|
|
65
88
|
DOCKER_HOST?: string;
|
|
66
89
|
/** Docker network sandbox containers attach to (default `molecule-sandbox`, ICC-off). Overridden by `config.network`. [C1-1] */
|
|
67
90
|
SANDBOX_DOCKER_NETWORK?: string;
|
|
91
|
+
/** Docker repository holding sandbox templates (default `molecule-sandbox-template`). Overridden by `config.templateRepository`. */
|
|
92
|
+
SANDBOX_TEMPLATE_REPOSITORY?: string;
|
|
93
|
+
/** Registry host for sharing templates across hosts. Overridden by `config.templateRegistry`. */
|
|
94
|
+
SANDBOX_TEMPLATE_REGISTRY?: string;
|
|
95
|
+
/** Base64 `X-Registry-Auth` value for `SANDBOX_TEMPLATE_REGISTRY`. Overridden by `config.templateRegistryAuth`. */
|
|
96
|
+
SANDBOX_TEMPLATE_REGISTRY_AUTH?: string;
|
|
68
97
|
}
|
|
69
98
|
//# sourceMappingURL=types.d.ts.map
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;;;;;;OAOG;IACH,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,sHAAsH;IACtH,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,0IAA0I;IAC1I,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,sCAAsC;IACtC,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,sCAAsC;IACtC,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,iDAAiD;IACjD,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,oDAAoD;IACpD,kBAAkB,CAAC,EAAE,MAAM,CAAA;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;;;;;;OAOG;IACH,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,sHAAsH;IACtH,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,0IAA0I;IAC1I,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,sCAAsC;IACtC,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,sCAAsC;IACtC,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,iDAAiD;IACjD,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,oDAAoD;IACpD,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B;;;;;;;;OAQG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B;;;;;OAKG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB;;;;OAIG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAA;CAC9B;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CAClD;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,kHAAkH;IAClH,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B,wJAAwJ;IACxJ,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,gIAAgI;IAChI,sBAAsB,CAAC,EAAE,MAAM,CAAA;IAC/B,oIAAoI;IACpI,2BAA2B,CAAC,EAAE,MAAM,CAAA;IACpC,iGAAiG;IACjG,yBAAyB,CAAC,EAAE,MAAM,CAAA;IAClC,mHAAmH;IACnH,8BAA8B,CAAC,EAAE,MAAM,CAAA;CACxC"}
|