@blamejs/pki 0.5.32 → 0.5.34

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.
@@ -27,6 +27,8 @@ var _bufferFrom = intrinsic.bufferFrom;
27
27
  // question this guard asked on the caller's behalf. See guard-intrinsic for the whole set.
28
28
  var _isView = intrinsic.isView;
29
29
  var _isBuffer = intrinsic.isBuffer;
30
+ var _asyncIterator = intrinsic.asyncIterator;
31
+ var _NO_ARGS = intrinsic.freeze([]);
30
32
  var _create = intrinsic.create;
31
33
  var _getPrototypeOf = intrinsic.getPrototypeOf;
32
34
  var _getOwnPropertyDescriptor = intrinsic.getOwnPropertyDescriptor;
@@ -80,6 +82,35 @@ function isByteSource(x) {
80
82
  return _isBuffer(x) || _isView(x) || _isArrayBuffer(x);
81
83
  }
82
84
 
85
+ // asyncStreamOf(x) -> a LAZY single-use async iterable of x's byte chunks, or null when x is not a
86
+ // byte stream.
87
+ //
88
+ // A door offering both a byte source and a streamed content (an async iterable of byte chunks) asks
89
+ // this ONE module which it has, instead of re-inlining the `for await` protocol test and drifting on
90
+ // what counts. It reads x's Symbol.asyncIterator EXACTLY ONCE, captures that method, and returns a
91
+ // thin iterable that CALLS it only when the consumer drives the stream. Deferring the call matters
92
+ // twice: the underlying iterator is acquired once, in the digest engine, so reading the protocol
93
+ // property a second time downstream cannot fire a one-shot or stateful accessor and refuse a stream a
94
+ // direct `for await` accepts; AND a caller whose iterator factory opens a file or socket has that
95
+ // resource opened only on a path that will actually consume it, never on a pre-hash rejection (a
96
+ // missing detached option, an empty signer list, a message needing no streamed digest) that would
97
+ // otherwise acquire and leak it. A byte source is bytes, never a stream, EVEN when a caller attached a
98
+ // Symbol.asyncIterator to it: the byte-source classification wins and is checked FIRST, so a Buffer /
99
+ // typed array / DataView goes down the byte path every time and its own accessors are never invoked
100
+ // here. A callable carrying the method is a valid async iterable that `for await` consumes, so it too
101
+ // yields a stream.
102
+ // @enforced-by behavioral -- a lazy stream wrapper has no rename-proof code shape; the RED vectors (a
103
+ // stream produced where a byte source is not, a one-shot-accessor stream still consumed, and the
104
+ // factory NOT invoked until the stream is driven) are the guard.
105
+ function asyncStreamOf(x) {
106
+ if (isByteSource(x) || x == null || (typeof x !== "object" && typeof x !== "function")) return null;
107
+ var acquire = x[_asyncIterator];
108
+ if (typeof acquire !== "function") return null;
109
+ var stream = {};
110
+ stream[_asyncIterator] = function () { return _apply(acquire, x, _NO_ARGS); };
111
+ return stream;
112
+ }
113
+
83
114
  // Is this value a byte store, asked of the internal slot rather than the prototype?
84
115
  //
85
116
  // `v instanceof ArrayBuffer` answers what a value inherits from, which is a different question
@@ -1057,6 +1088,6 @@ function fixedCall(ErrorClass, code, args, body) {
1057
1088
  // call, so a writable export would let a caller replace the check rather than pass it.
1058
1089
  module.exports = intrinsic.freeze({
1059
1090
  view: view, source: source, snapshot: snapshot, snapshotSource: snapshotSource,
1060
- isByteSource: isByteSource, lengthOf: lengthOf, outputView: outputView,
1091
+ isByteSource: isByteSource, asyncStreamOf: asyncStreamOf, lengthOf: lengthOf, outputView: outputView,
1061
1092
  snapshotDeep: snapshotDeep, fixArguments: fixArguments, fixedCall: fixedCall,
1062
1093
  });
@@ -59,6 +59,10 @@ var _bufferCompare = Buffer.compare;
59
59
  // response is about this issuer, a responder ID says this key signed it. Dispatched live, one
60
60
  // answering true makes a response about a DIFFERENT certificate match the one being asked about.
61
61
  var _bufferEquals = uncurry(Buffer.prototype.equals);
62
+ // Buffer#toString, uncurried at load. Used to build dedup keys (a hex string) from bytes the toolkit
63
+ // owns; dispatched live, a replaced toString could make two distinct values key alike or one value key
64
+ // differently on two reads, so a dedup over untrusted-adjacent bytes would merge or split wrongly.
65
+ var _bufferToString = uncurry(Buffer.prototype.toString);
62
66
 
63
67
  // The rest of what a guard reads from the runtime while it is deciding something. These are
64
68
  // grouped here rather than captured file by file so the set is enumerable in one place and a guard
@@ -94,6 +98,13 @@ var _mathMax = Math.max;
94
98
  var _parseInt = parseInt;
95
99
  var _promiseResolve = Promise.resolve;
96
100
  var _promiseReject = Promise.reject;
101
+ // The well-known async-iteration symbol, captured at load. `for await` uses this symbol at the
102
+ // language level regardless of the global `Symbol` object, so a classifier that reads
103
+ // `Symbol.asyncIterator` live would disagree with the loop the moment a co-resident replaced the
104
+ // writable `Symbol` global: a real async iterable would be looked up under the wrong key, judged not
105
+ // iterable, and refused where the engine would have consumed it. Reading the captured symbol keeps
106
+ // the door's answer and the loop's behavior the same.
107
+ var _asyncIterator = Symbol.asyncIterator;
97
108
 
98
109
  // The array operations, uncurried at load. These run on arrays the TOOLKIT owns -- an `ownKeys`
99
110
  // result, a local accumulator -- so the receiver is never a caller's object; what is caller-
@@ -274,6 +285,7 @@ module.exports = Object.freeze({
274
285
  bufferFrom: _bufferFrom,
275
286
  bufferAlloc: _bufferAlloc,
276
287
  bufferConcat: _bufferConcat,
288
+ bufToString: _bufferToString,
277
289
  isBuffer: _bufferIsBuffer,
278
290
  byteLength: _bufferByteLength,
279
291
  compare: _bufferCompare,
@@ -309,6 +321,7 @@ module.exports = Object.freeze({
309
321
  bigIntToString: _bigIntToString,
310
322
  promiseResolve: _promiseResolve,
311
323
  promiseReject: _promiseReject,
324
+ asyncIterator: _asyncIterator,
312
325
  forEach: _arrForEach,
313
326
  map: _arrMap,
314
327
  filter: _arrFilter,
package/lib/pki-build.js CHANGED
@@ -70,7 +70,8 @@ function _isSubDelim(c) {
70
70
  function _isUserinfoByte(c) { return _isUnreserved(c) || _isSubDelim(c) || c === 58; }
71
71
  // A path/query/fragment byte: unreserved / sub-delim / a non-bracket gen-delim (":" "/" "?" "#" "@").
72
72
  // "%" is handled separately (it must open a "%HEXHEX" triplet); the IP-literal brackets "[" "]" are
73
- // excluded, so an IPv6-literal host is refused by the shorthand and uses the object form.
73
+ // excluded here because they are authority syntax -- a bracketed IPv6 host is validated in
74
+ // _validUriAuthority (RFC 3986 sec. 3.2.2), never in the path.
74
75
  function _isUriByte(c) {
75
76
  return _isUnreserved(c) || _isSubDelim(c) || c === 58 || c === 47 || c === 63 || c === 35 || c === 64;
76
77
  }
@@ -122,8 +123,8 @@ function _componentBytesOk(s, from, to, byteOk) {
122
123
  // or an FQDN of strict-LDH labels (the email-domain grammar: no underscore, no hyphen at a label edge) with
123
124
  // at least one dot -- a single-label host such as "localhost" is not fully qualified and uses the object
124
125
  // form, matching the email domain. An IPv4-SHAPED host that is not a valid address is refused as ambiguous
125
- // (the same rule the top-level classifier uses), and an IPv6-literal host -- needing the excluded "[" "]"
126
- // -- uses the object form.
126
+ // (the same rule the top-level classifier uses). An IPv6-literal host is written in "[" "]" and validated
127
+ // by _validUriIp6Host from _validUriAuthority, never reaching this unbracketed-host check.
127
128
  function _validUriHost(s, from, to) {
128
129
  if (to <= from) return false; // empty host
129
130
  var host = _sliceStr(s, from, to), len = host.length;
@@ -135,10 +136,21 @@ function _validUriHost(s, from, to) {
135
136
  return _labelsValid(host, 0, len, _isHostLabelRange, true); // strict-LDH FQDN, root "." allowed
136
137
  }
137
138
 
139
+ // A bracketed IPv6-literal URI host (RFC 3986 sec. 3.2.2): the bytes between "[" and "]" must be a valid
140
+ // IPv6 address (16 packed octets), never an IPv4 literal (which uses the unbracketed IPv4address form). The
141
+ // bracket is the ONLY way to write an IPv6 host in a URI, and an IPv6 host is a valid iPAddress per RFC 5280
142
+ // sec. 4.2.1.6, so the shorthand accepts it rather than forcing the object form.
143
+ function _validUriIp6Host(s, from, to) {
144
+ if (to <= from) return false; // empty "[ ]"
145
+ var packed = _packIpLiteral(_sliceStr(s, from, to));
146
+ return packed !== null && intrinsic.sizeOf(packed) === 16; // IPv6 literal only (16 octets)
147
+ }
148
+
138
149
  // The authority in [from,to) is "[ userinfo "@" ] host [ ":" port ]" (RFC 3986 sec. 3.2): at most one "@"
139
- // (userinfo and reg-name each exclude it), a host validated by _validUriHost (RFC 5280 FQDN or IPv4), and
140
- // an all-digit -- possibly empty -- port after the host's single ":" (a reg-name has no ":"). The userinfo
141
- // bytes are validated against the RFC 3986 userinfo class.
150
+ // (userinfo and reg-name each exclude it), a host that is an RFC 5280 FQDN or IPv4 (_validUriHost) or a
151
+ // bracketed IPv6 literal (_validUriIp6Host), and an all-digit -- possibly empty -- port after the host's
152
+ // ":". A bracketed host is taken as ONE unit before the port, so a ":" inside the IPv6 literal is not read
153
+ // as the port separator. The userinfo bytes are validated against the RFC 3986 userinfo class.
142
154
  function _validUriAuthority(s, from, to) {
143
155
  var i, c, at = -1;
144
156
  for (i = from; i < to; i += 1) {
@@ -147,6 +159,17 @@ function _validUriAuthority(s, from, to) {
147
159
  }
148
160
  var hostStart = at === -1 ? from : at + 1;
149
161
  if (at !== -1 && !_componentBytesOk(s, from, at, _isUserinfoByte)) return false; // userinfo
162
+ if (hostStart < to && _charCodeAt(s, hostStart) === 91) { // "[" -> bracketed IPv6 host
163
+ var close = -1;
164
+ for (i = hostStart + 1; i < to; i += 1) { if (_charCodeAt(s, i) === 93) { close = i; break; } } // "]"
165
+ if (close === -1) return false; // unterminated "["
166
+ if (!_validUriIp6Host(s, hostStart + 1, close)) return false; // IPv6 literal in "[ ]"
167
+ var after = close + 1;
168
+ if (after === to) return true; // "[ ipv6 ]" with no port
169
+ if (_charCodeAt(s, after) !== 58) return false; // only a ":" port may follow "]"
170
+ for (i = after + 1; i < to; i += 1) { if (!_isDigitCode(_charCodeAt(s, i))) return false; } // port = *DIGIT
171
+ return true;
172
+ }
150
173
  var colon = -1;
151
174
  for (i = hostStart; i < to; i += 1) { if (_charCodeAt(s, i) === 58) { colon = i; break; } } // ":" -> port
152
175
  var hostEnd = colon === -1 ? to : colon;
package/lib/webcrypto.js CHANGED
@@ -54,6 +54,14 @@ var _hasOwn = intrinsic.hasOwn;
54
54
  var _stringify = intrinsic.stringify;
55
55
  var _bufferFrom = intrinsic.bufferFrom;
56
56
  var _bufToString = intrinsic.uncurry(Buffer.prototype.toString);
57
+ // The node Hash instance methods, captured at load. digestStream feeds chunks across `await` points,
58
+ // and a co-resident that replaced Hash.prototype.update while the stream was suspended between chunks
59
+ // could drop or alter one -- making digest("ab") equal digest("a") -- and, because both streamed
60
+ // signing and verification trust this digest, forge a valid verdict over bytes the stream never
61
+ // yielded. Calling the captured update/digest keeps the hash bound to the bytes the stream saw. The
62
+ // one-shot digest needs no such capture: its single update and digest run with no suspension between.
63
+ var _hashUpdate = intrinsic.uncurry(nodeCrypto.Hash.prototype.update);
64
+ var _hashDigest = intrinsic.uncurry(nodeCrypto.Hash.prototype.digest);
57
65
  var _bufferConcat = intrinsic.bufferConcat;
58
66
  var _bufferAlloc = intrinsic.bufferAlloc;
59
67
  var _isBuffer = intrinsic.isBuffer;
@@ -76,6 +84,9 @@ var _Uint8Array = intrinsic.Uint8Array;
76
84
  var _assign = intrinsic.assign;
77
85
  var _create = intrinsic.create;
78
86
  var _isView = intrinsic.isView;
87
+ var _asyncIterator = intrinsic.asyncIterator;
88
+ var _apply = intrinsic.apply;
89
+ var _NO_ARGS = intrinsic.freeze([]);
79
90
  var _toUpperCase = intrinsic.toUpperCase;
80
91
  var _toLowerCase = intrinsic.toLowerCase;
81
92
  var _stringIndexOf = intrinsic.stringIndexOf;
@@ -334,6 +345,74 @@ SubtleCrypto.prototype.digest = async function digest(algorithm, data) {
334
345
  return _toArrayBuffer(h.digest());
335
346
  };
336
347
 
348
+ // Digest an async iterable of byte chunks under several algorithms in ONE pass, returning one
349
+ // ArrayBuffer per algorithm in order. Each result is byte-identical to digest(algorithms[i], <the
350
+ // concatenated chunks>): the same createHash + XOF handling, fed incrementally so an arbitrarily
351
+ // large payload is never held whole. The single pass is what lets a multi-signer detached CMS sign
352
+ // hash a single-use stream once for every distinct digest algorithm. Which algorithm each Hash runs
353
+ // is fixed up front through the captured name resolution (_normalizeAlg / _toUpperCase / _hashNode),
354
+ // exactly as the one-shot digest resolves it, so a replaced toUpperCase cannot swap SHA-256 for SHA-1
355
+ // mid-stream. A chunk that is not a BufferSource throws webcrypto/data, before it reaches a hash.
356
+ SubtleCrypto.prototype.digestStream = async function digestStream(algorithms, source) {
357
+ if (!_isArray(algorithms) || !algorithms.length) {
358
+ throw new WebCryptoError("webcrypto/syntax", "digestStream: algorithms must be a non-empty array of digest algorithms");
359
+ }
360
+ // Follow the async-iteration protocol exactly as `for await` does: read the source's
361
+ // Symbol.asyncIterator ONCE, call it to obtain the iterator, then capture THAT iterator's `next` with
362
+ // a single read and drive it manually. Driving `source` by its own `next` would diverge from
363
+ // `for await` whenever Symbol.asyncIterator returns a distinct iterator, and an inherited `next` (say
364
+ // from a polluted prototype) would misclassify an ordinary iterable; going through the method avoids
365
+ // both. Native `for await` caches `next` after one read, so capturing each method once and reusing it
366
+ // -- rather than re-reading to drive the loop -- also consumes a one-shot or stateful accessor a
367
+ // direct `for await` accepts.
368
+ // Build (and thereby validate) the hashes BEFORE acquiring the iterator: an unsupported algorithm
369
+ // must reject before the iterator factory runs, so a factory that opens a file or socket is never
370
+ // invoked -- and never leaked outside the cleanup guard -- on a rejection, keeping the lazy-stream
371
+ // guarantee that a pre-hash rejection acquires no resource.
372
+ // A plain index loop, not `_map`: Array.prototype.map SKIPS holes, so a sparse list such as
373
+ // `[, "SHA-256"]` would leave a hole in `hashes` (later read as `undefined` and updated as a raw
374
+ // TypeError) or, if fully sparse, yield sparse output. Indexing every slot instead makes a hole read
375
+ // as `undefined`, which `_normalizeAlg` rejects with a typed webcrypto/syntax before any chunk is read.
376
+ var hashes = [];
377
+ for (var _h = 0; _h < algorithms.length; _h++) {
378
+ var name = _normalizeAlg(algorithms[_h], "digestStream").name;
379
+ var xof = XOF_NODE[_toUpperCase(_String(name))];
380
+ hashes[_h] = xof
381
+ ? nodeCrypto.createHash(xof.node, { outputLength: xof.length })
382
+ : nodeCrypto.createHash(_hashNode(name, "digestStream"));
383
+ }
384
+ var acquire = source == null ? undefined : source[_asyncIterator];
385
+ if (typeof acquire !== "function") {
386
+ throw new WebCryptoError("webcrypto/syntax", "digestStream: source must be an async iterable of byte chunks");
387
+ }
388
+ var iterator = _apply(acquire, source, _NO_ARGS);
389
+ try {
390
+ // The `next` retrieval and its validation are INSIDE the guard: the factory above may have opened a
391
+ // resource, so a non-callable next (or a throwing next accessor) must still reach the cleanup below
392
+ // that calls the iterator's return(), rather than leak on a malformed iterator protocol.
393
+ var nextMethod = iterator == null ? undefined : iterator.next;
394
+ if (typeof nextMethod !== "function") {
395
+ throw new WebCryptoError("webcrypto/syntax", "digestStream: source must be an async iterable of byte chunks");
396
+ }
397
+ for (;;) {
398
+ var step = await _apply(nextMethod, iterator, _NO_ARGS);
399
+ if (step && step.done) break;
400
+ var buf = _toBuf(step == null ? undefined : step.value, "digestStream chunk");
401
+ for (var i = 0; i < hashes.length; i++) _hashUpdate(hashes[i], buf);
402
+ }
403
+ } catch (e) {
404
+ // Best-effort completion, as `for await` performs on an early exit or throw: let the source release.
405
+ // The retrieval of `return` is INSIDE the guard too, so a throwing `return` accessor cannot replace
406
+ // the original error -- the malformed-chunk verdict is what the caller must see, not a cleanup fault.
407
+ try {
408
+ var ret = iterator == null ? undefined : iterator.return;
409
+ if (typeof ret === "function") await _apply(ret, iterator, _NO_ARGS);
410
+ } catch (_r) { /* cleanup is best-effort */ }
411
+ throw e;
412
+ }
413
+ return _map(hashes, function (h) { return _toArrayBuffer(_hashDigest(h)); });
414
+ };
415
+
337
416
  SubtleCrypto.prototype.generateKey = async function generateKey(algorithm, extractable, keyUsages) {
338
417
  var alg = _normalizeAlg(algorithm, "generateKey");
339
418
  // Before the partition below, not only in the CryptoKey constructor: the asymmetric branch splits
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/pki",
3
- "version": "0.5.32",
3
+ "version": "0.5.34",
4
4
  "description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
@@ -83,6 +83,6 @@
83
83
  "devDependencies": {
84
84
  "c8": "12.0.0",
85
85
  "esbuild": "0.28.2",
86
- "eslint": "10.8.1"
86
+ "eslint": "10.9.1"
87
87
  }
88
88
  }
package/sbom.cdx.json CHANGED
@@ -2,10 +2,10 @@
2
2
  "$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
3
3
  "bomFormat": "CycloneDX",
4
4
  "specVersion": "1.5",
5
- "serialNumber": "urn:uuid:e0315ce7-f8af-453b-be94-cae61e3f47ea",
5
+ "serialNumber": "urn:uuid:849173ca-4a17-451d-91be-1f2d75d83f00",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-08-28T18:08:22.906Z",
8
+ "timestamp": "2026-08-29T10:04:44.768Z",
9
9
  "lifecycles": [
10
10
  {
11
11
  "phase": "build"
@@ -19,14 +19,14 @@
19
19
  }
20
20
  ],
21
21
  "component": {
22
- "bom-ref": "@blamejs/pki@0.5.32",
22
+ "bom-ref": "@blamejs/pki@0.5.34",
23
23
  "type": "application",
24
24
  "name": "pki",
25
- "version": "0.5.32",
25
+ "version": "0.5.34",
26
26
  "scope": "required",
27
27
  "author": "blamejs contributors",
28
28
  "description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
29
- "purl": "pkg:npm/%40blamejs/pki@0.5.32",
29
+ "purl": "pkg:npm/%40blamejs/pki@0.5.34",
30
30
  "properties": [],
31
31
  "externalReferences": [
32
32
  {
@@ -54,7 +54,7 @@
54
54
  "components": [],
55
55
  "dependencies": [
56
56
  {
57
- "ref": "@blamejs/pki@0.5.32",
57
+ "ref": "@blamejs/pki@0.5.34",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]