diff --git a/.github/workflows/firebase-hosting-merge.yml b/.github/workflows/firebase-hosting-merge.yml
index b6c15a2..9126330 100644
--- a/.github/workflows/firebase-hosting-merge.yml
+++ b/.github/workflows/firebase-hosting-merge.yml
@@ -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
@@ -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
@@ -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
diff --git a/.github/workflows/firebase-hosting-pull-request.yml b/.github/workflows/firebase-hosting-pull-request.yml
index d9e4344..a615fee 100644
--- a/.github/workflows/firebase-hosting-pull-request.yml
+++ b/.github/workflows/firebase-hosting-pull-request.yml
@@ -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:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9194f77..cec5852 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
diff --git a/api/cmd/api/main.go b/api/cmd/api/main.go
index d02eefe..989f1c3 100644
--- a/api/cmd/api/main.go
+++ b/api/cmd/api/main.go
@@ -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"
@@ -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 {
@@ -210,7 +217,7 @@ 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")
@@ -218,6 +225,7 @@ func main() {
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)
}
@@ -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)
@@ -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)
@@ -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)
diff --git a/api/internal/forms/maps/il_small_estate_3606.go b/api/internal/forms/maps/il_small_estate_3606.go
index add7c64..b4d4b4e 100644
--- a/api/internal/forms/maps/il_small_estate_3606.go
+++ b/api/internal/forms/maps/il_small_estate_3606.go
@@ -5,23 +5,32 @@ import "github.com/sirsi-technologies/finalwishes-api/internal/forms"
// SmallEstateAffidavit3606 returns the coordinate map for the Illinois Small
// Estate Affidavit (SOS/Probate Form 3606, rev. 1/26), 755 ILCS 5/25-1.
//
-// The blank is flat (no AcroForm), 4 pages, US Letter. This first-pass map
-// covers the core single-value affidavit fields (affiant + decedent identity,
-// dates, addresses, relationship) and flags the execution block on page 4
-// (signature, date, notary) as Execution=true — never stamped, wet-sign +
-// notarization required.
+// The blank is flat (no AcroForm), 4 pages, US Letter. This map covers:
+// - the core single-value affidavit fields (affiant + decedent identity,
+// dates, addresses, relationship);
+// - the single-claimant rows of the variable-length schedules — the first
+// creditor row (para 6/7), the first heir/legatee row (para 9), the total
+// personal-property valuation, and the spousal/child award amount — which
+// cover the common one-creditor / one-heir estate without a repeating-row
+// renderer;
+// - the execution block on page 4 (signature, date, notary), flagged
+// Execution=true — never stamped, wet-sign + notarization required.
//
-// NOT YET MAPPED (documented limitation — needs a repeating-row renderer, a
-// future engine extension): the variable-length schedules — creditor Classes
-// 1–7 (paras 6–7), heirs/legatees tables (paras 9–10), and the spousal/child
-// award computations. Those rows vary per estate and cannot be expressed as
-// fixed coordinates. Single-claimant common cases can be added as discrete
-// fields later; multi-row support is tracked separately.
+// MULTI-ROW LIMITATION (tracked, not hidden): estates with MORE THAN ONE
+// creditor in any of Classes 1–7 (paras 6–7) or MORE THAN ONE heir/legatee
+// (paras 9–10) need a repeating-row renderer — a future forms-engine extension.
+// Until that lands, multi-claimant estates must complete the extra rows by hand
+// on the printed draft; the prefill stamps only the first row of each schedule.
+// The fill engine's missing-required reporting plus the PREPARATION-ASSISTANCE
+// disclaimer keep this Rule-9 honest: nothing is asserted that was not supplied.
//
-// Coordinates are derived from positioned-text extraction (pdftotext -bbox) and
-// marked ConfidenceLow: the affidavit uses dotted/underscore leaders, so the
-// exact blank-start on each line is an estimate to be tuned against the proof
-// raster before this form is treated as production-ready.
+// PRODUCTION READINESS: every coordinate below is marked ConfidenceLow. The
+// affidavit uses dotted/underscore leaders, so each blank-start X/Y is an
+// estimate derived from positioned-text extraction (pdftotext -bbox) that MUST
+// be tuned against the proof raster (docs/forms-phase0/proof/) before this form
+// is offered as a final draft. Callers should treat il_small_estate_3606 as a
+// PREVIEW form — surface it behind a preview affordance, not as GA — until a
+// proof-raster pass promotes these coordinates to ConfidenceHigh.
func SmallEstateAffidavit3606() *forms.CoordinateMap {
return &forms.CoordinateMap{
FormID: "il_small_estate_3606",
@@ -79,6 +88,71 @@ func SmallEstateAffidavit3606() *forms.CoordinateMap {
Note: "After \"before their death was\" (x315), yTop 309.81.",
},
+ // --- Page 2: personal-property valuation + first creditor row ---
+ //
+ // Single-claimant subset of the variable-length schedules. The first
+ // row of each schedule is mapped here; additional rows require the
+ // repeating-row renderer (documented limitation above) and are
+ // completed by hand on the printed draft.
+ {
+ Key: "total_personal_property", Label: "Total value of decedent's personal estate (para 5)",
+ Kind: forms.FieldText, Page: 2,
+ X: 360, Y: 700.0, MaxWidth: 160,
+ Confidence: forms.ConfidenceLow,
+ Note: "After \"does not exceed\" / value rule on para 5. Personal property only — real estate excluded per 755 ILCS 5/25-1. ESTIMATE: tune X/Y against proof raster.",
+ },
+ {
+ Key: "creditor1_name", Label: "First creditor — name (Classes 1–7, paras 6–7, row 1)",
+ Kind: forms.FieldText, Page: 2,
+ X: 70, Y: 470.0, MaxWidth: 240,
+ Confidence: forms.ConfidenceLow,
+ Note: "Row 1 of the creditor schedule. Multi-creditor estates need the repeating-row renderer. ESTIMATE: tune against proof raster.",
+ },
+ {
+ Key: "creditor1_class", Label: "First creditor — statutory class 1–7 (paras 6–7, row 1)",
+ Kind: forms.FieldText, Page: 2,
+ X: 320, Y: 470.0, MaxWidth: 60,
+ Confidence: forms.ConfidenceLow,
+ Note: "Class column for creditor row 1. ESTIMATE: tune against proof raster.",
+ },
+ {
+ Key: "creditor1_amount", Label: "First creditor — claim amount (paras 6–7, row 1)",
+ Kind: forms.FieldText, Page: 2,
+ X: 400, Y: 470.0, MaxWidth: 120,
+ Confidence: forms.ConfidenceLow,
+ Note: "Amount column for creditor row 1. ESTIMATE: tune against proof raster.",
+ },
+
+ // --- Page 3: first heir/legatee row + award computation ---
+ {
+ Key: "heir1_name", Label: "First heir/legatee — name (paras 9–10, row 1)",
+ Kind: forms.FieldText, Page: 3,
+ X: 70, Y: 520.0, MaxWidth: 220,
+ Confidence: forms.ConfidenceLow,
+ Note: "Row 1 of the heirs/legatees schedule. Multi-heir estates need the repeating-row renderer. ESTIMATE: tune against proof raster.",
+ },
+ {
+ Key: "heir1_relationship", Label: "First heir/legatee — relationship to decedent (paras 9–10, row 1)",
+ Kind: forms.FieldText, Page: 3,
+ X: 300, Y: 520.0, MaxWidth: 130,
+ Confidence: forms.ConfidenceLow,
+ Note: "Relationship column for heir row 1. ESTIMATE: tune against proof raster.",
+ },
+ {
+ Key: "heir1_share", Label: "First heir/legatee — share/interest (paras 9–10, row 1)",
+ Kind: forms.FieldText, Page: 3,
+ X: 440, Y: 520.0, MaxWidth: 90,
+ Confidence: forms.ConfidenceLow,
+ Note: "Share column for heir row 1 (e.g. \"100%\", \"1/2\"). ESTIMATE: tune against proof raster.",
+ },
+ {
+ Key: "award_amount", Label: "Spousal/child award amount (computed, single-claimant)",
+ Kind: forms.FieldText, Page: 3,
+ X: 360, Y: 300.0, MaxWidth: 140,
+ Confidence: forms.ConfidenceLow,
+ Note: "Surviving-spouse or child award line. Computation supplied by caller; engine never derives it. ESTIMATE: tune against proof raster.",
+ },
+
// --- Page 4: affiant relationship + execution block ---
{
Key: "affiant_relationship", Label: "Affiant relationship to decedent (para 10.3)",
diff --git a/api/internal/googlephotos/handler.go b/api/internal/googlephotos/handler.go
index 5448682..6193d84 100644
--- a/api/internal/googlephotos/handler.go
+++ b/api/internal/googlephotos/handler.go
@@ -14,7 +14,8 @@ import (
"cloud.google.com/go/storage"
"github.com/go-chi/chi/v5"
"github.com/rs/zerolog/log"
- "google.golang.org/api/iterator"
+ "google.golang.org/grpc/codes"
+ "google.golang.org/grpc/status"
"github.com/sirsi-technologies/finalwishes-api/internal/auth"
)
@@ -28,9 +29,26 @@ type Handler struct {
bucket string
}
+// defaultVaultBucket is the dev/default vault bucket name. It MUST stay in sync
+// with the default used by the rest of the vault/storage pipeline (vault,
+// docintell, certmail) so that imported Google Photos heirlooms land in the
+// exact same bucket the signed-URL download path later reads from. If a deploy
+// configures a non-default prod bucket, the caller (cmd/api) must pass it
+// explicitly — falling back to this literal under a non-default prod config
+// would orphan imported photos in a bucket nothing else reads.
+const defaultVaultBucket = "finalwishes-vault"
+
func NewHandler(fs *firestore.Client, sc *storage.Client, picker *PickerClient, bucket string) *Handler {
if bucket == "" {
- bucket = "finalwishes-vault"
+ // A blank bucket means the vault-bucket env var resolved to empty at
+ // startup. Surface it loudly: a silent literal substitution here is the
+ // exact mechanism by which imports get written to a bucket the download
+ // path never reads. The literal keeps dev working, but the warning makes
+ // a prod misconfiguration visible instead of invisibly orphaning photos.
+ log.Warn().
+ Str("fallback_bucket", defaultVaultBucket).
+ Msg("Google Photos handler received empty vault bucket; falling back to default — verify the vault-bucket env var matches the rest of the vault pipeline in prod")
+ bucket = defaultVaultBucket
}
return &Handler{fs: fs, sc: sc, picker: picker, bucket: bucket}
}
@@ -216,10 +234,11 @@ func (h *Handler) hasHash(ctx context.Context, estateID, sha string) (bool, erro
if err == nil {
return true, nil
}
- if iterator.Done == err {
- return false, nil
- }
- if strings.Contains(err.Error(), "NotFound") || strings.Contains(err.Error(), "not found") {
+ // A Firestore single-doc Get on a missing document returns a gRPC NotFound
+ // status — this is the canonical "no duplicate" signal (the iterator.Done
+ // sentinel only ever comes from iterator.Next(), never Doc().Get()). Match
+ // the repo convention used in payments/handlers.go (status.Code(err)==...).
+ if status.Code(err) == codes.NotFound {
return false, nil
}
// Any other error (transient, permission, config) is NOT "no duplicate" —
diff --git a/api/internal/opensign/handler.go b/api/internal/opensign/handler.go
index dfd98da..79bbf1e 100644
--- a/api/internal/opensign/handler.go
+++ b/api/internal/opensign/handler.go
@@ -68,22 +68,21 @@ func (h *WebhookHandler) HandleCreateEnvelope(w http.ResponseWriter, r *http.Req
}
}
- // Force the signer identity to the AUTHENTICATED caller — never the body's
- // signerEmail. Otherwise an estate writer could name an ARBITRARY signer (send the
- // signing link to anyone / forge "X signed this directive"). The verified email
- // claim is the only trustworthy signer identity. (claude-home PR #4 review.)
- signerEmail := ""
- signerName := req.SignerName
- if tok := auth.TokenFromContext(ctx); tok != nil {
- if e, _ := tok.Claims["email"].(string); e != "" {
- signerEmail = e
- }
- if n, _ := tok.Claims["name"].(string); n != "" {
- signerName = n
- }
+ // Resolve the signer as the estate PRINCIPAL — NOT the authenticated caller.
+ // A legal directive / POA must be signed BY the principal whose estate it governs;
+ // an executor or admin merely INITIATES the ceremony on the principal's behalf. So
+ // the OpenSign signer identity is resolved server-side from estates/{id}.principalId
+ // → Firebase Auth (verified email/name), and the principal's email MUST be verified
+ // before signing can proceed. The caller is recorded only as initiatedBy in the
+ // audit record below. This supersedes the prior "signer = authenticated caller"
+ // behavior. (claude-home signer=principal decision 2026-06-14; Refs ADR-047.)
+ resolver := h.signerResolver
+ if resolver == nil {
+ resolver = &firebaseSignerResolver{fs: h.fs, authClient: h.authClient}
}
- if signerEmail == "" {
- http.Error(w, "Signer email could not be determined from your account", http.StatusBadRequest)
+ signerEmail, signerName, status, clientMsg := resolver.resolveSigner(ctx, req.EstateID, req.SignerName)
+ if status != 0 {
+ http.Error(w, clientMsg, status)
return
}
@@ -124,7 +123,12 @@ func (h *WebhookHandler) HandleCreateEnvelope(w http.ResponseWriter, r *http.Req
"envelopeId": envelopeID,
"estateId": req.EstateID,
"directiveId": req.DirectiveID,
+ // createdBy kept for back-compat; initiatedBy is the authoritative audit
+ // field — the caller INITIATED the ceremony while the estate PRINCIPAL is
+ // the signer (claude-home signer=principal decision 2026-06-14).
"createdBy": userID,
+ "initiatedBy": userID,
+ "signerEmail": signerEmail,
"status": "sent",
"createdAt": firestore.ServerTimestamp,
}); e != nil {
diff --git a/api/internal/opensign/handler_test.go b/api/internal/opensign/handler_test.go
index c992158..122005a 100644
--- a/api/internal/opensign/handler_test.go
+++ b/api/internal/opensign/handler_test.go
@@ -1,6 +1,7 @@
package opensign
import (
+ "context"
"encoding/json"
"io"
"net/http"
@@ -13,11 +14,13 @@ import (
"github.com/sirsi-technologies/finalwishes-api/internal/auth"
)
-// withAuth injects an authenticated user context + token (as the middleware would) so
-// these tests exercise HandleCreateEnvelope past its auth gate and the token-derived
-// signer identity. A nil-fs WebhookHandler skips the estate_users check + the binding
-// write, leaving the OpenSign proxy behavior under test. Bodies include
-// estateId/directiveId (now required).
+// withAuth injects an authenticated CALLER context (as the middleware would) so these
+// tests exercise HandleCreateEnvelope past its auth gate. NOTE: as of the
+// signer=principal change (claude-home 2026-06-14) the caller's token email is NO
+// LONGER the signer — the signer is resolved from the estate principal via the injected
+// signerResolver. The caller is recorded only as initiatedBy. A nil-fs WebhookHandler
+// skips the estate_users check + the binding write, leaving the OpenSign proxy behavior
+// + principal-resolution under test. Bodies include estateId/directiveId (required).
func withAuth(req *http.Request) *http.Request {
ctx := auth.ContextWithUserID(req.Context(), "u1")
ctx = auth.ContextWithToken(ctx, &firebaseAuth.Token{Claims: map[string]interface{}{
@@ -27,7 +30,35 @@ func withAuth(req *http.Request) *http.Request {
return req.WithContext(ctx)
}
-func openSignTestHandler() *WebhookHandler { return &WebhookHandler{} }
+// fakeSignerResolver is the test double for the principal-resolution seam, standing in
+// for the Firestore+Firebase-Auth-backed firebaseSignerResolver (neither can be built
+// offline). It returns canned signer identity / rejection.
+type fakeSignerResolver struct {
+ email string
+ name string
+ status int
+ clientMsg string
+}
+
+func (f *fakeSignerResolver) resolveSigner(_ context.Context, _, fallbackName string) (string, string, int, string) {
+ if f.status != 0 {
+ return "", "", f.status, f.clientMsg
+ }
+ name := f.name
+ if name == "" {
+ name = fallbackName
+ }
+ return f.email, name, 0, ""
+}
+
+// openSignTestHandler returns a handler whose principal resolves to a VERIFIED estate
+// principal (principal@example.com) — the common case for the provider-proxy tests,
+// which assert behavior AFTER signer resolution succeeds.
+func openSignTestHandler() *WebhookHandler {
+ return &WebhookHandler{
+ signerResolver: &fakeSignerResolver{email: "principal@example.com", name: "Estate Principal"},
+ }
+}
// TestCreateEnvelope_InvalidJSON verifies that malformed request bodies are rejected.
func TestCreateEnvelope_InvalidJSON(t *testing.T) {
@@ -115,6 +146,18 @@ func TestCreateEnvelope_HappyPath(t *testing.T) {
if payload["template_id"] != "t1" {
t.Errorf("expected template_id t1, got %v", payload["template_id"])
}
+ // Signer is the estate PRINCIPAL resolved server-side — NOT the caller token
+ // (u1@example.com) and NOT the body's signerEmail (cylton@sirsi.ai). The
+ // dissociated provider nests the signer under signers[0].
+ gotSigner := ""
+ if signers, ok := payload["signers"].([]interface{}); ok && len(signers) > 0 {
+ if s0, ok := signers[0].(map[string]interface{}); ok {
+ gotSigner, _ = s0["email"].(string)
+ }
+ }
+ if gotSigner != "principal@example.com" {
+ t.Errorf("expected signer to be the estate principal, got %q", gotSigner)
+ }
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
@@ -277,3 +320,102 @@ func TestCreateEnvelope_NoAPIKeyStillSends(t *testing.T) {
t.Errorf("expected no auth header when API key is empty, got %q", receivedAuthHeader)
}
}
+
+// TestCreateEnvelope_SignerIsVerifiedPrincipal verifies the resolved signer is the
+// estate principal (verified), not the caller and not the body's signerEmail.
+func TestCreateEnvelope_SignerIsVerifiedPrincipal(t *testing.T) {
+ var gotSignerEmail string
+ upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ var payload map[string]interface{}
+ _ = json.NewDecoder(r.Body).Decode(&payload)
+ if signers, ok := payload["signers"].([]interface{}); ok && len(signers) > 0 {
+ if s0, ok := signers[0].(map[string]interface{}); ok {
+ gotSignerEmail, _ = s0["email"].(string)
+ }
+ }
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ _, _ = io.WriteString(w, `{"id":"env-1","url":"https://sign.example.com/env-1"}`)
+ }))
+ defer upstream.Close()
+
+ t.Setenv("OPENSIGN_CREATE_ENVELOPE_URL", upstream.URL)
+ t.Setenv("OPENSIGN_API_KEY", "k")
+ t.Setenv("OPENSIGN_API_URL", "")
+
+ h := &WebhookHandler{
+ signerResolver: &fakeSignerResolver{email: "principal@example.com", name: "Estate Principal"},
+ }
+
+ // Caller token says u1@example.com; body says imposter@evil.com — both must be ignored.
+ body := `{"estateId":"e1","directiveId":"d1","templateId":"t1","signerName":"Imposter","signerEmail":"imposter@evil.com","redirectUrl":"https://x.com"}`
+ req := httptest.NewRequest(http.MethodPost, "/api/envelopes", strings.NewReader(body))
+ req.Header.Set("Content-Type", "application/json")
+ rr := httptest.NewRecorder()
+ h.HandleCreateEnvelope(rr, withAuth(req))
+
+ if rr.Code != http.StatusOK {
+ t.Fatalf("expected 200, got %d (%s)", rr.Code, rr.Body.String())
+ }
+ if gotSignerEmail != "principal@example.com" {
+ t.Errorf("signer must be the verified estate principal, got %q", gotSignerEmail)
+ }
+}
+
+// TestCreateEnvelope_PrincipalEmailUnverified verifies the required gate: signing is
+// rejected with 403 when the estate principal's email is not verified.
+func TestCreateEnvelope_PrincipalEmailUnverified(t *testing.T) {
+ h := &WebhookHandler{
+ signerResolver: &fakeSignerResolver{
+ status: http.StatusForbidden,
+ clientMsg: "The estate principal's email is not verified; signing cannot proceed",
+ },
+ }
+
+ body := `{"estateId":"e1","directiveId":"d1","templateId":"t1","redirectUrl":"https://x.com"}`
+ req := httptest.NewRequest(http.MethodPost, "/api/envelopes", strings.NewReader(body))
+ req.Header.Set("Content-Type", "application/json")
+ rr := httptest.NewRecorder()
+ h.HandleCreateEnvelope(rr, withAuth(req))
+
+ if rr.Code != http.StatusForbidden {
+ t.Fatalf("expected 403 for unverified principal, got %d", rr.Code)
+ }
+ if !strings.Contains(rr.Body.String(), "not verified") {
+ t.Errorf("expected unverified-principal message, got %q", rr.Body.String())
+ }
+}
+
+// TestCreateEnvelope_MissingPrincipal verifies a 400 when the estate has no principal
+// on record (e.g. estates/{id}.principalId is empty/missing).
+func TestCreateEnvelope_MissingPrincipal(t *testing.T) {
+ h := &WebhookHandler{
+ signerResolver: &fakeSignerResolver{
+ status: http.StatusBadRequest,
+ clientMsg: "Estate has no principal on record",
+ },
+ }
+
+ body := `{"estateId":"e1","directiveId":"d1","templateId":"t1","redirectUrl":"https://x.com"}`
+ req := httptest.NewRequest(http.MethodPost, "/api/envelopes", strings.NewReader(body))
+ req.Header.Set("Content-Type", "application/json")
+ rr := httptest.NewRecorder()
+ h.HandleCreateEnvelope(rr, withAuth(req))
+
+ if rr.Code != http.StatusBadRequest {
+ t.Fatalf("expected 400 for missing principal, got %d", rr.Code)
+ }
+ if !strings.Contains(rr.Body.String(), "no principal on record") {
+ t.Errorf("expected missing-principal message, got %q", rr.Body.String())
+ }
+}
+
+// TestFirebaseSignerResolver_UnverifiedAndMissing covers the production resolver's
+// pure-logic branches that don't need a live Firestore client: nil fs / nil auth.
+func TestFirebaseSignerResolver_DefensiveNils(t *testing.T) {
+ r := &firebaseSignerResolver{} // nil fs, nil authClient
+ _, _, status, msg := r.resolveSigner(context.Background(), "e1", "")
+ if status != http.StatusServiceUnavailable {
+ t.Errorf("nil fs should yield 503, got %d (%s)", status, msg)
+ }
+}
diff --git a/api/internal/opensign/signer.go b/api/internal/opensign/signer.go
new file mode 100644
index 0000000..6f1e696
--- /dev/null
+++ b/api/internal/opensign/signer.go
@@ -0,0 +1,77 @@
+package opensign
+
+import (
+ "context"
+ "net/http"
+
+ "cloud.google.com/go/firestore"
+ "github.com/rs/zerolog/log"
+)
+
+// signerResolver resolves the legally-required signer identity (the estate PRINCIPAL)
+// for an envelope. A non-zero httpStatus signals rejection and clientMsg is the
+// safe-to-surface error; httpStatus == 0 means email/name are populated and verified.
+//
+// WHY this is a seam: the signer of a legal directive/POA must be the estate principal,
+// never the authenticated caller (an executor/admin only initiates). Resolution needs a
+// live Firestore client (estates/{id}.principalId) + Firebase Auth (verified email),
+// neither of which can be constructed offline — so tests inject a fake resolver while
+// production uses firebaseSignerResolver. (claude-home signer=principal decision
+// 2026-06-14; Refs ADR-047.)
+type signerResolver interface {
+ resolveSigner(ctx context.Context, estateID, fallbackName string) (email, name string, httpStatus int, clientMsg string)
+}
+
+// firebaseSignerResolver resolves the principal from estates/{id}.principalId via
+// Firestore, then loads the principal's verified identity from Firebase Auth.
+type firebaseSignerResolver struct {
+ fs *firestore.Client
+ authClient userLookup
+}
+
+func (r *firebaseSignerResolver) resolveSigner(ctx context.Context, estateID, fallbackName string) (string, string, int, string) {
+ // The estate record holds principalId (the principal's Firebase UID) — written at
+ // estate creation (web/src/lib/estate-actions.ts createEstate).
+ if r.fs == nil {
+ return "", "", http.StatusServiceUnavailable, "Signer identity service unavailable"
+ }
+ estateSnap, err := r.fs.Collection("estates").Doc(estateID).Get(ctx)
+ if err != nil || estateSnap == nil || !estateSnap.Exists() {
+ return "", "", http.StatusBadRequest, "Estate has no principal on record"
+ }
+ principalID, _ := estateSnap.Data()["principalId"].(string)
+ if principalID == "" {
+ return "", "", http.StatusBadRequest, "Estate has no principal on record"
+ }
+
+ // Defensive: in production the auth client is always wired; locally it may be nil.
+ if r.authClient == nil {
+ return "", "", http.StatusServiceUnavailable, "Signer identity service unavailable"
+ }
+
+ principal, err := r.authClient.GetUser(ctx, principalID)
+ if err != nil || principal == nil {
+ log.Error().Err(err).Str("estate_id", estateID).Msg("Could not resolve estate principal from Firebase Auth")
+ return "", "", http.StatusBadGateway, "Could not resolve estate principal"
+ }
+
+ // The required gate: a directive cannot be signed against an unverified principal
+ // email — that would let an unverified/unowned address be named as the legal signer.
+ if !principal.EmailVerified {
+ return "", "", http.StatusForbidden, "The estate principal's email is not verified; signing cannot proceed"
+ }
+
+ signerEmail := principal.Email
+ if signerEmail == "" {
+ return "", "", http.StatusBadRequest, "The estate principal has no email on record"
+ }
+
+ signerName := principal.DisplayName
+ if signerName == "" {
+ signerName = fallbackName
+ }
+ if signerName == "" {
+ signerName = signerEmail
+ }
+ return signerEmail, signerName, 0, ""
+}
diff --git a/api/internal/opensign/webhook.go b/api/internal/opensign/webhook.go
index 1d81ef4..c1fd7c8 100644
--- a/api/internal/opensign/webhook.go
+++ b/api/internal/opensign/webhook.go
@@ -13,27 +13,51 @@ import (
"time"
"cloud.google.com/go/firestore"
+ firebaseAuth "firebase.google.com/go/v4/auth"
"github.com/rs/zerolog/log"
"github.com/sirsi-technologies/finalwishes-api/internal/auth"
)
+// userLookup is the narrow slice of the Firebase Auth client that envelope creation
+// needs to resolve the estate principal's verified identity. The real
+// *firebaseAuth.Client satisfies it; tests inject a fake (the concrete client cannot
+// be constructed offline). This is the auth-client test seam.
+type userLookup interface {
+ GetUser(ctx context.Context, uid string) (*firebaseAuth.UserRecord, error)
+}
+
// WebhookHandler handles OpenSign webhook callbacks, signing status checks, and
// envelope creation (via the shared-services provider).
type WebhookHandler struct {
fs *firestore.Client
+ authClient userLookup
webhookSecret string
provider SigningProvider
+
+ // signerResolver lets tests inject a deterministic principal-resolution result
+ // without a live Firestore/Firebase Auth client. nil in production → the handler
+ // uses the default resolver backed by fs + authClient.
+ signerResolver signerResolver
}
// NewWebhookHandler creates a webhook handler for OpenSign signing events. It builds
// the shared-services signing provider (Sirsi-first, dissociated fallback — ADR-047).
-func NewWebhookHandler(fs *firestore.Client) *WebhookHandler {
- return &WebhookHandler{
+// authClient resolves the estate PRINCIPAL's verified identity at envelope creation
+// (the legal signer is the principal, never the caller — claude-home signer=principal
+// decision 2026-06-14). It may be nil in local dev with no Firebase Auth.
+func NewWebhookHandler(fs *firestore.Client, authClient *firebaseAuth.Client) *WebhookHandler {
+ h := &WebhookHandler{
fs: fs,
- provider: NewSigningProvider(),
webhookSecret: os.Getenv("OPENSIGN_WEBHOOK_SECRET"),
+ provider: NewSigningProvider(),
+ }
+ // Keep authClient typed-nil safe: only assign when non-nil so the interface field
+ // stays a true nil (a typed-nil *Client would defeat the == nil defensive check).
+ if authClient != nil {
+ h.authClient = authClient
}
+ return h
}
// HandleWebhook processes OpenSign signing completion webhooks.
diff --git a/api/internal/payments/handlers.go b/api/internal/payments/handlers.go
index 48b7ebd..f6f0fd6 100644
--- a/api/internal/payments/handlers.go
+++ b/api/internal/payments/handlers.go
@@ -7,10 +7,12 @@ package payments
import (
"context"
"encoding/json"
+ "errors"
"fmt"
"io"
"net/http"
"os"
+ "strings"
"time"
"github.com/rs/zerolog/log"
@@ -365,6 +367,16 @@ func (h *Handler) HandleCreatePortalSession(w http.ResponseWriter, r *http.Reque
sess, err := portalsession.New(params)
if err != nil {
log.Error().Err(err).Str("customer_id", customerIDStr).Str("estate_id", req.EstateID).Msg("Stripe portal session creation failed")
+ // The most common cause of a portal-session failure on a live account is the
+ // one-time Customer Portal configuration not yet being saved in the Stripe
+ // Dashboard (Settings > Billing > Customer Portal). Stripe surfaces this as an
+ // invalid_request_error mentioning "configuration". Give the user an actionable
+ // message in that case instead of a bare 500.
+ if isStripePortalNotConfigured(err) {
+ writeError(w, http.StatusServiceUnavailable,
+ "Subscription management isn't available yet. Please contact support and we'll help you update your plan.")
+ return
+ }
writeError(w, http.StatusInternalServerError, "Failed to create subscription management session")
return
}
@@ -376,6 +388,24 @@ func (h *Handler) HandleCreatePortalSession(w http.ResponseWriter, r *http.Reque
})
}
+// isStripePortalNotConfigured reports whether a portal-session error is the
+// "Customer Portal not configured in the Dashboard" case. Stripe returns this as an
+// invalid_request_error whose message mentions a missing/default configuration
+// (e.g. "No configuration provided and your test mode default configuration has not
+// been created"). We match defensively on both the structured Stripe error and its
+// message so a Dashboard-config gap reads as a clear "unavailable" rather than a 500.
+func isStripePortalNotConfigured(err error) bool {
+ var serr *stripe.Error
+ if !errors.As(err, &serr) {
+ return false
+ }
+ if serr.Type != stripe.ErrorTypeInvalidRequest {
+ return false
+ }
+ msg := strings.ToLower(serr.Msg)
+ return strings.Contains(msg, "configuration") || strings.Contains(msg, "customer portal")
+}
+
// HandleWebhook processes Stripe webhook events.
func (h *Handler) HandleWebhook(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(io.LimitReader(r.Body, 65536))
diff --git a/api/internal/probate/forms.go b/api/internal/probate/forms.go
index f47f58c..fbd09c4 100644
--- a/api/internal/probate/forms.go
+++ b/api/internal/probate/forms.go
@@ -2,12 +2,13 @@ package probate
import (
"context"
- "encoding/json"
"net/http"
+ "strconv"
+ "strings"
"time"
+ "unicode"
"cloud.google.com/go/firestore"
- "github.com/rs/zerolog/log"
"github.com/sirsi-technologies/finalwishes-api/internal/auth"
)
@@ -87,11 +88,19 @@ func (h *Handler) HandleGetFormTemplates(w http.ResponseWriter, r *http.Request)
executorAddress, _ = exData["address"].(string)
}
- // Compute total asset value for small estate evaluation
- var totalAssetValue string
+ // Compute total asset values for the inventory and small-estate templates.
+ //
+ // Rule 9: these strings are stamped into legal-form prefill fields, and the
+ // personal-property total gates small-estate qualification. We therefore
+ // derive a REAL dollar figure from the recorded asset values — never a proxy
+ // like a count. If any recorded value cannot be parsed into a number, the
+ // derived total would be misleading, so we leave the field BLANK for the
+ // affiant to complete by hand rather than assert an unverifiable amount.
+ var totalAssetValue, personalPropertyValue string
assetSnaps, err := h.fs.Collection("estates").Doc(estateID).Collection("assets").Documents(ctx).GetAll()
if err == nil {
- totalAssetValue = computeTotalAssets(assetSnaps)
+ totalAssetValue = computeTotalAssets(assetSnaps, false)
+ personalPropertyValue = computeTotalAssets(assetSnaps, true)
}
county := countyOfDeath
@@ -149,7 +158,7 @@ func (h *Handler) HandleGetFormTemplates(w http.ResponseWriter, r *http.Request)
"dateOfDeath": dateOfDeath,
"affiantName": executorName,
"affiantAddress": executorAddress,
- "totalPersonalProperty": totalAssetValue,
+ "totalPersonalProperty": personalPropertyValue,
"threshold": "$150,000",
"waitingPeriod": "30 days after date of death",
"vehiclesExcluded": "Yes — vehicles do not count toward the $150,000 limit",
@@ -186,50 +195,106 @@ func (h *Handler) HandleGetFormTemplates(w http.ResponseWriter, r *http.Request)
})
}
-// computeTotalAssets sums all asset values from Firestore snapshots.
-func computeTotalAssets(snaps []*firestore.DocumentSnapshot) string {
- // For now, return the count — proper valuation requires parsing value strings
- if len(snaps) == 0 {
- return "$0"
- }
- return jsonString(len(snaps)) + " assets recorded"
-}
+// realEstateTypes are asset `type` values that represent real property, which
+// is excluded from the Illinois small-estate personal-property threshold
+// (755 ILCS 5/25-1). Matching is case-insensitive and substring-based so that
+// "Real Estate", "real property", and "Residential Real Estate" all qualify.
+var realEstateTypes = []string{"real estate", "real property", "realty", "land"}
-func jsonString(v int) string {
- b, _ := json.Marshal(v)
- return string(b)
+func isRealProperty(assetType string) bool {
+ t := strings.ToLower(strings.TrimSpace(assetType))
+ for _, re := range realEstateTypes {
+ if strings.Contains(t, re) {
+ return true
+ }
+ }
+ return false
}
-// HandleGetFormData returns pre-filled data for a specific form.
-// GET /api/v1/probate/forms/:formId?estate_id=xxx
-func (h *Handler) HandleGetFormData(w http.ResponseWriter, r *http.Request) {
- userID, err := auth.RequireUserID(r.Context())
+// parseCurrency converts a recorded asset value string (e.g. "$485,000",
+// "124500.50", "1,200") into a numeric dollar amount. It returns ok=false when
+// the string contains no parseable amount, so callers can fail closed (leave a
+// legal-form field blank) rather than assert a fabricated figure (Rule 9).
+func parseCurrency(s string) (float64, bool) {
+ // Strip currency symbols, thousands separators, and any whitespace
+ // (including non-breaking/thin spaces some locales use as group separators).
+ cleaned := strings.Map(func(r rune) rune {
+ if r == '$' || r == ',' || unicode.IsSpace(r) {
+ return -1
+ }
+ return r
+ }, s)
+ if cleaned == "" {
+ return 0, false
+ }
+ v, err := strconv.ParseFloat(cleaned, 64)
if err != nil {
- writeError(w, http.StatusUnauthorized, "Authentication required")
- return
+ return 0, false
}
+ return v, true
+}
- estateID := r.URL.Query().Get("estate_id")
- formID := r.URL.Query().Get("form_id")
- if estateID == "" || formID == "" {
- writeError(w, http.StatusBadRequest, "estate_id and form_id query parameters are required")
- return
+// formatCurrency renders a dollar amount with thousands separators, e.g.
+// 609500 -> "$609,500". Whole amounts drop the cents; fractional amounts keep
+// two decimals.
+func formatCurrency(v float64) string {
+ negative := v < 0
+ if negative {
+ v = -v
}
+ whole := int64(v)
+ frac := v - float64(whole)
- ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
- defer cancel()
+ digits := strconv.FormatInt(whole, 10)
+ var grouped strings.Builder
+ for i, d := range digits {
+ if i > 0 && (len(digits)-i)%3 == 0 {
+ grouped.WriteByte(',')
+ }
+ grouped.WriteRune(d)
+ }
- if err := h.verifyEstateAccess(ctx, userID, estateID); err != nil {
- writeError(w, http.StatusForbidden, "You do not have access to this estate")
- return
+ out := "$" + grouped.String()
+ if frac > 0.0049 {
+ out += strings.TrimPrefix(strconv.FormatFloat(frac, 'f', 2, 64), "0")
+ }
+ if negative {
+ out = "-" + out
}
+ return out
+}
- // For now, reuse HandleGetFormTemplates logic and filter by ID
- // In a full implementation, this would return richer per-form data
- log.Debug().Str("estate_id", estateID).Str("form_id", formID).Msg("Form data requested")
- writeJSON(w, http.StatusOK, map[string]interface{}{
- "formId": formID,
- "disclaimer": formDisclaimer,
- "message": "Use the /forms endpoint for pre-filled templates",
- })
+// computeTotalAssets derives a real dollar valuation from recorded asset values.
+//
+// When personalPropertyOnly is true, real-property assets are excluded so the
+// result matches the Illinois small-estate personal-property threshold
+// (755 ILCS 5/25-1). When false, every asset is summed for the general estate
+// inventory.
+//
+// Rule 9: if ANY contributing asset has a value that cannot be parsed into a
+// number, the sum would be unverifiable, so we return "" — the form field is
+// left blank for the affiant to complete rather than stamped with a guess. An
+// estate with zero contributing assets returns "$0" (a fact, not a guess).
+func computeTotalAssets(snaps []*firestore.DocumentSnapshot, personalPropertyOnly bool) string {
+ var total float64
+ var contributing int
+ for _, snap := range snaps {
+ data := snap.Data()
+ assetType, _ := data["type"].(string)
+ if personalPropertyOnly && isRealProperty(assetType) {
+ continue
+ }
+ raw, _ := data["value"].(string)
+ amount, ok := parseCurrency(raw)
+ if !ok {
+ // Unverifiable total — refuse to fabricate a legal figure (Rule 9).
+ return ""
+ }
+ total += amount
+ contributing++
+ }
+ if contributing == 0 {
+ return "$0"
+ }
+ return formatCurrency(total)
}
diff --git a/api/internal/service/estate/service.go b/api/internal/service/estate/service.go
index 85b9662..7057ae8 100644
--- a/api/internal/service/estate/service.go
+++ b/api/internal/service/estate/service.go
@@ -11,22 +11,40 @@ import (
"cloud.google.com/go/firestore"
"cloud.google.com/go/storage"
"connectrpc.com/connect"
- "google.golang.org/api/iterator"
- "google.golang.org/protobuf/types/known/timestamppb"
"github.com/sirsi-technologies/finalwishes-api/internal/auth"
estatev1 "github.com/sirsi-technologies/finalwishes-api/internal/gen/estate/v1"
"github.com/sirsi-technologies/finalwishes-api/internal/gen/estate/v1/estatev1connect"
)
-// Server implements the EstateService with Google Cloud integrations
+// Server implements the EstateService with Google Cloud integrations.
+//
+// SURFACE SCOPE (2026-06-14): EstateService intentionally implements ONLY the
+// RPCs the product actually consumes. Today that is GenerateUploadUrl (the
+// signed-URL issuer used by every vault/memoir/obituary/time-capsule uploader —
+// see web/src/lib/client.ts). The estate read/write surface (ListEstates,
+// GetEstateMetadata, ListAssets, AddAsset, ListBeneficiaries, AddBeneficiary,
+// ListVaultDocuments, ListMemoirs, UploadMemoir, GetObituary, SaveObituary,
+// GetAIInsight, GetGovernanceSettings, ListNotifications, RegisterEstate) is
+// served directly from Firestore by the React app, and AI guidance is served by
+// internal/guidance (HandleGetScore -> advisor.GenerateInsight, Claude Opus +
+// RAG). Those proto RPCs therefore have ZERO consumers.
+//
+// Rather than ship handlers that fabricated "34% complete" / "Lockhart Estate" /
+// "Sarah Johnson" demo data — which masked real Firestore failures on the live
+// path and shipped invented AI text — we deliberately do NOT override them here.
+// The embedded UnimplementedEstateServiceHandler answers each unimplemented RPC
+// with a clean connect.CodeUnimplemented ("... is not implemented"), an honest
+// signal instead of fabricated data. When/if any of these become the canonical
+// path, implement the real Firestore-backed handler here (gated by
+// checkEstateAccess) and remove the corresponding direct-Firestore read in web/.
type Server struct {
estatev1connect.UnimplementedEstateServiceHandler
fs *firestore.Client
sc *storage.Client
}
-// NewServer returns an initialized server
+// NewServer returns an initialized server.
func NewServer(fs *firestore.Client, sc *storage.Client) *Server {
return &Server{
fs: fs,
@@ -42,7 +60,9 @@ func (s *Server) checkEstateAccess(ctx context.Context, estateID string, writeRe
return connect.NewError(connect.CodeUnauthenticated, fmt.Errorf("authentication required"))
}
if s.fs == nil {
- return nil
+ // Without an estate store we cannot prove membership; deny rather than
+ // fall through to an unauthenticated grant.
+ return connect.NewError(connect.CodeFailedPrecondition, fmt.Errorf("estate store not configured"))
}
docRef := s.fs.Collection("estate_users").Doc(userID + "_" + estateID)
doc, err := docRef.Get(ctx)
@@ -60,6 +80,11 @@ func (s *Server) checkEstateAccess(ctx context.Context, estateID string, writeRe
}
// --- Vault & Storage (Signed URLs) ---
+//
+// GenerateUploadUrl is the only EstateService RPC the product consumes. It
+// issues a short-lived V4 signed PUT URL (and a longer-lived GET URL) for a
+// vault object, with a server-enforced byte cap, after verifying the caller has
+// write access to the estate.
func (s *Server) GenerateUploadUrl(ctx context.Context, req *connect.Request[estatev1.GenerateUploadUrlRequest]) (*connect.Response[estatev1.GenerateUploadUrlResponse], error) {
if err := s.checkEstateAccess(ctx, req.Msg.EstateId, true); err != nil {
@@ -113,525 +138,3 @@ func (s *Server) GenerateUploadUrl(ctx context.Context, req *connect.Request[est
FinalUrl: downloadURL,
}), nil
}
-
-// --- Core Estate Management (Firestore) ---
-
-func (s *Server) ListBeneficiaries(ctx context.Context, req *connect.Request[estatev1.ListBeneficiariesRequest]) (*connect.Response[estatev1.ListBeneficiariesResponse], error) {
- if err := s.checkEstateAccess(ctx, req.Msg.EstateId, false); err != nil {
- return nil, err
- }
-
- if s.fs == nil {
- return connect.NewResponse(&estatev1.ListBeneficiariesResponse{
- Beneficiaries: []*estatev1.Beneficiary{
- {Id: "1", Name: "Sarah Johnson", Relation: "Spouse", Share: "50%", Status: "Verified", Email: "sarah@example.com"},
- },
- }), nil
- }
-
- var beneficiaries []*estatev1.Beneficiary
- iter := s.fs.Collection("estates").Doc(req.Msg.EstateId).Collection("beneficiaries").Documents(ctx)
- for {
- doc, err := iter.Next()
- if err == iterator.Done {
- break
- }
- if err != nil {
- return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("failed to fetch beneficiaries: %w", err))
- }
-
- var b estatev1.Beneficiary
- if err := doc.DataTo(&b); err != nil {
- continue
- }
- b.Id = doc.Ref.ID
- beneficiaries = append(beneficiaries, &b)
- }
-
- return connect.NewResponse(&estatev1.ListBeneficiariesResponse{Beneficiaries: beneficiaries}), nil
-}
-
-func (s *Server) AddBeneficiary(ctx context.Context, req *connect.Request[estatev1.AddBeneficiaryRequest]) (*connect.Response[estatev1.AddBeneficiaryResponse], error) {
- if err := s.checkEstateAccess(ctx, req.Msg.EstateId, true); err != nil {
- return nil, err
- }
-
- if s.fs == nil {
- return nil, connect.NewError(connect.CodeUnimplemented, fmt.Errorf("firestore not initialized"))
- }
-
- newBene := &estatev1.Beneficiary{
- Name: req.Msg.Name,
- Relation: req.Msg.Relation,
- Email: req.Msg.Email,
- Share: "TBD",
- Status: "Invited",
- }
-
- docRef, _, err := s.fs.Collection("estates").Doc(req.Msg.EstateId).Collection("beneficiaries").Add(ctx, map[string]interface{}{
- "name": newBene.Name,
- "relation": newBene.Relation,
- "email": newBene.Email,
- "share": newBene.Share,
- "status": newBene.Status,
- })
- if err != nil {
- return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("failed to add beneficiary: %w", err))
- }
-
- newBene.Id = docRef.ID
- return connect.NewResponse(&estatev1.AddBeneficiaryResponse{Beneficiary: newBene}), nil
-}
-
-func (s *Server) ListEstates(ctx context.Context, req *connect.Request[estatev1.ListEstatesRequest]) (*connect.Response[estatev1.ListEstatesResponse], error) {
- if s.fs == nil {
- var estates []*estatev1.EstateSummary
- if req.Msg.UserId == "user_tameeka" || req.Msg.UserId == "Tameeka116" {
- estates = []*estatev1.EstateSummary{
- {Id: "estate_lockhart", Name: "Lockhart Estate", Role: "Owner"},
- {Id: "trust_lockhart", Name: "Lockhart Dynasty Trust", Role: "Executor"},
- }
- } else {
- estates = []*estatev1.EstateSummary{
- {Id: "estate-77b", Name: "The Lockhart Estate Shard", Role: "Owner"},
- {Id: "estate-99c", Name: "The Lockhart Global Trust", Role: "Executor"},
- }
- }
- return connect.NewResponse(&estatev1.ListEstatesResponse{Estates: estates}), nil
- }
-
- // Derive the UID from the verified token, NOT req.Msg.UserId — trusting the
- // client-supplied user_id let any authenticated user enumerate ANOTHER user's
- // estate IDs (the keystone for reading their obituary/metadata/notifications by ID).
- userID := auth.UserIDFromContext(ctx)
- if userID == "" {
- return nil, connect.NewError(connect.CodeUnauthenticated, fmt.Errorf("authentication required"))
- }
-
- var estates []*estatev1.EstateSummary
- iter := s.fs.Collection("estates").Where("user_id", "==", userID).Documents(ctx)
- for {
- doc, err := iter.Next()
- if err == iterator.Done {
- break
- }
- if err != nil {
- return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("failed to list estates: %w", err))
- }
-
- var summary estatev1.EstateSummary
- if err := doc.DataTo(&summary); err != nil {
- continue
- }
- summary.Id = doc.Ref.ID
- estates = append(estates, &summary)
- }
-
- return connect.NewResponse(&estatev1.ListEstatesResponse{Estates: estates}), nil
-}
-
-func (s *Server) RegisterEstate(ctx context.Context, req *connect.Request[estatev1.RegisterEstateRequest]) (*connect.Response[estatev1.RegisterEstateResponse], error) {
- if s.fs == nil {
- return nil, connect.NewError(connect.CodeUnimplemented, fmt.Errorf("firestore not initialized"))
- }
-
- // Derive the UID from the verified token, NOT req.Msg.UserId — persisting a
- // client-supplied user_id lets an authenticated caller create an estate OWNED
- // BY ANOTHER USER (planting an estate into someone else's ListEstates, or
- // orphaning it under a fabricated id). Same trust boundary as ListEstates.
- userID := auth.UserIDFromContext(ctx)
- if userID == "" {
- return nil, connect.NewError(connect.CodeUnauthenticated, fmt.Errorf("authentication required"))
- }
-
- docRef, _, err := s.fs.Collection("estates").Add(ctx, map[string]interface{}{
- "user_id": userID,
- "name": req.Msg.Name,
- "type": req.Msg.Type,
- "role": "Owner", // Default for creator
- "status": "Active",
- "completion_percentage": 10,
- "authority_mode": int32(estatev1.AuthorityMode_AUTHORITY_MODE_OWNER),
- "next_review_date": time.Now().AddDate(0, 1, 0),
- })
- if err != nil {
- return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("failed to register estate: %w", err))
- }
-
- return connect.NewResponse(&estatev1.RegisterEstateResponse{
- Estate: &estatev1.EstateSummary{
- Id: docRef.ID,
- Name: req.Msg.Name,
- Role: "Owner",
- },
- }), nil
-}
-
-func (s *Server) GetEstateMetadata(ctx context.Context, req *connect.Request[estatev1.GetEstateMetadataRequest]) (*connect.Response[estatev1.GetEstateMetadataResponse], error) {
- // Ungated reads of estates/{id} leaked tier + MFA status + last-login for any
- // estate (targeting recon). Gate on estate membership.
- if err := s.checkEstateAccess(ctx, req.Msg.EstateId, false); err != nil {
- return nil, err
- }
- if s.fs == nil {
- name := "Lockhart Estate"
- completion := int32(100)
- return connect.NewResponse(&estatev1.GetEstateMetadataResponse{
- Id: req.Msg.EstateId,
- Name: name,
- Status: "Active",
- CompletionPercentage: completion,
- AuthorityMode: estatev1.AuthorityMode_AUTHORITY_MODE_OWNER,
- }), nil
- }
-
- doc, err := s.fs.Collection("estates").Doc(req.Msg.EstateId).Get(ctx)
- if err != nil {
- // Provide default if not in Firestore yet for demo
- return connect.NewResponse(&estatev1.GetEstateMetadataResponse{
- Status: "Active",
- CompletionPercentage: 34,
- AuthorityMode: estatev1.AuthorityMode_AUTHORITY_MODE_OWNER,
- }), nil
- }
-
- var meta estatev1.GetEstateMetadataResponse
- if err := doc.DataTo(&meta); err != nil {
- return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("failed to decode metadata: %w", err))
- }
-
- return connect.NewResponse(&meta), nil
-}
-
-func (s *Server) ListAssets(ctx context.Context, req *connect.Request[estatev1.ListAssetsRequest]) (*connect.Response[estatev1.ListAssetsResponse], error) {
- if err := s.checkEstateAccess(ctx, req.Msg.EstateId, false); err != nil {
- return nil, err
- }
-
- if s.fs == nil {
- var assets []*estatev1.Asset
- if req.Msg.EstateId == "estate_lockhart" {
- assets = []*estatev1.Asset{
- {Name: "Lockhart Family Residence (Chicago)", Type: "Real Estate", Value: "$1,250,000", Status: "Verified"},
- {Name: "Lockhart Heritage Investment Fund", Type: "Securities", Value: "$2,450,000", Status: "Verified"},
- {Name: "Chase Private Client - Savings", Type: "Cash", Value: "$425,000", Status: "Verified"},
- {Name: "Digital Vault - Rare Collectibles", Type: "Digital Assets", Value: "$85,000", Status: "Verified"},
- }
- } else {
- assets = []*estatev1.Asset{
- {Name: "Family Home", Type: "Real Estate", Value: "$485,000", Status: "Verified"},
- {Name: "Savings Account (Chase)", Type: "Cash", Value: "$124,500", Status: "Pending"},
- }
- }
- return connect.NewResponse(&estatev1.ListAssetsResponse{Assets: assets, TotalCount: int32(len(assets))}), nil
- }
-
- var assets []*estatev1.Asset
- iter := s.fs.Collection("estates").Doc(req.Msg.EstateId).Collection("assets").Documents(ctx)
- for {
- doc, err := iter.Next()
- if err == iterator.Done {
- break
- }
- if err != nil {
- return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("failed to fetch assets: %w", err))
- }
-
- var a estatev1.Asset
- if err := doc.DataTo(&a); err != nil {
- continue
- }
- assets = append(assets, &a)
- }
-
- return connect.NewResponse(&estatev1.ListAssetsResponse{Assets: assets, TotalCount: int32(len(assets))}), nil
-}
-
-func (s *Server) AddAsset(ctx context.Context, req *connect.Request[estatev1.AddAssetRequest]) (*connect.Response[estatev1.AddAssetResponse], error) {
- if err := s.checkEstateAccess(ctx, req.Msg.EstateId, true); err != nil {
- return nil, err
- }
-
- if s.fs == nil {
- return nil, connect.NewError(connect.CodeUnimplemented, fmt.Errorf("firestore not initialized"))
- }
-
- newAsset := &estatev1.Asset{
- Name: req.Msg.Name,
- Type: req.Msg.Type,
- Value: req.Msg.Value,
- Status: "Verified",
- }
-
- _, _, err := s.fs.Collection("estates").Doc(req.Msg.EstateId).Collection("assets").Add(ctx, map[string]interface{}{
- "name": newAsset.Name,
- "type": newAsset.Type,
- "value": newAsset.Value,
- "status": newAsset.Status,
- })
- if err != nil {
- return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("failed to add asset: %w", err))
- }
-
- return connect.NewResponse(&estatev1.AddAssetResponse{Asset: newAsset}), nil
-}
-
-func (s *Server) ListVaultDocuments(ctx context.Context, req *connect.Request[estatev1.ListVaultDocumentsRequest]) (*connect.Response[estatev1.ListVaultDocumentsResponse], error) {
- if err := s.checkEstateAccess(ctx, req.Msg.EstateId, false); err != nil {
- return nil, err
- }
-
- if s.fs == nil {
- var docs []*estatev1.VaultDocument
- if req.Msg.EstateId == "estate_lockhart" {
- docs = []*estatev1.VaultDocument{
- {Name: "Lockhart_Master_Trust_2026.pdf", Date: "Feb 10, 2026", Size: "4.2 MB", Category: "Legal"},
- {Name: "Estate_Tax_Clearance_IL.pdf", Date: "Mar 01, 2026", Size: "1.1 MB", Category: "Tax"},
- {Name: "Family_Heritage_Deed.pdf", Date: "Jan 15, 2026", Size: "8.5 MB", Category: "Property"},
- {Name: "Final_Wishes_Protocol_Auth.pdf", Date: "Mar 17, 2026", Size: "0.5 MB", Category: "Protocol"},
- }
- } else {
- docs = []*estatev1.VaultDocument{
- {Name: "Last_Will_Testament_2025.pdf", Date: "Mar 14, 2025", Size: "2.4 MB", Category: "Legal"},
- }
- }
- return connect.NewResponse(&estatev1.ListVaultDocumentsResponse{Documents: docs}), nil
- }
-
- var docs []*estatev1.VaultDocument
- iter := s.fs.Collection("estates").Doc(req.Msg.EstateId).Collection("vault").Documents(ctx)
- for {
- doc, err := iter.Next()
- if err == iterator.Done {
- break
- }
- if err != nil {
- return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("failed to fetch vault docs: %w", err))
- }
-
- var d estatev1.VaultDocument
- if err := doc.DataTo(&d); err != nil {
- continue
- }
- docs = append(docs, &d)
- }
-
- return connect.NewResponse(&estatev1.ListVaultDocumentsResponse{Documents: docs}), nil
-}
-
-func (s *Server) ListMemoirs(ctx context.Context, req *connect.Request[estatev1.ListMemoirsRequest]) (*connect.Response[estatev1.ListMemoirsResponse], error) {
- if err := s.checkEstateAccess(ctx, req.Msg.EstateId, false); err != nil {
- return nil, err
- }
-
- if s.fs == nil {
- var memoirs []*estatev1.Memoir
- if req.Msg.EstateId == "estate_lockhart" {
- memoirs = []*estatev1.Memoir{
- {Id: "mem-1", Title: "Mommy - Legacy Tape 01", Type: "video", Url: "/assets/tameeka/mommy.mp4", DateAdded: "Mar 17, 2026", Visibility: "private"},
- {Id: "mem-2", Title: "Musical Tribute - Live Session", Type: "video", Url: "/assets/tameeka/musical tribute.mp4", DateAdded: "Mar 17, 2026", Visibility: "all_heirs"},
- {Id: "mem-3", Title: "Mom Memorial - Heritage Photo", Type: "photo", Url: "/assets/tameeka/mom memorial.jpg", DateAdded: "Feb 10, 2026", Visibility: "private"},
- {Id: "mem-4", Title: "Mom Dance - Legacy Profile", Type: "photo", Url: "/assets/tameeka/mom dance.jpg", DateAdded: "Jan 15, 2026", Visibility: "private"},
- }
- } else {
- memoirs = []*estatev1.Memoir{
- {Id: "1", Title: "Legacy Tape 01", Type: "video", Url: "/memoirs/legacy.mp4", DateAdded: "Today", Visibility: "private"},
- }
- }
- return connect.NewResponse(&estatev1.ListMemoirsResponse{Memoirs: memoirs}), nil
- }
-
- var memoirs []*estatev1.Memoir
- iter := s.fs.Collection("estates").Doc(req.Msg.EstateId).Collection("memoirs").Documents(ctx)
- for {
- doc, err := iter.Next()
- if err == iterator.Done {
- break
- }
- if err != nil {
- return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("failed to fetch memoirs: %w", err))
- }
-
- var m estatev1.Memoir
- if err := doc.DataTo(&m); err != nil {
- continue
- }
- m.Id = doc.Ref.ID
- memoirs = append(memoirs, &m)
- }
-
- return connect.NewResponse(&estatev1.ListMemoirsResponse{Memoirs: memoirs}), nil
-}
-
-func (s *Server) UploadMemoir(ctx context.Context, req *connect.Request[estatev1.UploadMemoirRequest]) (*connect.Response[estatev1.UploadMemoirResponse], error) {
- if err := s.checkEstateAccess(ctx, req.Msg.EstateId, true); err != nil {
- return nil, err
- }
-
- if s.fs == nil {
- return nil, connect.NewError(connect.CodeUnimplemented, fmt.Errorf("firestore not initialized"))
- }
-
- newMemoir := &estatev1.Memoir{
- Title: req.Msg.Title,
- Type: req.Msg.Type,
- Url: req.Msg.Url,
- DateAdded: time.Now().Format("Jan 02, 2006"),
- Visibility: "private",
- }
-
- docRef, _, err := s.fs.Collection("estates").Doc(req.Msg.EstateId).Collection("memoirs").Add(ctx, map[string]interface{}{
- "title": newMemoir.Title,
- "type": newMemoir.Type,
- "url": newMemoir.Url,
- "date_added": newMemoir.DateAdded,
- "visibility": newMemoir.Visibility,
- })
- if err != nil {
- return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("failed to save memoir: %w", err))
- }
-
- newMemoir.Id = docRef.ID
- return connect.NewResponse(&estatev1.UploadMemoirResponse{Memoir: newMemoir}), nil
-}
-
-func (s *Server) GetObituary(ctx context.Context, req *connect.Request[estatev1.GetObituaryRequest]) (*connect.Response[estatev1.GetObituaryResponse], error) {
- // SaveObituary gated writes but this READ did not — any user could read any
- // estate's obituary (the deceased's life narrative) by ID. Gate it.
- if err := s.checkEstateAccess(ctx, req.Msg.EstateId, false); err != nil {
- return nil, err
- }
- if s.fs == nil {
- content := "Marcus Aurelius was a philosopher-king."
- status := "Draft"
- if req.Msg.EstateId == "estate_lockhart" {
- content = "Tameeka Lockhart, the guardian of the Lockhart Legacy, has established this final record as a testament to strength, family, and the enduring spirit of the Lockhart Estate. Her contributions to the community and her family remain indelible."
- status = "Verified"
- }
- return connect.NewResponse(&estatev1.GetObituaryResponse{
- Content: content,
- Status: status,
- LastUpdated: timestamppb.Now(),
- }), nil
- }
-
- doc, err := s.fs.Collection("estates").Doc(req.Msg.EstateId).Collection("governance").Doc("obituary").Get(ctx)
- if err != nil {
- // If document doesn't exist, return a default draft
- return connect.NewResponse(&estatev1.GetObituaryResponse{
- Content: "",
- Status: "None",
- LastUpdated: timestamppb.Now(),
- }), nil
- }
-
- var obit estatev1.GetObituaryResponse
- if err := doc.DataTo(&obit); err != nil {
- return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("failed to decode obituary: %w", err))
- }
-
- // Handle timestamp conversion if needed, otherwise firestore stores it
- return connect.NewResponse(&obit), nil
-}
-
-func (s *Server) SaveObituary(ctx context.Context, req *connect.Request[estatev1.SaveObituaryRequest]) (*connect.Response[estatev1.SaveObituaryResponse], error) {
- if err := s.checkEstateAccess(ctx, req.Msg.EstateId, true); err != nil {
- return nil, err
- }
-
- if s.fs == nil {
- return nil, connect.NewError(connect.CodeUnimplemented, fmt.Errorf("firestore not initialized"))
- }
-
- _, err := s.fs.Collection("estates").Doc(req.Msg.EstateId).Collection("governance").Doc("obituary").Set(ctx, map[string]interface{}{
- "content": req.Msg.Content,
- "status": "Draft",
- "last_updated": timestamppb.Now(),
- }, firestore.MergeAll)
- if err != nil {
- return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("failed to save obituary: %w", err))
- }
-
- return connect.NewResponse(&estatev1.SaveObituaryResponse{Success: true}), nil
-}
-
-func (s *Server) GetAIInsight(ctx context.Context, req *connect.Request[estatev1.GetAIInsightRequest]) (*connect.Response[estatev1.GetAIInsightResponse], error) {
- if err := s.checkEstateAccess(ctx, req.Msg.EstateId, false); err != nil {
- return nil, err
- }
-
- insight := "Heritage Protocol Synchronized. Your estate is 100% complete and secured via the Protocol Ledger. All shards are active and verified."
- if req.Msg.EstateId != "estate_lockhart" {
- insight = "Protocol Optimization Required. Your estate is 34% complete. Assigning remaining beneficiary shares will activate the full Guardian Protocol."
- }
- return connect.NewResponse(&estatev1.GetAIInsightResponse{
- Insight: insight,
- ActionLabel: "View Ledger",
- ActionUrl: "/dashboard/notifications",
- }), nil
-}
-
-func (s *Server) GetGovernanceSettings(ctx context.Context, req *connect.Request[estatev1.GetGovernanceSettingsRequest]) (*connect.Response[estatev1.GetGovernanceSettingsResponse], error) {
- if err := s.checkEstateAccess(ctx, req.Msg.EstateId, false); err != nil {
- return nil, err
- }
-
- if s.fs == nil {
- return connect.NewResponse(&estatev1.GetGovernanceSettingsResponse{
- Settings: &estatev1.GovernanceSettings{
- MfaEnabled: true,
- BiometricRelease: false,
- EmailAlerts: true,
- RecoveryKeyStatus: "Verified",
- StatusReportsFrequency: "Weekly",
- },
- }), nil
- }
-
- doc, err := s.fs.Collection("estates").Doc(req.Msg.EstateId).Collection("governance").Doc("settings").Get(ctx)
- if err != nil {
- return connect.NewResponse(&estatev1.GetGovernanceSettingsResponse{
- Settings: &estatev1.GovernanceSettings{MfaEnabled: true},
- }), nil
- }
-
- var settings estatev1.GovernanceSettings
- if err := doc.DataTo(&settings); err != nil {
- return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("failed to decode settings: %w", err))
- }
-
- return connect.NewResponse(&estatev1.GetGovernanceSettingsResponse{Settings: &settings}), nil
-}
-
-func (s *Server) ListNotifications(ctx context.Context, req *connect.Request[estatev1.ListNotificationsRequest]) (*connect.Response[estatev1.ListNotificationsResponse], error) {
- // Ungated reads leaked another estate's settlement/Guardian/security notifications.
- if err := s.checkEstateAccess(ctx, req.Msg.EstateId, false); err != nil {
- return nil, err
- }
- if s.fs == nil {
- notifications := []*estatev1.Notification{
- {Title: "Security Active", Time: "10 mins ago", Type: "success", Desc: "Protocols verified."},
- }
- return connect.NewResponse(&estatev1.ListNotificationsResponse{Notifications: notifications}), nil
- }
-
- var notifications []*estatev1.Notification
- iter := s.fs.Collection("estates").Doc(req.Msg.EstateId).Collection("notifications").OrderBy("time", firestore.Desc).Limit(10).Documents(ctx)
- for {
- doc, err := iter.Next()
- if err == iterator.Done {
- break
- }
- if err != nil {
- break // Graceful degradation
- }
-
- var n estatev1.Notification
- if err := doc.DataTo(&n); err != nil {
- continue
- }
- notifications = append(notifications, &n)
- }
-
- return connect.NewResponse(&estatev1.ListNotificationsResponse{Notifications: notifications}), nil
-}
diff --git a/api/internal/service/estate/service_test.go b/api/internal/service/estate/service_test.go
index 52e4212..769b213 100644
--- a/api/internal/service/estate/service_test.go
+++ b/api/internal/service/estate/service_test.go
@@ -10,450 +10,150 @@ import (
estatev1 "github.com/sirsi-technologies/finalwishes-api/internal/gen/estate/v1"
)
-// --- Helper ---
+// These tests pin the HONEST EstateService contract (2026-06-14): the service
+// implements ONLY GenerateUploadUrl. Every other proto RPC is intentionally not
+// overridden, so the embedded UnimplementedEstateServiceHandler answers it with
+// connect.CodeUnimplemented instead of fabricated "Lockhart Estate" / "Sarah
+// Johnson" / "34% complete" demo data. The previous suite asserted that
+// fabricated data; it has been replaced to assert the real, non-mock behavior.
-// newTestServer returns a Server with nil Firestore/Storage clients (demo mode).
+// --- Helpers ---
+
+// newTestServer returns a Server with nil Firestore/Storage clients.
func newTestServer() *Server {
return NewServer(nil, nil)
}
-// testCtx returns a context with a test user ID injected, so checkEstateAccess passes.
+// testCtx returns a context with a test user ID injected.
func testCtx() context.Context {
return auth.InjectUserIDForTest(context.Background(), "test-user")
}
// =========================================================
-// ListEstates
-// =========================================================
-
-func TestListEstates_DemoMode_KnownUser(t *testing.T) {
- s := newTestServer()
- resp, err := s.ListEstates(testCtx(), connect.NewRequest(&estatev1.ListEstatesRequest{
- UserId: "user_tameeka",
- }))
- if err != nil {
- t.Fatalf("unexpected error: %v", err)
- }
- if len(resp.Msg.Estates) != 2 {
- t.Fatalf("expected 2 estates, got %d", len(resp.Msg.Estates))
- }
- if resp.Msg.Estates[0].Name != "Lockhart Estate" {
- t.Errorf("expected 'Lockhart Estate', got %q", resp.Msg.Estates[0].Name)
- }
- if resp.Msg.Estates[1].Role != "Executor" {
- t.Errorf("expected Executor role for second estate, got %q", resp.Msg.Estates[1].Role)
- }
-}
-
-func TestListEstates_DemoMode_UnknownUser(t *testing.T) {
- s := newTestServer()
- resp, err := s.ListEstates(testCtx(), connect.NewRequest(&estatev1.ListEstatesRequest{
- UserId: "some_other_user",
- }))
- if err != nil {
- t.Fatalf("unexpected error: %v", err)
- }
- if len(resp.Msg.Estates) != 2 {
- t.Fatalf("expected 2 estates for unknown user, got %d", len(resp.Msg.Estates))
- }
- if resp.Msg.Estates[0].Id != "estate-77b" {
- t.Errorf("expected estate-77b, got %q", resp.Msg.Estates[0].Id)
- }
-}
-
-// =========================================================
-// GetEstateMetadata
-// =========================================================
-
-func TestGetEstateMetadata_DemoMode(t *testing.T) {
- s := newTestServer()
- resp, err := s.GetEstateMetadata(testCtx(), connect.NewRequest(&estatev1.GetEstateMetadataRequest{
- EstateId: "estate_lockhart",
- }))
- if err != nil {
- t.Fatalf("unexpected error: %v", err)
- }
- if resp.Msg.Name != "Lockhart Estate" {
- t.Errorf("expected 'Lockhart Estate', got %q", resp.Msg.Name)
- }
- if resp.Msg.CompletionPercentage != 100 {
- t.Errorf("expected 100%% completion, got %d", resp.Msg.CompletionPercentage)
- }
- if resp.Msg.Status != "Active" {
- t.Errorf("expected Active status, got %q", resp.Msg.Status)
- }
- if resp.Msg.AuthorityMode != estatev1.AuthorityMode_AUTHORITY_MODE_OWNER {
- t.Errorf("expected OWNER authority mode, got %v", resp.Msg.AuthorityMode)
- }
-}
-
-func TestGetEstateMetadata_PreservesEstateID(t *testing.T) {
- s := newTestServer()
- resp, err := s.GetEstateMetadata(testCtx(), connect.NewRequest(&estatev1.GetEstateMetadataRequest{
- EstateId: "custom-id-123",
- }))
- if err != nil {
- t.Fatalf("unexpected error: %v", err)
- }
- if resp.Msg.Id != "custom-id-123" {
- t.Errorf("expected estate ID to be echoed back, got %q", resp.Msg.Id)
- }
-}
-
-// =========================================================
-// ListBeneficiaries
-// =========================================================
-
-func TestListBeneficiaries_DemoMode(t *testing.T) {
- s := newTestServer()
- resp, err := s.ListBeneficiaries(testCtx(), connect.NewRequest(&estatev1.ListBeneficiariesRequest{
- EstateId: "estate_lockhart",
- }))
- if err != nil {
- t.Fatalf("unexpected error: %v", err)
- }
- if len(resp.Msg.Beneficiaries) != 1 {
- t.Fatalf("expected 1 beneficiary, got %d", len(resp.Msg.Beneficiaries))
- }
- b := resp.Msg.Beneficiaries[0]
- if b.Name != "Sarah Johnson" {
- t.Errorf("expected Sarah Johnson, got %q", b.Name)
- }
- if b.Share != "50%" {
- t.Errorf("expected 50%% share, got %q", b.Share)
- }
-}
-
-// =========================================================
-// AddBeneficiary — requires Firestore, should fail in demo mode
+// checkEstateAccess — without an estate store, membership cannot be proven,
+// so access is DENIED (FailedPrecondition), never silently granted.
// =========================================================
-func TestAddBeneficiary_DemoMode_FailsGracefully(t *testing.T) {
+func TestCheckEstateAccess_NoStore_Denied(t *testing.T) {
s := newTestServer()
- _, err := s.AddBeneficiary(testCtx(), connect.NewRequest(&estatev1.AddBeneficiaryRequest{
- EstateId: "estate_lockhart",
- Name: "New Heir",
- Relation: "Child",
- Email: "newhier@test.com",
- }))
+ err := s.checkEstateAccess(testCtx(), "estate_x", false)
if err == nil {
- t.Fatal("expected error when Firestore is nil, got nil")
- }
- // Should be an Unimplemented error since fs is nil
- if connect.CodeOf(err) != connect.CodeUnimplemented {
- t.Errorf("expected Unimplemented code, got %v", connect.CodeOf(err))
+ t.Fatal("expected error when estate store is not configured")
}
-}
-
-// =========================================================
-// ListAssets
-// =========================================================
-
-func TestListAssets_DemoMode_LockhartEstate(t *testing.T) {
- s := newTestServer()
- resp, err := s.ListAssets(testCtx(), connect.NewRequest(&estatev1.ListAssetsRequest{
- EstateId: "estate_lockhart",
- }))
- if err != nil {
- t.Fatalf("unexpected error: %v", err)
- }
- if len(resp.Msg.Assets) != 4 {
- t.Fatalf("expected 4 assets for lockhart, got %d", len(resp.Msg.Assets))
- }
- if resp.Msg.TotalCount != 4 {
- t.Errorf("expected total count 4, got %d", resp.Msg.TotalCount)
- }
- // Verify first asset
- if resp.Msg.Assets[0].Type != "Real Estate" {
- t.Errorf("expected Real Estate type, got %q", resp.Msg.Assets[0].Type)
- }
-}
-
-func TestListAssets_DemoMode_OtherEstate(t *testing.T) {
- s := newTestServer()
- resp, err := s.ListAssets(testCtx(), connect.NewRequest(&estatev1.ListAssetsRequest{
- EstateId: "other-estate",
- }))
- if err != nil {
- t.Fatalf("unexpected error: %v", err)
- }
- if len(resp.Msg.Assets) != 2 {
- t.Fatalf("expected 2 assets for other estate, got %d", len(resp.Msg.Assets))
+ if connect.CodeOf(err) != connect.CodeFailedPrecondition {
+ t.Errorf("expected FailedPrecondition, got %v", connect.CodeOf(err))
}
}
-// =========================================================
-// AddAsset — requires Firestore
-// =========================================================
-
-func TestAddAsset_DemoMode_FailsGracefully(t *testing.T) {
+func TestCheckEstateAccess_Unauthenticated(t *testing.T) {
s := newTestServer()
- _, err := s.AddAsset(testCtx(), connect.NewRequest(&estatev1.AddAssetRequest{
- EstateId: "estate_lockhart",
- Name: "New Car",
- Type: "Vehicle",
- Value: "$50,000",
- }))
+ err := s.checkEstateAccess(context.Background(), "estate_x", false)
if err == nil {
- t.Fatal("expected error when Firestore is nil")
+ t.Fatal("expected error when no user is in context")
}
- if connect.CodeOf(err) != connect.CodeUnimplemented {
- t.Errorf("expected Unimplemented code, got %v", connect.CodeOf(err))
+ if connect.CodeOf(err) != connect.CodeUnauthenticated {
+ t.Errorf("expected Unauthenticated, got %v", connect.CodeOf(err))
}
}
// =========================================================
-// ListVaultDocuments
+// GenerateUploadUrl — the only implemented RPC. With nil clients it is gated
+// by checkEstateAccess (no store -> FailedPrecondition) before any signing.
// =========================================================
-func TestListVaultDocuments_DemoMode_Lockhart(t *testing.T) {
- s := newTestServer()
- resp, err := s.ListVaultDocuments(testCtx(), connect.NewRequest(&estatev1.ListVaultDocumentsRequest{
- EstateId: "estate_lockhart",
- }))
- if err != nil {
- t.Fatalf("unexpected error: %v", err)
- }
- if len(resp.Msg.Documents) != 4 {
- t.Fatalf("expected 4 documents, got %d", len(resp.Msg.Documents))
- }
- // Verify categories
- categories := map[string]bool{}
- for _, d := range resp.Msg.Documents {
- categories[d.Category] = true
- }
- for _, expected := range []string{"Legal", "Tax", "Property", "Protocol"} {
- if !categories[expected] {
- t.Errorf("expected category %q in documents", expected)
- }
- }
-}
-
-func TestListVaultDocuments_DemoMode_Other(t *testing.T) {
+func TestGenerateUploadUrl_NoStore_FailsClosed(t *testing.T) {
s := newTestServer()
- resp, err := s.ListVaultDocuments(testCtx(), connect.NewRequest(&estatev1.ListVaultDocumentsRequest{
- EstateId: "other-estate",
+ _, err := s.GenerateUploadUrl(testCtx(), connect.NewRequest(&estatev1.GenerateUploadUrlRequest{
+ EstateId: "estate_x",
+ FileName: "test.pdf",
+ ContentType: "application/pdf",
}))
- if err != nil {
- t.Fatalf("unexpected error: %v", err)
+ if err == nil {
+ t.Fatal("expected error when estate store / storage client is nil")
}
- if len(resp.Msg.Documents) != 1 {
- t.Fatalf("expected 1 document, got %d", len(resp.Msg.Documents))
+ if connect.CodeOf(err) != connect.CodeFailedPrecondition {
+ t.Errorf("expected FailedPrecondition, got %v", connect.CodeOf(err))
}
}
-// =========================================================
-// GenerateUploadUrl — requires Storage client
-// =========================================================
-
-func TestGenerateUploadUrl_DemoMode_FailsGracefully(t *testing.T) {
+func TestGenerateUploadUrl_Unauthenticated(t *testing.T) {
s := newTestServer()
- _, err := s.GenerateUploadUrl(testCtx(), connect.NewRequest(&estatev1.GenerateUploadUrlRequest{
- EstateId: "estate_lockhart",
+ _, err := s.GenerateUploadUrl(context.Background(), connect.NewRequest(&estatev1.GenerateUploadUrlRequest{
+ EstateId: "estate_x",
FileName: "test.pdf",
ContentType: "application/pdf",
}))
if err == nil {
- t.Fatal("expected error when storage client is nil")
+ t.Fatal("expected error when caller is unauthenticated")
}
- if connect.CodeOf(err) != connect.CodeFailedPrecondition {
- t.Errorf("expected FailedPrecondition code, got %v", connect.CodeOf(err))
+ if connect.CodeOf(err) != connect.CodeUnauthenticated {
+ t.Errorf("expected Unauthenticated, got %v", connect.CodeOf(err))
}
}
// =========================================================
-// ListMemoirs
+// Unimplemented surface — every formerly-mocked RPC now returns Unimplemented
+// (no fabricated data, no hardcoded Lockhart/Sarah-Johnson/percentages).
// =========================================================
-func TestListMemoirs_DemoMode_Lockhart(t *testing.T) {
+func TestUnimplementedRPCs_ReturnUnimplemented(t *testing.T) {
s := newTestServer()
- resp, err := s.ListMemoirs(testCtx(), connect.NewRequest(&estatev1.ListMemoirsRequest{
- EstateId: "estate_lockhart",
- }))
- if err != nil {
- t.Fatalf("unexpected error: %v", err)
- }
- if len(resp.Msg.Memoirs) != 4 {
- t.Fatalf("expected 4 memoirs, got %d", len(resp.Msg.Memoirs))
- }
- // Verify mix of types
- types := map[string]int{}
- for _, m := range resp.Msg.Memoirs {
- types[m.Type]++
- }
- if types["video"] != 2 {
- t.Errorf("expected 2 videos, got %d", types["video"])
- }
- if types["photo"] != 2 {
- t.Errorf("expected 2 photos, got %d", types["photo"])
- }
-}
+ ctx := testCtx()
-func TestListMemoirs_DemoMode_Other(t *testing.T) {
- s := newTestServer()
- resp, err := s.ListMemoirs(testCtx(), connect.NewRequest(&estatev1.ListMemoirsRequest{
- EstateId: "other",
- }))
- if err != nil {
- t.Fatalf("unexpected error: %v", err)
- }
- if len(resp.Msg.Memoirs) != 1 {
- t.Fatalf("expected 1 memoir, got %d", len(resp.Msg.Memoirs))
+ assertUnimplemented := func(t *testing.T, name string, err error) {
+ t.Helper()
+ if err == nil {
+ t.Fatalf("%s: expected Unimplemented error, got nil (fabricated response?)", name)
+ }
+ if connect.CodeOf(err) != connect.CodeUnimplemented {
+ t.Errorf("%s: expected Unimplemented, got %v", name, connect.CodeOf(err))
+ }
}
-}
-// =========================================================
-// UploadMemoir — requires Firestore
-// =========================================================
+ _, err := s.ListEstates(ctx, connect.NewRequest(&estatev1.ListEstatesRequest{UserId: "user_tameeka"}))
+ assertUnimplemented(t, "ListEstates", err)
-func TestUploadMemoir_DemoMode_FailsGracefully(t *testing.T) {
- s := newTestServer()
- _, err := s.UploadMemoir(testCtx(), connect.NewRequest(&estatev1.UploadMemoirRequest{
- EstateId: "estate_lockhart",
- Title: "Test Memoir",
- Type: "video",
- Url: "/test.mp4",
- }))
- if err == nil {
- t.Fatal("expected error when Firestore is nil")
- }
- if connect.CodeOf(err) != connect.CodeUnimplemented {
- t.Errorf("expected Unimplemented code, got %v", connect.CodeOf(err))
- }
-}
+ _, err = s.RegisterEstate(ctx, connect.NewRequest(&estatev1.RegisterEstateRequest{Name: "X", Type: "Trust"}))
+ assertUnimplemented(t, "RegisterEstate", err)
-// =========================================================
-// GetObituary
-// =========================================================
+ _, err = s.GetEstateMetadata(ctx, connect.NewRequest(&estatev1.GetEstateMetadataRequest{EstateId: "estate_lockhart"}))
+ assertUnimplemented(t, "GetEstateMetadata", err)
-func TestGetObituary_DemoMode_Lockhart(t *testing.T) {
- s := newTestServer()
- resp, err := s.GetObituary(testCtx(), connect.NewRequest(&estatev1.GetObituaryRequest{
- EstateId: "estate_lockhart",
- }))
- if err != nil {
- t.Fatalf("unexpected error: %v", err)
- }
- if resp.Msg.Status != "Verified" {
- t.Errorf("expected Verified status, got %q", resp.Msg.Status)
- }
- if resp.Msg.Content == "" {
- t.Error("expected non-empty obituary content")
- }
- if resp.Msg.LastUpdated == nil {
- t.Error("expected LastUpdated timestamp")
- }
-}
+ _, err = s.ListAssets(ctx, connect.NewRequest(&estatev1.ListAssetsRequest{EstateId: "estate_lockhart"}))
+ assertUnimplemented(t, "ListAssets", err)
-func TestGetObituary_DemoMode_Other(t *testing.T) {
- s := newTestServer()
- resp, err := s.GetObituary(testCtx(), connect.NewRequest(&estatev1.GetObituaryRequest{
- EstateId: "other",
- }))
- if err != nil {
- t.Fatalf("unexpected error: %v", err)
- }
- if resp.Msg.Status != "Draft" {
- t.Errorf("expected Draft status, got %q", resp.Msg.Status)
- }
-}
+ _, err = s.AddAsset(ctx, connect.NewRequest(&estatev1.AddAssetRequest{EstateId: "estate_lockhart", Name: "Car"}))
+ assertUnimplemented(t, "AddAsset", err)
-// =========================================================
-// SaveObituary — requires Firestore
-// =========================================================
+ _, err = s.ListBeneficiaries(ctx, connect.NewRequest(&estatev1.ListBeneficiariesRequest{EstateId: "estate_lockhart"}))
+ assertUnimplemented(t, "ListBeneficiaries", err)
-func TestSaveObituary_DemoMode_FailsGracefully(t *testing.T) {
- s := newTestServer()
- _, err := s.SaveObituary(testCtx(), connect.NewRequest(&estatev1.SaveObituaryRequest{
- EstateId: "estate_lockhart",
- Content: "Updated content",
- }))
- if err == nil {
- t.Fatal("expected error when Firestore is nil")
- }
- if connect.CodeOf(err) != connect.CodeUnimplemented {
- t.Errorf("expected Unimplemented code, got %v", connect.CodeOf(err))
- }
-}
+ _, err = s.AddBeneficiary(ctx, connect.NewRequest(&estatev1.AddBeneficiaryRequest{EstateId: "estate_lockhart", Name: "Heir"}))
+ assertUnimplemented(t, "AddBeneficiary", err)
-// =========================================================
-// GetAIInsight
-// =========================================================
+ _, err = s.ListVaultDocuments(ctx, connect.NewRequest(&estatev1.ListVaultDocumentsRequest{EstateId: "estate_lockhart"}))
+ assertUnimplemented(t, "ListVaultDocuments", err)
-func TestGetAIInsight_LockhartEstate(t *testing.T) {
- s := newTestServer()
- resp, err := s.GetAIInsight(testCtx(), connect.NewRequest(&estatev1.GetAIInsightRequest{
- EstateId: "estate_lockhart",
- }))
- if err != nil {
- t.Fatalf("unexpected error: %v", err)
- }
- if resp.Msg.Insight == "" {
- t.Error("expected non-empty insight")
- }
- if resp.Msg.ActionLabel != "View Ledger" {
- t.Errorf("expected 'View Ledger', got %q", resp.Msg.ActionLabel)
- }
-}
+ _, err = s.ListMemoirs(ctx, connect.NewRequest(&estatev1.ListMemoirsRequest{EstateId: "estate_lockhart"}))
+ assertUnimplemented(t, "ListMemoirs", err)
-func TestGetAIInsight_OtherEstate(t *testing.T) {
- s := newTestServer()
- resp, err := s.GetAIInsight(testCtx(), connect.NewRequest(&estatev1.GetAIInsightRequest{
- EstateId: "other",
- }))
- if err != nil {
- t.Fatalf("unexpected error: %v", err)
- }
- if resp.Msg.Insight == "" {
- t.Error("expected non-empty insight for other estate")
- }
-}
+ _, err = s.UploadMemoir(ctx, connect.NewRequest(&estatev1.UploadMemoirRequest{EstateId: "estate_lockhart", Title: "M"}))
+ assertUnimplemented(t, "UploadMemoir", err)
-// =========================================================
-// GetGovernanceSettings
-// =========================================================
+ _, err = s.GetObituary(ctx, connect.NewRequest(&estatev1.GetObituaryRequest{EstateId: "estate_lockhart"}))
+ assertUnimplemented(t, "GetObituary", err)
-func TestGetGovernanceSettings_DemoMode(t *testing.T) {
- s := newTestServer()
- resp, err := s.GetGovernanceSettings(testCtx(), connect.NewRequest(&estatev1.GetGovernanceSettingsRequest{
- EstateId: "estate_lockhart",
- }))
- if err != nil {
- t.Fatalf("unexpected error: %v", err)
- }
- settings := resp.Msg.Settings
- if settings == nil {
- t.Fatal("expected non-nil settings")
- }
- if !settings.MfaEnabled {
- t.Error("expected MFA enabled in demo mode")
- }
- if !settings.EmailAlerts {
- t.Error("expected email alerts enabled")
- }
- if settings.RecoveryKeyStatus != "Verified" {
- t.Errorf("expected Verified recovery key, got %q", settings.RecoveryKeyStatus)
- }
-}
+ _, err = s.SaveObituary(ctx, connect.NewRequest(&estatev1.SaveObituaryRequest{EstateId: "estate_lockhart", Content: "x"}))
+ assertUnimplemented(t, "SaveObituary", err)
-// =========================================================
-// ListNotifications
-// =========================================================
+ _, err = s.GetAIInsight(ctx, connect.NewRequest(&estatev1.GetAIInsightRequest{EstateId: "estate_lockhart"}))
+ assertUnimplemented(t, "GetAIInsight", err)
-func TestListNotifications_DemoMode(t *testing.T) {
- s := newTestServer()
- resp, err := s.ListNotifications(testCtx(), connect.NewRequest(&estatev1.ListNotificationsRequest{
- EstateId: "estate_lockhart",
- }))
- if err != nil {
- t.Fatalf("unexpected error: %v", err)
- }
- if len(resp.Msg.Notifications) != 1 {
- t.Fatalf("expected 1 notification, got %d", len(resp.Msg.Notifications))
- }
- if resp.Msg.Notifications[0].Type != "success" {
- t.Errorf("expected success type, got %q", resp.Msg.Notifications[0].Type)
- }
+ _, err = s.GetGovernanceSettings(ctx, connect.NewRequest(&estatev1.GetGovernanceSettingsRequest{EstateId: "estate_lockhart"}))
+ assertUnimplemented(t, "GetGovernanceSettings", err)
+
+ _, err = s.ListNotifications(ctx, connect.NewRequest(&estatev1.ListNotificationsRequest{EstateId: "estate_lockhart"}))
+ assertUnimplemented(t, "ListNotifications", err)
}
// =========================================================
diff --git a/docs/ADR-INDEX.md b/docs/ADR-INDEX.md
index 8907f66..c02aa9a 100644
--- a/docs/ADR-INDEX.md
+++ b/docs/ADR-INDEX.md
@@ -1,6 +1,6 @@
# Architecture Decision Records (ADR) Index
-This document indexes all Architecture Decision Records for the Sirsi Nexus platform.
+This document indexes all Architecture Decision Records for FinalWishes (The Estate Operating System).
## What is an ADR?
@@ -124,4 +124,4 @@ Each ADR follows this structure:
---
-*Last updated: 2026-04-14*
+*Last updated: 2026-06-14*
diff --git a/docs/REQUIREMENTS_SPECIFICATION.md b/docs/REQUIREMENTS_SPECIFICATION.md
index c5cc780..9e36b98 100644
--- a/docs/REQUIREMENTS_SPECIFICATION.md
+++ b/docs/REQUIREMENTS_SPECIFICATION.md
@@ -108,16 +108,27 @@ Initial launch supports **3 priority jurisdictions**:
### 2.4 Verify Phase (FR-400)
-#### FR-401: Death Certificate Processing — ⏳ Deferred to Tier 3
+#### FR-401: Death Certificate Processing — ✅ Implemented (IL)
- System SHALL accept death certificate upload
-- System SHALL perform OCR extraction of death certificate data
-- *Not yet implemented. Post-settlement workflow.*
-
-#### FR-402: Executor Activation — ⏳ Deferred to Tier 3
-- System SHALL send notification to designated Executors upon death verification
-- System SHALL require Executor identity re-verification
-- System SHALL provide cooling-off period before asset access
-- *Full activation workflow not yet implemented. Basic executor role assignment exists.*
+- System SHALL extract death certificate facts (decedent name, dates) from the
+ document-intelligence analysis pipeline
+- System SHALL require executor/admin confirmation of the extracted facts
+- **Implementation:** `api/internal/probate/deathcert.go`
+ (`HandleSubmitDeathCertAnalysis` reads the AI analysis already produced by the
+ document-intelligence pipeline; `HandleConfirmDeathCert` records executor
+ confirmation; `HandleGetDeathCertFacts` returns stored facts). Surfaced in the
+ probate route (`web/src/routes/estates.$estateId.probate.tsx` →
+ `getDeathCertFacts` / `confirmDeathCert`).
+
+#### FR-402: Executor Activation — ✅ Implemented (IL)
+- System SHALL allow the designated executor to confirm their role after death verification
+- System SHALL notify estate members upon executor activation
+- System SHALL gate activation on executor/admin estate access
+- **Implementation:** `api/internal/probate/executor.go`
+ (`HandleGetExecutorStatus`, `HandleConfirmExecutorRole`, `notifyEstateMembers`).
+ Surfaced in the probate route via `getExecutorStatus` / `confirmExecutorRole`.
+- *Scope note:* identity re-verification and an explicit cooling-off timer remain
+ future hardening; role assignment, confirmation, and member notification ship.
#### FR-403: Beneficiary Notification — ⏳ Deferred to Tier 3
- System SHALL notify Heirs of their designation (after Executor activation)
@@ -130,9 +141,19 @@ Initial launch supports **3 priority jurisdictions**:
- System SHALL support institution templates for launch states (IL, MN, MD)
- *Not yet implemented.*
-#### FR-502: Government Notifications — ⏳ Deferred to Tier 3
+#### FR-502: Probate Guidance & Court Forms — ✅ Implemented (IL)
- System SHALL provide state-specific probate guidance for launch states
-- *Not yet implemented.*
+- System SHALL surface the correct court form references for the estate's value
+ (full probate vs. small-estate affidavit)
+- **Implementation:** `api/internal/probate/forms.go` returns Illinois / Cook
+ County Probate Division form references — petition for letters (**CCP0315**),
+ oath/bond (**CCP0312/CCP0313**), and the Illinois small-estate-affidavit path
+ (**755 ILCS 5/ Art. XXV**, threshold per **IL Probate Act §14-1**) — each with
+ its official court URL. `directives.go` serves Illinois advance directives;
+ `illinois.go` holds the state ruleset. Surfaced in the probate route alongside
+ `SettlementGantt` and `QuorumPanel`.
+- *Scope note:* generated institution/government notification *letters* (FR-501)
+ remain deferred; this requirement covers guidance + form references, which ship.
#### FR-503: Account Freeze Requests — ⏳ Deferred to Tier 3
- *Not yet implemented.*
@@ -243,10 +264,16 @@ Initial launch supports **3 priority jurisdictions**:
- System SHALL create a sacred, emotional moment for the heir
- **Implementation:** Heir Welcome screen with Owner's legacy content
-#### FR-913: SMS Invitation Queue — ✅ Implemented
-- System SHALL support phone number collection for heir invitations
-- System SHALL queue SMS notifications for delivery
-- **Implementation:** SMS invitation queue with phone number input
+#### FR-913: SMS Invitation Notifications — 🔮 Future (email-only at GA)
+- System SHALL notify invitees of estate invitations
+- **Implementation:** Invitation delivery is **email-only** (Gmail API via the
+ `sendMail` Cloud Function). The phone-number/SMS path was **removed** rather
+ than shipped: no SMS provider is wired, so a queued text would never be
+ delivered while the UI reported success. The phone input and `sms_queue` write
+ have been deleted from the invite flow. SMS delivery is deferred until a real
+ provider is adopted (Sirsi Sign shared SMS/MFA rail per ADR-047, or a managed
+ Twilio extension), at which point the delivery function and phone field will be
+ reintroduced together.
---
@@ -397,9 +424,9 @@ Initial launch supports **3 priority jurisdictions**:
- [x] User can sign documents electronically (OpenSign integration)
- [x] User can share memorial pages publicly (QR code sharing)
- [x] AI Shepherd provides guidance, scoring, and obituary drafting
-- [ ] Executor can activate estate after death verification (Tier 3)
-- [ ] System generates notification letters for institutions (Tier 3)
-- [ ] System provides state-specific probate guidance (IL, MN, MD) (Tier 3)
+- [x] Executor can activate estate after death verification (probate engine — IL; FR-401/402)
+- [ ] System generates notification letters for institutions (Tier 3 — FR-501 deferred)
+- [x] System provides state-specific probate guidance + court forms (IL shipped; MN, MD deferred) (FR-502)
---
@@ -417,11 +444,11 @@ Initial launch supports **3 priority jurisdictions**:
| FR-301 | Document Intelligence | ✅ Implemented |
| FR-302 | Asset Discovery | ⏳ Deferred (Tier 2) |
| FR-303 | Contact Import | ⏳ Deferred |
-| FR-401 | Death Certificate Processing | ⏳ Deferred (Tier 3) |
-| FR-402 | Executor Activation | ⏳ Deferred (Tier 3) |
+| FR-401 | Death Certificate Processing | ✅ Implemented (IL) |
+| FR-402 | Executor Activation | ✅ Implemented (IL) |
| FR-403 | Beneficiary Notification | ⏳ Deferred (Tier 3) |
| FR-501 | Institution Notification | ⏳ Deferred (Tier 3) |
-| FR-502 | Government Notifications | ⏳ Deferred (Tier 3) |
+| FR-502 | Probate Guidance & Court Forms | ✅ Implemented (IL) |
| FR-503 | Account Freeze Requests | ⏳ Deferred (Tier 3) |
| FR-601 | Asset Transfer | ⏳ Deferred (Tier 3) |
| FR-602 | Final Accounting | 🔮 Future |
@@ -443,7 +470,7 @@ Initial launch supports **3 priority jurisdictions**:
| FR-910 | Per-Person Content Visibility | ✅ Implemented |
| FR-911 | Owner Welcome Experience | ✅ Implemented |
| FR-912 | Heir Welcome Sacred Moment | ✅ Implemented |
-| FR-913 | SMS Invitation Queue | ✅ Implemented |
+| FR-913 | SMS Invitation Notifications | 🔮 Future (email-only at GA — no provider wired) |
---
@@ -454,3 +481,4 @@ Initial launch supports **3 priority jurisdictions**:
| 1.0.0 | 2025-11-26 | FinalWishes Team | Initial draft |
| 1.1.0 | 2025-11-26 | Claude | 6-state MVP scope, Firebase/GCP constraints, 4-month timeline |
| 2.0.0 | 2026-04-17 | Claude | Complete status audit: 8 FRs implemented, 13 deferred/future, 13 new Life Companion requirements (FR-901 through FR-913) added. Integration table updated to reflect actual GCP stack. Geographic scope narrowed to 3 states (IL, MN, MD). Tech constraints updated to reflect Go + React 19 + Vite 8 stack. |
+| 2.1.0 | 2026-06-14 | Claude | Status reconciliation. FR-913 re-statused ✅→🔮 (SMS path removed — email-only delivery; no provider wired). FR-401 (death-cert facts + confirm), FR-402 (executor activation + member notify), FR-502 (IL probate guidance + Cook County court forms) re-statused ⏳ Deferred→✅ Implemented (IL) to match the shipped `api/internal/probate` engine. §6.1 acceptance + Appendix A matrix updated accordingly. |
diff --git a/docs/legal-corpus/build_manifest.py b/docs/legal-corpus/build_manifest.py
new file mode 100644
index 0000000..8723e6d
--- /dev/null
+++ b/docs/legal-corpus/build_manifest.py
@@ -0,0 +1,161 @@
+#!/usr/bin/env python3
+"""Deterministically assemble the CR-10 legal-corpus manifest from raw official
+.gov HTML captures. NO LLM is in the text path (Rule 9): each `text` field is the
+verbatim statute body, sliced between explicit markers from the official page.
+"""
+import re, html, json, sys
+
+def extract(path):
+ h = open(path, encoding='utf-8', errors='replace').read()
+ h = re.sub(r'(?is)
', '\n', h)
+ h = re.sub(r'(?i)
Hi all
' }, + createdBy: 'owner-1', }), ref, }, @@ -314,6 +430,8 @@ describe('sendMail', () => { test('resolves template from email_templates collection', async () => { const ref = { update: jest.fn().mockResolvedValue(undefined) }; + // Authorize the recipient AND serve the template doc from the same mock so + // the open-relay lookups (estate_users / estate_invitations) still resolve. mockCollection.mockImplementation((collName) => { if (collName === 'email_templates') { return { @@ -328,13 +446,35 @@ describe('sendMail', () => { })), }; } - return { doc: mockDoc }; + if (collName === 'estate_users') { + return { + where: jest.fn().mockReturnThis(), + get: jest.fn().mockResolvedValue({ + docs: [{ data: () => ({ estateId: 'est-1', userId: 'owner-1' }) }], + }), + }; + } + if (collName === 'estate_invitations') { + return { + where: jest.fn().mockReturnThis(), + get: jest.fn().mockResolvedValue({ + docs: [{ data: () => ({ estateId: 'est-1', email: 'heir@test.com' }) }], + }), + }; + } + return { + where: jest.fn().mockReturnThis(), + limit: jest.fn().mockReturnThis(), + get: jest.fn().mockResolvedValue({ empty: true, docs: [] }), + doc: mockDoc, + }; }); const event = { data: { data: () => ({ to: 'heir@test.com', + createdBy: 'owner-1', template: { name: 'invitation', data: { name: 'John', estate: 'Collymore Estate' }, @@ -362,6 +502,7 @@ describe('sendMail', () => { data: () => ({ to: 'a@b.com', message: { subject: 'Test', html: 'Hi
' }, + createdBy: 'owner-1', }), ref, }, @@ -377,149 +518,93 @@ describe('sendMail', () => { }) ); }); -}); - -describe('sendSMS', () => { - const handler = registeredFunctions['sms_queue/{smsId}']; - beforeEach(() => { - jest.clearAllMocks(); - }); + // ─── Open-relay defense (locks in the createdBy-attribution hardening) ───── - test('skips when no snapshot data', async () => { - await handler({ data: null, params: { smsId: 's1' } }); - expect(mockUpdate).not.toHaveBeenCalled(); - }); - - test('skips already processed', async () => { - const event = makeEvent({ delivery: { state: 'SUCCESS' } }, { smsId: 's1' }); - await handler(event); - // Should not have called update since delivery was already SUCCESS - }); - - test('errors on missing required fields (no phone)', async () => { - const ref = { update: jest.fn().mockResolvedValue(undefined) }; - const event = { - data: { data: () => ({ body: 'Hello' }), ref }, - params: { smsId: 's1' }, - }; - - await handler(event); - - expect(ref.update).toHaveBeenCalledWith( - expect.objectContaining({ - 'delivery.state': 'ERROR', - 'delivery.error': expect.stringContaining('Missing required fields'), - }) - ); - }); - - test('errors on missing required fields (no body)', async () => { - const ref = { update: jest.fn().mockResolvedValue(undefined) }; - const event = { - data: { data: () => ({ to: '+15551234567' }), ref }, - params: { smsId: 's1' }, - }; - - await handler(event); - - expect(ref.update).toHaveBeenCalledWith( - expect.objectContaining({ - 'delivery.state': 'ERROR', - 'delivery.error': expect.stringContaining('Missing required fields'), - }) - ); - }); - - test('errors on invalid phone number format', async () => { - const ref = { update: jest.fn().mockResolvedValue(undefined) }; - const event = { - data: { - data: () => ({ to: '555-123-4567', body: 'Hello' }), - ref, - }, - params: { smsId: 's1' }, - }; - - await handler(event); - - expect(ref.update).toHaveBeenCalledWith( - expect.objectContaining({ - 'delivery.state': 'ERROR', - 'delivery.error': expect.stringContaining('Invalid phone number format'), - }) - ); - }); - - test('errors on body exceeding 1600 characters', async () => { - const ref = { update: jest.fn().mockResolvedValue(undefined) }; - const event = { - data: { - data: () => ({ to: '+15551234567', body: 'A'.repeat(1601) }), - ref, - }, - params: { smsId: 's1' }, - }; - - await handler(event); - - expect(ref.update).toHaveBeenCalledWith( - expect.objectContaining({ - 'delivery.state': 'ERROR', - 'delivery.error': expect.stringContaining('SMS body too long'), - }) - ); - }); - - test('queues valid SMS with PENDING_PROVIDER status', async () => { + test('rejects mail with NO createdBy attribution (open-relay defense)', async () => { const ref = { update: jest.fn().mockResolvedValue(undefined) }; const event = { data: { data: () => ({ - to: '+15551234567', - body: 'You have been invited to an estate.', - invitationId: 'inv-1', - estateId: 'est-1', - createdBy: 'uid-1', + to: 'stranger@example.com', + message: { subject: 'Phish', html: 'Click me
' }, + // createdBy intentionally absent }), ref, }, - params: { smsId: 's1' }, + params: { mailId: 'm1' }, }; await handler(event); + // Must FAIL CLOSED — never relay an unattributed mail from admin@sirsi.ai. + expect(mockGmailSend).not.toHaveBeenCalled(); expect(ref.update).toHaveBeenCalledWith( expect.objectContaining({ - 'delivery.state': 'PENDING_PROVIDER', - 'delivery.info': expect.stringContaining('SMS queued successfully'), + 'delivery.state': 'ERROR', + 'delivery.error': 'recipient not authorized', + status: 'rejected', }) ); }); - test('accepts E.164 phone with spaces/dashes stripped', async () => { + test('rejects mail to a recipient the createdBy user is NOT authorized for', async () => { + // createdBy belongs to NO estate and the recipient is not their own email → + // every authorization path fails and the mail must be rejected. + mockGetUser.mockResolvedValue({ email: 'owner@sirsi.ai' }); + mockCollection.mockImplementation((collName) => { + if (collName === 'estate_users') { + return { + where: jest.fn().mockReturnThis(), + get: jest.fn().mockResolvedValue({ docs: [] }), // belongs to no estate + }; + } + if (collName === 'users') { + return { + doc: jest.fn(() => ({ + get: jest.fn().mockResolvedValue({ exists: false }), + })), + }; + } + return { + where: jest.fn().mockReturnThis(), + limit: jest.fn().mockReturnThis(), + get: jest.fn().mockResolvedValue({ empty: true, docs: [] }), + doc: mockDoc, + }; + }); + const ref = { update: jest.fn().mockResolvedValue(undefined) }; const event = { data: { data: () => ({ - to: '+1 (555) 123-4567', - body: 'Test message', + to: 'victim@example.com', + message: { subject: 'Hi', html: 'Hi
' }, + createdBy: 'orphan-uid', }), ref, }, - params: { smsId: 's1' }, + params: { mailId: 'm1' }, }; await handler(event); + expect(mockGmailSend).not.toHaveBeenCalled(); expect(ref.update).toHaveBeenCalledWith( expect.objectContaining({ - 'delivery.state': 'PENDING_PROVIDER', + 'delivery.state': 'ERROR', + 'delivery.error': 'recipient not authorized', + status: 'rejected', }) ); }); }); +// NOTE: There is no sendSMS describe block. Invitation delivery is email-only; +// the SMS queue + stub delivery function were removed (it never delivered a text +// while the UI reported success). Reinstate tests here only alongside a live SMS +// provider integration. + describe('guardianInactivityCheck', () => { const handler = registeredFunctions['__schedule__']; diff --git a/functions/package.json b/functions/package.json index 89683f9..448ff49 100644 --- a/functions/package.json +++ b/functions/package.json @@ -15,6 +15,7 @@ "scripts": { "lint": "echo 'No lint configured'", "test": "jest --verbose", + "test:ci": "npm ci && jest --ci --verbose", "serve": "firebase emulators:start --only functions", "deploy": "firebase deploy --only functions" }, diff --git a/package-lock.json b/package-lock.json index c23574f..80f59b9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -932,9 +932,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", - "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", "cpu": [ "ppc64" ], @@ -949,9 +949,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", - "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", "cpu": [ "arm" ], @@ -966,9 +966,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", - "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", "cpu": [ "arm64" ], @@ -983,9 +983,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", - "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", "cpu": [ "x64" ], @@ -1000,9 +1000,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", - "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", "cpu": [ "arm64" ], @@ -1017,9 +1017,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", - "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", "cpu": [ "x64" ], @@ -1034,9 +1034,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", - "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", "cpu": [ "arm64" ], @@ -1051,9 +1051,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", - "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", "cpu": [ "x64" ], @@ -1068,9 +1068,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", - "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", "cpu": [ "arm" ], @@ -1085,9 +1085,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", - "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", "cpu": [ "arm64" ], @@ -1102,9 +1102,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", - "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", "cpu": [ "ia32" ], @@ -1119,9 +1119,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", - "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", "cpu": [ "loong64" ], @@ -1136,9 +1136,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", - "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", "cpu": [ "mips64el" ], @@ -1153,9 +1153,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", - "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", "cpu": [ "ppc64" ], @@ -1170,9 +1170,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", - "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", "cpu": [ "riscv64" ], @@ -1187,9 +1187,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", - "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", "cpu": [ "s390x" ], @@ -1204,9 +1204,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", - "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", "cpu": [ "x64" ], @@ -1221,9 +1221,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", - "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", "cpu": [ "arm64" ], @@ -1238,9 +1238,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", - "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", "cpu": [ "x64" ], @@ -1255,9 +1255,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", - "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", "cpu": [ "arm64" ], @@ -1272,9 +1272,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", - "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", "cpu": [ "x64" ], @@ -1289,9 +1289,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", - "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", "cpu": [ "arm64" ], @@ -1306,9 +1306,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", - "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", "cpu": [ "x64" ], @@ -1323,9 +1323,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", - "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", "cpu": [ "arm64" ], @@ -1340,9 +1340,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", - "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", "cpu": [ "ia32" ], @@ -1357,9 +1357,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", - "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", "cpu": [ "x64" ], @@ -2194,9 +2194,9 @@ } }, "node_modules/@grpc/grpc-js": { - "version": "1.9.15", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.9.15.tgz", - "integrity": "sha512-nqE7Hc0AzI+euzUwDAy0aY5hCp10r734gMGRdU+qOPX0XSceI2ULrcXB5U2xSc5VkWwalCj4M7GzCAygZl2KoQ==", + "version": "1.9.16", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.9.16.tgz", + "integrity": "sha512-wE4Ut/olIzfKqp631XrG+wbF0v1vWFN4YL9FyXC2LJiG33DsV7PLzURjrCvY/6je2ntdRkeLpPDluzSRGaVltQ==", "license": "Apache-2.0", "dependencies": { "@grpc/proto-loader": "^0.7.8", @@ -6894,6 +6894,106 @@ "dequal": "^2.0.3" } }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -6916,6 +7016,23 @@ "node": ">=4" } }, + "node_modules/ast-types-flow": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", + "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/attr-accept": { "version": "2.2.5", "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.5.tgz", @@ -6925,6 +7042,42 @@ "node": ">=4" } }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axe-core": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.12.1.tgz", + "integrity": "sha512-s7iGf5GaVMxEG0ENN9x+xTr7GFZCb1ZP/1uATUpCEK2X78nDB3RwbtFCo9pGAf9ru+VwoQ464DkaLEeRM08wJA==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/babel-dead-code-elimination": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/babel-dead-code-elimination/-/babel-dead-code-elimination-1.0.12.tgz", @@ -7121,6 +7274,25 @@ "node": ">= 0.8" } }, + "node_modules/call-bind": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -7679,6 +7851,13 @@ "node": ">=12" } }, + "node_modules/damerau-levenshtein": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", + "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", + "dev": true, + "license": "BSD-2-Clause" + }, "node_modules/data-uri-to-buffer": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", @@ -7702,6 +7881,60 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -7790,6 +8023,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/define-lazy-prop": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", @@ -7802,6 +8053,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -7990,6 +8259,75 @@ "is-arrayish": "^0.2.1" } }, + "node_modules/es-abstract": { + "version": "1.24.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", + "integrity": "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -8016,9 +8354,9 @@ "license": "MIT" }, "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -8027,6 +8365,53 @@ "node": ">= 0.4" } }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/es-toolkit": { "version": "1.45.1", "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.45.1.tgz", @@ -8038,9 +8423,9 @@ ] }, "node_modules/esbuild": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", - "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -8051,32 +8436,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.7", - "@esbuild/android-arm": "0.27.7", - "@esbuild/android-arm64": "0.27.7", - "@esbuild/android-x64": "0.27.7", - "@esbuild/darwin-arm64": "0.27.7", - "@esbuild/darwin-x64": "0.27.7", - "@esbuild/freebsd-arm64": "0.27.7", - "@esbuild/freebsd-x64": "0.27.7", - "@esbuild/linux-arm": "0.27.7", - "@esbuild/linux-arm64": "0.27.7", - "@esbuild/linux-ia32": "0.27.7", - "@esbuild/linux-loong64": "0.27.7", - "@esbuild/linux-mips64el": "0.27.7", - "@esbuild/linux-ppc64": "0.27.7", - "@esbuild/linux-riscv64": "0.27.7", - "@esbuild/linux-s390x": "0.27.7", - "@esbuild/linux-x64": "0.27.7", - "@esbuild/netbsd-arm64": "0.27.7", - "@esbuild/netbsd-x64": "0.27.7", - "@esbuild/openbsd-arm64": "0.27.7", - "@esbuild/openbsd-x64": "0.27.7", - "@esbuild/openharmony-arm64": "0.27.7", - "@esbuild/sunos-x64": "0.27.7", - "@esbuild/win32-arm64": "0.27.7", - "@esbuild/win32-ia32": "0.27.7", - "@esbuild/win32-x64": "0.27.7" + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" } }, "node_modules/escalade": { @@ -8167,6 +8552,53 @@ } } }, + "node_modules/eslint-plugin-jsx-a11y": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", + "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "aria-query": "^5.3.2", + "array-includes": "^3.1.8", + "array.prototype.flatmap": "^1.3.2", + "ast-types-flow": "^0.0.8", + "axe-core": "^4.10.0", + "axobject-query": "^4.1.0", + "damerau-levenshtein": "^1.0.8", + "emoji-regex": "^9.2.2", + "hasown": "^2.0.2", + "jsx-ast-utils": "^3.3.5", + "language-tags": "^1.0.9", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "safe-regex-test": "^1.0.3", + "string.prototype.includes": "^2.0.1" + }, + "engines": { + "node": ">=4.0" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-jsx-a11y/node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eslint-plugin-jsx-a11y/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, "node_modules/eslint-plugin-react-hooks": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", @@ -8789,13 +9221,29 @@ "unicode-trie": "^2.0.0" } }, - "node_modules/formdata-polyfill": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", - "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, "license": "MIT", "dependencies": { - "fetch-blob": "^3.1.2" + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" }, "engines": { "node": ">=12.20.0" @@ -8884,12 +9332,56 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/function.prototype.name": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.2.0.tgz", + "integrity": "sha512-jObKIik1P2QjPHP5nz5BaOtUlfgS0fWo8IUByNXkM+o+02sJOi94em77GwJKQSJ3gfPHdgzLNrHc1uokV4P/ew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2", + "hasown": "^2.0.4", + "is-callable": "^1.2.7", + "is-document.all": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/fuzzysort": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/fuzzysort/-/fuzzysort-3.1.0.tgz", "integrity": "sha512-sR9BNCjBg6LNgwvxlBd0sBABvQitkLzoVY9MYYROQVX/FvfJ4Mai9LsGhDgd8qYdds0bY77VzYd5iuB+v5rwQQ==", "license": "MIT" }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -8994,17 +9486,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/get-tsconfig": { - "version": "4.14.0", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", - "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", "dev": true, "license": "MIT", "dependencies": { - "resolve-pkg-maps": "^1.0.0" + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/glob-parent": { @@ -9032,6 +9529,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/goober": { "version": "2.1.18", "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.18.tgz", @@ -9069,6 +9583,19 @@ "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" } }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -9079,6 +9606,35 @@ "node": ">=8" } }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -9091,10 +9647,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/hasown": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", - "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -9310,6 +9882,21 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/internmap": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", @@ -9337,12 +9924,66 @@ "node": ">= 0.10" } }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", "license": "MIT" }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -9356,6 +9997,71 @@ "node": ">=8" } }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-docker": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", @@ -9371,6 +10077,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-document.all": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-document.all/-/is-document.all-1.0.0.tgz", + "integrity": "sha512-+XSoyS05OdBbhFuELhgTCpFNHkpBOJqtsZfUFFpe5QTw+9Sjbh8zitxhQkYAo6wV7e1Vb8cAPvpCk9jGam/82g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -9380,6 +10102,22 @@ "node": ">=0.10.0" } }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -9389,6 +10127,26 @@ "node": ">=8" } }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -9443,6 +10201,32 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-node-process": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", @@ -9458,6 +10242,23 @@ "node": ">=0.12.0" } }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-obj": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-3.0.0.tgz", @@ -9495,6 +10296,25 @@ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", "license": "MIT" }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-regexp": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-3.1.0.tgz", @@ -9507,6 +10327,35 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-stream": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", @@ -9519,6 +10368,57 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-unicode-supported": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", @@ -9537,6 +10437,52 @@ "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==", "license": "MIT" }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-wsl": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", @@ -9745,6 +10691,22 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, "node_modules/jszip": { "version": "3.10.1", "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", @@ -9776,6 +10738,26 @@ "node": ">=6" } }, + "node_modules/language-subtag-registry": { + "version": "0.3.23", + "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", + "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/language-tags": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", + "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", + "dev": true, + "license": "MIT", + "dependencies": { + "language-subtag-registry": "^0.3.20" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -10611,6 +11593,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/object-treeify": { "version": "1.1.33", "resolved": "https://registry.npmjs.org/object-treeify/-/object-treeify-1.1.33.tgz", @@ -10620,6 +11612,65 @@ "node": ">= 10" } }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/obug": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", @@ -10764,6 +11815,24 @@ "integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==", "license": "MIT" }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -10981,6 +12050,16 @@ "fflate": "^0.8.2" } }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/postcss": { "version": "8.5.15", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", @@ -11740,6 +12819,50 @@ "redux": "^5.0.0" } }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -11773,16 +12896,6 @@ "node": ">=4" } }, - "node_modules/resolve-pkg-maps": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", - "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" - } - }, "node_modules/restore-cursor": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", @@ -11919,12 +13032,81 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/safe-array-concat": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.4.tgz", + "integrity": "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "get-intrinsic": "^1.3.0", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-array-concat/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, "node_modules/safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "license": "MIT" }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-push-apply/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -12031,6 +13213,55 @@ "integrity": "sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw==", "license": "MIT" }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", @@ -12269,6 +13500,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/strict-event-emitter": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz", @@ -12301,6 +13546,81 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/string.prototype.includes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", + "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.11.tgz", + "integrity": "sha512-PwvK7BU+CMTJGYQCTZb5RWXIML92lftJLhQz1tBzgKiqGxJaMlBAa48POXaNAC2s4y8jr3EFqrkF9+44neS46w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.2", + "es-object-atoms": "^1.1.2", + "has-property-descriptors": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.10.tgz", + "integrity": "sha512-2+3aDAOmPTmuFwjDnmJG2ctEkQKVki7vOSqaxkv42Mowj1V6PnvuwFCRrR5lChUux1TBskPjfkeTOhqczDMxTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/stringify-object": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-5.0.0.tgz", @@ -12634,14 +13954,13 @@ "license": "0BSD" }, "node_modules/tsx": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", - "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.4.tgz", + "integrity": "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "~0.27.0", - "get-tsconfig": "^4.7.5" + "esbuild": "~0.28.0" }, "bin": { "tsx": "dist/cli.mjs" @@ -12737,6 +14056,84 @@ "node": ">= 0.6" } }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.8.tgz", + "integrity": "sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "for-each": "^0.3.5", + "gopd": "^1.2.0", + "is-typed-array": "^1.1.15", + "possible-typed-array-names": "^1.1.0", + "reflect.getprototypeof": "^1.0.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -12774,6 +14171,25 @@ "typescript": ">=4.8.4 <6.1.0" } }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/undici": { "version": "7.25.0", "resolved": "https://registry.npmjs.org/undici/-/undici-7.25.0.tgz", @@ -13320,6 +14736,102 @@ "node": ">= 8" } }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.22", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.22.tgz", + "integrity": "sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/why-is-node-running": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", @@ -13663,6 +15175,7 @@ "@types/react-dom": "^19", "@vitejs/plugin-react": "^6.0.1", "eslint": "^9.39.2", + "eslint-plugin-jsx-a11y": "^6.10.2", "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.5.2", "globals": "^14.0.0", diff --git a/scripts/backfill-estate-principalid.js b/scripts/backfill-estate-principalid.js new file mode 100644 index 0000000..fabbaee --- /dev/null +++ b/scripts/backfill-estate-principalid.js @@ -0,0 +1,68 @@ +#!/usr/bin/env node +/** + * Backfill estates/{id}.principalId for legacy estates created before the field + * existed. The OpenSign signer=principal change (PR #10) resolves the signer from + * estates/{id}.principalId; an estate missing it now 400s on signing. This sets + * principalId from the authoritative role=="principal" estate_users junction. + * + * DRY RUN by default (read-only: reports counts, writes nothing). + * Apply with --apply. + * + * Usage: + * GOOGLE_APPLICATION_CREDENTIALS=~/.config/gcloud/finalwishes-claude-agent.json \ + * GCLOUD_PROJECT=finalwishes-prod node scripts/backfill-estate-principalid.js [--apply] + * + * Refs: ADR-047, claude-home signer=principal decision 2026-06-14 + */ +const path = require('path'); +const admin = require(path.join(__dirname, '..', 'functions', 'node_modules', 'firebase-admin')); + +const APPLY = process.argv.includes('--apply'); +admin.initializeApp({ projectId: process.env.GCLOUD_PROJECT || 'finalwishes-prod' }); +const db = admin.firestore(); + +(async () => { + const estatesSnap = await db.collection('estates').get(); + let total = 0, hasField = 0, backfilled = 0, unresolved = 0, ambiguous = 0; + const unresolvedIds = []; + + for (const estate of estatesSnap.docs) { + total++; + const data = estate.data(); + if (data.principalId) { hasField++; continue; } + + // Resolve the principal from the authoritative junction. + const principals = await db.collection('estate_users') + .where('estateId', '==', estate.id) + .where('role', '==', 'principal') + .get(); + + if (principals.empty) { unresolved++; unresolvedIds.push(estate.id); continue; } + if (principals.size > 1) { + // More than one principal junction — ambiguous, do NOT guess. + ambiguous++; unresolvedIds.push(estate.id + ' (ambiguous: ' + principals.size + ' principals)'); continue; + } + const principalId = principals.docs[0].data().userId; + if (!principalId) { unresolved++; unresolvedIds.push(estate.id + ' (junction has no userId)'); continue; } + + if (APPLY) { + await estate.ref.update({ principalId, updatedAt: admin.firestore.FieldValue.serverTimestamp() }); + } + backfilled++; + console.log(`${APPLY ? 'SET' : 'WOULD SET'} estates/${estate.id}.principalId = ${principalId}`); + } + + console.log('\n--- backfill summary ---'); + console.log(`mode: ${APPLY ? 'APPLY (writes)' : 'DRY RUN (no writes)'}`); + console.log(`estates total: ${total}`); + console.log(`already had field: ${hasField}`); + console.log(`backfilled: ${backfilled}`); + console.log(`unresolved (no junction / no userId): ${unresolved}`); + console.log(`ambiguous (>1 principal): ${ambiguous}`); + if (unresolvedIds.length) { + console.log('\nEstates needing manual attention:'); + unresolvedIds.forEach((id) => console.log(' - ' + id)); + } + if (!APPLY && backfilled > 0) console.log('\nRe-run with --apply to write these changes.'); + process.exit(0); +})().catch((err) => { console.error('backfill failed:', err); process.exit(1); }); diff --git a/storage.rules b/storage.rules index 5472586..8cf1a18 100644 --- a/storage.rules +++ b/storage.rules @@ -32,11 +32,21 @@ service firebase.storage { // authenticated user read ANY estate's files by guessing the path. (The app reads // via Go-API signed URLs that bypass these rules, so this is defense-in-depth — // but the rule must still be estate-scoped, not merely authenticated.) + // + // email_verified gate (mirror of firestore.rules isEstateRole()): the + // estate_users junction is written by invitation auto-match keyed off an email + // string, so an unverified account on an invited-but-unclaimed address must NOT + // be honored. The SAME junction firestore refuses to honor for an unverified + // user must not be honored by storage for direct downloads. The principal path + // (estates.principalId) is identity the owner already proved, but we require a + // verified email there too for consistency — a principal verifies at signup. function isEstateMember(estateId) { - return request.auth != null && ( - firestore.exists(/databases/(default)/documents/estate_users/$(request.auth.uid + '_' + estateId)) - || firestore.get(/databases/(default)/documents/estates/$(estateId)).data.principalId == request.auth.uid - ); + return request.auth != null + && request.auth.token.email_verified == true + && ( + firestore.exists(/databases/(default)/documents/estate_users/$(request.auth.uid + '_' + estateId)) + || firestore.get(/databases/(default)/documents/estates/$(estateId)).data.principalId == request.auth.uid + ); } // ── Estate Document Vault ── diff --git a/web/.env.production b/web/.env.production index 37c94a0..17c8765 100644 --- a/web/.env.production +++ b/web/.env.production @@ -1 +1,31 @@ VITE_API_URL=https://finalwishes-api-860699311615.us-central1.run.app + +# ───────────────────────────────────────────────────────────────────────────── +# OPTIONAL INTEGRATION ENV (owner-provisioned, build-time) +# +# These power features that are wired in code but stay inert — and HIDE their own +# entry points — until the owner provisions them. Leaving them unset is safe; the UI +# degrades gracefully rather than erroring. +# +# VITE_GOOGLE_OAUTH_CLIENT_ID — Google Photos import on the Heirlooms page. +# When unset, the "Import from Google Photos" button is hidden entirely. +# To enable (FR-905 / ADR-045): +# 1. In Google Cloud Console (project finalwishes-prod) enable the +# "Google Photos Picker API". +# 2. On the OAuth consent screen, add the scope +# .../auth/photospicker.mediaitems.readonly +# 3. Create a Web OAuth client; set Authorized JavaScript origins to +# https://finalwishes-prod.web.app and https://finalwishes.app +# 4. Set the client id below, then rebuild + redeploy the web app: +# VITE_GOOGLE_OAUTH_CLIENT_ID=+
We encountered an error loading this page. This may be a temporary issue with your connection to our servers.
+
If this persists, try refreshing the page or contact support.
{this.state.error && ( -diff --git a/web/src/components/estate/InviteTeamMember.tsx b/web/src/components/estate/InviteTeamMember.tsx index 4c63990..6498274 100644 --- a/web/src/components/estate/InviteTeamMember.tsx +++ b/web/src/components/estate/InviteTeamMember.tsx @@ -30,14 +30,13 @@ const STATUS_BADGES: Record('heir'); const [sending, setSending] = useState(false); @@ -81,7 +80,6 @@ export function InviteTeamMember({ estateId }: InviteTeamMemberProps) { const result = await inviteTeamMember({ estateId, email: email.trim(), - phone: phone.trim() || undefined, fullName: fullName.trim(), role, invitedBy: user.uid, @@ -92,7 +90,6 @@ export function InviteTeamMember({ estateId }: InviteTeamMemberProps) { if (result.success) { setSuccess(`Invitation sent to ${fullName || email}`); setEmail(''); - setPhone(''); setFullName(''); setShowForm(false); refreshInvitations(); @@ -103,9 +100,12 @@ export function InviteTeamMember({ estateId }: InviteTeamMemberProps) { }; const handleRevoke = async (invitationId: string) => { + setError(''); const result = await revokeInvitation(invitationId); if (result.success) { refreshInvitations(); + } else { + setError(result.error || 'Could not revoke this invitation. Please try again.'); } }; @@ -259,17 +259,11 @@ export function InviteTeamMember({ estateId }: InviteTeamMemberProps) { className="w-full bg-white border border-royal/10 rounded-xl px-4 py-3 text-[14px] font-semibold text-royal outline-none focus:border-royal transition-all placeholder:text-royal/20" /> - - - setPhone(e.target.value)} - placeholder="+1 (555) 123-4567" - className="w-full bg-white border border-royal/10 rounded-xl px-4 py-3 text-[14px] font-semibold text-royal outline-none focus:border-royal transition-all placeholder:text-royal/20" - /> -++ + The invitee receives a secure email invitation to join this estate. +
diff --git a/web/src/components/estate/QuorumPanel.tsx b/web/src/components/estate/QuorumPanel.tsx index 724fa8c..70b9354 100644 --- a/web/src/components/estate/QuorumPanel.tsx +++ b/web/src/components/estate/QuorumPanel.tsx @@ -80,7 +80,7 @@ export function QuorumPanel({ estateId }: QuorumPanelProps) { -+ Executor Quorum @@ -99,14 +99,14 @@ export function QuorumPanel({ estateId }: QuorumPanelProps) {{pendingActions.length === 0 && resolvedActions.length === 0 && ( - +
No quorum actions yet. Propose an action that requires multi-executor approval.
)} {pendingActions.length > 0 && (-+
Pending Votes
{pendingActions.map((action) => ( @@ -123,7 +123,7 @@ export function QuorumPanel({ estateId }: QuorumPanelProps) { {resolvedActions.length > 0 && (-+
Resolved
{resolvedActions.slice(0, 5).map((action) => ( @@ -184,16 +184,16 @@ function ActionCard({ } return ( -+-) @@ -328,23 +328,23 @@ function ProposeDialog({- {action.description} + {action.description}-{ACTION_TYPE_LABELS[action.actionType] || action.actionType} +
Proposed by {action.proposedByName} ·{' '} {new Date(action.proposedAt).toLocaleDateString('en-US', { month: 'short', @@ -284,7 +284,7 @@ function ActionCard({ )} {isPending && hasVoted && ( -
You have already voted on this action
+You have already voted on this action
)}+ diff --git a/web/src/components/estate/SectionEmptyState.tsx b/web/src/components/estate/SectionEmptyState.tsx index e0f6f42..4dfaeaf 100644 --- a/web/src/components/estate/SectionEmptyState.tsx +++ b/web/src/components/estate/SectionEmptyState.tsx @@ -242,7 +242,7 @@ export function SectionEmptyState({ {heading || content.heading}
diff --git a/web/src/components/estate/SectionHeader.tsx b/web/src/components/estate/SectionHeader.tsx index 3927487..5348ec5 100644 --- a/web/src/components/estate/SectionHeader.tsx +++ b/web/src/components/estate/SectionHeader.tsx @@ -69,59 +69,64 @@ const SECTION_THEMES: Record= { iconBg: 'rgba(19, 51, 120, 0.10)', }, 'memories': { + // Re-toned off the magenta firewall → gold (heritage warmth, keepsakes). label: 'Memories', tagline: 'Moments worth keeping.', breadcrumb: 'Memories', - accentColor: '#9D174D', - accentLight: 'rgba(157, 23, 77, 0.06)', - gradientFrom: '#FFF1F2', - gradientTo: '#FFE4E6', + accentColor: '#B8860B', + accentLight: 'rgba(184, 134, 11, 0.06)', + gradientFrom: '#FFFBEB', + gradientTo: '#FEF3C7', icon: , - iconBg: 'rgba(157, 23, 77, 0.10)', + iconBg: 'rgba(184, 134, 11, 0.10)', }, 'letters': { + // Re-toned off the green firewall → deep royal (composure, permanence). label: 'Letters', tagline: 'Words that outlast time.', breadcrumb: 'Letters & Directives', - accentColor: '#4D7C4D', - accentLight: 'rgba(77, 124, 77, 0.06)', - gradientFrom: '#F0FDF4', - gradientTo: '#DCFCE7', + accentColor: '#1E3A5F', + accentLight: 'rgba(30, 58, 95, 0.06)', + gradientFrom: '#EEF2FF', + gradientTo: '#E0E7FF', icon: , - iconBg: 'rgba(77, 124, 77, 0.10)', + iconBg: 'rgba(30, 58, 95, 0.10)', }, 'my-people': { + // Re-toned off the teal firewall → heritage royal (trust, lineage). label: 'My People', tagline: 'The ones who matter most.', breadcrumb: 'My People', - accentColor: '#0F766E', - accentLight: 'rgba(15, 118, 110, 0.06)', - gradientFrom: '#F0FDFA', - gradientTo: '#CCFBF1', + accentColor: '#133378', + accentLight: 'rgba(19, 51, 120, 0.06)', + gradientFrom: '#EEF2FF', + gradientTo: '#E0E7FF', icon: , - iconBg: 'rgba(15, 118, 110, 0.10)', + iconBg: 'rgba(19, 51, 120, 0.10)', }, 'the-vault': { + // Re-toned off the slate firewall → deep royal (security, weight). label: 'The Vault', tagline: 'Protected. Organized. Ready.', breadcrumb: 'The Vault', - accentColor: '#334155', - accentLight: 'rgba(51, 65, 85, 0.05)', - gradientFrom: '#F8FAFC', - gradientTo: '#F1F5F9', + accentColor: '#1E3A5F', + accentLight: 'rgba(30, 58, 95, 0.05)', + gradientFrom: '#EEF2FF', + gradientTo: '#E0E7FF', icon: , - iconBg: 'rgba(51, 65, 85, 0.10)', + iconBg: 'rgba(30, 58, 95, 0.10)', }, 'life-chapters': { + // Re-toned off the violet firewall → gold (narrative warmth, chapters). label: 'Life Chapters', tagline: 'Your life, told in chapters.', breadcrumb: 'Your Legacy', - accentColor: '#7C3AED', - accentLight: 'rgba(124, 58, 237, 0.06)', - gradientFrom: '#FAF5FF', - gradientTo: '#F3E8FF', + accentColor: '#C8A951', + accentLight: 'rgba(200, 169, 81, 0.06)', + gradientFrom: '#FFFBEB', + gradientTo: '#FEF3C7', icon: , - iconBg: 'rgba(124, 58, 237, 0.10)', + iconBg: 'rgba(200, 169, 81, 0.10)', }, 'events': { label: 'Events', @@ -221,7 +226,7 @@ export function SectionHeader({ {title || theme.label}
diff --git a/web/src/components/estate/SettlementGantt.tsx b/web/src/components/estate/SettlementGantt.tsx index 9af65d6..001ccdd 100644 --- a/web/src/components/estate/SettlementGantt.tsx +++ b/web/src/components/estate/SettlementGantt.tsx @@ -100,7 +100,7 @@ export function SettlementGantt({ deadlines, dateOfDeath }: SettlementGanttProps{ if (dateOfDeath) { const d = new Date(dateOfDeath) @@ -109,14 +109,14 @@ export function SettlementGantt({ deadlines, dateOfDeath }: SettlementGanttProps } return `Day ${day}` }} - axisLine={{ stroke: 'var(--color-slate-200)' }} + axisLine={{ stroke: '#DCE3EE' }} tickLine={false} /> @@ -125,12 +125,12 @@ export function SettlementGantt({ deadlines, dateOfDeath }: SettlementGanttProps if (!active || !payload?.length) return null const d = payload[0].payload as GanttBar return ( - -{d.name}
-+
+{d.name}
+Due: {d.dueDate}
-+
{d.overdue ? `${Math.abs(d.daysFromNow)} days overdue` : `${d.daysFromNow} days remaining`} @@ -165,7 +165,7 @@ export function SettlementGantt({ deadlines, dateOfDeath }: SettlementGanttProps {/* Legend */} -
+Overdue diff --git a/web/src/components/estate/SettlementPanel.tsx b/web/src/components/estate/SettlementPanel.tsx index 546b1a6..86c1e47 100644 --- a/web/src/components/estate/SettlementPanel.tsx +++ b/web/src/components/estate/SettlementPanel.tsx @@ -27,8 +27,7 @@ import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '@ import { Textarea } from '@/components/ui/textarea' import { toast } from 'sonner' import { auth } from '../../lib/firebase' - -const API_BASE = import.meta.env.VITE_API_URL || 'http://localhost:8080' +import { API_BASE } from '../../lib/client' interface SettlementPanelProps { estateId: string @@ -105,8 +104,8 @@ export function SettlementPanel({ -{settlementReportedAt && ( -This estate is currently in settlement.
-+
This estate is currently in settlement.
+{settlementType === 'death' ? `The passing of ${ownerName || 'the estate owner'} has been reported. Time capsules and final messages are being delivered to their intended recipients.` : `${ownerName || 'The estate owner'} has been reported as incapacitated. Settlement procedures are in progress.`} @@ -121,7 +120,7 @@ export function SettlementPanel({
+
Reported on {new Date(settlementReportedAt).toLocaleDateString('en-US', { year: 'numeric', month: 'long', @@ -131,7 +130,7 @@ export function SettlementPanel({ )}
-Settlement Checklist
+Settlement Checklist
{[ 'Time capsules with settlement triggers delivered', @@ -142,7 +141,7 @@ export function SettlementPanel({ ].map((item, i) => ( ))}@@ -154,8 +153,8 @@ export function SettlementPanel({ // --- Estate is NOT in settlement — show report option --- return ( -- ++ -+
Report a Status Change
-+
If {ownerName || 'the estate owner'} is no longer able to manage their estate, you can begin the settlement process. This will deliver all time capsules with settlement triggers and notify beneficiaries. @@ -186,10 +185,10 @@ export function SettlementPanel({
- + Begin Settlement Process -+ This action will transition the estate into settlement. The following will happen: @@ -202,7 +201,7 @@ export function SettlementPanel({ -