@blamejs/pki 0.6.15 → 0.6.16

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 CHANGED
@@ -4,6 +4,19 @@ All notable changes to `@blamejs/pki` are documented here. The format
4
4
  follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this
5
5
  project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## v0.6.16 — 2026-09-01
8
+
9
+ pki.scep.enroll and pki.scep.renew poll a PENDING enrollment to its issued certificate, sending RFC 8894 CertPoll requests until the CA answers or a bounded poll-count and wait budget is exhausted.
10
+
11
+ ### Added
12
+
13
+ - pki.scep.build and pki.scep.parse now handle the CertPoll (GetCertInitial, messageType 20) request: a pkcsPKIEnvelope carrying an IssuerAndSubject (the CA's subject name and the request's subject name), signed under the transaction attributes with the original transactionID (RFC 8894 sec. 3.3.3).
14
+ - pki.scep.enroll and pki.scep.renew poll a PENDING response to a terminal status. New options bound the loop: pollCount (maximum CertPoll requests, default 5), maxTotalWait (total Retry-After sleep budget in seconds), sleep (an injectable (ms) -> Promise sleeper), and onRetryAfter (an observer of each delay). A budget exhausted with no terminal response throws scep/poll-exhausted carrying the transactionId to resume with. For a deployment whose request is encrypted to a separate RA certificate, issuerCert names the issuing CA the CertPoll must carry (default caCert).
15
+
16
+ ### Changed
17
+
18
+ - pki.scep.enroll and pki.scep.renew poll by default when the CA answers PENDING: a PENDING enrollment now resolves to the issued certificate or throws, rather than returning { status: "PENDING" } for the caller to retry. Pass pollCount: 0 to restore the single-shot PENDING return and drive the retry yourself.
19
+
7
20
  ## v0.6.15 — 2026-09-01
8
21
 
9
22
  Verifying a Sigstore bundle no longer matches a pinned identity against a non-text subjectAltName value; the value is surfaced as null so the policy fails closed instead of comparing raw bytes. The library source also ships without its explanatory comments, keeping the JSDoc that generates the API reference and the SPDX license headers.
package/lib/scep.js CHANGED
@@ -53,6 +53,8 @@ var pkix = require("./schema-pkix");
53
53
  var guard = require("./guard-all");
54
54
  var C = require("./constants");
55
55
  var httpTransport = require("./http-transport");
56
+ var retryAfter = require("./http-retry-after");
57
+ var _defaultSleep = require("./sleep").sleep;
56
58
  var nodeCrypto = require("node:crypto");
57
59
 
58
60
  function _err(code, msg, cause) { return new ScepError(code, msg, cause); }
@@ -66,7 +68,7 @@ var FAIL_INFO = { badAlg: "0", badMessageCheck: "1", badRequest: "2", badTime: "
66
68
  function _invert(table) { var o = Object.create(null); for (var k in table) if (Object.prototype.hasOwnProperty.call(table, k)) o[table[k]] = k; return o; }
67
69
  var MESSAGE_TYPE_BY_CODE = _invert(MESSAGE_TYPE), PKI_STATUS_BY_CODE = _invert(PKI_STATUS), FAIL_INFO_BY_CODE = _invert(FAIL_INFO);
68
70
 
69
- var _PARSE_SUPPORTED = { PKCSReq: 1, RenewalReq: 1, CertRep: 1 };
71
+ var _PARSE_SUPPORTED = { PKCSReq: 1, RenewalReq: 1, CertRep: 1, CertPoll: 1 };
70
72
 
71
73
  var ENVELOPE_CIPHER = "aes-128-cbc";
72
74
 
@@ -239,23 +241,28 @@ async function build(spec) {
239
241
  guard.identifier.assertKnownKeys(spec, _BUILD_KEYS, _err, "scep/bad-input", "pki.scep.build has an unknown option: ");
240
242
  var mtCode = MESSAGE_TYPE[spec.messageType];
241
243
  if (mtCode == null) throw _err("scep/bad-message-type", "unknown messageType " + JSON.stringify(spec.messageType) + " (RFC 8894 sec. 3.2.1.2)");
242
- if (spec.messageType !== "PKCSReq" && spec.messageType !== "RenewalReq") {
243
- throw _err("scep/bad-message-type", "pki.scep.build issues a request (PKCSReq / RenewalReq); " + spec.messageType + " is not built in this release");
244
+ if (spec.messageType !== "PKCSReq" && spec.messageType !== "RenewalReq" && spec.messageType !== "CertPoll") {
245
+ throw _err("scep/bad-message-type", "pki.scep.build issues a request (PKCSReq / RenewalReq / CertPoll); " + spec.messageType + " is not built in this release");
244
246
  }
245
247
  if (spec.signer == null || typeof spec.signer !== "object") throw _err("scep/bad-input", "a signer { cert, key } is required");
246
248
  var senderNonce = _nonce(spec.senderNonce, "senderNonce");
247
249
  var txnId = _transactionId(spec.transactionId);
248
- var messageData = guard.bytes.snapshotSource(spec.messageData, ScepError, "scep/bad-input", "messageData");
249
250
  var recipient = guard.bytes.snapshotSource(spec.recipient, ScepError, "scep/bad-input", "recipient");
250
251
  _assertRsaRecipient(recipient);
251
252
  var signerCert = guard.bytes.snapshotSource(spec.signer.cert, ScepError, "scep/bad-input", "signer.cert");
252
253
  _assertSigningKeyUsage(signerCert, "signer certificate");
253
254
  var signerKey = spec.signer.key;
254
- var parsedCsr;
255
- try { parsedCsr = schemaCsr.parse(messageData); }
256
- catch (e) { throw _err("scep/bad-input", "messageData is not a valid PKCS#10 CertificationRequest (RFC 8894 sec. 3.3.1): " + e.message, e); }
257
- if (!(await csrVerify.verifyCsrSignature(parsedCsr))) {
258
- throw _err("scep/bad-popo", "the PKCS#10 messageData failed its proof-of-possession: its self-signature does not verify under its own subject public key, so a CA would reject it");
255
+ var messageData;
256
+ if (spec.messageType === "CertPoll") {
257
+ messageData = _buildIssuerAndSubject(recipient, spec.requestSubject, spec.issuer);
258
+ } else {
259
+ messageData = guard.bytes.snapshotSource(spec.messageData, ScepError, "scep/bad-input", "messageData");
260
+ var parsedCsr;
261
+ try { parsedCsr = schemaCsr.parse(messageData); }
262
+ catch (e) { throw _err("scep/bad-input", "messageData is not a valid PKCS#10 CertificationRequest (RFC 8894 sec. 3.3.1): " + e.message, e); }
263
+ if (!(await csrVerify.verifyCsrSignature(parsedCsr))) {
264
+ throw _err("scep/bad-popo", "the PKCS#10 messageData failed its proof-of-possession: its self-signature does not verify under its own subject public key, so a CA would reject it");
265
+ }
259
266
  }
260
267
  var pkcsPKIEnvelope;
261
268
  try {
@@ -276,7 +283,26 @@ async function build(spec) {
276
283
  throw e;
277
284
  }
278
285
  }
279
- var _BUILD_KEYS = { messageType: 1, messageData: 1, recipient: 1, signer: 1, transactionId: 1, senderNonce: 1 };
286
+ var _BUILD_KEYS = { messageType: 1, messageData: 1, recipient: 1, requestSubject: 1, issuer: 1, signer: 1, transactionId: 1, senderNonce: 1 };
287
+
288
+ function _buildIssuerAndSubject(recipientCertDer, requestSubjectSource, issuerOverride) {
289
+ var subject = guard.bytes.snapshotSource(requestSubjectSource, ScepError, "scep/bad-input", "requestSubject");
290
+ var issuer, issuerNode, subjNode;
291
+ try {
292
+ issuer = issuerOverride != null
293
+ ? guard.bytes.snapshotSource(issuerOverride, ScepError, "scep/bad-input", "issuer")
294
+ : schemaX509.parse(recipientCertDer).subject.bytes;
295
+ issuerNode = asn1.decode(issuer);
296
+ subjNode = asn1.decode(subject);
297
+ } catch (e) {
298
+ if (e instanceof ScepError) throw e;
299
+ throw _err("scep/bad-input", "the CertPoll IssuerAndSubject Names are not valid DER (RFC 8894 sec. 3.3.3): " + e.message, e);
300
+ }
301
+ if (!pkix.rootSequenceChildren(issuerNode, 0) || !pkix.rootSequenceChildren(subjNode, 0)) {
302
+ throw _err("scep/bad-input", "the CertPoll issuer and requestSubject must each be a Name (a SEQUENCE) DER (RFC 8894 sec. 3.3.3)");
303
+ }
304
+ return b.sequence([issuer, subject]);
305
+ }
280
306
 
281
307
  /**
282
308
  * @primitive pki.scep.parse
@@ -289,7 +315,7 @@ var _BUILD_KEYS = { messageType: 1, messageData: 1, recipient: 1, signer: 1, tra
289
315
  * Disassemble a SCEP pkiMessage (RFC 8894 sec. 3): verify the outer SignedData signature, read the
290
316
  * transaction attributes BOUND to the verified signer (never from a separate untrusted parse), and,
291
317
  * given the recipient key, decrypt the pkcsPKIEnvelope to recover the messageData. It reads the
292
- * enrollment message types PKCSReq, RenewalReq, and CertRep; the CertPoll, GetCert, and GetCRL client
318
+ * enrollment message types PKCSReq, RenewalReq, CertRep, and CertPoll; the GetCert and GetCRL client
293
319
  * queries a CA processes are refused (`scep/unsupported-message-type`). The verdict is
294
320
  * `{ signatureValid, signerAuthenticated, signerCert, messageType, transactionId, senderNonce,
295
321
  * recipientNonce, pkiStatus, failInfo, failInfoText, messageData, certificates, crls }` (fields null
@@ -418,6 +444,14 @@ async function parse(bytes, opts) {
418
444
  } else if (messageType === "PKCSReq" || messageType === "RenewalReq") {
419
445
  try { schemaCsr.parse(dec.content); }
420
446
  catch (e) { throw _err("scep/bad-request-payload", "a " + messageType + " messageData must be a PKCS#10 CertificationRequest (RFC 8894 sec. 3.3.1)", e); }
447
+ } else if (messageType === "CertPoll") {
448
+ var ias;
449
+ try { ias = asn1.decode(dec.content); }
450
+ catch (e) { throw _err("scep/bad-request-payload", "a CertPoll messageData must be an IssuerAndSubject (RFC 8894 sec. 3.3.3)", e); }
451
+ var iasCh = pkix.rootSequenceChildren(ias, 2, 2);
452
+ if (!iasCh || !pkix.rootSequenceChildren(iasCh[0], 0) || !pkix.rootSequenceChildren(iasCh[1], 0)) {
453
+ throw _err("scep/bad-request-payload", "a CertPoll messageData must be an IssuerAndSubject: a SEQUENCE of two Name values (RFC 8894 sec. 3.3.3)");
454
+ }
421
455
  }
422
456
  }
423
457
  return out;
@@ -763,6 +797,62 @@ async function getNextCACert(baseUrl, opts) {
763
797
  function _defaultTransactionId() {
764
798
  return nodeCrypto.randomBytes(16).toString("hex");
765
799
  }
800
+ var POLL_COUNT_MAX = 1000;
801
+
802
+ function _dispatchCertRep(rep, transactionId, spki) {
803
+ if (rep.messageType !== "CertRep") throw _err("scep/unexpected-message-type", "the SCEP response is a " + rep.messageType + ", not a CertRep");
804
+ if (rep.transactionId !== transactionId) throw _err("scep/transaction-id-mismatch", "the CertRep transactionID does not match the request (RFC 8894 sec. 3.2.1.1)");
805
+ if (rep.pkiStatus === "FAILURE") {
806
+ var failErr = _err("scep/enrollment-failed", "the CA refused the request with failInfo " + JSON.stringify(rep.failInfo) + (rep.failInfoText ? " (" + rep.failInfoText + ")" : ""));
807
+ failErr.failInfo = rep.failInfo;
808
+ failErr.failInfoText = rep.failInfoText;
809
+ throw failErr;
810
+ }
811
+ if (rep.pkiStatus === "PENDING") return { pending: true };
812
+ var repCerts = rep.certificates || [];
813
+ var issued = findIssuedCert(repCerts, spki.bytes);
814
+ if (issued == null) throw _err("scep/no-issued-cert", "the SUCCESS CertRep did not carry a certificate matching the request public key");
815
+ if (findIssuedCert(repCerts.filter(function (c) { return c !== issued; }), spki.bytes) != null) {
816
+ throw _err("scep/ambiguous-issued-cert", "the SUCCESS CertRep carried more than one certificate matching the request public key; the issued certificate is ambiguous (RFC 8894 sec. 3.3)");
817
+ }
818
+ return { pending: false, result: { status: "SUCCESS", certificate: issued, certificates: repCerts, transactionId: rep.transactionId } };
819
+ }
820
+
821
+ function _pollExhausted(which, transactionId, senderNonce) {
822
+ var e = _err("scep/poll-exhausted", "the CA kept the request PENDING and " + which + " was exhausted before it resolved (RFC 8894 sec. 4.4); resume the transaction later with this transactionId");
823
+ e.transactionId = transactionId;
824
+ e.senderNonce = senderNonce;
825
+ return e;
826
+ }
827
+
828
+ async function _pollForIssuance(ctx) {
829
+ var issuerName;
830
+ try { issuerName = schemaX509.parse(ctx.issuerCert).subject.bytes; }
831
+ catch (e) { throw _err("scep/bad-input", "the issuerCert whose subject names the CertPoll issuer is not a parseable certificate (RFC 8894 sec. 3.3.3)", e); }
832
+ var res = ctx.firstResponse, count = 0, waitedMs = 0, lastNonce = ctx.firstSenderNonce;
833
+ for (;;) {
834
+ var delaySec = 1;
835
+ var ra = _header(res.headers, "retry-after");
836
+ if (typeof ra === "string" && ra.trim() !== "") {
837
+ var parsed = retryAfter.parse(ra, { now: Date.now(), E: _err, code: "scep/bad-input", lenient: true });
838
+ if (parsed.retryAfterSeconds != null) delaySec = parsed.retryAfterSeconds;
839
+ }
840
+ count += 1;
841
+ if (count > ctx.pollCount) throw _pollExhausted("the poll count", ctx.transactionId, lastNonce);
842
+ waitedMs += delaySec * C.TIME.seconds(1);
843
+ if (waitedMs > ctx.maxTotalWait * C.TIME.seconds(1)) throw _pollExhausted("the total wait budget", ctx.transactionId, lastNonce);
844
+ if (ctx.onRetryAfter) ctx.onRetryAfter(delaySec);
845
+ await ctx.sleep(delaySec * C.TIME.seconds(1));
846
+ var sn = nodeCrypto.randomBytes(16);
847
+ lastNonce = sn;
848
+ var certPoll = await build({ messageType: "CertPoll", requestSubject: ctx.csrSubject, issuer: issuerName, recipient: ctx.caCert, signer: ctx.signer, transactionId: ctx.transactionId, senderNonce: sn });
849
+ res = await _client("POST", ctx.postUrl, certPoll, { "content-type": CT_PKI_MESSAGE }, ctx.clientOpts);
850
+ var body = _requireOk(res, [CT_PKI_MESSAGE], "PKIOperation");
851
+ var rep = await parse(body, { signerCert: ctx.signerCert, expectedSenderNonce: sn, recipientKey: ctx.recipientKey });
852
+ var d = _dispatchCertRep(rep, ctx.transactionId, ctx.spki);
853
+ if (!d.pending) return d.result;
854
+ }
855
+ }
766
856
  async function _pkiOperation(messageType, baseUrl, opts, verbName) {
767
857
  opts = guard.identifier.optionsObject(opts, _err, "scep/bad-input", "pki.scep." + verbName + " options");
768
858
  guard.identifier.assertKnownKeys(opts, _ENROLL_KEYS, _err, "scep/bad-input", "pki.scep." + verbName + " has an unknown option: ");
@@ -772,8 +862,9 @@ async function _pkiOperation(messageType, baseUrl, opts, verbName) {
772
862
  if (opts.recipientKey == null || typeof opts.recipientKey !== "object") throw _err("scep/bad-input", "a recipientKey { cert, key } is required to decrypt the CA response");
773
863
  var caCert = guard.bytes.snapshotSource(opts.caCert, ScepError, "scep/bad-input", "the caCert");
774
864
  var signerCert = opts.responderCert != null ? guard.bytes.snapshotSource(opts.responderCert, ScepError, "scep/bad-input", "the responderCert") : caCert;
865
+ var issuerCert = opts.issuerCert != null ? guard.bytes.snapshotSource(opts.issuerCert, ScepError, "scep/bad-input", "the issuerCert") : caCert;
775
866
  var recipientKey = _captureRecipientKey(opts.recipientKey);
776
- var signer = opts.signer;
867
+ var signer = { cert: guard.bytes.snapshotSource(opts.signer.cert, ScepError, "scep/bad-input", "signer.cert"), key: opts.signer.key };
777
868
  var clientOpts = {
778
869
  transport: opts.transport,
779
870
  tls: opts.tls != null ? Object.assign({}, opts.tls) : opts.tls,
@@ -784,31 +875,28 @@ async function _pkiOperation(messageType, baseUrl, opts, verbName) {
784
875
  };
785
876
  var postUrl = _scepUrl(baseUrl, "PKIOperation");
786
877
  var csrView = guard.bytes.snapshotSource(opts.csr, ScepError, "scep/bad-input", "the csr");
787
- var spki;
788
- try { spki = schemaCsr.parse(csrView).subjectPublicKeyInfo; }
878
+ var spki, csrSubject;
879
+ try { var _pcsr = schemaCsr.parse(csrView); spki = _pcsr.subjectPublicKeyInfo; csrSubject = _pcsr.subject.bytes; }
789
880
  catch (e) { throw _err("scep/bad-input", "the csr is not a valid PKCS#10 CertificationRequest", e); }
881
+ var pollCount = guard.limits.cap(opts.pollCount, "pollCount", 5, { E: _err, code: "scep/bad-input", min: 0, max: POLL_COUNT_MAX });
882
+ var maxTotalWait = guard.limits.cap(opts.maxTotalWait, "maxTotalWait", retryAfter.MAX_RETRY_AFTER_SECONDS, { E: _err, code: "scep/bad-input", min: 0, max: retryAfter.MAX_RETRY_AFTER_SECONDS });
883
+ var sleep = typeof opts.sleep === "function" ? opts.sleep : _defaultSleep;
884
+ var onRetryAfter = typeof opts.onRetryAfter === "function" ? opts.onRetryAfter : null;
790
885
  var senderNonce = nodeCrypto.randomBytes(16);
791
886
  var transactionId = opts.transactionId != null ? opts.transactionId : _defaultTransactionId();
792
887
  var reqMsg = await build({ messageType: messageType, messageData: csrView, recipient: caCert, signer: signer, transactionId: transactionId, senderNonce: senderNonce });
793
888
  var res = await _client("POST", postUrl, reqMsg, { "content-type": CT_PKI_MESSAGE }, clientOpts);
794
889
  var body = _requireOk(res, [CT_PKI_MESSAGE], "PKIOperation");
795
890
  var rep = await parse(body, { signerCert: signerCert, expectedSenderNonce: senderNonce, recipientKey: recipientKey });
796
- if (rep.messageType !== "CertRep") throw _err("scep/unexpected-message-type", "the SCEP response is a " + rep.messageType + ", not a CertRep");
797
- if (rep.transactionId !== transactionId) throw _err("scep/transaction-id-mismatch", "the CertRep transactionID does not match the request (RFC 8894 sec. 3.2.1.1)");
798
- if (rep.pkiStatus === "FAILURE") {
799
- var failErr = _err("scep/enrollment-failed", "the CA refused the request with failInfo " + JSON.stringify(rep.failInfo) + (rep.failInfoText ? " (" + rep.failInfoText + ")" : ""));
800
- failErr.failInfo = rep.failInfo;
801
- failErr.failInfoText = rep.failInfoText;
802
- throw failErr;
803
- }
804
- if (rep.pkiStatus === "PENDING") return { status: "PENDING", transactionId: rep.transactionId, senderNonce: senderNonce };
805
- var repCerts = rep.certificates || [];
806
- var issued = findIssuedCert(repCerts, spki.bytes);
807
- if (issued == null) throw _err("scep/no-issued-cert", "the SUCCESS CertRep did not carry a certificate matching the request public key");
808
- if (findIssuedCert(repCerts.filter(function (c) { return c !== issued; }), spki.bytes) != null) {
809
- throw _err("scep/ambiguous-issued-cert", "the SUCCESS CertRep carried more than one certificate matching the request public key; the issued certificate is ambiguous (RFC 8894 sec. 3.3)");
810
- }
811
- return { status: "SUCCESS", certificate: issued, certificates: repCerts, transactionId: rep.transactionId };
891
+ var d = _dispatchCertRep(rep, transactionId, spki);
892
+ if (!d.pending) return d.result;
893
+ if (pollCount <= 0) return { status: "PENDING", transactionId: rep.transactionId, senderNonce: senderNonce };
894
+ return _pollForIssuance({
895
+ firstResponse: res, firstSenderNonce: senderNonce, postUrl: postUrl, clientOpts: clientOpts,
896
+ caCert: caCert, csrSubject: csrSubject, issuerCert: issuerCert, signer: signer, signerCert: signerCert, recipientKey: recipientKey,
897
+ spki: spki, transactionId: transactionId, pollCount: pollCount, maxTotalWait: maxTotalWait,
898
+ sleep: sleep, onRetryAfter: onRetryAfter,
899
+ });
812
900
  }
813
901
 
814
902
  /**
@@ -824,8 +912,13 @@ async function _pkiOperation(messageType, baseUrl, opts, verbName) {
824
912
  * the CertRep. The CA response is authenticated against the CA certificate and its `recipientNonce` must
825
913
  * echo the request's fresh `senderNonce`. On SUCCESS the issued certificate is selected out of the
826
914
  * response by SubjectPublicKeyInfo byte-match against the request; a FAILURE throws
827
- * `scep/enrollment-failed` carrying the CA's failInfo; a PENDING returns `{ status: "PENDING",
828
- * transactionId }` for the caller to retry. The content key is transported under RSAES-OAEP, so the CA
915
+ * `scep/enrollment-failed` carrying the CA's failInfo. A CA that answers PENDING (manual enrollment, RFC
916
+ * 8894 sec. 2.5) is polled automatically: the client issues CertPoll (GetCertInitial) messages reusing
917
+ * the transactionID, each authenticated against the CA certificate with a fresh nonce it must echo,
918
+ * until the CA answers SUCCESS or FAILURE or a `pollCount` / `maxTotalWait` budget is exhausted
919
+ * (`scep/poll-exhausted`, carrying the `transactionId` to resume). Pass `pollCount: 0` to disable polling
920
+ * and get the single `{ status: "PENDING", transactionId }` return for a caller that drives the retry
921
+ * itself. The content key is transported under RSAES-OAEP, so the CA
829
922
  * certificate must be an RSA `keyEncipherment` certificate that accepts OAEP; the shared-secret
830
923
  * authenticator (RFC 8894 sec. 2.4) is the `challengePassword` the caller placed in the PKCS#10.
831
924
  *
@@ -835,14 +928,19 @@ async function _pkiOperation(messageType, baseUrl, opts, verbName) {
835
928
  * - `signer` -- `{ cert, key }`, the outer SignedData signer (for a PKCSReq, a self-signed certificate over the request key).
836
929
  * - `recipientKey` -- `{ cert, key }`, the RSA key the CA encrypts the response to, to decrypt the CertRep.
837
930
  * - `responderCert` -- the certificate to authenticate the CA's response signature against (defaults to `caCert`).
931
+ * - `issuerCert` -- the issuing CA certificate whose subject names the CertPoll issuer (defaults to `caCert`; set it when the request is encrypted to a separate RA certificate, RFC 8894 sec. 3.3.3).
838
932
  * - `transactionId` -- a caller-unique PrintableString (defaults to 16 random bytes, hex-encoded, unique per call).
933
+ * - `pollCount` -- the maximum number of CertPoll requests on a PENDING response (default 5; `0` disables polling).
934
+ * - `maxTotalWait` -- the total Retry-After sleep budget in seconds across all polls (default the Retry-After horizon).
935
+ * - `sleep` -- an injectable sleeper `(ms) -> Promise` for the Retry-After wait (default a real timer).
936
+ * - `onRetryAfter` -- an optional observer `(delaySeconds) -> void` called before each poll sleep.
839
937
  * - `transport` / `timeout` / `maxResponseBytes` / `maxRedirects` / `tls` / `allowCrossOriginRedirect` -- transport bounds.
840
938
  * @example
841
939
  * // requires: scepCsr, caCertDer, clientCertDer, clientKeyPkcs8, scepTransport
842
940
  * var out = await pki.scep.enroll("http://ca.example/scep", { csr: scepCsr, caCert: caCertDer,
843
941
  * signer: { cert: clientCertDer, key: clientKeyPkcs8 },
844
942
  * recipientKey: { cert: clientCertDer, key: clientKeyPkcs8 }, transport: scepTransport });
845
- * out.status; // "SUCCESS", or "PENDING" to retry
943
+ * out.status; // "SUCCESS" (a PENDING request is polled to a terminal status unless pollCount is 0)
846
944
  */
847
945
  function enroll(baseUrl, opts) { return _pkiOperation("PKCSReq", baseUrl, opts, "enroll"); }
848
946
 
@@ -874,7 +972,7 @@ function _withKeys(extra) { var o = {}, k; for (k in _TRANSPORT_KEYS) o[k] = 1;
874
972
  var _CACAPS_KEYS = _withKeys({ expectSCEPStandard: 1, requireStrongProfile: 1 });
875
973
  var _CACERT_KEYS = _withKeys({ expectedFingerprint: 1, fingerprintAlgorithm: 1 });
876
974
  var _NEXTCACERT_KEYS = _withKeys({ caCertificate: 1 });
877
- var _ENROLL_KEYS = _withKeys({ csr: 1, caCert: 1, signer: 1, recipientKey: 1, responderCert: 1, transactionId: 1 });
975
+ var _ENROLL_KEYS = _withKeys({ csr: 1, caCert: 1, signer: 1, recipientKey: 1, responderCert: 1, issuerCert: 1, transactionId: 1, pollCount: 1, maxTotalWait: 1, sleep: 1, onRetryAfter: 1 });
878
976
 
879
977
  module.exports = {
880
978
  build: build,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/pki",
3
- "version": "0.6.15",
3
+ "version": "0.6.16",
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",
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:b21c243d-9b6e-49ab-9c19-3427ff43e9bc",
5
+ "serialNumber": "urn:uuid:e63bdf7d-807a-4e39-8cb7-7eb6a47b5808",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-09-01T08:58:18.522Z",
8
+ "timestamp": "2026-09-01T10:42:10.477Z",
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.6.15",
22
+ "bom-ref": "@blamejs/pki@0.6.16",
23
23
  "type": "application",
24
24
  "name": "pki",
25
- "version": "0.6.15",
25
+ "version": "0.6.16",
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.6.15",
29
+ "purl": "pkg:npm/%40blamejs/pki@0.6.16",
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.6.15",
57
+ "ref": "@blamejs/pki@0.6.16",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]