webcrypto: ML-DSA + ML-KEM, ChaCha20-Poly1305, raw-secret/raw-public, toCryptoKey, v26 SubtleCrypto surface (+10 tests, webcrypto 58%→76%) - #34838
Conversation
Bun's WebCrypto reported the right failures with the wrong names, codes and
ordering, so code that works on Bun can behave differently on Node. Each
behavior below was checked against a real Node v26.3.0 build before changing
it, and the vendored Node webcrypto tests are synced to that version.
CryptoKey.usages / JWK key_ops ordering. usages() walked the usage bitmap
alphabetically. The KeyUsage enum in the WebCrypto spec orders them encrypt,
decrypt, sign, verify, deriveKey, deriveBits, wrapKey, unwrapKey, which is what
Node emits. Most visible on ECDH: Node reports ["deriveKey","deriveBits"] and
Bun reported the reverse. key_ops is built from usages(), so JWK export was
wrong too.
SubtleCrypto error messages. Aligned the operation guards with Node's wording:
"CryptoKey doesn't match AlgorithmIdentifier" -> "Key algorithm mismatch";
"CryptoKey doesn't support encryption" -> "Unable to use this key to encrypt"
(likewise decrypt/sign/verify/wrapKey/unwrapKey); the two derive cases ->
"baseKey does not have deriveBits/deriveKey usage"; "The CryptoKey is
nonextractable" -> "key is not extractable". deriveBits/deriveKey also checked
the algorithm match before the usage; Node checks usage first, and on
doubly-invalid input the order decides which error wins.
Missing vs ill-typed dictionary members. throwRequiredMemberTypeError only
fires when a required member is absent, which Node reports as
ERR_MISSING_OPTION rather than ERR_INVALID_ARG_TYPE; that holds for every
required member, including a params dictionary with no name at all. A member
that was present but ill-typed threw a bare "Type error" with no code, so
deriveBits({name:'ECDH', public:{}}) was indistinguishable from an internal
failure; it now reports ERR_INVALID_ARG_TYPE like Node. Both helpers are used
only by the webcrypto bindings.
crypto.getRandomValues. Float32Array, Float64Array, DataView, ArrayBuffer and
SharedArrayBuffer were all filled with random bytes. The spec allows only
integer-typed views; everything else must raise TypeMismatchError, which Node
does. The message already existed, only the check was missing.
HKDF / PBKDF2 / ECDH derived-length errors. A null length and a length that is
not a multiple of 8 both produced an OperationError with an empty message,
surfacing as the generic "The operation failed for an operation-specific
reason". Node distinguishes "length cannot be null" from "length must be a
multiple of 8", and ECDH/X25519 say "derived bit length is too small".
Illegal constructor. Interfaces that cannot be constructed at all routed both
call and construct through the "called without new" path, so new CryptoKey()
answered "Use `new CryptoKey(...)` instead of `CryptoKey(...)`" — advice it had
already followed. Both paths now report "Illegal constructor", matching Node,
browsers, and the wording the Rust helper's own documentation already claimed
to produce. MessagePort opts into ERR_CONSTRUCT_CALL_INVALID and keeps Node's
message for that code, "Constructor cannot be called".
Tests: the vendored Node webcrypto tests are byte-identical to upstream, and
only tests that pass are added — tests needing algorithms Bun does not
implement are left alone rather than quarantined. test-webcrypto-random.js had
been edited to assert Bun's old behavior ("These types are allowed in Bun");
those assertions are restored, leaving only the 65536-byte quota commented out,
since that needs a QuotaExceededError global Bun does not have yet.
…ard hash aliases
Second pass on Node v26.3.0 webcrypto compatibility, taking the vendored
upstream suite from 22 to 30 passing files. As before, every behavior was
checked against a real Node v26.3.0 build first.
Non-standard hash aliases removed. The registry registered SHA1/SHA224/SHA256/
SHA384/SHA512 as alternative names beside the hyphenated spec names. It already
matches case-insensitively as the spec requires, so the alias table was an extra
deviation: `hash: 'SHA256'` was accepted where Node and browsers throw
NotSupportedError, meaning code written against Bun could fail elsewhere. Nothing
in the repo relied on them (`Bun.CryptoHasher("SHA256")` and node:crypto's
`createHash` are separate surfaces). Dropping them also lets the registry
re-assert that each algorithm registers exactly once.
Unrecognized algorithm names. Only `digest` said "Unrecognized algorithm name",
via a hardcoded string that overrode every message. The other operations reported
the DOMException default, "The operation is not supported.". The message now comes
from the layer that owns the invariant — every failing lookup in
normalizeCryptoAlgorithmParameters — so all operations agree with Node, including
a recognized name used for the wrong operation (`digest('AES-GCM')`).
Import errors. Every import rejection carried the right error name but a generic
message: JWK "use"/"alg"/"crv" mismatches, unsupported key usages, and empty
usages on a private or secret key all collapsed into "Data provided to an
operation does not meet requirements". They now carry Node's text.
"Invalid key type" vs "Invalid keyData". Importing an EC key as RSA reported the
same generic DataError as random bytes. RSA/EC/OKP conflated "the parser found a
well-formed key of another type" with "this is not a key", because both were one
condition. They are split, and the distinction reaches the caller through an
optional out-param. OKP hand-parses its DER, and a byte compare cannot tell those
two cases apart, so it confirms with the real parser before claiming a type
mismatch — otherwise garbage would be reported as a wrong-typed key.
Ed25519 JWK "alg". Export omitted it and import never checked it, so a JWK with
`alg: 'foo'` was accepted. RFC 8037 gives the Edwards curves an "alg" of the curve
name or "EdDSA"; the montgomery curves have none, so X25519 still omits it.
ECDH derive errors, HKDF info. ECDH reported nothing for a curve or algorithm
mismatch. HKDF accepted an `info` longer than the 1024 bytes Node allows.
util.inspect(CryptoKey) printed `CryptoKey {}`. The attributes are prototype
accessors, so the key has no own enumerable properties. It now prints the four
slots like Node, reading the internal state directly rather than through the
getters, whose cached objects user code can mutate.
Tests: 17 more upstream files (30 of Node's 53), all byte-identical to upstream.
Three still test nothing under BoringSSL (`common.skip` on `requires OpenSSL >= 3`).
The rest need algorithms or APIs Bun lacks — ML-KEM, ML-DSA, TurboSHAKE,
ChaCha20-Poly1305, AES-OCB, Ed448/X448, subtle.getPublicKey, SubtleCrypto.supports,
KeyObject.prototype.toCryptoKey, QuotaExceededError — or Node's internal/* module
registry, which is not portable.
body.test.ts built its fixtures with `crypto.getRandomValues(new bufferType(n))` over a list that includes ArrayBuffer, SharedArrayBuffer and the Float views. getRandomValues takes integer-typed views only — Node throws TypeMismatchError for all five — so the fixtures were relying on Bun's older, laxer check. Fill through a Uint8Array view over the same bytes instead, which keeps every bufferType in the matrix.
…memory `random_get` called `crypto.getRandomValues(this.memory.buffer, bufPtr, bufLen)`. getRandomValues takes a single integer-typed view and ignores any further arguments, so bufPtr and bufLen were dropped on the floor and the whole wasm linear memory was overwritten with random bytes on every call — 65241 of 65536 bytes outside the requested 16-byte window, in the added test's terms. Passing a Uint8Array view over exactly the requested range fixes that, and also keeps the syscall working now that getRandomValues rejects a plain ArrayBuffer: every wasm guest that reaches random_get (Rust getrandom, Go crypto/rand, WASI-libc getentropy) would otherwise trap.
WASI preview1 defines random_get as returning errno (0 on success). The handler returned bufLen, so a guest asking for 16 bytes would see errno 16 back. Pre-existing on main, but since this PR already touches the function to fix the windowing bug it makes sense to complete the fix here. The test now asserts WASI_ESUCCESS and the tautological expect(WASI_ESUCCESS).toBe(0) is dropped.
…ters normalizeCryptoAlgorithmParameters declares a JSC ThrowScope, so every caller must perform an exception check before the next exception-scope use. digest was the only SubtleCrypto method that did; generateKey, deriveKey, deriveBits, and importKey had RETURN_IF_EXCEPTION after the early return that rejects the promise, and encrypt, sign, verify, wrapKey, and unwrapKey had no scope at all. With BUN_JSC_validateExceptionChecks=1, 25 of 36 crypto.subtle paths aborted, including the decrypt, wrapKey, and unwrapKey success paths. Give every method the digest shape: RETURN_IF_EXCEPTION right after the normalize call, and RELEASE_AND_RETURN on the trailing algorithm call, whose callbacks can reach DeferredPromise synchronously. The two wrapped-key callback lambdas get the same treatment for their throw-scoped toJS<IDLDictionary<JsonWebKey>>, JSONStringify, and JSONParse calls. The reorder in wrapKey also fixes a user-reachable debug assertion: a throwing `name` getter on the wrap algorithm makes the first normalize return ExistingExceptionError, which tripped the ASSERT at SubtleCrypto.cpp:1144 and then ran the second normalize with a pending JS exception. No user-visible change on release builds: the fixture's 39-case transcript is byte-identical before and after.
…lback
m_pendingPromises is a HashMap<DeferredPromise*, Ref<DeferredPromise>> and
get() peeks the value as a raw pointer, so removing the map entry before
rejecting frees the DeferredPromise the reject is about to use. The
unwrapKey callback already holds a RefPtr across this pattern; match it.
Exercise the path with a fixture case that makes wrapKey("jwk")'s internal
JSON.stringify throw via an inherited Object.prototype.toJSON. Without the
exception check this PR adds after JSONStringify, that input also leaves
the promise unsettled forever on release builds: the pending exception
survives into a later DeferredPromise::reject, which reports it as
uncaught and returns without settling.
The keyTypeMismatch out-param was only set when the 4-byte OID prefix mismatched, but Ed25519/X25519/Ed448/X448 all share that prefix and differ only in the fifth byte. Importing an Ed25519 SPKI as X25519 fell through the switch arm without setting the flag and reported the generic 'Invalid keyData' instead of Node's 'Invalid key type'. Cover both switch arms in importSpki and importPkcs8 via a shared lambda, still gated on the real DER parser so garbage with a matching prefix is not misreported. Also: add Float16Array to the getRandomValues rejection matrix (the old test asserted it was filled; nothing covered the new rejection), and trim the Bun-authored divergence comment in test-webcrypto-random.js to 3 lines.
… OKP-as-EC as key-type mismatch Same-class follow-ups to the error-message work: - importKey's 'Usages cannot be empty when importing a private/secret key.' now also appears in deriveKey's and unwrapKey's inner import callbacks, which share the identical predicate. generateKey carries Node's 'Usages cannot be empty when creating a key.' for both the single-key and key-pair arms. - CryptoKeyEC::platformImportSpki required a two-element AlgorithmIdentifier before checking the OID, so an OKP SPKI (one-element AlgId per RFC 8410) bailed without setting keyTypeMismatch and reported 'Invalid keyData' instead of 'Invalid key type'. Set the flag at that guard too, gated on d2i_PUBKEY succeeding so garbage is not misreported. importPkcs8 already checks EVP_PKEY_base_id and was unaffected.
ECDH's deriveBits got Node's 'key algorithm mismatch' and 'Named curve
mismatch', but the byte-identical checks in X25519's parallel were left as the
empty-message InvalidAccessError. Line 86 is reachable today
(deriveBits({name:'X25519', public: ecdhKey}, x25519Private)), and the vendored
cfrg tests assert this exact message behind a keys.X448 guard that is skipped
under BoringSSL, so it would fail the moment X448 lands.
New test covers both directions against the ECDH sibling.
…JWK alg straggler The four RSA importKey implementations have the same usage-guard shape that ECDSA/ECDH/Ed25519/X25519 got Node's 'Unsupported key usage for a <name> key' for; all four RSA files were already touched here to thread keyTypeMismatch and add the JWK 'use'/'alg' messages, so the usage guards in the same functions get the same treatment. Also: RSAES-PKCS1-v1_5's JWK 'alg' mismatch was the only one of the four still at the empty message; its three siblings got the message in this PR. generateKey's usage checks are left alone: the EC/OKP files left those at the empty message too, and AES/HMAC importKey is in files this PR does not touch.
Node's ec.js and cfrg.js build the message from a single template that uses 'an'; the RSA siblings added in 9cb0754 matched that, but the EC/OKP parallels said 'a ECDSA' etc. The vendored tests only assert on the /Unsupported key usage/ regex so both spellings pass; this aligns the exact text with Node and the RSA siblings.
Implements the ChaCha20-Poly1305 WebCrypto algorithm over BoringSSL's EVP_aead_chacha20_poly1305, with an AeadParams dictionary derived from the identical AesGcmParams shape, and adds node's `raw-secret` import/export format. `raw-secret` is also accepted as a `raw` alias for AES-* and HMAC, matching node. CryptoKeyRaw gains extractability so it can back a name-only key algorithm, as node's does. Other digests and ciphers in this area were ruled out first by grepping the vendored BoringSSL: TurboSHAKE, KangarooTwelve, AES-OCB and Argon2 have no symbols there, so those tests are blocked on the primitive rather than on binding work. JsonWebKey.kty is no longer a required IDL member. WebCrypto's dictionary does not mark it required, and node rejects a non-JWK object with DataError where bun threw a TypeError; both now throw DataError. One bun-owned assertion in test/js/web/crypto/web-crypto.test.ts is updated to the node-parity error while keeping its settle and leak guard intact. No vendored node test was edited. Adds test-webcrypto-encrypt-decrypt-chacha20-poly1305 and test-webcrypto-aead-decrypt-detached-buffer from Node v26.3.0, verbatim, with their fixture.
|
Updated 5:10 PM PT - Jul 23rd, 2026
✅ @cirospaciari, your commit d8947814ee5207498eea353d11cb99abaaa25003 passed in 🧪 To try this PR locally: bunx bun-pr 34838That installs a local version of the PR into your bun-34838 --bun |
|
Found 2 issues this PR may fix:
🤖 Generated with Claude Code |
….3.0
Passes on this branch as-is; copied verbatim from upstream. It forges all four
CryptoKey prototype getters and checks that util.inspect, exportKey('jwk'),
KeyObject.from, createHmac and crypto.sign/verify all read the native slots
rather than the forged ones, and that Object.prototype pollution of hash /
publicExponent does not leak into a generated AES-GCM key.
Verified 3/3 and tamper-checked, with the same tamper run against the node
v26.3.0 binary as a control.
No-Verification-Needed: test-only diff, no runtime surface to drive
Node routes every algorithm through the same validateKeyOps, so the duplicate scan the AKP arm already had applies to the oct algorithms too. Extract it as hasDuplicateJwkKeyOps next to the JsonWebKey struct and call it from all three JWK pre-validation blocks. [allow size]
Buffer formats no longer run the JWK dictionary conversion, so a poisoned member getter cannot fire and no pending exception is cleared before reporting node's type error; jwk conversion errors still propagate. Mirror the optional kty in convertDictionaryToJS, and collapse the RSAES KEM-usage matrix to the single deprecation assertion its gate allows. [allow size]
There was a problem hiding this comment.
No new findings on this pass — all prior rounds' items are addressed and resolved. That said, this is a ~460k-char diff across 85 files adding six new crypto algorithms (ML-DSA-44/65/87, ML-KEM-768/1024, ChaCha20-Poly1305), a new CryptoKeyAKP key class, four new KeyUsage values, three new key formats, and the v26 SubtleCrypto surface (supports/getPublicKey/encap/decap) plus KeyObject.prototype.toCryptoKey, so it warrants a human look.
What was reviewed across passes: the reachable RELEASE_ASSERT on structuredClone of ChaCha/AKP keys (now DataCloneError); deriveKey/encap/decap → inner-importKey format routing for ChaCha/HKDF/PBKDF2; the raw-secret/raw-public/raw-seed aliasing sweep across every algorithm's import/export switch and its ordering vs. the extractable check in wrapKey; the kty-optional IDL change and both mirrored converters; importKey's per-format keyData converter (no tryClearException, no getter reads on buffer formats); the shared hasDuplicateJwkKeyOps for AKP/ChaCha/HMAC; and the KEM-usage bitmap sweep across every usagesAreInvalid predicate.
Extended reasoning...
Overview
This PR implements post-quantum ML-DSA (44/65/87) and ML-KEM (768/1024) plus ChaCha20-Poly1305 as WebCrypto algorithms over BoringSSL's EVP layer, backed by a new CryptoKeyAKP key class with JWK kty:"AKP", seed-only PKCS#8, and raw-public/raw-seed formats. It also adds Node's v26 SubtleCrypto surface (SubtleCrypto.supports() static, getPublicKey(), encapsulateBits/Key, decapsulateBits/Key), KeyObject.prototype.toCryptoKey, four new KEM CryptoKeyUsage values, and the raw-secret key format. Cross-cutting: JsonWebKey.kty becomes optional in the IDL, Crypto.prototype.subtle moves to the prototype with an ERR_INVALID_THIS brand check, and structured clone of the new key types throws DataCloneError (Node clones them; documented as a follow-up). Nearly every existing CryptoAlgorithm*.cpp gets touched for the new formats and KEM-usage exclusions.
Security risks
This is core crypto: new signature (ML-DSA) and KEM (ML-KEM) primitives wired to BoringSSL, new AEAD cipher (ChaCha20-Poly1305), new key import/export formats, and a bridge from node:crypto KeyObject into WebCrypto CryptoKey. The BoringSSL calls look correctly bounded (two-call length-then-fill patterns, ERR_clear_error on failure paths, RAII EvpPKeyPtr/EvpPKeyCtxPtr), JWK pub is verified against the seed-derived public key on private-JWK import, and all length/tag/nonce validation runs before dispatch — but a human should confirm the ML-DSA context-string handling, the seed-only PKCS#8 length table, and that SubtleCrypto.supports()'s tryClearException inside the normalize lambda is scoped to converter noise only.
Level of scrutiny
High. New user-facing crypto API surface, new key class, security-sensitive C++ over BoringSSL, and architectural additions (encapsulate/decapsulate as new CryptoAlgorithm virtuals, CryptoKeyClass::AKP). The structured-clone DataCloneError compromise and the subtle prototype-getter relocation in ZigGlobalObject are design decisions worth a maintainer's eye.
Other factors
The PR has been through many automated review rounds; every raised finding (including the original process-aborting structuredClone crash, the deriveKey format mismatch, the tryClearException swallowing user throws, and several sibling-site sweeps) was addressed with live-Node verification and regression tests. Eleven vendored Node tests are added byte-identical to upstream, and the description reports clean regression runs including BUN_JSC_validateExceptionChecks=1. The remaining scale and security sensitivity are what put this beyond auto-approval.
Node clones these key types; Bun rejected them with a deliberate DataCloneError guard. Key serialization format version 2 adds: - a CryptoKeyClassSubtag::AKP arm — public keys serialize as raw public bytes, private keys as PKCS#8 (which embeds the FIPS 203/204 seed, so raw-seed exportability survives the round trip like in Node) - the four KEM usage tags, replacing the countUsages desync assert - identifier tags for ChaCha20-Poly1305 and the five ML algorithms (ChaCha rides the existing Raw key-class arm) - readRawKey now honors the stored extractable flag; HKDF/PBKDF2 keys always stored false, so v1 payloads deserialize unchanged Also from the same review batch: - undersized AEAD decrypt inputs reject with the default OperationError message like Node (ChaCha20-Poly1305 and the pre-existing AES-GCM site; nothing asserted the old text) - BoringSSL error-queue hygiene on every new failure path (ChaCha seal/open, ML-KEM ctx/init, ML-DSA sign/verify init, AKP generatePair and exports): clear the queue unless the caller attaches it as a cause, per the importAkpKey convention - exportRawSeed shrinks its buffer to the reported length, matching rawPublicKeyBytes Tests: clone round-trips (material/algorithm/usages/extractable) for ChaCha and both ML halves, extractable=false preservation, a Worker postMessage delivery test, and the decrypt message; all fail on the unfixed build. Vendored webcrypto ML/ChaCha/clone-transfer suites pass. [allow size]
… key readAKPKey's importRawPublic/importPkcs8 leave the parse error in the queue on failure and nothing attaches it as a cause, matching the convention the rest of the clone change follows. Also await the two rejection assertions added with the clone tests so they can actually fail, and pin the non-extractable export message. [allow size]
The AES-GCM arm of the message change had no test; extend the ChaCha20-Poly1305 case to a matrix (verified generic in node v26.3.0). [allow size]
There was a problem hiding this comment.
No new findings on d894781. This is a large, security-sensitive crypto PR (85 files: ML-DSA/ML-KEM over BoringSSL, ChaCha20-Poly1305, new SubtleCrypto surface, structured-clone format bump, Crypto.prototype.subtle brand check), so it warrants a human sign-off.
What was reviewed across the earlier rounds (all resolved):
- structuredClone of ChaCha20-Poly1305/AKP keys — reachable
RELEASE_ASSERT, extractable round-trip, BoringSSL error-queue hygiene inreadAKPKey. importKeykeyData converter dispatch — format-first branch, noclearException, zero getter invocations on buffer formats;kty-optional consistency across both JWK converters.- Format aliasing at all four import/export/wrap/unwrap sites;
wrapKeyextractable-check ordering;deriveKey/encapsulateKeyinner-import format for ChaCha/HKDF/PBKDF2. - JWK validation ordering and duplicate
key_opsfor HMAC/ChaCha/AKP; HMAC's deadimportJwkdeleted; test vacuity (un-awaited.rejects, RSAES parameterization, AES-GCM undersized-decrypt message).
Extended reasoning...
Overview
This PR implements Node v26's WebCrypto surface across 85 files: ML-DSA-44/65/87 and ML-KEM-768/1024 (new CryptoKeyAKP class over BoringSSL EVP), ChaCha20-Poly1305 (EVP_aead_chacha20_poly1305), the raw-secret/raw-public/raw-seed key formats with per-algorithm aliasing, SubtleCrypto methods encapsulate{Bits,Key}/decapsulate{Bits,Key}/getPublicKey/supports, KeyObject.prototype.toCryptoKey, four new KEM key-usages, and a broad error-contract sweep. It bumps the SerializedScriptValue key-format version to 2, moves the subtle accessor onto Crypto.prototype with an ERR_INVALID_THIS brand check, and vendors 10 upstream Node tests.
Security risks
Everything here is security-sensitive: post-quantum key generation/import/export, AEAD encrypt/decrypt, KEM encapsulation, key-format parsing of untrusted DER/JWK bytes, and structured-clone deserialization of key material. The BoringSSL error-queue discipline, EVP_PKEY ownership (up_ref in toCryptoKey, EvpPKeyPtr RAII everywhere else), and the memcmp on JWK pub/seed consistency all look correct. The SubtleCrypto::supports implementation intentionally uses tryClearException inside a "probe" pattern — that's the one place exception clearing is by design.
Level of scrutiny
High. This is production crypto exposed to user-controlled inputs, adds new native key classes with their own lifetime/serialization, and changes globally-visible surface (Crypto.prototype.subtle placement, JsonWebKey.kty no longer required). The PR has been through ~8 bot-review rounds with every thread resolved by follow-up commits, but the sheer breadth (new algorithm classes, new SubtleCrypto methods, format-version bump) is beyond what I'd auto-approve.
Other factors
Test coverage is substantial (10 vendored Node tests + expanded web-crypto.test.ts, moving webcrypto compat 58%→76%). Every prior finding — including a reachable RELEASE_ASSERT_NOT_REACHED on structuredClone, an exception-swallowing tryClearException in importKey, and several vacuous test assertions — was confirmed against live Node v26.3.0 and fixed with a regression test. No unresolved threads remain. ML-KEM-512 is deliberately unregistered (BoringSSL lacks EVP support), which the test fixtures skip; the AsymmetricKeyValue switch omits ML_KEM_512 accordingly, which is consistent.
… size]
supports() disagreed with Node's own vectors in 262 places. The two causes:
exportKey and getPublicKey normalized their algorithm as importKey, so a bare
name like "RSA-PSS" was rejected for missing a hash even though neither
operation takes algorithm members. Resolve the name only, matching
normalizeAlgorithm(alg, 'exportKey'). This also fixes
supports('wrapKey', 'AES-KW', 'HMAC').
The parameter checks Bun's algorithms perform at execution time were missing
from the probe, so supports() promised operations that would have thrown:
HMAC lengths that are zero or not a multiple of 8, AES lengths other than
128/192/256, EC named curves outside P-256/P-384/P-521, PBKDF2 with zero
iterations, ChaCha20-Poly1305 with an iv that is not 12 bytes or a tag that is
not 128 bits, and ECDH/X25519 deriveBits without a public key. A zero
deriveBits length is now accepted (a null one still is not), matching Node.
That leaves 68 disagreements, all of one class: the fixtures gate SHA-3,
TurboSHAKE and KAngarooTwelve on process.features.openssl_is_boringssl, which
Bun reports as true while its WebCrypto does implement SHA-3.
test-webcrypto-supports.mjs therefore stays out of the vendored set.
The ChaCha20-Poly1305 iv and tag lengths become named constants on the
algorithm class so the probe and the implementation cannot drift.
[allow size] the binary-size gate baselines against main, and this branch is
stacked on #34838, whose ML-DSA and ML-KEM support accounts for the growth.
…#36833) Net -285 LOC (349 deletions, 64 insertions including the source-lint test). Each symbol was verified with `rg -w <symbol> src/ build/debug/codegen/` to have zero references outside its own definition, then confirmed by a full `bun bd` build and `bun run rust:check-all` across all 10 targets. ### `src/platform/darwin.rs` (whole file, 210 LOC) The entire file duplicates `bun_sys::darwin`: the `OSLog`/`Signpost`/`Interval` API and the `nocancel` extern block have zero callers. `bun_perf` (the only signpost consumer) imports `bun_sys::darwin::OSLog` and `bun_sys::darwin::os_log::signpost::*`. `bun_platform` is force-linked only for `linux.rs`'s `#[no_mangle]` export. Also drops the now-unused `bun_opaque`/`strum` deps from `bun_platform/Cargo.toml` and the stale doc comment in `src/sys/lib.rs` that pointed here. ### webcrypto C++ - `CryptoKeyHMAC::create` + const-ref ctor, `CryptoKeyAES::create` + const-ref ctor: never called; `generate`/`importRaw`/`importJwk` all construct via the rvalue ctor directly. - `JSCryptoKey::fromJS`: declared, never defined, never called. - `JSSubtleCrypto::toWrapped`: only reachable via `convert<IDLInterface<SubtleCrypto>>`; no such call exists. - `OpenSSLCryptoUniquePtr.h`: `X509Ptr`/`BIOPtr` aliases (zero refs) and the `OPENSSL_VERSION_NUMBER >= 0x30000000L` block (BoringSSL defines `0x1010107f`, so it never compiles, and no code references `OsslParamBldPtr`/`OsslParamPtr`/`EVPKDFCtxPtr`/`EVPKDFPtr`). - `CommonCryptoDERUtilities.h`: `extraBytesNeededForEncodedLength` is only called from the same TU; header decl removed, made `static` in the .cpp. - `ScriptExecutionContext.h`: `wrapCryptoKey`/`unwrapCryptoKey` stubs plus the commented-out virtual decls; leftover from the earlier `SerializedCryptoKeyWrap` removal. ### sqlite / NodeVM C++ - `lazy_sqlite3.h`: `sqlite3_column_int` / `sqlite3_memory_used` / `sqlite3_prepare16_v3` typedef+var+define+dlsym lines (code uses `sqlite3_column_int64`, tracks memory via `sqlite_malloc_amount`, uses `sqlite3_prepare_v3`). - `SQLiteSingleton::schema_versions`: never read, never appended to. - `JSStatementSync::allowBareNamedParams`/`allowUnknownNamedParams`/`rowStructure` getters: members are accessed directly. - `DOMIsoSubspaces`/`DOMClientIsoSubspaces::m_*subspaceForJSSQLStatementConstructor`: `JSSQLStatementConstructor` lives in `JSFunction`'s subspace per the `static_assert` in `JSSQLStatement.h`. - `NodeVMGlobalObject::sigintReceived`: not virtual, never called; `SigintWatcher::signalAll` invokes `vm().notifyNeedTermination()` directly. - `NodeVMModuleRequest::specifier`/`importAttributes` getters: `toJS` reads `m_specifier`/`m_importAttributes` directly. ### Rust - `bun_ast::PartTag::{JsxImport, CjsImports, ReactFastRefresh}`: never assigned or compared. - `bun_ast::flags::JSXElement::HasAnyDynamic`: never inserted or tested. - `bun_ast::import_record::Tag::Tailwind`: last variant, never constructed or matched. - `bun_ast::BindingNodeList` type alias + its unused re-export in `bun_js_parser::parser`. - `bun_ast::StoreAstAllocHeap::reset`: callers invoke the free fn `store_ast_alloc_heap::reset()` directly. - `bun_jsc::JSPromise::reject_task`: sibling `resolve_task` has 4 callers, `reject_task` has zero. - `bun_jsc::JSRuntimeType::UNDEFINED`: only `NOTHING` is referenced. ### src/js - `readline.js`: unused `ObjectSetPrototypeOf` primordial destructure. - `repl.js`: unused `ArrayPrototypeSlice` primordial destructure. - `zlib.ts`: collapse redundant `ArrayBufferIsView` intermediate alias. ### Verification - `bun bd` builds clean - `bun run rust:check-all` passes all 10 targets (incl. `aarch64-apple-darwin` for the `bun_platform` change) - Smoke tests pass: `web-crypto.test.ts`, `sqlite.test.js`, `node-sqlite.test.ts`, `zlib.test.js`, `transpiler.test.js` - `test/internal/source-lints/dead-symbols-platform-webcrypto-sqlite-vm.test.ts` fails on main, passes on this branch ### Followups (not deleted; left for review) - `src/runtime/api/bun/h2/connection.rs:1731-1941` outbound-stream API (`begin_header_block`/`encode_header`/`send_header_block`/`send_data`/`send_push_promise` + transitively `SendWindow::{available,consume}`, `Coder::{take_pending_size_update,encode}`, `write_table_size_update`, ~215 LOC): only called from `#[cfg(test)]`. File carries `#![allow(dead_code)]` and was authored in #31584; likely intentional WIP scaffolding for migrating `h2_frame_parser`'s outbound path. - `src/jsc/bindings/webcrypto/*.idl` (29 files, ~1055 LOC): not processed by any build step (`scripts/glob-sources.ts` globs only `*.cpp`), but #34838 edited them recently so they may be maintained as documentation. - `src/jsc/ErrorCode.rs`: `Zig_ErrorCodeJSErrorObject` `#[no_mangle]` static with zero refs in any `.cpp`/`.h` (sibling `Zig_ErrorCodeParserError` is declared in `headers-handwritten.h`; this one is not). <!-- robobun:evidence:begin --> --- **[review]** gate passed · iteration 0 · 34 files touched <details><summary>fails on main (without fix)</summary> ```console ASAN without fix: 3 FAILED $ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/internal/source-lints/dead-symbols-platform-webcrypto-sqlite-vm.test.ts bun test v1.4.0 (e2a9bd9) test/internal/source-lints/dead-symbols-platform-webcrypto-sqlite-vm.test.ts: 19 | function src(p: string): string { 20 | return readFileSync(path.join(repoRoot, p), "utf8"); 21 | } 22 | 23 | test("bun_platform no longer declares a darwin module (duplicated bun_sys::darwin)", () => { 24 | expect(src("src/platform/lib.rs")).not.toMatch(/pub mod darwin;/); ^ error: expect(received).not.toMatch(expected) Expected substring or pattern: not /pub mod darwin;/ Received: "#![allow(non_snake_case, non_camel_case_types, non_upper_case_globals)]\n#![warn(unused_must_use)]\n//! Per-OS APIs that don't fit in `bun_sys` (signposts, the `sys_epoll_pwait2` export).\n\n// Android is listed alongside Linux so the `#[no_mangle]` C exports\n// (`sys_epoll_pwait2`, …) reach the linker on the `*-linux-android` targets.\n#[cfg(target_os = \"macos\")]\npub mod darwin;\n#[cfg(any(target_os = \"linux\", t ... (truncated) release without fix: 3 FAILED bun test v1.4.0-canary.1 (1498d7b) test/internal/source-lints/dead-symbols-platform-webcrypto-sqlite-vm.test.ts: 19 | function src(p: string): string { 20 | return readFileSync(path.join(repoRoot, p), "utf8"); 21 | } 22 | 23 | test("bun_platform no longer declares a darwin module (duplicated bun_sys::darwin)", () => { 24 | expect(src("src/platform/lib.rs")).not.toMatch(/pub mod darwin;/); ^ error: expect(received).not.toMatch(expected) Expected substring or pattern: not /pub mod darwin;/ Received: "#![allow(non_snake_case, non_camel_case_types, non_upper_case_globals)]\n#![warn(unused_must_use)]\n//! Per-OS APIs that don't fit in `bun_sys` (signposts, the `sys_epoll_pwait2` export).\n\n// Android is listed alongside Linux so the `#[no_mangle]` C exports\n// (`sys_epoll_pwait2`, …) reach the linker on the `*-linux-android` targets.\n#[cfg(target_os = \"macos\")]\npub mod darwin;\n#[cfg(any(target_os = \"linux\", target_os = \"android\"))]\npub(crate) mod linux;\n" at <anonymous> (/workspace/bun/test/internal/source-lints/dead-symbols-platform-webcrypto-sqlite-vm.test.ts:24:42) (fail) bun_platform no long ... (truncated) ``` </details> <details><summary>passes on PR (with fix)</summary> ```console ASAN with fix: all passed $ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/internal/source-lints/dead-symbols-platform-webcrypto-sqlite-vm.test.ts bun test v1.4.0 (e2a9bd9) test/internal/source-lints/dead-symbols-platform-webcrypto-sqlite-vm.test.ts: (pass) bun_platform no longer declares a darwin module (duplicated bun_sys::darwin) [10.05ms] (pass) dead C++ symbols in webcrypto/sqlite/NodeVM do not reappear [23.34ms] (pass) dead Rust symbols in ast/jsc do not reappear [14.33ms] 3 pass 0 fail 3 expect() calls Ran 3 tests across 1 file. [2.09s] __F:0:S:0 release with fix: all passed $ bun scripts/build.ts --profile=release [configured] bun-profile → bun (stripped) in 733ms (unchanged) ninja: Entering directory `/workspace/bun/build/release' [1/139] gen generated_host_exports.rs generated_host_exports.rs: 94 exports (host=3, lazy=10, generic=81, rust=0); 238 extern-C blocks audited [2/139] gen cpp.rs (cppbind) [3/139] gen JS modules (bundle-modules) Preprocess modules (9279ms) Bundle modules (59ms) Postprocesss modules (259ms) Bundle Functions (943ms) Generate Code (32ms) [10.59s] Bundled "src/js" for production 2558 kb 193 internal modules 13 native modules 90 internal functions across 19 files [3/138] cargo bun_bin → libbun_rust.a (--target x86_64-unknown-linux-gnu) nightly-2026-07-20-x86_64-unknown-linux-gnu unchanged - rustc 1.99.0-nightly (9f36de775 2026-07-19) �[1m�[92m Compiling�[0m bun_core v0.0.0 (/workspace/bun/src/bun_core) �[1m�[92m Compiling�[0m bun_errno v0.0.0 (/workspace/bun/src/errno) �[1m�[92m Compiling�[0m bun_ptr v0.0.0 (/workspace/bun/src/ptr) �[1m�[92m Compiling�[0m bun_boringssl_sys v0.0.0 (/workspace/bun/src/boringssl_sys) �[1m�[92m Compiling�[0m bun_safety v0.0.0 (/workspace/bun/src/safety) �[1 ... (truncated) ``` </details> <details><summary>diff hotspot</summary> ``` Cargo.lock | 2 - src/ast/import_record.rs | 2 - src/ast/lib.rs | 7 +- src/ast/nodes.rs | 6 +- src/js/node/readline.js | 1 - src/js/node/repl.js | 1 - src/js/node/zlib.ts | 3 +- src/js_parser/parser.rs | 4 +- src/jsc/JSPromise.rs | 12 -- src/jsc/JSRuntimeType.rs | 1 - src/jsc/bindings/NodeVM.cpp | 5 - src/jsc/bindings/NodeVM.h | 1 - src/jsc/bindings/NodeVMModule.h | 3 - src/jsc/bindings/ScriptExecutionContext.h | 16 -- src/jsc/bindings/sqlite/JSSQLStatement.cpp | 2 - src/jsc/bindings/sqlite/NodeSqlite.h | 3 - src/jsc/bindings/sqlite/lazy_sqlite3.h | 18 -- src/jsc/bindings/webcore/DOMClientIsoSubspaces.h | 1 - src/jsc/bindings/webcore/DOMIsoSubspaces.h | 1 - .../webcrypto/CommonCryptoDERUtilities.cpp | 2 +- .../bindings/webcrypto/CommonCryptoDERUtilities.h | 1 - src/jsc/bindings/webcrypto/CryptoKeyAES.cpp | 7 - src/jsc/bindings/webcrypto/CryptoKeyAES.h | 5 - src/jsc/bindings/webcrypto/CryptoKeyHMAC.cpp | 7 - src/jsc/bindings/webcrypto/CryptoKeyHMAC.h | 6 - src/jsc/bindings/webcrypto/JSCryptoKey.h | 2 - src/jsc/bindings/webcrypto/JSSubtleCrypto.cpp | 7 - src/jsc/bindings/webcrypto/JSSubtleCrypto.h | 1 - .../bindings/webcrypto/OpenSSLCryptoUniquePtr.h | 13 -- src/platform/Cargo.toml | 2 - src/platform/darwin.rs | 210 --------------------- src/platform/lib.rs | 4 +- src/sys/lib.rs ... (truncated) ``` </details> **gate history** · 1 passed · 0 rejected · iteration 0 <details><summary>evidence per changed file</summary> ``` file reads edits tests Cargo.lock 0 0 0 src/ast/import_record.rs 1 1 0 src/ast/lib.rs 3 3 0 src/ast/nodes.rs 2 2 0 src/js/node/readline.js 1 1 0 src/js/node/repl.js 1 1 0 src/js/node/zlib.ts 1 1 0 src/js_parser/parser.rs 1 1 0 src/jsc/JSPromise.rs 1 1 0 src/jsc/JSRuntimeType.rs 1 1 0 src/jsc/bindings/NodeVM.cpp 1 1 0 src/jsc/bindings/NodeVM.h 1 1 0 src/jsc/bindings/NodeVMModule.h 1 1 0 src/jsc/bindings/ScriptExecutionContext.h 2 1 0 src/jsc/bindings/sqlite/JSSQLStatement.cpp 1 1 0 src/jsc/bindings/sqlite/NodeSqlite.h 2 2 0 (+ 18 more files) ``` </details> <!-- robobun:evidence:end --> --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
…oven-sh#36833) Net -285 LOC (349 deletions, 64 insertions including the source-lint test). Each symbol was verified with `rg -w <symbol> src/ build/debug/codegen/` to have zero references outside its own definition, then confirmed by a full `bun bd` build and `bun run rust:check-all` across all 10 targets. ### `src/platform/darwin.rs` (whole file, 210 LOC) The entire file duplicates `bun_sys::darwin`: the `OSLog`/`Signpost`/`Interval` API and the `nocancel` extern block have zero callers. `bun_perf` (the only signpost consumer) imports `bun_sys::darwin::OSLog` and `bun_sys::darwin::os_log::signpost::*`. `bun_platform` is force-linked only for `linux.rs`'s `#[no_mangle]` export. Also drops the now-unused `bun_opaque`/`strum` deps from `bun_platform/Cargo.toml` and the stale doc comment in `src/sys/lib.rs` that pointed here. ### webcrypto C++ - `CryptoKeyHMAC::create` + const-ref ctor, `CryptoKeyAES::create` + const-ref ctor: never called; `generate`/`importRaw`/`importJwk` all construct via the rvalue ctor directly. - `JSCryptoKey::fromJS`: declared, never defined, never called. - `JSSubtleCrypto::toWrapped`: only reachable via `convert<IDLInterface<SubtleCrypto>>`; no such call exists. - `OpenSSLCryptoUniquePtr.h`: `X509Ptr`/`BIOPtr` aliases (zero refs) and the `OPENSSL_VERSION_NUMBER >= 0x30000000L` block (BoringSSL defines `0x1010107f`, so it never compiles, and no code references `OsslParamBldPtr`/`OsslParamPtr`/`EVPKDFCtxPtr`/`EVPKDFPtr`). - `CommonCryptoDERUtilities.h`: `extraBytesNeededForEncodedLength` is only called from the same TU; header decl removed, made `static` in the .cpp. - `ScriptExecutionContext.h`: `wrapCryptoKey`/`unwrapCryptoKey` stubs plus the commented-out virtual decls; leftover from the earlier `SerializedCryptoKeyWrap` removal. ### sqlite / NodeVM C++ - `lazy_sqlite3.h`: `sqlite3_column_int` / `sqlite3_memory_used` / `sqlite3_prepare16_v3` typedef+var+define+dlsym lines (code uses `sqlite3_column_int64`, tracks memory via `sqlite_malloc_amount`, uses `sqlite3_prepare_v3`). - `SQLiteSingleton::schema_versions`: never read, never appended to. - `JSStatementSync::allowBareNamedParams`/`allowUnknownNamedParams`/`rowStructure` getters: members are accessed directly. - `DOMIsoSubspaces`/`DOMClientIsoSubspaces::m_*subspaceForJSSQLStatementConstructor`: `JSSQLStatementConstructor` lives in `JSFunction`'s subspace per the `static_assert` in `JSSQLStatement.h`. - `NodeVMGlobalObject::sigintReceived`: not virtual, never called; `SigintWatcher::signalAll` invokes `vm().notifyNeedTermination()` directly. - `NodeVMModuleRequest::specifier`/`importAttributes` getters: `toJS` reads `m_specifier`/`m_importAttributes` directly. ### Rust - `bun_ast::PartTag::{JsxImport, CjsImports, ReactFastRefresh}`: never assigned or compared. - `bun_ast::flags::JSXElement::HasAnyDynamic`: never inserted or tested. - `bun_ast::import_record::Tag::Tailwind`: last variant, never constructed or matched. - `bun_ast::BindingNodeList` type alias + its unused re-export in `bun_js_parser::parser`. - `bun_ast::StoreAstAllocHeap::reset`: callers invoke the free fn `store_ast_alloc_heap::reset()` directly. - `bun_jsc::JSPromise::reject_task`: sibling `resolve_task` has 4 callers, `reject_task` has zero. - `bun_jsc::JSRuntimeType::UNDEFINED`: only `NOTHING` is referenced. ### src/js - `readline.js`: unused `ObjectSetPrototypeOf` primordial destructure. - `repl.js`: unused `ArrayPrototypeSlice` primordial destructure. - `zlib.ts`: collapse redundant `ArrayBufferIsView` intermediate alias. ### Verification - `bun bd` builds clean - `bun run rust:check-all` passes all 10 targets (incl. `aarch64-apple-darwin` for the `bun_platform` change) - Smoke tests pass: `web-crypto.test.ts`, `sqlite.test.js`, `node-sqlite.test.ts`, `zlib.test.js`, `transpiler.test.js` - `test/internal/source-lints/dead-symbols-platform-webcrypto-sqlite-vm.test.ts` fails on main, passes on this branch ### Followups (not deleted; left for review) - `src/runtime/api/bun/h2/connection.rs:1731-1941` outbound-stream API (`begin_header_block`/`encode_header`/`send_header_block`/`send_data`/`send_push_promise` + transitively `SendWindow::{available,consume}`, `Coder::{take_pending_size_update,encode}`, `write_table_size_update`, ~215 LOC): only called from `#[cfg(test)]`. File carries `#![allow(dead_code)]` and was authored in oven-sh#31584; likely intentional WIP scaffolding for migrating `h2_frame_parser`'s outbound path. - `src/jsc/bindings/webcrypto/*.idl` (29 files, ~1055 LOC): not processed by any build step (`scripts/glob-sources.ts` globs only `*.cpp`), but oven-sh#34838 edited them recently so they may be maintained as documentation. - `src/jsc/ErrorCode.rs`: `Zig_ErrorCodeJSErrorObject` `#[no_mangle]` static with zero refs in any `.cpp`/`.h` (sibling `Zig_ErrorCodeParserError` is declared in `headers-handwritten.h`; this one is not). <!-- robobun:evidence:begin --> --- **[review]** gate passed · iteration 0 · 34 files touched <details><summary>fails on main (without fix)</summary> ```console ASAN without fix: 3 FAILED $ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/internal/source-lints/dead-symbols-platform-webcrypto-sqlite-vm.test.ts bun test v1.4.0 (e2a9bd9) test/internal/source-lints/dead-symbols-platform-webcrypto-sqlite-vm.test.ts: 19 | function src(p: string): string { 20 | return readFileSync(path.join(repoRoot, p), "utf8"); 21 | } 22 | 23 | test("bun_platform no longer declares a darwin module (duplicated bun_sys::darwin)", () => { 24 | expect(src("src/platform/lib.rs")).not.toMatch(/pub mod darwin;/); ^ error: expect(received).not.toMatch(expected) Expected substring or pattern: not /pub mod darwin;/ Received: "#![allow(non_snake_case, non_camel_case_types, non_upper_case_globals)]\n#![warn(unused_must_use)]\n//! Per-OS APIs that don't fit in `bun_sys` (signposts, the `sys_epoll_pwait2` export).\n\n// Android is listed alongside Linux so the `#[no_mangle]` C exports\n// (`sys_epoll_pwait2`, …) reach the linker on the `*-linux-android` targets.\n#[cfg(target_os = \"macos\")]\npub mod darwin;\n#[cfg(any(target_os = \"linux\", t ... (truncated) release without fix: 3 FAILED bun test v1.4.0-canary.1 (1498d7b) test/internal/source-lints/dead-symbols-platform-webcrypto-sqlite-vm.test.ts: 19 | function src(p: string): string { 20 | return readFileSync(path.join(repoRoot, p), "utf8"); 21 | } 22 | 23 | test("bun_platform no longer declares a darwin module (duplicated bun_sys::darwin)", () => { 24 | expect(src("src/platform/lib.rs")).not.toMatch(/pub mod darwin;/); ^ error: expect(received).not.toMatch(expected) Expected substring or pattern: not /pub mod darwin;/ Received: "#![allow(non_snake_case, non_camel_case_types, non_upper_case_globals)]\n#![warn(unused_must_use)]\n//! Per-OS APIs that don't fit in `bun_sys` (signposts, the `sys_epoll_pwait2` export).\n\n// Android is listed alongside Linux so the `#[no_mangle]` C exports\n// (`sys_epoll_pwait2`, …) reach the linker on the `*-linux-android` targets.\n#[cfg(target_os = \"macos\")]\npub mod darwin;\n#[cfg(any(target_os = \"linux\", target_os = \"android\"))]\npub(crate) mod linux;\n" at <anonymous> (/workspace/bun/test/internal/source-lints/dead-symbols-platform-webcrypto-sqlite-vm.test.ts:24:42) (fail) bun_platform no long ... (truncated) ``` </details> <details><summary>passes on PR (with fix)</summary> ```console ASAN with fix: all passed $ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/internal/source-lints/dead-symbols-platform-webcrypto-sqlite-vm.test.ts bun test v1.4.0 (e2a9bd9) test/internal/source-lints/dead-symbols-platform-webcrypto-sqlite-vm.test.ts: (pass) bun_platform no longer declares a darwin module (duplicated bun_sys::darwin) [10.05ms] (pass) dead C++ symbols in webcrypto/sqlite/NodeVM do not reappear [23.34ms] (pass) dead Rust symbols in ast/jsc do not reappear [14.33ms] 3 pass 0 fail 3 expect() calls Ran 3 tests across 1 file. [2.09s] __F:0:S:0 release with fix: all passed $ bun scripts/build.ts --profile=release [configured] bun-profile → bun (stripped) in 733ms (unchanged) ninja: Entering directory `/workspace/bun/build/release' [1/139] gen generated_host_exports.rs generated_host_exports.rs: 94 exports (host=3, lazy=10, generic=81, rust=0); 238 extern-C blocks audited [2/139] gen cpp.rs (cppbind) [3/139] gen JS modules (bundle-modules) Preprocess modules (9279ms) Bundle modules (59ms) Postprocesss modules (259ms) Bundle Functions (943ms) Generate Code (32ms) [10.59s] Bundled "src/js" for production 2558 kb 193 internal modules 13 native modules 90 internal functions across 19 files [3/138] cargo bun_bin → libbun_rust.a (--target x86_64-unknown-linux-gnu) nightly-2026-07-20-x86_64-unknown-linux-gnu unchanged - rustc 1.99.0-nightly (9f36de775 2026-07-19) �[1m�[92m Compiling�[0m bun_core v0.0.0 (/workspace/bun/src/bun_core) �[1m�[92m Compiling�[0m bun_errno v0.0.0 (/workspace/bun/src/errno) �[1m�[92m Compiling�[0m bun_ptr v0.0.0 (/workspace/bun/src/ptr) �[1m�[92m Compiling�[0m bun_boringssl_sys v0.0.0 (/workspace/bun/src/boringssl_sys) �[1m�[92m Compiling�[0m bun_safety v0.0.0 (/workspace/bun/src/safety) �[1 ... (truncated) ``` </details> <details><summary>diff hotspot</summary> ``` Cargo.lock | 2 - src/ast/import_record.rs | 2 - src/ast/lib.rs | 7 +- src/ast/nodes.rs | 6 +- src/js/node/readline.js | 1 - src/js/node/repl.js | 1 - src/js/node/zlib.ts | 3 +- src/js_parser/parser.rs | 4 +- src/jsc/JSPromise.rs | 12 -- src/jsc/JSRuntimeType.rs | 1 - src/jsc/bindings/NodeVM.cpp | 5 - src/jsc/bindings/NodeVM.h | 1 - src/jsc/bindings/NodeVMModule.h | 3 - src/jsc/bindings/ScriptExecutionContext.h | 16 -- src/jsc/bindings/sqlite/JSSQLStatement.cpp | 2 - src/jsc/bindings/sqlite/NodeSqlite.h | 3 - src/jsc/bindings/sqlite/lazy_sqlite3.h | 18 -- src/jsc/bindings/webcore/DOMClientIsoSubspaces.h | 1 - src/jsc/bindings/webcore/DOMIsoSubspaces.h | 1 - .../webcrypto/CommonCryptoDERUtilities.cpp | 2 +- .../bindings/webcrypto/CommonCryptoDERUtilities.h | 1 - src/jsc/bindings/webcrypto/CryptoKeyAES.cpp | 7 - src/jsc/bindings/webcrypto/CryptoKeyAES.h | 5 - src/jsc/bindings/webcrypto/CryptoKeyHMAC.cpp | 7 - src/jsc/bindings/webcrypto/CryptoKeyHMAC.h | 6 - src/jsc/bindings/webcrypto/JSCryptoKey.h | 2 - src/jsc/bindings/webcrypto/JSSubtleCrypto.cpp | 7 - src/jsc/bindings/webcrypto/JSSubtleCrypto.h | 1 - .../bindings/webcrypto/OpenSSLCryptoUniquePtr.h | 13 -- src/platform/Cargo.toml | 2 - src/platform/darwin.rs | 210 --------------------- src/platform/lib.rs | 4 +- src/sys/lib.rs ... (truncated) ``` </details> **gate history** · 1 passed · 0 rejected · iteration 0 <details><summary>evidence per changed file</summary> ``` file reads edits tests Cargo.lock 0 0 0 src/ast/import_record.rs 1 1 0 src/ast/lib.rs 3 3 0 src/ast/nodes.rs 2 2 0 src/js/node/readline.js 1 1 0 src/js/node/repl.js 1 1 0 src/js/node/zlib.ts 1 1 0 src/js_parser/parser.rs 1 1 0 src/jsc/JSPromise.rs 1 1 0 src/jsc/JSRuntimeType.rs 1 1 0 src/jsc/bindings/NodeVM.cpp 1 1 0 src/jsc/bindings/NodeVM.h 1 1 0 src/jsc/bindings/NodeVMModule.h 1 1 0 src/jsc/bindings/ScriptExecutionContext.h 2 1 0 src/jsc/bindings/sqlite/JSSQLStatement.cpp 1 1 0 src/jsc/bindings/sqlite/NodeSqlite.h 2 2 0 (+ 18 more files) ``` </details> <!-- robobun:evidence:end --> --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Implements node v26's WebCrypto surface additions, verified case-by-case against the node v26.3.0 binary. Adds 10 vendored upstream tests. Based on main. Stacked on #34431.
What changed
encapsulateBits/encapsulateKey/decapsulateBits/decapsulateKey(per-path result enumeration order matches node, which itself differs between the two), spki/pkcs8/jwk/raw-* import/export, andstructuredCloneof AKP keys guarded withDataCloneErrorinstead of a release assert.EVP_aead_chacha20_poly1305, with node's 12-byte nonce validation andraw-secret-only raw import (node rejects user-facingrawfor ChaCha;deriveKey's inner import usesraw-secretaccordingly).raw-secret/raw-publickey formats, accepted as aliases where node aliases them (all algorithms pre-dispatch — restricting to EC/OKP would break HKDF parity); shared helpers at all four import/export/wrap/unwrap sites.KeyObject.prototype.toCryptoKey,SubtleCrypto.supports(), and getPublicKey coverage across RSA/EC/OKP/AKP.DataError/TypeErrorclasses and messages for JWK validation (use/key_ops/extcheck order probed on doubly-invalid input, duplicatekey_opsrejected via one shared helper),ERR_INVALID_ARG_TYPEfor non-buffer keyData with zero getter invocations (format-first converter dispatch, no exception clearing), wrapKey's extractable check ordered before format aliasing, and the KEM-usage rejections for RSA variants.JsonWebKey.ktyis no longer a required IDL member (WebCrypto does not mark it required; node rejects a non-JWK object withDataErrorwhere Bun threwTypeError).Verification
web-crypto suite 87+ pass / 0 fail with per-claim probes captured from live node; the four vendored ML tests plus
test-webcrypto-export-import*pass; regression tests proven failing on the unfixed build for each behavior fix (getter counts, clone guard, coercion counts).Compat impact (upstream node v26.3.0 test files vendored, in-tree = passing)