diff --git a/frontend/private-device-data.js b/frontend/private-device-data.js index 6811723..55e0d74 100644 --- a/frontend/private-device-data.js +++ b/frontend/private-device-data.js @@ -54,6 +54,7 @@ await stopWorkerOutbox(); await deletePrivateDatabase('stackchain-background-outbox-v1'); await deletePrivateDatabase('stackchain-offline-work-v2'); + await deletePrivateDatabase('stackchain-unfiled-captures-v1'); 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 b7efc84..f85ac11 100644 --- a/frontend/service-worker.js +++ b/frontend/service-worker.js @@ -117,8 +117,21 @@ async function withSessionCsrf(work) { } } +async function deletePrivateDatabase(name) { + if (!indexedDB) return; + await new Promise((resolve, reject) => { + let request; + try { request = indexedDB.deleteDatabase(name); } + catch (error) { reject(error); return; } + request.onsuccess = () => resolve(); + request.onerror = () => reject(request.error || new Error('IndexedDB deletion failed.')); + request.onblocked = () => reject(new Error('IndexedDB deletion was blocked.')); + }); +} + async function purgeRevokedSessionData() { await issueSync.purge(); + await deletePrivateDatabase('stackchain-unfiled-captures-v1'); const keys = await caches.keys(); await Promise.all( keys.filter(key => key.startsWith('stackchain-dashboard-')).map(key => caches.delete(key)) @@ -155,6 +168,7 @@ async function offlineLeaseState(cache) { async function expiredOfflineResponse() { await issueSync.purge(); + await deletePrivateDatabase('stackchain-unfiled-captures-v1'); const keys = await caches.keys(); await Promise.all( keys.filter(key => key.startsWith('stackchain-dashboard-')).map(key => caches.delete(key)) @@ -269,6 +283,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'); 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 d16452f..c4b5727 100644 --- a/frontend/session.js +++ b/frontend/session.js @@ -456,6 +456,7 @@ await stopWorkerOutbox(); await deletePrivateDatabase('stackchain-background-outbox-v1'); await deletePrivateDatabase('stackchain-offline-work-v2'); + await deletePrivateDatabase('stackchain-unfiled-captures-v1'); } catch (_error) { const error = new Error('Could not clear private queued work from this device.'); onClearError(error); diff --git a/tests/test_dashboard_session_frontend.py b/tests/test_dashboard_session_frontend.py index 29d832b..7965d19 100644 --- a/tests/test_dashboard_session_frontend.py +++ b/tests/test_dashboard_session_frontend.py @@ -348,7 +348,8 @@ process.stdout.write(JSON.stringify(state)); assert result["deletedAtIdle"] == [] assert result["deletedDatabases"] == [ - "stackchain-background-outbox-v1", "stackchain-offline-work-v2" + "stackchain-background-outbox-v1", "stackchain-offline-work-v2", + "stackchain-unfiled-captures-v1", ] assert result["remaining"] == ["gitea.preference"] assert result["replaced"] == [ @@ -373,7 +374,8 @@ process.stdout.write(JSON.stringify(state)); assert result["valid"] is False assert result["remaining"] == ["gitea.preference"] assert result["deletedDatabases"] == [ - "stackchain-background-outbox-v1", "stackchain-offline-work-v2" + "stackchain-background-outbox-v1", "stackchain-offline-work-v2", + "stackchain-unfiled-captures-v1", ] assert result["deletedCaches"] == ["stackchain-dashboard-shell-v15"] assert result["replaced"] == ["/dashboard/login?reason=session-expired"] @@ -503,7 +505,8 @@ process.stdout.write(JSON.stringify(state)); ] assert result["remaining"] == ["gitea.preference"] assert result["deletedDatabases"] == [ - "stackchain-background-outbox-v1", "stackchain-offline-work-v2" + "stackchain-background-outbox-v1", "stackchain-offline-work-v2", + "stackchain-unfiled-captures-v1", ] assert result["deletedCaches"] == ["stackchain-dashboard-shell-v15"] assert result["workerMessages"] == [{"type": "stackchain-purge-outbox"}] @@ -525,7 +528,8 @@ process.stdout.write(JSON.stringify(state)); assert result["remaining"] == ["gitea.preference"] assert result["deletedDatabases"] == [ - "stackchain-background-outbox-v1", "stackchain-offline-work-v2" + "stackchain-background-outbox-v1", "stackchain-offline-work-v2", + "stackchain-unfiled-captures-v1", ] assert result["deletedCaches"] == ["stackchain-dashboard-shell-v15"] assert result["workerMessages"] == [{"type": "stackchain-purge-outbox"}] @@ -620,7 +624,8 @@ process.stdout.write(JSON.stringify(state)); assert "stackchain.private" in result["removed"] assert result["remaining"] == ["gitea.preference"] assert result["deletedDatabases"] == [ - "stackchain-background-outbox-v1", "stackchain-offline-work-v2" + "stackchain-background-outbox-v1", "stackchain-offline-work-v2", + "stackchain-unfiled-captures-v1", ] assert result["deletedCaches"] == ["stackchain-dashboard-shell-v15"] assert result["assigned"] == "/dashboard/login" diff --git a/tests/test_private_device_data.py b/tests/test_private_device_data.py index 43d8c0f..da53942 100644 --- a/tests/test_private_device_data.py +++ b/tests/test_private_device_data.py @@ -46,6 +46,7 @@ const clear = createPrivateDeviceDataPurger({{ assert state["databases"] == [ "stackchain-background-outbox-v1", "stackchain-offline-work-v2", + "stackchain-unfiled-captures-v1", ] assert state["caches"] == ["stackchain-dashboard-shell-v37"] assert state["workerMessages"] == [{"type": "stackchain-purge-outbox"}] diff --git a/tests/test_service_worker.py b/tests/test_service_worker.py index f049c0a..7ae4df5 100644 --- a/tests/test_service_worker.py +++ b/tests/test_service_worker.py @@ -13,7 +13,7 @@ def run_worker_scenario(scenario: str) -> dict: const fs = require('fs'); const vm = require('vm'); const listeners = {{}}; -const state = {{ added: [], deleted: [], claimed: false, skipped: false, fetches: [], puts: [], backgroundFlushes: 0, backgroundResumes: 0, outboxPurges: 0, outboxLifecycle: [], notifications: [], focused: [], opened: [], failFetch: false, stallFetch: false, lateFetch: false, fetchAborted: false, fetchStatus: 200, fetchRedirected: false, cachedBody: null }}; +const state = {{ added: [], deleted: [], deletedDatabases: [], claimed: false, skipped: false, fetches: [], puts: [], backgroundFlushes: 0, backgroundResumes: 0, outboxPurges: 0, outboxLifecycle: [], notifications: [], focused: [], opened: [], failFetch: false, stallFetch: false, lateFetch: false, fetchAborted: false, fetchStatus: 200, fetchRedirected: false, cachedBody: null }}; const storedResponses = new Map(); storedResponses.set( 'https://forge.example/dashboard/__offline-session-lease', @@ -58,6 +58,12 @@ const context = {{ }}, }}, importScripts: () => {{}}, + indexedDB: {{deleteDatabase:name=>{{ + state.deletedDatabases.push(name); + const request={{}}; + queueMicrotask(()=>request.onsuccess?.()); + return request; + }}}}, caches: {{ open: async () => cache, keys: async () => ['stackchain-dashboard-old', 'another-app-cache'], @@ -244,6 +250,9 @@ def test_revoked_background_session_purges_worker_data_and_notifies_dashboard_cl assert result["outcome"] == {"status": 401, "code": "session_revoked"} assert result["state"]["outboxPurges"] == 1 + assert result["state"]["deletedDatabases"] == [ + "stackchain-unfiled-captures-v1" + ] assert result["state"]["deleted"] == ["stackchain-dashboard-old"] assert result["state"]["clientMessages"] == [ {"type": "stackchain-session-revoked"} @@ -331,6 +340,9 @@ def test_device_purge_message_stops_worker_outbox_and_acknowledges_completion(): ) assert result["state"]["outboxPurges"] == 1 + assert result["state"]["deletedDatabases"] == [ + "stackchain-unfiled-captures-v1" + ] assert result["replies"] == [{"ok": True}] @@ -773,6 +785,9 @@ def test_expired_offline_lease_purges_private_worker_data_and_refuses_cached_she assert result["status"] == 401 assert result["body"] == "Your Stackchain session expired. Reconnect and sign in." assert result["state"]["outboxPurges"] == 1 + assert result["state"]["deletedDatabases"] == [ + "stackchain-unfiled-captures-v1" + ] assert result["state"]["deleted"] == ["stackchain-dashboard-old"] assert "private cached dashboard" not in result["body"]