feat!: give asynchronous work its own entry points - #564
Conversation
Three things want an asynchronous path and none of them had a working one:
Web Crypto (`crypto.subtle` is promise-only), remote keys in an HSM or KMS,
and `checkSignature`'s own callback form — which called `verifySignature` in
its three-argument synchronous shape and never passed the callback down, so an
async-only verifier could not report a valid signature at all.
`computeSignatureAsync` and `checkSignatureAsync` join the synchronous pair,
and `validateElementAgainstReferencesAsync` joins `validateElementAgainstReferences`.
The logic is written once. Only three operations can be asynchronous —
hashing, signing, verifying — so each flow is a sequence of synchronous phases
with two barriers, and the mode lives at the top of the two orchestrators
rather than being threaded through five private methods:
prepareSignature -> collectReferenceDigests -> [hash] -> canonicalize
SignedInfo -> [sign] -> finalizeSignature
prepareVerification -> locateReferences -> [hash] -> compare digests ->
[verify] -> conclude
`collectReferenceDigests` builds the `Reference` elements and leaves each
`DigestValue` empty; `locateReference` resolves and canonicalizes, and
`acceptReferenceDigest` compares. Everything between the barriers is shared, so
the two entry points produce byte-identical output.
The methods on `HashAlgorithm` and `SignatureAlgorithm` become optional and
gain `Async` twins. An implementation provides whichever forms its backend
supports and no more: nobody writes both. The asynchronous entry points fall
back to a synchronous method, so they accept every algorithm the synchronous
ones do; the reverse cannot work, so reaching an async-only algorithm from
`computeSignature` names the entry point that would have:
WebCryptoSha256 is async-only; use computeSignatureAsync()
The callback overloads and `createOptionalCallbackFunction` are gone. "Sync
unless you pass a callback" was observable: switching `signatureAlgorithm`
changed whether the caller's own try/catch caught a handler's error and whether
state assigned after the call was visible to the handler. Node's answer to this
is a pair of separately named functions, and that is what this is. Passing a
callback now throws a `TypeError` naming the replacement, because signing
successfully and never calling back is the silent break the removal exists to
avoid.
BREAKING CHANGE: `computeSignature(xml, callback)`,
`computeSignature(xml, options, callback)` and `checkSignature(xml, callback)`
are removed, as are `createOptionalCallbackFunction` and `ErrorFirstCallback`.
Use `computeSignatureAsync()` / `checkSignatureAsync()`. `getHash`,
`getSignature` and `verifySignature` are now optional members of their
interfaces; existing synchronous implementations are unaffected, but code that
calls them through the interface type has to account for that.
Closes #546
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Warning Review limit reachedNext included review available in 59 minutes. View limit detailsLimit details: You’ve used all 2 included reviews currently available. You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Review configuration: ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (6)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Closed in favour of #571 — the same commits, opened from 🤖 Generated with Claude Code |
Closes #546
Three things want an asynchronous path and none of them had a working one: Web Crypto (
crypto.subtleis promise-only), remote keys in an HSM or KMS, andcheckSignature's own callback form.That last one is worth stating plainly — it did not work at all.
checkSignaturecalledverifySignaturein its three-argument synchronous shape and never passed the callback down. Reproduced onmasterwith an async-only verifier:A verifier that answers only through its callback cannot report a valid signature.
checkSignatureAsyncreplaces that path, and there is a test for it citing the issue.New entry points
computeSignatureAsync,checkSignatureAsync, andvalidateElementAgainstReferencesAsyncjoin their synchronous counterparts.computeSignatureAsyncresolves with the instance sogetSignedXml()can be chained.Written once
Only three operations can be asynchronous, so each flow is a sequence of synchronous phases with two barriers, and the mode lives at the top of two thin orchestrators rather than being threaded through five private methods:
collectReferenceDigestsbuilds theReferenceelements and leaves eachDigestValueempty.validateReferencesplits intolocateReference(resolve and canonicalize) andacceptReferenceDigest(compare, record signed content).validateElementAgainstReferencesbecomes two thin loops over one lazy generator, so a caller that matches on the first reference still does no work for the rest.Everything between the barriers is shared, so the two entry points produce identical output. Asserted for all four bundled signature algorithms; the pre-existing byte-exact expected-XML test now runs through
computeSignatureAsyncand still matches to the byte.Optional async twins
getHash,getSignatureandverifySignaturebecome optional and gaingetHashAsync,getSignatureAsync,verifySignatureAsync. An implementation provides whichever forms its backend supports and no more — nobody writes both, which is what the issue asks for. Making the synchronous methods optional is what lets an async-only algorithm exist without a throwing stub; the guards below make the failure legible instead.The asynchronous entry points fall back to a synchronous method, so they accept every algorithm the synchronous ones do, the bundled
node:cryptoalgorithms included.Fail closed at the wrong entry point
Retiring the callbacks
createOptionalCallbackFunctionandErrorFirstCallbackare gone. "Sync unless you pass a callback" was observable: switchingsignatureAlgorithmchanged whether the caller's owntry/catchcaught a handler's error and whether state assigned after the call was visible to the handler. Node's answer to this is a pair of separately named functions.The issue asks for the break to be loud. For TypeScript it is a compile error. For JavaScript it would have been silent — signing successfully and never calling back — so
computeSignatureandcheckSignaturereject a function argument:Open questions from the issue
*Asyncmethods onSignedXml, not a separate class.checkSignature's async path in 6.x first? Superseded here rather than repaired; the callback form it belonged to is gone.computeSignatureAsyncwith nosignatureAlgorithmrejects rather than throwing before the promise settles.Not changed
Reference digests are still checked before the
SignedInfosignature. TheTODOabout reversing that order is a separate behaviour change and stays aTODO.Verification
npm run build && npm test && npm run lintclean; 257 passing (241 + 16). All pre-existing tests pass unmodified apart from the one that used the callback form.🤖 Generated with Claude Code