Summarises the code review + dependency scan: no exploitable client/server issue found; documents why npm audit fix was reverted (broke the build) and which advisories are unreachable/accepted (xlsx export-only, HashRouter open-redirect, MCP client-side, build-tool deps). Fixes the outdated 'never innerHTML' claim (popups use innerHTML with escapeHtml) and updates the contact NIP-05 to the seeded skatespots@skate-spots.org.
Completes the admin moderation coverage from the previous commit.
Lab profiles (coaches, filmers, editors, photographers, builders, artists,
graffiti, tattoo, trainers, musicians):
- useLabProfiles / useLabProfile and useCoaches / useCoach now filter out
admin-hidden entries (list) and 404 hidden ones (detail) via the shared
moderation layer. Added filterAdminHiddenMixed() for the mixed-kind Lab
union (kind read from each item's event).
- CoachDetailPage (the shared Lab detail page) now shows the delete/takedown
card to admins too, not just the owner, with admin-specific copy.
DeleteEntityDialog already handles the owner-or-admin logic.
Gear / marketplace listings (NIP-99, kind 30402):
- useMarketplaceItems / useMarketplaceAtSpot filter out admin-hidden listings.
- GearSwapItemCard: admins get a "Remove (admin)" action in the card menu with
a confirmation dialog that routes through useDeleteEntity (NIP-09 delete +
hidden label). Report stays for non-owners.
SECURITY.md: updated the admin-moderation coverage list and auditor file map.
Build + lint pass, no console errors.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
The two SkateSpots admin pubkeys (already in src/lib/admins.ts) can now remove
any listing, not just their own. Because a NIP-09 deletion signed by a
non-author isn't honored by most relays, admin takedowns are made reliable with
a client-side moderation layer:
- useDeleteEntity: owners still publish a plain NIP-09 kind:5. Admins acting on
someone else's content publish the kind:5 AND a NIP-32 kind:1985 `hidden`
label referencing the target coordinate/id, authored by the admin.
- useAdminModeration (new): fetches admin-authored `hidden` labels app-wide and
exposes a fast lookup + filterAdminHidden() helper. Only labels whose author
is in the hardcoded ADMIN_PUBKEYS are honored (author = trust boundary,
re-verified client-side; never derived from a d-tag).
- Wired the hide filter into the list + detail hooks for spots, crews (incl.
by-focus), tours, events, and shops via React Query `select`, so removed
content disappears from lists and 404s on its detail page.
UI:
- DeleteEntityDialog now renders for owner OR admin, with distinct
"Remove (admin)" framing and copy (used on Crew, Coach, Tour, Event pages).
- SpotDetailPage & ShopDetailPage: routed their inline delete through
useDeleteEntity, show the delete button for admins, and relabel the confirm
dialog for admin takedowns.
- Added the delete control to Tour and Event detail pages (previously none).
SECURITY.md: documented the admin moderation trust boundary and extended the
auditor file map. Verified build + no console errors.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Follow-up to the map-XSS work. The Capacitor app ships the same dist/ bundle
(webDir: 'dist') and renders the same untrusted relay content, so it shared
the same injection surface — but the CSP only existed as HTTP headers
(public/_headers, netlify.toml), which never reach a WebView loading from a
local scheme. The prior "OS sandbox is enough" comment was wrong: the sandbox
isolates the app from OTHER apps; it does not stop a same-origin injected
script from reading this app's own localStorage (nsec, NWC string, Cashu seed)
or IndexedDB (Cashu bearer ecash proofs, native-only wallet).
Changes:
- index.html: add a <meta http-equiv="Content-Security-Policy"> so the policy
ships inside the bundle and applies to BOTH web and native. script-src stays
'self' 'wasm-unsafe-eval' (no unsafe-inline/eval). Capacitor local origins
(https://localhost, capacitor://localhost, ionic://localhost) are allowlisted
on script/style/img/connect so asset loading & live-reload still work.
Replaces the old "CSP intentionally omitted" comment. Verified the app boots
and the map renders with no CSP violations.
- SECURITY.md: correct the CSP section (now three synced locations incl. the
meta tag; explain the local-scheme reason and the OS-sandbox misconception),
add Cashu ecash/seed to the threat model and residual-risks, note the shared
bundle, and extend the auditor file map (useCashu.ts, platform.ts,
capacitor.config.ts).
Note: the actual injection fix (createElement + textContent in map popups)
already shipped on mobile since it lives in the shared bundle; this commit
adds the defense-in-depth layer that was previously web-only.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Add root SECURITY.md documenting the threat model (plaintext nsec / NWC in
localStorage inherent to client-side Nostr), reporting channels (Nostr DM /
NIP-05), in/out of scope, the CSP and its trade-offs, untrusted-data
rendering rules, URL/CSS sanitization, author-scoped queries, NWC budget
guidance, accepted residual risks, and an auditor file map.
- netlify.toml now serves the same Content-Security-Policy as public/_headers
and uses X-Frame-Options: DENY + Permissions-Policy, so the hardening holds
regardless of which static host serves the web build.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>