jorgex-stack 1.9.61 → 1.9.63

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.
@@ -0,0 +1,21 @@
1
+ interface ObservedPiCiArtifact {
2
+ version: string;
3
+ tarballUrl: string;
4
+ integrity: string;
5
+ bytes: number;
6
+ sha256: string;
7
+ sha512: string;
8
+ }
9
+ interface PrepareObservedPiCiArtifactInput {
10
+ tarballPath: string;
11
+ candidatePath: string;
12
+ fetchImpl: typeof fetch;
13
+ }
14
+ /**
15
+ * T06 CI observed artifact: resolve the live `dist-tags.latest` candidate,
16
+ * download the exact verified tarball, then freeze the observed identity as
17
+ * a bounded private JSON. Filesystem guards run before any network.
18
+ */
19
+ declare function prepareObservedPiCiArtifact(input: PrepareObservedPiCiArtifactInput): Promise<ObservedPiCiArtifact>;
20
+
21
+ export { type ObservedPiCiArtifact, type PrepareObservedPiCiArtifactInput, prepareObservedPiCiArtifact };
@@ -0,0 +1,473 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/lib/pi-ci-artifact.ts
4
+ import fs2 from "fs";
5
+ import path2 from "path";
6
+ import { randomBytes as randomBytes2 } from "crypto";
7
+ import { pathToFileURL } from "url";
8
+
9
+ // src/lib/npm-provider.ts
10
+ import fs from "fs";
11
+ import path from "path";
12
+ import { createHash, randomBytes, timingSafeEqual } from "crypto";
13
+ var REGISTRY_HOST = "registry.npmjs.org";
14
+ var ACCEPT = "application/vnd.npm.install-v1+json";
15
+ var MAX_METADATA_BYTES = 4 * 1024 * 1024;
16
+ var FETCH_TIMEOUT_MS = 1e4;
17
+ var STABLE_SEMVER = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
18
+ var PACKAGE_NAME = /^(?:@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;
19
+ function isStableSemverVersion(value) {
20
+ return typeof value === "string" && STABLE_SEMVER.test(value);
21
+ }
22
+ function isCanonicalSha512Integrity(integrity) {
23
+ if (typeof integrity !== "string" || !integrity.startsWith("sha512-")) return false;
24
+ const b64 = integrity.slice("sha512-".length);
25
+ if (!/^[A-Za-z0-9+/]+={0,2}$/.test(b64)) return false;
26
+ let bytes;
27
+ try {
28
+ bytes = Buffer.from(b64, "base64");
29
+ } catch {
30
+ return false;
31
+ }
32
+ return bytes.length === 64 && bytes.toString("base64") === b64;
33
+ }
34
+ function fail(message) {
35
+ throw new Error(`npm-provider: ${message}`);
36
+ }
37
+ function isRecord(value) {
38
+ return value !== null && typeof value === "object" && !Array.isArray(value);
39
+ }
40
+ function registryUrl(packageName) {
41
+ return `https://${REGISTRY_HOST}/${packageName}`;
42
+ }
43
+ function canonicalTarballUrl(packageName, version) {
44
+ const shortName = packageName.includes("/") ? packageName.slice(packageName.lastIndexOf("/") + 1) : packageName;
45
+ return `https://${REGISTRY_HOST}/${packageName}/-/${shortName}-${version}.tgz`;
46
+ }
47
+ async function readBoundedText(response) {
48
+ if (response.body === null) fail("missing response body");
49
+ const reader = response.body.getReader();
50
+ const chunks = [];
51
+ let total = 0;
52
+ try {
53
+ for (; ; ) {
54
+ const { done, value } = await reader.read();
55
+ if (done) break;
56
+ total += value.byteLength;
57
+ if (total >= MAX_METADATA_BYTES) {
58
+ await reader.cancel().catch(() => {
59
+ });
60
+ fail("metadata exceeds 4 MiB");
61
+ }
62
+ chunks.push(value);
63
+ }
64
+ } finally {
65
+ reader.releaseLock();
66
+ }
67
+ return Buffer.concat(chunks.map((chunk) => Buffer.from(chunk))).toString("utf8");
68
+ }
69
+ function assertCanonicalIntegrity(integrity) {
70
+ if (!isCanonicalSha512Integrity(integrity)) fail("invalid integrity (expected canonical sha512 SRI)");
71
+ }
72
+ async function resolveLatestNpmPackageRelease(packageName, fetchImpl) {
73
+ if (typeof packageName !== "string" || !PACKAGE_NAME.test(packageName)) {
74
+ fail("invalid package name");
75
+ }
76
+ const url = registryUrl(packageName);
77
+ let response;
78
+ try {
79
+ response = await fetchImpl(url, {
80
+ headers: { Accept: ACCEPT },
81
+ redirect: "error",
82
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
83
+ });
84
+ } catch (error) {
85
+ fail(`registry fetch failed: ${error instanceof Error ? error.message : String(error)}`);
86
+ }
87
+ if (!response.ok) fail(`registry responded ${response.status}`);
88
+ if (response.url !== url) fail(`unexpected response URL ${response.url}`);
89
+ const lengthHeader = response.headers.get("content-length");
90
+ if (lengthHeader !== null) {
91
+ const trimmed = lengthHeader.trim();
92
+ if (!/^\d+$/.test(trimmed)) fail("invalid content-length");
93
+ if (Number(trimmed) > MAX_METADATA_BYTES) fail("metadata exceeds 4 MiB");
94
+ }
95
+ const text = await readBoundedText(response);
96
+ let data;
97
+ try {
98
+ data = JSON.parse(text);
99
+ } catch {
100
+ fail("malformed packument JSON");
101
+ }
102
+ if (!isRecord(data) || data["name"] !== packageName) fail("malformed packument name");
103
+ const tags = data["dist-tags"];
104
+ if (!isRecord(tags) || typeof tags["latest"] !== "string") fail("missing dist-tags.latest");
105
+ const latest = tags["latest"];
106
+ if (!isStableSemverVersion(latest)) fail(`unstable or malformed version ${latest}`);
107
+ const versions = data["versions"];
108
+ if (!isRecord(versions)) fail("missing versions map");
109
+ const entry = versions[latest];
110
+ if (!isRecord(entry)) fail(`missing versions[${latest}]`);
111
+ if (entry["name"] !== packageName || entry["version"] !== latest) {
112
+ fail("version mismatch in versions entry");
113
+ }
114
+ if (entry["deprecated"] !== void 0) fail(`version ${latest} is deprecated`);
115
+ const dist = entry["dist"];
116
+ if (!isRecord(dist)) fail("missing dist");
117
+ const { tarball, integrity } = dist;
118
+ if (typeof tarball !== "string" || typeof integrity !== "string") {
119
+ fail("missing dist.tarball/integrity");
120
+ }
121
+ let parsed;
122
+ try {
123
+ parsed = new URL(tarball);
124
+ } catch {
125
+ fail("foreign tarball URL");
126
+ }
127
+ if (parsed.protocol !== "https:" || parsed.host !== REGISTRY_HOST || tarball !== canonicalTarballUrl(packageName, latest)) {
128
+ fail("foreign tarball URL");
129
+ }
130
+ assertCanonicalIntegrity(integrity);
131
+ return { version: latest, tarballUrl: tarball, integrity };
132
+ }
133
+ var MAX_TARBALL_BYTES = 128 * 1024 * 1024;
134
+ var TARBALL_FETCH_TIMEOUT_MS = 12e4;
135
+ function expectedSha512Digest(integrity) {
136
+ assertCanonicalIntegrity(integrity);
137
+ return Buffer.from(integrity.slice("sha512-".length), "base64");
138
+ }
139
+ function assertCanonicalReleaseInput(packageName, release) {
140
+ if (!isRecord(release)) fail("invalid release");
141
+ const { version, tarballUrl, integrity } = release;
142
+ if (!isStableSemverVersion(version)) fail("invalid release version");
143
+ if (typeof tarballUrl !== "string") fail("foreign tarball URL");
144
+ let parsed;
145
+ try {
146
+ parsed = new URL(tarballUrl);
147
+ } catch {
148
+ fail("foreign tarball URL");
149
+ }
150
+ if (parsed.protocol !== "https:" || parsed.host !== REGISTRY_HOST || tarballUrl !== canonicalTarballUrl(packageName, version)) {
151
+ fail("foreign tarball URL");
152
+ }
153
+ return { tarballUrl, expectedSha512: expectedSha512Digest(integrity) };
154
+ }
155
+ function readExistingArtifact(file) {
156
+ const stat = fs.lstatSync(file, { throwIfNoEntry: false });
157
+ if (stat === void 0) return null;
158
+ if (stat.isSymbolicLink() || !stat.isFile()) fail("destination is not a regular file");
159
+ let fd;
160
+ try {
161
+ fd = fs.openSync(file, "r");
162
+ } catch {
163
+ fail("cannot read existing destination");
164
+ }
165
+ const sha256 = createHash("sha256");
166
+ const sha512 = createHash("sha512");
167
+ const buffer = Buffer.allocUnsafe(1024 * 1024);
168
+ let bytes = 0;
169
+ try {
170
+ for (; ; ) {
171
+ const read = fs.readSync(fd, buffer, 0, buffer.length, null);
172
+ if (read === 0) break;
173
+ bytes += read;
174
+ if (bytes >= MAX_TARBALL_BYTES) fail("existing destination exceeds tarball bound");
175
+ sha256.update(buffer.subarray(0, read));
176
+ sha512.update(buffer.subarray(0, read));
177
+ }
178
+ } finally {
179
+ try {
180
+ fs.closeSync(fd);
181
+ } catch {
182
+ }
183
+ }
184
+ return { bytes, sha256: sha256.digest("hex"), sha512: sha512.digest("hex") };
185
+ }
186
+ async function downloadVerifiedNpmPackageTarball(packageName, release, destination, fetchImpl) {
187
+ if (typeof packageName !== "string" || !PACKAGE_NAME.test(packageName)) {
188
+ fail("invalid package name");
189
+ }
190
+ const { tarballUrl, expectedSha512 } = assertCanonicalReleaseInput(packageName, release);
191
+ if (typeof destination !== "string" || destination === "") fail("invalid destination");
192
+ const existing = readExistingArtifact(destination);
193
+ if (existing !== null) {
194
+ const existing512 = Buffer.from(existing.sha512, "hex");
195
+ if (existing512.length === expectedSha512.length && timingSafeEqual(existing512, expectedSha512)) {
196
+ return { path: destination, bytes: existing.bytes, sha256: existing.sha256, sha512: existing.sha512 };
197
+ }
198
+ fail("destination bytes do not match requested integrity");
199
+ }
200
+ let response;
201
+ try {
202
+ response = await fetchImpl(tarballUrl, {
203
+ headers: { accept: "application/octet-stream" },
204
+ redirect: "error",
205
+ signal: AbortSignal.timeout(TARBALL_FETCH_TIMEOUT_MS)
206
+ });
207
+ } catch (error) {
208
+ fail(`tarball fetch failed: ${error instanceof Error ? error.message : String(error)}`);
209
+ }
210
+ if (!response.ok) fail(`tarball responded ${response.status}`);
211
+ if (response.url !== tarballUrl) fail(`unexpected tarball response URL ${response.url}`);
212
+ const declared = response.headers.get("content-length");
213
+ if (declared !== null) {
214
+ const trimmed = declared.trim();
215
+ if (!/^\d+$/.test(trimmed)) fail("invalid tarball content-length");
216
+ if (Number(trimmed) > MAX_TARBALL_BYTES) fail("tarball exceeds 128 MiB");
217
+ }
218
+ if (response.body === null) fail("missing tarball body");
219
+ const tempPath = path.join(
220
+ path.dirname(destination),
221
+ `npm-tarball-partial-${process.pid}-${randomBytes(8).toString("hex")}`
222
+ );
223
+ let fd;
224
+ try {
225
+ fd = fs.openSync(tempPath, "wx", 384);
226
+ } catch {
227
+ fail("cannot stage tarball temp");
228
+ }
229
+ let fdClosed = false;
230
+ const closeFd = () => {
231
+ if (fdClosed) return;
232
+ fdClosed = true;
233
+ try {
234
+ fs.closeSync(fd);
235
+ } catch {
236
+ }
237
+ };
238
+ const discardStaged = () => {
239
+ closeFd();
240
+ try {
241
+ fs.rmSync(tempPath, { force: true });
242
+ } catch {
243
+ }
244
+ };
245
+ try {
246
+ const reader = response.body.getReader();
247
+ const sha256 = createHash("sha256");
248
+ const sha512 = createHash("sha512");
249
+ let bytes = 0;
250
+ try {
251
+ for (; ; ) {
252
+ const { done, value } = await reader.read();
253
+ if (done) break;
254
+ bytes += value.byteLength;
255
+ if (bytes + value.byteLength >= MAX_TARBALL_BYTES) {
256
+ await reader.cancel().catch(() => {
257
+ });
258
+ fail("tarball exceeds 128 MiB");
259
+ }
260
+ sha256.update(value);
261
+ sha512.update(value);
262
+ let offset = 0;
263
+ while (offset < value.byteLength) {
264
+ offset += fs.writeSync(fd, value, offset, value.byteLength - offset);
265
+ }
266
+ }
267
+ } finally {
268
+ reader.releaseLock();
269
+ }
270
+ closeFd();
271
+ const actual512 = sha512.digest();
272
+ if (!timingSafeEqual(actual512, expectedSha512)) fail("tarball integrity mismatch");
273
+ try {
274
+ fs.linkSync(tempPath, destination);
275
+ } catch {
276
+ fail("tarball publish raced with existing destination");
277
+ }
278
+ try {
279
+ fs.rmSync(tempPath, { force: true });
280
+ } catch {
281
+ }
282
+ return { path: destination, bytes, sha256: sha256.digest("hex"), sha512: actual512.toString("hex") };
283
+ } catch (error) {
284
+ discardStaged();
285
+ if (error instanceof Error && error.message.startsWith("npm-provider: ")) throw error;
286
+ fail(`tarball download failed: ${error instanceof Error ? error.message : String(error)}`);
287
+ }
288
+ }
289
+
290
+ // src/lib/pi-release-resolver.ts
291
+ var MAX_METADATA_BYTES2 = 4 * 1024 * 1024;
292
+ async function resolveLatestPiRelease(fetchImpl) {
293
+ try {
294
+ return await resolveLatestNpmPackageRelease("jorgex-pi", fetchImpl);
295
+ } catch (error) {
296
+ if (error instanceof Error && error.message.startsWith("pi-release-resolver: ")) throw error;
297
+ if (error instanceof Error) {
298
+ const detail = error.message.startsWith("npm-provider: ") ? error.message.slice("npm-provider: ".length) : error.message;
299
+ throw new Error(`pi-release-resolver: ${detail}`);
300
+ }
301
+ throw new Error(`pi-release-resolver: ${String(error)}`);
302
+ }
303
+ }
304
+ async function downloadVerifiedPiTarball(release, destination, fetchImpl) {
305
+ try {
306
+ return await downloadVerifiedNpmPackageTarball("jorgex-pi", release, destination, fetchImpl);
307
+ } catch (error) {
308
+ if (error instanceof Error && error.message.startsWith("pi-release-resolver: ")) throw error;
309
+ if (error instanceof Error) {
310
+ const detail = error.message.startsWith("npm-provider: ") ? error.message.slice("npm-provider: ".length) : error.message;
311
+ throw new Error(`pi-release-resolver: ${detail}`);
312
+ }
313
+ throw new Error(`pi-release-resolver: ${String(error)}`);
314
+ }
315
+ }
316
+
317
+ // src/lib/pi-ci-artifact.ts
318
+ var MAX_CANDIDATE_BYTES = 64 * 1024;
319
+ function fail2(message) {
320
+ throw new Error(`pi-ci-artifact: ${message}`);
321
+ }
322
+ function lstatOrNull(p) {
323
+ try {
324
+ return fs2.lstatSync(p);
325
+ } catch {
326
+ return null;
327
+ }
328
+ }
329
+ function assertAbsolute(label, value) {
330
+ if (typeof value !== "string" || value === "" || !path2.isAbsolute(value)) {
331
+ fail2(`${label} must be a non-empty absolute path`);
332
+ }
333
+ return path2.resolve(value);
334
+ }
335
+ function assertParentRealDir(resolvedFile, label) {
336
+ const dir = path2.dirname(resolvedFile);
337
+ const st = lstatOrNull(dir);
338
+ if (st === null || !st.isDirectory() || st.isSymbolicLink()) {
339
+ fail2(`${label} parent must be a real directory: ${dir}`);
340
+ }
341
+ return dir;
342
+ }
343
+ async function prepareObservedPiCiArtifact(input) {
344
+ if (input === null || typeof input !== "object" || Array.isArray(input)) {
345
+ fail2("input must be an object");
346
+ }
347
+ const { tarballPath, candidatePath, fetchImpl } = input;
348
+ if (typeof fetchImpl !== "function") {
349
+ fail2("fetchImpl must be a function");
350
+ }
351
+ const tarballResolved = assertAbsolute("tarballPath", tarballPath);
352
+ const candidateResolved = assertAbsolute("candidatePath", candidatePath);
353
+ if (tarballResolved === candidateResolved) {
354
+ fail2("tarballPath and candidatePath must differ");
355
+ }
356
+ const candidateDir = assertParentRealDir(candidateResolved, "candidatePath");
357
+ assertParentRealDir(tarballResolved, "tarballPath");
358
+ const candidateExisting = lstatOrNull(candidateResolved);
359
+ if (candidateExisting !== null) {
360
+ fail2(`candidate already exists: ${candidateResolved}`);
361
+ }
362
+ const tarballExisting = lstatOrNull(tarballResolved);
363
+ if (tarballExisting !== null && tarballExisting.isSymbolicLink()) {
364
+ fail2(`tarball path must not be a symlink: ${tarballResolved}`);
365
+ }
366
+ let release;
367
+ try {
368
+ release = await resolveLatestPiRelease(fetchImpl);
369
+ } catch (error) {
370
+ if (error instanceof Error && /^(pi-ci-artifact|pi-release-resolver): /.test(error.message)) {
371
+ throw error;
372
+ }
373
+ fail2(`provider release resolution failed: ${error instanceof Error ? error.message : String(error)}`);
374
+ }
375
+ let artifact;
376
+ try {
377
+ artifact = await downloadVerifiedPiTarball(
378
+ release,
379
+ tarballResolved,
380
+ fetchImpl
381
+ );
382
+ } catch (error) {
383
+ if (error instanceof Error && /^(pi-ci-artifact|pi-release-resolver): /.test(error.message)) {
384
+ throw error;
385
+ }
386
+ fail2(`verified tarball acquisition failed: ${error instanceof Error ? error.message : String(error)}`);
387
+ }
388
+ const payload = {
389
+ version: release.version,
390
+ tarballUrl: release.tarballUrl,
391
+ integrity: release.integrity,
392
+ bytes: artifact.bytes,
393
+ sha256: artifact.sha256,
394
+ sha512: artifact.sha512
395
+ };
396
+ const serialized = `${JSON.stringify(payload)}
397
+ `;
398
+ if (Buffer.byteLength(serialized, "utf8") >= MAX_CANDIDATE_BYTES) {
399
+ fail2("candidate JSON exceeds 64 KiB");
400
+ }
401
+ const tempPath = path2.join(
402
+ candidateDir,
403
+ `pi-ci-candidate-partial-${process.pid}-${randomBytes2(8).toString("hex")}.json`
404
+ );
405
+ let fd;
406
+ try {
407
+ fd = fs2.openSync(tempPath, "wx", 384);
408
+ } catch {
409
+ fail2(`cannot stage candidate temp: ${tempPath}`);
410
+ }
411
+ let fdClosed = false;
412
+ const closeFd = () => {
413
+ if (fdClosed) return;
414
+ fdClosed = true;
415
+ try {
416
+ fs2.closeSync(fd);
417
+ } catch {
418
+ }
419
+ };
420
+ const discardStaged = () => {
421
+ closeFd();
422
+ try {
423
+ fs2.rmSync(tempPath, { force: true });
424
+ } catch {
425
+ }
426
+ };
427
+ try {
428
+ try {
429
+ fs2.writeSync(fd, serialized, null, "utf8");
430
+ } catch {
431
+ fail2("cannot write candidate temp");
432
+ }
433
+ closeFd();
434
+ try {
435
+ fs2.chmodSync(tempPath, 384);
436
+ } catch {
437
+ fail2("cannot secure candidate temp");
438
+ }
439
+ try {
440
+ fs2.linkSync(tempPath, candidateResolved);
441
+ } catch {
442
+ fail2(`candidate already exists: ${candidateResolved}`);
443
+ }
444
+ try {
445
+ fs2.rmSync(tempPath, { force: true });
446
+ } catch {
447
+ }
448
+ } catch (error) {
449
+ discardStaged();
450
+ if (error instanceof Error && error.message.startsWith("pi-ci-artifact: ")) throw error;
451
+ fail2(`candidate publish failed: ${error instanceof Error ? error.message : String(error)}`);
452
+ }
453
+ return payload;
454
+ }
455
+ if (process.argv[1] !== void 0 && import.meta.url === pathToFileURL(process.argv[1]).href) {
456
+ const args = process.argv.slice(2);
457
+ if (args.length !== 2 || args[0] === void 0 || args[1] === void 0) {
458
+ console.error("pi-ci-artifact: expected <absolute-tarball> <absolute-candidate>");
459
+ process.exit(1);
460
+ }
461
+ const [tarballArg, candidateArg] = args;
462
+ await prepareObservedPiCiArtifact({
463
+ tarballPath: tarballArg,
464
+ candidatePath: candidateArg,
465
+ fetchImpl: fetch
466
+ }).catch((error) => {
467
+ console.error(error instanceof Error ? error.message : "pi-ci-artifact: failed");
468
+ process.exit(1);
469
+ });
470
+ }
471
+ export {
472
+ prepareObservedPiCiArtifact
473
+ };