@pipobscure/bundle 0.0.1
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/HISTORY.md +1924 -0
- package/README.md +623 -0
- package/bundle.run +0 -0
- package/dist/api.d.ts +147 -0
- package/dist/api.d.ts.map +1 -0
- package/dist/api.js +174 -0
- package/dist/api.js.map +1 -0
- package/dist/archive.d.ts +115 -0
- package/dist/archive.d.ts.map +1 -0
- package/dist/archive.js +188 -0
- package/dist/archive.js.map +1 -0
- package/dist/audit.d.ts +78 -0
- package/dist/audit.d.ts.map +1 -0
- package/dist/audit.js +119 -0
- package/dist/audit.js.map +1 -0
- package/dist/cli.d.ts +23 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +555 -0
- package/dist/cli.js.map +1 -0
- package/dist/files.d.ts +53 -0
- package/dist/files.d.ts.map +1 -0
- package/dist/files.js +118 -0
- package/dist/files.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +35 -0
- package/dist/index.js.map +1 -0
- package/dist/launch.d.ts +97 -0
- package/dist/launch.d.ts.map +1 -0
- package/dist/launch.js +267 -0
- package/dist/launch.js.map +1 -0
- package/dist/main.d.ts +3 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/main.js +19 -0
- package/dist/main.js.map +1 -0
- package/dist/manifest.d.ts +139 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +504 -0
- package/dist/manifest.js.map +1 -0
- package/dist/oidc.d.ts +40 -0
- package/dist/oidc.d.ts.map +1 -0
- package/dist/oidc.js +320 -0
- package/dist/oidc.js.map +1 -0
- package/dist/preload.d.ts +14 -0
- package/dist/preload.d.ts.map +1 -0
- package/dist/preload.js +38 -0
- package/dist/preload.js.map +1 -0
- package/dist/provider.d.ts +83 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/provider.js +206 -0
- package/dist/provider.js.map +1 -0
- package/dist/record.d.ts +2 -0
- package/dist/record.d.ts.map +1 -0
- package/dist/record.js +23 -0
- package/dist/record.js.map +1 -0
- package/dist/recorder.d.ts +64 -0
- package/dist/recorder.d.ts.map +1 -0
- package/dist/recorder.js +111 -0
- package/dist/recorder.js.map +1 -0
- package/dist/register.d.ts +2 -0
- package/dist/register.d.ts.map +1 -0
- package/dist/register.js +28 -0
- package/dist/register.js.map +1 -0
- package/dist/sea.d.ts +97 -0
- package/dist/sea.d.ts.map +1 -0
- package/dist/sea.js +220 -0
- package/dist/sea.js.map +1 -0
- package/dist/sigstore.d.ts +112 -0
- package/dist/sigstore.d.ts.map +1 -0
- package/dist/sigstore.js +385 -0
- package/dist/sigstore.js.map +1 -0
- package/dist/skill.d.ts +36 -0
- package/dist/skill.d.ts.map +1 -0
- package/dist/skill.js +108 -0
- package/dist/skill.js.map +1 -0
- package/package.json +84 -0
- package/shell-base +2 -0
- package/skills/audit-bundle/SKILL.md +271 -0
- package/src/api.ts +293 -0
- package/src/archive.ts +312 -0
- package/src/audit.ts +206 -0
- package/src/cli.ts +575 -0
- package/src/files.ts +156 -0
- package/src/index.ts +114 -0
- package/src/launch.ts +336 -0
- package/src/main.ts +20 -0
- package/src/manifest.ts +615 -0
- package/src/oidc.ts +372 -0
- package/src/preload.ts +40 -0
- package/src/provider.ts +270 -0
- package/src/record.ts +25 -0
- package/src/recorder.ts +166 -0
- package/src/register.ts +30 -0
- package/src/sea.ts +341 -0
- package/src/sigstore.ts +492 -0
- package/src/skill.ts +132 -0
- package/src/types/node-vfs.d.ts +90 -0
- package/src/types/node-zip.d.ts +85 -0
package/dist/provider.js
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import * as VFS from 'node:vfs';
|
|
2
|
+
import * as ZLIB from 'node:zlib';
|
|
3
|
+
import * as CRYPTO from 'node:crypto';
|
|
4
|
+
import * as PATH from 'node:path';
|
|
5
|
+
import * as FS from 'node:fs';
|
|
6
|
+
import { AUTHORITY, signatureOf, verifySync } from './manifest.js';
|
|
7
|
+
// A `node:vfs` file provider for signed archives — `.bundle` files — layered on
|
|
8
|
+
// the built-in `ZipProvider`. It is what turns "this archive is signed" into
|
|
9
|
+
// "this archive is what runs":
|
|
10
|
+
//
|
|
11
|
+
// * At mount time (`open()` below) the whole-file hash is recomputed, the
|
|
12
|
+
// signature over it is checked against the leaf certificate in
|
|
13
|
+
// `AUTHORITY.PEM`, and that chain is anchored in the trust store. An
|
|
14
|
+
// archive that fails any of those never becomes a filesystem at all.
|
|
15
|
+
//
|
|
16
|
+
// * At *fetch* time every member is hashed as it is read and compared with
|
|
17
|
+
// the digest recorded for it in the archive that was verified at mount.
|
|
18
|
+
// The mount-time hash already covers every member's bytes, but it covers
|
|
19
|
+
// them *as they were when the file was hashed* — a `ZipFile` reads members
|
|
20
|
+
// lazily from an open fd, so anything that rewrites the file underneath a
|
|
21
|
+
// running program would otherwise be served unchecked. Verified content is
|
|
22
|
+
// kept (members are an application's own files, not the runtime), so each
|
|
23
|
+
// member is read and hashed at most once and what later reads see is the
|
|
24
|
+
// copy that was verified, not a fresh read of the file.
|
|
25
|
+
//
|
|
26
|
+
// Registering this with `vfs.registerProvider()` puts it ahead of the built-in
|
|
27
|
+
// ZIP provider, so `--vfs-load` hands it the source first. It claims files by
|
|
28
|
+
// extension (`.bundle`) *and* by content — anything carrying our signature
|
|
29
|
+
// marker — so renaming a signed archive cannot quietly downgrade it to the
|
|
30
|
+
// unverified built-in provider.
|
|
31
|
+
export const EXTENSION = '.bundle';
|
|
32
|
+
const READ_FLAGS = FS.constants.O_WRONLY | FS.constants.O_RDWR | FS.constants.O_CREAT |
|
|
33
|
+
FS.constants.O_TRUNC | FS.constants.O_APPEND | FS.constants.O_EXCL;
|
|
34
|
+
/**
|
|
35
|
+
* Verify `path` and return a provider that serves it, or throw. `options` are
|
|
36
|
+
* the same as `register()`'s.
|
|
37
|
+
*/
|
|
38
|
+
export function open(path, options) {
|
|
39
|
+
const opts = settings(options);
|
|
40
|
+
const resolved = PATH.resolve(path);
|
|
41
|
+
// One ZipFile is opened here and handed to both the verification and the
|
|
42
|
+
// provider: the central directory is read once, and the digests the
|
|
43
|
+
// provider checks against are the ones the verified hash covered.
|
|
44
|
+
const archive = ZLIB.ZipFile.openSync(resolved);
|
|
45
|
+
try {
|
|
46
|
+
const res = verifySync(resolved, {
|
|
47
|
+
archive, deep: opts.deep, extraRoots: opts.extraRoots,
|
|
48
|
+
trustedRoot: opts.trustedRoot, identity: opts.identity, issuer: opts.issuer,
|
|
49
|
+
});
|
|
50
|
+
const acceptable = res.state === 'valid' || (opts.allowUntrusted && res.state === 'valid-untrusted');
|
|
51
|
+
if (!acceptable)
|
|
52
|
+
throw refusal(resolved, res);
|
|
53
|
+
return new BundleProvider(archive, { hashAlg: res.hashAlg, digests: res.digests });
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
archive.closeSync();
|
|
57
|
+
throw err;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Register this provider with `node:vfs` so the `--vfs-load` startup flag
|
|
62
|
+
* selects it for signed archives. Meant to be preloaded, before `--vfs-load`
|
|
63
|
+
* picks a provider:
|
|
64
|
+
*
|
|
65
|
+
* node --experimental-vfs -r @pipobscure/bundle/register --vfs-load=app.bundle
|
|
66
|
+
*/
|
|
67
|
+
export function register(options) {
|
|
68
|
+
const opts = settings(options);
|
|
69
|
+
VFS.registerProvider({
|
|
70
|
+
name: opts.name,
|
|
71
|
+
canHandle: (resolvedPath, stats) => stats.isFile() && claims(resolvedPath, opts),
|
|
72
|
+
create: (resolvedPath) => open(resolvedPath, opts),
|
|
73
|
+
});
|
|
74
|
+
return opts;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* A read-only `ZipProvider` that will not hand out a member's content until
|
|
78
|
+
* that content has been hashed and matched against the digest recorded for it.
|
|
79
|
+
*/
|
|
80
|
+
export class BundleProvider extends VFS.ZipProvider {
|
|
81
|
+
#archive;
|
|
82
|
+
#hashAlg;
|
|
83
|
+
#digests;
|
|
84
|
+
#verified;
|
|
85
|
+
/**
|
|
86
|
+
* `digests` is the member-name -> hex-digest map from a `verifySync()` of
|
|
87
|
+
* this very archive; `hashAlg` the algorithm those digests are in.
|
|
88
|
+
*/
|
|
89
|
+
constructor(archive, { hashAlg = 'sha256', digests = new Map() } = {}) {
|
|
90
|
+
super(archive);
|
|
91
|
+
this.#archive = archive;
|
|
92
|
+
this.#hashAlg = hashAlg;
|
|
93
|
+
this.#digests = digests;
|
|
94
|
+
this.#verified = new VFS.MemoryProvider();
|
|
95
|
+
// The manifest itself carries no digest of its own — it is what names
|
|
96
|
+
// the algorithms — but its bytes were read during the mount-time check,
|
|
97
|
+
// which the whole-file hash covered. Keep that copy so every path the
|
|
98
|
+
// mount serves comes from verified bytes.
|
|
99
|
+
if (archive.has(AUTHORITY)) {
|
|
100
|
+
const entry = archive.getSync(AUTHORITY);
|
|
101
|
+
this.#keep(AUTHORITY, entry.contentSync(), entry.mode);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// A signed archive is a fixed artifact: any write would invalidate the
|
|
105
|
+
// signature it was mounted on, so the mount is read-only regardless of how
|
|
106
|
+
// the underlying archive was opened.
|
|
107
|
+
get readonly() { return true; }
|
|
108
|
+
async open(path, flags, mode) {
|
|
109
|
+
const name = normalize(path);
|
|
110
|
+
if (reads(flags) && this.#check(name))
|
|
111
|
+
return this.#verified.open(path, flags, mode);
|
|
112
|
+
return super.open(path, flags, mode);
|
|
113
|
+
}
|
|
114
|
+
openSync(path, flags, mode) {
|
|
115
|
+
const name = normalize(path);
|
|
116
|
+
if (reads(flags) && this.#check(name))
|
|
117
|
+
return this.#verified.openSync(path, flags, mode);
|
|
118
|
+
return super.openSync(path, flags, mode);
|
|
119
|
+
}
|
|
120
|
+
// Whether `name` can be served from verified content, reading and checking
|
|
121
|
+
// it on first use. `false` means this is not a member with a recorded
|
|
122
|
+
// digest (a directory, or nothing at all) and the base provider should
|
|
123
|
+
// answer — including with the ENOENT/EISDIR it would normally raise.
|
|
124
|
+
#check(name) {
|
|
125
|
+
if (this.#verified.existsSync(`/${name}`))
|
|
126
|
+
return true;
|
|
127
|
+
const recorded = this.#digests.get(name);
|
|
128
|
+
if (recorded === undefined)
|
|
129
|
+
return false;
|
|
130
|
+
const entry = this.#archive.getSync(name);
|
|
131
|
+
const content = entry.contentSync();
|
|
132
|
+
const actual = CRYPTO.createHash(this.#hashAlg).update(content).digest('hex');
|
|
133
|
+
if (actual !== recorded) {
|
|
134
|
+
throw Object.assign(new Error(`bundle: content of '${name}' does not match its signed digest ` +
|
|
135
|
+
`(expected ${recorded}, got ${actual})`), { code: 'ERR_BUNDLE_INTEGRITY', member: name });
|
|
136
|
+
}
|
|
137
|
+
this.#keep(name, content, entry.mode);
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
#keep(name, content, mode) {
|
|
141
|
+
const dir = PATH.posix.dirname(`/${name}`);
|
|
142
|
+
if (dir !== '/' && dir !== '.')
|
|
143
|
+
this.#verified.mkdirSync(dir, { recursive: true });
|
|
144
|
+
this.#verified.writeFileSync(`/${name}`, content, { mode: mode || 0o444 });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// Whether this provider should back `resolvedPath` (already known to be a
|
|
148
|
+
// file): by name for the extensions it owns, and by content for anything
|
|
149
|
+
// carrying our signature marker.
|
|
150
|
+
function claims(resolvedPath, opts) {
|
|
151
|
+
const lower = resolvedPath.toLowerCase();
|
|
152
|
+
if (opts.extensions.some((ext) => lower.endsWith(ext)))
|
|
153
|
+
return true;
|
|
154
|
+
return opts.claimSigned && signatureOf(resolvedPath) !== null;
|
|
155
|
+
}
|
|
156
|
+
function refusal(path, res) {
|
|
157
|
+
const detail = res.subject ? `${res.reason} [${res.subject.replace(/\n/g, ', ')}]` : res.reason;
|
|
158
|
+
return Object.assign(new Error(`bundle: refusing to mount '${path}': ${res.state} — ${detail}`), { code: 'ERR_BUNDLE_UNTRUSTED', state: res.state });
|
|
159
|
+
}
|
|
160
|
+
// VFS paths are normalized to `/`-rooted POSIX paths; ZIP member names
|
|
161
|
+
// have no leading slash.
|
|
162
|
+
function normalize(path) {
|
|
163
|
+
return path.startsWith('/') ? path.slice(1) : path;
|
|
164
|
+
}
|
|
165
|
+
// Whether `flags` opens purely for reading — the only case verified content can
|
|
166
|
+
// answer. Anything else goes to the base provider, which refuses it (EROFS)
|
|
167
|
+
// because this provider is read-only. Mirrors how `node:fs` itself reads flags:
|
|
168
|
+
// a string means what it says, a number is a bitmask, anything else is 'r'.
|
|
169
|
+
function reads(flags) {
|
|
170
|
+
if (typeof flags === 'string')
|
|
171
|
+
return flags === 'r';
|
|
172
|
+
if (typeof flags !== 'number')
|
|
173
|
+
return true;
|
|
174
|
+
return (flags & READ_FLAGS) === 0;
|
|
175
|
+
}
|
|
176
|
+
const kSettings = Symbol('bundle.settings');
|
|
177
|
+
function settings(options = {}) {
|
|
178
|
+
if (options[kSettings])
|
|
179
|
+
return options;
|
|
180
|
+
const opts = options;
|
|
181
|
+
return {
|
|
182
|
+
[kSettings]: true,
|
|
183
|
+
name: opts.name ?? 'bundle',
|
|
184
|
+
extensions: (opts.extensions ?? [EXTENSION]).map((ext) => ext.toLowerCase()),
|
|
185
|
+
claimSigned: opts.claimSigned ?? true,
|
|
186
|
+
extraRoots: pems(opts.roots ?? envList('BUNDLE_ROOTS')),
|
|
187
|
+
allowUntrusted: opts.allowUntrusted ?? envFlag('BUNDLE_ALLOW_UNTRUSTED'),
|
|
188
|
+
deep: opts.deep ?? false,
|
|
189
|
+
identity: opts.identity ?? (process.env['BUNDLE_IDENTITY'] || undefined),
|
|
190
|
+
issuer: opts.issuer ?? (process.env['BUNDLE_ISSUER'] || undefined),
|
|
191
|
+
trustedRoot: opts.trustedRoot ?? (process.env['BUNDLE_SIGSTORE_ROOT'] || undefined),
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
// Accepts roots as PEM text or as paths to PEM files, so a caller can pass
|
|
195
|
+
// either and `BUNDLE_ROOTS` can name files.
|
|
196
|
+
function pems(roots) {
|
|
197
|
+
return (roots ?? []).map((root) => (root.includes('-----BEGIN') ? root : FS.readFileSync(root, 'utf-8')));
|
|
198
|
+
}
|
|
199
|
+
function envList(name) {
|
|
200
|
+
return (process.env[name] ?? '').split(PATH.delimiter).filter(Boolean);
|
|
201
|
+
}
|
|
202
|
+
function envFlag(name) {
|
|
203
|
+
const value = process.env[name];
|
|
204
|
+
return value !== undefined && value !== '' && value !== '0' && value !== 'false';
|
|
205
|
+
}
|
|
206
|
+
//# sourceMappingURL=provider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provider.js","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"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"}
|
package/dist/record.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"record.d.ts","sourceRoot":"","sources":["../src/record.ts"],"names":[],"mappings":""}
|
package/dist/record.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { preload, sibling } from './preload.js';
|
|
2
|
+
// The preload that puts manifest recording back where `--vfs-manifest` used to
|
|
3
|
+
// be: registering a recording provider is all it does, so `--vfs-load` finds it
|
|
4
|
+
// already in place when it picks a provider for a directory.
|
|
5
|
+
//
|
|
6
|
+
// BUNDLE_MANIFEST=app.manifest node --experimental-vfs \
|
|
7
|
+
// -r @pipobscure/bundle/record --vfs-load=./lib
|
|
8
|
+
//
|
|
9
|
+
// `--import @pipobscure/bundle/record` works just as well: mounting is deferred
|
|
10
|
+
// until both `-r` and `--import` preloads have run, and this module imports
|
|
11
|
+
// cleanly from ESM.
|
|
12
|
+
//
|
|
13
|
+
// Configure it through the environment — a preload takes no arguments:
|
|
14
|
+
//
|
|
15
|
+
// BUNDLE_MANIFEST where to write the file list; unset records nothing
|
|
16
|
+
//
|
|
17
|
+
// For anything more (recording an archive mount, several manifests, keeping an
|
|
18
|
+
// existing list), import `@pipobscure/bundle/recorder` and call `register()` or
|
|
19
|
+
// `recording()` from a preload module of your own.
|
|
20
|
+
preload(() => {
|
|
21
|
+
sibling(import.meta.filename, 'recorder').register();
|
|
22
|
+
});
|
|
23
|
+
//# sourceMappingURL=record.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"record.js","sourceRoot":"","sources":["../src/record.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAGhD,+EAA+E;AAC/E,gFAAgF;AAChF,6DAA6D;AAC7D,EAAE;AACF,2DAA2D;AAC3D,sDAAsD;AACtD,EAAE;AACF,gFAAgF;AAChF,4EAA4E;AAC5E,oBAAoB;AACpB,EAAE;AACF,uEAAuE;AACvE,EAAE;AACF,yEAAyE;AACzE,EAAE;AACF,+EAA+E;AAC/E,gFAAgF;AAChF,mDAAmD;AAEnD,OAAO,CAAC,GAAG,EAAE;IACT,OAAO,CAAkB,OAAO,IAAI,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC,QAAQ,EAAE,CAAC;AAC1E,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import * as VFS from 'node:vfs';
|
|
2
|
+
export interface ManifestOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Start a fresh list. Defaults to true only on the main thread: worker
|
|
5
|
+
* threads inherit `execArgv`, so this preload runs in them too, and a
|
|
6
|
+
* worker must append to what the main thread already wrote rather than
|
|
7
|
+
* wipe it mid-run.
|
|
8
|
+
*/
|
|
9
|
+
truncate?: boolean | undefined;
|
|
10
|
+
}
|
|
11
|
+
export interface RecorderOptions extends ManifestOptions {
|
|
12
|
+
/** Where to write the file list (default: the `BUNDLE_MANIFEST` variable). */
|
|
13
|
+
manifest?: string | Manifest | undefined;
|
|
14
|
+
/** Identifier reported in diagnostics (default: 'bundle-manifest'). */
|
|
15
|
+
name?: string | undefined;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The manifest file itself: which paths have been written, and the appending.
|
|
19
|
+
* Paths are appended as soon as they are read rather than buffered and flushed
|
|
20
|
+
* at exit, so nothing is lost if the process is killed instead of exiting.
|
|
21
|
+
*/
|
|
22
|
+
export declare class Manifest {
|
|
23
|
+
#private;
|
|
24
|
+
constructor(path: string, { truncate }?: ManifestOptions);
|
|
25
|
+
get path(): string;
|
|
26
|
+
/** Every path recorded so far, in the order they were first read. */
|
|
27
|
+
get paths(): string[];
|
|
28
|
+
/**
|
|
29
|
+
* Record one VFS path, once. Best effort: bookkeeping must never break the
|
|
30
|
+
* read that triggered it.
|
|
31
|
+
*/
|
|
32
|
+
record(path: string): void;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The shape `recording()` accepts and returns — any provider class. The rest
|
|
36
|
+
* parameter is what lets the wrapper be a mixin over a class whose constructor
|
|
37
|
+
* takes whatever it likes, and hand back one with the same signature.
|
|
38
|
+
*/
|
|
39
|
+
type ProviderClass = new (...args: any[]) => VFS.VirtualProvider;
|
|
40
|
+
/**
|
|
41
|
+
* Wrap a provider class so every read through it is recorded in `manifest`.
|
|
42
|
+
* Works over any `VirtualProvider` subclass — a `RealFSProvider` for a
|
|
43
|
+
* directory mount, a `ZipProvider` or the signed-archive provider from
|
|
44
|
+
* './provider.ts' for an archive one — and leaves the constructor signature of
|
|
45
|
+
* the class it wraps untouched:
|
|
46
|
+
*
|
|
47
|
+
* const Recording = recording(VFS.RealFSProvider, new Manifest('app.manifest'));
|
|
48
|
+
* const provider = new Recording('/path/to/directory');
|
|
49
|
+
*/
|
|
50
|
+
export declare function recording<T extends ProviderClass>(Base: T, manifest: Manifest): T;
|
|
51
|
+
/**
|
|
52
|
+
* Register a recording `RealFSProvider` so a directory loaded with
|
|
53
|
+
* `--vfs-load` is backed by it. Meant to be preloaded, before the mount is made:
|
|
54
|
+
*
|
|
55
|
+
* BUNDLE_MANIFEST=app.manifest node --experimental-vfs \
|
|
56
|
+
* -r @pipobscure/bundle/record --vfs-load=./lib
|
|
57
|
+
*
|
|
58
|
+
* With no manifest destination there is nothing to record, so nothing is
|
|
59
|
+
* registered and directory mounts are left exactly as node makes them; the
|
|
60
|
+
* return value is then null.
|
|
61
|
+
*/
|
|
62
|
+
export declare function register(options?: RecorderOptions): Manifest | null;
|
|
63
|
+
export {};
|
|
64
|
+
//# sourceMappingURL=recorder.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recorder.d.ts","sourceRoot":"","sources":["../src/recorder.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,GAAG,MAAM,UAAU,CAAC;AAgChC,MAAM,WAAW,eAAe;IAC5B;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CAClC;AAED,MAAM,WAAW,eAAgB,SAAQ,eAAe;IACpD,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,MAAM,GAAG,QAAQ,GAAG,SAAS,CAAC;IACzC,uEAAuE;IACvE,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC7B;AAED;;;;GAIG;AACH,qBAAa,QAAQ;;IAMjB,YAAY,IAAI,EAAE,MAAM,EAAE,EAAE,QAAuB,EAAE,GAAE,eAAoB,EAG1E;IAED,IAAI,IAAI,IAAI,MAAM,CAAuB;IAEzC,qEAAqE;IACrE,IAAI,KAAK,IAAI,MAAM,EAAE,CAA4B;IAEjD;;;OAGG;IACH,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CASzB;CACJ;AAED;;;;GAIG;AACH,KAAK,aAAa,GAAG,KAAK,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,GAAG,CAAC,eAAe,CAAC;AAEjE;;;;;;;;;GASG;AACH,wBAAgB,SAAS,CAAC,CAAC,SAAS,aAAa,EAAE,IAAI,EAAE,CAAC,EAAE,QAAQ,EAAE,QAAQ,GAAG,CAAC,CA0BjF;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,QAAQ,CAAC,OAAO,GAAE,eAAoB,GAAG,QAAQ,GAAG,IAAI,CAevE"}
|
package/dist/recorder.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import * as VFS from 'node:vfs';
|
|
2
|
+
import * as FS from 'node:fs';
|
|
3
|
+
import { isMainThread } from 'node:worker_threads';
|
|
4
|
+
/**
|
|
5
|
+
* The manifest file itself: which paths have been written, and the appending.
|
|
6
|
+
* Paths are appended as soon as they are read rather than buffered and flushed
|
|
7
|
+
* at exit, so nothing is lost if the process is killed instead of exiting.
|
|
8
|
+
*/
|
|
9
|
+
export class Manifest {
|
|
10
|
+
#path;
|
|
11
|
+
#seen = new Set();
|
|
12
|
+
// Concurrent appends are safe without any coordination — O_APPEND writes do
|
|
13
|
+
// not interleave — which is what lets worker threads share one manifest.
|
|
14
|
+
constructor(path, { truncate = isMainThread } = {}) {
|
|
15
|
+
this.#path = path;
|
|
16
|
+
if (truncate)
|
|
17
|
+
FS.writeFileSync(path, '');
|
|
18
|
+
}
|
|
19
|
+
get path() { return this.#path; }
|
|
20
|
+
/** Every path recorded so far, in the order they were first read. */
|
|
21
|
+
get paths() { return [...this.#seen]; }
|
|
22
|
+
/**
|
|
23
|
+
* Record one VFS path, once. Best effort: bookkeeping must never break the
|
|
24
|
+
* read that triggered it.
|
|
25
|
+
*/
|
|
26
|
+
record(path) {
|
|
27
|
+
try {
|
|
28
|
+
const relative = String(path).replace(/^\/+/, '');
|
|
29
|
+
if (relative === '' || this.#seen.has(relative))
|
|
30
|
+
return;
|
|
31
|
+
this.#seen.add(relative);
|
|
32
|
+
FS.appendFileSync(this.#path, `${relative}\n`);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// ignored
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Wrap a provider class so every read through it is recorded in `manifest`.
|
|
41
|
+
* Works over any `VirtualProvider` subclass — a `RealFSProvider` for a
|
|
42
|
+
* directory mount, a `ZipProvider` or the signed-archive provider from
|
|
43
|
+
* './provider.ts' for an archive one — and leaves the constructor signature of
|
|
44
|
+
* the class it wraps untouched:
|
|
45
|
+
*
|
|
46
|
+
* const Recording = recording(VFS.RealFSProvider, new Manifest('app.manifest'));
|
|
47
|
+
* const provider = new Recording('/path/to/directory');
|
|
48
|
+
*/
|
|
49
|
+
export function recording(Base, manifest) {
|
|
50
|
+
return class Recording extends Base {
|
|
51
|
+
async readFile(path, options) {
|
|
52
|
+
const content = await super.readFile(path, options);
|
|
53
|
+
manifest.record(path);
|
|
54
|
+
return content;
|
|
55
|
+
}
|
|
56
|
+
readFileSync(path, options) {
|
|
57
|
+
const content = super.readFileSync(path, options);
|
|
58
|
+
manifest.record(path);
|
|
59
|
+
return content;
|
|
60
|
+
}
|
|
61
|
+
async open(path, flags, mode) {
|
|
62
|
+
const handle = await super.open(path, flags, mode);
|
|
63
|
+
if (reads(flags))
|
|
64
|
+
manifest.record(path);
|
|
65
|
+
return handle;
|
|
66
|
+
}
|
|
67
|
+
openSync(path, flags, mode) {
|
|
68
|
+
const handle = super.openSync(path, flags, mode);
|
|
69
|
+
if (reads(flags))
|
|
70
|
+
manifest.record(path);
|
|
71
|
+
return handle;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Register a recording `RealFSProvider` so a directory loaded with
|
|
77
|
+
* `--vfs-load` is backed by it. Meant to be preloaded, before the mount is made:
|
|
78
|
+
*
|
|
79
|
+
* BUNDLE_MANIFEST=app.manifest node --experimental-vfs \
|
|
80
|
+
* -r @pipobscure/bundle/record --vfs-load=./lib
|
|
81
|
+
*
|
|
82
|
+
* With no manifest destination there is nothing to record, so nothing is
|
|
83
|
+
* registered and directory mounts are left exactly as node makes them; the
|
|
84
|
+
* return value is then null.
|
|
85
|
+
*/
|
|
86
|
+
export function register(options = {}) {
|
|
87
|
+
const configured = options.manifest ?? process.env['BUNDLE_MANIFEST'];
|
|
88
|
+
if (!configured)
|
|
89
|
+
return null;
|
|
90
|
+
const manifest = configured instanceof Manifest ? configured : new Manifest(configured, options);
|
|
91
|
+
const Recording = recording(VFS.RealFSProvider, manifest);
|
|
92
|
+
VFS.registerProvider({
|
|
93
|
+
name: options.name ?? 'bundle-manifest',
|
|
94
|
+
// Directory mounts only: an archive mount has its own provider (and,
|
|
95
|
+
// for a `.bundle`, one that verifies what it serves), and quietly
|
|
96
|
+
// replacing it here would be the wrong trade for a file list.
|
|
97
|
+
canHandle: (_resolvedPath, stats) => stats.isDirectory(),
|
|
98
|
+
create: (resolvedPath) => new Recording(resolvedPath),
|
|
99
|
+
});
|
|
100
|
+
return manifest;
|
|
101
|
+
}
|
|
102
|
+
// Whether `flags` opens for reading. Mirrors how `node:fs` reads them: a string
|
|
103
|
+
// means what it says, a number is a bitmask, anything else is 'r'.
|
|
104
|
+
function reads(flags) {
|
|
105
|
+
if (typeof flags === 'string')
|
|
106
|
+
return flags[0] === 'r' || flags.includes('+');
|
|
107
|
+
if (typeof flags !== 'number')
|
|
108
|
+
return true;
|
|
109
|
+
return (flags & (FS.constants.O_WRONLY | FS.constants.O_TRUNC)) === 0;
|
|
110
|
+
}
|
|
111
|
+
//# sourceMappingURL=recorder.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recorder.js","sourceRoot":"","sources":["../src/recorder.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"register.d.ts","sourceRoot":"","sources":["../src/register.ts"],"names":[],"mappings":""}
|
package/dist/register.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { preload, sibling } from './preload.js';
|
|
2
|
+
// The preload entry point: registering the signed-archive provider is all this
|
|
3
|
+
// does, so `--vfs-load` finds it already in place when it picks a provider for
|
|
4
|
+
// its source.
|
|
5
|
+
//
|
|
6
|
+
// node --experimental-vfs -r @pipobscure/bundle/register --vfs-load=app.bundle
|
|
7
|
+
//
|
|
8
|
+
// `--import @pipobscure/bundle/register` works just as well: mounting is
|
|
9
|
+
// deferred until both `-r` and `--import` preloads have run, and this module
|
|
10
|
+
// imports cleanly from ESM. Either way the point is that registering is a
|
|
11
|
+
// preload flag and nothing more — no module of your own to write.
|
|
12
|
+
//
|
|
13
|
+
// Configure it through the environment — a preload takes no arguments:
|
|
14
|
+
//
|
|
15
|
+
// BUNDLE_ROOTS extra trusted root certificates, as a
|
|
16
|
+
// path-delimiter-separated list of PEM files
|
|
17
|
+
// BUNDLE_ALLOW_UNTRUSTED accept a good signature whose chain is not anchored
|
|
18
|
+
// in the trust store
|
|
19
|
+
// BUNDLE_IDENTITY require this sigstore signing identity
|
|
20
|
+
// BUNDLE_ISSUER require this sigstore OIDC issuer
|
|
21
|
+
// BUNDLE_SIGSTORE_ROOT path to the sigstore trust root to check against
|
|
22
|
+
//
|
|
23
|
+
// For anything more, import `@pipobscure/bundle/provider` and call `register()`
|
|
24
|
+
// with options from a preload module of your own.
|
|
25
|
+
preload(() => {
|
|
26
|
+
sibling(import.meta.filename, 'provider').register();
|
|
27
|
+
});
|
|
28
|
+
//# sourceMappingURL=register.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"register.js","sourceRoot":"","sources":["../src/register.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAGhD,+EAA+E;AAC/E,+EAA+E;AAC/E,cAAc;AACd,EAAE;AACF,iFAAiF;AACjF,EAAE;AACF,yEAAyE;AACzE,6EAA6E;AAC7E,0EAA0E;AAC1E,kEAAkE;AAClE,EAAE;AACF,uEAAuE;AACvE,EAAE;AACF,kEAAkE;AAClE,uEAAuE;AACvE,gFAAgF;AAChF,+CAA+C;AAC/C,mEAAmE;AACnE,8DAA8D;AAC9D,6EAA6E;AAC7E,EAAE;AACF,gFAAgF;AAChF,kDAAkD;AAElD,OAAO,CAAC,GAAG,EAAE;IACT,OAAO,CAAkB,OAAO,IAAI,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC,QAAQ,EAAE,CAAC;AAC1E,CAAC,CAAC,CAAC"}
|
package/dist/sea.d.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { type Baked, type Mounted } from './launch.ts';
|
|
2
|
+
import { type BuildResult } from './api.ts';
|
|
3
|
+
import type { VerificationResult } from './manifest.ts';
|
|
4
|
+
import type { Signer } from './archive.ts';
|
|
5
|
+
export interface BootstrapOptions extends Baked {
|
|
6
|
+
/** The container to verify and mount (default: `process.execPath`). */
|
|
7
|
+
container?: string | undefined;
|
|
8
|
+
}
|
|
9
|
+
export type { Mounted };
|
|
10
|
+
/**
|
|
11
|
+
* Verify the running container and mount the archive appended to it. Returns
|
|
12
|
+
* where it landed; nothing has been executed out of it yet.
|
|
13
|
+
*/
|
|
14
|
+
export declare function mountSelf(options?: BootstrapOptions): Mounted;
|
|
15
|
+
/** Verify the running container, mount it, and run the application inside. */
|
|
16
|
+
export declare function bootstrap(options?: BootstrapOptions): Promise<void>;
|
|
17
|
+
/**
|
|
18
|
+
* Verify the running container without mounting it — for an application that
|
|
19
|
+
* wants to report on its own provenance ("signed by X at Y").
|
|
20
|
+
*/
|
|
21
|
+
export declare function verifySelf(options?: BootstrapOptions): VerificationResult;
|
|
22
|
+
/** The default flags the container runs itself with. */
|
|
23
|
+
export declare const SEA_EXEC_ARGV: string[];
|
|
24
|
+
export interface VerifierOptions {
|
|
25
|
+
/**
|
|
26
|
+
* Include the sigstore libraries, so the container can check a
|
|
27
|
+
* sigstore-signed archive rather than degrading it to untrusted
|
|
28
|
+
* (default: true). Costs roughly a megabyte of dependency tree.
|
|
29
|
+
*/
|
|
30
|
+
sigstore?: boolean | undefined;
|
|
31
|
+
}
|
|
32
|
+
export interface SeaBaseOptions extends VerifierOptions {
|
|
33
|
+
/** Where to write the base binary. */
|
|
34
|
+
output: string;
|
|
35
|
+
/**
|
|
36
|
+
* The node binary to embed (default: the running one). This is the runtime
|
|
37
|
+
* the finished container ships, so it decides which platform it runs on.
|
|
38
|
+
*/
|
|
39
|
+
node?: string | undefined;
|
|
40
|
+
/** Options baked into the stub and handed to `bootstrap()` at startup. */
|
|
41
|
+
bootstrap?: BootstrapOptions | undefined;
|
|
42
|
+
/** Runtime flags the container applies to itself. */
|
|
43
|
+
execArgv?: string[] | undefined;
|
|
44
|
+
/** A prebuilt verifier bundle to embed, instead of building one. */
|
|
45
|
+
verifier?: string | undefined;
|
|
46
|
+
/** Scratch directory for the generated stub and config. */
|
|
47
|
+
scratch?: string | undefined;
|
|
48
|
+
}
|
|
49
|
+
export interface SeaOptions extends SeaBaseOptions {
|
|
50
|
+
/** The application archive to append. Signed or not; it is re-signed here. */
|
|
51
|
+
app: string;
|
|
52
|
+
/** A base built earlier, instead of building one now. */
|
|
53
|
+
base?: string | undefined;
|
|
54
|
+
/** Sign the finished container. Without one it is built but left unsigned. */
|
|
55
|
+
signer?: Signer | undefined;
|
|
56
|
+
hashAlg?: string | undefined;
|
|
57
|
+
signAlg?: string | undefined;
|
|
58
|
+
/** Progress, one line at a time. */
|
|
59
|
+
log?: ((line: string) => void) | undefined;
|
|
60
|
+
}
|
|
61
|
+
export interface SeaBaseResult {
|
|
62
|
+
output: string;
|
|
63
|
+
size: number;
|
|
64
|
+
/** Members of the verifier bundle embedded in the blob. */
|
|
65
|
+
verifier: string[];
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Build the SEA base: a node runtime whose injected main mounts this package
|
|
69
|
+
* out of its own blob and hands over to `bootstrap()`. The result is a binary
|
|
70
|
+
* with no application in it yet — append one with `buildSea` or with
|
|
71
|
+
* `sign --prefix`.
|
|
72
|
+
*/
|
|
73
|
+
export declare function createSeaBase(options: SeaBaseOptions): Promise<SeaBaseResult>;
|
|
74
|
+
/**
|
|
75
|
+
* Build a self-validating executable: a SEA base with `app` appended and the
|
|
76
|
+
* whole thing signed as one file. Everything the container will check — the
|
|
77
|
+
* runtime, the verifier and the application — is inside what the signature
|
|
78
|
+
* covers.
|
|
79
|
+
*/
|
|
80
|
+
export declare function buildSea(options: SeaOptions): Promise<BuildResult>;
|
|
81
|
+
/**
|
|
82
|
+
* The files the embedded verifier needs, as paths relative to this package's
|
|
83
|
+
* root. That is this package's own compiled modules plus, unless turned off,
|
|
84
|
+
* the sigstore libraries — resolved through `node_modules` rather than listed,
|
|
85
|
+
* so the set cannot fall behind the dependency tree.
|
|
86
|
+
*/
|
|
87
|
+
export declare function verifierFiles({ sigstore }?: VerifierOptions): string[];
|
|
88
|
+
/**
|
|
89
|
+
* The CommonJS stub injected as the SEA's main script. It runs at the root of
|
|
90
|
+
* the mounted verifier bundle, so requiring this package is a relative path and
|
|
91
|
+
* nothing more; everything else it needs to decide, `launch` decides by looking
|
|
92
|
+
* at the binary's own tail.
|
|
93
|
+
*
|
|
94
|
+
* It stays the one piece no test can exercise from source, so it stays small.
|
|
95
|
+
*/
|
|
96
|
+
export declare function stubSource(options: BootstrapOptions): string;
|
|
97
|
+
//# sourceMappingURL=sea.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sea.d.ts","sourceRoot":"","sources":["../src/sea.ts"],"names":[],"mappings":"AAIA,OAAO,EAAwB,KAAK,KAAK,EAAE,KAAK,OAAO,EAAE,MAAM,aAAa,CAAC;AAC7E,OAAO,EAA4B,KAAK,WAAW,EAAE,MAAM,UAAU,CAAC;AACtE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAExD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAyD3C,MAAM,WAAW,gBAAiB,SAAQ,KAAK;IAC3C,uEAAuE;IACvE,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAClC;AACD,YAAY,EAAE,OAAO,EAAE,CAAC;AAExB;;;GAGG;AACH,wBAAgB,SAAS,CAAC,OAAO,GAAE,gBAAqB,GAAG,OAAO,CAEjE;AAED,8EAA8E;AAC9E,wBAAsB,SAAS,CAAC,OAAO,GAAE,gBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAE7E;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,OAAO,GAAE,gBAAqB,GAAG,kBAAkB,CAE7E;AAID,wDAAwD;AACxD,eAAO,MAAM,aAAa,UAA0C,CAAC;AAErE,MAAM,WAAW,eAAe;IAC5B;;;;OAIG;IACH,QAAQ,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CAClC;AAED,MAAM,WAAW,cAAe,SAAQ,eAAe;IACnD,sCAAsC;IACtC,MAAM,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,0EAA0E;IAC1E,SAAS,CAAC,EAAE,gBAAgB,GAAG,SAAS,CAAC;IACzC,qDAAqD;IACrD,QAAQ,CAAC,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;IAChC,oEAAoE;IACpE,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,2DAA2D;IAC3D,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAChC;AAED,MAAM,WAAW,UAAW,SAAQ,cAAc;IAC9C,8EAA8E;IAC9E,GAAG,EAAE,MAAM,CAAC;IACZ,yDAAyD;IACzD,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,8EAA8E;IAC9E,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B,oCAAoC;IACpC,GAAG,CAAC,EAAE,CAAC,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC,GAAG,SAAS,CAAC;CAC9C;AAED,MAAM,WAAW,aAAa;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,2DAA2D;IAC3D,QAAQ,EAAE,MAAM,EAAE,CAAC;CACtB;AAED;;;;;GAKG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,aAAa,CAAC,CA+DnF;AAED;;;;;GAKG;AACH,wBAAsB,QAAQ,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,WAAW,CAAC,CA4BxE;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,EAAE,QAAe,EAAE,GAAE,eAAoB,GAAG,MAAM,EAAE,CAWjF;AAuCD;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,gBAAgB,GAAG,MAAM,CAyB5D"}
|