From e8dacce40fc87e58b1a1318966a2d0903a39f6b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?= Date: Thu, 12 Mar 2026 22:51:10 +0100 Subject: [PATCH 1/6] fix(crypto): support structuredClone for CryptoKey Uses the cloneable resource registry from #32672 to enable structured cloning of CryptoKey objects. This allows CryptoKeys (including non-extractable ones) to be passed to Workers via postMessage and cloned with structuredClone(). Closes #12734 Co-Authored-By: Claude Opus 4.6 --- ext/crypto/00_crypto.js | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/ext/crypto/00_crypto.js b/ext/crypto/00_crypto.js index 15e7657893beba..bff41d516747f1 100644 --- a/ext/crypto/00_crypto.js +++ b/ext/crypto/00_crypto.js @@ -438,9 +438,29 @@ function constructKey(type, extractable, usages, algorithm, handle) { key[_algorithm] = algorithm; key[_handle] = handle; key[kKeyObject] = WeakMapPrototypeGet(KEY_STORE, handle); + key[core.hostObjectBrand] = () => ({ + type: "CryptoKey", + keyType: type, + extractable, + usages, + algorithm, + keyData: WeakMapPrototypeGet(KEY_STORE, handle), + }); return key; } +core.registerCloneableResource("CryptoKey", (data) => { + const handle = {}; + WeakMapPrototypeSet(KEY_STORE, handle, data.keyData); + return constructKey( + data.keyType, + data.extractable, + data.usages, + data.algorithm, + handle, + ); +}); + // https://w3c.github.io/webcrypto/#concept-usage-intersection /** * @param {string[]} a From 9262e900f52c7d2b9f8a2d64c92deb22d5ef3763 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?= Date: Thu, 12 Mar 2026 22:58:00 +0100 Subject: [PATCH 2/6] fix(web): support structuredClone for DOMException Uses the cloneable resource registry to enable structured cloning of DOMException objects. Serializes message, name, and stack; code is derived from name on deserialization. Co-Authored-By: Claude Opus 4.6 --- ext/web/01_dom_exception.js | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/ext/web/01_dom_exception.js b/ext/web/01_dom_exception.js index d654d07317f7ea..7a868a4abace80 100644 --- a/ext/web/01_dom_exception.js +++ b/ext/web/01_dom_exception.js @@ -7,7 +7,7 @@ /// /// -import { primordials } from "ext:core/mod.js"; +import { core, primordials } from "ext:core/mod.js"; const { Error, ErrorPrototype, @@ -137,6 +137,12 @@ class DOMException { error[_name] = name; error[_code] = code; error[webidl.brand] = webidl.brand; + error[core.hostObjectBrand] = () => ({ + type: "DOMException", + message, + name, + stack: error.stack, + }); return error; } @@ -217,4 +223,18 @@ for (let i = 0; i < entries.length; ++i) { ObjectDefineProperty(DOMException.prototype, key, desc); } +core.registerCloneableResource("DOMException", (data) => { + const ex = new DOMException(data.message, data.name); + if (data.stack !== undefined) { + ObjectDefineProperty(ex, "stack", { + __proto__: null, + value: data.stack, + configurable: true, + writable: true, + enumerable: false, + }); + } + return ex; +}); + export { DOMException, DOMExceptionPrototype }; From 81666c62c74572e228079d19d4d8d1731bfc9c30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?= Date: Fri, 13 Mar 2026 08:48:38 +0100 Subject: [PATCH 3/6] fix: make hostObjectBrand non-enumerable on CryptoKey and DOMException The hostObjectBrand symbol property was enumerable, causing Node's deepStrictEqual to compare the function instances and fail since each object has a different closure. Co-Authored-By: Claude Opus 4.6 --- ext/crypto/00_crypto.js | 21 ++++++++++++++------- ext/web/01_dom_exception.js | 16 +++++++++++----- 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/ext/crypto/00_crypto.js b/ext/crypto/00_crypto.js index bff41d516747f1..8661820aa47834 100644 --- a/ext/crypto/00_crypto.js +++ b/ext/crypto/00_crypto.js @@ -65,6 +65,7 @@ const { JSONStringify, MathCeil, ObjectAssign, + ObjectDefineProperty, ObjectHasOwn, ObjectPrototypeIsPrototypeOf, SafeArrayIterator, @@ -438,13 +439,19 @@ function constructKey(type, extractable, usages, algorithm, handle) { key[_algorithm] = algorithm; key[_handle] = handle; key[kKeyObject] = WeakMapPrototypeGet(KEY_STORE, handle); - key[core.hostObjectBrand] = () => ({ - type: "CryptoKey", - keyType: type, - extractable, - usages, - algorithm, - keyData: WeakMapPrototypeGet(KEY_STORE, handle), + ObjectDefineProperty(key, core.hostObjectBrand, { + __proto__: null, + value: () => ({ + type: "CryptoKey", + keyType: type, + extractable, + usages, + algorithm, + keyData: WeakMapPrototypeGet(KEY_STORE, handle), + }), + enumerable: false, + configurable: false, + writable: false, }); return key; } diff --git a/ext/web/01_dom_exception.js b/ext/web/01_dom_exception.js index 7a868a4abace80..f50ebac2e37122 100644 --- a/ext/web/01_dom_exception.js +++ b/ext/web/01_dom_exception.js @@ -137,11 +137,17 @@ class DOMException { error[_name] = name; error[_code] = code; error[webidl.brand] = webidl.brand; - error[core.hostObjectBrand] = () => ({ - type: "DOMException", - message, - name, - stack: error.stack, + ObjectDefineProperty(error, core.hostObjectBrand, { + __proto__: null, + value: () => ({ + type: "DOMException", + message, + name, + stack: error.stack, + }), + enumerable: false, + configurable: false, + writable: false, }); return error; From 5ecf1aa3a3eaacc2b052dc0b6e02e44bf62d4f71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?= Date: Fri, 13 Mar 2026 09:09:50 +0100 Subject: [PATCH 4/6] test: add structuredClone tests for CryptoKey and DOMException Co-Authored-By: Claude Opus 4.6 --- tests/unit/structured_clone_test.ts | 82 +++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/tests/unit/structured_clone_test.ts b/tests/unit/structured_clone_test.ts index 93050858b6831d..07a8754758c4d6 100644 --- a/tests/unit/structured_clone_test.ts +++ b/tests/unit/structured_clone_test.ts @@ -59,3 +59,85 @@ Deno.test("correct DataCloneError message", () => { // ab2 should not be detached after above failure structuredClone(ab2, { transfer: [ab2] }); }); + +Deno.test("structuredClone CryptoKey", async () => { + // AES key + const aesKey = await crypto.subtle.generateKey( + { name: "AES-GCM", length: 256 }, + true, + ["encrypt", "decrypt"], + ); + const aesClone = structuredClone(aesKey); + assert(aesKey !== aesClone); + assertEquals(aesClone.type, aesKey.type); + assertEquals(aesClone.extractable, aesKey.extractable); + assertEquals(aesClone.algorithm, aesKey.algorithm); + assertEquals([...aesClone.usages], [...aesKey.usages]); + + // Verify the cloned key actually works + const data = new TextEncoder().encode("hello"); + const iv = crypto.getRandomValues(new Uint8Array(12)); + const encrypted = await crypto.subtle.encrypt( + { name: "AES-GCM", iv }, + aesClone, + data, + ); + const decrypted = await crypto.subtle.decrypt( + { name: "AES-GCM", iv }, + aesKey, + encrypted, + ); + assertEquals(new Uint8Array(decrypted), data); + + // Non-extractable key can be cloned + const nonExtractable = await crypto.subtle.generateKey( + { name: "AES-GCM", length: 256 }, + false, + ["encrypt", "decrypt"], + ); + const nonExtractableClone = structuredClone(nonExtractable); + assertEquals(nonExtractableClone.extractable, false); + assertEquals(nonExtractableClone.algorithm, nonExtractable.algorithm); + + // HMAC key + const hmacKey = await crypto.subtle.generateKey( + { name: "HMAC", hash: "SHA-256" }, + true, + ["sign", "verify"], + ); + const hmacClone = structuredClone(hmacKey); + assertEquals(hmacClone.type, hmacKey.type); + assertEquals(hmacClone.algorithm, hmacKey.algorithm); + assertEquals([...hmacClone.usages], [...hmacKey.usages]); + + // EC key pair + const ecKeyPair = await crypto.subtle.generateKey( + { name: "ECDSA", namedCurve: "P-256" }, + true, + ["sign", "verify"], + ) as CryptoKeyPair; + const ecPrivateClone = structuredClone(ecKeyPair.privateKey); + const ecPublicClone = structuredClone(ecKeyPair.publicKey); + assertEquals(ecPrivateClone.type, "private"); + assertEquals(ecPublicClone.type, "public"); + + // Ed25519 key pair + const edKeyPair = await crypto.subtle.generateKey( + "Ed25519", + true, + ["sign", "verify"], + ) as CryptoKeyPair; + const edClone = structuredClone(edKeyPair.privateKey); + assertEquals(edClone.type, "private"); + assertEquals(edClone.algorithm.name, "Ed25519"); +}); + +Deno.test("structuredClone DOMException", () => { + const original = new DOMException("test message", "DataError"); + const cloned = structuredClone(original); + assert(original !== cloned); + assertEquals(cloned.message, "test message"); + assertEquals(cloned.name, "DataError"); + assertEquals(cloned.code, original.code); + assert(cloned instanceof DOMException); +}); From 00dc38db23c35e5fa9c1d112dd5c58e6c5c75379 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?= Date: Fri, 13 Mar 2026 09:16:33 +0100 Subject: [PATCH 5/6] remove DOMException test (wrong PR) Co-Authored-By: Claude Opus 4.6 --- tests/unit/structured_clone_test.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/tests/unit/structured_clone_test.ts b/tests/unit/structured_clone_test.ts index 07a8754758c4d6..ed7665e56ce934 100644 --- a/tests/unit/structured_clone_test.ts +++ b/tests/unit/structured_clone_test.ts @@ -131,13 +131,3 @@ Deno.test("structuredClone CryptoKey", async () => { assertEquals(edClone.type, "private"); assertEquals(edClone.algorithm.name, "Ed25519"); }); - -Deno.test("structuredClone DOMException", () => { - const original = new DOMException("test message", "DataError"); - const cloned = structuredClone(original); - assert(original !== cloned); - assertEquals(cloned.message, "test message"); - assertEquals(cloned.name, "DataError"); - assertEquals(cloned.code, original.code); - assert(cloned instanceof DOMException); -}); From 2b895ebfbc0b8b3ab5bea34a2651cff118f27879 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?= Date: Fri, 13 Mar 2026 09:19:02 +0100 Subject: [PATCH 6/6] revert domexception --- ext/web/01_dom_exception.js | 28 +--------------------------- 1 file changed, 1 insertion(+), 27 deletions(-) diff --git a/ext/web/01_dom_exception.js b/ext/web/01_dom_exception.js index f50ebac2e37122..d654d07317f7ea 100644 --- a/ext/web/01_dom_exception.js +++ b/ext/web/01_dom_exception.js @@ -7,7 +7,7 @@ /// /// -import { core, primordials } from "ext:core/mod.js"; +import { primordials } from "ext:core/mod.js"; const { Error, ErrorPrototype, @@ -137,18 +137,6 @@ class DOMException { error[_name] = name; error[_code] = code; error[webidl.brand] = webidl.brand; - ObjectDefineProperty(error, core.hostObjectBrand, { - __proto__: null, - value: () => ({ - type: "DOMException", - message, - name, - stack: error.stack, - }), - enumerable: false, - configurable: false, - writable: false, - }); return error; } @@ -229,18 +217,4 @@ for (let i = 0; i < entries.length; ++i) { ObjectDefineProperty(DOMException.prototype, key, desc); } -core.registerCloneableResource("DOMException", (data) => { - const ex = new DOMException(data.message, data.name); - if (data.stack !== undefined) { - ObjectDefineProperty(ex, "stack", { - __proto__: null, - value: data.stack, - configurable: true, - writable: true, - enumerable: false, - }); - } - return ex; -}); - export { DOMException, DOMExceptionPrototype };