NextMyDay/internal/core/web/pwa.go
Claus Lohmar dd865d4f27 feat(web): add the web shell and the PWA (SPEC-009)
- One base layout (layout.html) with the header, the nav from Registry.Nav()
  (D-038), a content slot and the CSRF token; every page renders inside it and
  a module supplies content only
- Embedded same-origin assets (app.css · app.js · webauthn.js · sw.js ·
  manifest.webmanifest · icons) served by the reserved `static` workflow
- webauthn.js drives the three ceremonies from data-* attributes — no inline
  script, so the CSP `script-src 'self'` holds (AC-3)
- manifest start_url "/" (D-011); the service worker never caches an
  authenticated response or /{GUID} (A5)
2026-10-14 10:53:57 +00:00

45 lines
1.5 KiB
Go

package web
import (
"net/http"
"path"
"strings"
)
// ── the service worker's cache policy (A5, SPEC-009 §6) ──────────────────────
//
// THE RULE: the service worker may cache STATIC ASSETS — and nothing else. It
// must never cache an authenticated response and never a /{GUID} page (A5), so
// it does not intercept such a request at all.
//
// StaticCachePrefixes is the ONE source of truth for "static". The embedded
// service worker (static/sw.js) declares the same list; a test asserts the two
// agree, so the policy cannot drift (AC-5).
// StaticCachePrefixes are the only request-path prefixes the service worker may
// cache. Authenticated responses and /{GUID} pages are never in it.
var StaticCachePrefixes = []string{StaticPath}
// CacheablePath reports whether a same-origin request may be cached by the
// service worker (A5): a GET of a static asset, and nothing else. The path is
// CLEANED first, so "/static/../{GUID}" is judged as "/{GUID}" — a traversal
// cannot smuggle an authenticated path into the cache.
//
// It is the policy the embedded worker implements (static/sw.js) and the shape
// its test asserts; no caching decision is taken anywhere else.
func CacheablePath(method, requestPath string) bool {
if method != http.MethodGet {
return false
}
p := requestPath
if !strings.HasPrefix(p, "/") {
p = "/" + p
}
clean := path.Clean(p)
for _, prefix := range StaticCachePrefixes {
if strings.HasPrefix(clean, prefix) {
return true
}
}
return false
}