const { test } = require('node:test'); const assert = require('node:assert/strict'); const fs = require('node:fs'); const path = require('node:path'); const vm = require('node:vm'); const { webcrypto } = require('node:crypto'); const { JSDOM } = require('jsdom'); const root = path.join(__dirname, '..'); const read = file => fs.readFileSync(path.join(root, file), 'utf8'); const tick = () => new Promise(resolve => setImmediate(resolve)); const deferred = () => { let resolve, reject; const promise = new Promise((yes, no) => { resolve = yes; reject = no; }); return { promise, resolve, reject }; }; const response = (data, status = 200) => ({ status, ok: status < 400, json: async () => data }); // Real application scripts, DOM and ES-module bodies. Only transport, storage, // time, recorder hardware and navigation are faked; no external resources load. async function browser({ local = {}, session = {}, native = false, broadcast = false, url = 'https://offline.invalid/' } = {}) { const dom = new JSDOM(read('public/index.html'), { url }); const w = dom.window; await tick(); // Avoid a duplicate DOMContentLoaded when evaluating auth.js below. for (const [k, v] of Object.entries(local)) w.localStorage.setItem(k, v); for (const [k, v] of Object.entries(session)) w.sessionStorage.setItem(k, v); const timers = new Map(), calls = [], secure = new Map(), channels = []; let serial = 0, reloads = 0; const c = { document: w.document, localStorage: w.localStorage, sessionStorage: w.sessionStorage, Event: w.Event, CustomEvent: w.CustomEvent, DOMException: w.DOMException, AbortController: w.AbortController, URL: w.URL, URLSearchParams: w.URLSearchParams, Blob: w.Blob, FormData: w.FormData, crypto: webcrypto, history: w.history, location: { href: url, origin: new URL(url).origin, pathname: '/', search: new URL(url).search, reload() { reloads++; if (c.reloadThrows) throw new Error('navigation denied'); } }, navigator: { mediaDevices: { getUserMedia: () => Promise.reject(new Error('No fake mic configured')) }, clipboard: { writeText: text => { c.clipboard = text; return Promise.resolve(); } }, sendBeacon: (...args) => { calls.push({ beacon: args }); return true; } }, addEventListener: w.addEventListener.bind(w), removeEventListener: w.removeEventListener.bind(w), dispatchEvent: w.dispatchEvent.bind(w), console: { log() {}, warn() {}, error() {} }, setTimeout(fn, ms) { const id = ++serial; timers.set(id, { fn, ms }); return id; }, clearTimeout(id) { timers.delete(id); }, setInterval() { return ++serial; }, clearInterval() {}, showToast() {}, showLoading() {}, hideLoading() {}, showBusy() {}, hideBusy() {}, getAuthHeaders() { return {}; }, fetch(input, init = {}) { const url = typeof input === 'string' ? input : input.url || input.href; calls.push({ url, init }); return c.api(url, init); }, api: async () => response({ success: true }), Capacitor: { isNativePlatform: () => native, Plugins: { SecureStoragePlugin: { get: async ({ key }) => ({ value: secure.get(key) }), set: async ({ key, value }) => { secure.set(key, value); }, remove: async ({ key }) => { secure.delete(key); } } } }, MediaRecorder: class extends w.EventTarget { constructor(stream) { super(); this.stream = stream; this.state = 'inactive'; } start() { this.state = 'recording'; } stop() { this.state = 'inactive'; this.stopped = true; } static isTypeSupported() { return true; } }, SpeechRecognition: class { start() { this.started = true; } abort() { this.aborted = true; } } }; if (broadcast) c.BroadcastChannel = class { constructor() { channels.push(this); } postMessage(data) { this.sent = data; } }; c.window = c; vm.createContext(c); const cache = {}; function module(file) { if (cache[file]) return cache[file]; const imports = {}; let source = read(file).replace(/^import ([\s\S]+?) from '([^']+)';$/gm, (_, names, relative) => { const dependency = module(path.posix.normalize(path.posix.join(path.posix.dirname(file), relative))); if (names.startsWith('* as ')) imports[names.slice(5)] = dependency; else names.slice(1, -1).split(',').forEach(part => { const [from, to = from] = part.trim().split(/\s+as\s+/); imports[to] = dependency[from]; }); return ''; }); const exports = [...source.matchAll(/export (?:async )?function (\w+)/g)].map(match => match[1]); source = source.replace(/\bexport /g, ''); const fn = vm.runInContext('(function(' + Object.keys(imports).join(',') + ') {\n' + source + '\nreturn {' + exports.join(',') + '};\n})', c, { filename: file }); return cache[file] = fn(...Object.values(imports)); } function script(file) { vm.runInContext(read(file), c, { filename: file }); } script('public/js/secureStorage.js'); script('public/js/accountBoundary.js'); module('public/js/authFetch.js'); function owner(id) { assert.equal(c.AccountBoundary.enter({ id }, true), true); c.CURRENT_USER = { id }; } let ssoHandler; async function auth(id, meStatus = 200) { c.api = async url => response(url === '/api/auth/me' ? (id ? { user: { id, name: id, role: 'user' } } : {}) : {}, url === '/api/auth/me' ? meStatus : 200); // jsdom cannot synthesize trusted clicks: retain the actual registered handler. const button = w.document.getElementById('btn-sso'); const add = button.addEventListener.bind(button); button.addEventListener = (type, handler, options) => { if (type === 'click') ssoHandler = handler; return add(type, handler, options); }; script('public/js/auth.js'); w.document.dispatchEvent(new w.Event('DOMContentLoaded')); await tick(); } function login(id) { c.api = async url => response(url === '/api/auth/login' ? { success: true, token: 'token-' + id, sessionId: 'sid-' + id, user: { id, name: id, role: 'user' } } : {}); w.document.getElementById('login-email').value = id + '@example.invalid'; w.document.getElementById('login-password').value = 'fake-password'; w.document.getElementById('login-form').dispatchEvent(new w.Event('submit', { cancelable: true })); } function storageMessage(data) { w.localStorage.setItem(c.AccountBoundary.key, JSON.stringify(data)); w.dispatchEvent(new w.StorageEvent('storage', { key: c.AccountBoundary.key, newValue: JSON.stringify(data) })); } function runTimers(ms) { for (const [id, timer] of [...timers]) if (timer.ms === ms) { timers.delete(id); timer.fn(); } } function sso(trusted = true) { const event = { isTrusted: trusted, defaultPrevented: false, preventDefault() { this.defaultPrevented = true; } }; return { event, done: Promise.resolve(ssoHandler(event)) }; } return { c, w, calls, secure, channels, owner, auth, login, sso, module, script, storageMessage, runTimers, reloads: () => reloads, snapshot: () => ({ local: { ...w.localStorage }, session: { ...w.sessionStorage } }), close: () => w.close() }; } function fakeIndexedDB(c, records) { let next = 20; const transactions = []; const db = { transaction() { const tx = new c.window.document.defaultView.EventTarget(); transactions.push(tx); let pending = 0, aborted = false; const writes = []; const later = fn => { pending++; queueMicrotask(() => { if (!aborted) fn(); pending--; complete(); }); }; function complete() { if (pending || aborted) return; for (const write of writes.splice(0)) write(); tx.dispatchEvent(new c.Event('complete')); if (tx.oncomplete) tx.oncomplete(); } tx.abort = () => { aborted = true; tx.dispatchEvent(new c.Event('abort')); if (tx.onabort) tx.onabort(); }; function request(value) { const req = {}; later(() => { req.result = value; if (req.onsuccess) req.onsuccess(); }); return req; } tx.objectStore = () => ({ get: id => request(records.get(id)), getAll: () => request([...records.values()]), add(record) { const id = ++next; writes.push(() => records.set(id, { ...record, id })); return request(id); }, delete(id) { writes.push(() => records.delete(id)); later(() => {}); }, openCursor() { const req = {}, values = [...records.values()]; let index = 0; function advance() { later(() => { const value = values[index++]; req.result = value ? { value, delete() { writes.push(() => records.delete(value.id)); }, continue: advance } : null; if (req.onsuccess) req.onsuccess({ target: req }); }); } advance(); return req; } }); return tx; } }; c.indexedDB = { open() { const req = {}; queueMicrotask(() => req.onsuccess({ target: { result: db } })); return req; } }; return transactions; } const preferences = { ped_last_tab: 'clinical-assistant', ped_sidebar_collapsed: '1', ped_web_speech_enabled: '1', 'other-app': 'retain' }; test('actual auth: A → failed logout → fresh login B; canceled/throwing reload stays hidden, inert and recoverable', async () => { const a = await browser({ local: { ...preferences, ped_ed_draft_v1: 'legacy PHI', ped_visit_statuses: 'legacy' }, session: { _savedEncId_ed: 'old', _idempKey_ed: 'old', ped_visit_age: 'old' } }); try { await a.auth('A'); assert.equal(a.c.AccountBoundary.capture(), 'A'); assert.equal(a.w.localStorage.getItem('ped_ed_draft_v1'), 'legacy PHI'); assert.equal(a.w.sessionStorage.getItem('_savedEncId_ed'), 'old'); a.c.reloadThrows = true; a.c.api = async () => { throw new Error('offline logout'); }; a.w.document.getElementById('btn-logout').click(); assert.equal(a.c.AccountBoundary.blocked(), true); assert.equal(a.w.document.getElementById('main-app').inert, true); assert.equal(a.w.document.documentElement.classList.contains('account-transition'), true); assert.equal(a.w.document.getElementById('account-recovery').inert, undefined); a.w.dispatchEvent(new a.c.Event('beforeunload')); await tick(); assert.ok(a.reloads()); assert.equal(a.c.AccountBoundary.enter({ id: 'B' }, true), false); await assert.rejects(a.c.fetch('/api/notes', { method: 'POST', keepalive: true }), { name: 'AbortError' }); const b = await browser(a.snapshot()); try { await b.auth('A'); // Failed logout's cookie remains valid, but must not be consulted. assert.ok(!b.calls.some(call => call.url === '/api/auth/me')); assert.equal(b.c.CURRENT_USER, undefined); b.login('B'); await tick(); assert.equal(b.c.AccountBoundary.capture(), 'B'); for (const [key, value] of Object.entries(preferences)) assert.equal(b.w.localStorage.getItem(key), value); } finally { b.close(); } } finally { a.close(); } }); test('401, storage fallback, BroadcastChannel and BFCache all converge; queued A event cannot erase B native credentials', async () => { for (const mode of ['401', 'storage', 'broadcast', 'bfcache']) { const b = await browser({ native: true, broadcast: mode === 'broadcast' }); try { b.owner('A'); b.secure.set('ped_scribe_token', 'token-B'); const late = deferred(); b.c.api = () => late.promise; const pending = b.c.fetch('/api/notes').catch(err => err); if (mode === '401') late.resolve(response({}, 401)); else if (mode === 'bfcache') b.w.dispatchEvent(new b.w.PageTransitionEvent('pageshow', { persisted: true })); else { const state = { owner: 'B', generation: 'new-B', signedOut: false }; if (mode === 'storage') b.storageMessage(state); else { b.w.localStorage.setItem(b.c.AccountBoundary.key, JSON.stringify(state)); b.channels[0].onmessage({ data: state }); } late.resolve(response({ secret: 'A' })); } if (mode === 'bfcache') late.resolve(response({ secret: 'A' })); assert.equal((await pending).name, 'AbortError'); assert.equal(b.c.AccountBoundary.blocked(), true); assert.ok(b.reloads()); assert.equal(b.secure.get('ped_scribe_token'), 'token-B'); assert.equal(b.calls[0].init.signal.aborted, true); } finally { b.close(); } } const b = await browser({ broadcast: true }); try { b.owner('B'); const count = b.reloads(); b.channels[0].onmessage({ data: { owner: 'A', generation: 'obsolete', signedOut: true } }); assert.equal(b.reloads(), count); assert.equal(b.c.AccountBoundary.capture(), 'B'); } finally { b.close(); } }); test('fetch uses actual same-origin URL, gates pre-auth PHI, pending body/Request/caller abort and beacon writes', async () => { const b = await browser(); try { await assert.rejects(b.c.fetch('/api/notes'), { name: 'AbortError' }); assert.equal(b.calls.length, 0); b.owner('A'); b.c.api = async () => response({}, 401); await b.c.fetch('https://external.invalid/api/notes'); await b.c.fetch('/not-api?next=/api/notes'); assert.equal(b.c.AccountBoundary.active(), true); const body = deferred(); b.c.api = async () => ({ status: 200, json: () => body.promise }); const req = new Request('https://offline.invalid/api/notes', { method: 'PUT', body: 'A' }); const resp = await b.c.fetch(req); const pending = resp.json().catch(err => err); b.c.AccountBoundary.end(); body.resolve({ secret: 'A' }); assert.equal((await pending).name, 'AbortError'); assert.equal(b.c.navigator.sendBeacon('/api/logs/client-error', 'old clinical text'), false); await assert.rejects(b.c.fetch(new b.c.URL('/api/notes', b.c.location.href)), { name: 'AbortError' }); } finally { b.close(); } const a = await browser(); try { a.owner('A'); const abort = new a.c.AbortController(); abort.abort(); await a.c.fetch('/api/notes', { signal: abort.signal }); assert.equal(a.calls[0].init.signal.aborted, true); } finally { a.close(); } }); test('actual native auth preserves logout headers and waits for new token/user/session persistence before replacement reload', async () => { const b = await browser({ native: true }); try { b.secure.set('ped_scribe_token', 'token-A'); await b.auth('A'); const persisted = deferred(); const originalSet = b.c.SecureStorage.set; b.c.SecureStorage.set = async (key, value) => { await persisted.promise; return originalSet(key, value); }; b.login('B'); await tick(); assert.equal(b.c.AccountBoundary.blocked(), true); assert.equal(b.reloads(), 0); assert.equal(b.secure.get('ped_scribe_token'), 'token-A'); persisted.resolve(); await tick(); assert.equal(b.secure.get('ped_scribe_token'), 'token-B'); assert.equal(b.secure.get('ped_session_id'), 'sid-B'); assert.equal(JSON.parse(b.secure.get('ped_scribe_user')).id, 'B'); assert.ok(b.reloads()); assert.equal(b.c.CURRENT_USER.id, 'A', 'B must not enter retained A module state'); } finally { b.close(); } const a = await browser({ native: true }); try { a.secure.set('ped_scribe_token', 'token-A'); await a.auth('A'); a.w.document.getElementById('btn-logout').click(); await tick(); assert.equal(a.calls.find(call => call.url === '/api/auth/logout').init.headers.Authorization, 'Bearer token-A'); assert.equal(a.secure.has('ped_scribe_token'), false); } finally { a.close(); } }); test('actual SSO boot respects signed-out latch, one-shot bounded intent and verified /me result', async () => { const latch = JSON.stringify({ generation: 'logout', owner: null, signedOut: true }); for (const [intent, user, allowed] of [[null, 'A', false], [Date.now() - 600000, 'A', false], [Date.now(), null, false], [Date.now(), 'B', true]]) { const b = await browser({ url: 'https://offline.invalid/?sso=ok', local: { ped_account_boundary_v1: latch }, session: intent ? { ped_sso_intent: String(intent) } : {} }); try { await b.auth(user); assert.equal(b.c.AccountBoundary.active(), allowed); assert.equal(b.c.AccountBoundary.signedOut(), !allowed); assert.equal(b.w.sessionStorage.getItem('ped_sso_intent'), null); } finally { b.close(); } } }); test('actual SSO start freezes and publishes before logout, requires cookie-only 401, then keeps the existing href', async () => { for (const owned of [false, true]) { const b = await browser({ native: true, local: owned ? {} : { ped_account_boundary_v1: JSON.stringify({ owner: null, signedOut: true, generation: 'old-logout' }) } }); try { b.secure.set('ped_scribe_token', 'token-A'); await b.auth('A'); const logout = deferred(), probe = deferred(); b.calls.length = 0; b.c.api = url => { assert.equal(b.c.AccountBoundary.blocked(), true, 'Freeze precedes transport/cookie mutation'); assert.equal(b.c.AccountBoundary.signedOut(), true, 'Publish precedes transport/cookie mutation'); return url === '/api/auth/logout' ? logout.promise : probe.promise; }; const href = b.w.document.getElementById('btn-sso').href; const start = b.sso(); assert.equal(start.event.defaultPrevented, true); assert.equal(b.c.AccountBoundary.blocked(), true); assert.equal(b.c.AccountBoundary.signedOut(), true); assert.equal(b.w.document.activeElement.textContent, 'Reload'); assert.equal(b.calls.length, 1); assert.equal(b.calls[0].init.method, 'POST'); assert.equal(b.calls[0].init.credentials, 'same-origin'); assert.equal(b.calls[0].init.headers.Authorization, owned ? 'Bearer token-A' : undefined, 'Signed-out requests never use an unverified native token'); assert.equal(b.w.sessionStorage.getItem('ped_sso_intent'), null); logout.resolve(response({ success: true })); await tick(); assert.equal(b.calls[1].url, '/api/auth/me'); assert.equal(b.calls[1].init.credentials, 'same-origin'); assert.equal(b.calls[1].init.headers, undefined, 'Old native Authorization must not reach the probe'); assert.equal(b.calls[1].init.cache, 'no-store'); assert.equal(b.w.sessionStorage.getItem('ped_sso_intent'), null); probe.resolve(response({}, 401)); await start.done; assert.equal(b.c.location.href, href); assert.ok(Date.now() - Number(b.w.sessionStorage.getItem('ped_sso_intent')) < 5000); assert.equal(b.c.AccountBoundary.blocked(), true, 'Canceled navigation must not unlock this realm'); const next = await browser({ ...b.snapshot(), url: 'https://offline.invalid/?sso=ok', native: true }); try { await next.auth('B'); assert.equal(next.c.AccountBoundary.capture(), 'B'); assert.equal(next.w.sessionStorage.getItem('ped_sso_intent'), null); } finally { next.close(); } } finally { b.close(); } } }); test('actual SSO failure, false logout success, stale sibling and navigation denial stay latched without erasing B credentials', async () => { for (const mode of ['logout-network', 'logout-500', 'probe-network', 'probe-200', 'probe-403', 'probe-503', 'sibling', 'navigation']) { const b = await browser({ native: true, broadcast: true }); try { b.secure.set('ped_scribe_token', 'token-A'); await b.auth('A'); const probe = deferred(); b.c.api = async url => { if (url === '/api/auth/logout') { if (mode === 'logout-network') throw new Error('offline'); return response({ success: true }, mode === 'logout-500' ? 500 : 200); } return probe.promise; }; if (mode === 'navigation') Object.defineProperty(b.c.location, 'href', { get: () => 'https://offline.invalid/', set() { throw new Error('navigation denied'); } }); const start = b.sso(); await tick(); if (mode === 'sibling') { b.secure.set('ped_scribe_token', 'token-B'); b.storageMessage({ owner: 'B', generation: 'new-B', signedOut: false }); b.channels[0].onmessage({ data: { owner: 'A', generation: 'obsolete-A', signedOut: true } }); } if (mode === 'probe-network') probe.reject(new Error('offline')); else probe.resolve(response({}, /^probe-/.test(mode) ? Number(mode.slice(6)) : 401)); await start.done; assert.equal(b.c.location.href, 'https://offline.invalid/'); assert.equal(b.w.sessionStorage.getItem('ped_sso_intent'), null); assert.equal(b.c.AccountBoundary.blocked(), true); assert.equal(b.w.document.getElementById('account-recovery').getAttribute('role'), 'alert'); assert.equal(b.w.document.getElementById('main-app').inert, true); if (mode === 'sibling') { assert.equal(b.c.AccountBoundary.read().owner, 'B'); assert.equal(b.secure.get('ped_scribe_token'), 'token-B'); } else assert.equal(b.c.AccountBoundary.signedOut(), true); } finally { b.close(); } } }); test('apparently signed-out SSO verifies cookie absence; synthetic clicks never create intent', async () => { const b = await browser(); try { await b.auth(null); b.calls.length = 0; b.c.api = async url => response({}, url === '/api/auth/me' ? 401 : 200); const synthetic = b.sso(false); await synthetic.done; assert.equal(synthetic.event.defaultPrevented, true); assert.equal(b.w.sessionStorage.getItem('ped_sso_intent'), null); assert.equal(b.calls.length, 0); const start = b.sso(); await start.done; assert.equal(start.event.defaultPrevented, true); assert.deepEqual(b.calls.map(call => call.url), ['/api/auth/logout', '/api/auth/me']); assert.equal(b.c.AccountBoundary.blocked(), true); assert.ok(b.w.sessionStorage.getItem('ped_sso_intent')); assert.equal(b.c.location.href, b.w.document.getElementById('btn-sso').href); assert.equal(b.w.document.getElementById('btn-sso').getAttribute('href'), '/api/auth/oidc'); } finally { b.close(); } }); test('SSO after bootstrap failure clears an unobserved old cookie before Back can restore its account', async () => { const b = await browser(); try { await b.auth('A', 503); assert.equal(b.c.AccountBoundary.read(), null); assert.equal(b.c.AccountBoundary.active(), false); let oldCookie = true; b.calls.length = 0; b.c.api = async url => { assert.equal(b.c.AccountBoundary.blocked(), true); assert.equal(b.c.AccountBoundary.signedOut(), true); if (url === '/api/auth/logout') { oldCookie = false; return response({ success: true }); } assert.equal(url, '/api/auth/me'); return oldCookie ? response({ user: { id: 'A' } }) : response({}, 401); }; const start = b.sso(); assert.equal(start.event.defaultPrevented, true, 'Missing owner metadata is not proof of cookie absence'); await start.done; assert.equal(oldCookie, false); assert.deepEqual(b.calls.map(call => call.url), ['/api/auth/logout', '/api/auth/me']); assert.equal(b.calls[1].init.headers, undefined); assert.equal(b.calls[1].init.cache, 'no-store'); assert.equal(b.c.location.href, b.w.document.getElementById('btn-sso').href); const back = await browser(b.snapshot()); try { await back.auth('A'); // Even a surviving old server session cannot silently re-enter. assert.equal(back.c.AccountBoundary.active(), false); assert.ok(!back.calls.some(call => call.url === '/api/auth/me')); } finally { back.close(); } } finally { b.close(); } }); test('platform recorder guards stop actual AudioRecorder callers, speech and late mic permission without old audio writes', async () => { const b = await browser(); try { b.owner('A'); // Evaluate the real shared recorder implementation, not a rewritten test recorder. const source = read('public/js/app.js'); vm.runInContext(source.slice(source.indexOf('function AudioRecorder()'), source.indexOf('// ── Native mobile helpers')), b.c); const stream = { getTracks: () => [track] }, track = { stop() { this.stopped = true; } }; // Install fake hardware before a fresh boundary (the production script wraps it at boot). const mic = deferred(); const fresh = await browser(); try { fresh.c.navigator.mediaDevices.getUserMedia = () => mic.promise; fresh.script('public/js/accountBoundary.js'); fresh.owner('A'); vm.runInContext(source.slice(source.indexOf('function AudioRecorder()'), source.indexOf('// ── Native mobile helpers')), fresh.c); const recorder = new fresh.c.AudioRecorder(); const start = recorder.start().catch(err => err); fresh.c.AccountBoundary.end(); mic.resolve(stream); assert.equal((await start).name, 'AbortError'); assert.equal(track.stopped, true); } finally { fresh.close(); } const rec = new b.c.MediaRecorder(stream); rec.onstop = () => { throw new Error('Late callback'); }; rec.start(); const speech = new b.c.SpeechRecognition(); speech.start(); b.c.AccountBoundary.end(); assert.equal(rec.stopped, true); assert.equal(rec.onstop, null); assert.equal(speech.aborted, true); assert.throws(() => rec.start(), { name: 'AbortError' }); } finally { b.close(); } }); test('actual audio server-first fallback and IndexedDB transactions cannot migrate across owners; list/retry/delete enforce owner', async () => { const records = new Map([ [1, { id: 1, owner: 'A', timestamp: Date.now(), blob: 'audio-A' }], [2, { id: 2, owner: 'B', timestamp: Date.now(), blob: 'audio-B' }], [3, { id: 3, timestamp: Date.now(), blob: 'legacy' }], [4, { id: 4, timestamp: Date.now() - 86400001, blob: 'expired legacy' }] ]); const a = await browser(); try { fakeIndexedDB(a.c, records); a.module('public/js/audioBackup.js'); await tick(); assert.equal(records.has(3), true, 'Fresh unowned audio survives boot without adoption'); assert.equal(records.has(4), false, 'Existing 24-hour age policy still applies'); a.owner('A'); const delayed = deferred(); a.c.api = () => delayed.promise; const save = a.c.saveAudioBackup(new a.c.Blob(['old']), 'notes'); a.c.AccountBoundary.end(); delayed.reject(new Error('offline')); assert.equal(await save, null); assert.equal(records.size, 3); } finally { a.close(); } const b = await browser(); try { b.owner('B'); fakeIndexedDB(b.c, records); b.module('public/js/audioBackup.js'); b.c.api = async () => response({ success: false }); const backups = await b.c.getAudioBackups(); assert.deepEqual(Array.from(backups, row => row.id), ['local_2']); for (const id of [1, 3]) { await assert.rejects(b.c.retryAudioBackup('local_' + id), /Backup not found/); await b.c.deleteAudioBackup('local_' + id); assert.equal(records.has(id), true); } let transcribed; b.c.transcribeAudio = async blob => { transcribed = blob; return { success: true, text: 'B text' }; }; await b.c.retryAudioBackup('local_2'); assert.equal(transcribed, 'audio-B'); await b.c.deleteAudioBackup('local_2'); assert.equal(records.has(2), false); const id = await b.c.saveAudioBackup(new b.c.Blob(['B']), 'notes'); assert.equal(records.get(id).owner, 'B'); } finally { b.close(); } }); test('actual Notes autosave, beforeunload keepalive and recorder continuations are discarded on transition', async () => { const b = await browser(); try { b.owner('A'); b.w.document.getElementById('notes-tab').innerHTML = read('public/components/notes.html'); b.c.api = async () => response({ success: true, notes: [] }); b.module('public/js/notes.js'); b.w.document.dispatchEvent(new b.c.CustomEvent('tabChanged', { detail: { tab: 'notes' } })); await tick(); b.w.document.getElementById('btn-notes-new').click(); const title = b.w.document.getElementById('note-title'); title.value = 'A private note'; title.dispatchEvent(new b.c.Event('input')); b.w.dispatchEvent(new b.c.Event('beforeunload')); await tick(); assert.ok(b.calls.some(call => call.init.keepalive && JSON.parse(call.init.body).title === 'A private note')); const delayed = deferred(); b.c.api = () => delayed.promise; b.runTimers(1200); const count = b.calls.length; b.c.AccountBoundary.end(); b.w.dispatchEvent(new b.c.Event('beforeunload')); b.runTimers(1200); delayed.resolve(response({ success: true, id: 42 })); await tick(); b.runTimers(1200); assert.equal(b.calls.length, count, 'No late autosave/list refresh/keepalive'); } finally { b.close(); } const a = await browser(); try { a.owner('A'); const stopped = deferred(); a.c.AudioRecorder = class { start() { return Promise.resolve(); } stop() { return stopped.promise; } }; let transcribed = 0, applied = 0; a.c.transcribeAudio = async () => { transcribed++; return { success: true, text: 'A' }; }; const { createNotesRecorder } = a.module('public/js/notes/recorder.js'); const recorder = createNotesRecorder({ noteFromVoice: async () => ({ success: true }), applyGeneratedNote() { applied++; } }); recorder.start(); await tick(); recorder.stop(false); a.c.AccountBoundary.end(); stopped.resolve(new a.c.Blob(['audio'])); await tick(); assert.equal(transcribed, 0); assert.equal(applied, 0); } finally { a.close(); } }); test('actual ED draft/idempotency/save callbacks stay owner-scoped; delayed persistence never adopts B', async () => { const a = await browser({ local: preferences }); let snapshot; try { a.owner('A'); a.w.document.getElementById('ed-tab').innerHTML = read('public/components/ed-encounter.html'); a.script('public/js/encounters.js'); a.module('public/js/ed-encounters.js'); const label = a.w.document.getElementById('ed-label'); label.value = 'A patient'; label.dispatchEvent(new a.c.Event('input', { bubbles: true })); a.runTimers(300); assert.match(a.w.localStorage.getItem('ped_ed_draft_v1:owner:A'), /A patient/); const delayed = deferred(); a.c.api = () => delayed.promise; a.w.document.getElementById('btn-ed-save').click(); label.value = 'late A edit'; label.dispatchEvent(new a.c.Event('input', { bubbles: true })); a.c.AccountBoundary.end(); a.runTimers(300); delayed.resolve(response({ success: true, id: 91 })); await tick(); assert.equal(a.c._savedEncId_ed, undefined); assert.equal(a.w.sessionStorage.getItem('_savedEncId_ed:owner:A'), null); assert.doesNotMatch(a.w.localStorage.getItem('ped_ed_draft_v1:owner:A'), /late A edit/); snapshot = a.snapshot(); } finally { a.close(); } const b = await browser(snapshot); try { b.owner('B'); b.w.document.getElementById('ed-tab').innerHTML = ''; b.script('public/js/encounters.js'); b.module('public/js/ed-encounters.js'); b.w.document.dispatchEvent(new b.c.CustomEvent('tabChanged', { detail: { tab: 'ed' } })); assert.equal(b.w.document.getElementById('ed-label').value, ''); b.w.document.getElementById('ed-label').value = 'B patient'; b.w.document.getElementById('btn-ed-save').click(); assert.ok(b.w.sessionStorage.getItem('_idempKey_ed:owner:B')); assert.notEqual(b.w.sessionStorage.getItem('_idempKey_ed:owner:B'), b.w.sessionStorage.getItem('_idempKey_ed:owner:A')); } finally { b.close(); } }); test('legacy ED, visit and idempotency storage is preserved but never adopted by B actual modules', async () => { const local = { ped_ed_draft_v1: JSON.stringify({ label: 'Legacy patient', state: { stages: [] } }), ped_visit_statuses: JSON.stringify({ 'newborn.newbornBlood': { status: 'Done', note: 'Legacy clinical note' } }) }; const session = { _savedEncId_ed: 'legacy-42', _idempKey_ed: 'legacy-key', ped_visit_age: 'Legacy age' }; const b = await browser({ local, session }); try { b.owner('B'); b.w.document.getElementById('ed-tab').innerHTML = ''; b.w.document.getElementById('wellvisit-tab').innerHTML = read('public/components/wellvisit.html'); b.script('public/js/encounters.js'); b.module('public/js/ed-encounters.js'); const schedule = b.module('public/js/wellVisit/scheduleData.js'); schedule.applyWellVisitScheduleGlobals(JSON.parse(read('public/data/well-visit/schedule.json')), b.c); b.module('public/js/wellVisit.js'); for (const tab of ['ed', 'wellvisit']) b.w.document.dispatchEvent(new b.c.CustomEvent('tabChanged', { detail: { tab } })); await tick(); assert.equal(b.w.document.getElementById('ed-label').value, ''); assert.equal(b.c._savedEncId_ed, undefined); assert.ok(![...b.w.document.querySelectorAll('.visit-note-input')].some(input => input.value === 'Legacy clinical note')); assert.notEqual(b.c._wellVisitAge, 'Legacy age'); b.w.document.getElementById('ed-label').value = 'B patient'; b.w.document.getElementById('btn-ed-save').click(); await tick(); const saved = JSON.parse(b.calls.find(call => call.url === '/api/encounters/saved' && call.init.method === 'POST').init.body); assert.notEqual(saved.idempotency_key, 'legacy-key'); assert.notEqual(b.w.sessionStorage.getItem('_idempKey_ed:owner:B'), 'legacy-key'); assert.ok(b.w.sessionStorage.getItem('_idempKey_ed:owner:B')); const note = b.w.document.querySelector('.visit-note-input'); assert.ok(note, 'Real well-visit schedule rendered'); note.value = 'B visit note'; note.dispatchEvent(new b.c.Event('input', { bubbles: true })); assert.match(b.w.localStorage.getItem('ped_visit_statuses:owner:B'), /B visit note/); assert.doesNotMatch(b.w.localStorage.getItem('ped_visit_statuses:owner:B'), /Legacy/); for (const [key, value] of Object.entries(local)) assert.equal(b.w.localStorage.getItem(key), value); for (const [key, value] of Object.entries(session)) assert.equal(b.w.sessionStorage.getItem(key), value); } finally { b.close(); } }); test('actual cookie-authenticated B callers never send a valid leftover A token', async () => { const b = await browser({ local: { ped_scribe_token: 'valid-token-A' } }); try { await b.auth('B'); b.c.SecureStorage.getSync = () => 'valid-token-A'; // Hostile leftover cache is not an identity. b.script('public/js/app.js'); b.c.showToast = b.c.showLoading = b.c.hideLoading = b.c.showBusy = b.c.hideBusy = () => {}; b.c.copyText = () => {}; fakeIndexedDB(b.c, new Map()); b.module('public/js/audioBackup.js'); b.module('public/js/documents.js'); b.module('public/js/learningHub.js'); b.calls.length = 0; // Synthetic server uses the production Bearer-first precedence. A remains valid. b.c.api = async (url, init) => { const authenticated = init.headers?.Authorization === 'Bearer valid-token-A' ? 'A' : 'B'; assert.equal(authenticated, 'B', url); assert.equal(init.headers?.Authorization, undefined, url); if (init.body instanceof b.c.FormData) assert.equal(init.headers?.['Content-Type'], undefined, url); if (url.endsWith('/audio')) return { ...response({}), blob: async () => new b.c.Blob(['B audio']) }; if (url === '/api/transcribe/status') return response({ available: true }); if (url === '/api/admin/learning/ai-generate') return response({ success: false }); return response({ success: true, id: 9, backups: [{ id: 9 }], text: 'B transcript', s3_configured: true, documents: [] }); }; await b.c.getAudioBackups(); await b.c.saveAudioBackup(new b.c.Blob(['B']), 'notes'); await b.c.retryAudioBackup('server_9'); await b.c.deleteAudioBackup('server_9'); b.c.checkTranscribeStatus(); await tick(); assert.equal(b.c._transcribeAvailable, true); await b.c.transcribeAudio(new b.c.Blob(['B'])); const controls = b.w.document.createElement('div'); controls.innerHTML = '
'; b.w.document.body.append(controls); Object.defineProperty(controls.querySelector('#doc-file-input'), 'files', { value: [new b.w.File(['B'], 'fixture.txt')] }); controls.querySelector('[data-action]').click(); controls.querySelector('#btn-doc-upload').click(); controls.querySelector('#btn-lh-ai-generate').click(); await tick(); const reached = b.calls.map(call => call.url); for (const url of ['/api/audio-backups', '/api/audio-backups/9/audio', '/api/audio-backups/9', '/api/transcribe/status', '/api/transcribe', '/api/logs/client-event', '/api/documents/upload', '/api/admin/learning/ai-generate']) assert.ok(reached.includes(url), url); // Also assert recorded headers: callers may intentionally catch transport rejection. for (const { url, init } of b.calls) { assert.equal(init.headers?.Authorization, undefined, url); if (init.body instanceof b.c.FormData) assert.equal(init.headers?.['Content-Type'], undefined, url); } assert.equal(b.w.localStorage.getItem('ped_scribe_token'), 'valid-token-A'); } finally { b.close(); } }); test('actual SecureStorage rejects failed native/local writes, caches only confirmed persistence, and never downgrades native writes', async () => { const b = await browser({ native: true }); try { const plugin = b.c.Capacitor.Plugins.SecureStoragePlugin; b.secure.set('ped_scribe_token', 'token-A'); await b.c.SecureStorage.hydrate(['ped_scribe_token']); plugin.set = async () => { throw new Error('native write denied'); }; await assert.rejects(b.c.SecureStorage.set('ped_scribe_token', 'token-B'), /native write denied/); assert.equal(b.c.SecureStorage.getSync('ped_scribe_token'), 'token-A'); assert.equal(b.secure.get('ped_scribe_token'), 'token-A'); delete b.c.Capacitor.Plugins.SecureStoragePlugin; for (const key of ['ped_scribe_token', 'ped_scribe_user', 'ped_session_id', 'ped_bio_creds']) { await assert.rejects(b.c.SecureStorage.set(key, 'secret'), /Secure storage unavailable/); assert.equal(b.w.localStorage.getItem(key), null); } } finally { b.close(); } const web = await browser(); try { Object.getPrototypeOf(web.w.localStorage).setItem = () => { throw new Error('local write denied'); }; await assert.rejects(web.c.SecureStorage.set('ped_scribe_token', 'token-B'), /local write denied/); assert.equal(web.c.memCache.ped_scribe_token, undefined); } finally { web.close(); } }); test('actual native auth cannot publish B or automatically reload after credential persistence rejection; sibling B is preserved', async () => { for (const sibling of [false, true]) { const b = await browser({ native: true }); try { b.secure.set('ped_scribe_token', 'token-A'); await b.auth('A'); const pending = deferred(); b.c.Capacitor.Plugins.SecureStoragePlugin.set = ({ key }) => key === 'ped_scribe_token' ? pending.promise : Promise.resolve(); b.login('B'); await tick(); if (sibling) { b.secure.set('ped_scribe_token', 'sibling-token-B'); // Persist the sibling state without delivering an event until the rejection is handled. b.w.localStorage.setItem(b.c.AccountBoundary.key, JSON.stringify({ owner: 'B', generation: 'sibling-B', signedOut: false })); } pending.reject(new Error('native write denied')); await tick(); assert.equal(b.reloads(), 0); assert.equal(b.c.AccountBoundary.blocked(), true); assert.notEqual(b.c.CURRENT_USER?.id, 'B'); if (sibling) { assert.equal(b.c.AccountBoundary.read().generation, 'sibling-B'); assert.equal(b.secure.get('ped_scribe_token'), 'sibling-token-B'); } else { assert.equal(b.secure.get('ped_scribe_token'), 'token-A'); assert.equal(b.c.SecureStorage.getSync('ped_scribe_token'), 'token-A'); assert.equal(b.c.AccountBoundary.read().signedOut, true); b.w.document.querySelector('#account-recovery button').click(); assert.equal(b.reloads(), 1); const fresh = await browser({ ...b.snapshot(), native: true }); try { fresh.secure.set('ped_scribe_token', 'token-A'); await fresh.auth('A'); assert.ok(!fresh.calls.some(call => call.url === '/api/auth/me')); assert.equal(fresh.c.AccountBoundary.active(), false); } finally { fresh.close(); } } } finally { b.close(); } } }); test('failed durable latch blocks every automatic/recovery reload until safe retry, including native cookie-401 with surviving A', async () => { for (const native of [false, true]) for (const mode of ['logout', 'sso', 'login', '401', 'unconfirmed-write']) { const b = await browser({ native }); try { b.secure.set('ped_scribe_token', 'token-A'); await b.auth('A'); const proto = Object.getPrototypeOf(b.w.localStorage), setItem = proto.setItem; let attempts = 0; proto.setItem = function(key, value) { if (key === b.c.AccountBoundary.key) { attempts++; if (mode === 'unconfirmed-write') return; throw new Error('durable latch denied'); } return setItem.call(this, key, value); }; b.c.Capacitor.Plugins.SecureStoragePlugin.remove = async () => { throw new Error('native removal denied'); }; b.c.api = async url => { if (url === '/api/auth/logout') throw new Error('logout unavailable'); return response({}, 401); // Cookie failure does not revoke a still-valid native token. }; if (mode === 'sso') await b.sso().done; else if (mode === 'login') { b.login('B'); await tick(); } else if (mode === '401') await assert.rejects(b.c.fetch('/api/notes'), { name: 'AbortError' }); else b.w.document.getElementById('btn-logout').click(); await tick(); b.runTimers(3000); assert.equal(b.c.AccountBoundary.read().owner, 'A', mode); assert.equal(b.c.AccountBoundary.blocked(), true, mode); assert.equal(b.reloads(), 0, mode); const recovery = b.w.document.querySelector('#account-recovery button'); recovery.click(); b.w.dispatchEvent(new b.w.PageTransitionEvent('pageshow', { persisted: true })); await b.c.AccountBoundary.cookieSessionRequest(); b.c.AccountBoundary.reload(); assert.equal(b.reloads(), 0, mode); assert.ok(attempts >= 3); assert.match(b.w.document.querySelector('#account-recovery p').textContent, /Cannot safely reload/); assert.equal(b.secure.get('ped_scribe_token'), 'token-A'); assert.equal(b.c.AccountBoundary.read().owner, 'A'); proto.setItem = setItem; recovery.click(); assert.equal(b.reloads(), 1, mode); assert.equal(b.c.AccountBoundary.read().signedOut, true); const fresh = await browser({ ...b.snapshot(), native }); try { fresh.secure.set('ped_scribe_token', 'token-A'); await fresh.auth('A'); assert.ok(!fresh.calls.some(call => call.url === '/api/auth/me'), mode); assert.equal(fresh.c.AccountBoundary.active(), false); } finally { fresh.close(); } } finally { b.close(); } } }); test('native SSO B never authenticates with late hydrated A credentials', async () => { const b = await browser({ native: true, url: 'https://offline.invalid/?sso=ok' }); try { const stale = deferred(); b.c.Capacitor.Plugins.SecureStoragePlugin.get = ({ key }) => key === 'ped_scribe_token' ? stale.promise : Promise.resolve({ value: null }); await b.auth('B'); assert.equal(b.c.AccountBoundary.capture(), 'B'); assert.equal(b.c.AUTH_TOKEN, ''); stale.resolve({ value: 'valid-token-A' }); await tick(); assert.equal(b.c.SecureStorage.getSync('ped_scribe_token'), 'valid-token-A'); assert.equal(b.c.getAuthHeaders().Authorization, undefined); } finally { b.close(); } }); function authForm(b, kind, id = 'A') { if (kind === 'biometric') { b.w.document.getElementById('btn-bio-login').click(); return; } const prefix = kind === 'register' ? 'reg' : 'login'; if (kind === 'register') b.w.document.getElementById('reg-name').value = id; b.w.document.getElementById(prefix + '-email').value = id + '@example.invalid'; b.w.document.getElementById(prefix + '-password').value = 'synthetic-password'; b.w.document.getElementById(kind + '-form').dispatchEvent(new b.c.Event('submit', { cancelable: true })); } function enableRegistration(b) { b.c.turnstile = { render(el, opts) { opts.callback('synthetic-challenge'); return 1; }, reset() {} }; b.c.onloadTurnstileCallback(); b.w.document.getElementById('show-register').click(); } const loginResponse = id => response({ success: true, token: 'token-' + id, sessionId: 'sid-' + id, user: { id, name: id, role: 'user' } }); test('auth admission holds login/register/biometric busy through retrieval, response and all native writes; overlap and SSO are rejected', async () => { for (const kind of ['login', 'register', 'biometric']) { const b = await browser({ native: true }); try { await b.auth(null); enableRegistration(b); const biometric = deferred(), network = deferred(), persistence = deferred(); let prompts = 0, hides = 0; b.c.hideLoading = () => { hides++; }; b.c.Capacitor.Plugins.BiometricAuthNative = { authenticate() { prompts++; return biometric.promise; }, checkBiometry: async () => ({ isAvailable: false }) }; b.secure.set('ped_bio_creds', JSON.stringify({ username: 'A@example.invalid', password: 'synthetic-password' })); const plugin = b.c.Capacitor.Plugins.SecureStoragePlugin; plugin.set = async ({ key, value }) => { await persistence.promise; b.secure.set(key, value); }; b.c.api = () => network.promise; b.calls.length = 0; const overlap = async () => { authForm(b, 'login', 'B'); authForm(b, 'register', 'B'); authForm(b, 'biometric'); const sso = b.sso(); assert.equal(sso.event.defaultPrevented, true); assert.equal(b.w.sessionStorage.getItem('ped_sso_intent'), null); assert.equal(b.c.AccountBoundary.blocked(), false, 'Pending authentication must reject SSO before logout/freeze'); await sso.done; assert.equal(b.w.document.getElementById('auth-screen').getAttribute('aria-busy'), 'true'); assert.equal(hides, 0, 'Loading must remain visible until persistence settles'); }; authForm(b, kind); if (kind === 'biometric') { await overlap(); assert.equal(b.calls.length, 0); assert.equal(prompts, 1); biometric.resolve(); await tick(); } await overlap(); assert.equal(b.calls.length, 1); network.resolve(loginResponse('A')); await tick(); await overlap(); assert.equal(b.calls.length, 1); assert.equal(b.c.CURRENT_USER, undefined); persistence.resolve(); await tick(); assert.equal(hides, 1); assert.equal(b.w.document.getElementById('auth-screen').hasAttribute('aria-busy'), false); assert.equal(b.c.AccountBoundary.capture(), 'A'); assert.equal(b.secure.get('ped_scribe_token'), 'token-A'); assert.equal(b.secure.get('ped_session_id'), 'sid-A'); } finally { b.close(); } } }); test('native auth admission waits for successful pending writes even when another credential write rejects', async () => { const b = await browser({ native: true }); try { await b.auth(null); const pending = deferred(); let hides = 0; b.c.hideLoading = () => { hides++; }; b.c.Capacitor.Plugins.SecureStoragePlugin.set = async ({ key, value }) => { if (key === 'ped_scribe_user') throw new Error('synthetic write rejection'); await pending.promise; b.secure.set(key, value); }; b.login('A'); await tick(); assert.equal(hides, 0); authForm(b, 'login', 'B'); await tick(); assert.equal(b.calls.filter(call => call.url === '/api/auth/login').length, 1); pending.resolve(); await tick(); assert.equal(hides, 1); assert.equal(b.c.AccountBoundary.signedOut(), true); assert.equal(b.c.AccountBoundary.blocked(), true); assert.equal(b.reloads(), 0); } finally { b.close(); } }); test('successful uncancellable native A write after sibling B never autoreloads or adopts A in fresh bootstrap/recovery', async () => { const a = await browser({ native: true }); try { await a.auth(null); const late = deferred(); a.c.Capacitor.Plugins.SecureStoragePlugin.set = async ({ key, value }) => { if (key === 'ped_scribe_token') await late.promise; a.secure.set(key, value); }; a.login('A'); await tick(); // A second actual auth realm completes B against the same synthetic native store. const sibling = await browser({ ...a.snapshot(), native: true }); let sharedB; try { sibling.c.Capacitor.Plugins.SecureStoragePlugin = { get: async ({ key }) => ({ value: a.secure.get(key) }), set: async ({ key, value }) => { a.secure.set(key, value); }, remove: async ({ key }) => { a.secure.delete(key); } }; await sibling.auth(null); sibling.login('B'); await tick(); assert.equal(sibling.c.AccountBoundary.capture(), 'B'); assert.equal(a.secure.get('ped_scribe_token'), 'token-B'); sharedB = sibling.c.AccountBoundary.read(); a.storageMessage(sharedB); } finally { sibling.close(); } late.resolve(); await tick(); assert.equal(a.secure.get('ped_scribe_token'), 'token-A', 'Native bridge completion is genuinely successful, not canceled by the test'); assert.equal(JSON.parse(a.secure.get('ped_scribe_user')).id, 'B', 'No stale cleanup may erase newer B'); assert.equal(a.secure.get('ped_session_id'), 'sid-B'); assert.equal(a.c.AccountBoundary.read().generation, sharedB.generation); assert.equal(a.c.AccountBoundary.blocked(), true); assert.equal(a.reloads(), 0, 'Late successful writes must not navigate into stale A'); assert.equal(a.c.getAuthHeaders().Authorization, undefined); const fresh = await browser({ ...a.snapshot(), native: true }); try { for (const [key, value] of a.secure) fresh.secure.set(key, value); let writes = 0; fresh.c.Capacitor.Plugins.SecureStoragePlugin.set = async () => { writes++; }; await fresh.auth('A'); // /me legitimately verifies persisted A, but shared owner is B. assert.equal(fresh.c.AccountBoundary.active(), false); assert.equal(fresh.c.CURRENT_USER, undefined); assert.equal(fresh.c.AccountBoundary.read().generation, sharedB.generation); assert.equal(writes, 0, 'Passive bootstrap must not persist unadmitted credentials'); assert.equal(fresh.reloads(), 0); const notice = fresh.w.document.getElementById('account-recovery'); assert.equal(notice.getAttribute('role'), 'alert'); const button = notice.querySelector('button'); assert.equal(button.textContent, 'Sign in again'); assert.equal(fresh.w.document.activeElement, button); button.click(); assert.equal(fresh.reloads(), 1); assert.equal(fresh.c.AccountBoundary.read().generation, sharedB.generation, 'Recovery does not sign B out'); const recovery = await browser({ ...fresh.snapshot(), native: true }); try { for (const [key, value] of a.secure) recovery.secure.set(key, value); await recovery.auth('A'); assert.equal(recovery.c.AccountBoundary.active(), false); assert.ok(!recovery.calls.some(call => call.url === '/api/auth/me')); assert.equal(recovery.w.document.getElementById('auth-screen').style.display, 'flex'); assert.equal(recovery.c.AccountBoundary.blocked(), false, 'Sign-in is available only in the new unauthenticated realm'); recovery.login('C'); await tick(); assert.equal(recovery.c.AccountBoundary.capture(), 'C'); assert.equal(recovery.secure.get('ped_scribe_token'), 'token-C'); assert.equal(recovery.c.AccountBoundary.needsSignIn(), false); } finally { recovery.close(); } } finally { fresh.close(); } // The stale realm's recovery also preserves B; its in-flight native call is not undone. a.w.document.querySelector('#account-recovery button').click(); assert.equal(a.reloads(), 1); assert.equal(a.c.AccountBoundary.read().generation, sharedB.generation); assert.equal(a.secure.get('ped_scribe_token'), 'token-A'); } finally { a.close(); } }); test('passive enter rejects another published owner; recovery navigation requires confirmed realm-local storage', async () => { for (const mode of ['throw', 'unconfirmed', 'read']) { const b = await browser(); try { b.storageMessage({ owner: 'B', generation: 'new-B', signedOut: false }); assert.equal(b.c.AccountBoundary.enter({ id: 'A' }), false); assert.equal(b.c.AccountBoundary.read().generation, 'new-B'); const proto = Object.getPrototypeOf(b.w.sessionStorage), setItem = proto.setItem, getItem = proto.getItem; proto.setItem = function(key, value) { if (key === 'ped_signin_required' && mode !== 'read') { if (mode === 'throw') throw new Error('denied'); return; } return setItem.call(this, key, value); }; proto.getItem = function(key) { if (key === 'ped_signin_required' && mode === 'read') throw new Error('readback denied'); return getItem.call(this, key); }; const recovery = b.w.document.querySelector('#account-recovery button'); recovery.click(); assert.equal(b.reloads(), 0); assert.match(b.w.document.querySelector('#account-recovery p').textContent, /Cannot safely reload/); proto.setItem = setItem; proto.getItem = getItem; recovery.click(); assert.equal(b.reloads(), 1); assert.equal(b.c.AccountBoundary.read().generation, 'new-B'); } finally { b.close(); } } }); function readAloud(b) { b.script('public/js/app.js'); b.c.userFeatures = { read_aloud: true }; const card = b.w.document.createElement('div'); card.className = 'card'; card.innerHTML = 'Synthetic A private note
Synthetic newer note
'; b.w.document.body.append(card); const audio = [], spoken = [], revoked = []; let urls = 0, cancels = 0; b.c.URL.createObjectURL = () => 'blob:synthetic-' + ++urls; b.c.URL.revokeObjectURL = url => revoked.push(url); b.c.Audio = class { constructor(url) { this.url = url; audio.push(this); } play() { this.playing = true; return b.c.playResult || Promise.resolve(); } pause() { this.playing = false; this.paused = true; } }; b.c.SpeechSynthesisUtterance = function(text) { this.text = text; }; b.c.speechSynthesis = { speak(utterance) { spoken.push(utterance); }, cancel() { cancels++; } }; const tts = () => ({ ...response({}), headers: { get: () => 'synthetic' }, blob: async () => new b.c.Blob(['synthetic audio']) }); return { audio, spoken, revoked, tts, cancels: () => cancels }; } test('actual TTS account abort and delayed blob cannot speak captured A via browser fallback', async () => { for (const stage of ['request', 'blob', 'abort-without-freeze']) { const b = await browser(); try { b.owner('A'); const speech = readAloud(b), late = deferred(); b.c.api = (url, init) => { if (stage === 'blob') return Promise.resolve({ ...speech.tts(), blob: () => late.promise }); if (stage === 'request') init.signal.addEventListener('abort', () => late.reject(new b.c.DOMException('account changed', 'AbortError'))); return late.promise; }; b.c.speakText('speech-a'); await tick(); if (stage === 'abort-without-freeze') late.reject(new b.c.DOMException('canceled', 'AbortError')); else b.c.AccountBoundary.freeze(); if (stage === 'blob') late.resolve(new b.c.Blob(['late A'])); await tick(); assert.equal(speech.spoken.length, 0, stage + ': abort is never a browser fallback'); assert.equal(speech.audio.length, 0, stage); } finally { b.close(); } } }); test('actual account freeze stops already-playing server audio and browser synthesis and revokes URLs', async () => { for (const mode of ['audio', 'synthesis']) { const b = await browser(); try { b.owner('A'); const speech = readAloud(b); b.c.api = async () => { if (mode === 'synthesis') throw new Error('synthetic TTS outage'); return speech.tts(); }; b.c.speakText('speech-a'); await tick(); if (mode === 'audio') assert.equal(speech.audio[0].playing, true); else assert.equal(speech.spoken[0].text, 'Synthetic A private note'); const before = speech.cancels(); b.c.AccountBoundary.freeze(); assert.ok(speech.cancels() > before, 'Freeze must cancel synthesis even if navigation is canceled'); assert.equal(b.c.currentlyReadingId, null); if (mode === 'audio') { assert.equal(speech.audio[0].paused, true); assert.deepEqual(speech.revoked, [speech.audio[0].url]); } } finally { b.close(); } } }); test('actual TTS playback generation fences stale fetch, blob, play rejection and ended callbacks without stopping newer audio', async () => { for (const stage of ['request', 'blob', 'play', 'ended']) { const b = await browser(); try { b.owner('A'); const speech = readAloud(b), late = deferred(); b.c.api = () => stage === 'request' ? late.promise : Promise.resolve({ ...speech.tts(), ...(stage === 'blob' ? { blob: () => late.promise } : {}) }); if (stage === 'play') b.c.playResult = late.promise; b.c.speakText('speech-a'); await tick(); const ended = speech.audio[0]?.onended; b.c.playResult = undefined; b.c.api = async () => speech.tts(); b.c.speakText('speech-next'); await tick(); const latest = speech.audio.at(-1); assert.equal(latest.playing, true); const before = speech.cancels(); if (stage === 'request') late.resolve(speech.tts()); if (stage === 'blob') late.resolve(new b.c.Blob(['late A'])); if (stage === 'play') late.reject(new Error('late play rejection')); if (stage === 'ended') ended(); await tick(); assert.equal(speech.audio.at(-1), latest); assert.equal(latest.playing, true); assert.equal(speech.cancels(), before); assert.equal(speech.spoken.length, 0); assert.equal(b.c.currentlyReadingId, 'speech-next'); b.c.stopReading(); assert.ok(speech.revoked.includes(latest.url)); } finally { b.close(); } } }); test('fresh explicit sign-in recovery permits SSO only after the existing cookie-only 401 preflight', async () => { const b = await browser({ native: true, session: { ped_signin_required: '1' }, local: { ped_account_boundary_v1: JSON.stringify({ owner: 'B', generation: 'new-B', signedOut: false }) } }); try { b.secure.set('ped_scribe_token', 'token-A'); await b.auth('A'); assert.ok(!b.calls.some(call => call.url === '/api/auth/me')); b.calls.length = 0; b.c.api = async url => response({}, url === '/api/auth/me' ? 401 : 200); await b.sso().done; assert.deepEqual(b.calls.map(call => call.url), ['/api/auth/logout', '/api/auth/me']); assert.equal(b.calls[1].init.headers, undefined); assert.equal(b.c.AccountBoundary.needsSignIn(), false); const callback = await browser({ ...b.snapshot(), native: true, url: 'https://offline.invalid/?sso=ok' }); try { await callback.auth('C'); assert.equal(callback.c.AccountBoundary.capture(), 'C'); assert.equal(callback.c.getAuthHeaders().Authorization, undefined); } finally { callback.close(); } } finally { b.close(); } }); test('biometric retrieval cannot start a new login after its captured shared generation changes', async () => { const b = await browser({ native: true }); try { await b.auth(null); const retrieval = deferred(); b.c.Capacitor.Plugins.BiometricAuthNative = { authenticate: () => retrieval.promise }; b.secure.set('ped_bio_creds', JSON.stringify({ username: 'A@example.invalid', password: 'synthetic-password' })); authForm(b, 'biometric'); b.storageMessage({ owner: 'B', generation: 'sibling-B', signedOut: false }); retrieval.resolve(); await tick(); assert.ok(!b.calls.some(call => call.url === '/api/auth/login')); assert.equal(b.c.AccountBoundary.read().generation, 'sibling-B'); assert.equal(b.c.AccountBoundary.blocked(), true); assert.equal(b.reloads(), 0); } finally { b.close(); } }); test('TTS 401/403/503 and feature revocation never fall back to browser speech', async () => { for (const mode of [401, 403, 503, 'revoked']) { const b = await browser(); try { b.owner('A'); const speech = readAloud(b), late = deferred(); b.c.api = () => late.promise; b.c.speakText('speech-a'); if (mode === 'revoked') b.c.userFeatures.read_aloud = false; late.resolve(response({}, mode === 'revoked' ? 500 : mode)); await tick(); assert.equal(speech.spoken.length, 0); assert.equal(speech.audio.length, 0); } finally { b.close(); } } }); test('late passive bootstrap cannot reshow sign-in or overwrite a completed explicit login', async () => { const b = await browser(); try { const bootstrap = deferred(); b.c.api = async url => url === '/api/auth/me' ? bootstrap.promise : response({}); b.script('public/js/auth.js'); b.w.document.dispatchEvent(new b.c.Event('DOMContentLoaded')); b.login('B'); await tick(); assert.equal(b.c.AccountBoundary.capture(), 'B'); bootstrap.resolve(response({ user: { id: 'A' } })); await tick(); assert.equal(b.c.AccountBoundary.capture(), 'B'); assert.equal(b.w.document.getElementById('auth-screen').style.display, 'none'); assert.equal(b.reloads(), 0); } finally { b.close(); } }); test('a transient passive owner read failure cannot be mistaken for absent ownership and republish A', async () => { const b = await browser(); try { b.storageMessage({ owner: 'B', generation: 'new-B', signedOut: false }); const proto = Object.getPrototypeOf(b.w.localStorage), getItem = proto.getItem; let reads = 0; proto.getItem = function(key) { if (key === b.c.AccountBoundary.key && ++reads === 2) throw new Error('transient read failure'); return getItem.call(this, key); }; assert.equal(b.c.AccountBoundary.enter({ id: 'A' }), false); assert.equal(b.c.AccountBoundary.read().owner, 'B'); assert.equal(b.c.AccountBoundary.blocked(), true); assert.equal(b.reloads(), 0); } finally { b.close(); } });