Skip to content

feat(pwa): manifest + service worker backend — #86 (backend half)#200

Merged
MartinCastroAlvarez merged 1 commit into
mainfrom
feat/pwa-86-backend
May 26, 2026
Merged

feat(pwa): manifest + service worker backend — #86 (backend half)#200
MartinCastroAlvarez merged 1 commit into
mainfrom
feat/pwa-86-backend

Conversation

@MartinCastroAlvarez
Copy link
Copy Markdown
Owner

Serving layer for the installable PWA (#86), built as new files (low collision with the frontend churn). Security lane owns it: anonymous manifest with no per-user data, SW served with Service-Worker-Allowed: <mount> (scope ≤ mount), hand-rolled SW (no Workbox) honoring no-store + mutation-safety + cache-on-logout (dar:purge). 8 tests pass. Frontend SW registration + install affordance are the follow-up (browser-verified). Tier 5.

…alf)

Serving layer for the installable PWA (Issue #86), built as new files
to avoid colliding with the active frontend churn. Security lane owns
this surface because its load-bearing properties are security ones.

- django_admin_react/pwa.py — ManifestView (`<mount>/web.manifest`) +
  ServiceWorkerView (`<mount>/sw.js`).
  * Manifest: anonymous (install prompt is pre-login), request-time
    computed, NO per-user data — only mount-/header-derived + static
    fields; theme colours from `Sec-CH-Prefers-Color-Scheme` with a
    `Vary` header.
  * SW: served with `Service-Worker-Allowed: <mount>` (scope = mount,
    never sibling Django views) + `Cache-Control: no-cache` (revalidate
    so a deploy ships a new SW; not no-store, which would break SW
    update checks).
- templates/admin_react/sw.js — hand-rolled SW (no Workbox / no new npm
  dep) honoring the contract: scope ≤ mount (pass-through otherwise),
  never cache non-GET (mutation safety), never cache a `no-store`
  response (the package's API reads stay uncached), and a `dar:purge`
  message handler for cache-on-logout (read payloads can't outlive the
  session). Mount injected via `escapejs` (crafted-path safe).
- conf.py — optional PWA_NAME / PWA_SHORT_NAME / PWA_ICONS (sane
  zero-config defaults).
- urls.py — `web.manifest` + `sw.js` routes before the SPA catch-all.

Tests (tests/test_pwa.py, 8): manifest anonymous + no-per-user-data +
mount + theme-hint + short_name override; SW scope header + no-cache +
embeds mount + no-store/mutation/purge guards. All pass.

Frontend follow-up (needs browser verification): SW registration in
main.tsx + the install-prompt affordance + the logout `dar:purge`
postMessage. The cache-on-logout *contract* is enforced server-side
here via the SW; the SPA hook that fires it is the frontend slot.

Tier 5 — adds top-level URL patterns + conf defaults. Human review
welcome. Backend half of #86; read/UX contract is docs/ux/pwa.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@MartinCastroAlvarez MartinCastroAlvarez merged commit 3108f8a into main May 26, 2026
@MartinCastroAlvarez MartinCastroAlvarez deleted the feat/pwa-86-backend branch May 26, 2026 21:49
});

// --- cache purge on logout (contract §5) ----------------------------------
self.addEventListener('message', (event) => {
MartinCastroAlvarez added a commit that referenced this pull request May 26, 2026
… js/missing-origin-check) (#208)

The PWA service worker's `message` handler (the `dar:purge`
cache-on-logout hook, #200) processed messages without verifying the
sender origin — CodeQL `js/missing-origin-check` (medium). A
cross-origin frame must never be able to drive the SW cache.

Add `if (event.origin && event.origin !== self.location.origin)
return;` so only same-origin clients (the SPA pages this worker
controls) can trigger a purge. Same-origin internal
`client.postMessage` (empty origin) is still accepted; anything
cross-origin is dropped.

This is the one open CodeQL alert on main (the other 10 are fixed
via #191/#193). Clears it → 0 open. Test asserts the served SW
embeds the origin check.

Co-authored-by: Martin Castro Laminrs <mcastro@laminr.ai>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
MartinCastroAlvarez pushed a commit that referenced this pull request May 26, 2026
Ships the batch merged since 0.2.0a2:
- SECURITY: SW message-handler origin check (#208, CodeQL
  js/missing-origin-check) — the artifact's service worker now
  rejects cross-origin cache-control messages.
- PWA backend serving (#200), React login form end-to-end (#190),
  create/Add form completing CRUD (#199), autocomplete FK widget
  (#207), date_hierarchy drill-down (#205), action-label + list
  cosmetics fixes (#203/#204), lint cleanup (#202).

368 tests pass. Tier 6 — version bump; publish via the Security
deploy-gate under the standing "deploy regularly if secure" directive.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
MartinCastroAlvarez added a commit that referenced this pull request May 26, 2026
Ships the batch merged since 0.2.0a2:
- SECURITY: SW message-handler origin check (#208, CodeQL
  js/missing-origin-check) — the artifact's service worker now
  rejects cross-origin cache-control messages.
- PWA backend serving (#200), React login form end-to-end (#190),
  create/Add form completing CRUD (#199), autocomplete FK widget
  (#207), date_hierarchy drill-down (#205), action-label + list
  cosmetics fixes (#203/#204), lint cleanup (#202).

368 tests pass. Tier 6 — version bump; publish via the Security
deploy-gate under the standing "deploy regularly if secure" directive.

Co-authored-by: Martin Castro Laminrs <mcastro@laminr.ai>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
MartinCastroAlvarez added a commit that referenced this pull request May 26, 2026
…ce the original pass (#216)

docs/threat-model.md §4 STRIDE'd only the original 6 endpoint groups
(registry/list/detail/create-update/delete/shell) with pre-merge
"lands in PR #N" language. The surface has grown a lot since; this
adds STRIDE coverage (§4.7–4.16) for every endpoint added after,
citing the real mitigations + their tests:

- 4.7  login/logout (#168/#190) — generic-403 no-enumeration, CSRF,
       policy-before-session, session-fixation, no password logging.
- 4.8  autocomplete (#97) — target-model view gate, no cross-model leak.
- 4.9  actions runner (#101) — whitelist via get_actions, never getattr.
- 4.10 bulk PATCH (#103) — per-row perms, cap, per-row LogEntry.
- 4.11 history (#158/#162) — object-view gate, change-message field-name
       note, the LogEntry get-queryset-rule exception.
- 4.12 delete-preview (#164) — delete-perm gate, counts-only (< HTML admin).
- 4.13 inline writes (#183) — per-row-state gates, atomic rollback,
       deny-by-default unknown inline, generic-400 (no str(exc)).
- 4.14 panel hook (#111) — declared-panels-only resolution.
- 4.15 schema (#108) — staff-gated static envelope schema.
- 4.16 PWA manifest+SW (#86/#200/#208) — no per-user manifest data,
       scope ≤ mount, no-store honored, cache-on-logout, origin check.

Keeps the security doc current with the shipped wire surface — an
audit-readiness must for a public security-sensitive package. Docs-only
(Tier 1).

Co-authored-by: Martin Castro Laminrs <mcastro@laminr.ai>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
MartinCastroAlvarez added a commit that referenced this pull request May 26, 2026
…86 frontend) (#219)

The PWA backend (manifest + hand-rolled SW serving) shipped in #200,
but nothing linked the manifest or registered the SW, so the browser
never offered "Install" and the SW never activated. This wires the
frontend half.

- `index.html` template: `<link rel="manifest" href="{{ mount_point }}web.manifest">`
  + `theme-color` / `apple-mobile-web-app-*` meta so the browser's
  install heuristics fire.
- `main.tsx`: register `<mount>sw.js` scoped to the mount on `load`.
  Best-effort (offline/install is progressive enhancement); the SW
  itself honors `no-store` so authenticated reads are never cached, and
  the mount scope is permitted by the backend's
  `Service-Worker-Allowed: <mount>` header.

Install-prompt affordance + logout cache-purge (`dar:purge`) are the
remaining follow-ups on #86.

Test: `test_shell_links_pwa_manifest` asserts the shell links the
manifest + theme-color. Typecheck green (7 pkgs); build ok; prettier +
ruff + black clean; 15 spa-index tests pass.

Tier 4 (web) + the template (Tier 3-ish, no logic). Self-merging under
the repo-owner's full-tier authorization.

Co-authored-by: Martin Castro Laminrs <mcastro@laminr.ai>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
MartinCastroAlvarez added a commit that referenced this pull request May 26, 2026
* chore(release): bump to v0.2.0a4 — sixth PyPI alpha

Ships the parity batch merged since 0.2.0a3:
- Inline editing in the SPA (#223) + inline field-type metadata (#220).
- PWA end-to-end: SW registration + manifest link + install affordance
  (#219, #222) on top of the backend serving (#200).
- Navigable ForeignKey links in list + detail (#215, #217) — `to`
  emitted only for registered models (anti-adjacency-leak, #89 posture).
- List column customizer (#214), portal Modal (#221), threat-model
  refresh (#216).

Security review of the batch (issue #225): code is secure + documented;
one low-risk incomplete control (cache-purge-on-logout not yet wired
frontend-side — moot under the no-store read posture) is tracked.
CodeQL 0, Dependabot 0, 374 tests pass. Tier 6.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(build): sync pnpm-lock.yaml with apps/web package.json (react-dom/@types/react-dom)

A recent PR (#221/#222) added `react-dom` + `@types/react-dom` to
`frontend/apps/web/package.json` but didn't regenerate the lockfile,
so `pnpm install --frozen-lockfile` (the build + CI path) fails on
`main`:

    specifiers in the lockfile (...) don't match specs in package.json

Regenerated with `pnpm install --lockfile-only` (no version changes —
only adds the already-declared specifiers to the lock). Frozen install
now passes. Release-blocking build-integrity fix found while cutting
0.2.0a4.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Martin Castro Laminrs <mcastro@laminr.ai>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants