import { describe, expect, test, afterEach } from 'bun:test'; import { createWorkerCryptoProvider, SubtleCryptoProvider, WorkerCryptoProvider, } from '../src/index.js'; const WORKER_URL = new URL('../src/worker.ts', import.meta.url); const subtle = new SubtleCryptoProvider(); let provider: WorkerCryptoProvider | null = null; afterEach(async () => { if (provider) { await provider.destroy(); provider = null; } }); async function makeProvider(idleTimeoutMs = 30_000): Promise { provider = await createWorkerCryptoProvider({ workerUrl: WORKER_URL, idleTimeoutMs, }); return provider; } describe('WorkerCryptoProvider — roundtrip and parity', () => { test('handshake completes', async () => { const p = await makeProvider(); expect(p).toBeInstanceOf(WorkerCryptoProvider); }); test('AES-GCM encrypt → worker, decrypt locally — produces same plaintext', async () => { const p = await makeProvider(); const key = subtle.randomBytes(32); const plaintext = new TextEncoder().encode('hello shade workers — large enough payload'); const enc = await p.aesGcmEncrypt(key, plaintext); expect(enc.nonce.length).toBe(12); // Decrypt with the local SubtleCryptoProvider — proves wire compatibility const dec = await subtle.aesGcmDecrypt(key, enc.ciphertext, enc.nonce); expect(dec).toEqual(plaintext); }); test('AES-GCM with AAD round-trips through worker', async () => { const p = await makeProvider(); const key = subtle.randomBytes(32); const plaintext = subtle.randomBytes(1024); const aad = subtle.randomBytes(16); const enc = await p.aesGcmEncrypt(key, plaintext, aad); const dec = await p.aesGcmDecrypt(key, enc.ciphertext, enc.nonce, aad); expect(dec).toEqual(plaintext); }); test('AES-GCM decrypt rejects tampered ciphertext', async () => { const p = await makeProvider(); const key = subtle.randomBytes(32); const plaintext = new TextEncoder().encode('untampered'); const enc = await p.aesGcmEncrypt(key, plaintext); enc.ciphertext[0]! ^= 0x01; await expect(p.aesGcmDecrypt(key, enc.ciphertext, enc.nonce)).rejects.toThrow(); }); test('HKDF parity with SubtleCryptoProvider', async () => { const p = await makeProvider(); const ikm = subtle.randomBytes(32); const salt = subtle.randomBytes(16); const info = new TextEncoder().encode('test info'); const a = await p.hkdf(ikm, salt, info, 64); const b = await subtle.hkdf(ikm, salt, info, 64); expect(a).toEqual(b); }); test('HMAC-SHA256 parity with SubtleCryptoProvider', async () => { const p = await makeProvider(); const key = subtle.randomBytes(32); const data = subtle.randomBytes(256); const a = await p.hmacSha256(key, data); const b = await subtle.hmacSha256(key, data); expect(a).toEqual(b); }); test('X25519 DH agrees with SubtleCryptoProvider', async () => { const p = await makeProvider(); const alice = await p.generateX25519KeyPair(); const bob = await subtle.generateX25519KeyPair(); const ab = await p.x25519(alice.privateKey, bob.publicKey); const ba = await subtle.x25519(bob.privateKey, alice.publicKey); expect(ab).toEqual(ba); }); test('Ed25519 sign in worker, verify locally', async () => { const p = await makeProvider(); const kp = await p.generateEd25519KeyPair(); const msg = new TextEncoder().encode('please sign me'); const sig = await p.sign(kp.privateKey, msg); expect(await subtle.verify(kp.publicKey, msg, sig)).toBe(true); }); test('Ed25519 verify rejects tampered signature', async () => { const p = await makeProvider(); const kp = await subtle.generateEd25519KeyPair(); const msg = new TextEncoder().encode('msg'); const sig = await subtle.sign(kp.privateKey, msg); sig[0]! ^= 0x01; expect(await p.verify(kp.publicKey, msg, sig)).toBe(false); }); test('local sync helpers do not round-trip', async () => { const p = await makeProvider(); const a = p.randomBytes(16); expect(a.length).toBe(16); expect(p.constantTimeEqual(a, a)).toBe(true); expect(p.constantTimeEqual(a, new Uint8Array(16))).toBe(false); expect(typeof p.randomUint32()).toBe('number'); }); test('errors from worker propagate as rejected promises', async () => { const p = await makeProvider(); const wrongKey = subtle.randomBytes(32); const ct = subtle.randomBytes(48); const nonce = subtle.randomBytes(12); await expect(p.aesGcmDecrypt(wrongKey, ct, nonce)).rejects.toThrow(); }); test('parallel calls do not interleave incorrectly', async () => { const p = await makeProvider(); const key = subtle.randomBytes(32); const inputs = Array.from({ length: 16 }, (_, i) => new TextEncoder().encode(`payload-${i}-${'x'.repeat(50 * i)}`), ); const encs = await Promise.all(inputs.map((pt) => p.aesGcmEncrypt(key, pt))); const decs = await Promise.all( encs.map((e) => p.aesGcmDecrypt(key, e.ciphertext, e.nonce)), ); decs.forEach((d, i) => expect(d).toEqual(inputs[i]!)); }); test('after destroy(), calls reject', async () => { const p = await makeProvider(); await p.destroy(); await expect(p.aesGcmEncrypt(subtle.randomBytes(32), new Uint8Array(8))).rejects.toThrow( /destroyed/, ); provider = null; }); test('rotate() respawns transparently', async () => { const p = await makeProvider(); const key = subtle.randomBytes(32); await p.aesGcmEncrypt(key, new Uint8Array(8)); await p.rotate(); const out = await p.aesGcmEncrypt(key, new TextEncoder().encode('still works')); const dec = await subtle.aesGcmDecrypt(key, out.ciphertext, out.nonce); expect(new TextDecoder().decode(dec)).toBe('still works'); }); test('idle-timeout terminates worker but next call respawns', async () => { const p = await makeProvider(120); const key = subtle.randomBytes(32); await p.aesGcmEncrypt(key, new Uint8Array(8)); // Wait for the idle timer to fire. await new Promise((r) => setTimeout(r, 250)); // Next call should still succeed — proves respawn works. const out = await p.aesGcmEncrypt(key, new TextEncoder().encode('respawned')); const dec = await subtle.aesGcmDecrypt(key, out.ciphertext, out.nonce); expect(new TextDecoder().decode(dec)).toBe('respawned'); }); test('configureWorkerCrypto throws on protocol mismatch', async () => { // Spawn with a fake "spawn" that returns a worker echoing the wrong version. const fakeProvider = new WorkerCryptoProvider({ workerUrl: WORKER_URL, spawn: () => { type Listener = (ev: { data: unknown }) => void; const listeners: Listener[] = []; return { postMessage(msg: unknown): void { const m = msg as { id: number; method: string }; if (m.method === 'init') { setTimeout(() => { for (const l of listeners) { l({ data: { id: m.id, ok: false, error: { name: 'Error', message: 'protocol version mismatch' }, }, }); } }, 0); } }, addEventListener(type: string, listener: Listener): void { if (type === 'message') listeners.push(listener); }, removeEventListener(): void { // no-op }, terminate(): void { // no-op }, }; }, }); await expect(fakeProvider.handshake()).rejects.toThrow(/protocol/); await fakeProvider.destroy(); }); });