// One verified owner per document. Never unlock: a new account needs a new JS realm. (function() { var KEY = 'ped_account_boundary_v1'; var owner = null, generation = null, locked = false, revision = 0; var controller = new AbortController(); var channel = null; var storageFailed = false; var signInRequired = false; var SIGN_IN_KEY = 'ped_signin_required'; function needsSignIn() { try { return sessionStorage.getItem(SIGN_IN_KEY) === '1'; } catch (e) { return true; } } function recoverSignIn() { signInRequired = true; freeze(); document.querySelector('#account-recovery p').textContent = 'Your saved credentials no longer match this session. Sign in again in a fresh page.'; document.querySelector('#account-recovery button').textContent = 'Sign in again'; } function read() { try { return JSON.parse(localStorage.getItem(KEY) || 'null'); } catch (e) { storageFailed = true; return null; } } function publish(next) { try { var value = JSON.stringify(next); localStorage.setItem(KEY, value); if (localStorage.getItem(KEY) !== value) throw new Error('Account isolation write not confirmed'); storageFailed = false; } catch (e) { storageFailed = true; freeze(); throw new Error('Account isolation storage unavailable'); } try { if (channel) channel.postMessage(next); } catch (e) {} return next; } function fresh(nextOwner) { return { owner: nextOwner, generation: crypto.randomUUID(), signedOut: !nextOwner }; } function abortError() { return new DOMException('Account changed; reload required', 'AbortError'); } function reload() { // A failed publication is only realm-local until a durable signed-out latch // exists. Never discard that failure by navigating back to surviving auth. if (storageFailed) { freeze(); try { publish(fresh(null)); } catch (e) { document.querySelector('#account-recovery p').textContent = 'Cannot safely reload: session storage is unavailable. Restore storage access, then retry Reload.'; return; } } if (signInRequired) { // Realm-local recovery must not erase a newer sibling's owner/credentials. try { sessionStorage.setItem(SIGN_IN_KEY, '1'); if (sessionStorage.getItem(SIGN_IN_KEY) !== '1') throw new Error('Sign-in recovery write not confirmed'); } catch (e) { document.querySelector('#account-recovery p').textContent = 'Cannot safely reload: session storage is unavailable. Restore storage access, then retry Sign in again.'; return; } } try { window.location.reload(); } catch (e) { /* Recovery stays available. */ } } function freeze() { if (locked) return; locked = true; revision++; controller.abort(); // CSS also hides late modal/async DOM insertions outside main-app. document.documentElement.classList.add('account-transition'); Array.from(document.body.children).forEach(function(el) { el.inert = true; }); var notice = document.createElement('section'); notice.id = 'account-recovery'; notice.setAttribute('role', 'alert'); var text = document.createElement('p'); text.textContent = 'Your account session changed. Reload to continue safely.'; var button = document.createElement('button'); button.textContent = 'Reload'; button.addEventListener('click', reload); notice.append(text, button); document.body.appendChild(notice); button.focus(); window.dispatchEvent(new Event('account-boundary')); } function current() { if (locked) return false; var shared = read(); if (owner && (!shared || shared.signedOut || shared.owner !== owner || shared.generation !== generation)) { freeze(); reload(); } return !locked && !storageFailed && !!owner; } function capture() { return current() ? owner : null; } function valid(ticket) { return !!ticket && ticket === owner && current(); } function receive(message) { var latest = read(); // Ignore queued events from A after B has already published its session. if (!message || !latest || message.generation !== latest.generation) return; if (owner && (message.signedOut || message.owner !== owner || message.generation !== generation)) { freeze(); reload(); // Sibling events must never delete another tab's newly persisted native credentials. } } try { channel = new BroadcastChannel('pedscribe-auth'); channel.onmessage = function(e) { receive(e.data); }; } catch (e) {} window.addEventListener('storage', function(e) { if (e.key !== KEY) return; if (!e.newValue) { if (owner) { freeze(); reload(); } return; } try { receive(JSON.parse(e.newValue)); } catch (err) {} }); window.addEventListener('pageshow', function(e) { if (e.persisted) { freeze(); reload(); } else if (owner) current(); }); // Hide before a BFCache snapshot is taken, not merely after it is restored. window.addEventListener('pagehide', freeze); // Preserve unowned legacy clinical data, but never read or adopt it. // Clinical callers use storageKey() only after a verified owner enters. window.AccountBoundary = { key: KEY, read: read, freeze: freeze, reload: reload, capture: capture, valid: valid, recoverSignIn: recoverSignIn, needsSignIn: needsSignIn, completeSignIn: function() { sessionStorage.removeItem(SIGN_IN_KEY); }, active: current, blocked: function() { return locked; }, error: abortError, revision: function() { return revision; }, signal: function() { return controller.signal; }, signedOut: function() { var state = read(); return storageFailed || !!(state && state.signedOut); }, startLogin: function() { if (locked) throw abortError(); if (owner) freeze(); publish(fresh(null)); // Freeze siblings before a login response can replace the shared cookie. return revision; }, end: function() { freeze(); var published = false; try { publish(fresh(null)); published = true; } catch (e) { /* reload() retries the durable latch. */ } window.AUTH_TOKEN = null; window.CURRENT_USER = null; return published; }, enter: function(user, explicit) { if (!user || user.id == null || storageFailed) { freeze(); return false; } var nextOwner = String(user.id); if (locked || (owner && owner !== nextOwner)) { freeze(); return false; } if (!explicit && (this.signedOut() || needsSignIn())) return false; var state = read(); if (storageFailed) { freeze(); return false; } if (!explicit && state && state.owner !== nextOwner) { recoverSignIn(); return false; } if (explicit || !state) state = publish(fresh(nextOwner)); owner = nextOwner; generation = state.generation; window.dispatchEvent(new Event('account-ready')); return true; }, // Successful replacement login persists credentials first, but cannot enter this document. publishLogin: function(user) { publish(fresh(String(user.id))); }, storageKey: function(key) { if (!current()) throw abortError(); return key + ':owner:' + encodeURIComponent(owner); } }; // All actual recorder callers share these platform boundaries, including // Notes, ED, the assistant and recorders constructed during pause/resume. var recorders = new Set(), streams = new Set(), recognizers = new Set(); if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) { var getUserMedia = navigator.mediaDevices.getUserMedia.bind(navigator.mediaDevices); navigator.mediaDevices.getUserMedia = function(constraints) { var ticket = capture(); if (!ticket) return Promise.reject(abortError()); return getUserMedia(constraints).then(function(stream) { if (!valid(ticket)) { stream.getTracks().forEach(function(t) { t.stop(); }); throw abortError(); } streams.add(stream); return stream; }); }; } if (window.MediaRecorder) { var startRecording = MediaRecorder.prototype.start; MediaRecorder.prototype.start = function() { if (!current()) throw abortError(); recorders.add(this); var recorder = this; this.addEventListener('stop', function() { recorders.delete(recorder); streams.delete(recorder.stream); }, { once: true }); ['stop', 'dataavailable'].forEach(function(type) { recorder.addEventListener(type, function(e) { if (!current()) e.stopImmediatePropagation(); }, { capture: true }); }); return startRecording.apply(this, arguments); }; } [window.SpeechRecognition, window.webkitSpeechRecognition].filter(function(value, i, all) { return value && all.indexOf(value) === i; }).forEach(function(Recognition) { var start = Recognition.prototype.start; Recognition.prototype.start = function() { if (!current()) throw abortError(); recognizers.add(this); return start.apply(this, arguments); }; }); window.addEventListener('account-boundary', function() { recognizers.forEach(function(rec) { rec.onresult = rec.onend = rec.onerror = null; try { rec.abort(); } catch (e) {} }); recorders.forEach(function(rec) { rec.ondataavailable = rec.onstop = rec.onerror = null; try { if (rec.state !== 'inactive') rec.stop(); } catch (e) {} }); streams.forEach(function(stream) { stream.getTracks().forEach(function(track) { track.stop(); }); }); recorders.clear(); streams.clear(); recognizers.clear(); if (window.nativeStopRecordingService) window.nativeStopRecordingService(); if (window.nativeKeepAwake) window.nativeKeepAwake(false); }); })();