@blamejs/core 0.17.23 → 0.18.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.
@@ -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. Falls back to a lone signature-shaped
329
- // asset only when the release ships exactly one (the common one-asset-one-sig
330
- // case); anything ambiguous fails closed (null) rather than pairing a signature
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: the asset name plus a signature suffix.
334
- for (var s = 0; s < _SIG_SUFFIXES.length; s++) {
335
- var hit = _findEntryByName(entries, assetName + _SIG_SUFFIXES[s]);
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), else null.
353
- var sigShaped = entries.filter(function (e) { return _SIG_SHAPE.test(e.name); });
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` via the same atomic copy used by `swap`.
948
- * Operators run rollback when a post-swap healthcheck reports the new
949
- * binary is bad. Throws SelfUpdateError when the backup file is
950
- * missing or the copy fails.
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, { fileMode: 0o600 });
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
  };
@@ -18,7 +18,7 @@
18
18
  "hashes": {
19
19
  "server": "sha256:2b30a26f728c5349f4c4b47834f862a4f77393b1224fc12b22abe3ce2cfab78f"
20
20
  },
21
- "refreshedAt": "2026-07-25T17:09:57.034Z"
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-25T17:09:57.034Z"
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-25T17:09:57.034Z"
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-25T17:09:57.034Z"
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-25T17:09:57.034Z"
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-25T17:09:57.034Z"
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-25T17:09:57.034Z"
162
+ "refreshedAt": "2026-07-27T03:18:03.695Z"
163
163
  },
164
- "peculiar-pki": {
165
- "version": "2.0.0+pkijs-3.4.0",
166
- "license": "MIT",
167
- "author": "Peculiar Ventures",
168
- "source": "https://github.com/PeculiarVentures",
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
- "pkijs",
183
- "crypto"
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 --alias:reflect-metadata=reflect-metadata/lite --external:node:crypto --external:crypto",
189
- "bundledAt": "2026-07-13T00:00:00Z",
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:2307ef65e070757ffb13442b377e45efb9fa1a10432d9b39618387720ab990ed"
188
+ "server": "sha256:ca475ca19f86eb9cce5ad185132b9e69fea49d1e72bae404c4845cc80727f5be"
192
189
  },
193
- "refreshedAt": "2026-07-25T17:09:57.034Z"
190
+ "refreshedAt": "2026-07-27T03:18:03.695Z"
194
191
  }
195
192
  }
196
193
  }