summaryrefslogtreecommitdiff
path: root/crates/chat-crypto/tests/browser.ts
diff options
context:
space:
mode:
authorDoan Luu Duc Tai <doan@luuduct.ai>2026-10-31 12:25:30 -0500
committerDoan Luu Duc Tai <doan@luuduct.ai>2026-10-31 12:25:30 -0500
commit9139398d465f5899cc31bc4cdd54daa7392ca550 (patch)
treedebf0b15a969e8c64e3b3597f4eb7953b7f047e1 /crates/chat-crypto/tests/browser.ts
parentef61fc5aca6eca613a055c507a060b70bd537a37 (diff)
Migrate browser code and tests to strict TypeScript
Diffstat (limited to 'crates/chat-crypto/tests/browser.ts')
-rw-r--r--crates/chat-crypto/tests/browser.ts89
1 files changed, 89 insertions, 0 deletions
diff --git a/crates/chat-crypto/tests/browser.ts b/crates/chat-crypto/tests/browser.ts
new file mode 100644
index 0000000..75095ce
--- /dev/null
+++ b/crates/chat-crypto/tests/browser.ts
@@ -0,0 +1,89 @@
+// Copyright (C) 2026 Doan Luu Duc Tai <doan@luuduct.ai>
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+import { createServer } from 'node:http';
+import { readFile } from 'node:fs/promises';
+import { resolve } from 'node:path';
+import assert from 'node:assert/strict';
+import { chromium } from 'playwright';
+import { serverOrigin } from '../../../tests/browser-support.js';
+
+const assets = new Map<string, [string, string]>([
+ ['/wasi.mjs', [resolve('var/build/typescript/crates/chat-crypto/browser/wasi.js'), 'text/javascript']],
+ ['/probe.wasm', [resolve('target/wasm32-wasip1/release/stack-chat-crypto.wasm'), 'application/wasm']],
+]);
+const server = createServer(async (request, response) => {
+ response.setHeader('Cache-Control', 'no-store');
+ if (request.url === '/') {
+ response.setHeader('Content-Type', 'text/html');
+ response.end('<!doctype html><title>Local cryptographic compatibility test</title>');
+ return;
+ }
+ const asset = assets.get(request.url || '');
+ if (!asset) { response.writeHead(404); response.end(); return; }
+ response.setHeader('Content-Type', asset[1]);
+ response.end(await readFile(asset[0]));
+});
+await new Promise<void>(resolveReady => server.listen(0, '127.0.0.1', resolveReady));
+const origin = serverOrigin(server);
+const browser = await chromium.launch({headless: true, channel: "chromium"});
+const deadline = setTimeout(() => browser.close(), 120000);
+try {
+ const page = await browser.newPage();
+ const external: string[] = [];
+ page.on('request', request => {
+ if (!request.url().startsWith(origin + '/')) external.push(request.url());
+ });
+ await page.goto(origin);
+ const result = await page.evaluate(async () => {
+ const host = '/wasi.mjs';
+ await import(host);
+ const { runProbe } = globalThis.stackChatWasi;
+ const bytes = await (await fetch('/probe.wasm', { cache: 'no-store' })).arrayBuffer();
+ const valid = await runProbe(bytes);
+ const again = await runProbe(bytes);
+ const getRandomValues = crypto.getRandomValues;
+ let rejected = false;
+ crypto.getRandomValues = () => { throw new Error('Entropy unavailable'); };
+ try { await runProbe(bytes); } catch (error) { rejected = (error instanceof Error ? error.message : String(error)).includes("Entropy unavailable"); }
+ finally { crypto.getRandomValues = getRandomValues; }
+ const modified = new Uint8Array(bytes.slice(0));
+ const pattern = new TextEncoder().encode('random_get');
+ const offset = modified.findIndex((_value, index) => pattern.every((byte, position) => modified[index + position] === byte));
+ if (offset < 0) throw new Error('Expected entropy import was not found');
+ modified.set(new TextEncoder().encode('reject_get'), offset);
+ let unknownImportRejected = false;
+ try { await runProbe(modified.buffer); }
+ catch (error) { unknownImportRejected = (error instanceof Error ? error.message : String(error)).includes('Unsupported WASI import: wasi_snapshot_preview1.reject_get'); }
+ return { valid, again, rejected, unknownImportRejected };
+ });
+ assert.match(result.valid.output, /^PASS: backend=OpenSSL 3\.6\.5;/);
+ assert.match(result.again.output, /^PASS: backend=OpenSSL 3\.6\.5;/);
+ assert.ok(result.valid.randomCalls > 0 && result.valid.randomBytes > 0);
+ assert.ok(result.rejected, 'Unavailable browser entropy must fail closed');
+ assert.ok(result.unknownImportRejected, 'Unknown host imports must fail closed');
+ assert.deepEqual(external, []);
+ assert.deepEqual(await page.context().cookies(), []);
+ assert.equal(await page.evaluate(() => localStorage.length + sessionStorage.length), 0);
+ assert.deepEqual(await page.evaluate(async () => indexedDB.databases()), []);
+ assert.equal(await page.evaluate(async () => (await navigator.serviceWorker.getRegistrations()).length), 0);
+ console.log(result.valid.output.trim());
+ console.log(`Browser CSPRNG: ${result.valid.randomCalls} calls, ${result.valid.randomBytes} bytes; missing entropy and unknown imports rejected.`);
+ console.log('PASS: actual Chromium WASM execution; no external requests, cookies, web storage, or IndexedDB.');
+} finally {
+ clearTimeout(deadline);
+ await browser.close();
+ await new Promise(resolveClosed => server.close(resolveClosed));
+}