1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
|
// 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 assert from 'node:assert/strict';
import { createServer } from 'node:http';
import { mkdtemp, readFile, writeFile, symlink, rm, mkdir, rename } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { resolve, join } from 'node:path';
import { randomBytes } from 'node:crypto';
import { spawn, execFileSync } from 'node:child_process';
import { chromium, firefox } from '../../../.tools/browser/node_modules/playwright/index.mjs';
const root = await mkdtemp(join(tmpdir(), 'stack-chat-e2e-'));
const token = randomBytes(32).toString('hex');
const origin = 'http://127.0.0.1:9080';
const password = 'browser-test-only-unique-passphrase';
await writeFile(join(root, '.env'), `CHAT_ORIGIN=${origin}\nCHAT_SETUP_TOKEN=${token}\n`, {mode:0o600});
for (const path of ['crates','target','docs','web','LICENSE']) await symlink(resolve(path), join(root,path));
await mkdir(join(root,'var/build'), {recursive:true});
let server;
let serverErrors = '';
async function launchServer() {
server = spawn(resolve('target/debug/stack-chat-server'), ['--root',root], {stdio:['ignore','pipe','pipe']});
server.stderr.on('data', bytes => { serverErrors += bytes.toString(); });
await Promise.race([
new Promise((ready, reject) => { server.stdout.once('data', ready); server.once('exit', () => reject(new Error(serverErrors || 'Server exited'))); }),
new Promise((_, reject) => setTimeout(() => reject(new Error('Server did not start')), 20000).unref()),
]);
}
async function stopServer() {
if (server.exitCode !== null || server.signalCode !== null) return;
const stopped = new Promise(done => server.once('exit',done));
server.kill('SIGINT');
await stopped;
}
await launchServer();
const libre = process.env.CHAT_LIBREJS === '1';
let persistent;
let browser;
if (libre) {
const profile = join(root,'firefox-profile');
await mkdir(join(profile,'extensions'),{recursive:true});
await writeFile(join(profile,'extensions','jid1-KtlZuoiikVfFew@jetpack.xpi'),await readFile(resolve('.tools/librejs.xpi')));
persistent = await firefox.launchPersistentContext(profile, {headless:true,acceptDownloads:true,firefoxUserPrefs:{
'extensions.autoDisableScopes':0,'extensions.enabledScopes':15,'extensions.startupScanScopes':15,
'extensions.update.enabled':false,'app.update.auto':false,'datareporting.healthreport.uploadEnabled':false,
'browser.sessionstore.resume_from_crash':false,
}});
browser = { close: () => persistent.close() };
} else { browser = await chromium.launch({headless:true,channel:'chromium'}); }
const deadline = setTimeout(() => browser.close(), 300000);
const contexts = [];
const external = [];
const errors = [];
const bodies = [];
async function page() {
const context = persistent || await browser.newContext({acceptDownloads:true});
contexts.push(context);
const page = await context.newPage();
page.setDefaultTimeout(90000);
page.on('pageerror', error => errors.push(error.message));
page.on('request', request => {
if (!request.url().startsWith(origin + '/')) external.push(request.url());
if (request.url() === origin + '/api') bodies.push(JSON.parse(request.postData()));
});
return page;
}
async function waitStatus(page, text) { await page.waitForFunction(value => document.getElementById('status').textContent.includes(value), text); }
async function create(page, name, owner = false) {
if (owner) await page.fill('#setup-token', token); else await page.click('#create-choice');
await page.fill('#name', name);
await page.fill('#new-password', password);
await page.fill('#confirm-password', password);
const downloaded = Promise.race([page.waitForEvent('download'), page.waitForFunction(() => /failed|timed out/i.test(document.getElementById('status').textContent)).then(async () => { throw new Error(await page.locator('#status').textContent()); })]);
await page.click('#create-form button');
const download = await downloaded;
const bytes = await readFile(await download.path());
await waitStatus(page,'Reopen your downloaded');
assert.equal(await page.locator('#chat').isVisible(), false);
assert.equal(bodies.some(body => body.op === 'authenticate'), owner ? false : true);
return {name:'identity.chat',mimeType:'application/octet-stream',buffer:bytes};
}
async function unlock(page, file, phrase = password) {
await page.setInputFiles('#file', file);
await page.fill('#password', phrase);
await page.click('#unlock-form button');
}
async function storageEmpty(page) {
assert.equal(await page.evaluate(() => localStorage.length + sessionStorage.length),0);
assert.deepEqual(await page.evaluate(() => indexedDB.databases()),[]);
assert.equal(await page.evaluate(async () => (await navigator.serviceWorker.getRegistrations()).length),0);
assert.deepEqual(await page.context().cookies(),[]);
}
async function verifyLibreJS() {
let executed = false;
const controlServer = createServer((request,response) => {
response.setHeader('Cache-Control','no-store');
if (request.url === '/control.js') {
response.setHeader('Content-Type','text/javascript');
response.end("window.unlicensedControl = true; fetch('/executed');");
} else if (request.url === '/executed') { executed = true; response.end('unexpected'); }
else { response.setHeader('Content-Type','text/html'); response.end('<!doctype html><script src="/control.js"></script><p>Unlicensed external-script rejection control.</p>'); }
});
await new Promise(done => controlServer.listen(0,'127.0.0.1',done));
const controlPage = await persistent.newPage();
try {
await controlPage.goto(`http://127.0.0.1:${controlServer.address().port}/`);
await controlPage.waitForTimeout(1500);
assert.equal(await controlPage.evaluate(() => window.unlicensedControl),undefined,'LibreJS must reject the unlicensed control');
assert.equal(executed,false);
} finally { await controlPage.close(); await new Promise(done => controlServer.close(done)); }
}
try {
if (libre) await verifyLibreJS();
const owner = await page();
const response = await owner.goto(origin + '/#setup');
assert.match(response.headers()['cache-control'], /no-store/);
assert.match(response.headers()['content-security-policy'], /frame-ancestors 'none'/);
await waitStatus(owner,'Owner setup.');
const ownerFile = await create(owner,'Doan',true);
await unlock(owner,ownerFile);
await waitStatus(owner,'Unlocked in memory only');
assert.match(await owner.locator('#who').textContent(), /OWNER/);
const visitor = await page();
await visitor.goto(origin);
await waitStatus(visitor,'Choose CREATE');
const visitorFile = await create(visitor,'A friend');
assert.ok(!visitorFile.buffer.toString().includes('A friend'));
await unlock(visitor,visitorFile,'wrong-but-long-password');
await waitStatus(visitor,'Cryptographic operation failed');
assert.equal(await visitor.locator('#chat').isVisible(),false);
await unlock(visitor,visitorFile);
await waitStatus(visitor,'Unlocked in memory only');
assert.equal(await visitor.locator('#messages li').count(),0);
assert.match(await visitor.locator('#counts').textContent(),/Total: 0/);
const malicious = '<script>window.chatInjection = true</script> hello λ';
await visitor.fill('#text',malicious);
await visitor.click('#send-form button');
await waitStatus(visitor,'Encrypted message stored');
await owner.click('#refresh-inbox');
await owner.locator('#conversations button').first().click();
await waitStatus(owner,'Conversation opened');
assert.match(await owner.locator('#thread-name').textContent(),/A friend/);
assert.equal(await owner.locator('#messages li').count(),0);
await owner.fill('#history-count','1');
await owner.click('#history-form button');
await waitStatus(owner,'Selected history decrypted');
assert.equal(await owner.locator('#messages p').textContent(),malicious);
assert.equal(await owner.evaluate(() => window.chatInjection),undefined);
await owner.fill('#text','Reply from owner');
await owner.click('#send-form button');
await waitStatus(owner,'Encrypted message stored');
await visitor.fill('#history-count','2');
await visitor.click('#history-form button');
await waitStatus(visitor,'Selected history decrypted');
assert.equal(await visitor.locator('#messages li').count(),2);
assert.match(await visitor.locator('#counts').textContent(),/Total: 2 \| Sent: 1 \| Received: 1/);
let interrupted = false;
await visitor.route('**/api',async route => {
const body = route.request().postDataJSON();
if (body.op === 'send' && !interrupted) { interrupted = true; await route.fetch(); await route.abort(); }
else await route.continue();
});
await visitor.fill('#text','Safe retry after a lost response');
await visitor.click('#send-form button');
await visitor.locator('#retry').waitFor({state:'visible'});
await visitor.waitForFunction(() => !document.getElementById('retry').disabled);
await visitor.click('#retry');
await waitStatus(visitor,'Encrypted message stored');
assert.match(await visitor.locator('#counts').textContent(),/Total: 3/);
await visitor.reload();
await waitStatus(visitor,'Choose CREATE');
assert.equal(await visitor.locator('#messages li').count(),0);
assert.equal(await visitor.locator('#file').inputValue(),'');
assert.equal(await visitor.locator('#password').inputValue(),'');
await visitor.click('#open-choice');
await unlock(visitor,visitorFile);
await waitStatus(visitor,'Unlocked in memory only');
assert.match(await visitor.locator('#counts').textContent(),/Total: 3/);
assert.equal(await visitor.locator('#messages li').count(),0);
await visitor.fill('#history-count','1');
await visitor.click('#history-form button');
await waitStatus(visitor,'Selected history decrypted');
assert.match(await visitor.locator('#messages p').textContent(),/Safe retry/);
await visitor.click('#older');
await waitStatus(visitor,'Selected history decrypted');
assert.equal(await visitor.locator('#messages li').count(),2);
await visitor.fill('#history-count','0');
await visitor.click('#history-form button');
await waitStatus(visitor,'No message history loaded');
assert.equal(await visitor.locator('#messages li').count(),0);
const snapshot = join(root,'snapshot.sqlite');
execFileSync(resolve('target/debug/stack-chat-server'),['--root',root,'--snapshot',snapshot],{stdio:'pipe'});
await stopServer();
await rm(join(root,'var/data/chat/chat.sqlite'));
await rename(snapshot,join(root,'var/data/chat/chat.sqlite'));
await launchServer();
await visitor.reload();
await waitStatus(visitor,'Choose CREATE');
await visitor.click('#open-choice');
await unlock(visitor,visitorFile);
await waitStatus(visitor,'Unlocked in memory only');
assert.match(await visitor.locator('#counts').textContent(),/Total: 3/);
await visitor.fill('#history-count','3');
await visitor.click('#history-form button');
await waitStatus(visitor,'Selected history decrypted');
assert.equal(await visitor.locator('#messages li').count(),3);
assert.ok((await visitor.locator('#messages p').allTextContents()).includes(malicious));
await owner.reload();
await waitStatus(owner,'Choose CREATE');
await owner.click('#open-choice');
await unlock(owner,ownerFile);
await waitStatus(owner,'Unlocked in memory only');
assert.match(await owner.locator('#who').textContent(),/OWNER/);
await visitor.fill('#history-count','3');
await visitor.click('#history-form button');
await visitor.click('#lock');
await visitor.waitForTimeout(500);
assert.equal(await visitor.locator('#messages li').count(),0);
await visitor.goto(origin + '/LIMITATIONS.txt');
await visitor.goBack();
await visitor.waitForFunction(() => !document.getElementById('entry').hidden);
assert.equal(await visitor.locator('#messages li').count(),0);
assert.equal(await visitor.locator('#password').inputValue(),'');
assert.equal(await visitor.locator('#chat').isVisible(),false);
await storageEmpty(visitor);
await storageEmpty(owner);
assert.deepEqual(external,[]);
assert.deepEqual(errors,[]);
for (const body of bodies) {
for (const forbidden of ['file','password','secret','name','text']) assert.ok(!(forbidden in body),`Leaked ${forbidden}`);
assert.ok(!JSON.stringify(body).includes(malicious));
}
const database = await readFile(join(root,'var/data/chat/chat.sqlite'));
for (const plaintext of [malicious,'A friend','Reply from owner','Safe retry after a lost response',password]) assert.ok(!database.includes(Buffer.from(plaintext)),`Plaintext persisted: ${plaintext}`);
const blocked = await visitor.request.post(origin + '/api',{data:{op:'config'},headers:{Origin:'https://foreign.invalid'}});
assert.equal(blocked.status(),403);
assert.equal(serverErrors,'');
if (process.env.CHAT_SCREENSHOT) await visitor.screenshot({path:resolve('.tools/chat-preview.png'),fullPage:true});
if (libre) {
const extensions = JSON.parse(await readFile(join(root,'firefox-profile','extensions.json'),'utf8'));
const addon = extensions.addons.find(addon => addon.id === 'jid1-KtlZuoiikVfFew@jetpack');
assert.ok(addon?.active,'LibreJS must actually be active');
console.log(`LibreJS ${addon.version} is active in Firefox; complete chat workflow succeeded.`);
}
console.log('PASS: real browser/WASI owner setup, protected file recovery, wrong password, visitor/owner encrypted messages, safe text rendering, N=0, counts, pagination, retry deduplication, reload, live SQLite snapshot restoration and both identity recoveries, explicit lock, origin rejection, no browser storage or external requests, no private uploads or database plaintext.');
} finally {
clearTimeout(deadline);
await browser.close();
await stopServer();
await rm(root,{recursive:true,force:true});
}
|