@blamejs/core 0.17.24 → 0.18.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/CHANGELOG.md +6 -0
- package/NOTICE +12 -13
- package/README.md +3 -3
- package/lib/audit.js +69 -1
- package/lib/auth/oauth.js +57 -0
- package/lib/cli.js +15 -5
- package/lib/daemon.js +192 -2
- package/lib/guard-tenant-id.js +12 -1
- package/lib/mtls-ca.js +173 -18
- package/lib/mtls-engine-default.js +339 -314
- package/lib/outbox.js +43 -13
- package/lib/redact.js +39 -6
- package/lib/safe-json.js +9 -1
- package/lib/self-update.js +215 -24
- package/lib/vendor/MANIFEST.json +27 -30
- package/lib/vendor/blamejs-pki.cjs +28419 -0
- package/lib/webhook-dispatcher.js +55 -24
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
- package/lib/vendor/pki.cjs +0 -39718
package/lib/self-update.js
CHANGED
|
@@ -297,6 +297,11 @@ function _matchAsset(name, pattern, fallback) {
|
|
|
297
297
|
// first asset whose name fits the well-known shape (tarball / zip /
|
|
298
298
|
// .sig). The fallback is documented as best-effort; operators with
|
|
299
299
|
// multi-asset releases should pass a pattern explicitly.
|
|
300
|
+
// The `: false` arm is unreachable from the public API: every call that
|
|
301
|
+
// reaches this ternary (a non-string, non-RegExp pattern) passes a truthy
|
|
302
|
+
// fallback, and the null-fallback call sites only run with a validated
|
|
303
|
+
// string/RegExp pattern that never reaches the ternary.
|
|
304
|
+
/* c8 ignore next */
|
|
300
305
|
return fallback ? fallback.test(name) : false;
|
|
301
306
|
}
|
|
302
307
|
|
|
@@ -321,18 +326,82 @@ function _findEntryByName(entries, name) {
|
|
|
321
326
|
return null;
|
|
322
327
|
}
|
|
323
328
|
|
|
329
|
+
// Strip the final extension from an asset name so a signature that REPLACES the
|
|
330
|
+
// extension (app.bin -> app.sig) pairs as strongly as one that APPENDS a suffix
|
|
331
|
+
// (app.bin -> app.bin.sig). Returns the name unchanged when there is no leading
|
|
332
|
+
// stem to keep (no extension, or a leading-dot dotfile) so no over-broad stem is
|
|
333
|
+
// derived to match a foreign sidecar against.
|
|
334
|
+
function _assetStem(name) {
|
|
335
|
+
var dot = name.lastIndexOf(".");
|
|
336
|
+
return dot > 0 ? name.slice(0, dot) : name;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// _soleArtifactWithStem — is `assetName` the ONLY non-signature artifact in the
|
|
340
|
+
// release whose extension-stripped stem is `stem`? The replace-convention pairing
|
|
341
|
+
// (below) is only unambiguous when it is: with app.bin AND app.exe present, a
|
|
342
|
+
// single app.sig can't be attributed to either.
|
|
343
|
+
function _soleArtifactWithStem(assetName, stem, entries) {
|
|
344
|
+
for (var i = 0; i < entries.length; i++) {
|
|
345
|
+
var n = entries[i].name;
|
|
346
|
+
if (n === assetName) continue; // the asset itself
|
|
347
|
+
if (_SIG_SHAPE.test(n)) continue; // signatures aren't artifacts
|
|
348
|
+
// Ambiguous when another artifact would derive the SAME `stem + suffix`
|
|
349
|
+
// signature name: either it shares the extension-stripped stem (its replace
|
|
350
|
+
// convention), OR its full name IS the stem (its APPEND convention — app.tar's
|
|
351
|
+
// app.tar.sig is also app.tar.gz's replace-derived app.tar.sig).
|
|
352
|
+
if (_assetStem(n) === stem || n === stem) return false;
|
|
353
|
+
}
|
|
354
|
+
return true;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// _pathsAlias — do two already-realpath-resolved absolute paths refer to the same
|
|
358
|
+
// file? An exact match, or a CASE-ONLY difference: realpathSync does not always
|
|
359
|
+
// canonicalize the final component's case, so on a case-insensitive volume
|
|
360
|
+
// (Windows / default macOS) a backup path can alias the reserved quarantine path
|
|
361
|
+
// with different letter casing. Comparing case-insensitively on every platform is
|
|
362
|
+
// fail-closed here: the quarantine suffix (.rollback-bad) is framework-reserved,
|
|
363
|
+
// so a case-variant of it is only ever the same reserved path, never a distinct
|
|
364
|
+
// operator backup that happens to collide.
|
|
365
|
+
function _pathsAlias(a, b) {
|
|
366
|
+
return a === b || a.toLowerCase() === b.toLowerCase();
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// _derivedSignatureNames — the exact signature names that unambiguously sign
|
|
370
|
+
// `assetName`: the asset name plus each suffix (append convention) and, when the
|
|
371
|
+
// asset has an extension to strip AND is the sole artifact with that stem, the
|
|
372
|
+
// stem plus each suffix (replace convention). Every derived name ends in a
|
|
373
|
+
// signature suffix, so the boundary after the stem is always that suffix's leading
|
|
374
|
+
// delimiter — a bare-prefix look-alike (application.sig for app.bin, stem `app`)
|
|
375
|
+
// never derives. The replace convention is withheld when another artifact shares
|
|
376
|
+
// the stem (app.bin + app.exe), where a lone app.sig is ambiguous.
|
|
377
|
+
function _derivedSignatureNames(assetName, entries) {
|
|
378
|
+
var stem = _assetStem(assetName);
|
|
379
|
+
var stemUnique = stem !== assetName && _soleArtifactWithStem(assetName, stem, entries);
|
|
380
|
+
var names = [];
|
|
381
|
+
for (var s = 0; s < _SIG_SUFFIXES.length; s++) {
|
|
382
|
+
names.push(assetName + _SIG_SUFFIXES[s]);
|
|
383
|
+
if (stemUnique) names.push(stem + _SIG_SUFFIXES[s]);
|
|
384
|
+
}
|
|
385
|
+
return names;
|
|
386
|
+
}
|
|
387
|
+
|
|
324
388
|
// _selectSignatureFor — pick the detached signature OF `assetName`, not a
|
|
325
389
|
// first-match-wins signature that may sign a DIFFERENT sidecar. Selecting the
|
|
326
390
|
// asset first and DERIVING the expected signature name from it is the pairing
|
|
327
391
|
// contract: a returned { asset, signature } is guaranteed to be an asset and
|
|
328
|
-
// the signature over exactly that asset.
|
|
329
|
-
// asset
|
|
330
|
-
//
|
|
392
|
+
// the signature over exactly that asset. The derived names cover both common
|
|
393
|
+
// one-asset-one-sig conventions — the suffix APPENDED to the asset name
|
|
394
|
+
// (app.bin -> app.bin.sig) and the extension REPLACED (app.bin -> app.sig).
|
|
395
|
+
// Falls back to a lone signature-shaped asset only when the release ships
|
|
396
|
+
// exactly one AND that sidecar references the full asset name (the common
|
|
397
|
+
// algorithm-suffixed case); anything ambiguous OR a lone sidecar whose name is
|
|
398
|
+
// unrelated to the asset fails closed (null) rather than pairing a signature
|
|
331
399
|
// that may not sign the returned asset.
|
|
332
400
|
function _selectSignatureFor(assetName, entries, signaturePattern) {
|
|
333
|
-
// (a) Strong pairing:
|
|
334
|
-
|
|
335
|
-
|
|
401
|
+
// (a) Strong pairing: an exact derived signature name (append or replace).
|
|
402
|
+
var derived = _derivedSignatureNames(assetName, entries);
|
|
403
|
+
for (var s = 0; s < derived.length; s++) {
|
|
404
|
+
var hit = _findEntryByName(entries, derived[s]);
|
|
336
405
|
// When the operator constrained signaturePattern, the derived name must
|
|
337
406
|
// also satisfy it; otherwise the derived name is authoritative.
|
|
338
407
|
if (hit && (signaturePattern === undefined || _matchAsset(hit.name, signaturePattern, null))) {
|
|
@@ -348,9 +417,13 @@ function _selectSignatureFor(assetName, entries, signaturePattern) {
|
|
|
348
417
|
});
|
|
349
418
|
return stemMatches.length === 1 ? _assetObj(stemMatches[0]) : null;
|
|
350
419
|
}
|
|
351
|
-
// (c) No operator pattern, no derived hit: accept a lone signature-shaped
|
|
352
|
-
// asset (single-sig release)
|
|
353
|
-
|
|
420
|
+
// (c) No operator pattern, no derived hit: accept a lone signature-shaped asset
|
|
421
|
+
// ONLY when it references the asset stem (single-sig release). A lone sidecar
|
|
422
|
+
// whose name is unrelated to the asset may sign a different object, so it fails
|
|
423
|
+
// closed (null) — never pair a name-unrelated signature to the asset.
|
|
424
|
+
var sigShaped = entries.filter(function (e) {
|
|
425
|
+
return _SIG_SHAPE.test(e.name) && e.name.indexOf(assetName) === 0;
|
|
426
|
+
});
|
|
354
427
|
return sigShaped.length === 1 ? _assetObj(sigShaped[0]) : null;
|
|
355
428
|
}
|
|
356
429
|
|
|
@@ -445,9 +518,11 @@ async function poll(opts) {
|
|
|
445
518
|
} catch (e) {
|
|
446
519
|
_safeAuditEmit("selfupdate.poll.checked", "denied", {
|
|
447
520
|
releasesUrl: opts.releasesUrl, reason: "request-failed",
|
|
521
|
+
/* c8 ignore next -- String(e) fallback: request rejections are always Errors with a message */
|
|
448
522
|
message: (e && e.message) || String(e),
|
|
449
523
|
});
|
|
450
524
|
throw new SelfUpdateError("selfupdate/poll-failed",
|
|
525
|
+
/* c8 ignore next */
|
|
451
526
|
"selfUpdate.poll: request failed: " + ((e && e.message) || String(e)));
|
|
452
527
|
}
|
|
453
528
|
|
|
@@ -469,6 +544,10 @@ async function poll(opts) {
|
|
|
469
544
|
"selfUpdate.poll: upstream returned HTTP " + res.statusCode);
|
|
470
545
|
}
|
|
471
546
|
|
|
547
|
+
// The non-Buffer arm is defensive: httpClient.request always resolves a
|
|
548
|
+
// Buffer body (Buffer.alloc(0) for an empty response), so the string/null
|
|
549
|
+
// normalization never runs on the real transport.
|
|
550
|
+
/* c8 ignore next 2 */
|
|
472
551
|
var bodyBuf = Buffer.isBuffer(res.body) ? res.body :
|
|
473
552
|
(res.body == null ? Buffer.alloc(0) : Buffer.from(String(res.body), "utf8"));
|
|
474
553
|
var parsed;
|
|
@@ -477,9 +556,11 @@ async function poll(opts) {
|
|
|
477
556
|
} catch (e) {
|
|
478
557
|
_safeAuditEmit("selfupdate.poll.checked", "denied", {
|
|
479
558
|
releasesUrl: opts.releasesUrl, reason: "bad-json",
|
|
559
|
+
/* c8 ignore next -- String(e) fallback: safeJson.parse throws Errors with a message */
|
|
480
560
|
message: (e && e.message) || String(e),
|
|
481
561
|
});
|
|
482
562
|
throw new SelfUpdateError("selfupdate/bad-json",
|
|
563
|
+
/* c8 ignore next */
|
|
483
564
|
"selfUpdate.poll: response is not valid JSON: " + ((e && e.message) || String(e)));
|
|
484
565
|
}
|
|
485
566
|
|
|
@@ -676,9 +757,13 @@ async function verify(opts) {
|
|
|
676
757
|
var code = _mapStandaloneKind(e && e.kind);
|
|
677
758
|
_safeAuditEmit("selfupdate.verify.failed", "denied", {
|
|
678
759
|
assetPath: opts.assetPath, signaturePath: opts.signaturePath,
|
|
760
|
+
// Fallbacks are defensive: standaloneVerifier throws only via `_svErr`,
|
|
761
|
+
// which always sets `.kind` and a message, so neither alternate runs.
|
|
762
|
+
/* c8 ignore next */
|
|
679
763
|
reason: (e && e.kind) || "verify-error", message: (e && e.message) || String(e),
|
|
680
764
|
});
|
|
681
765
|
throw new SelfUpdateError(code,
|
|
766
|
+
/* c8 ignore next */
|
|
682
767
|
"selfUpdate.verify: " + ((e && e.message) || String(e)));
|
|
683
768
|
}
|
|
684
769
|
|
|
@@ -716,20 +801,20 @@ function _validateSwapOpts(opts, label) {
|
|
|
716
801
|
"selfUpdate.swap: opts.hashAlgo must be one of " + ALLOWED_HASH_ALGS.join(", "));
|
|
717
802
|
}
|
|
718
803
|
};
|
|
719
|
-
// swap re-reads the from-bytes to re-hash them (closing the verify->swap
|
|
720
|
-
// window); its cap must be declarable so it matches the maxBytes an
|
|
721
|
-
// operator passed to selfUpdate.verify for the same asset — otherwise swap
|
|
722
|
-
// would refuse a large binary that verify accepted. Optional; defaults to
|
|
723
|
-
// the same C.BYTES.gib(1) cap the body applies.
|
|
724
|
-
schema.maxBytes = function (value) {
|
|
725
|
-
numericBounds.requirePositiveFiniteIntIfPresent(value,
|
|
726
|
-
"selfUpdate.swap: opts.maxBytes", SelfUpdateError, "selfupdate/bad-max-bytes");
|
|
727
|
-
};
|
|
728
804
|
}
|
|
729
805
|
schema.to = { rule: "required-string", code: "selfupdate/bad-to",
|
|
730
806
|
label: "selfUpdate." + label + ": opts.to" };
|
|
731
807
|
schema.backupTo = { rule: "required-string", code: "selfupdate/bad-backup",
|
|
732
808
|
label: "selfUpdate." + label + ": opts.backupTo" };
|
|
809
|
+
// maxBytes is a declared opt for BOTH labels: swap re-reads the from-bytes to
|
|
810
|
+
// re-hash them (closing the verify->swap window) and rollback reads backupTo to
|
|
811
|
+
// restore it. Either read must be raisable past atomicFile's 64 MiB default so a
|
|
812
|
+
// large prior binary (a Node SEA is 100+ MiB) is not refused before it starts.
|
|
813
|
+
// Optional; each body defaults to the same C.BYTES.gib(1) cap.
|
|
814
|
+
schema.maxBytes = function (value) {
|
|
815
|
+
numericBounds.requirePositiveFiniteIntIfPresent(value,
|
|
816
|
+
"selfUpdate." + label + ": opts.maxBytes", SelfUpdateError, "selfupdate/bad-max-bytes");
|
|
817
|
+
};
|
|
733
818
|
validateOpts.shape(opts, schema, "selfUpdate." + label, SelfUpdateError, "selfupdate/bad-opts");
|
|
734
819
|
}
|
|
735
820
|
|
|
@@ -745,12 +830,18 @@ async function _relocateFile(src, dst, fileMode) {
|
|
|
745
830
|
atomicFile.renameWithRetry(src, dst);
|
|
746
831
|
return;
|
|
747
832
|
} catch (e) {
|
|
833
|
+
// The EXDEV (cross-volume) fall-through needs two filesystems and is not
|
|
834
|
+
// reachable in a single-volume test; the whole cross-device arm — the
|
|
835
|
+
// non-EXDEV re-throw guard and the copy+unlink fallback — is ignored for
|
|
836
|
+
// coverage since its else-branch can't be isolated from the guard.
|
|
837
|
+
/* c8 ignore start */
|
|
748
838
|
if (!e || e.code !== "EXDEV") throw e;
|
|
749
839
|
// Cross-volume: a rename can't cross the device boundary. Preserve the
|
|
750
840
|
// bytes by copy, then remove the source (best-effort — a locked cross-volume
|
|
751
841
|
// source is the documented limitation of this rare fallback).
|
|
752
842
|
await atomicFile.copy(src, dst, { fileMode: fileMode });
|
|
753
843
|
try { nodeFs.unlinkSync(src); } catch (_u) { /* cross-vol source cleanup — operator-cleanable */ }
|
|
844
|
+
/* c8 ignore stop */
|
|
754
845
|
}
|
|
755
846
|
}
|
|
756
847
|
|
|
@@ -765,6 +856,13 @@ async function _relocateFile(src, dst, fileMode) {
|
|
|
765
856
|
// for `rollback_failed` couldn't tell a successful swap-with-rollback from a
|
|
766
857
|
// failed both-binaries-lost scenario). SSDF RV.1.
|
|
767
858
|
async function _safeRollback(backupTo, to, hadOriginal) {
|
|
859
|
+
// The active-restore path (hadOriginal === true) only runs when the install
|
|
860
|
+
// write fails AFTER a successful move-aside — a state that can't be forced
|
|
861
|
+
// through the public API without an fs-layer mock (a move-aside that succeeds
|
|
862
|
+
// guarantees the subsequent same-directory install write also succeeds). The
|
|
863
|
+
// whole body is ignored for coverage since the hadOriginal fall-through
|
|
864
|
+
// branch can't be isolated from the early return.
|
|
865
|
+
/* c8 ignore start */
|
|
768
866
|
if (!hadOriginal) return null;
|
|
769
867
|
try {
|
|
770
868
|
await _relocateFile(backupTo, to, 0o600);
|
|
@@ -778,6 +876,7 @@ async function _safeRollback(backupTo, to, hadOriginal) {
|
|
|
778
876
|
});
|
|
779
877
|
return err;
|
|
780
878
|
}
|
|
879
|
+
/* c8 ignore stop */
|
|
781
880
|
}
|
|
782
881
|
|
|
783
882
|
// Atomic swap of `from` -> `to` with rollback on failure. Steps:
|
|
@@ -854,6 +953,7 @@ async function swap(opts) {
|
|
|
854
953
|
// or re-read would reopen).
|
|
855
954
|
var swapAlg = opts.hashAlgo || DEFAULT_HASH_ALG;
|
|
856
955
|
var fromMode;
|
|
956
|
+
/* c8 ignore next -- statSync catch is TOCTOU-defensive: existsSync(from) just passed */
|
|
857
957
|
try { fromMode = (nodeFs.statSync(from).mode & 0o777); } catch (_m) { fromMode = 0o600; }
|
|
858
958
|
var fromBytes;
|
|
859
959
|
try {
|
|
@@ -864,6 +964,7 @@ async function swap(opts) {
|
|
|
864
964
|
} catch (e) {
|
|
865
965
|
throw new SelfUpdateError("selfupdate/swap-read-failed",
|
|
866
966
|
"selfUpdate.swap: failed to read from for the integrity re-check (a symlinked source is refused): " +
|
|
967
|
+
/* c8 ignore next */
|
|
867
968
|
((e && e.message) || String(e)));
|
|
868
969
|
}
|
|
869
970
|
var actualHash = nodeCrypto.createHash(swapAlg).update(fromBytes).digest("hex");
|
|
@@ -892,6 +993,7 @@ async function swap(opts) {
|
|
|
892
993
|
} catch (e) {
|
|
893
994
|
throw new SelfUpdateError("selfupdate/backup-failed",
|
|
894
995
|
"selfUpdate.swap: failed to move " + to + " aside to " + backupTo + ": " +
|
|
996
|
+
/* c8 ignore next */
|
|
895
997
|
((e && e.message) || String(e)));
|
|
896
998
|
}
|
|
897
999
|
}
|
|
@@ -909,6 +1011,9 @@ async function swap(opts) {
|
|
|
909
1011
|
await atomicFile.write(to, fromBytes, { fileMode: fromMode, overwrite: true });
|
|
910
1012
|
} catch (e) {
|
|
911
1013
|
var rbErr = await _safeRollback(backupTo, to, hadOriginal);
|
|
1014
|
+
// The rollback-also-failed arm needs the install write to fail after a
|
|
1015
|
+
// successful move-aside — unforceable without an fs mock (see _safeRollback).
|
|
1016
|
+
/* c8 ignore start */
|
|
912
1017
|
if (rbErr) {
|
|
913
1018
|
throw new SelfUpdateError("selfupdate/swap-rollback-failed",
|
|
914
1019
|
"selfUpdate.swap: install of " + to + " failed AND rollback ALSO failed — " +
|
|
@@ -916,12 +1021,16 @@ async function swap(opts) {
|
|
|
916
1021
|
". install-error=" + ((e && e.message) || String(e)) +
|
|
917
1022
|
"; rollback-error=" + rbErr.message);
|
|
918
1023
|
}
|
|
1024
|
+
/* c8 ignore stop */
|
|
919
1025
|
throw new SelfUpdateError("selfupdate/swap-failed",
|
|
1026
|
+
/* c8 ignore next */
|
|
920
1027
|
"selfUpdate.swap: install of " + to + " failed: " + ((e && e.message) || String(e)));
|
|
921
1028
|
}
|
|
922
1029
|
// Consume the source asset now that the verified bytes are installed
|
|
923
1030
|
// (best-effort — the install already succeeded; a leftover temp is
|
|
924
|
-
// operator-cleanable).
|
|
1031
|
+
// operator-cleanable). The unlink-failure catch is unforceable on the
|
|
1032
|
+
// supported platforms (a readable regular file is always removable here).
|
|
1033
|
+
/* c8 ignore next */
|
|
925
1034
|
try { nodeFs.unlinkSync(from); } catch (_u) { /* tmp source leak — operator-cleanable */ }
|
|
926
1035
|
|
|
927
1036
|
// Step 4 — fsync directories so the install is durable.
|
|
@@ -944,14 +1053,23 @@ async function swap(opts) {
|
|
|
944
1053
|
* @since 0.6.0
|
|
945
1054
|
* @related b.selfUpdate.swap, b.atomicFile.copy
|
|
946
1055
|
*
|
|
947
|
-
* Restore `backupTo` → `to
|
|
948
|
-
*
|
|
949
|
-
*
|
|
950
|
-
*
|
|
1056
|
+
* Restore `backupTo` → `to`. When a bad-binary `to` is present it is first MOVED
|
|
1057
|
+
* ASIDE with a rename — which frees the path even for a locked, running Windows
|
|
1058
|
+
* image (Windows refuses an in-place replace of a mapped executable but allows
|
|
1059
|
+
* the move) — so the restore is a CREATE at the freed path, not a replace of a
|
|
1060
|
+
* locked file; the quarantined bad binary is then removed (best-effort). The
|
|
1061
|
+
* backup read is capped at `maxBytes` (default 1 GiB) so a large prior binary (a
|
|
1062
|
+
* Node SEA is 100+ MiB) restores rather than being refused at atomicFile's 64 MiB
|
|
1063
|
+
* copy default. Operators run rollback when a post-swap healthcheck reports the
|
|
1064
|
+
* new binary is bad. Throws SelfUpdateError when the backup file is missing, the
|
|
1065
|
+
* move-aside fails, or the copy fails; a copy failure after the move-aside
|
|
1066
|
+
* restores the quarantined image back over `to` so a failed rollback never
|
|
1067
|
+
* leaves the target absent.
|
|
951
1068
|
*
|
|
952
1069
|
* @opts
|
|
953
1070
|
* to: string, // required — target path to restore
|
|
954
1071
|
* backupTo: string, // required — source backup path
|
|
1072
|
+
* maxBytes: number, // backup read cap (default 1 GiB)
|
|
955
1073
|
*
|
|
956
1074
|
* @example
|
|
957
1075
|
* try {
|
|
@@ -974,13 +1092,79 @@ async function rollback(opts) {
|
|
|
974
1092
|
}
|
|
975
1093
|
|
|
976
1094
|
atomicFile.ensureDir(nodePath.dirname(to));
|
|
1095
|
+
|
|
1096
|
+
// Move the outgoing (bad) `to` ASIDE to a quarantine path via a rename before
|
|
1097
|
+
// restoring — the SAME move-aside swap uses so the restore is a create at a
|
|
1098
|
+
// freed path, not a replace of a possibly-locked running image (Windows refuses
|
|
1099
|
+
// the in-place replace but allows the move). A move-aside failure surfaces as
|
|
1100
|
+
// rollback-failed with the original `to` left intact (fail closed).
|
|
1101
|
+
var quarantine = to + ".rollback-bad";
|
|
1102
|
+
// Reject a backupTo that aliases the quarantine path: the move-aside would
|
|
1103
|
+
// first unlink the quarantine (deleting the known-good backup), then move the
|
|
1104
|
+
// bad `to` into it, then copy those bad bytes back over `to` — corrupting the
|
|
1105
|
+
// target AND destroying the backup while reporting success. Fail closed before
|
|
1106
|
+
// touching either file. Compare REALPATH-resolved paths, not path.resolve():
|
|
1107
|
+
// path.resolve leaves SYMLINKS unresolved, so a symlinked backupTo (or a
|
|
1108
|
+
// symlinked parent dir) pointing at the quarantine would slip past. backupTo
|
|
1109
|
+
// exists (checked above); the quarantine may not, so realpath its existing
|
|
1110
|
+
// parent dir + append the basename to get its canonical path.
|
|
1111
|
+
var realBackup = nodeFs.realpathSync(backupTo);
|
|
1112
|
+
var realQuarantine = nodePath.join(nodeFs.realpathSync(nodePath.dirname(quarantine)),
|
|
1113
|
+
nodePath.basename(quarantine));
|
|
1114
|
+
if (_pathsAlias(realBackup, realQuarantine)) {
|
|
1115
|
+
throw new SelfUpdateError("selfupdate/rollback-failed",
|
|
1116
|
+
"selfUpdate.rollback: backupTo resolves to the reserved quarantine path " +
|
|
1117
|
+
JSON.stringify(quarantine) + " (it would be overwritten by the move-aside)");
|
|
1118
|
+
}
|
|
1119
|
+
var hadTarget = nodeFs.existsSync(to);
|
|
1120
|
+
if (hadTarget) {
|
|
1121
|
+
try { nodeFs.unlinkSync(quarantine); } catch (_stale) { /* no stale quarantine (the common case) */ }
|
|
1122
|
+
try {
|
|
1123
|
+
await _relocateFile(to, quarantine, 0o600);
|
|
1124
|
+
} catch (e) {
|
|
1125
|
+
throw new SelfUpdateError("selfupdate/rollback-failed",
|
|
1126
|
+
"selfUpdate.rollback: failed to move current " + to + " aside to " + quarantine +
|
|
1127
|
+
/* c8 ignore next */
|
|
1128
|
+
" before restore: " + ((e && e.message) || String(e)));
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
|
|
977
1132
|
try {
|
|
978
|
-
await atomicFile.copy(backupTo, to, {
|
|
1133
|
+
await atomicFile.copy(backupTo, to, {
|
|
1134
|
+
fileMode: 0o600,
|
|
1135
|
+
maxBytes: typeof opts.maxBytes === "number" ? opts.maxBytes : C.BYTES.gib(1),
|
|
1136
|
+
});
|
|
979
1137
|
} catch (e) {
|
|
1138
|
+
// The copy failed AFTER `to` was moved aside to `quarantine` (exceeds
|
|
1139
|
+
// maxBytes, unreadable source, destination write error), so `to` is now
|
|
1140
|
+
// absent — a failed rollback must not become a next-launch outage with no
|
|
1141
|
+
// binary at all. Restore the quarantined image back over `to` (a rename,
|
|
1142
|
+
// lock-safe) before surfacing the error, so an executable (the pre-rollback
|
|
1143
|
+
// one) still exists — the same fail-closed restore the swap failure path does.
|
|
1144
|
+
if (hadTarget) {
|
|
1145
|
+
try {
|
|
1146
|
+
await _relocateFile(quarantine, to, 0o600);
|
|
1147
|
+
/* c8 ignore start -- restore-failure is the catastrophic both-lost case: renaming an existing quarantine back over the now-absent `to` cannot be forced through the public API without an fs-layer mock */
|
|
1148
|
+
} catch (re) {
|
|
1149
|
+
_safeAuditEmit("selfupdate.rollback.restore_failed", "denied", {
|
|
1150
|
+
to: to, quarantine: quarantine, reason: "rollback-restore-failed",
|
|
1151
|
+
message: (re && re.message) || String(re),
|
|
1152
|
+
});
|
|
1153
|
+
}
|
|
1154
|
+
/* c8 ignore stop */
|
|
1155
|
+
}
|
|
980
1156
|
throw new SelfUpdateError("selfupdate/rollback-failed",
|
|
981
1157
|
"selfUpdate.rollback: copy " + backupTo + " -> " + to + " failed: " +
|
|
1158
|
+
/* c8 ignore next */
|
|
982
1159
|
((e && e.message) || String(e)));
|
|
983
1160
|
}
|
|
1161
|
+
// The known-good backup is restored; drop the quarantined bad binary
|
|
1162
|
+
// (best-effort — a locked / read-only quarantine is operator-cleanable). The
|
|
1163
|
+
// unlink-failure catch is unforceable on the supported platforms.
|
|
1164
|
+
if (hadTarget) {
|
|
1165
|
+
/* c8 ignore next */
|
|
1166
|
+
try { nodeFs.unlinkSync(quarantine); } catch (_q) { /* quarantined bad binary — operator-cleanable */ }
|
|
1167
|
+
}
|
|
984
1168
|
atomicFile.fsyncDir(nodePath.dirname(to));
|
|
985
1169
|
|
|
986
1170
|
_safeAuditEmit("selfupdate.rollback.completed", "success", {
|
|
@@ -1175,6 +1359,7 @@ async function confirmHealthy(opts) {
|
|
|
1175
1359
|
} catch (e) {
|
|
1176
1360
|
throw new SelfUpdateError("selfupdate/probation-confirm-failed",
|
|
1177
1361
|
"selfUpdate.confirmHealthy: failed to clear probation marker " + markerPath + ": " +
|
|
1362
|
+
/* c8 ignore next */
|
|
1178
1363
|
((e && e.message) || String(e)));
|
|
1179
1364
|
}
|
|
1180
1365
|
}
|
|
@@ -1267,18 +1452,23 @@ async function evaluateOnBoot(opts) {
|
|
|
1267
1452
|
try {
|
|
1268
1453
|
var backupBytes = atomicFile.fdSafeReadSync(backupTo, { maxBytes: C.BYTES.gib(1) });
|
|
1269
1454
|
var restoreMode;
|
|
1455
|
+
/* c8 ignore next -- statSync catch is TOCTOU-defensive: `to` was just read for the hash check */
|
|
1270
1456
|
try { restoreMode = (nodeFs.statSync(to).mode & 0o777); } catch (_sm) { restoreMode = 0o600; }
|
|
1271
1457
|
await atomicFile.write(to, backupBytes, { fileMode: restoreMode, overwrite: true });
|
|
1272
1458
|
} catch (e) {
|
|
1273
1459
|
_safeAuditEmit("selfupdate.probation.rollback_failed", "denied", {
|
|
1274
1460
|
to: to, backupTo: backupTo, markerPath: markerPath,
|
|
1461
|
+
/* c8 ignore next */
|
|
1275
1462
|
reason: "restore-failed", message: (e && e.message) || String(e),
|
|
1276
1463
|
});
|
|
1277
1464
|
throw new SelfUpdateError("selfupdate/probation-rollback-failed",
|
|
1278
1465
|
"selfUpdate.evaluateOnBoot: probation rollback restore of " + to + " failed: " +
|
|
1466
|
+
/* c8 ignore next */
|
|
1279
1467
|
((e && e.message) || String(e)));
|
|
1280
1468
|
}
|
|
1281
1469
|
atomicFile.fsyncDir(nodePath.dirname(to));
|
|
1470
|
+
// Marker cleanup is best-effort; the unlink-failure catch is unforceable here.
|
|
1471
|
+
/* c8 ignore next */
|
|
1282
1472
|
try { nodeFs.unlinkSync(markerPath); } catch (_u) { /* marker cleanup best-effort */ }
|
|
1283
1473
|
|
|
1284
1474
|
_safeAuditEmit("selfupdate.probation.rolled_back", "success", {
|
|
@@ -1311,4 +1501,5 @@ module.exports = {
|
|
|
1311
1501
|
compareTags: _compareTags,
|
|
1312
1502
|
// Internal — exposed for the layer-0 test suite only.
|
|
1313
1503
|
_compareTags: _compareTags,
|
|
1504
|
+
_pathsAlias: _pathsAlias,
|
|
1314
1505
|
};
|
package/lib/vendor/MANIFEST.json
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"hashes": {
|
|
19
19
|
"server": "sha256:2b30a26f728c5349f4c4b47834f862a4f77393b1224fc12b22abe3ce2cfab78f"
|
|
20
20
|
},
|
|
21
|
-
"refreshedAt": "2026-07-
|
|
21
|
+
"refreshedAt": "2026-07-27T03:18:03.695Z"
|
|
22
22
|
},
|
|
23
23
|
"@noble/curves": {
|
|
24
24
|
"version": "2.2.0",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"hashes": {
|
|
41
41
|
"server": "sha256:2880c288b1285ef51d356d057bee6f0c8a00de36638cf47b47617e8c1faf10d5"
|
|
42
42
|
},
|
|
43
|
-
"refreshedAt": "2026-07-
|
|
43
|
+
"refreshedAt": "2026-07-27T03:18:03.695Z"
|
|
44
44
|
},
|
|
45
45
|
"@noble/post-quantum": {
|
|
46
46
|
"version": "0.6.1",
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
"hashes": {
|
|
72
72
|
"server": "sha256:f9c94094b3c10fe73dac5343289da582454ea6053494fab2bf66099d9103d6c3"
|
|
73
73
|
},
|
|
74
|
-
"refreshedAt": "2026-07-
|
|
74
|
+
"refreshedAt": "2026-07-27T03:18:03.695Z"
|
|
75
75
|
},
|
|
76
76
|
"@simplewebauthn/server": {
|
|
77
77
|
"version": "13.3.2",
|
|
@@ -94,7 +94,7 @@
|
|
|
94
94
|
"hashes": {
|
|
95
95
|
"server": "sha256:e83195dc9f189385da9c856ef38843f4466f93ea8f3d7fc2efcb1e1b18da6f20"
|
|
96
96
|
},
|
|
97
|
-
"refreshedAt": "2026-07-
|
|
97
|
+
"refreshedAt": "2026-07-27T03:18:03.695Z"
|
|
98
98
|
},
|
|
99
99
|
"SecLists-common-passwords-top-10000": {
|
|
100
100
|
"version": "10k-most-common (master)",
|
|
@@ -114,7 +114,7 @@
|
|
|
114
114
|
},
|
|
115
115
|
"runtime_artifact": "lib/vendor/common-passwords-top-10000.data.js",
|
|
116
116
|
"integrity_layers": "sha256 + sha3-512 + SLH-DSA-SHAKE-256f signature + in-payload canary (where applicable)",
|
|
117
|
-
"refreshedAt": "2026-07-
|
|
117
|
+
"refreshedAt": "2026-07-27T03:18:03.695Z"
|
|
118
118
|
},
|
|
119
119
|
"bimi-trust-anchors": {
|
|
120
120
|
"version": "operator-managed",
|
|
@@ -139,7 +139,7 @@
|
|
|
139
139
|
},
|
|
140
140
|
"runtime_artifact": "lib/vendor/bimi-trust-anchors.data.js",
|
|
141
141
|
"integrity_layers": "sha256 + sha3-512 + SLH-DSA-SHAKE-256f signature + in-payload canary (where applicable)",
|
|
142
|
-
"refreshedAt": "2026-07-
|
|
142
|
+
"refreshedAt": "2026-07-27T03:18:03.695Z"
|
|
143
143
|
},
|
|
144
144
|
"publicsuffix-list": {
|
|
145
145
|
"version": "master",
|
|
@@ -159,38 +159,35 @@
|
|
|
159
159
|
},
|
|
160
160
|
"runtime_artifact": "lib/vendor/public-suffix-list.data.js",
|
|
161
161
|
"integrity_layers": "sha256 + sha3-512 + SLH-DSA-SHAKE-256f signature + in-payload canary (where applicable)",
|
|
162
|
-
"refreshedAt": "2026-07-
|
|
162
|
+
"refreshedAt": "2026-07-27T03:18:03.695Z"
|
|
163
163
|
},
|
|
164
|
-
"
|
|
165
|
-
"version": "
|
|
166
|
-
"license": "
|
|
167
|
-
"author": "
|
|
168
|
-
"source": "https://github.com/
|
|
169
|
-
"_about": "Meta-bundle of @peculiar/x509 + pkijs + reflect-metadata + every transitive ASN.1 schema package. Used by lib/mtls-engine-default.js as the pure-JS CA + PKCS#12 engine wired into b.mtlsCa.",
|
|
170
|
-
"components": {
|
|
171
|
-
"@peculiar/x509": {
|
|
172
|
-
"url": "https://github.com/PeculiarVentures/x509",
|
|
173
|
-
"version": "2.0.0"
|
|
174
|
-
},
|
|
175
|
-
"pkijs": {
|
|
176
|
-
"url": "https://github.com/PeculiarVentures/PKI.js",
|
|
177
|
-
"version": "3.4.0"
|
|
178
|
-
}
|
|
179
|
-
},
|
|
164
|
+
"@blamejs/pki": {
|
|
165
|
+
"version": "0.3.25",
|
|
166
|
+
"license": "Apache-2.0",
|
|
167
|
+
"author": "blamejs",
|
|
168
|
+
"source": "https://github.com/blamejs/pki",
|
|
180
169
|
"exports": [
|
|
181
170
|
"x509",
|
|
182
|
-
"
|
|
183
|
-
"
|
|
171
|
+
"crl",
|
|
172
|
+
"pkcs12",
|
|
173
|
+
"key",
|
|
174
|
+
"webcrypto",
|
|
175
|
+
"schema",
|
|
176
|
+
"csr",
|
|
177
|
+
"cms",
|
|
178
|
+
"ocsp",
|
|
179
|
+
"tsp"
|
|
184
180
|
],
|
|
185
181
|
"files": {
|
|
186
|
-
"server": "lib/vendor/pki.cjs"
|
|
182
|
+
"server": "lib/vendor/blamejs-pki.cjs"
|
|
187
183
|
},
|
|
188
|
-
"bundler": "esbuild --format=cjs --platform=node --
|
|
189
|
-
"bundledAt": "2026-07-
|
|
184
|
+
"bundler": "esbuild --format=cjs --platform=node --external:crypto --external:node:crypto",
|
|
185
|
+
"bundledAt": "2026-07-26T00:00:00Z",
|
|
186
|
+
"cpe": "cpe:2.3:a:blamejs:pki:0.3.25:*:*:*:*:node.js:*:*",
|
|
190
187
|
"hashes": {
|
|
191
|
-
"server": "sha256:
|
|
188
|
+
"server": "sha256:ca475ca19f86eb9cce5ad185132b9e69fea49d1e72bae404c4845cc80727f5be"
|
|
192
189
|
},
|
|
193
|
-
"refreshedAt": "2026-07-
|
|
190
|
+
"refreshedAt": "2026-07-27T03:18:03.695Z"
|
|
194
191
|
}
|
|
195
192
|
}
|
|
196
193
|
}
|