Summary
The island's node:crypto shim carries a hashing-and-random slice only. Packages that do real cryptography import and initialize fine, then throw on the first crypto call — and scriptc coverage --dynamic reports no blockers for such a program, so the failure only surfaces at runtime.
I hit this trying to compile a WhatsApp protocol client (Noise handshake, Signal protocol, AES-GCM transport). It is not a performance question — the handshake cannot start.
Environment
scriptc 0.0.17, Ubuntu 24.04 x86_64, clang 18.1.3, cmake 3.28.3, Node 22.22.2 as differential baseline. Package under test: zapo-js@1.6.2, zero runtime dependencies, Uint8Array throughout (no Buffer).
What works
Measured against Node, byte-identical output:
| primitive |
result |
| SHA-256 |
✅ identical |
| SHA-1 |
✅ identical |
| MD5 |
✅ identical |
| HMAC-SHA-256 |
✅ identical |
What throws
| primitive |
error |
| SHA-512 |
Digest method not supported |
| HMAC-SHA-512 |
Digest method not supported |
| HKDF |
crypto.hkdfSync is not available in the scriptc island (the embedded runtime carries the hashing/random slice only) |
| AES-GCM |
crypto.createCipheriv is not available in the scriptc island |
| AES-CBC |
crypto.createCipheriv is not available in the scriptc island |
| X25519 ECDH |
crypto.createPrivateKey is not available in the scriptc island |
From packages/runtime/src/scr_island.c, makeCrypto: getHashes() returns ["md5", "sha1", "sha256"]; getCiphers() and getCurves() both return []; 32 APIs are die() stubs, including createCipheriv, createDecipheriv, createPrivateKey, createPublicKey, createSecretKey, diffieHellman, createECDH, sign, verify, hkdf, hkdfSync, scrypt, generateKeyPair.
crypto.subtle is assigned from globalThis.crypto, but the embedded engine does not expose a WebCrypto implementation that covers these, so it is not an alternative route.
Coverage reports no blockers
For a program importing the client:
statements analyzed 4
compile statically 0 (0%)
compile dynamically 4 (100%) (island sites)
embedded npm code imports Node builtins and unresolved specifiers:
node:crypto shimmed (zapo-js)
node:events shimmed (zapo-js)
... 11 more builtins ...
builds with --dynamic — no remaining blockers
It builds, the client object constructs correctly, output matches Node — and then it throws on the first createPrivateKey. Suggestion: shimmed currently covers both "fully implemented" and "present but stubbed". A separate marker for the latter would make coverage predictive of what actually runs.
The static surface is narrower still
Probing each API in a plain (non---dynamic) build:
| expression |
static |
createHash('sha256').update(x).digest('hex') |
✅ |
createHash('sha1').update(x).digest('hex') |
✅ |
randomBytes(n) |
✅ |
randomUUID() |
✅ |
createHash('sha512') / ('md5') |
❌ SC2020 |
createHmac(...) |
❌ SC2020 |
createCipheriv(...) |
❌ SC2020 |
hkdfSync(...) |
❌ SC2020 |
createPrivateKey |
❌ SC2020 |
generateKeyPairSync('x25519') |
❌ SC2020 |
timingSafeEqual(...) |
❌ SC2020 |
webcrypto |
❌ SC2020 — "the WebCrypto object has no lowering — the lowered crypto surface is randomUUID, randomBytes, and the createHash chain" |
So the static path has less crypto than the island (no HMAC, no MD5, no SHA-512), which is surprising given static is the default and preferred lane. Also note createHash only lowers as a complete chain — binding the intermediate (const h = createHash('sha256')) fails with 'Hash' is typed by @types/node but has no scriptc lowering yet.
Request
Rough priority order for making protocol clients viable:
- X25519 (
createPrivateKey + diffieHellman, or WebCrypto deriveBits) — blocks the handshake outright
- AES-GCM via
createCipheriv — blocks all post-handshake transport
- HKDF (
hkdfSync)
- SHA-512 and HMAC-SHA-512
- Ed25519
sign / verify
- AES-CBC / AES-CTR
Items 1–3 alone would take this from "cannot connect" to "usable".
The payoff is real for this class of workload. Measured here on pure TS: ~2 ms startup vs Node's ~27 ms, and 2.1 MB RSS vs 47 MB for an equivalent HTTP server. For multi-tenant connection handling that difference is the whole point.
Summary
The island's
node:cryptoshim carries a hashing-and-random slice only. Packages that do real cryptography import and initialize fine, then throw on the first crypto call — andscriptc coverage --dynamicreports no blockers for such a program, so the failure only surfaces at runtime.I hit this trying to compile a WhatsApp protocol client (Noise handshake, Signal protocol, AES-GCM transport). It is not a performance question — the handshake cannot start.
Environment
scriptc 0.0.17, Ubuntu 24.04 x86_64, clang 18.1.3, cmake 3.28.3, Node 22.22.2 as differential baseline. Package under test:
zapo-js@1.6.2, zero runtime dependencies,Uint8Arraythroughout (noBuffer).What works
Measured against Node, byte-identical output:
What throws
Digest method not supportedDigest method not supportedcrypto.hkdfSync is not available in the scriptc island (the embedded runtime carries the hashing/random slice only)crypto.createCipheriv is not available in the scriptc islandcrypto.createCipheriv is not available in the scriptc islandcrypto.createPrivateKey is not available in the scriptc islandFrom
packages/runtime/src/scr_island.c,makeCrypto:getHashes()returns["md5", "sha1", "sha256"];getCiphers()andgetCurves()both return[]; 32 APIs aredie()stubs, includingcreateCipheriv,createDecipheriv,createPrivateKey,createPublicKey,createSecretKey,diffieHellman,createECDH,sign,verify,hkdf,hkdfSync,scrypt,generateKeyPair.crypto.subtleis assigned fromglobalThis.crypto, but the embedded engine does not expose a WebCrypto implementation that covers these, so it is not an alternative route.Coverage reports no blockers
For a program importing the client:
It builds, the client object constructs correctly, output matches Node — and then it throws on the first
createPrivateKey. Suggestion:shimmedcurrently covers both "fully implemented" and "present but stubbed". A separate marker for the latter would makecoveragepredictive of what actually runs.The static surface is narrower still
Probing each API in a plain (non-
--dynamic) build:createHash('sha256').update(x).digest('hex')createHash('sha1').update(x).digest('hex')randomBytes(n)randomUUID()createHash('sha512')/('md5')createHmac(...)createCipheriv(...)hkdfSync(...)createPrivateKeygenerateKeyPairSync('x25519')timingSafeEqual(...)webcryptoSo the static path has less crypto than the island (no HMAC, no MD5, no SHA-512), which is surprising given static is the default and preferred lane. Also note
createHashonly lowers as a complete chain — binding the intermediate (const h = createHash('sha256')) fails with'Hash' is typed by @types/node but has no scriptc lowering yet.Request
Rough priority order for making protocol clients viable:
createPrivateKey+diffieHellman, or WebCryptoderiveBits) — blocks the handshake outrightcreateCipheriv— blocks all post-handshake transporthkdfSync)sign/verifyItems 1–3 alone would take this from "cannot connect" to "usable".
The payoff is real for this class of workload. Measured here on pure TS: ~2 ms startup vs Node's ~27 ms, and 2.1 MB RSS vs 47 MB for an equivalent HTTP server. For multi-tenant connection handling that difference is the whole point.