Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
545e2c4
feat(corpus): source verified IL/MD/MN estate-planning statutory text…
cyltoncollymore Jun 14, 2026
4a4500a
fix(googlephotos): canonical NotFound dedupe + loud bucket-fallback g…
cyltoncollymore Jun 14, 2026
ac93155
fix(web): remove email.ts 'system' createdBy trap (fail-closed mail r…
cyltoncollymore Jun 14, 2026
dd12e6d
docs(rule30): document all 26 lib modules, 4 missing routes, fix stal…
cyltoncollymore Jun 14, 2026
4dbe392
fix(heir): constrain HeirWelcome soul-log query so heirs/executors ca…
cyltoncollymore Jun 14, 2026
3649cb9
fix(accept-invite): replace async-grant timeout dead-end with self-he…
cyltoncollymore Jun 14, 2026
8e87c41
style(design-tokens): wire Royal Neo-Deco into shadcn primitive defaults
cyltoncollymore Jun 14, 2026
d29410c
fix(estate): purge fabricated demo data + dead mock RPCs from EstateS…
cyltoncollymore Jun 14, 2026
b40d521
fix(integrations): graceful guards for unprovisioned config-gap features
cyltoncollymore Jun 14, 2026
aa38530
fix(design): enforce Royal ink + brand CTAs across estate routes (Rul…
cyltoncollymore Jun 14, 2026
bd4fbc9
fix(auth): unbreak fiduciary MFA enrollment + principal grace + estat…
cyltoncollymore Jun 14, 2026
3491b26
fix(api/probate-forms): real asset valuation, drop dead stub, expand …
cyltoncollymore Jun 14, 2026
5318e5f
fix(functions): remove dead SMS stub, repair stale tests, reconcile spec
cyltoncollymore Jun 14, 2026
faa2136
Merge branch 'area/web-heir-soullog-query' into integration/completion
cyltoncollymore Jun 14, 2026
92536ee
Merge branch 'area/functions-sms-and-stale-tests' into integration/co…
cyltoncollymore Jun 14, 2026
3ee795d
Merge branch 'area/api-estate-service-mocks' into integration/completion
cyltoncollymore Jun 14, 2026
52a506f
Merge branch 'area/api-probate-forms-valuation' into integration/comp…
cyltoncollymore Jun 14, 2026
eb76a2f
Merge branch 'area/api-googlephotos-bucket-dedupe' into integration/c…
cyltoncollymore Jun 14, 2026
4d13066
Merge branch 'area/web-integration-config-guards' into integration/co…
cyltoncollymore Jun 14, 2026
938cb1c
Merge branch 'area/web-accept-invite-retry' into integration/completion
cyltoncollymore Jun 14, 2026
7d66008
Merge branch 'area/design-tokens-shadcn-primitives' into integration/…
cyltoncollymore Jun 14, 2026
5b2b945
Merge branch 'area/design-route-color-fidelity' into integration/comp…
cyltoncollymore Jun 14, 2026
58600ff
Merge branch 'area/web-email-helpers-trap' into integration/completion
cyltoncollymore Jun 14, 2026
b67cb28
Merge branch 'area/docs-rule30-readmes' into integration/completion
cyltoncollymore Jun 14, 2026
24a4ae7
Merge branch 'feat/cr10-corpus' into integration/completion
cyltoncollymore Jun 14, 2026
7943d35
ci(functions): add functions-test Jest job to PR + merge workflows
cyltoncollymore Jun 14, 2026
e2c20dd
test(web): align persona and invitation contracts
cyltoncollymore Jun 14, 2026
c18188c
test(web): align persona and invitation contracts
cyltoncollymore Jun 14, 2026
86bb075
fix(web): replace undefined slate icon color with royal ink token in …
cyltoncollymore Jun 14, 2026
00691d5
chore(web): add eslint-plugin-jsx-a11y to lint config [a11y-tooling]
cyltoncollymore Jun 14, 2026
33a9cc5
chore(deps): bump esbuild 0.28.1 and @grpc/grpc-js 1.9.16 in lockfile
cyltoncollymore Jun 14, 2026
585e3ef
fix(web): honor ActionResult on vault/asset/chapter/invite writes + v…
cyltoncollymore Jun 14, 2026
de38ca8
fix(web): brand design tokens + kill undefined slate CSS vars (Royal …
cyltoncollymore Jun 14, 2026
2a5376a
perf(web): split vendor chunks + lazy-load probate to slim 1.22MB entry
cyltoncollymore Jun 14, 2026
633fee6
fix(security): close invitation-seizure via verified identity + stora…
cyltoncollymore Jun 14, 2026
2c45c64
fix(web): accessible shared editor toolbar + directives/obituary hard…
cyltoncollymore Jun 14, 2026
66707d3
fix(web): header/sidebar/landing/search a11y + onboarding+nav slate s…
cyltoncollymore Jun 14, 2026
1be6587
fix(web): correct memoir upload route + unify API base + value-aware …
cyltoncollymore Jun 14, 2026
f0fe542
feat(web): glass Card variant + heirlooms/soul-log brand sweep
cyltoncollymore Jun 14, 2026
735c483
Merge branch 'w2/security-authz' into wave2/hardening
Jun 14, 2026
c098716
Merge branch 'w2/contract-runtime-paths' into wave2/hardening
Jun 14, 2026
2d53873
Merge branch 'w2/vault-assets-states' into wave2/hardening
Jun 14, 2026
b6def6d
Merge branch 'w2/design-tokens-css' into wave2/hardening
Jun 14, 2026
5e18ebf
Merge branch 'w2/design-cards-heirlooms' into wave2/hardening
Jun 14, 2026
6bafa7a
Merge branch 'w2/editor-a11y' into wave2/hardening
Jun 14, 2026
7428988
Merge branch 'w2/layout-nav-a11y' into wave2/hardening
Jun 14, 2026
a6e926a
Merge branch 'w2/timecapsule-slate-var' into wave2/hardening
Jun 14, 2026
26dbaa7
Merge branch 'w2/dep-security' into wave2/hardening
Jun 14, 2026
d1c5150
Merge branch 'w2/a11y-tooling' into wave2/hardening
Jun 14, 2026
65c961e
fix(opensign): signer is estate principal, not caller; require verifi…
Jun 14, 2026
44e302a
style(guards): sweep slate-* to Royal Neo-Deco tokens
Jun 14, 2026
4effa22
style(ui): sweep slate-* to Royal Neo-Deco in ui + skeletons
Jun 14, 2026
4506a3e
style(web): sweep slate -> Royal Neo-Deco in estate-detail routes
Jun 14, 2026
0b78c9e
style(web): Royal Neo-Deco sweep for routes shell + onboarding
Jun 14, 2026
0d047f1
style(routes): Royal Neo-Deco sweep of top-level marketing routes
Jun 14, 2026
1a7ff83
style(web): Royal Neo-Deco slate sweep — components-layout-landing
Jun 14, 2026
b63c66d
style(estate): sweep slate-* to Royal Neo-Deco tokens
Jun 14, 2026
0012403
Merge branch 'w3/routes-shell-onboarding' into wave3/royal-sweep
Jun 14, 2026
8b588f5
Merge branch 'w3/routes-estate-detail-subtree' into wave3/royal-sweep
Jun 14, 2026
9177c56
Merge branch 'w3/components-estate' into wave3/royal-sweep
Jun 14, 2026
b217a66
Merge branch 'w3/components-guards' into wave3/royal-sweep
Jun 14, 2026
3726c18
Merge branch 'w3/components-layout-landing' into wave3/royal-sweep
Jun 14, 2026
5bd74e4
Merge branch 'w3/components-ui-skeletons' into wave3/royal-sweep
Jun 14, 2026
2fe5f9c
ops(signing): estates principalId backfill (dry-run default) for sign…
Jun 14, 2026
eae6b13
Merge remote-tracking branch 'origin/wave2/hardening' into consolidat…
Jun 14, 2026
2477eea
Merge remote-tracking branch 'origin/wave3/royal-sweep' into consolid…
Jun 14, 2026
1184718
Merge remote-tracking branch 'origin/feat/signer-principal' into cons…
Jun 14, 2026
412f03b
fix(lint): adopt jsx-a11y incrementally (warn, not error) — unblock b…
Jun 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 28 additions & 3 deletions .github/workflows/firebase-hosting-merge.yml
Original file line number Diff line number Diff line change
Expand Up @@ -128,13 +128,38 @@ jobs:
- name: Build API
run: go build -v ./cmd/api

# ===========================================
# Functions: Test (Jest)
# ===========================================
functions-test:
name: Functions - Test (Jest)
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./functions
steps:
- uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
cache-dependency-path: functions/package-lock.json

- name: Install dependencies
run: npm ci

- name: Run tests
run: npm test

# ===========================================
# Deploy: Firebase Hosting (finalwishes-prod)
# ===========================================
deploy-hosting:
name: Deploy - Firebase Hosting
runs-on: ubuntu-latest
needs: [web-build, api-check]
needs: [web-build, api-check, functions-test]
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
steps:
- uses: actions/checkout@v4
Expand Down Expand Up @@ -186,7 +211,7 @@ jobs:
deploy-api:
name: Deploy - API to Cloud Run
runs-on: ubuntu-latest
needs: [api-check]
needs: [api-check, functions-test]
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
steps:
- uses: actions/checkout@v4
Expand Down Expand Up @@ -226,7 +251,7 @@ jobs:
deploy-functions:
name: Deploy - Firebase Functions
runs-on: ubuntu-latest
needs: [web-build, api-check]
needs: [web-build, api-check, functions-test]
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
steps:
- uses: actions/checkout@v4
Expand Down
28 changes: 27 additions & 1 deletion .github/workflows/firebase-hosting-pull-request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,38 @@ jobs:
- name: Run tests
run: npm test

# ===========================================
# Functions: Test (Jest)
# ===========================================
functions-test:
name: Functions - Test (Jest)
if: ${{ github.event.pull_request.head.repo.full_name == github.repository }}
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./functions
steps:
- uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
cache-dependency-path: functions/package-lock.json

- name: Install dependencies
run: npm ci

- name: Run tests
run: npm test

# ===========================================
# Web: Build & Deploy Preview
# ===========================================
build_and_preview:
name: Web - Build & Preview
needs: [api-check, web-test]
needs: [api-check, web-test, functions-test]
if: ${{ github.event.pull_request.head.repo.full_name == github.repository }}
runs-on: ubuntu-latest
steps:
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,14 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and [Sem
- **Shared-services signing provider — consume Sirsi first, dissociated fallback (ADR-047)** (`api/internal/opensign/provider.go`, `handler.go`, `webhook.go`) — the OpenSign create flow now goes through a `SigningProvider` that CONSUMES THE SIRSI SIGN SERVICE FIRST (`SERVICES_REGISTRY` endpoint, Bearer/ADR-006 HMAC, tenant-attributed) and falls back to dissociated/self-hosted infra ONLY on a Sirsi-org **availability** failure (transport/timeout/5xx) — never on a clean business rejection (4xx), which is surfaced. Each result records `ServedBy` for consumption observability. Realizes the owner's shared-services model (tenants buy Sirsi services; resilient to Sirsi-org outages) portfolio-wide. The server-side envelope→directive binding + fail-closed webhook are unchanged. Config via `SIRSI_SIGN_*` (primary) + `OPENSIGN_*` (fallback) env/secrets.
- **Google Photos import — frontend flow (CR-12, ADR-045)** (`web/src/lib/google-photos-import.ts`, `web/src/routes/estates.$estateId.heirlooms.tsx`) — wires the existing backend Picker routes (`/api/v1/heirlooms/{estateId}/google-photos/*`) to the UI: an "Import from Google Photos" button on the Heirloom Registry obtains a Picker-scoped Google OAuth token via Google Identity Services (works for email/password users — no Firebase session disruption), creates a picking session, opens Google's own picker, polls until the user finishes, then triggers the server-side import (download + de-dup + store as heirlooms). Realtime list shows imports automatically. GIS load retries on script error + a 90s token timeout guards a dismissed popup. **OWNER PREREQUISITES (build-time/infra):** enable the Google Photos Picker API on `finalwishes-prod`, add `…/auth/photospicker.mediaitems.readonly` to the OAuth consent screen, and set `VITE_GOOGLE_OAUTH_CLIENT_ID`. Until configured, the button reports "not configured." Full end-to-end verification requires those prereqs.
- **Legal RAG corpus ingestion pipeline (CR-10, ADR-044)** (`api/cmd/corpus-ingest`, `api/internal/guidance/rag.go`, `docs/legal-corpus/manifest.md`) — an idempotent CLI that loads the Shepherd's legal corpus (`legal_corpus_sources` + `legal_corpus_chunks`, pgvector) from a manifest of **owner-verified, verbatim** statute text. Chunks on blank-line paragraph boundaries (≤~2000 chars, never mid-sentence), embeds each chunk with `gemini-embedding-001`/`RETRIEVAL_DOCUMENT` (new `EmbedDocument` — asymmetric to the retriever's `RETRIEVAL_QUERY`), and upserts by id. `-dry-run` parses+chunks with no DB/Vertex. The pipeline NEVER generates/paraphrases/truncates legal text (Rule 9); engineering owns the pipeline, owner/legal owns sourcing the verified text (IL/MD/MN launch statutes). Built + vetted + dry-run verified; corpus population + the CR-10 evidence run remain the owner/data step.
- **CI now runs the Cloud Functions Jest suite** (`.github/workflows/firebase-hosting-pull-request.yml`, `.github/workflows/firebase-hosting-merge.yml`) — added a `functions-test` job (ubuntu-latest, Node 22, `working-directory: ./functions`, `npm ci` → `npm test`) to both PR and merge workflows. Previously `functions/index.test.js` (18 tests covering `autoMatchInvitation` backfill, `sendMail` recipient validation, MIME header-injection guards, Guardian inactivity) ran in neither CI workflow and could silently rot while functions were still deployed. The job now gates the PR aggregate (`build_and_preview`) alongside `api-check`/`web-test`, and gates all three merge deploys (`deploy-hosting`, `deploy-api`, `deploy-functions`) so no release ships with red functions tests. Closes the deferred residual from the completion wave (the fix-bucket globs excluded workflow YAML).

### Changed
- **OpenSign signer is now the estate PRINCIPAL, not the caller — with a verified-email gate (ADR-047; claude-home signer=principal decision 2026-06-14)** (`api/internal/opensign/handler.go`, `signer.go`, `webhook.go`, `cmd/api/main.go`) — a legal directive/POA must be signed BY the estate principal whose estate it governs; an executor/admin only INITIATES the ceremony. `HandleCreateEnvelope` no longer takes the signer identity from the authenticated caller's token claims. It now resolves the signer SERVER-SIDE from `estates/{id}.principalId` → Firebase Auth (verified email + display name), and **REJECTS with HTTP 403 if the principal's email is not verified** (signing cannot proceed against an unverified address). Missing/empty `principalId` → 400; auth client unavailable → 503; Firebase lookup failure → 502. The caller is recorded only as `initiatedBy` in the server-only `signing_envelopes` audit record (`createdBy` retained for back-compat; `signerEmail` now persisted). `NewWebhookHandler` gains an `*firebaseAuth.Client` param (threaded from `main.go`, declared at outer scope so it is in scope at all 3 call sites). Resolution is fronted by a small `signerResolver` interface (production `firebaseSignerResolver` backed by Firestore+Firebase Auth; a fake in tests) — the auth client cannot be constructed offline. This supersedes the prior "signer = authenticated caller" behavior. `provider.go` untouched.
- **Soul Log per-recipient narrowing — `sharedWith` UIDs (ADR-046 #1 residual closed)** (`firestore.rules`, `firestore.indexes.json`, `web/src/routes/estates.$estateId.soul-log.lazy.tsx`, `web/src/lib/firestore.ts`, `functions/index.js`, `scripts/migrate-soullog-sharedwith.js`) — a non-owner could read EVERY `shared` Soul Log entry (incl. other heirs'); now a non-owner reads only entries shared WITH THEM. Entries carry `sharedWith` (array of heir UIDs); the read rule + non-owner query gate on `request.auth.uid in resource.data.sharedWith` (array-contains, new composite index). The composer resolves tagged heir names→UIDs at save; `autoMatchInvitation` backfills a heir's UID into `sharedWith` of entries tagged with their name when they accept (so pre-registration sharing resolves). Ships an idempotent DRY-RUN-by-default migration for existing entries (`scripts/migrate-soullog-sharedwith.js` — owner runs `--apply` AFTER review). **Migration must run before the rule deploys, as the rule/query is breaking for un-migrated entries.**

### Fixed
- **PR test contract drift — email-only invitations and settings MFA access** (`web/src/lib/invitations.test.ts`, `web/src/lib/persona.test.ts`, `web/src/components/guards/RoleGuard.test.ts`) — aligned Vitest expectations with the documented production contract: invitations remain email-only until a live SMS provider/function exists, so phone input from older callers is ignored rather than persisted or queued; fiduciary/heir access to `settings` remains allowed for MFA/profile security while protected estate surfaces stay blocked.
- **HIGH — OpenSign signing ceremony now estate-bound (H1 create-side closed; Assiduous pattern)** (`api/internal/opensign/handler.go`, `webhook.go`, `cmd/api/main.go`, `firestore.rules`, `web/src/routes/estates.$estateId.directives.lazy.tsx`) — `CreateEnvelopeHandler` was a standalone func that took `signerEmail`/templateId from the body with **no estate binding**, and the webhook stamped the verified result onto **whatever directive matched the envelopeId across ALL estates** (a client-writable field). Adopted the proven Assiduous `opensign.Service` pattern: the create handler is now a method on the fs-bearing handler, requires `estateId`+`directiveId`, verifies estate **writer** access, and records a **server-only `signing_envelopes/{envelopeId}` → (estate,directive) mapping**; `handleSigningCompleted`/`handleSigningDeclined`/status-poll resolve that mapping with a direct GET and update **only the bound directive** — the signing evidence chain can no longer be redirected to another estate's directive. The signer identity is now forced to the AUTHENTICATED caller (token email claim) — a writer can no longer name an arbitrary signerEmail (claude-home PR #4 review). (The unauthenticated webhook **forge** was already closed separately.) Added `auth.ContextWithUserID` test helper.

### Added
Expand Down
17 changes: 12 additions & 5 deletions api/cmd/api/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"cloud.google.com/go/firestore"
"cloud.google.com/go/storage"
firebase "firebase.google.com/go/v4"
firebaseAuth "firebase.google.com/go/v4/auth"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/go-chi/cors"
Expand Down Expand Up @@ -200,6 +201,12 @@ func main() {

// Initialize Firebase Admin for auth token verification
var authMiddleware func(http.Handler) http.Handler
// authClient is also consumed by the OpenSign create-envelope handler to resolve the
// estate PRINCIPAL's verified identity (the legal signer is the principal, never the
// caller — claude-home signer=principal decision 2026-06-14). Declared at the outer
// scope so it is in scope at the NewWebhookHandler call sites below; stays nil in
// local dev with no Firebase Auth.
var authClient *firebaseAuth.Client

firebaseApp, err := firebase.NewApp(ctx, nil)
if err != nil {
Expand All @@ -210,14 +217,15 @@ func main() {
log.Warn().Err(err).Msg("Firebase Admin SDK initialization failed — auth middleware disabled (local dev only)")
authMiddleware = func(next http.Handler) http.Handler { return next }
} else {
authClient, err := firebaseApp.Auth(ctx)
ac, err := firebaseApp.Auth(ctx)
if err != nil {
if projectID != "" {
log.Fatal().Err(err).Msg("Firebase Auth client initialization failed in production mode")
}
log.Warn().Err(err).Msg("Firebase Auth client initialization failed — auth middleware disabled (local dev only)")
authMiddleware = func(next http.Handler) http.Handler { return next }
} else {
authClient = ac
log.Info().Msg("Firebase Admin Auth initialized — token verification active")
authMiddleware = auth.Middleware(authClient)
}
Expand Down Expand Up @@ -335,7 +343,7 @@ func main() {
r.Route("/api/v1", func(r chi.Router) {
r.Use(authMiddleware)
r.Route("/opensign", func(r chi.Router) {
oh := opensign.NewWebhookHandler(fs)
oh := opensign.NewWebhookHandler(fs, authClient)
r.Post("/create-envelope", oh.HandleCreateEnvelope)
if fs != nil {
r.Get("/status", oh.HandleCheckSigningStatus)
Expand All @@ -344,13 +352,13 @@ func main() {
})
// OpenSign webhook — no auth, uses webhook signature verification
if fs != nil {
webhookHandler := opensign.NewWebhookHandler(fs)
webhookHandler := opensign.NewWebhookHandler(fs, authClient)
r.Post("/api/v1/opensign/webhook", webhookHandler.HandleWebhook)
}

r.Group(func(r chi.Router) {
r.Use(authMiddleware)
r.Post("/api/envelopes", opensign.NewWebhookHandler(fs).HandleCreateEnvelope)
r.Post("/api/envelopes", opensign.NewWebhookHandler(fs, authClient).HandleCreateEnvelope)
})

// Guidance routes (The Shepherd v3 — Claude Opus via sirsi-ai, Genkit fallback)
Expand Down Expand Up @@ -473,7 +481,6 @@ func main() {
r.Post("/death-cert/confirm", probateHandler.HandleConfirmDeathCert)
r.Get("/death-cert", probateHandler.HandleGetDeathCertFacts)
r.Get("/forms", probateHandler.HandleGetFormTemplates)
r.Get("/forms/data", probateHandler.HandleGetFormData)
r.Get("/executor/status", probateHandler.HandleGetExecutorStatus)
r.Post("/executor/confirm", probateHandler.HandleConfirmExecutorRole)
r.Get("/advance-directives", probateHandler.HandleGetAdvanceDirectives)
Expand Down
Loading
Loading