Makes the offline-first vault installable as a standalone PWA, and enforces
"no network access aside from the app's own resources" once installed.
PWA:
- public/manifest.webmanifest: scope ./ (confined to the app's own dir), three
icons (192/512 + transparent maskable), standalone display, brand theme. No
push/shortcuts (deliberate: offline-first vault, nothing to push).
- public/sw.js: a NETWORK SANDBOX service worker. Allows only same-origin
requests inside the worker's own directory (index, manifest, icons); returns
403 for every cross-origin request and for same-origin paths outside the app
dir. This blocks in-app <img>/<script>/fetch exfiltration at the source.
- src/lib/pwa.js + main.js: register the worker (relative path, works at / in
dev and under /password_manager/ in prod).
- index.html: dev CSP (permits Vite HMR WebSocket) + PWA meta/link tags.
Security:
- scripts/inline-assets.js swaps the dev CSP for the STRICT production CSP in
the shipped dist: connect-src 'none' (no fetch/XHR/WebSockets anywhere),
form-action 'none', object-src/base-uri 'none', img/font/media 'self' data:.
The browser-native connect-src closes the WebSocket gap the SW cannot see.
- Known boundary documented in-file: frame-ancestors / X-Frame-Options is
HTTP-header-only (ignored in meta) and not set by the static host, so
clickjacking is permissive; all exfiltration channels are closed regardless.
Build/deploy:
- inline-assets.js now PRESERVES manifest/sw.js/icons in dist (was deleting).
- deploy.yml uploads + byte-verifies index.html, manifest, sw.js, and all three
icons to WebDAV.
Tests:
- tests/lib/sw-policy.test.js: 5 tests for the sandbox allow/deny logic
(in-scope allowed; cross-origin, out-of-scope, scheme/port mismatch denied).
- Verified in headless Chromium against the built dist: SW registers + is
active, and both a cross-origin fetch and a same-origin-out-of-scope fetch
are refused by the CSP before leaving the device.
Fixup: dropped 'frame-ancestors' from the meta CSP after confirming the browser
ignores it there (it is a header-only directive).
- .gitea/workflows/deploy.yml: test+build on every push; auto-deploy
dist/index.html to /password_manager via WebDAV on push to main (DELETE-
then-PUT to bypass stale-file cache), verifying deployed bytes match.
- Build injects __VAULT_COMMIT__ (from VITE_COMMIT_HASH=github.sha in CI,
git HEAD locally) and main.js logs console.info({ commit_hash }) on
startup so a deploy is verifiable against its source commit.