@serve.zone/pallet-bundle 35.3.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/dist_ts_bundle/classes.controlprocess.d.ts +71 -0
- package/dist_ts_bundle/classes.controlprocess.js +441 -0
- package/dist_ts_bundle/classes.controlstderr.d.ts +36 -0
- package/dist_ts_bundle/classes.controlstderr.js +95 -0
- package/dist_ts_bundle/classes.releaseclient.d.ts +38 -0
- package/dist_ts_bundle/classes.releaseclient.js +285 -0
- package/dist_ts_bundle/errors.d.ts +35 -0
- package/dist_ts_bundle/errors.js +20 -0
- package/dist_ts_bundle/index.d.ts +10 -0
- package/dist_ts_bundle/index.js +11 -0
- package/dist_ts_bundle/inventory.d.ts +90 -0
- package/dist_ts_bundle/inventory.js +78 -0
- package/dist_ts_bundle/paths.d.ts +12 -0
- package/dist_ts_bundle/paths.js +76 -0
- package/dist_ts_bundle/plugins.d.ts +14 -0
- package/dist_ts_bundle/plugins.js +19 -0
- package/dist_ts_bundle/protocol.d.ts +48 -0
- package/dist_ts_bundle/protocol.js +47 -0
- package/dist_ts_bundle/units.d.ts +67 -0
- package/dist_ts_bundle/units.js +128 -0
- package/dist_ts_bundle/verify.d.ts +75 -0
- package/dist_ts_bundle/verify.js +263 -0
- package/dist_ts_bundle/version.d.ts +7 -0
- package/dist_ts_bundle/version.js +17 -0
- package/license.md +21 -0
- package/package.json +35 -0
- package/readme.md +176 -0
- package/ts_bundle/classes.controlprocess.ts +453 -0
- package/ts_bundle/classes.controlstderr.ts +92 -0
- package/ts_bundle/classes.releaseclient.ts +288 -0
- package/ts_bundle/errors.ts +40 -0
- package/ts_bundle/index.ts +10 -0
- package/ts_bundle/inventory.ts +104 -0
- package/ts_bundle/paths.ts +74 -0
- package/ts_bundle/plugins.ts +25 -0
- package/ts_bundle/protocol.ts +56 -0
- package/ts_bundle/readme.md +176 -0
- package/ts_bundle/units.ts +165 -0
- package/ts_bundle/verify.ts +353 -0
- package/ts_bundle/version.ts +18 -0
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
import { PalletBundleError, requireBundle } from './errors.js';
|
|
3
|
+
import { canonicalPath, requireCanonicalDirectory } from './paths.js';
|
|
4
|
+
import { palletBundleVersion } from './version.js';
|
|
5
|
+
import {
|
|
6
|
+
isPalletControlTarget, palletControlBundleName, palletControlComponents, palletControlNoticeTrees, palletControlPackageName,
|
|
7
|
+
palletControlPaths, palletControlRecordKeys, palletControlRecordName, palletControlRoles, palletControlSourceInventories,
|
|
8
|
+
palletNativeTarget, type TPalletControlRole, type TPalletControlTarget,
|
|
9
|
+
} from './inventory.js';
|
|
10
|
+
|
|
11
|
+
/** The trusted identity of one Pallet release, as a consumer pins it. */
|
|
12
|
+
export interface IPalletBundlePin {
|
|
13
|
+
readonly version: string;
|
|
14
|
+
readonly sourceCommit: string;
|
|
15
|
+
/** SHA-256 of the release's `tspack-manifest.json`, which pins every file of every bundle. */
|
|
16
|
+
readonly manifestSha256: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** A bundle's files as its sealed tspack manifest records them. */
|
|
20
|
+
export type TPalletSealedFiles = Readonly<Record<string, plugins.tspack.ITsPackFileRecord>>;
|
|
21
|
+
|
|
22
|
+
export interface IPalletControlExecutable {
|
|
23
|
+
/** Absolute path inside the verified bundle directory. */
|
|
24
|
+
readonly path: string;
|
|
25
|
+
readonly sha256: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Who built a native executable, from its tsrust provenance. */
|
|
29
|
+
export interface IPalletControlProvenance {
|
|
30
|
+
readonly projectName: string;
|
|
31
|
+
readonly projectVersion: string;
|
|
32
|
+
readonly gitCommit: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface IPalletControlBundle {
|
|
36
|
+
readonly directory: string;
|
|
37
|
+
readonly target: TPalletControlTarget;
|
|
38
|
+
readonly version: string;
|
|
39
|
+
readonly sourceCommit: string;
|
|
40
|
+
readonly sourceDirty: boolean;
|
|
41
|
+
/** The Deno the control executable was compiled with. */
|
|
42
|
+
readonly denoVersion: string;
|
|
43
|
+
readonly lockSha256: string;
|
|
44
|
+
readonly executables: Readonly<Record<TPalletControlRole, IPalletControlExecutable>>;
|
|
45
|
+
readonly provenance: Readonly<Record<Exclude<TPalletControlRole, 'control'>, IPalletControlProvenance>>;
|
|
46
|
+
readonly clockManifestSha256: string;
|
|
47
|
+
readonly dnsManifestSha256: string;
|
|
48
|
+
readonly vpnInventorySha256: string;
|
|
49
|
+
/** The digest `containerd-serve` compiles in; it pins containerd, its shim, runc and the pause image. */
|
|
50
|
+
readonly containerdManifestSha256: string;
|
|
51
|
+
readonly containerdVersion: string;
|
|
52
|
+
readonly runcVersion: string;
|
|
53
|
+
readonly files: TPalletSealedFiles;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** A released bundle: every file verified against the pinned release manifest. */
|
|
57
|
+
export interface IPalletVerifiedControlBundle extends IPalletControlBundle {
|
|
58
|
+
readonly manifestSha256: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** What a build is expected to be; the consumer's pin, or the packaging step's own source identity. */
|
|
62
|
+
export interface IPalletControlBuildExpectation {
|
|
63
|
+
readonly version: string;
|
|
64
|
+
readonly sourceCommit: string;
|
|
65
|
+
readonly sourceDirty: boolean;
|
|
66
|
+
readonly target: TPalletControlTarget;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const hash = /^[a-f0-9]{64}$/;
|
|
70
|
+
const commit = /^[a-f0-9]{40}$/;
|
|
71
|
+
const semver = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
|
|
72
|
+
const executableMode = 0o755;
|
|
73
|
+
const fileMode = 0o644;
|
|
74
|
+
const maximumMetadataBytes = 2 * 1024 * 1024;
|
|
75
|
+
|
|
76
|
+
type TRecord = Record<string, unknown>;
|
|
77
|
+
const record = (valueArg: unknown): valueArg is TRecord =>
|
|
78
|
+
typeof valueArg === 'object' && valueArg !== null && !Array.isArray(valueArg) &&
|
|
79
|
+
Object.getPrototypeOf(valueArg) === Object.prototype;
|
|
80
|
+
const sameKeys = (valueArg: TRecord, keysArg: readonly string[]): boolean => {
|
|
81
|
+
const keys = Object.keys(valueArg);
|
|
82
|
+
return keys.length === keysArg.length && keysArg.every((key) => Object.hasOwn(valueArg, key));
|
|
83
|
+
};
|
|
84
|
+
const list = (valueArg: unknown): valueArg is readonly unknown[] => Array.isArray(valueArg);
|
|
85
|
+
const safeName = (nameArg: unknown): nameArg is string =>
|
|
86
|
+
typeof nameArg === 'string' && nameArg.length <= 1024 && /^[A-Za-z0-9][A-Za-z0-9._+@/-]*$/.test(nameArg) &&
|
|
87
|
+
!nameArg.split('/').some((part) => ['', '.', '..'].includes(part));
|
|
88
|
+
const invalid = (): never => { throw new PalletBundleError('invalid_release'); };
|
|
89
|
+
|
|
90
|
+
export const validPalletBundlePin = (valueArg: unknown): valueArg is IPalletBundlePin =>
|
|
91
|
+
record(valueArg) && sameKeys(valueArg, ['version', 'sourceCommit', 'manifestSha256']) &&
|
|
92
|
+
typeof valueArg.version === 'string' && semver.test(valueArg.version) &&
|
|
93
|
+
typeof valueArg.sourceCommit === 'string' && commit.test(valueArg.sourceCommit) &&
|
|
94
|
+
typeof valueArg.manifestSha256 === 'string' && hash.test(valueArg.manifestSha256);
|
|
95
|
+
|
|
96
|
+
/** Refuses a malformed pin, and a pin of another Pallet version than this package states. */
|
|
97
|
+
export async function requirePalletBundlePin(pinArg: unknown): Promise<IPalletBundlePin> {
|
|
98
|
+
requireBundle(validPalletBundlePin(pinArg), 'invalid_options');
|
|
99
|
+
if (pinArg.version !== await palletBundleVersion()) throw new PalletBundleError('version_mismatch');
|
|
100
|
+
return Object.freeze({ version: pinArg.version, sourceCommit: pinArg.sourceCommit, manifestSha256: pinArg.manifestSha256 });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Reads one sealed file back and proves its bytes are the sealed record's. */
|
|
104
|
+
async function readSealed(directoryArg: string, filesArg: TPalletSealedFiles, nameArg: string): Promise<Buffer> {
|
|
105
|
+
const sealed = filesArg[nameArg];
|
|
106
|
+
requireBundle(sealed && safeName(nameArg) && sealed.mode === fileMode && sealed.size <= maximumMetadataBytes, 'invalid_release');
|
|
107
|
+
const filename = plugins.path.join(directoryArg, nameArg);
|
|
108
|
+
const file = await plugins.fs.open(filename, plugins.fsConstants.O_RDONLY | plugins.fsConstants.O_NOFOLLOW)
|
|
109
|
+
.catch(() => { throw new PalletBundleError('invalid_release'); });
|
|
110
|
+
try {
|
|
111
|
+
const info = await file.stat();
|
|
112
|
+
requireBundle(info.isFile() && info.size === sealed.size, 'invalid_release');
|
|
113
|
+
const bytes = Buffer.alloc(info.size);
|
|
114
|
+
let offset = 0;
|
|
115
|
+
while (offset < bytes.length) {
|
|
116
|
+
const { bytesRead } = await file.read(bytes, offset, bytes.length - offset, offset);
|
|
117
|
+
requireBundle(bytesRead > 0, 'invalid_release');
|
|
118
|
+
offset += bytesRead;
|
|
119
|
+
}
|
|
120
|
+
requireBundle(plugins.crypto.createHash('sha256').update(bytes).digest('hex') === sealed.sha256, 'artifact_mismatch');
|
|
121
|
+
return bytes;
|
|
122
|
+
} finally {
|
|
123
|
+
await file.close();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function readJson(directoryArg: string, filesArg: TPalletSealedFiles, nameArg: string): Promise<unknown> {
|
|
128
|
+
const bytes = await readSealed(directoryArg, filesArg, nameArg);
|
|
129
|
+
try { return JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes)); }
|
|
130
|
+
catch { return invalid(); }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** The files below the prefixes, as the sealed manifest names them. */
|
|
134
|
+
const below = (filesArg: TPalletSealedFiles, prefixesArg: readonly string[]): string[] =>
|
|
135
|
+
Object.keys(filesArg).filter((name) => prefixesArg.some((prefix) => name.startsWith(prefix))).sort();
|
|
136
|
+
const sameSet = (leftArg: readonly string[], rightArg: readonly string[]): boolean =>
|
|
137
|
+
leftArg.length === rightArg.length && [...leftArg].sort().every((name, index) => name === [...rightArg].sort()[index]);
|
|
138
|
+
|
|
139
|
+
/** The facts of a checked control record that a bundle states. */
|
|
140
|
+
type TPalletControlRecordFacts = Pick<IPalletControlBundle, 'denoVersion' | 'lockSha256' | 'clockManifestSha256' |
|
|
141
|
+
'containerdManifestSha256' | 'dnsManifestSha256' | 'vpnInventorySha256'>;
|
|
142
|
+
|
|
143
|
+
/** The control record: its identity, its fixed paths, and each declared file's sealed record. */
|
|
144
|
+
function checkRecord(recordArg: unknown, filesArg: TPalletSealedFiles,
|
|
145
|
+
expectedArg: IPalletControlBuildExpectation): TPalletControlRecordFacts {
|
|
146
|
+
requireBundle(record(recordArg) && sameKeys(recordArg, palletControlRecordKeys), 'invalid_release');
|
|
147
|
+
const control = recordArg;
|
|
148
|
+
const compiler = control.compiler;
|
|
149
|
+
requireBundle(control.packageName === palletControlPackageName && control.version === expectedArg.version &&
|
|
150
|
+
control.sourceCommit === expectedArg.sourceCommit && control.sourceDirty === expectedArg.sourceDirty &&
|
|
151
|
+
control.target === expectedArg.target && control.expectedUid === palletControlPaths.expectedUid &&
|
|
152
|
+
control.dataRoot === palletControlPaths.dataRoot && control.socketPath === palletControlPaths.socketPath &&
|
|
153
|
+
control.runtimeSocketPath === palletControlPaths.runtimeSocketPath &&
|
|
154
|
+
typeof control.lockSha256 === 'string' && hash.test(control.lockSha256) &&
|
|
155
|
+
record(compiler) && Object.values(compiler).every((value) => typeof value === 'string') &&
|
|
156
|
+
typeof compiler.deno === 'string' && semver.test(compiler.deno), 'invalid_release');
|
|
157
|
+
const componentDigest = (componentArg: keyof typeof palletControlComponents): string => {
|
|
158
|
+
const component = palletControlComponents[componentArg];
|
|
159
|
+
const digest = control[component.recordKey];
|
|
160
|
+
requireBundle(typeof digest === 'string' && hash.test(digest) && filesArg[component.manifest]?.sha256 === digest &&
|
|
161
|
+
filesArg[component.manifest].mode === fileMode, 'invalid_release');
|
|
162
|
+
return digest;
|
|
163
|
+
};
|
|
164
|
+
const facts: TPalletControlRecordFacts = {
|
|
165
|
+
denoVersion: compiler.deno, lockSha256: control.lockSha256,
|
|
166
|
+
clockManifestSha256: componentDigest('clock'), containerdManifestSha256: componentDigest('containerd'),
|
|
167
|
+
dnsManifestSha256: componentDigest('dns'), vpnInventorySha256: componentDigest('vpn'),
|
|
168
|
+
};
|
|
169
|
+
const declared = control.files;
|
|
170
|
+
requireBundle(record(declared) && Object.keys(declared).length > 0, 'invalid_release');
|
|
171
|
+
for (const [name, described] of Object.entries(declared)) {
|
|
172
|
+
const sealed = filesArg[name];
|
|
173
|
+
requireBundle(safeName(name) && record(described) && sameKeys(described, ['sha256', 'size']) && sealed &&
|
|
174
|
+
described.sha256 === sealed.sha256 && described.size === sealed.size, 'invalid_release');
|
|
175
|
+
}
|
|
176
|
+
// Every executable the bundle carries is one the control record declares.
|
|
177
|
+
for (const [name, sealed] of Object.entries(filesArg)) {
|
|
178
|
+
requireBundle(safeName(name) && (sealed.mode === fileMode || (sealed.mode === executableMode && Object.hasOwn(declared, name))),
|
|
179
|
+
'invalid_release');
|
|
180
|
+
}
|
|
181
|
+
for (const role of Object.values(palletControlRoles)) {
|
|
182
|
+
requireBundle(Object.hasOwn(declared, role.path) && filesArg[role.path]?.mode === executableMode &&
|
|
183
|
+
filesArg[role.path].size > 0, 'invalid_release');
|
|
184
|
+
if (role.provenance) {
|
|
185
|
+
requireBundle(Object.hasOwn(declared, role.provenance.path) && filesArg[role.provenance.path]?.mode === fileMode,
|
|
186
|
+
'invalid_release');
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return facts;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** A listing component: its manifest names exactly the files below its directory. */
|
|
193
|
+
async function checkListing(directoryArg: string, filesArg: TPalletSealedFiles, manifestArg: string,
|
|
194
|
+
prefixArg: string): Promise<TRecord> {
|
|
195
|
+
const manifest = await readJson(directoryArg, filesArg, manifestArg);
|
|
196
|
+
requireBundle(record(manifest) && list(manifest.files) && manifest.files.length > 0, 'invalid_release');
|
|
197
|
+
const listed: string[] = [];
|
|
198
|
+
for (const entry of manifest.files) {
|
|
199
|
+
requireBundle(record(entry) && safeName(entry.path) && typeof entry.sha256 === 'string' && hash.test(entry.sha256) &&
|
|
200
|
+
Number.isSafeInteger(entry.size) && (entry.mode === executableMode || entry.mode === fileMode), 'invalid_release');
|
|
201
|
+
const name = `${prefixArg}${entry.path}`;
|
|
202
|
+
const sealed = filesArg[name];
|
|
203
|
+
requireBundle(name !== manifestArg && sealed && sealed.sha256 === entry.sha256 && sealed.size === entry.size &&
|
|
204
|
+
sealed.mode === entry.mode, 'invalid_release');
|
|
205
|
+
listed.push(name);
|
|
206
|
+
}
|
|
207
|
+
requireBundle(new Set(listed).size === listed.length && sameSet(below(filesArg, [prefixArg]), [manifestArg, ...listed]),
|
|
208
|
+
'invalid_release');
|
|
209
|
+
return manifest;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** A `tsrust.native-notices` tree: its manifest names every file below the prefix by digest. */
|
|
213
|
+
async function checkNoticeTree(directoryArg: string, filesArg: TPalletSealedFiles, prefixArg: string): Promise<void> {
|
|
214
|
+
const manifestName = `${prefixArg}manifest.json`;
|
|
215
|
+
const manifest = await readJson(directoryArg, filesArg, manifestName);
|
|
216
|
+
requireBundle(record(manifest) && manifest.format === 'tsrust.native-notices' && record(manifest.files), 'invalid_release');
|
|
217
|
+
const named = Object.entries(manifest.files);
|
|
218
|
+
requireBundle(named.length > 0 && named.length <= 1024 && !Object.hasOwn(manifest.files, 'manifest.json'), 'invalid_release');
|
|
219
|
+
for (const [name, digest] of named) {
|
|
220
|
+
const sealed = filesArg[`${prefixArg}${name}`];
|
|
221
|
+
requireBundle(safeName(name) && typeof digest === 'string' && hash.test(digest) && sealed?.sha256 === digest &&
|
|
222
|
+
sealed.mode === fileMode, 'invalid_release');
|
|
223
|
+
}
|
|
224
|
+
requireBundle(sameSet(below(filesArg, [prefixArg]), [manifestName, ...named.map(([name]) => `${prefixArg}${name}`)]),
|
|
225
|
+
'invalid_release');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** A notice and corresponding-source inventory: its manifest lists every file below its prefixes. */
|
|
229
|
+
async function checkSourceInventory(directoryArg: string, filesArg: TPalletSealedFiles, manifestArg: string,
|
|
230
|
+
prefixesArg: readonly string[]): Promise<void> {
|
|
231
|
+
const manifest = await readJson(directoryArg, filesArg, manifestArg);
|
|
232
|
+
requireBundle(record(manifest) && list(manifest.files) && manifest.files.length > 0, 'invalid_release');
|
|
233
|
+
const listed: string[] = [];
|
|
234
|
+
for (const entry of manifest.files) {
|
|
235
|
+
requireBundle(record(entry), 'invalid_release');
|
|
236
|
+
const path = entry.path;
|
|
237
|
+
requireBundle(safeName(path) && path !== manifestArg && prefixesArg.some((prefix) => path.startsWith(prefix)) &&
|
|
238
|
+
typeof entry.sha256 === 'string' && hash.test(entry.sha256) && Number.isSafeInteger(entry.size) &&
|
|
239
|
+
filesArg[path]?.sha256 === entry.sha256 && filesArg[path].size === entry.size &&
|
|
240
|
+
filesArg[path].mode === fileMode, 'invalid_release');
|
|
241
|
+
listed.push(path);
|
|
242
|
+
}
|
|
243
|
+
requireBundle(new Set(listed).size === listed.length && sameSet(below(filesArg, prefixesArg), [manifestArg, ...listed]),
|
|
244
|
+
'invalid_release');
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** One native executable's tsrust provenance, bound to the sealed executable and this bundle's target. */
|
|
248
|
+
async function checkProvenance(directoryArg: string, filesArg: TPalletSealedFiles, roleArg: Exclude<TPalletControlRole, 'control'>,
|
|
249
|
+
expectedArg: IPalletControlBuildExpectation): Promise<IPalletControlProvenance> {
|
|
250
|
+
const role = palletControlRoles[roleArg];
|
|
251
|
+
const description = role.provenance!;
|
|
252
|
+
const provenance = await readJson(directoryArg, filesArg, description.path);
|
|
253
|
+
requireBundle(record(provenance), 'invalid_release');
|
|
254
|
+
const build = provenance.buildInfo;
|
|
255
|
+
requireBundle(record(build), 'invalid_release');
|
|
256
|
+
const own = roleArg === 'runtime';
|
|
257
|
+
requireBundle(provenance.format === 'tsrust.build-provenance.v2' && provenance.binarySha256 === filesArg[role.path].sha256 &&
|
|
258
|
+
build.projectName === description.projectName && build.binary === description.binary &&
|
|
259
|
+
build.target === palletNativeTarget(expectedArg.target) &&
|
|
260
|
+
typeof build.projectVersion === 'string' && semver.test(build.projectVersion) &&
|
|
261
|
+
typeof build.gitCommit === 'string' && commit.test(build.gitCommit) &&
|
|
262
|
+
build.gitDirty === (own ? expectedArg.sourceDirty : false) &&
|
|
263
|
+
(!own || (build.projectVersion === expectedArg.version && build.gitCommit === expectedArg.sourceCommit)), 'invalid_release');
|
|
264
|
+
return Object.freeze({ projectName: description.projectName, projectVersion: build.projectVersion, gitCommit: build.gitCommit });
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Checks a control build directory against the sealed records of its files: the control record,
|
|
269
|
+
* every executable's role and provenance, the component manifests and the notice and source
|
|
270
|
+
* inventories. Pallet's packaging runs this over every build before sealing it, with the build's
|
|
271
|
+
* own source identity; consumers use {@link verifyPalletControlBundle}, which also binds the pin.
|
|
272
|
+
*/
|
|
273
|
+
export async function inspectPalletControlBuild(directoryArg: string, filesArg: TPalletSealedFiles,
|
|
274
|
+
expectedArg: IPalletControlBuildExpectation): Promise<IPalletControlBundle> {
|
|
275
|
+
const directory = canonicalPath(directoryArg);
|
|
276
|
+
requireBundle(isPalletControlTarget(expectedArg.target) && semver.test(expectedArg.version) &&
|
|
277
|
+
commit.test(expectedArg.sourceCommit) && typeof expectedArg.sourceDirty === 'boolean' && record(filesArg), 'invalid_options');
|
|
278
|
+
await requireCanonicalDirectory(directory);
|
|
279
|
+
const control = checkRecord(await readJson(directory, filesArg, palletControlRecordName), filesArg, expectedArg);
|
|
280
|
+
await checkListing(directory, filesArg, palletControlComponents.clock.manifest, palletControlComponents.clock.directory);
|
|
281
|
+
const containerd = await checkListing(directory, filesArg, palletControlComponents.containerd.manifest,
|
|
282
|
+
palletControlComponents.containerd.directory);
|
|
283
|
+
requireBundle(typeof containerd.containerdVersion === 'string' && typeof containerd.runcVersion === 'string', 'invalid_release');
|
|
284
|
+
for (const tree of palletControlNoticeTrees) await checkNoticeTree(directory, filesArg, tree);
|
|
285
|
+
for (const inventory of palletControlSourceInventories) {
|
|
286
|
+
await checkSourceInventory(directory, filesArg, inventory.manifest, inventory.prefixes);
|
|
287
|
+
}
|
|
288
|
+
const provenance = {
|
|
289
|
+
store: await checkProvenance(directory, filesArg, 'store', expectedArg),
|
|
290
|
+
runtime: await checkProvenance(directory, filesArg, 'runtime', expectedArg),
|
|
291
|
+
guard: await checkProvenance(directory, filesArg, 'guard', expectedArg),
|
|
292
|
+
dns: await checkProvenance(directory, filesArg, 'dns', expectedArg),
|
|
293
|
+
vpn: await checkProvenance(directory, filesArg, 'vpn', expectedArg),
|
|
294
|
+
};
|
|
295
|
+
const executable = (roleArg: TPalletControlRole): IPalletControlExecutable => Object.freeze({
|
|
296
|
+
path: plugins.path.join(directory, palletControlRoles[roleArg].path), sha256: filesArg[palletControlRoles[roleArg].path].sha256,
|
|
297
|
+
});
|
|
298
|
+
return Object.freeze({
|
|
299
|
+
directory, target: expectedArg.target, version: expectedArg.version, sourceCommit: expectedArg.sourceCommit,
|
|
300
|
+
sourceDirty: expectedArg.sourceDirty, denoVersion: control.denoVersion, lockSha256: control.lockSha256,
|
|
301
|
+
executables: Object.freeze({ control: executable('control'), store: executable('store'), runtime: executable('runtime'),
|
|
302
|
+
guard: executable('guard'), dns: executable('dns'), vpn: executable('vpn') }),
|
|
303
|
+
provenance: Object.freeze(provenance),
|
|
304
|
+
clockManifestSha256: control.clockManifestSha256, dnsManifestSha256: control.dnsManifestSha256,
|
|
305
|
+
vpnInventorySha256: control.vpnInventorySha256, containerdManifestSha256: control.containerdManifestSha256,
|
|
306
|
+
containerdVersion: containerd.containerdVersion, runcVersion: containerd.runcVersion,
|
|
307
|
+
files: Object.freeze({ ...filesArg }),
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Verifies an extracted release bundle of the pinned version: `filesArg` are the records of the
|
|
313
|
+
* sealed release manifest whose digest the pin states (tspack's `extractBundle` result), and the
|
|
314
|
+
* pin must name this package's own Pallet version.
|
|
315
|
+
*/
|
|
316
|
+
export async function verifyPalletControlBundle(directoryArg: string, filesArg: TPalletSealedFiles, pinArg: IPalletBundlePin,
|
|
317
|
+
targetArg: TPalletControlTarget): Promise<IPalletVerifiedControlBundle> {
|
|
318
|
+
const pin = await requirePalletBundlePin(pinArg);
|
|
319
|
+
requireBundle(isPalletControlTarget(targetArg), 'invalid_options');
|
|
320
|
+
const bundle = await inspectPalletControlBuild(directoryArg, filesArg,
|
|
321
|
+
{ version: pin.version, sourceCommit: pin.sourceCommit, sourceDirty: false, target: targetArg });
|
|
322
|
+
return Object.freeze({ ...bundle, manifestSha256: pin.manifestSha256 });
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Extracts and verifies the pinned bundle from a download directory that holds the release's
|
|
327
|
+
* `tspack-manifest.json`, `SHA256SUMS.txt` and the target's archive. The bundle is extracted into a
|
|
328
|
+
* fresh private directory below `stagingParentArg`, which the caller owns from then on.
|
|
329
|
+
*/
|
|
330
|
+
export async function extractPalletControlBundle(downloadDirectoryArg: string, targetArg: TPalletControlTarget,
|
|
331
|
+
stagingParentArg: string, pinArg: IPalletBundlePin, signalArg?: AbortSignal): Promise<IPalletVerifiedControlBundle> {
|
|
332
|
+
const pin = await requirePalletBundlePin(pinArg);
|
|
333
|
+
requireBundle(isPalletControlTarget(targetArg), 'invalid_options');
|
|
334
|
+
await requireCanonicalDirectory(canonicalPath(stagingParentArg));
|
|
335
|
+
let extracted: plugins.tspack.ITsPackExtractResult;
|
|
336
|
+
try {
|
|
337
|
+
extracted = await new plugins.tspack.TsPack().extractBundle(downloadDirectoryArg, targetArg, stagingParentArg, {
|
|
338
|
+
expectedManifestSha256: pin.manifestSha256, expectedSourceCommit: pin.sourceCommit, requireRelease: true,
|
|
339
|
+
...(signalArg ? { signal: signalArg } : {}),
|
|
340
|
+
});
|
|
341
|
+
} catch (error) {
|
|
342
|
+
if (signalArg?.aborted) throw new PalletBundleError('aborted', { cause: error });
|
|
343
|
+
throw new PalletBundleError('invalid_release', { cause: error });
|
|
344
|
+
}
|
|
345
|
+
try {
|
|
346
|
+
requireBundle(extracted.manifest.packageName === palletControlPackageName &&
|
|
347
|
+
extracted.manifest.name === palletControlBundleName && extracted.manifest.version === pin.version, 'invalid_release');
|
|
348
|
+
return await verifyPalletControlBundle(extracted.directory, extracted.artifact.files, pin, targetArg);
|
|
349
|
+
} catch (error) {
|
|
350
|
+
await plugins.fs.rm(extracted.directory, { recursive: true, force: true });
|
|
351
|
+
throw error;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
|
|
3
|
+
let ownVersion: Promise<string> | undefined;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The Pallet version this package describes. The package is released from Pallet's repository with
|
|
7
|
+
* Pallet's own version, so this is the one bundle version whose inventory and contracts it states.
|
|
8
|
+
* It is read from the package manifest beside the compiled module, which is Pallet's root manifest
|
|
9
|
+
* in the repository and the published package's own manifest in `node_modules`.
|
|
10
|
+
*/
|
|
11
|
+
export const palletBundleVersion = (): Promise<string> => ownVersion ??= (async () => {
|
|
12
|
+
const manifest: unknown = JSON.parse(await plugins.fs.readFile(new URL('../package.json', import.meta.url), 'utf8'));
|
|
13
|
+
const version = typeof manifest === 'object' && manifest !== null && 'version' in manifest ? manifest.version : undefined;
|
|
14
|
+
if (typeof version !== 'string' || !/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.test(version)) {
|
|
15
|
+
throw new Error('The Pallet bundle package manifest carries no release version.');
|
|
16
|
+
}
|
|
17
|
+
return version;
|
|
18
|
+
})();
|