Operator sign in
Use an enrolled passkey, or enter the dashboard access token for bootstrap and recovery. The token is exchanged for a private, short-lived session and is never stored on this device.
diff --git a/frontend/index.html b/frontend/index.html index 7c4568b..db24aca 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1243,6 +1243,7 @@
+ diff --git a/frontend/private-data-registry.js b/frontend/private-data-registry.js new file mode 100644 index 0000000..3ca2b84 --- /dev/null +++ b/frontend/private-data-registry.js @@ -0,0 +1,9 @@ +(function (root) { + const databases = Object.freeze([ + 'stackchain-background-outbox-v1', + 'stackchain-offline-work-v2', + 'stackchain-unfiled-captures-v1', + ]); + if (typeof module !== 'undefined' && module.exports) module.exports = databases; + else root.stackchainPrivateDatabases = databases; +})(typeof globalThis !== 'undefined' ? globalThis : this); diff --git a/frontend/private-device-data.js b/frontend/private-device-data.js index 55e0d74..8fe4b94 100644 --- a/frontend/private-device-data.js +++ b/frontend/private-device-data.js @@ -1,5 +1,10 @@ (function (root, factory) { - if (typeof module !== 'undefined' && module.exports) module.exports = factory; + if (typeof module !== 'undefined' && module.exports) { + module.exports = options => factory({ + ...options, + privateDatabases: require('./private-data-registry.js'), + }); + } else root.stackchainPrivateDeviceData = factory({ localStorage: root.localStorage, sessionStorage: root.sessionStorage, @@ -7,9 +12,10 @@ caches: root.caches, serviceWorker: root.navigator?.serviceWorker, MessageChannel: root.MessageChannel, + privateDatabases: root.stackchainPrivateDatabases, }); })(typeof window !== 'undefined' ? window : this, function createPrivateDeviceDataPurger({ - localStorage, sessionStorage, indexedDB, caches, serviceWorker, MessageChannel, + localStorage, sessionStorage, indexedDB, caches, serviceWorker, MessageChannel, privateDatabases, }) { function removeOwnedStorage(storage) { if (!storage) return; @@ -52,9 +58,7 @@ removeOwnedStorage(localStorage); if (sessionStorage !== localStorage) removeOwnedStorage(sessionStorage); await stopWorkerOutbox(); - await deletePrivateDatabase('stackchain-background-outbox-v1'); - await deletePrivateDatabase('stackchain-offline-work-v2'); - await deletePrivateDatabase('stackchain-unfiled-captures-v1'); + for (const name of privateDatabases) await deletePrivateDatabase(name); const keys = await caches?.keys?.() || []; await Promise.all( keys.filter(key => key.startsWith('stackchain-dashboard-')).map(key => caches.delete(key)) diff --git a/frontend/service-worker.js b/frontend/service-worker.js index 6cc2a6e..cdb698c 100644 --- a/frontend/service-worker.js +++ b/frontend/service-worker.js @@ -1,10 +1,12 @@ const BASE = new URL('./', self.location.href).pathname; +importScripts(BASE + 'static/private-data-registry.js'); importScripts(BASE + 'static/background-issue-sync.js'); const CACHE = 'stackchain-dashboard-shell-v102'; const OFFLINE_LEASE_URL = new URL(BASE + '__offline-session-lease', self.location.origin).href; const OUTAGE_STATUSES = new Set([500, 502, 503, 504]); const NAVIGATION_TIMEOUT_MS = self.__STACKCHAIN_NAVIGATION_TIMEOUT_MS || 4000; const PUSH_ACTION_TIMEOUT_MS = self.__STACKCHAIN_PUSH_ACTION_TIMEOUT_MS || 8000; +const PRIVATE_DATABASES = self.stackchainPrivateDatabases; const SHELL = [ BASE, BASE + 'manifest.webmanifest', @@ -210,9 +212,13 @@ async function deletePrivateDatabase(name) { }); } +async function deletePrivateDatabases() { + for (const name of PRIVATE_DATABASES) await deletePrivateDatabase(name); +} + async function purgeRevokedSessionData() { await issueSync.purge(); - await deletePrivateDatabase('stackchain-unfiled-captures-v1'); + await deletePrivateDatabases(); const keys = await caches.keys(); await Promise.all( keys.filter(key => key.startsWith('stackchain-dashboard-')).map(key => caches.delete(key)) @@ -249,7 +255,7 @@ async function offlineLeaseState(cache) { async function expiredOfflineResponse() { await issueSync.purge(); - await deletePrivateDatabase('stackchain-unfiled-captures-v1'); + await deletePrivateDatabases(); const keys = await caches.keys(); await Promise.all( keys.filter(key => key.startsWith('stackchain-dashboard-')).map(key => caches.delete(key)) @@ -370,7 +376,7 @@ self.addEventListener('message', event => { if (event.data?.type === 'stackchain-purge-outbox') event.waitUntil((async () => { try { await issueSync.purge(); - await deletePrivateDatabase('stackchain-unfiled-captures-v1'); + await deletePrivateDatabases(); event.ports?.[0]?.postMessage({ ok: true }); } catch (error) { event.ports?.[0]?.postMessage({ ok: false, error: String(error?.message || 'Outbox purge failed.') }); diff --git a/frontend/session.js b/frontend/session.js index c4b5727..debc694 100644 --- a/frontend/session.js +++ b/frontend/session.js @@ -1,5 +1,10 @@ (function (root, factory) { - if (typeof module !== 'undefined' && module.exports) module.exports = factory; + if (typeof module !== 'undefined' && module.exports) { + module.exports = options => factory({ + ...options, + privateDatabases: require('./private-data-registry.js'), + }); + } else { const base = new URL('./', root.location.href).pathname; const originalFetch = root.fetch.bind(root); @@ -15,6 +20,7 @@ serviceWorker: root.navigator?.serviceWorker, credentials: root.navigator?.credentials, MessageChannel: root.MessageChannel, + privateDatabases: root.stackchainPrivateDatabases, location: root.location, addActivityListener: (type, listener, options) => root.addEventListener(type, listener, options), confirmAction: message => root.confirm(message), @@ -82,7 +88,7 @@ root.stackchainSession = boundary; } })(typeof window !== 'undefined' ? window : this, function createSessionBoundary({ - cookie, origin, base, fetchImpl, localStorage, sessionStorage, indexedDB, caches, serviceWorker, credentials, MessageChannel, location, confirmAction, addActivityListener, + cookie, origin, base, fetchImpl, localStorage, sessionStorage, indexedDB, caches, serviceWorker, credentials, MessageChannel, privateDatabases, location, confirmAction, addActivityListener, promptAuthorization = () => null, onExpired = () => {}, onClearError = () => {}, @@ -454,9 +460,7 @@ if (sessionStorage !== localStorage) removeDashboardStorage(sessionStorage); try { await stopWorkerOutbox(); - await deletePrivateDatabase('stackchain-background-outbox-v1'); - await deletePrivateDatabase('stackchain-offline-work-v2'); - await deletePrivateDatabase('stackchain-unfiled-captures-v1'); + for (const name of privateDatabases) await deletePrivateDatabase(name); } catch (_error) { const error = new Error('Could not clear private queued work from this device.'); onClearError(error); diff --git a/src/views.py b/src/views.py index 3a62b00..385e09e 100644 --- a/src/views.py +++ b/src/views.py @@ -17,7 +17,7 @@ LOGIN_HTML = """Use an enrolled passkey, or enter the dashboard access token for bootstrap and recovery. The token is exchanged for a private, short-lived session and is never stored on this device.