feat: wave 2 hardening — CVEs, bundle, completeness + design polish#9
Merged
Conversation
…timecapsule route DELIVERY_TYPES icon at timecapsule.lazy.tsx:1038 used the undefined var(--color-slate-400) for the unselected state, silently falling back to inherited color. Map it to var(--ink-muted) (#4A5C7A, royal-tinted secondary) to match the design-tokens-css brand mapping, so unselected delivery-trigger icons render the intended Royal Neo-Deco ink. Selected state unchanged (var(--gold)). Bucket: timecapsule-slate-var (P2)
Root-cause guard for the clickable-div / missing-label / unlabeled-icon-button patterns that accumulated unflagged across web/. Wires the jsx-a11y recommended flat ruleset into web/eslint.config.mjs and pins the named guards as errors: - click-events-have-key-events (div onClick without keyboard handler) - no-static-element-interactions (non-native interactive elements) - anchor-has-content (empty/unlabeled anchors) - control-has-associated-label (unlabeled controls / icon-only buttons) - label-has-associated-control (orphaned form labels) Now surfaces 93 existing a11y issues at lint time so the per-component fixes landing in sibling buckets cannot silently regress. Manifest + root lockfile updated (eslint-plugin-jsx-a11y@^6.10.2). Build + vitest (204) green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Resolves open high-severity Dependabot alerts on dev/build-time transitive dependencies (pure lockfile regeneration, no package.json edits): - esbuild 0.27.7 -> 0.28.1 (Dependabot #172 GHSA-gv7w-rqvm-qjhr high dev-server SSRF; #171 GHSA-g7r4-m6w7-qqqr low). tsx lifts 4.21->4.22.4 to pull esbuild ~0.28; vite already permits 0.28.x. - @grpc/grpc-js 1.9.15 -> 1.9.16 (Dependabot #170 GHSA-5375-pq7m-f5r2; #169 GHSA-99f4-grh7-6pcq, both high memory-allocation DoS). Existing ~1.9.0 constraint from @firebase/firestore already permits 1.9.16. Verified: npm ci resolves clean (0 vulnerabilities), web build green, npm ls shows esbuild@0.28.1 and @grpc/grpc-js@1.9.16. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ault row a11y
Fire-and-forget ActionResult calls swallowed {success:false} (actions never
throw), showing false success or silent failure. Surface error/success feedback
at every call site and roll back optimistic UI on failure.
- vault.lazy.tsx: visibility picker (Everyone + per-heir) now awaits
updateVaultDocument; on failure reverts setVisibleTo to the prior value and
toasts "Could not update who can see this document" (privacy hazard fix).
handleDelete gains an else-branch toast on failed archive; handleDownload
toasts on the swallowed catch. Document rows converted from clickable <div>
to a real <button type="button"> with aria-label "Open <name>" and a focus
ring so preview/download are keyboard-operable.
- assets.tsx: handleAddAsset/handleUpdateAsset/handleArchiveAsset capture the
ActionResult — success toast + close modal on success, error toast on failure.
- life-chapters.tsx: handleRemoveEntry gates its success toast on result.success,
else toasts the error, matching the file's other handlers.
- InviteTeamMember.tsx: handleRevoke sets the error banner on a failed revoke
instead of silently no-op'ing.
estate-actions.ts already returns correct ActionResults; no change needed there.
Bucket: vault-assets-states (P1). Build + vitest (204) green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…Neo-Deco) design-tokens-css (P1). Repair the brand design-system core in globals.css and the shared estate components that carried undefined var(--color-slate-*) inline styles (invalid in this Tailwind v4 setup — slate is not registered as a CSS var, so text color silently broke). globals.css: - Anchor :root defaults for the glass-card system (--card-bg/--card-border/ --card-shadow/--card-shadow-hover/--card-backdrop) so .glass-card and the forthcoming ui/card variant (design-cards-heirlooms bucket) resolve outside a themed surface; add reusable --gold-glow token + .gold-glow/.gold-glow-hover. - Add on-brand neutrals (--neutral-border #DCE3EE, --neutral-faint #F4F6FB) to replace generic slate for borders/faint fills. - Re-tone the status-pulse/.status-dot accent from emerald green to Gold (firewall: never emerald/green/teal as a brand accent). - Delete the dead industrial .sirsi-card/.sirsi-metric-*/.sirsi-table-*/ .sirsi-status-led-*/.sirsi-progress-* block (sharp edges, slate, monospace, emerald LED) — referenced by nothing, the exact Royal Neo-Deco anti-pattern. SectionHeader.tsx: - Re-tone off-brand SECTION_THEMES off the firewall: memories #9D174D->gold, letters #4D7C4D->deep royal, my-people #0F766E->heritage royal, the-vault #334155->deep royal, life-chapters #7C3AED->gold. Royal + Gold anchors. - Fix section-title color: var(--color-slate-900) -> var(--ink). SectionEmptyState.tsx: - Fix empty-section heading color: var(--color-slate-900) -> var(--ink). SettlementGantt.tsx: - Pass concrete brand hex to recharts (CSS vars don't resolve in recharts tick/ axis props): axis ticks #4A5C7A/#142848, axis line #DCE3EE; re-tone tooltip + legend slate utilities to brand ink. Urgency bar colors kept (semantic). Rule 27 (no generic slate/grey). web build + 204 vitest tests green. obituary/timecapsule slate slices delegated to editor-a11y/timecapsule-slate-var. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Carve stable vendor code out of the main app entry and demand-load the heavy probate route so the eager index chunk drops from 1.22MB to 621KB. vite.config.ts: - Add build.rollupOptions.output.manualChunks splitting vendor-react, vendor-firebase, vendor-charts (recharts+d3), vendor-motion (framer-motion), vendor-editor (tiptap/prosemirror), and a named vendor-pdf chunk for the already-dynamic @react-pdf/renderer. - Set chunkSizeWarningLimit to 900. - Resolves framer-motion entry-coupling on the estate layout route via the vendor-motion chunk (no edit to estates.$estateId.tsx). estates.$estateId.probate.tsx -> .lazy.tsx: - Move the 645-line ProbatePage component (which imports SettlementGantt, pulling recharts) into estates.$estateId.probate.lazy.tsx via createLazyFileRoute. Reduce the eager route to createFileRoute only so recharts is no longer on the main entry — demand-loaded per route. - SettlementGantt.tsx is NOT touched; the split is purely route-level. routeTree.gen.ts regenerated by the TanStack router plugin to wire the lazy probate route. Build green (npm run build), 204/204 vitest pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ge email_verified gate
P0 security-authz residuals (wave-2). Two hardened gates that the wave-1
email_verified defense left half-open on the write side and in storage.
CRITICAL — invitation-seizure (functions/index.js, autoMatchInvitation):
The trigger keyed the invitation match + estate_users access grant off
userData.email, which is client-writable. An attacker could verify an
account they control, then write users/{uid}.email = 'victim-invited@...'
to seize the executor/heir role on that invitation — defeating the wave-1
firestore.rules isEstateRole() email_verified gate (which only guards reads).
Fix: resolve identity via admin.auth().getUser(uid); use rec.email
(lowercased/trimmed); return when !rec.emailVerified; match invitations
against the VERIFIED auth email and ignore userData.email entirely.
Defense-in-depth (firestore.rules, users/{userId}):
- create: pin request.resource.data.email == request.auth.token.email so a
profile email a user does not own can never be persisted (closes any
downstream path still reading users/{uid}.email).
- update: add 'email' to the diff denylist — email is identity, set once at
registration, never client-mutable thereafter.
storage.rules isEstateMember():
Mirrored the firestore.rules isEstateRole() email_verified gate. The same
estate_users junction firestore refuses to honor for an unverified user was
being honored by storage for direct downloads. Now requires
request.auth.token.email_verified == true before honoring the junction /
principalId. Low active impact (reads go through Go-API signed URLs that
bypass these rules) but it is the same hardened gate, now consistent.
Tests: functions/index.test.js — autoMatch fixtures updated to the verified-
identity path; added 3 seizure-defense tests (unverified-account-grants-
nothing, forged-profile-email-never-queried, getUser-throws-no-fabricated-
identity). 21/21 green. No legal text fabricated (Rule 9). No gate weakened
to pass a test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ening
Editor a11y (directives + obituary rich-text editors):
- Extract one shared EditorToolbar component (exported from
directives.lazy.tsx, imported by obituary.lazy.tsx) — both editors
were byte-identical inline toolbars.
- Wrap toolbar in role="toolbar" aria-label="Text formatting".
- Add explicit aria-label to every icon-only Button (Bold, Italic,
Heading, Bullet list, Numbered list, Undo, Redo).
- Add aria-pressed={isActive} to all toggle buttons so active state is
exposed to AT instead of being conveyed by background color alone.
Directives "Confirm Signed":
- Replace fire-and-forget updateDirective with a result-checked call on
this legal document — success toast only on result.success, else
toast.error(result.error || fallback).
Obituary export HTML (delegated from design-tokens-css, same file):
- Hardcode hex for the emailed obituary HTML, which renders outside app
CSS scope and cannot resolve var(--color-slate-*) tokens. Royal
Neo-Deco ink #142848 / muted #4A5C7A (+ literal surface hex).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…weep layout-nav-a11y bucket (P1). Accessibility hardening across the chrome a new owner first encounters, plus the Rule-27 grayscale-text sweep on the onboarding funnel and sidebar nav. AdminHeader.tsx - Global search input: aria-label="Search estate", role=combobox, aria-expanded, aria-controls (listbox id), aria-autocomplete=list, aria-activedescendant tracking the highlighted option; aria-label="Clear search" on the X button. - Photo viewer: replaced hand-rolled overlay with accessible Dialog/DialogContent + VisuallyHidden DialogTitle (focus trap, Escape, focus restoration, labeled close) mirroring Sidebar's pattern. - "View As" Owner/Incapacity/Settlement: role=radiogroup + role=radio / aria-checked so the selected mode is not color-only. SearchResults.tsx - Exported stable SEARCH_LISTBOX_ID + searchOptionId(index); each option now carries a stable id and the shell carries the listbox id. - Populated the aria-live region: "N results" / "No results found". Sidebar.tsx - Profile avatar: wrapped photo-open affordance in a focusable <button aria-label="View profile photo"> (only when a photo exists); plain Avatar otherwise. - Inactive nav items: text-slate-400 -> text-[var(--ink-muted)], hover:text-slate-900 -> hover:text-[var(--royal)] (Rule 27). index.tsx - Both password show/hide toggles: aria-label (Show/Hide password) + aria-pressed. estates.create.tsx - Onboarding funnel grayscale sweep (Rule 27): headings/values text-slate-900 -> text-[var(--ink)]; descriptions/labels/hints text-slate-500/400 -> text-[var(--ink-muted)]. Placeholder slate-300 left intentionally faint. Build green (vite build + 204 vitest tests pass). No new lint errors. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…useCollection key
Bucket: contract-runtime-paths (P0) — frontend↔backend contract & runtime-URL correctness.
- memoirs.lazy.tsx: POST video memoirs to the registered route
/api/v1/memoirs/upload-video (was unregistered /api/v1/youtube/upload →
always 404, silently breaking the white-glove video-memoir feature).
Field names + {error:{message}} error shape already align with
HandleUploadVideo (api/cmd/api/main.go:385-388, api/internal/youtube/handler.go).
- Add memoir-upload-contract.test.ts asserting the posted URL matches the
registered route and never the old one.
- Collapse per-file `VITE_API_URL || 'http://localhost:8080'` re-derivations to
the single safe API_BASE helper exported from lib/client.ts (localhost only
when the page itself is on localhost; else VITE_API_URL or same-origin).
Imported in pricing.tsx, tier-gating.ts, dashboard.lazy.tsx, lockbox.lazy.tsx,
estates.$estateId.tsx, SettlementPanel.tsx, memoirs.lazy.tsx. Removes the
single point of failure where a missing .env.production silently routed
payments + 8 journeys to localhost:8080.
- firestore.ts useCollection: key the effect on the FULL normalized constraint
(type + field path + op + value + direction + limit) via normalizeConstraint,
not just c.type — fixes the latent stale-list bug where a value change on a
fixed field path (e.g. where('userId','==',X)) failed to re-subscribe.
Adds regression tests for re-subscribe-on-value-change and no-churn-on-equal.
- lockbox.lazy.tsx handleArchive: surface the archiveLockboxItem result with
success/error toasts (was fully silent), matching handleSave.
- memoirs.lazy.tsx VideoCard/PhotoCard: thumbnails are keyboard-accessible
<button> with aria-label (Play/View {title}); removed redundant onClick from
the now-non-interactive <h4> titles.
Royal Neo-Deco preserved. No security weakened. No backend change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ui/card.tsx: add variant="glass" (--card-bg backdrop-blur + gold hairline border + gold-glow hover) and bake the brand hover/transition into both variants (transition-all duration-300, hover:-translate-y-0.5, gold border + --card-shadow-hover on hover, gold focus-visible ring). Own the radius scale here (cards rounded-3xl / rounded-2xl@sm, inner chips rounded-xl) and drop CardFooter's bg-muted/border-t that fought glass. Token fallbacks are inlined so glass renders on the white flagship screens outside .glass-theme. heirlooms.tsx: - stats + heirloom cards now use Card variant="glass"; removed per-route rounded-*/border/hover overrides (radius now unified on the primitive). - Rule-27 sweep: slate text -> --ink/--ink-muted, slate fills -> --royal/5 & --gold/8, slate borders -> --royal hairlines. - visibility picker no longer fire-and-forget: persistVisibility awaits updateHeirloom, reverts setVisibleTo + toasts on failure. - archive toast gated on result.success (toast.error with reason otherwise). soul-log.lazy.tsx: EntryCard + ShepherdCard use Card variant="glass"; Rule-27 slate text/fill/border sweep (dark video frames preserved). build green (vite build), 204/204 vitest pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
# Conflicts: # package-lock.json
|
Visit the preview URL for this PR (updated for commit d1c5150): https://finalwishes-prod--pr9-wave2-hardening-1jdpj46c.web.app (expires Sun, 21 Jun 2026 18:38:38 GMT) 🔥 via Firebase Hosting GitHub Action 🌎 Sign: 6c224b6590e2084ad0c46f0efd6a68ed08b63fa3 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Wave 2 Hardening — Integration
Stacked on PR #8 (
integration/completion). Merges 11 disjoint-glob fix branches into a single hardening wave.Buckets
Security & deps
w2/dep-security— esbuild 0.27.7→0.28.1, tsx 4.21→4.22.4, @grpc/grpc-js 1.9.15→1.9.16 (Dependabot HIGH: gRPC DoS GHSA-5375-pq7m-f5r2 / GHSA-99f4-grh7-6pcq; esbuild dev-server SSRF GHSA-gv7w-rqvm-qjhr).npm audit --omit=devnow reports 0 vulnerabilities.w2/security-authz— close invitation-seizure via verified identity; storageemail_verifiedgate; firestore.rules hardening. opensign/provider.go signing-secret wiring untouched.Correctness & runtime paths
w2/contract-runtime-paths— fix memoir upload route, unify API base, value-aware useCollection key.w2/vault-assets-states— honor ActionResult on vault/asset/chapter/invite writes; vault row a11y.Performance
w2/bundle-perf— split vendor chunks + lazy-load probate (probate.tsx → probate.lazy.tsx). Entryindexnow 623KB (gzip 155KB); vendor-pdf (1.4MB) is now a lazy chunk.Design polish (Royal Neo-Deco)
w2/design-tokens-css— brand tokens + remove undefined slate CSS vars.w2/design-cards-heirlooms— glass Card variant + heirlooms/soul-log sweep.w2/editor-a11y— accessible shared editor toolbar; directives/obituary hardening.w2/layout-nav-a11y— header/sidebar/landing/search a11y + nav slate sweep.w2/timecapsule-slate-var— royal-ink token for timecapsule icon.w2/a11y-tooling— eslint-plugin-jsx-a11y in lint config.Net slate-utility occurrences in
web/src: 910 → 733 (design sweep reduced, did not regress).Build / Test status (all green on wave2/hardening)
go build ./...clean ·go vet ./...clean ·go test ./internal/...all passnpm run buildgreen ·npx vitest run— 208 passed / 14 filesnpm test— 21 passednpm civerifies merged lockfile integrity;npm audit --omit=dev= 0 vulnerabilitiesHonest residuals
web/src/routes/estates.$estateId.probate.lazy.tsxpulls vendor-pdf (1.4MB lazy chunk) → triggers Vite >900KB warning. Non-blocking (lazy-loaded, not in entry). Future: dynamic-import the pdf path within probate.slate-*occurrences remain inweb/src(incl.text-slate-900headings in probate route). Pre-existing on base; partially swept this wave. Not introduced here. Follow-up sweep recommended for full Royal Neo-Deco compliance.🤖 Generated with Claude Code