From 141debb5235ac6406a97d1d9f94e471de2b73882 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 27 Sep 2026 22:33:09 +0000 Subject: [PATCH 1/4] API sweep: every route under app/api as four callers; fix what it found scripts/api-audit calls every method of every app/api route (155 in 147 files) against a local production build, as signed out, a child, a parent and a super admin, with no body, an empty JSON object and a body that is not JSON. First sweep: 24 FAIL. After these fixes: 0 FAIL, 30 BLOCKED (an honest 503 for a provider this build has no key for), 11 reviewed. - Google Calendar connect: with no Google credentials the link threw a blank 500; it now returns the parent to the calendar with a toast. - Billing: portal, cancel and change-plan read STRIPE_SECRET_KEY alone, ignoring the key Super Admin -> Stripe Setup saves and checkout uses. All four now resolve it the same way and answer 503 "billing isn't set up" with no key; checkout reads the body before building a client. - Stripe webhook: read STRIPE_WEBHOOK_SECRET alone and built a client with getStripe() just to verify, so with the key in Stripe Setup every real event was answered "signature invalid". It now uses the Setup secret first and Stripe's keyless static verifier. - AI routes: nine let the provider's "not configured" throw become a 500, four a 502 "temporarily unavailable"; each now asks isAIConfigured() first. health/coach checked only the environment and missed a key saved in Admin -> AI Engine. - /api/ai/savings answered a child with advice built from the family's transactions, budgets, bills and subscriptions, which the finance service and the privacy export keep for adults. Now 403. Each fix has a test that fails against the old code. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01776xJyhVe8xJQvTGeNfT9T --- app/api/admin/marketing/ai/route.ts | 5 +- app/api/ai/gift/route.ts | 5 +- app/api/ai/habits/route.ts | 5 +- app/api/ai/health/coach/route.ts | 7 +- app/api/ai/import/route.ts | 5 +- app/api/ai/invest/route.ts | 5 +- app/api/ai/notes/route.ts | 5 +- app/api/ai/relationship/route.ts | 5 +- app/api/ai/savings/route.ts | 6 + app/api/ai/wallet/child/[childId]/route.ts | 5 +- app/api/ai/wallet/route.ts | 5 +- app/api/ai/weekly-briefing/route.ts | 5 +- app/api/billing/cancel/route.ts | 9 +- app/api/billing/change-plan/route.ts | 9 +- app/api/billing/checkout/route.ts | 9 +- app/api/billing/portal/route.ts | 10 +- app/api/google/calendar/auth/route.ts | 10 +- app/api/recipes/suggest/route.ts | 5 +- app/api/recipes/transform/route.ts | 5 +- app/api/vacations/ai/route.ts | 5 +- app/api/webhooks/stripe/route.ts | 13 +- components/modules/calendar-module.tsx | 1 + lib/i18n/messages/de-DE.json | 3 + lib/i18n/messages/en-US.json | 3 + lib/i18n/messages/es-ES.json | 3 + lib/i18n/messages/fr-FR.json | 3 + lib/i18n/messages/it-IT.json | 3 + lib/i18n/messages/nl-NL.json | 3 + lib/i18n/messages/pt-PT.json | 3 + lib/stripe.ts | 9 + scripts/api-audit/sessions.mjs | 87 ++++++++++ scripts/api-audit/summarize.mjs | 53 ++++++ scripts/api-audit/sweep.mjs | 86 ++++++++++ ...t-given-the-familys-savings-advice.test.ts | 50 ++++++ ...a-write-route-is-gated-and-bounded.test.ts | 4 +- ...its-journal-observability-outcomes.test.ts | 2 +- .../ai-savings-observability-outcomes.test.ts | 2 +- tests/ai-savings-response-integrity.test.ts | 2 +- ...ly-briefing-observability-outcomes.test.ts | 2 +- ...says-when-the-engine-is-not-set-up.test.ts | 58 +++++++ tests/api-vacations-ai-output.test.ts | 2 +- tests/billing-price-verification.test.ts | 10 +- ...e-stripe-keys-the-admin-configured.test.ts | 161 ++++++++++++++++++ ...lendar-connect-without-credentials.test.ts | 67 ++++++++ tests/relationship-ai-gift-history.test.ts | 2 +- 45 files changed, 719 insertions(+), 38 deletions(-) create mode 100644 scripts/api-audit/sessions.mjs create mode 100644 scripts/api-audit/summarize.mjs create mode 100644 scripts/api-audit/sweep.mjs create mode 100644 tests/a-child-is-not-given-the-familys-savings-advice.test.ts create mode 100644 tests/an-ai-route-says-when-the-engine-is-not-set-up.test.ts create mode 100644 tests/billing-uses-the-stripe-keys-the-admin-configured.test.ts create mode 100644 tests/google-calendar-connect-without-credentials.test.ts diff --git a/app/api/admin/marketing/ai/route.ts b/app/api/admin/marketing/ai/route.ts index cd3494ca6..bd6960aeb 100644 --- a/app/api/admin/marketing/ai/route.ts +++ b/app/api/admin/marketing/ai/route.ts @@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from 'next/server'; import { settleAll } from '@/lib/supabase/settle'; import { getTranslations } from '@/lib/i18n/server'; import { requireMarketingAdmin, logMarketingAudit, isMarketingAuthError, marketingRefusalBody } from '@/lib/marketing/admin'; -import { resolveProvider } from '@/lib/ai/provider'; +import { resolveProvider, isAIConfigured } from '@/lib/ai/provider'; import { getMarketingCustomers, summarizeCustomers } from '@/lib/marketing/customers'; import { fmtMoney } from '@/lib/utils/format'; import { enforceAIRateLimit } from '@/lib/server/ai-rate-limit'; @@ -33,6 +33,9 @@ export async function POST(req: NextRequest) { const t = await getTranslations(); try { const { supabase, actorId, actorEmail } = await requireMarketingAdmin(); + // No key: say so before any work, rather than let the provider's throw + // reach the catch below as a generic failure. + if (!(await isAIConfigured())) return NextResponse.json({ error: t('ai.theAiEngineIsnT'), code: 'not_configured' }, { status: 503 }); const limited = await enforceAIRateLimit(supabase, `marketing-ai:${actorId}`, { limit: 20 }); if (!limited.ok) return NextResponse.json( { error: t('ai.tooManyMarketingAiRequests') }, diff --git a/app/api/ai/gift/route.ts b/app/api/ai/gift/route.ts index 21dc9c2c9..cd1cdfcfd 100644 --- a/app/api/ai/gift/route.ts +++ b/app/api/ai/gift/route.ts @@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from 'next/server'; import { getTranslations } from '@/lib/i18n/server'; import { createServiceClient } from '@/lib/supabase/server'; import { settleAll, describeReadError } from '@/lib/supabase/settle'; -import { resolveProvider } from '@/lib/ai/provider'; +import { resolveProvider, isAIConfigured } from '@/lib/ai/provider'; import { rateLimit, clientIp } from '@/lib/server/rate-limit'; import { rateLimitDb } from '@/lib/server/rate-limit-db'; import { buildGiftAssistPrompt, parseGiftSuggestions } from '@/lib/wallet/gift-ai'; @@ -38,6 +38,9 @@ export async function POST(req: NextRequest) { if (!token) return NextResponse.json({ error: t('gift.missingGiftLink') }, { status: 400 }); const relationship = typeof body.relationship === 'string' ? body.relationship.slice(0, 40).trim() || null : null; + // No key: say so before any work, rather than let the provider's throw + // reach the catch below as a generic failure. + if (!(await isAIConfigured())) return NextResponse.json({ error: t('ai.theAiEngineIsnT'), code: 'not_configured' }, { status: 503 }); // The twin of C1-S9-31, in the same feature: /pay/ redirects here. // A refused read left `link` null and produced "This gift link is no longer // available" to someone outside the family trying to send money — a dead end diff --git a/app/api/ai/habits/route.ts b/app/api/ai/habits/route.ts index 0bf4b70bd..7efbd4da5 100644 --- a/app/api/ai/habits/route.ts +++ b/app/api/ai/habits/route.ts @@ -5,7 +5,7 @@ import { readAll } from '@/lib/supabase/read-all'; import { withAiRequest } from '@/lib/ai/observability'; import { scopeFromUserContext } from '@/lib/services/scope'; import { requireUserContext } from '@/lib/supabase/auth'; -import { resolveProvider } from '@/lib/ai/provider'; +import { resolveProvider, isAIConfigured } from '@/lib/ai/provider'; import { enforceAIRateLimit } from '@/lib/server/ai-rate-limit'; import { buildCoachPrompt, parseCoachResponse, type HabitStat } from '@/lib/habits/ai'; import { @@ -21,6 +21,9 @@ export async function POST() { const ctx = await requireUserContext(); const { familyId } = ctx.active; const supabase = await createServer(); + // No key: say so before any work, rather than let the provider's throw + // reach the catch below as a generic failure. + if (!(await isAIConfigured())) return NextResponse.json({ error: t('ai.theAiEngineIsnT'), code: 'not_configured' }, { status: 503 }); const limited = await enforceAIRateLimit(supabase, `ai-habits:${ctx.user.id}`, { limit: 15 }); if (!limited.ok) return NextResponse.json( { error: t('habits.tooManyHabitCoachRequests') }, diff --git a/app/api/ai/health/coach/route.ts b/app/api/ai/health/coach/route.ts index 4ad013d13..497dd3b0f 100644 --- a/app/api/ai/health/coach/route.ts +++ b/app/api/ai/health/coach/route.ts @@ -6,7 +6,7 @@ import { settleAll } from '@/lib/supabase/settle'; import { refuseUnlessEntitled } from '@/lib/server/route-feature-gate'; import { withAiRequest } from '@/lib/ai/observability'; import { scopeFromUserContext } from '@/lib/services/scope'; -import { resolveProvider, describeAIError } from '@/lib/ai/provider'; +import { resolveProvider, describeAIError, isAIConfigured } from '@/lib/ai/provider'; import { enforceAIRateLimit } from '@/lib/server/ai-rate-limit'; import { MAX_PROVIDER_JSON_BYTES, readBoundedRequestJsonOrEmpty } from '@/lib/server/bounded-request-body'; @@ -25,7 +25,10 @@ export async function POST(req: Request) { let ctx; try { ctx = await requireUserContext(); } catch { return NextResponse.json({ error: t('coach.unauthorized') }, { status: 401 }); } - if (!process.env.ANTHROPIC_API_KEY && !process.env.OPENAI_API_KEY) { + // isAIConfigured, not the environment alone: a key saved in Admin → AI Engine + // is the one resolveProvider uses, and reading only the environment told a + // family with that key that the engine was not set up. + if (!(await isAIConfigured())) { return NextResponse.json({ error: t('coach.theAiEngineIsnT') }, { status: 503 }); } diff --git a/app/api/ai/import/route.ts b/app/api/ai/import/route.ts index 76540cdd6..815d8b1e8 100644 --- a/app/api/ai/import/route.ts +++ b/app/api/ai/import/route.ts @@ -3,7 +3,7 @@ import { getTranslations } from '@/lib/i18n/server'; import { createServer } from '@/lib/supabase/server'; import { refuseUnlessEntitled } from '@/lib/server/route-feature-gate'; import { requireUserContext } from '@/lib/supabase/auth'; -import { resolveProvider } from '@/lib/ai/provider'; +import { resolveProvider, isAIConfigured } from '@/lib/ai/provider'; import { withAiRequest } from '@/lib/ai/observability'; import { scopeFromUserContext } from '@/lib/services/scope'; import { AI_TOOLS, runAction } from '@/lib/ai/actions'; @@ -134,6 +134,9 @@ export async function POST(req: NextRequest) { const refused = await refuseUnlessEntitled(supabase, ctx.active.familyId, ['/dashboard/inbox']); if (refused) return refused; + // No key: say so before any work, rather than let the provider's throw + // reach the catch below as a generic failure. + if (!(await isAIConfigured())) return NextResponse.json({ error: t('ai.theAiEngineIsnT'), code: 'not_configured' }, { status: 503 }); const limited = await enforceAIRateLimit(supabase, `ai-import:${userId}`, { limit: 20 }); if (!limited.ok) { return NextResponse.json( diff --git a/app/api/ai/invest/route.ts b/app/api/ai/invest/route.ts index aa47bd66a..30b421f4e 100644 --- a/app/api/ai/invest/route.ts +++ b/app/api/ai/invest/route.ts @@ -3,7 +3,7 @@ import { getTranslations } from '@/lib/i18n/server'; import { createServer } from '@/lib/supabase/server'; import { settleAll } from '@/lib/supabase/settle'; import { requireUserContext, effectivePlanLevel } from '@/lib/supabase/auth'; -import { resolveProvider } from '@/lib/ai/provider'; +import { resolveProvider, isAIConfigured } from '@/lib/ai/provider'; import { withAiRequest } from '@/lib/ai/observability'; import { scopeFromUserContext, dayKeyInTz, zonedDayBoundsMs } from '@/lib/services/scope'; import { resolveFamilyPlanLevel } from '@/lib/server/plan'; @@ -29,6 +29,9 @@ export async function POST(req: NextRequest) { if (aiCoachLevel(tier) === 'none') { return NextResponse.json({ error: t('invest.theMoneyMentorIsAvailable') }, { status: 403 }); } + // With no key the provider threw "not configured" and the catch below + // answered a generic 500; the other AI routes say so with a 503 first. + if (!(await isAIConfigured())) return NextResponse.json({ error: t('ai.theAiEngineIsnT'), code: 'not_configured' }, { status: 503 }); const limited = await enforceAIRateLimit(supabase, `ai-invest:${ctx.user.id}`, { limit: 10 }); if (!limited.ok) return NextResponse.json( { error: t('invest.tooManyMoneyMentorRequests') }, diff --git a/app/api/ai/notes/route.ts b/app/api/ai/notes/route.ts index e727bf15d..c23ffe64d 100644 --- a/app/api/ai/notes/route.ts +++ b/app/api/ai/notes/route.ts @@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from 'next/server'; import { getTranslations } from '@/lib/i18n/server'; import { requireUserContext } from '@/lib/supabase/auth'; import { createServer } from '@/lib/supabase/server'; -import { resolveProvider } from '@/lib/ai/provider'; +import { resolveProvider, isAIConfigured } from '@/lib/ai/provider'; import { withAiRequest } from '@/lib/ai/observability'; import { scopeFromUserContext } from '@/lib/services/scope'; import { buildNotesPrompt, parseNotesResponse } from '@/lib/notes/ai'; @@ -18,6 +18,9 @@ export async function POST(req: NextRequest) { try { const ctx = await requireUserContext(); const supabase = await createServer(); + // No key: say so before any work, rather than let the provider's throw + // reach the catch below as a generic failure. + if (!(await isAIConfigured())) return NextResponse.json({ error: t('ai.theAiEngineIsnT'), code: 'not_configured' }, { status: 503 }); const limited = await enforceAIRateLimit(supabase, `ai-notes:${ctx.user.id}`, { limit: 20 }); if (!limited.ok) return NextResponse.json( { error: t('notes.tooManyNoteAnalysisRequests') }, diff --git a/app/api/ai/relationship/route.ts b/app/api/ai/relationship/route.ts index 592d86f00..b9aa21610 100644 --- a/app/api/ai/relationship/route.ts +++ b/app/api/ai/relationship/route.ts @@ -3,7 +3,7 @@ import { getTranslations } from '@/lib/i18n/server'; import { createServer } from '@/lib/supabase/server'; import { settleAll, describeReadError } from '@/lib/supabase/settle'; import { requireUserContext } from '@/lib/supabase/auth'; -import { resolveProvider } from '@/lib/ai/provider'; +import { resolveProvider, isAIConfigured } from '@/lib/ai/provider'; import { withAiRequest } from '@/lib/ai/observability'; import { scopeFromUserContext, dayKeyInTz, todayKeyFor, zonedDayBoundsMs } from '@/lib/services/scope'; import { isMissingRelationError } from '@/lib/supabase/errors'; @@ -30,6 +30,9 @@ export async function POST() { const ctx = await requireUserContext(); const familyId = ctx.active.familyId; const supabase = await createServer(); + // With no key the provider threw "not configured" and the catch below + // answered a generic 500; the other AI routes say so with a 503 first. + if (!(await isAIConfigured())) return NextResponse.json({ error: t('ai.theAiEngineIsnT'), code: 'not_configured' }, { status: 503 }); const limited = await enforceAIRateLimit(supabase, `ai-relationship:${ctx.user.id}`, { limit: 10 }); if (!limited.ok) return NextResponse.json( { error: t('relationship.tooManyRelationshipHelperRequests') }, diff --git a/app/api/ai/savings/route.ts b/app/api/ai/savings/route.ts index 2ec23a2ee..0cf8a9fa2 100644 --- a/app/api/ai/savings/route.ts +++ b/app/api/ai/savings/route.ts @@ -5,6 +5,7 @@ import { refuseUnlessEntitled } from '@/lib/server/route-feature-gate'; import { settleAll, describeReadError } from '@/lib/supabase/settle'; import { readAllAsQuery } from '@/lib/supabase/read-all'; import { requireUserContext } from '@/lib/supabase/auth'; +import { isManager } from '@/lib/constants/roles'; import { withAiRequest } from '@/lib/ai/observability'; import { scopeFromUserContext } from '@/lib/services/scope'; import { resolveProvider } from '@/lib/ai/provider'; @@ -26,6 +27,11 @@ export async function POST() { const tr = await getTranslations(); const ctx = await requireUserContext(); const { familyId } = ctx.active; + // The family's money is the adults': the finance service refuses anyone else + // and the privacy export withholds `finances` from them. This route reads the + // same tables through its own queries, and answered a child with the + // family's overspending and subscriptions. + if (!isManager(ctx.active.role)) return NextResponse.json({ error: tr('savings.privateToTheAdults') }, { status: 403 }); const supabase = await createServer(); // The page in front of this is feature-gated; this endpoint was not, and it // calls a model. Same resolver, so the two cannot disagree. diff --git a/app/api/ai/wallet/child/[childId]/route.ts b/app/api/ai/wallet/child/[childId]/route.ts index 06e3ece47..6a99f3d24 100644 --- a/app/api/ai/wallet/child/[childId]/route.ts +++ b/app/api/ai/wallet/child/[childId]/route.ts @@ -3,7 +3,7 @@ import { getTranslations } from '@/lib/i18n/server'; import { createServer } from '@/lib/supabase/server'; import { settleAll, describeReadError } from '@/lib/supabase/settle'; import { requireUserContext, effectivePlanLevel } from '@/lib/supabase/auth'; -import { resolveProvider } from '@/lib/ai/provider'; +import { resolveProvider, isAIConfigured } from '@/lib/ai/provider'; import { withAiRequest } from '@/lib/ai/observability'; import { scopeFromUserContext, dayKeyInTz, zonedDayBoundsMs } from '@/lib/services/scope'; import { resolveFamilyPlanLevel } from '@/lib/server/plan'; @@ -24,6 +24,9 @@ export async function POST(_req: Request, { params }: { params: Promise<{ childI const ctx = await requireUserContext(); const familyId = ctx.active.familyId; const supabase = await createServer(); + // No key: say so before any work, rather than let the provider's throw + // reach the catch below as a generic failure. + if (!(await isAIConfigured())) return NextResponse.json({ error: tr('ai.theAiEngineIsnT'), code: 'not_configured' }, { status: 503 }); const limited = await enforceAIRateLimit(supabase, `ai-wallet-child:${ctx.user.id}:${childId}`, { limit: 10 }); if (!limited.ok) return NextResponse.json( { error: tr('child.tooManyChildMoneyCoach') }, diff --git a/app/api/ai/wallet/route.ts b/app/api/ai/wallet/route.ts index 3d5323880..cecab97ca 100644 --- a/app/api/ai/wallet/route.ts +++ b/app/api/ai/wallet/route.ts @@ -3,7 +3,7 @@ import { getTranslations } from '@/lib/i18n/server'; import { createServer } from '@/lib/supabase/server'; import { settleAll } from '@/lib/supabase/settle'; import { requireUserContext, effectivePlanLevel } from '@/lib/supabase/auth'; -import { resolveProvider } from '@/lib/ai/provider'; +import { resolveProvider, isAIConfigured } from '@/lib/ai/provider'; import { withAiRequest } from '@/lib/ai/observability'; import { scopeFromUserContext, dayKeyInTz, zonedDayBoundsMs } from '@/lib/services/scope'; import { resolveFamilyPlanLevel } from '@/lib/server/plan'; @@ -28,6 +28,9 @@ export async function POST() { if (aiCoachLevel(tier) === 'none') { return NextResponse.json({ error: tr('wallet.theAiMoneyCoachIs') }, { status: 403 }); } + // With no key the provider threw "not configured" and the catch below + // answered a generic 500; the other AI routes say so with a 503 first. + if (!(await isAIConfigured())) return NextResponse.json({ error: tr('ai.theAiEngineIsnT'), code: 'not_configured' }, { status: 503 }); const limited = await enforceAIRateLimit(supabase, `ai-wallet:${ctx.user.id}`, { limit: 10 }); if (!limited.ok) return NextResponse.json( { error: tr('wallet.tooManyAiMoneyCoach') }, diff --git a/app/api/ai/weekly-briefing/route.ts b/app/api/ai/weekly-briefing/route.ts index 8e3bb97ee..ba0f62427 100644 --- a/app/api/ai/weekly-briefing/route.ts +++ b/app/api/ai/weekly-briefing/route.ts @@ -8,7 +8,7 @@ import { requireUserContext, effectivePlanLevel } from '@/lib/supabase/auth'; import { resolveFamilyPlanLevel } from '@/lib/server/plan'; import { weekWindow, weekRangeLabel, choreCompletionRate, bucketByDay, dayLoad } from '@/lib/ai/weekly'; import { dayKeyInZone } from '@/lib/schedule/zoned'; -import { resolveProvider } from '@/lib/ai/provider'; +import { resolveProvider, isAIConfigured } from '@/lib/ai/provider'; import { enforceAIRateLimit } from '@/lib/server/ai-rate-limit'; import { MAX_SMALL_JSON_BYTES, readBoundedRequestJsonOrEmpty } from '@/lib/server/bounded-request-body'; @@ -29,6 +29,9 @@ export async function POST(req: NextRequest) { if ((await effectivePlanLevel(await resolveFamilyPlanLevel(supabase, familyId))) < 2) { return NextResponse.json({ error: t('weeklyBriefing.weeklyAiBriefingIsA') }, { status: 402 }); } + // With no key the provider threw "not configured" and the catch below + // answered a generic 500; the other AI routes say so with a 503 first. + if (!(await isAIConfigured())) return NextResponse.json({ error: t('ai.theAiEngineIsnT'), code: 'not_configured' }, { status: 503 }); const limited = await enforceAIRateLimit(supabase, `ai-weekly-briefing:${ctx.user.id}`, { limit: 5 }); if (!limited.ok) return NextResponse.json( { error: t('weeklyBriefing.tooManyWeeklyBriefingRequests') }, diff --git a/app/api/billing/cancel/route.ts b/app/api/billing/cancel/route.ts index 1cad7405f..363daf432 100644 --- a/app/api/billing/cancel/route.ts +++ b/app/api/billing/cancel/route.ts @@ -3,7 +3,8 @@ import { getTranslations } from '@/lib/i18n/server'; import { requireUserContext } from '@/lib/supabase/auth'; import { createServer, createServiceClient } from '@/lib/supabase/server'; import { wroteNoRows } from '@/lib/supabase/errors'; -import { getStripe } from '@/lib/stripe'; +import { stripeFromKey } from '@/lib/stripe'; +import { getStripeSettings, effectiveSecretKey } from '@/lib/stripe/settings'; import { isAdmin } from '@/lib/constants/roles'; import { enforceRequestRateLimit } from '@/lib/server/request-rate-limit'; import { readBoundedRequestJson } from '@/lib/server/bounded-request-body'; @@ -57,7 +58,11 @@ export async function POST(req: NextRequest) { { error: t('cancel.tooManyBillingRequestsPlease') }, { status: 429, headers: { 'Retry-After': String(limited.retryAfter) } }, ); - await getStripe().subscriptions.update(sub.provider_ref, { cancel_at_period_end: cancelAtPeriodEnd }); + // The key checkout used: Super Admin → Stripe Setup first, then the + // environment (getStripe() read only the environment). + const secretKey = effectiveSecretKey(await getStripeSettings()); + if (!secretKey) return NextResponse.json({ error: t('checkout.billingIsNotSetUp') }, { status: 503 }); + await stripeFromKey(secretKey).subscriptions.update(sub.provider_ref, { cancel_at_period_end: cancelAtPeriodEnd }); // Stripe has already changed by here. A sync matching no rows left the local // row saying the opposite of what the family just chose — "cancels at period diff --git a/app/api/billing/change-plan/route.ts b/app/api/billing/change-plan/route.ts index 6382af5a3..c93a5f65d 100644 --- a/app/api/billing/change-plan/route.ts +++ b/app/api/billing/change-plan/route.ts @@ -3,7 +3,8 @@ import { getTranslations } from '@/lib/i18n/server'; import { getUserContext, requireUserContext, type UserContext } from '@/lib/supabase/auth'; import { createServer, createServiceClient } from '@/lib/supabase/server'; import { wroteNoRows } from '@/lib/supabase/errors'; -import { getStripe, STRIPE_PLANS } from '@/lib/stripe'; +import { stripeFromKey, STRIPE_PLANS } from '@/lib/stripe'; +import { getStripeSettings, effectiveSecretKey } from '@/lib/stripe/settings'; import { isAdmin } from '@/lib/constants/roles'; import { canChangeSubscriptionInPlace, slugToStripePlan } from '@/lib/billing/plans'; import { canonicalStripePlan, isStripePlanKey, verifyStripePlanPrice } from '@/lib/billing/price-catalog'; @@ -86,7 +87,11 @@ export async function POST(req: NextRequest) { { error: t('changePlan.tooManyBillingRequestsPlease') }, { status: 429, headers: { 'Retry-After': String(limited.retryAfter) } }, ); - const stripe = getStripe(); + // The key checkout used: Super Admin → Stripe Setup first, then the + // environment (getStripe() read only the environment). + const secretKey = effectiveSecretKey(await getStripeSettings()); + if (!secretKey) return NextResponse.json({ error: t('checkout.billingIsNotSetUp') }, { status: 503 }); + const stripe = stripeFromKey(secretKey); if (!await verifyStripePlanPrice(stripe, plan, priceId)) { console.error('[billing-change-plan] Configured Stripe price is unavailable or does not match the plan'); diff --git a/app/api/billing/checkout/route.ts b/app/api/billing/checkout/route.ts index 5ed9e0b55..90dac9275 100644 --- a/app/api/billing/checkout/route.ts +++ b/app/api/billing/checkout/route.ts @@ -24,8 +24,6 @@ export async function POST(req: NextRequest) { } const familyId = ctx.active.familyId; const supabase = await createServer(); - const stripeSettings = await getStripeSettings(); - const stripe = stripeFromKey(effectiveSecretKey(stripeSettings)); const body = await readBoundedRequestJson(req, MAX_BILLING_REQUEST_BYTES); if (!body.ok) { @@ -39,6 +37,13 @@ export async function POST(req: NextRequest) { const priceId = STRIPE_PLANS[plan]; if (!priceId) return NextResponse.json({ error: t('checkout.invalidPlan') }, { status: 400 }); + // Super Admin → Stripe Setup first, then the environment. With neither, + // stripeFromKey threw and the catch below answered a generic 500. + const stripeSettings = await getStripeSettings(); + const secretKey = effectiveSecretKey(stripeSettings); + if (!secretKey) return NextResponse.json({ error: t('checkout.billingIsNotSetUp') }, { status: 503 }); + const stripe = stripeFromKey(secretKey); + const limited = await enforceRequestRateLimit(supabase, `billing:checkout:${familyId}:${ctx.user.id}`, { limit: 10 }); if (!limited.ok) return NextResponse.json( { error: t('checkout.tooManyBillingRequestsPlease') }, diff --git a/app/api/billing/portal/route.ts b/app/api/billing/portal/route.ts index 3c9d75a8c..e29dd6cf0 100644 --- a/app/api/billing/portal/route.ts +++ b/app/api/billing/portal/route.ts @@ -2,7 +2,8 @@ import { NextRequest, NextResponse } from 'next/server'; import { getTranslations } from '@/lib/i18n/server'; import { requireUserContext } from '@/lib/supabase/auth'; import { createServer } from '@/lib/supabase/server'; -import { getStripe } from '@/lib/stripe'; +import { stripeFromKey } from '@/lib/stripe'; +import { getStripeSettings, effectiveSecretKey } from '@/lib/stripe/settings'; import { isAdmin } from '@/lib/constants/roles'; import { enforceRequestRateLimit } from '@/lib/server/request-rate-limit'; @@ -15,7 +16,12 @@ export async function POST(req: NextRequest) { } const familyId = ctx.active.familyId; const supabase = await createServer(); - const stripe = getStripe(); + // The same key checkout used: Super Admin → Stripe Setup first, then the + // environment. getStripe() read only the environment, so a family that + // subscribed with the configured key could not open the portal. + const secretKey = effectiveSecretKey(await getStripeSettings()); + if (!secretKey) return NextResponse.json({ error: t('checkout.billingIsNotSetUp') }, { status: 503 }); + const stripe = stripeFromKey(secretKey); const { data, error: billingCustomerError } = await supabase .from('billing_customers') diff --git a/app/api/google/calendar/auth/route.ts b/app/api/google/calendar/auth/route.ts index 1a2395830..85efb5f88 100644 --- a/app/api/google/calendar/auth/route.ts +++ b/app/api/google/calendar/auth/route.ts @@ -1,7 +1,7 @@ import { NextRequest, NextResponse } from 'next/server'; import { randomBytes } from 'node:crypto'; import { requireUserContext } from '@/lib/supabase/auth'; -import { getGoogleOAuthUrl } from '@/lib/google'; +import { getGoogleOAuthUrl, googleClientId, googleClientSecret } from '@/lib/google'; // Redirects the user to Google's OAuth consent screen (Calendar read scope). // @@ -13,6 +13,14 @@ import { getGoogleOAuthUrl } from '@/lib/google'; export async function GET(req: NextRequest) { await requireUserContext(); // must be signed in; identity comes from the session + // Without both credentials Google refuses the consent request, and letting + // getGoogleOAuthUrl's error escape left the parent on a blank 500 page. Hand + // them back to the calendar, which says the connection isn't set up — the + // same answer the Outlook link beside it gives (/api/sync/[provider]/auth). + if (!googleClientId() || !googleClientSecret()) { + return NextResponse.redirect(new URL('/dashboard/calendar?gcal=not_configured', req.nextUrl.origin)); + } + const state = randomBytes(32).toString('base64url'); const res = NextResponse.redirect(getGoogleOAuthUrl(state, req.nextUrl.origin)); res.cookies.set('gcal_oauth_state', state, { diff --git a/app/api/recipes/suggest/route.ts b/app/api/recipes/suggest/route.ts index 68cc23578..767f45e5a 100644 --- a/app/api/recipes/suggest/route.ts +++ b/app/api/recipes/suggest/route.ts @@ -3,7 +3,7 @@ import { getTranslations } from '@/lib/i18n/server'; import { requireUserContext } from '@/lib/supabase/auth'; import { createServer } from '@/lib/supabase/server'; import { enforceAIRateLimit } from '@/lib/server/ai-rate-limit'; -import { resolveProvider } from '@/lib/ai/provider'; +import { resolveProvider, isAIConfigured } from '@/lib/ai/provider'; import { withAiRequest } from '@/lib/ai/observability'; import { scopeFromUserContext } from '@/lib/services/scope'; import { buildSuggestPrompt, parseSuggestions, type VaultRecipeLite } from '@/lib/recipes/suggest'; @@ -20,6 +20,9 @@ export async function POST(req: NextRequest) { try { ctx = await requireUserContext(); } catch { return NextResponse.json({ error: t('suggest.unauthorized') }, { status: 401 }); } const supabase = await createServer(); + // No key: say so before any work, rather than let the provider's throw + // reach the catch below as a generic failure. + if (!(await isAIConfigured())) return NextResponse.json({ error: t('ai.theAiEngineIsnT'), code: 'not_configured' }, { status: 503 }); const limited = await enforceAIRateLimit(supabase, `ai-recipe-suggest:${ctx.user.id}`, { limit: 15 }); if (!limited.ok) return NextResponse.json( { error: t('suggest.tooManyRecipeSuggestionsPlease') }, diff --git a/app/api/recipes/transform/route.ts b/app/api/recipes/transform/route.ts index 45d5fe75f..a504b1135 100644 --- a/app/api/recipes/transform/route.ts +++ b/app/api/recipes/transform/route.ts @@ -3,7 +3,7 @@ import { getTranslations } from '@/lib/i18n/server'; import { requireUserContext } from '@/lib/supabase/auth'; import { createServer } from '@/lib/supabase/server'; import { enforceAIRateLimit } from '@/lib/server/ai-rate-limit'; -import { resolveProvider } from '@/lib/ai/provider'; +import { resolveProvider, isAIConfigured } from '@/lib/ai/provider'; import { withAiRequest } from '@/lib/ai/observability'; import { scopeFromUserContext } from '@/lib/services/scope'; import { buildTransformPrompt, parseTransformResult, getRecipeAiAction, type RecipeAiActionId } from '@/lib/recipes/ai-actions'; @@ -23,6 +23,9 @@ export async function POST(req: NextRequest) { try { ctx = await requireUserContext(); } catch { return NextResponse.json({ error: t('transform.unauthorized') }, { status: 401 }); } const supabase = await createServer(); + // No key: say so before any work, rather than let the provider's throw + // reach the catch below as a generic failure. + if (!(await isAIConfigured())) return NextResponse.json({ error: t('ai.theAiEngineIsnT'), code: 'not_configured' }, { status: 503 }); const limited = await enforceAIRateLimit(supabase, `ai-recipe-transform:${ctx.user.id}`, { limit: 12 }); if (!limited.ok) return NextResponse.json( { error: t('transform.tooManyRecipeTransformationsPlease') }, diff --git a/app/api/vacations/ai/route.ts b/app/api/vacations/ai/route.ts index d715169e0..c5bce85ad 100644 --- a/app/api/vacations/ai/route.ts +++ b/app/api/vacations/ai/route.ts @@ -5,7 +5,7 @@ import { settleAll } from '@/lib/supabase/settle'; import { createServer } from '@/lib/supabase/server'; import { refuseUnlessEntitled } from '@/lib/server/route-feature-gate'; import { enforceAIRateLimit } from '@/lib/server/ai-rate-limit'; -import { resolveProvider } from '@/lib/ai/provider'; +import { resolveProvider, isAIConfigured } from '@/lib/ai/provider'; import { withAiRequest } from '@/lib/ai/observability'; import { scopeFromUserContext } from '@/lib/services/scope'; import { summarizeBudget } from '@/lib/vacations/budget'; @@ -37,6 +37,9 @@ export async function POST(req: NextRequest) { // Same resolver, so the two cannot disagree. const refused = await refuseUnlessEntitled(supabase, ctx.active.familyId, ['/dashboard/vacations']); if (refused) return refused; + // No key: say so before any work, rather than let the provider's throw + // reach the catch below as a generic failure. + if (!(await isAIConfigured())) return NextResponse.json({ error: t('ai.theAiEngineIsnT'), code: 'not_configured' }, { status: 503 }); const limited = await enforceAIRateLimit(supabase, `ai-vacations:${ctx.user.id}`, { limit: 20 }); if (!limited.ok) return NextResponse.json( { error: t('ai.tooManyTripAiRequests') }, diff --git a/app/api/webhooks/stripe/route.ts b/app/api/webhooks/stripe/route.ts index 49a473cc4..d28218bab 100644 --- a/app/api/webhooks/stripe/route.ts +++ b/app/api/webhooks/stripe/route.ts @@ -1,6 +1,7 @@ import { NextRequest, NextResponse } from 'next/server'; import { getTranslations } from '@/lib/i18n/server'; -import { getStripe } from '@/lib/stripe'; +import { constructWebhookEvent } from '@/lib/stripe'; +import { getStripeSettings, effectiveWebhookSecret } from '@/lib/stripe/settings'; import { createServiceClient } from '@/lib/supabase/server'; import { settleAll } from '@/lib/supabase/settle'; import { markReferralConverted, rewardConvertedReferral } from '@/lib/referrals/server'; @@ -139,13 +140,17 @@ export async function POST(req: NextRequest) { if (!boundedBody.ok) return NextResponse.json({ error: boundedBody.reason === 'too_large' ? 'Payload too large' : 'Unable to read payload' }, { status: boundedBody.reason === 'too_large' ? 413 : 400 }); const body = boundedBody.text; const sig = req.headers.get('stripe-signature') ?? ''; - const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET ?? ''; + // Super Admin → Stripe Setup's signing secret first, then the environment — + // the order that page promises. Reading only the environment ignored a + // secret saved there, and building the client with getStripe() threw when + // STRIPE_SECRET_KEY was unset, which the catch below reported as a bad + // signature: every real event refused, and no subscription ever recorded. + const webhookSecret = effectiveWebhookSecret(await getStripeSettings()) ?? ''; if (!webhookSecret) return NextResponse.json({ error: t('stripe.webhookNotConfigured') }, { status: 503 }); let event: Stripe.Event; try { - const stripe = getStripe(); - event = stripe.webhooks.constructEvent(body, sig, webhookSecret); + event = constructWebhookEvent(body, sig, webhookSecret); } catch (err) { return NextResponse.json({ error: t('stripe.webhookSignatureInvalid') }, { status: 400 }); } diff --git a/components/modules/calendar-module.tsx b/components/modules/calendar-module.tsx index 7b4e33055..0e6f5b508 100644 --- a/components/modules/calendar-module.tsx +++ b/components/modules/calendar-module.tsx @@ -301,6 +301,7 @@ export function CalendarModule() { const params = new URLSearchParams(window.location.search); if (params.get('gcal') === 'connected') { success(tr('calendarModule.googleCalendarConnected')); window.history.replaceState({}, '', window.location.pathname); } else if (params.get('gcal') === 'error') { toastError(tr('calendarModule.googleCalendarConnectionFailed')); window.history.replaceState({}, '', window.location.pathname); } + else if (params.get('gcal') === 'not_configured') { toastError(tr('calendarModule.googleCalendarNotConfigured')); window.history.replaceState({}, '', window.location.pathname); } return () => { active = false; }; }, [success, toastError]); // eslint-disable-line react-hooks/exhaustive-deps diff --git a/lib/i18n/messages/de-DE.json b/lib/i18n/messages/de-DE.json index 5d59a498b..9c2975179 100644 --- a/lib/i18n/messages/de-DE.json +++ b/lib/i18n/messages/de-DE.json @@ -3350,6 +3350,7 @@ "calendarModule.family": "Familie", "calendarModule.googleCalendarConnected": "Google Kalender verbunden!", "calendarModule.googleCalendarConnectionFailed": "Die Verbindung mit Google Kalender ist fehlgeschlagen.", + "calendarModule.googleCalendarNotConfigured": "Google Kalender ist auf diesem Server noch nicht eingerichtet und kann deshalb nicht verbunden werden.", "calendarModule.nameMe": "{name} (ich)", "calendarModule.show.birthdays": "Geburtstage", "calendarModule.show.holidays": "Feiertage", @@ -3760,6 +3761,7 @@ "checkInView.tapAStatusAboveTo": "Tippen Sie oben auf einen Status, um Ihren ersten Check-in zu senden.", "checkout.billingAccountStatusIsTemporarily": "Der Status des Abrechnungskontos ist vorübergehend nicht verfügbar.", "checkout.couldNotCreateCheckoutSession": "Es konnte keine Checkout-Sitzung erstellt werden", + "checkout.billingIsNotSetUp": "Die Abrechnung ist auf diesem Server noch nicht eingerichtet, daher wurde nichts geändert.", "checkout.couldNotSaveTheBilling": "Das Abrechnungskonto konnte nicht gespeichert werden. Bitte versuchen Sie es erneut.", "checkout.invalidPlan": "Ungültiger Tarif", "checkout.onlyAParentCanStart": "Nur ein Elternteil kann ein Abonnement starten.", @@ -12464,6 +12466,7 @@ "savings.savingsGoals": "Sparziele", "savings.target": "Ziel ($)", "savings.tooManySavingsRequestsPlease": "Zu viele Sparanfragen. Bitte versuchen Sie es in Kürze erneut.", + "savings.privateToTheAdults": "Spartipps beruhen auf den Finanzen der Familie, und die sind den Erwachsenen dieser Familie vorbehalten.", "savingsCoach.aiSavingsSuggestions": "KI-Sparvorschläge", "savingsCoach.analyzeMyFinances": "Meine Finanzen analysieren", "savingsView.addANameAndTarget": "Fügen Sie einen Namen und ein Ziel hinzu", diff --git a/lib/i18n/messages/en-US.json b/lib/i18n/messages/en-US.json index 965c7522b..ed0090568 100644 --- a/lib/i18n/messages/en-US.json +++ b/lib/i18n/messages/en-US.json @@ -3507,6 +3507,7 @@ "calendarImport.unsupportedTimezone": "This calendar uses an unsupported or conflicting time zone definition. Export it in UTC or connect your calendar instead.", "calendarModule.googleCalendarConnected": "Google Calendar connected!", "calendarModule.googleCalendarConnectionFailed": "Google Calendar connection failed.", + "calendarModule.googleCalendarNotConfigured": "Google Calendar isn’t set up on this server yet, so it can’t be connected.", "calendarModule.stayOnTopOfYour": "Stay on top of your family's schedule.", "calendarModule.syncFailed": "Sync failed", "calendarModule.show.birthdays": "Birthdays", @@ -3919,6 +3920,7 @@ "checkInView.tapAStatusAboveTo": "Tap a status above to post your first check-in.", "checkout.billingAccountStatusIsTemporarily": "Billing account status is temporarily unavailable.", "checkout.couldNotCreateCheckoutSession": "Could not create checkout session", + "checkout.billingIsNotSetUp": "Billing isn’t set up on this server yet, so nothing was changed.", "checkout.couldNotSaveTheBilling": "Could not save the billing account. Please try again.", "checkout.invalidPlan": "Invalid plan", "checkout.onlyAParentCanStart": "Only a parent can start a subscription.", @@ -12646,6 +12648,7 @@ "savings.savingsGoals": "Savings Goals", "savings.target": "Target ($)", "savings.tooManySavingsRequestsPlease": "Too many savings requests. Please try again shortly.", + "savings.privateToTheAdults": "Savings advice comes from the family’s finances, which are private to the adults in this family.", "savingsCoach.aiSavingsSuggestions": "AI Savings Suggestions", "savingsCoach.analyzeMyFinances": "Analyze my finances", "savingsView.addANameAndTarget": "Add a name and target", diff --git a/lib/i18n/messages/es-ES.json b/lib/i18n/messages/es-ES.json index 98952660a..9198a3da4 100644 --- a/lib/i18n/messages/es-ES.json +++ b/lib/i18n/messages/es-ES.json @@ -3350,6 +3350,7 @@ "calendarModule.family": "Familia", "calendarModule.googleCalendarConnected": "¡Google Calendar conectado!", "calendarModule.googleCalendarConnectionFailed": "Ha fallado la conexión con Google Calendar.", + "calendarModule.googleCalendarNotConfigured": "Google Calendar aún no está configurado en este servidor, así que no se puede conectar.", "calendarModule.nameMe": "{name} (yo)", "calendarModule.show.birthdays": "Cumpleaños", "calendarModule.show.holidays": "Festivos", @@ -3760,6 +3761,7 @@ "checkInView.tapAStatusAboveTo": "Toca un estado de arriba para publicar tu primer registro.", "checkout.billingAccountStatusIsTemporarily": "El estado de la cuenta de facturación no está disponible temporalmente.", "checkout.couldNotCreateCheckoutSession": "No se ha podido crear la sesión de pago", + "checkout.billingIsNotSetUp": "La facturación aún no está configurada en este servidor, así que no se ha cambiado nada.", "checkout.couldNotSaveTheBilling": "No se ha podido guardar la cuenta de facturación. Inténtalo de nuevo.", "checkout.invalidPlan": "Plan no válido", "checkout.onlyAParentCanStart": "Solo un progenitor puede iniciar una suscripción.", @@ -12466,6 +12468,7 @@ "savings.savingsGoals": "Objetivos de ahorro", "savings.target": "Objetivo ($)", "savings.tooManySavingsRequestsPlease": "Demasiadas solicitudes de ahorro. Inténtalo de nuevo en breve.", + "savings.privateToTheAdults": "Los consejos de ahorro se basan en las finanzas familiares, que son privadas de los adultos de esta familia.", "savingsCoach.aiSavingsSuggestions": "Sugerencias de ahorro con IA", "savingsCoach.analyzeMyFinances": "Analizar mis finanzas", "savingsView.addANameAndTarget": "Añade un nombre y un objetivo", diff --git a/lib/i18n/messages/fr-FR.json b/lib/i18n/messages/fr-FR.json index 4cdc7c6e9..3e8f33037 100644 --- a/lib/i18n/messages/fr-FR.json +++ b/lib/i18n/messages/fr-FR.json @@ -3350,6 +3350,7 @@ "calendarModule.family": "Famille", "calendarModule.googleCalendarConnected": "Google Agenda connecté !", "calendarModule.googleCalendarConnectionFailed": "La connexion à Google Agenda a échoué.", + "calendarModule.googleCalendarNotConfigured": "Google Agenda n’est pas encore configuré sur ce serveur, il ne peut donc pas être connecté.", "calendarModule.nameMe": "{name} (moi)", "calendarModule.show.birthdays": "Anniversaires", "calendarModule.show.holidays": "Jours fériés", @@ -3760,6 +3761,7 @@ "checkInView.tapAStatusAboveTo": "Appuyez sur un statut ci-dessus pour publier votre premier point.", "checkout.billingAccountStatusIsTemporarily": "Le statut du compte de facturation est temporairement indisponible.", "checkout.couldNotCreateCheckoutSession": "Impossible de créer la session de paiement", + "checkout.billingIsNotSetUp": "La facturation n’est pas encore configurée sur ce serveur, rien n’a donc été modifié.", "checkout.couldNotSaveTheBilling": "Impossible d’enregistrer le compte de facturation. Veuillez réessayer.", "checkout.invalidPlan": "Formule invalide", "checkout.onlyAParentCanStart": "Seul un parent peut démarrer un abonnement.", @@ -12466,6 +12468,7 @@ "savings.savingsGoals": "Objectifs d'épargne", "savings.target": "Objectif ($)", "savings.tooManySavingsRequestsPlease": "Trop de requêtes d’épargne. Veuillez réessayer dans un instant.", + "savings.privateToTheAdults": "Les conseils d’économie reposent sur les finances de la famille, réservées aux adultes de cette famille.", "savingsCoach.aiSavingsSuggestions": "Suggestions d'économies par IA", "savingsCoach.analyzeMyFinances": "Analyser mes finances", "savingsView.addANameAndTarget": "Ajoutez un nom et un objectif", diff --git a/lib/i18n/messages/it-IT.json b/lib/i18n/messages/it-IT.json index caf02bbc0..5db93e6c0 100644 --- a/lib/i18n/messages/it-IT.json +++ b/lib/i18n/messages/it-IT.json @@ -3350,6 +3350,7 @@ "calendarModule.family": "Famiglia", "calendarModule.googleCalendarConnected": "Google Calendar collegato!", "calendarModule.googleCalendarConnectionFailed": "Collegamento a Google Calendar non riuscito.", + "calendarModule.googleCalendarNotConfigured": "Google Calendar non è ancora configurato su questo server, quindi non può essere collegato.", "calendarModule.nameMe": "{name} (io)", "calendarModule.show.birthdays": "Compleanni", "calendarModule.show.holidays": "Festività", @@ -3760,6 +3761,7 @@ "checkInView.tapAStatusAboveTo": "Tocca uno stato qui sopra per pubblicare il tuo primo check-in.", "checkout.billingAccountStatusIsTemporarily": "Lo stato dell’account di fatturazione non è temporaneamente disponibile.", "checkout.couldNotCreateCheckoutSession": "Impossibile creare la sessione di pagamento", + "checkout.billingIsNotSetUp": "La fatturazione non è ancora configurata su questo server, quindi non è stato modificato nulla.", "checkout.couldNotSaveTheBilling": "Impossibile salvare l’account di fatturazione. Riprova.", "checkout.invalidPlan": "Piano non valido", "checkout.onlyAParentCanStart": "Solo un genitore può avviare un abbonamento.", @@ -12466,6 +12468,7 @@ "savings.savingsGoals": "Obiettivi di risparmio", "savings.target": "Obiettivo ($)", "savings.tooManySavingsRequestsPlease": "Troppe richieste di risparmio. Riprova tra poco.", + "savings.privateToTheAdults": "I consigli di risparmio si basano sulle finanze della famiglia, riservate agli adulti di questa famiglia.", "savingsCoach.aiSavingsSuggestions": "Suggerimenti di risparmio con IA", "savingsCoach.analyzeMyFinances": "Analizza le mie finanze", "savingsView.addANameAndTarget": "Aggiungi un nome e un obiettivo", diff --git a/lib/i18n/messages/nl-NL.json b/lib/i18n/messages/nl-NL.json index ec00a6825..b10d872ec 100644 --- a/lib/i18n/messages/nl-NL.json +++ b/lib/i18n/messages/nl-NL.json @@ -3350,6 +3350,7 @@ "calendarModule.family": "Gezin", "calendarModule.googleCalendarConnected": "Google Agenda gekoppeld!", "calendarModule.googleCalendarConnectionFailed": "Koppeling met Google Agenda mislukt.", + "calendarModule.googleCalendarNotConfigured": "Google Agenda is nog niet ingesteld op deze server en kan daarom niet worden gekoppeld.", "calendarModule.nameMe": "{name} (ik)", "calendarModule.show.birthdays": "Verjaardagen", "calendarModule.show.holidays": "Feestdagen", @@ -3760,6 +3761,7 @@ "checkInView.tapAStatusAboveTo": "Tik hierboven op een status om je eerste check-in te plaatsen.", "checkout.billingAccountStatusIsTemporarily": "De status van het factureringsaccount is tijdelijk niet beschikbaar.", "checkout.couldNotCreateCheckoutSession": "Kan geen afrekensessie aanmaken", + "checkout.billingIsNotSetUp": "Facturering is nog niet ingesteld op deze server, dus er is niets gewijzigd.", "checkout.couldNotSaveTheBilling": "Kan het factureringsaccount niet opslaan. Probeer het opnieuw.", "checkout.invalidPlan": "Ongeldig abonnement", "checkout.onlyAParentCanStart": "Alleen een ouder kan een abonnement starten.", @@ -12464,6 +12466,7 @@ "savings.savingsGoals": "Spaardoelen", "savings.target": "Doel ($)", "savings.tooManySavingsRequestsPlease": "Te veel spaarverzoeken. Probeer het zo dadelijk opnieuw.", + "savings.privateToTheAdults": "Spaaradvies is gebaseerd op de financiën van het gezin, en die zijn alleen voor de volwassenen in dit gezin.", "savingsCoach.aiSavingsSuggestions": "AI-bespaarsuggesties", "savingsCoach.analyzeMyFinances": "Mijn financiën analyseren", "savingsView.addANameAndTarget": "Voeg een naam en een doel toe", diff --git a/lib/i18n/messages/pt-PT.json b/lib/i18n/messages/pt-PT.json index 118177fc4..87af6c9bf 100644 --- a/lib/i18n/messages/pt-PT.json +++ b/lib/i18n/messages/pt-PT.json @@ -3351,6 +3351,7 @@ "calendarModule.family": "Família", "calendarModule.googleCalendarConnected": "Google Agenda ligado!", "calendarModule.googleCalendarConnectionFailed": "A ligação ao Google Agenda falhou.", + "calendarModule.googleCalendarNotConfigured": "O Google Agenda ainda não está configurado neste servidor, por isso não pode ser ligado.", "calendarModule.nameMe": "{name} (eu)", "calendarModule.show.birthdays": "Aniversários", "calendarModule.show.holidays": "Feriados", @@ -3761,6 +3762,7 @@ "checkInView.tapAStatusAboveTo": "Toque num estado acima para publicar o seu primeiro check-in.", "checkout.billingAccountStatusIsTemporarily": "O estado da conta de faturação está temporariamente indisponível.", "checkout.couldNotCreateCheckoutSession": "Não foi possível criar a sessão de pagamento", + "checkout.billingIsNotSetUp": "A faturação ainda não está configurada neste servidor, por isso nada foi alterado.", "checkout.couldNotSaveTheBilling": "Não foi possível guardar a conta de faturação. Tente novamente.", "checkout.invalidPlan": "Plano inválido", "checkout.onlyAParentCanStart": "Só um progenitor pode iniciar uma subscrição.", @@ -12465,6 +12467,7 @@ "savings.savingsGoals": "Objetivos de poupança", "savings.target": "Meta ($)", "savings.tooManySavingsRequestsPlease": "Demasiados pedidos de poupança. Tente novamente em breve.", + "savings.privateToTheAdults": "Os conselhos de poupança baseiam-se nas finanças da família, que são privadas dos adultos desta família.", "savingsCoach.aiSavingsSuggestions": "Sugestões de poupança com IA", "savingsCoach.analyzeMyFinances": "Analisar as minhas finanças", "savingsView.addANameAndTarget": "Adicione um nome e uma meta", diff --git a/lib/stripe.ts b/lib/stripe.ts index b49221b38..ff1716105 100644 --- a/lib/stripe.ts +++ b/lib/stripe.ts @@ -21,6 +21,15 @@ export function getStripe() { * Super Admin → Stripe Setup), falling back to the env-based singleton when no * key is provided. Lets the Bubaly Stripe account be configured at runtime. */ +/** + * Verify and parse a Stripe webhook payload. Signature checking needs only the + * endpoint's signing secret, never an API key, so a deployment whose secret key + * lives in Super Admin → Stripe Setup (or nowhere yet) can still verify events. + */ +export function constructWebhookEvent(body: string, signature: string, secret: string): Stripe.Event { + return Stripe.webhooks.constructEvent(body, signature, secret); +} + export function stripeFromKey(secretKey?: string | null): Stripe { const key = secretKey?.trim(); if (!key) return getStripe(); diff --git a/scripts/api-audit/sessions.mjs b/scripts/api-audit/sessions.mjs new file mode 100644 index 000000000..3ab1fcad0 --- /dev/null +++ b/scripts/api-audit/sessions.mjs @@ -0,0 +1,87 @@ +#!/usr/bin/env node +// API audit — make the four callers the sweep uses: signed out, a child, a +// parent, and a parent who is also a super admin. Local Supabase ONLY: the +// accounts it creates and deletes are disposable. +// +// Parents come from scripts/page-audit-session.mjs (the real sign-in form and +// the real onboarding), so their families are provisioned the way a person's +// is. The child is a `child` member of the parent's family with its own auth +// user, signed in with supabase-js and turned into the same SSR cookies the +// app sets, through @supabase/ssr itself. +// +// node scripts/api-audit/sessions.mjs --base http://localhost:3107 --out api-sessions.json + +import { execFileSync } from 'node:child_process'; +import { readFileSync, writeFileSync, mkdtempSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; +import { createClient } from '@supabase/supabase-js'; +import { createServerClient } from '@supabase/ssr'; + +const arg = (name, fallback) => { + const i = process.argv.indexOf(`--${name}`); + return i > -1 ? process.argv[i + 1] : fallback; +}; +const base = arg('base', 'http://localhost:3107'); +const out = arg('out', 'api-sessions.json'); +const url = process.env.NEXT_PUBLIC_SUPABASE_URL ?? ''; +const anon = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY ?? ''; +const service = process.env.SUPABASE_SERVICE_ROLE_KEY ?? ''; +if (!/^http:\/\/(127\.0\.0\.1|localhost):/.test(url)) { + console.error(`Refusing: NEXT_PUBLIC_SUPABASE_URL (${url || 'unset'}) is not a local Supabase.`); + process.exit(1); +} +const admin = createClient(url, service, { auth: { autoRefreshToken: false, persistSession: false } }); +const dir = mkdtempSync(path.join(tmpdir(), 'api-audit-')); + +function cookieHeaderFromState(file) { + const state = JSON.parse(readFileSync(file, 'utf8')); + return state.cookies.map((c) => `${c.name}=${c.value}`).join('; '); +} + +function parent(email, family) { + const state = path.join(dir, `${email}.json`); + execFileSync(process.execPath, ['scripts/page-audit-session.mjs', '--base', base, '--email', email, '--family', family, '--out', state], { stdio: 'inherit', env: process.env }); + const meta = JSON.parse(readFileSync(state.replace(/\.json$/, '.meta.json'), 'utf8')); + return { ...meta, cookie: cookieHeaderFromState(state) }; +} + +/** The SSR cookies @supabase/ssr writes for a session, as one Cookie header. */ +async function cookieFor(session) { + const jar = new Map(); + const ssr = createServerClient(url, anon, { + cookies: { + getAll: () => [...jar].map(([name, value]) => ({ name, value })), + setAll: (list) => { for (const { name, value } of list) value ? jar.set(name, value) : jar.delete(name); }, + }, + }); + const { error } = await ssr.auth.setSession({ access_token: session.access_token, refresh_token: session.refresh_token }); + if (error) throw error; + return [...jar].map(([n, v]) => `${n}=${v}`).join('; '); +} + +async function child(familyId) { + const email = 'api-audit-child@example.test'; + const password = 'Api-Audit-Child-2026!'; + const { data: list } = await admin.auth.admin.listUsers({ perPage: 1000 }); + const old = list?.users.find((u) => u.email === email); + if (old) await admin.auth.admin.deleteUser(old.id); + const { data: created, error } = await admin.auth.admin.createUser({ email, password, email_confirm: true, user_metadata: { full_name: 'Robin Rivera' } }); + if (error) throw error; + const { data: member, error: memberError } = await admin.from('family_members') + .insert({ family_id: familyId, user_id: created.user.id, role: 'child', display_name: 'Robin', is_active: true }) + .select('id').single(); + if (memberError) throw memberError; + const client = createClient(url, anon, { auth: { autoRefreshToken: false, persistSession: false } }); + const { data: signedIn, error: signInError } = await client.auth.signInWithPassword({ email, password }); + if (signInError) throw signInError; + return { email, userId: created.user.id, familyId, memberId: member.id, role: 'child', cookie: await cookieFor(signedIn.session) }; +} + +const p = parent('api-audit-parent@example.test', 'API Audit Family'); +const a = parent('api-audit-admin@example.test', 'API Audit Admin Family'); +const { error: superError } = await admin.from('super_admins').upsert({ email: a.email }); +if (superError) throw superError; +const c = await child(p.familyId); +writeFileSync(out, JSON.stringify({ base, callers: { anon: { cookie: '' }, child: c, parent: p, admin: { ...a, superAdmin: true } } }, null, 2)); +console.log(`Callers written to ${out}: child ${c.userId}, parent ${p.userId}, super admin ${a.userId}`); diff --git a/scripts/api-audit/summarize.mjs b/scripts/api-audit/summarize.mjs new file mode 100644 index 000000000..4e45451ec --- /dev/null +++ b/scripts/api-audit/summarize.mjs @@ -0,0 +1,53 @@ +#!/usr/bin/env node +// API audit — judge a sweep. Every call lands in exactly one bucket, and each +// route method gets one verdict line, so nothing in the sweep goes unread. +// +// FAIL a 5xx, a dropped connection or timeout, a stack trace or internal +// error text in the body, an /api/admin route that answers 2xx to a +// child or a non-admin parent, or a cron route that answers anything +// but 401/403 without the cron secret. +// BLOCKED a 503 whose body says the feature is not set up on this server +// (no AI, payment, push, GIF or telephony credentials): an honest +// answer, and the rest of the route needs those credentials to test. +// REVIEW a 2xx to a signed-out caller (public by design, or a leak?), or a +// 2xx to a body that is not JSON (did it accept garbage?). +// OK everything else: a 4xx with a reason, a redirect to sign-in. +// +// node scripts/api-audit/summarize.mjs api-sweep.jsonl + +import { readFileSync } from 'node:fs'; + +const rows = readFileSync(process.argv[2] ?? 'api-sweep.jsonl', 'utf8').trim().split('\n').map((l) => JSON.parse(l)); +const NOT_SET_UP = /not (?:set up|configured)|isn[’']t (?:set up|configured)|not_configured/i; +const LEAK = /\bat [\w.<>]+ \(|node_modules\/|\bPostgrestError\b|violates (row-level|foreign key|check|not-null)|duplicate key value|relation "[^"]+" does not exist|syntax error at or near|TypeError:|ReferenceError:/; + +export function judge(r) { + const two = r.status >= 200 && r.status < 300; + if (r.status === 0) return ['FAIL', `no response (${r.error})`]; + if (r.status === 503 && NOT_SET_UP.test(r.head ?? '')) return ['BLOCKED', 'not set up on this server']; + if (r.status >= 500) return ['FAIL', `${r.status}`]; + if (LEAK.test(r.head ?? '')) return ['FAIL', `internal detail in body`]; + if (r.route.startsWith('/api/admin') && two && (r.caller === 'child' || r.caller === 'parent')) return ['FAIL', `admin route ${r.status} to ${r.caller}`]; + if (r.route.startsWith('/api/cron') && ![401, 403].includes(r.status)) return ['FAIL', `cron route ${r.status} without the cron secret`]; + if (two && r.caller === 'anon') return ['REVIEW', `${r.status} signed out`]; + if (two && r.probe === 'not-json') return ['REVIEW', `${r.status} to a non-JSON body`]; + return ['OK', `${r.status}`]; +} + +const byRoute = new Map(); +for (const r of rows) { + const key = `${r.method} ${r.route}`; + const [verdict, why] = judge(r); + const entry = byRoute.get(key) ?? { verdicts: [], statuses: [] }; + entry.verdicts.push({ verdict, why, caller: r.caller, probe: r.probe }); + entry.statuses.push(`${r.caller[0]}${r.probe === 'not-json' ? '!' : ''}:${r.status}`); + byRoute.set(key, entry); +} +const totals = { FAIL: 0, BLOCKED: 0, REVIEW: 0, OK: 0 }; +for (const [key, { verdicts, statuses }] of byRoute) { + const worst = ['FAIL', 'BLOCKED', 'REVIEW'].find((level) => verdicts.some((v) => v.verdict === level)) ?? 'OK'; + totals[worst]++; + const reasons = [...new Set(verdicts.filter((v) => v.verdict === worst && worst !== 'OK').map((v) => `${v.caller}/${v.probe}: ${v.why}`))]; + console.log(`${worst.padEnd(6)} ${key} [${statuses.join(' ')}]${reasons.length ? ' ' + reasons.join('; ') : ''}`); +} +console.log(`\n${byRoute.size} route methods: ${totals.FAIL} FAIL, ${totals.BLOCKED} BLOCKED, ${totals.REVIEW} REVIEW, ${totals.OK} OK`); diff --git a/scripts/api-audit/sweep.mjs b/scripts/api-audit/sweep.mjs new file mode 100644 index 000000000..72cb61bf7 --- /dev/null +++ b/scripts/api-audit/sweep.mjs @@ -0,0 +1,86 @@ +#!/usr/bin/env node +// API audit — call every method of every route under app/api as each caller, +// with the inputs a careless or hostile client sends: nothing, an empty JSON +// object, a body that is not JSON. One JSON line per call. +// +// It does not judge; scripts/api-audit/summarize.mjs does. It only refuses to +// run against anything but a local base URL, because it POSTs to every route. +// +// node scripts/api-audit/sweep.mjs api-sessions.json api-sweep.jsonl + +import { readFileSync, writeFileSync, readdirSync, statSync } from 'node:fs'; +import path from 'node:path'; + +const [sessionsFile = 'api-sessions.json', outFile = 'api-sweep.jsonl'] = process.argv.slice(2); +const { base, callers } = JSON.parse(readFileSync(sessionsFile, 'utf8')); +if (!/^http:\/\/(127\.0\.0\.1|localhost):/.test(base)) { + console.error(`Refusing: ${base} is not a local server.`); + process.exit(1); +} + +/** Every app/api route file and the methods it exports. */ +export function discoverRoutes(root = 'app/api') { + const found = []; + const walk = (dir) => { + for (const name of readdirSync(dir)) { + const full = path.join(dir, name); + if (statSync(full).isDirectory()) walk(full); + else if (name === 'route.ts') { + const src = readFileSync(full, 'utf8'); + const methods = [...src.matchAll(/^export (?:async )?(?:function|const) (GET|POST|PUT|PATCH|DELETE)\b/gm)].map((m) => m[1]); + const route = '/' + path.relative('app', path.dirname(full)).split(path.sep).join('/'); + for (const method of [...new Set(methods)]) found.push({ file: full, route, method }); + } + } + }; + walk(root); + return found.sort((a, b) => (a.route + a.method).localeCompare(b.route + b.method)); +} + +// A dynamic segment gets a well-formed id that matches nothing, so a route that +// looks it up must answer "not found", not throw. +const NOBODY = '00000000-0000-4000-8000-00000000abcd'; +const concrete = (route) => route.replace(/\[\[?\.\.\.[^\]]+\]\]?/g, 'nothing').replace(/\[[^\]]+\]/g, NOBODY); + +const PROBES = { + GET: [{ probe: 'bare' }], + POST: [ + { probe: 'empty-json', body: '{}', type: 'application/json' }, + { probe: 'not-json', body: '{"unterminated', type: 'application/json' }, + ], +}; +for (const m of ['PUT', 'PATCH', 'DELETE']) PROBES[m] = PROBES.POST; + +async function call(target, method, caller, probe) { + const headers = { origin: base, 'user-agent': 'bubaly-api-audit' }; + if (caller.cookie) headers.cookie = caller.cookie; + if (probe.type) headers['content-type'] = probe.type; + const started = Date.now(); + try { + const res = await fetch(base + target, { method, headers, body: probe.body, redirect: 'manual', signal: AbortSignal.timeout(45_000) }); + const text = await res.text(); + return { + status: res.status, ms: Date.now() - started, + type: res.headers.get('content-type') ?? '', location: res.headers.get('location') ?? '', + bytes: text.length, head: text.slice(0, 400), + }; + } catch (error) { + return { status: 0, ms: Date.now() - started, error: String(error?.message ?? error) }; + } +} + +const routes = discoverRoutes(); +const lines = []; +const order = ['anon', 'child', 'parent', 'admin']; +for (const r of routes) { + const target = concrete(r.route); + for (const who of order) { + for (const probe of PROBES[r.method]) { + const result = await call(target, r.method, callers[who], probe); + lines.push(JSON.stringify({ ...r, target, caller: who, probe: probe.probe, ...result })); + } + } + process.stderr.write('.'); +} +writeFileSync(outFile, lines.join('\n') + '\n'); +console.error(`\n${lines.length} calls over ${routes.length} route methods → ${outFile}`); diff --git a/tests/a-child-is-not-given-the-familys-savings-advice.test.ts b/tests/a-child-is-not-given-the-familys-savings-advice.test.ts new file mode 100644 index 000000000..a463f51c9 --- /dev/null +++ b/tests/a-child-is-not-given-the-familys-savings-advice.test.ts @@ -0,0 +1,50 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +// Found by the API sweep (scripts/api-audit): POST /api/ai/savings answered a +// signed-in CHILD with advice built from the family's transactions, budgets, +// unpaid bills and subscriptions — "Trim Dining spending: you're $32 over your +// Dining budget", "Cancel 2 unused subscriptions (Netflix $15.49/mo, …)". +// +// Everywhere else the family's finances are the adults': the finance service +// refuses a non-manager (lib/services/finances: "Family finances are private +// to the adults in this family"), and the privacy export withholds its +// `finances` section from a child. This route read the same tables through its +// own queries and never asked. + +const mocks = vi.hoisted(() => ({ role: 'child', reads: [] as string[] })); +vi.mock('@/lib/i18n/server', () => ({ getTranslations: async () => (key: string) => key })); +vi.mock('@/lib/supabase/auth', () => ({ + requireUserContext: vi.fn(async () => ({ user: { id: 'u' }, active: { role: mocks.role, familyId: 'f', family: { timezone: 'UTC' } } })), +})); +vi.mock('@/lib/supabase/server', () => ({ + createServer: async () => ({ from: (table: string) => { + mocks.reads.push(table); + const b = { select: () => b, eq: () => b, neq: () => b, gte: () => b, order: () => b, range: async () => ({ data: [], error: null }), + then: (resolve: (v: unknown) => void) => resolve({ data: [], error: null }) }; + return b; + } }), +})); +vi.mock('@/lib/server/route-feature-gate', () => ({ refuseUnlessEntitled: async () => null })); +vi.mock('@/lib/server/ai-rate-limit', () => ({ enforceAIRateLimit: async () => ({ ok: true }) })); +vi.mock('@/lib/ai/provider', () => ({ resolveProvider: vi.fn(), isAIConfigured: async () => false })); + +beforeEach(() => { mocks.reads = []; }); + +describe('savings advice is for the adults who run the family money', () => { + it.each(['child', 'teen', 'guest'])('refuses a %s before reading any finance table', async (role) => { + mocks.role = role; + const { POST } = await import('@/app/api/ai/savings/route'); + const res = await POST(); + expect(res.status).toBe(403); + expect(await res.json()).toEqual({ error: 'savings.privateToTheAdults' }); + expect(mocks.reads).toEqual([]); + }); + + it.each(['parent', 'adult'])('still answers a %s', async (role) => { + mocks.role = role; + const { POST } = await import('@/app/api/ai/savings/route'); + const res = await POST(); + expect(res.status).toBe(200); + expect(mocks.reads).toEqual(expect.arrayContaining(['transactions', 'budgets', 'bills', 'subscriptions_tracked'])); + }); +}); diff --git a/tests/a-write-route-is-gated-and-bounded.test.ts b/tests/a-write-route-is-gated-and-bounded.test.ts index f93b8bb97..0176d3c07 100644 --- a/tests/a-write-route-is-gated-and-bounded.test.ts +++ b/tests/a-write-route-is-gated-and-bounded.test.ts @@ -47,7 +47,7 @@ const GATES = [ 'hasCronAuthorization', 'secretEquals', 'bearerMatches', // a Bearer secret in constant time (lib/server/secret-compare): the Guardian escalation - 'stripe.webhooks', 'constructEvent', 'svix-signature', // signature-verified webhooks + 'constructWebhookEvent', 'constructEvent', 'svix-signature', // signature-verified webhooks (lib/stripe's keyless verifier, Stripe's own, Resend's) 'validateTwilioSignature', // the Twilio callbacks: guardian inbound, screening, contact centre 'verifyTwilioRequest', // the same check through lib/server/twilio-ingress, which no build mode skips ]; @@ -70,7 +70,7 @@ const BOUNDED = ['readBoundedRequestJson', 'MAX_SMALL_JSON_BYTES', 'MAX_BODY_BYT * `includes()` keeps answering yes about a gate that no longer runs — which the * calibration for this file caught by failing to fail. Identifiers are matched * on word boundaries; the few entries that are not plain identifiers - * (`stripe.webhooks`, `svix-signature`, `auth.getUser`, `getUser()`) fall back + * (`svix-signature`, `auth.getUser`, `getUser()`) fall back * to a literal search. */ function uses(src: string, needle: string): boolean { diff --git a/tests/ai-habits-journal-observability-outcomes.test.ts b/tests/ai-habits-journal-observability-outcomes.test.ts index 4619e94bb..1252de987 100644 --- a/tests/ai-habits-journal-observability-outcomes.test.ts +++ b/tests/ai-habits-journal-observability-outcomes.test.ts @@ -12,7 +12,7 @@ const mocks = vi.hoisted(() => ({ vi.mock('@/lib/supabase/auth', () => ({ requireUserContext: mocks.context })); vi.mock('@/lib/supabase/server', () => ({ createServer: mocks.server })); -vi.mock('@/lib/ai/provider', () => ({ resolveProvider: mocks.provider })); +vi.mock('@/lib/ai/provider', () => ({ resolveProvider: mocks.provider, isAIConfigured: async () => true })); vi.mock('@/lib/server/ai-rate-limit', () => ({ enforceAIRateLimit: mocks.rate })); vi.mock('@/lib/services/scope', () => ({ scopeFromUserContext: (ctx: { active: { familyId: string }; user: { id: string } }, db: unknown) => ({ diff --git a/tests/ai-savings-observability-outcomes.test.ts b/tests/ai-savings-observability-outcomes.test.ts index ee4e29b62..be178a686 100644 --- a/tests/ai-savings-observability-outcomes.test.ts +++ b/tests/ai-savings-observability-outcomes.test.ts @@ -61,7 +61,7 @@ const validSuggestion = { title: 'Example title', detail: 'Example detail.' }; beforeEach(() => { vi.resetAllMocks(); mocks.settled.length = 0; - mocks.requireUserContext.mockResolvedValue({ user: { id: 'user-1' }, active: { familyId: 'family-1' } }); + mocks.requireUserContext.mockResolvedValue({ user: { id: 'user-1' }, active: { familyId: 'family-1', role: 'parent' } }); mocks.enforceAIRateLimit.mockResolvedValue({ ok: true }); mocks.resolveProvider.mockResolvedValue({ complete: mocks.complete }); mocks.complete.mockResolvedValue({ text: '{}', model: 'mock-model' }); diff --git a/tests/ai-savings-response-integrity.test.ts b/tests/ai-savings-response-integrity.test.ts index 8041fb8a2..bdd43bb75 100644 --- a/tests/ai-savings-response-integrity.test.ts +++ b/tests/ai-savings-response-integrity.test.ts @@ -53,7 +53,7 @@ function reply(summary: unknown, suggestions: unknown) { beforeEach(() => { vi.resetAllMocks(); - mocks.requireUserContext.mockResolvedValue({ user: { id: 'user-1' }, active: { familyId: 'family-1' } }); + mocks.requireUserContext.mockResolvedValue({ user: { id: 'user-1' }, active: { familyId: 'family-1', role: 'parent' } }); mocks.enforceAIRateLimit.mockResolvedValue({ ok: true }); mocks.resolveProvider.mockResolvedValue({ complete: mocks.complete }); mocks.createServer.mockResolvedValue({ diff --git a/tests/ai-weekly-briefing-observability-outcomes.test.ts b/tests/ai-weekly-briefing-observability-outcomes.test.ts index 69111e8fb..44b7bc83c 100644 --- a/tests/ai-weekly-briefing-observability-outcomes.test.ts +++ b/tests/ai-weekly-briefing-observability-outcomes.test.ts @@ -27,7 +27,7 @@ vi.mock('@/lib/supabase/auth', () => ({ vi.mock('@/lib/supabase/server', () => ({ createServer: mocks.createServer })); vi.mock('@/lib/server/plan', () => ({ resolveFamilyPlanLevel: async () => 2 })); vi.mock('@/lib/server/ai-rate-limit', () => ({ enforceAIRateLimit: mocks.enforceAIRateLimit })); -vi.mock('@/lib/ai/provider', () => ({ resolveProvider: mocks.resolveProvider })); +vi.mock('@/lib/ai/provider', () => ({ resolveProvider: mocks.resolveProvider, isAIConfigured: async () => true })); vi.mock('@/lib/services/scope', () => ({ scopeFromUserContext: () => ({}) })); vi.mock('@/lib/ai/weekly', () => ({ weekWindow: () => mocks.week, diff --git a/tests/an-ai-route-says-when-the-engine-is-not-set-up.test.ts b/tests/an-ai-route-says-when-the-engine-is-not-set-up.test.ts new file mode 100644 index 000000000..74e46a819 --- /dev/null +++ b/tests/an-ai-route-says-when-the-engine-is-not-set-up.test.ts @@ -0,0 +1,58 @@ +import { readFileSync, readdirSync, statSync } from 'node:fs'; +import path from 'node:path'; +import { describe, expect, it } from 'vitest'; + +// Found by the API sweep (scripts/api-audit): with no AI key, eighteen AI +// routes told the family "the AI engine isn't set up" with a 503, and the rest +// let the provider's "OpenAI API key is not configured" throw reach a generic +// catch — a 500 "failed to generate", or a 502 "temporarily unavailable", +// which says try again to someone for whom trying again cannot work. One +// (health/coach) did check, but only the environment, so a key saved in +// Admin → AI Engine was reported as missing. +// +// isAIConfigured() (lib/ai/provider.ts) knows every place a key can come from. +// Every route that calls the provider asks it first, unless it has a real +// answer without the model — named below, with what it answers. + +const FALLS_BACK_WITHOUT_A_MODEL: Record = { + 'app/api/ai/journal/route.ts': 'answers an evergreen journaling prompt', + 'app/api/ai/savings/route.ts': 'answers data-driven savings suggestions from the family’s own budgets', + 'app/api/behavior/insight/route.ts': 'answers an insight computed from the behaviour data', +}; + +function routes(dir = 'app/api'): string[] { + return readdirSync(dir).flatMap((name) => { + const full = path.join(dir, name); + if (statSync(full).isDirectory()) return routes(full); + return name === 'route.ts' ? [full.split(path.sep).join('/')] : []; + }); +} + +const callers = routes().filter((f) => /\bresolveProvider\(/.test(readFileSync(f, 'utf8'))); + +describe('an AI route says when the engine is not set up', () => { + it('finds the routes that call the provider', () => { + expect(callers.length).toBeGreaterThanOrEqual(30); + }); + + it.each(callers.filter((f) => !(f in FALLS_BACK_WITHOUT_A_MODEL)))('%s asks isAIConfigured() before calling the provider', (file) => { + const src = readFileSync(file, 'utf8'); + const asked = src.search(/\bisAIConfigured\(\)/); + expect(asked, `${file} never asks isAIConfigured()`).toBeGreaterThan(-1); + expect(asked, `${file} asks only after calling the provider`).toBeLessThan(src.search(/\bresolveProvider\(\)/)); + }); + + it('checks no key source by hand: the environment alone misses a key saved in Admin → AI Engine', () => { + for (const file of callers) { + expect(readFileSync(file, 'utf8'), file).not.toMatch(/if\s*\(\s*!process\.env\.(OPENAI|ANTHROPIC)_API_KEY/); + } + }); + + it('keeps the fallback list honest: each named route still calls the provider and still has a fallback', () => { + for (const file of Object.keys(FALLS_BACK_WITHOUT_A_MODEL)) { + const src = readFileSync(file, 'utf8'); + expect(src, file).toMatch(/\bresolveProvider\(/); + expect(src, file).toMatch(/fallback/i); + } + }); +}); diff --git a/tests/api-vacations-ai-output.test.ts b/tests/api-vacations-ai-output.test.ts index 371583b59..dd510e882 100644 --- a/tests/api-vacations-ai-output.test.ts +++ b/tests/api-vacations-ai-output.test.ts @@ -21,7 +21,7 @@ vi.mock('@/lib/supabase/server', () => ({ createServiceClient: () => entitledServiceClient(), })); vi.mock('@/lib/server/ai-rate-limit', () => ({ enforceAIRateLimit: mocks.enforceAIRateLimit })); -vi.mock('@/lib/ai/provider', () => ({ resolveProvider: mocks.resolveProvider })); +vi.mock('@/lib/ai/provider', () => ({ resolveProvider: mocks.resolveProvider, isAIConfigured: async () => true })); vi.mock('@/lib/vacations/packing', () => ({ suggestPacking: mocks.suggestPacking })); import { POST } from '@/app/api/vacations/ai/route'; diff --git a/tests/billing-price-verification.test.ts b/tests/billing-price-verification.test.ts index 3e24ae184..59a2927d6 100644 --- a/tests/billing-price-verification.test.ts +++ b/tests/billing-price-verification.test.ts @@ -105,13 +105,19 @@ vi.mock('@/lib/stripe', async () => { const stripe = { prices: { retrieve: mocks.retrievePrice }, customers: { create: mocks.createCustomer }, checkout: { sessions: { create: mocks.createCheckout } }, subscriptions: { retrieve: mocks.retrieveSubscription, update: mocks.updateSubscription }, webhooks: { constructEvent: mocks.constructEvent } }; - return { getStripe: () => stripe, stripeFromKey: () => stripe, STRIPE_PLANS: { + return { getStripe: () => stripe, stripeFromKey: () => stripe, constructWebhookEvent: mocks.constructEvent, STRIPE_PLANS: { basic_monthly: prices.stripePrices.basic_monthly.id, basic_annual: prices.stripePrices.basic_annual.id, plus_monthly: prices.stripePrices.plus_monthly.id, plus_annual: prices.stripePrices.plus_annual.id, family_monthly: prices.stripePrices.basic_monthly.id, family_annual: prices.stripePrices.basic_annual.id, } }; }); -vi.mock('@/lib/stripe/settings', () => ({ getStripeSettings: async () => ({}), effectiveSecretKey: () => '' })); +// A configured deployment: the routes resolve their key and signing secret the +// way lib/stripe/settings does (a missing key is now a 503 before any Stripe call). +vi.mock('@/lib/stripe/settings', () => ({ + getStripeSettings: async () => ({}), + effectiveSecretKey: () => 'sk_test_fixture', + effectiveWebhookSecret: () => process.env.STRIPE_WEBHOOK_SECRET || null, +})); vi.mock('@/lib/stripe/service-fee', () => ({ serviceFeeAddInvoiceItems: () => undefined })); vi.mock('@/lib/server/request-rate-limit', () => ({ enforceRequestRateLimit: async () => ({ ok: true }) })); vi.mock('@/lib/stripe/webhook', () => ({ recordEvent: mocks.recordEvent, markEventProcessed: mocks.markProcessed, markEventError: mocks.markError })); diff --git a/tests/billing-uses-the-stripe-keys-the-admin-configured.test.ts b/tests/billing-uses-the-stripe-keys-the-admin-configured.test.ts new file mode 100644 index 000000000..2493ab0bc --- /dev/null +++ b/tests/billing-uses-the-stripe-keys-the-admin-configured.test.ts @@ -0,0 +1,161 @@ +import { NextRequest } from 'next/server'; +import Stripe from 'stripe'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +// Found by the API sweep (scripts/api-audit). Super Admin → Stripe Setup saves a +// secret key and a webhook signing secret to `stripe_settings`, and promises +// "configured value first, then env" (lib/stripe/settings.ts). Only checkout +// kept that promise: +// +// - portal, cancel and change-plan built their client with getStripe(), which +// reads STRIPE_SECRET_KEY alone. A family that subscribed through the +// configured key could not open the portal, cancel, or change plan. +// - the Stripe webhook read STRIPE_WEBHOOK_SECRET alone, and built a client +// with getStripe() just to check the signature. With the key unset that +// threw, and the catch answered "signature invalid": every real event +// refused, so no subscription was ever recorded. +// - with no key anywhere, every billing route answered a generic 500. +// +// Real Stripe signature verification below, no mock of it: the signed payload +// is made with Stripe's own test-header generator. + +// A configured price, so checkout reaches the key check (STRIPE_PLANS is read +// from the environment when lib/stripe first loads). +vi.hoisted(() => { process.env.STRIPE_PRICE_BASIC_MONTHLY = 'price_fixture_basic_monthly'; }); + +const mocks = vi.hoisted(() => ({ + settings: {} as Record, + keysUsed: [] as string[], + portal: vi.fn(async () => ({ url: 'https://billing.stripe.test/session' })), + update: vi.fn(async () => ({})), + recordEvent: vi.fn(async () => ({ outcome: 'duplicate' })), +})); + +vi.mock('@/lib/i18n/server', () => ({ getTranslations: async () => (key: string) => key })); +vi.mock('@/lib/supabase/auth', () => ({ + requireUserContext: vi.fn(async () => ({ + user: { id: 'user-a', email: 'fixture@example.test' }, + active: { role: 'parent', familyId: 'family-a', family: { name: 'Fixture family' } }, + })), +})); +vi.mock('@/lib/supabase/server', () => { + const rows: Record = { + billing_customers: { customer_ref: 'cus_fixture' }, + subscriptions: { provider_ref: 'sub_fixture', status: 'active' }, + }; + const db = { from: (table: string) => { + const b = { select: () => b, eq: () => b, update: () => b, + maybeSingle: async () => ({ data: rows[table] ?? null, error: null }), + then: (resolve: (v: unknown) => void) => resolve({ data: [{ id: 'x' }], error: null }) }; + return b; + } }; + return { createServer: async () => db, createServiceClient: () => db }; +}); +vi.mock('@/lib/server/request-rate-limit', () => ({ enforceRequestRateLimit: async () => ({ ok: true }) })); +vi.mock('@/lib/stripe/settings', async () => { + const real = await vi.importActual('@/lib/stripe/settings'); + return { ...real, getStripeSettings: async () => mocks.settings }; +}); +vi.mock('@/lib/stripe', async () => { + const real = await vi.importActual('@/lib/stripe'); + return { + ...real, + getStripe: () => { throw new Error('getStripe() reads only STRIPE_SECRET_KEY; billing routes must not use it'); }, + stripeFromKey: (key: string) => { + mocks.keysUsed.push(key); + return { billingPortal: { sessions: { create: mocks.portal } }, subscriptions: { update: mocks.update } }; + }, + }; +}); +vi.mock('@/lib/stripe/webhook', () => ({ recordEvent: mocks.recordEvent, markEventProcessed: vi.fn(), markEventError: vi.fn() })); + +const saved = { key: process.env.STRIPE_SECRET_KEY, hook: process.env.STRIPE_WEBHOOK_SECRET }; +beforeEach(() => { + delete process.env.STRIPE_SECRET_KEY; + delete process.env.STRIPE_WEBHOOK_SECRET; + mocks.settings = {}; + mocks.keysUsed = []; + vi.clearAllMocks(); +}); +afterEach(() => { + if (saved.key === undefined) delete process.env.STRIPE_SECRET_KEY; else process.env.STRIPE_SECRET_KEY = saved.key; + if (saved.hook === undefined) delete process.env.STRIPE_WEBHOOK_SECRET; else process.env.STRIPE_WEBHOOK_SECRET = saved.hook; +}); + +const post = (path: string, body: unknown, headers: Record = {}) => + new NextRequest(`https://www.bubaly.com${path}`, { + method: 'POST', body: typeof body === 'string' ? body : JSON.stringify(body), + headers: { 'content-type': 'application/json', ...headers }, + }); + +describe('billing routes use the Stripe key Super Admin → Stripe Setup saved', () => { + it('opens the portal with the configured key when the environment has none', async () => { + mocks.settings = { secret_key: 'sk_test_from_admin' }; + const { POST } = await import('@/app/api/billing/portal/route'); + const res = await POST(post('/api/billing/portal', {})); + expect(res.status).toBe(200); + expect(mocks.keysUsed).toEqual(['sk_test_from_admin']); + expect(mocks.portal).toHaveBeenCalledTimes(1); + }); + + it('cancels with the configured key when the environment has none', async () => { + mocks.settings = { secret_key: 'sk_test_from_admin' }; + const { POST } = await import('@/app/api/billing/cancel/route'); + const res = await POST(post('/api/billing/cancel', { resume: false })); + expect(mocks.keysUsed).toEqual(['sk_test_from_admin']); + expect(mocks.update).toHaveBeenCalledWith('sub_fixture', { cancel_at_period_end: true }); + expect(res.status).toBeLessThan(500); + }); + + it.each([ + ['portal', '/api/billing/portal', {}], + ['cancel', '/api/billing/cancel', { resume: false }], + ['checkout', '/api/billing/checkout', { plan: 'basic_monthly' }], + ])('%s says billing is not set up, with no Stripe call, when no key exists anywhere', async (name, path, body) => { + const { POST } = await import(`@/app/api/billing/${name}/route`); + const res = await POST(post(path, body)); + expect(res.status).toBe(503); + expect(await res.json()).toEqual({ error: 'checkout.billingIsNotSetUp' }); + expect(mocks.keysUsed).toEqual([]); + }); + + it('checkout reads the body before anything else: a malformed one is a 400, not a 500', async () => { + const { POST } = await import('@/app/api/billing/checkout/route'); + const res = await POST(post('/api/billing/checkout', '{"unterminated')); + expect(res.status).toBe(400); + }); +}); + +describe('the Stripe webhook verifies with the configured signing secret', () => { + const payload = JSON.stringify({ id: 'evt_fixture', object: 'event', type: 'customer.subscription.updated', data: { object: {} } }); + const signed = (secret: string) => Stripe.webhooks.generateTestHeaderString({ payload, secret }); + + it('accepts an event signed with the secret saved in Stripe Setup, with no Stripe env at all', async () => { + mocks.settings = { webhook_secret: 'whsec_from_admin' }; + const { POST } = await import('@/app/api/webhooks/stripe/route'); + const res = await POST(post('/api/webhooks/stripe', payload, { 'stripe-signature': signed('whsec_from_admin') })); + expect(res.status).toBe(200); + expect(mocks.recordEvent).toHaveBeenCalledTimes(1); + }); + + it('still refuses an event signed with any other secret', async () => { + mocks.settings = { webhook_secret: 'whsec_from_admin' }; + const { POST } = await import('@/app/api/webhooks/stripe/route'); + const res = await POST(post('/api/webhooks/stripe', payload, { 'stripe-signature': signed('whsec_someone_else') })); + expect(res.status).toBe(400); + expect(mocks.recordEvent).not.toHaveBeenCalled(); + }); + + it('falls back to STRIPE_WEBHOOK_SECRET when Stripe Setup holds none', async () => { + process.env.STRIPE_WEBHOOK_SECRET = 'whsec_from_env'; + const { POST } = await import('@/app/api/webhooks/stripe/route'); + const res = await POST(post('/api/webhooks/stripe', payload, { 'stripe-signature': signed('whsec_from_env') })); + expect(res.status).toBe(200); + }); + + it('is "not configured" with no secret anywhere', async () => { + const { POST } = await import('@/app/api/webhooks/stripe/route'); + const res = await POST(post('/api/webhooks/stripe', payload, { 'stripe-signature': signed('whsec_x') })); + expect(res.status).toBe(503); + }); +}); diff --git a/tests/google-calendar-connect-without-credentials.test.ts b/tests/google-calendar-connect-without-credentials.test.ts new file mode 100644 index 000000000..b4bc07299 --- /dev/null +++ b/tests/google-calendar-connect-without-credentials.test.ts @@ -0,0 +1,67 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { NextRequest } from 'next/server'; + +// Found by the API sweep (scripts/api-audit): on a server without Google +// credentials, "Connect Google" on /dashboard/calendar is a plain link to +// /api/google/calendar/auth, and that route let getGoogleOAuthUrl's +// "GOOGLE_CLIENT_ID is not set" error escape. The parent who clicked it got a +// blank 500 page and no way back to their calendar. +// +// The Outlook link beside it already does the right thing for the same state +// (/api/sync/[provider]/auth redirects to its setup page). This route now does +// the same: back to the calendar, which says the connection isn't set up. + +vi.mock('@/lib/supabase/auth', () => ({ requireUserContext: vi.fn(async () => ({ user: { id: 'u' } })) })); + +const ORIGIN = 'https://www.bubaly.com'; +const saved = { id: process.env.GOOGLE_CLIENT_ID, secret: process.env.GOOGLE_CLIENT_SECRET }; +const restore = (key: string, value: string | undefined) => { if (value === undefined) delete process.env[key]; else process.env[key] = value; }; + +beforeEach(() => { + process.env.GOOGLE_CLIENT_ID = 'client-fixture.apps.googleusercontent.com'; + process.env.GOOGLE_CLIENT_SECRET = 'secret-fixture'; +}); +afterEach(() => { + restore('GOOGLE_CLIENT_ID', saved.id); + restore('GOOGLE_CLIENT_SECRET', saved.secret); + vi.resetModules(); +}); + +async function start() { + const { GET } = await import('@/app/api/google/calendar/auth/route'); + return GET(new NextRequest(`${ORIGIN}/api/google/calendar/auth`)); +} + +describe('connecting Google Calendar on a server without Google credentials', () => { + it.each([ + ['no client id', 'GOOGLE_CLIENT_ID'], + ['no client secret', 'GOOGLE_CLIENT_SECRET'], + ])('returns the parent to the calendar when there is %s', async (_case, missing) => { + delete process.env[missing]; + const res = await start(); + expect(res.status).toBe(307); + const to = new URL(res.headers.get('location')!); + expect(to.origin).toBe(ORIGIN); + expect(to.pathname).toBe('/dashboard/calendar'); + expect(to.searchParams.get('gcal')).toBe('not_configured'); + // No state cookie for a consent request that was never made. + expect(res.cookies.get('gcal_oauth_state')).toBeUndefined(); + }); + + it('still sends a configured server to Google with a single-use state', async () => { + const res = await start(); + const to = new URL(res.headers.get('location')!); + expect(to.host).toBe('accounts.google.com'); + expect(to.searchParams.get('state')).toBe(res.cookies.get('gcal_oauth_state')?.value); + }); + + it('tells the parent on the calendar page, in their language', async () => { + const { readFileSync } = await import('node:fs'); + const src = readFileSync('components/modules/calendar-module.tsx', 'utf8'); + expect(src).toMatch(/params\.get\('gcal'\) === 'not_configured'\)\s*\{\s*toastError\(tr\('calendarModule\.googleCalendarNotConfigured'\)\)/); + for (const locale of ['en-US', 'de-DE', 'es-ES', 'fr-FR', 'it-IT', 'nl-NL', 'pt-PT']) { + const messages = JSON.parse(readFileSync(`lib/i18n/messages/${locale}.json`, 'utf8')); + expect(messages['calendarModule.googleCalendarNotConfigured'], locale).toBeTruthy(); + } + }); +}); diff --git a/tests/relationship-ai-gift-history.test.ts b/tests/relationship-ai-gift-history.test.ts index cd5a639fa..69dc76643 100644 --- a/tests/relationship-ai-gift-history.test.ts +++ b/tests/relationship-ai-gift-history.test.ts @@ -7,7 +7,7 @@ const mocks = vi.hoisted(() => ({ })); vi.mock('@/lib/supabase/auth', () => ({ requireUserContext: mocks.context })); vi.mock('@/lib/supabase/server', () => ({ createServer: mocks.server })); -vi.mock('@/lib/ai/provider', () => ({ resolveProvider: mocks.provider })); +vi.mock('@/lib/ai/provider', () => ({ resolveProvider: mocks.provider, isAIConfigured: async () => true })); vi.mock('@/lib/server/ai-rate-limit', () => ({ enforceAIRateLimit: mocks.rate })); vi.mock('@/lib/server/audit', () => ({ logAudit: mocks.audit })); From d377aa5993bc48cbedcbaaae4681a89d1cf1c838 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 27 Sep 2026 22:33:10 +0000 Subject: [PATCH 2/4] finalaudit: record the API sweep in Register B 142 API rows move from NOT STARTED to IN PROGRESS with the sweep's per-caller statuses, verdict and fix; the eleven already in progress get the result appended; two routes the inventory missed are added. Register B: 14,228 items, 13,688 NOT STARTED, 525 IN PROGRESS. Records the open High finding API-SWEEP-06: finance tables are readable by a child at the database while the app treats them as adults-only. PRODUCTION READY: NO. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01776xJyhVe8xJQvTGeNfT9T --- finalaudit.md | 369 ++++++++++++++++++++++++++++---------------------- 1 file changed, 210 insertions(+), 159 deletions(-) diff --git a/finalaudit.md b/finalaudit.md index bd42d243b..890b3ce8b 100644 --- a/finalaudit.md +++ b/finalaudit.md @@ -1,5 +1,54 @@ # Final Production Audit +## API sweep — every route under app/api, four callers — 2026-09-27 + +*Claude session_01776xJyhVe8xJQvTGeNfT9T. Register B's API rows, which no one had claimed.* + +**What was run.** Every method of every `app/api/**/route.ts` (155 route methods in 147 files, discovered from disk) was called against a local production build of `main` at `25a3a808` (Next 16.3.6) and a local Supabase with every migration through `0458`. Each was called as four callers: signed out, a `child` member, a non-admin parent, and a parent who is a super admin (`scripts/api-audit/sessions.mjs`, which creates both parents through the real sign-in form and onboarding). Each call sent the input a careless or hostile client sends: nothing on a GET, and on a POST an empty JSON object and a body that is not JSON (`scripts/api-audit/sweep.mjs`, 1,008 calls). `scripts/api-audit/summarize.mjs` puts every call in one bucket: + +- **FAIL**: a 5xx, a dropped connection, a stack trace or database error text in the body, an `/api/admin` route answering 2xx to a child or non-admin parent, or a cron route answering anything but 401/403 without the cron secret. +- **BLOCKED**: a 503 that says the feature is not set up on this server. +- **REVIEW**: a 2xx to a signed-out caller or to a non-JSON body. +- **OK**: everything else. + +**First sweep: 24 FAIL.** +- 7 were real 500s: the four AI routes `invest`, `relationship`, `wallet` and `weekly-briefing`; billing `checkout` and `portal`; and `google/calendar/auth` with an empty body. +- The rest were honest "not configured" 503s, counted as FAIL until the summariser learned to tell them apart. +- Reading the routes behind the 500s found three more defects than the sweep could see. + +**After the fixes: 0 FAIL, 30 BLOCKED, 11 REVIEW, 114 OK.** +- All 30 BLOCKED routes answer an honest 503 because this build has no AI, Stripe, push, GIF or Twilio credentials. +- All 11 REVIEW routes were read. Each is a public read-only endpoint (`health`, `build-info`, `blog/search-index`, `services/descriptions`, `exit-intent/resolve`) or reads an optional body with `readBoundedRequestJsonOrEmpty`, which treats an unreadable body as empty on purpose. +- No admin route answered a child or a non-admin parent. No cron route ran without its secret. No response carried internal detail. + +| ID | Severity | Route(s) | Problem | Fix | Test (red before the fix) | +|---|---|---|---|---|---| +| API-SWEEP-01 | Medium | `GET /api/google/calendar/auth` | With no Google credentials, "Connect Google" on `/dashboard/calendar` threw and left the parent on a blank 500 page. | Returns the parent to the calendar with `gcal=not_configured` and a translated toast, as the Outlook link beside it already does. | `tests/google-calendar-connect-without-credentials.test.ts` | +| API-SWEEP-02 | **High** | `POST /api/billing/{checkout,portal,cancel,change-plan}` | Super Admin → Stripe Setup saves a secret key and promises "configured value first, then env", but only checkout honoured it. Portal, cancel and change-plan read `STRIPE_SECRET_KEY` alone, so a family that subscribed through the configured key could not open the portal, cancel or change plan. With no key anywhere, all four answered 500. Checkout also built its client before reading the body, so a malformed body got a 500 instead of a 400. | All four resolve the key the same way (Setup, then env). Without one they answer 503 "billing isn't set up" before any Stripe call. Checkout reads the body first. | `tests/billing-uses-the-stripe-keys-the-admin-configured.test.ts` (8 of 10 cases red before) | +| API-SWEEP-03 | **High** | `POST /api/webhooks/stripe` | Read `STRIPE_WEBHOOK_SECRET` alone and built a client with `getStripe()` just to check the signature. On a deployment whose key and secret live in Stripe Setup, that threw, and the catch answered "signature invalid" to every real event, so no subscription would ever be recorded. | The Setup secret first, then env, verified with Stripe's keyless static verifier (`constructWebhookEvent` in `lib/stripe.ts`). A wrong secret is still refused. | Same file, with real signatures from Stripe's own test-header generator | +| API-SWEEP-04 | Medium | 14 AI routes; `ai/health/coach` | With no AI key, 18 AI routes said "the engine isn't set up" (503). Nine let the provider's throw reach a generic catch (500). Four answered 502 "temporarily unavailable", which tells the family to retry when retrying cannot work. `ai/health/coach` checked only the environment, so a key saved in Admin → AI Engine was reported missing. | Each asks `isAIConfigured()` before calling the provider. Three routes keep their deliberate model-free fallback, named in the guard: `journal`, `savings` and `behavior/insight`. | `tests/an-ai-route-says-when-the-engine-is-not-set-up.test.ts` (15 of 31 red before) | +| API-SWEEP-05 | **High** (privacy) | `POST /api/ai/savings` | Answered a signed-in child with advice built from the family's transactions, budgets, unpaid bills and subscriptions. The finance service refuses non-managers ("Family finances are private to the adults in this family"), and the privacy export withholds `finances` from a child. | 403 for non-managers before any read. | `tests/a-child-is-not-given-the-familys-savings-advice.test.ts` (3 of 5 red before) | +| API-SWEEP-06 | **High** (privacy) — ⚠️ **OPEN** | database: `transactions`, `budgets`, `bills`, `subscriptions_tracked` | Found while tracing API-SWEEP-05. Probed with a child's own session against the local database: each table's SELECT policy is `is_family_member(family_id)`, so a child reads the family's budgets, bills and tracked subscriptions straight from the REST API. `subscriptions_tracked` also lets any member *write* (`Members manage subscriptions_tracked`, command `*`). Migration 0267 guards only the writes on the first three. The app treats finances as adults-only in three places, and the database does not. | Not fixed here. A manager-only SELECT policy is the obvious fix, but the home page, wallet hub, voting, briefing and agent readers also read these tables. It needs the owner's decision on what a child may see, then a migration, then those readers checked. Production application of any migration is the owner's (PROD-DB-0177). | Proposed probe: `docs/audit/`-style SQL asserting a child JWT reads zero rows from each table | + +**What each API row now says.** Register B's 142 NOT STARTED API rows move to 🔄 IN PROGRESS, not PASS. Each row records: +- the sweep's per-caller statuses; +- its verdict; +- the fix where there was one; +- "Remaining: a valid request, its database side effects and response shape, end to end". + +The eleven rows other sessions already had in progress keep their text, with the sweep's result appended. Two routes on disk had no row (`GET /api/blog/search-index`, `GET /api/cron/library-feeds`); they are added as API-9158428527C6 and API-9DB7C4EE8804. Register B: **14,228 items: 13,688 NOT STARTED, 525 IN PROGRESS, 10 FIXED + PASS, 4 BLOCKED, 1 FAIL.** No closure is claimed. + +**Re-running it** (local Supabase only; both scripts refuse anything else): + +``` +npm run build && npx next start -p 3107 +node scripts/api-audit/sessions.mjs --base http://localhost:3107 --out api-sessions.json +node scripts/api-audit/sweep.mjs api-sessions.json api-sweep.jsonl +node scripts/api-audit/summarize.mjs api-sweep.jsonl +``` + +**PRODUCTION READY: NO.** This sweep closes the unauthenticated, cross-role and bad-input boundary of every API route on a local build. It does not verify a valid request's workflow, live providers or production. + ## Framework browser failure and CSP readiness correction — 2026-09-27 **The newer framework hosted run failed its final browser gate.** [CI36340228681](https://github.com/NewWorldVenture/Bubaly/actions/runs/36340228681) tests b718 with actual checkout79ec1110/treebd217. E2E finishes at18:50:54 UTC with **1,788 PASS plus one FAIL /1,789 in24.5m**, no flaky count. Both attempts of `csp.spec.ts` for `/blog` time out after30s in `page.goto(..., { waitUntil: 'networkidle' })`, before the heading and CSP assertions. Both retained snapshots show the page heading, article cards, category controls and footer rendered. No CSP assertion failure is shown, but the test never reaches that assertion, so no CSP pass is inferred. The precise outstanding request is unknown because the network trace was excluded from the uploaded artifact. Web/Database/Mobile pass, including20,858 units in each of three zones, build246, types/lint and static/production audits. Receipt: `C:/Users/Daniel/AppData/Local/Temp/bubaly-framework-b718-publication-20260927/final-ci-receipt.json`; artifact10939411020 retains both snapshots. The older602 qualified success and all prior failures remain recorded. @@ -816,19 +865,19 @@ IDs link each route to its row in the Session A register (`PAGE-`) and the Sessi *Different scheme and granularity from the block above; see Register B below.* - Started: 2026-09-12T12:41:52.12Z -- Last Updated: 2026-09-27 (82da production verified; b718 framework E2E1FAIL; CSP readiness correction local; fresh602/607 hosted acceptance pending) +- Last Updated: 2026-09-27 22:40Z (API sweep: all 155 route methods × four callers; 142 API rows NOT STARTED → IN PROGRESS, 2 rows added; fixes API-SWEEP-01…05 — see "API sweep — every route under app/api" at the top). Earlier: 82da production verified; b718 framework E2E1FAIL; CSP readiness correction local; fresh602/607 hosted acceptance pending - Page audit (every page on bubaly.com, shared by all bots): see **"Page audit — every page on bubaly.com"** at the end of this file — batches to claim, findings P-01…P-22, and a status row for all 398 routes. - Released: **#541 merged to `main` at `533554be` on 2026-09-26 18:55Z** (merge commit, 242 commits). `main`'s CI on that head is green in all four jobs — Typecheck · Lint · Test · Build (unit tests on three host zones), Mobile, Database (migration replay, 68 boundary probes, re-apply onto an existing schema) and E2E. Production serves it: `GET https://www.bubaly.com/api/build-info` answered `{"revision":"533554be…"}` at 19:13Z, and `/api/health` answered database, auth and service-role **ok** and `status: degraded` because four feature secrets are unset in the production runtime (see Critical Blockers). **Then #580 merged to `main` at `7e54596d` on 2026-09-26 20:21Z** (the units verified after #541: AUDIT-011's 39 re-controlled probes, SEC-009, m6/m9/m12/m30/m0/m28+m29/m42+m43/m18/m35, migrations 0343 and 0349/0352/0360 unapplied and in the ledger); `main`'s CI on that head failed one E2E case (`phone-auth-http` durable-session close) that passed on the next `main` run untouched, and production answered `{"revision":"7e54596d…"}`. **Then #579 merged at `671c5f6a` on 2026-09-27 00:00Z** (another session's pass C1-K: member-write boundaries 0344–0380, trust fail-safes; recorded by that session in the *Release · #579* section below, with the production-migration blocker at 0177) and #582 at `6ff770da`, its release note. Production answered `{"revision":"6ff770da…"}` at 00:44Z, `/api/health` still `degraded` on the same four missing secrets. **Then #581 merged to `main` at `dcc0b42b` on 2026-09-27 01:41Z** (this branch's second tranche: migrations 0381–0387 unapplied and in the ledger, the remaining SRV-001 medium leads, AUTHZ-011, SEC-008, SEC-009, AUDIT-011's 39 re-controlled probes, money in the reader's locale from billing to the marketplace; merged with #579's 0344–0380 after a renumber). Every check was green on the head `3e0e5fc3` — E2E once re-run after the same phone-login durable-session hang `main` had shown on `7e54596d`, recorded on the PR. When it failed a third time (on #583's `fb033b8e`) it was root-caused rather than re-run: the login form's "Continue with phone" button rendered ENABLED from the server with no handler, and once hydrated its handler refused a not-yet-mounted form, so a tap in that window did nothing — the spec clicked right after `domcontentloaded` and waited 120 s for a phone field that never opened. It is a product defect, not only a test one (a family on a slow phone tapping the primary way in got nothing). The phone button, and the signup form's three entry buttons with the same shape, are now held disabled until the form mounts, as the email fieldset already was; `tests/e2e/login-readiness.spec.ts` pins it (red with the fix reverted: "Expected: disabled, Received: enabled"). Production answered `{"revision":"dcc0b42b…"}` at 01:46Z; `/api/health` database, auth and service-role **ok**, `status: degraded` on the same four unset secrets (PROD-ENV). The owner's "Supabase production migrations" workflow ran on the push and will stop at 0177 as before (PROD-DB-0177). **Then #584 merged at `0306c985` on 2026-09-27 02:20Z** (another session's C1-K-56: a notification for someone else is written by Bubaly, not by a member; `0388` unapplied), and production answered `{"revision":"0306c985…"}` at 02:45Z, as that session recorded. **Then #556 merged at `2eb62151` on 2026-09-27 10:51Z** (the four-worker audit branch: its finding IDs, and migrations `0406`–`0418` less `0412`, `0413` and `0417`, all unapplied and in the ledger). This session closed it out: it merged `main` into the branch, renumbered its migrations out of the range `main` had taken, fixed `releaseNumber`, which still called a deadline wrapper `main` had renamed, and moved two ordering guards onto the shared source-order helper. Every check was green on its head `d99faef0`. Three of the branch's decisions were flagged on the PR for a reviewer and stay the owner's to confirm: `0416` changes who may read a journal entry that is not marked private (its owner, or a manager, per `main`'s 0364 rule, where before any family member could); the fallback phone number now needs a country code; and the behaviour-log policy question (`0417` stays dropped). Production answered `{"revision":"2eb62151…"}` at 11:00Z; `/api/health` database, auth and service-role **ok**, `status: degraded` on the same four unset secrets. `main`'s own CI on `2eb62151` finished with every job green except one E2E case: `phone-auth-http` "held genuine SMS verification cannot replace logout" timed out at 120 s, and on its retry (1,295 passed). It is the pre-hydration phone tap root-caused on #583 (`requestCode` presses "Continue with phone" right after load, and before hydration that tap did nothing). The fix, `1f52f00b`, is on #583 and not yet on `main`, so this red has its fix waiting in the open PR rather than an unknown cause. **This is a deployment, not a readiness declaration**: no migration from `0318` on has been applied to production, and `PRODUCTION READY` stays **NO**. **Then #548 merged to `main` at `338b6b12` on 2026-09-27 ~11:55Z** (another session's PR: member-write boundaries `0426`–`0443`, unapplied; recorded by that session in its own sections). Production answered `{"revision":"338b6b12…"}` at 12:02Z. **Then #588, #589 and #590 merged** (a parallel session's live page audit: 16 blog heroes that 404'd, the review page's seeder text, white on danger at 2.80:1), **and #583 merged to `main` at `06dd3f7e` on 2026-09-27 12:48Z** after every job was green on its head `370fc8a8` (Typecheck · Lint · Test · Build, E2E, Database, Mobile) — which also brings `main` the phone-login fix its E2E had been red on. `main`'s CI on `06dd3f7e` is green. Production answered `{"revision":"06dd3f7e…"}` at 13:02Z; `/api/health` database, auth and service-role ok, `status: degraded` on the unset feature secrets. See *Page Audit* above for the per-page state. **Then #591 merged to `main` at `7563462e769386ad5a3cea53e82337cdfc3d9294` on 2026-09-27 16:01:34Z.** Its source head `e798814125c41465158d762177589df10b1612f2` passed all four jobs in CI run `36329827922` (Web completed 15:54:07Z; E2E completed 15:57:45Z). Those checks cover that incoming source, not the combined #592 worker revision. The incoming page-audit sections and their recorded per-page outcomes are preserved below; exact deployed revision and combined-source gates remain to be verified. - Page-audit release record from PR #593 (merge timestamp reconciled to the GitHub event; its observed production time remains unchanged): **Then #591 merged to `main` at `7563462e` on 2026-09-27 16:01:34Z** (this session's page audit: the three-check ledger and every fix it found — titles, not-found paths, alias redirects, grid overflow, the calendar's realtime channel, relative-time hydration, and the signed-in phone pass: 0 axe violations on all 393 signed-in routes in the third crawl), after every job was green on its head `e7988141`. Production answered `{"revision":"7563462e…"}` at 16:06Z; `/api/health` database, auth and service-role **ok**, `status: degraded` on the unset feature secrets. The signed-out production re-crawl that would move its 358 rows from *fixed, live* to *clean* has not been run: the command was refused by this environment's permission check, and is left for the owner (see *Page Audit*). -- Total Audit Items: 14226 — recounted from the current Register B rows; every prior ID is retained. Ten recorded closures remain source-qualified; no new closure is added by this checkpoint. -- Not Started: 13830 -- In Progress: 381 +- Total Audit Items: 14228 — recounted from the current Register B rows after the API sweep added two routes the inventory had missed (API-9158428527C6, API-9DB7C4EE8804); every prior ID is retained. Ten recorded closures remain source-qualified; the sweep adds none. +- Not Started: 13688 +- In Progress: 525 - Passed: 0 - Fixed + Passed: 10 (AUTHZ-002, AUTHZ-003, AUTHZ-005, SEC-005; three worker and three media coverage records). Exact scoped hosted/deployment evidence and all earlier failed checkpoints are retained; no additional product workflows are implied. - Status normalization: MAIN-F-C07, MAIN-F-C09 and MAIN-F-C10 now use IN PROGRESS, matching their pending integrated retests. Their original CLOSED / MITIGATED / MOSTLY MITIGATED verdicts remain verbatim in their Notes and evidence. - Blocked: 4 - Failed: 1 (SEC-001). Repository fixes and replayed-policy checks for AUTHZ-003 and AUTHZ-005 remain recorded as FIXED + PASS with their explicit production-migration limitation. No deployed-policy verification is inferred. -- Overall Completion: **0.07%** (10 recorded closures / 14,226 rows). Four credential/provider rows remain BLOCKED without completion credit. This is audit signoff, not a product-development percentage. +- Overall Completion: **0.07%** (10 recorded closures / 14,228 rows). Four credential/provider rows remain BLOCKED without completion credit. This is audit signoff, not a product-development percentage. > **On these counts, plainly.** *Total Audit Items* is the Codex cycle's > enumeration of the whole target space — every route, table, policy, component @@ -2523,7 +2572,7 @@ not read the same on this page.* hosted-CI/deployed-release evidence this session had no credentials to produce. The two share zero finding IDs. Neither is discarded. --> -# Register B — parallel session inventory (14,226 items) +# Register B — parallel session inventory (14,228 items) - PROD-ENV (owner, measured 2026-09-26 19:13Z): production's `/api/health` reports `CRON_SECRET`, `CHILD_LOGIN_SECRET`, `MARKETING_UNSUB_SECRET` and `GUARDIAN_INTERNAL_SECRET` unset. Per `lib/health/status.ts`, `hasCronAuthorization` is fail-closed, so without `CRON_SECRET` every one of the 24 scheduled jobs in `vercel.json` answers 401 (nightly notifications, wallet allowance, chore reminders, the weekly digest, …); without `CHILD_LOGIN_SECRET` no child in any family can sign in; the other two gate provider ingress and signed links and fail closed. Setting them in the Vercel production environment is the owner's action; the four `ENV-*` rows are BLOCKED on it. Also unset, per the same check: `RESEND_API_KEY`, `INTERNAL_SECRET`, `CONTACT_CENTER_INBOUND_SECRET` were NOT in the missing list, so those are set. - PROD-DB-0177 (owner, measured 2026-09-27 01:15Z on the replayed schema; the failure itself recorded by the #579 session from the owner's "Supabase production migrations" runs on `533554be`, `7e54596d` and `671c5f6a`): production's migration ledger stops at `0176`, so **none of 0177–0387 is applied** and every boundary those migrations close is held in production only by application code. The cause is one statement: `0177_remove_synthetic_auth_users.sql` is a single `DO` block that, for EVERY single-column foreign key to `auth.users`, runs `delete from where = any()`. On the replayed schema that is **493** foreign keys, **481** of them with no index on the referencing column, so the block is hundreds of sequential scans inside one statement and Supabase's `statement_timeout` cancels it (SQLSTATE 57014) — production at 0176 has fewer keys but the same shape. It is idempotent and best-effort by design (a per-table failure is noticed and skipped), so it is safe to run outside the workflow. **The operator remedy, in order**: (1) in the Supabase SQL editor or `psql` against production, `set statement_timeout = 0;` (session-local) and run the body of 0177 once — expect the `Retired synthetic Auth users: N removed, M still referenced` notice; (2) `supabase migration repair --status applied 0177` so the ledger agrees; (3) re-run the "Supabase production migrations" workflow, which will then apply 0178 onward in order (each already replayed by CI on a fresh database). Alternatively delete the `onb…@seed-onb.bubaly.test` / `person…@seed.bubaly.test` users through the Auth admin API first, which makes 0177 a no-op, then repair and re-run. **No agent applied, retried or altered how the workflow applies anything**, and none will: applying migrations to production is the owner's action, and a change that made 0177 pass inside the workflow would apply 0178–0387 on the next push to `main` by proxy. Until this is done, `PRODUCTION READY` cannot move, whatever the code says. - API-387E2B30BCD7: Current signed ingress retention passes focused tests, including deterministic filing after candidate failure. New integrated ingress hosted acceptance, real provider delivery, controlled old-handler cutover and production configuration remain open; see main-integration-cycle-20260919.md. @@ -11212,149 +11261,149 @@ not read the same on this page.* | INCOMPLETE-DF6D7B36BE38 | INCOMPLETE | Triage marker at line 505 | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | {/* The hero used to share a 3-column grid with the demo card, which took | | INCOMPLETE-C93921443751 | INCOMPLETE | Triage marker at line 52 | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | className="min-w-0 flex-1 bg-transparent text-sm outline-none placeholder:text-white/40 sm:text-base" | | INCOMPLETE-D3507BBBC254 | INCOMPLETE | Triage marker at line 28 | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | // One whole sentence with a {action} placeholder rather than three fragments | -| API-7BACE12808B7 | API | POST /api/ab/track | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-972FC3BFEB60 | API | GET /api/admin/benchmarks/export | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-192980CC87C1 | API | POST /api/admin/marketing/ai | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-77A1BDDE52B2 | API | GET /api/admin/marketing/email/send | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-68728C68AD8D | API | POST /api/admin/marketing/email/send | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-E1BCA929F06B | API | POST /api/ai/assist | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-E984841AFEA4 | API | POST /api/ai/auto/accident | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-B817F4FD65DE | API | POST /api/ai/briefing | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-E8DE1DE9AFA2 | API | POST /api/ai/chat | 🔄 IN PROGRESS | Medium | tests/a-filtered-delete-is-not-a-deletion.test.ts (list measured from pg_policies by docs/audit/gated-write-tables-check.sql) | Conversation title update filtered id alone; Added .eq('family_id', …) + .select('id'); refuses on an empty result (logs on no-op) | Guard green | Browser workflow not yet exercised. See finalaudit.md Q54. | -| API-1DF9D2390329 | API | POST /api/ai/chef | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-754EB564B7B5 | API | POST /api/ai/flyer | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-4332A474F9DA | API | POST /api/ai/gift | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-A3A4F06659C5 | API | POST /api/ai/habits | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-31CA668A95F5 | API | POST /api/ai/health/coach | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-BE4AC9438408 | API | POST /api/ai/home/diagnose | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-329D84D35085 | API | POST /api/ai/home/find-pro | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-75F7180DAD95 | API | POST /api/ai/home/forecast | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-C216882AA5DE | API | POST /api/ai/home/utility-savings | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-9D988F2B8ACC | API | POST /api/ai/import | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-80AAFA4CCA10 | API | POST /api/ai/insights | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-67B80E5EBF4C | API | POST /api/ai/invest | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-E44B52560D9C | API | POST /api/ai/journal | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-5F3292E1BB0A | API | POST /api/ai/meals/nutrition | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-2AD065090629 | API | POST /api/ai/meals/plan | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-E95D3BD32EAD | API | POST /api/ai/notes | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-045500BCF0A1 | API | POST /api/ai/pantry-chef | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-5C9FB320E768 | API | POST /api/ai/relationship | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-A1FF58EB51A2 | API | POST /api/ai/requests | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-31E78F63AA9E | API | POST /api/ai/resolve-conflict | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-D43D567DBE01 | API | GET /api/ai | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-42F4CE50BBB9 | API | POST /api/ai | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-6855DE440AC2 | API | POST /api/ai/runs/[id]/answer | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-3DB3A40247D5 | API | POST /api/ai/runs/[id]/cancel | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-45439B3E3DBB | API | POST /api/ai/runs/[id]/pause | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-AD1807ED4F6A | API | POST /api/ai/runs/[id]/rerun | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-5DB8F5A367DA | API | POST /api/ai/runs/[id]/resume | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-2DCD34E48CC5 | API | GET /api/ai/runs/[id] | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-9F45E8EE3CFB | API | POST /api/ai/savings | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-AF853B35E97C | API | POST /api/ai/schedule | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-7597FF39C2ED | API | POST /api/ai/trip | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-41D8A1C2AE43 | API | POST /api/ai/voice/speak | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-8955795CBD4F | API | POST /api/ai/voice/transcribe | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-85D247FBAEB7 | API | POST /api/ai/wallet/child/[childId] | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-525FA07CE158 | API | POST /api/ai/wallet | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-326F6604F14A | API | POST /api/ai/weekly-briefing | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-5DF6AEE61A0B | API | POST /api/autopilot/scan | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-DE112DF76F8A | API | POST /api/behavior/insight | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-83DBB97C8A0A | API | POST /api/billing/cancel | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-968F4094B484 | API | POST /api/billing/change-plan | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-5328789F6A3F | API | POST /api/billing/checkout | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-78D77493E36C | API | POST /api/billing/portal | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-5F170E3A49A7 | API | GET /api/blog/like | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-1E8AF3D8807C | API | POST /api/blog/like | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-30F8DADE4622 | API | GET /api/blog/save | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-109685A51F09 | API | POST /api/blog/save | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-98BB54B3E611 | API | POST /api/blog/subscribe | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-FEE8A89E9C00 | API | GET /api/blog/unsubscribe | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-44E3F76CB80E | API | GET /api/build-info | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-1D27A496479B | API | POST /api/calendar/sync | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-80142EB1C3FC | API | GET /api/concierge-calls/place | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-1B0FCD08C0B2 | API | POST /api/contact | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-0EB76703163C | API | POST /api/contact-center/email | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-387E2B30BCD7 | API | POST /api/contact-center/sms | 🔄 IN PROGRESS | High | Current 2026-09-19 integration: signed canonical ingress is retained before optional candidate work; timeout/locale failures preserve deterministic filing without a reply, and a late ignored-abort candidate cannot emit. Contact Center, marketing push and social publication regression passes 875 checks across 32 suites. Hosted acceptance of the new ingress fixture and real provider delivery remain pending. Historical evidence: Full application source a61804db passes 15,011 unit tests across 1,202 files with zero failures/skips. Focused helper checks pass 223 cases, concierge checks pass 38, and signed ingress/authorized reader cover new Unicode replay and legacy boundaries. New disposable signed HTTP/PostgreSQL text case is discovered; hosted execution pending. | Retain the signed original ingress before optional context and candidate work; freeze the first reply candidate before any automatic emission; exact one-time final emission reservation; held legacy/uncertain replies; verified deterministic projection. Candidate timeout or locale failure retains deterministic summary/classification and files without an automatic reply. Signed delivery callbacks bind token, account and first provider SID, advance status monotonically and expose safe provider progress in the authorized inbox. New text uses scalar-safe PostgreSQL/XML bounds before candidate fingerprinting; bounded family labels preserve useful fallbacks; old oversized queued replies are held without rewriting stored content. Provider completion receives the candidate cancellation signal. | Application a61804db passes all 15,011 unit tests in 1,202 files, zero failed/skipped, in 43.60 seconds. The exact frozen source was committed without changes during execution. Application a61804db builds 252 pages (44-second compilation). Strict types, lint with four baseline warnings, localization and query audit (491 tables, 77 functions, 144 routes) PASS. See contact-center-text-bounds-cycle.md. Signed delivery publication 0d68bdbb passes CI 34718960420: 1,064 scheduled E2E cases and 1,064 passes in 8.0 minutes, with both authenticated/durable flags enabled; identical tested tree becff049. Web, database, mobile, finance and Vercel PASS. New text source a61804db requires its own hosted execution. See guardian-hosted-acceptance.md. | Private checks and synthetic hosted callbacks are separate from real provider delivery. No SQL, dependency or shared navigation changes; exact source inventory preserved. See docs/final-audit/main-integration-cycle-20260919.md; new hosted ingress acceptance remains pending. | -| API-97ED864085E2 | API | POST /api/contact-center/voice | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-B4E47347CDDF | API | POST /api/contact-center/voice/transcription | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-96040DFB5635 | API | GET /api/cron/admin-digest | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-9BCA8F281320 | API | GET /api/cron/ai-runs | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-8CC89F0675A6 | API | GET /api/cron/automations | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-C70A0D9938B0 | API | GET /api/cron/autopilot-scan | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-1AB4E900BF69 | API | GET /api/cron/calendar-feeds | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-1D509B56AAEE | API | GET /api/cron/checkout-abandoned | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-F190F9FD81D5 | API | GET /api/cron/chore-reminders | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-19C785624D88 | API | GET /api/cron/close-auctions | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-A1B70253196E | API | GET /api/cron/family-routines | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-9FBC14D60B9D | API | GET /api/cron/feedback-github-sync | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-24F0D3A37ABD | API | GET /api/cron/guardian-learning | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-A018B97030A5 | API | GET /api/cron/journey-recovery | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-B7C1506769BC | API | GET /api/cron/marketing | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-418A48B840A8 | API | GET /api/cron/marketing-providers | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-AAC0F7C0EF4E | API | GET /api/cron/model-refresh | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-3FFBC3414FDB | API | GET /api/cron/network-aggregate | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-F06602E9AAE7 | API | GET /api/cron/notifications | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-0F84C6DEF913 | API | GET /api/cron/provider-sync | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-0FE523360549 | API | GET /api/cron/push-scan | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-3A5082FCD0B8 | API | GET /api/cron/return-reminders | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-DCEC08074039 | API | GET /api/cron/wallet-allowance | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-F536B0FB53D0 | API | GET /api/cron/weekly-digest | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-1AC88B3A7ED8 | API | POST /api/email/invite | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-F65D59457C92 | API | POST /api/email/welcome | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-A3760E1A3194 | API | POST /api/exit-intent/resolve | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-1EA8BE1CCF2A | API | POST /api/exit-intent/track | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-5D86C14BDD41 | API | POST /api/forms/submit | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-8E5BBB079A7B | API | GET /api/gif/search | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-6C9D5D3559D9 | API | GET /api/google/calendar/auth | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-F4FA62CD1839 | API | GET /api/google/calendar/callback | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-C4F20C9B8291 | API | POST /api/google/calendar/sync | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-CDB65ADB1E12 | API | GET /api/google/calendar/sync | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-DE92C0D3E1A2 | API | POST /api/guardian/escalate | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-4624F4C80639 | API | GET /api/guardian/escalate/twiml | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-82DE1172411D | API | POST /api/guardian/escalate/twiml | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-BBD0A5DB630F | API | POST /api/guardian/inbound/sms | 🔄 IN PROGRESS | High | Source 237933c1 integrates application and unit files byte-identical to tested isolated eee4ec06. Full unit gate: 1,180 files / 14,151 PASS in42.35s. Focused Guardian groups: 84 policy/rule/caller cases and176 signed SMS/lease/security cases PASS, including four real-pipeline SMS integrations. See guardian-policy-read-checkpoint.md. | Service-owned exact-input/communication/decision receipts authorize replay. Missing proof triggers fresh screening; neutral input survives ledger-read failure. Full conditional decision updates and verified readback precede notification. | Publication fae35e90: CI 34717274616, tested checkout 62d8a325 with tree bb838fce equal to publication; 14,636 units, 252-page build, all 1,062 scheduled browser cases PASS. Database: 300 migrations, zero failures, eleven probes; finance: 66 assertions, mobile and Vercel PASS. See guardian-hosted-acceptance.md. | Guardian service-owned decision authority and autonomous recovery now pass hosted disposable PostgreSQL/HTTP checks. | -| API-262DB8655008 | API | POST /api/guardian/inbound/voice | 🔄 IN PROGRESS | High | Ten signed actual-pipeline caller cases PASS across voice/WhatsApp, including required policy failures, healthy blocked decisions and invalid signatures. Callback claims are controlled as granted. | Required-policy failures return503 before downstream routing or notification. | Included in full14151-unit and production-build PASS at isolatedeee4ec06, integrated237933c1. | Existing destination-read behavior, unclaimed retry acknowledgement and ten-minute callback reclaim remain unverified; no durable voice/WhatsApp intake claim. | -| API-90346B8397DA | API | POST /api/guardian/inbound/whatsapp | 🔄 IN PROGRESS | High | Ten signed actual-pipeline caller cases PASS across voice/WhatsApp, including required policy failures, healthy blocked decisions and invalid signatures. Callback claims are controlled as granted. | Required-policy failures return503 before downstream routing or notification. | Included in full14151-unit and production-build PASS at isolatedeee4ec06, integrated237933c1. | Existing destination-read behavior, unclaimed retry acknowledgement and ten-minute callback reclaim remain unverified; no durable voice/WhatsApp intake claim. | -| API-5A16104BF051 | API | POST /api/guardian/screen | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-A3BF950AE646 | API | POST /api/guardian/status/voicemail | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-2F38298B7BB9 | API | GET /api/health | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-02C7F9F1B8EA | API | POST /api/lp/track | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-E3E78AD03D61 | API | GET /api/marketing/unsubscribe | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-262A058A148E | API | POST /api/marketing/unsubscribe | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-2C772F6CE84C | API | POST /api/mkt/consent | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-E172ED0D13F4 | API | GET /api/mkt/consent | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-C06797833926 | API | POST /api/mkt/track | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-A59CC3A2156D | API | POST /api/moving/recalculate | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-FDA0AB66957A | API | POST /api/notifications/generate | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-A99EA8197759 | API | POST /api/paperwork/capture | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-C0C3F892998F | API | POST /api/paperwork/link | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-BB4FF5888E65 | API | GET /api/privacy/export | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-B502B4642DF3 | API | POST /api/push/subscribe | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-818E7ADDB5D4 | API | POST /api/push/test | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-224863753D3F | API | POST /api/push/unsubscribe | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-EECB4738F390 | API | GET /api/recipes/search | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-D1DF9BBFEFD5 | API | POST /api/recipes/suggest | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-28B5240AADC2 | API | POST /api/recipes/transform | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-A3A1F108621E | API | GET /api/services/descriptions | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-E2E956FFD1B3 | API | POST /api/social/ai | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-F571E7B557A1 | API | GET /api/subscriptions/candidates | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-95D8CD8A9A26 | API | GET /api/subscriptions/price-history | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-D7D066AF0B43 | API | GET /api/sync/feeds/[token] | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-21D5C99CA684 | API | GET /api/sync/google/auth | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-9C3C1BDFD72F | API | GET /api/sync/google/callback | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-E9E9049B9B6D | API | POST /api/sync/google/disconnect | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-8E58562A6CBF | API | POST /api/sync/google/sync | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-5F0880D1E5C9 | API | POST /api/sync/run | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-0808E9D1E51D | API | GET /api/sync/[provider]/auth | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-1C8A03D9F52E | API | GET /api/sync/[provider]/callback | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-6B7F27956BB2 | API | POST /api/sync/[provider]/disconnect | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-487FF44303FE | API | POST /api/vacations/ai | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-FCD111ED9432 | API | POST /api/vacations/confirmation-import | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-97822A801C67 | API | POST /api/vacations/weather | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-19260D46CFD1 | API | POST /api/webhooks/money | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-C4A25E75B606 | API | POST /api/webhooks/resend | 🔄 IN PROGRESS | High | docs/audit/email-counter-once-check.sql (a released-and-retaken claim answers already_applied and the counter stays 1; a stale worker counts nothing; unknown campaign left alone; only counter columns; no client can execute) · tests/resend-webhook-execution.test.ts (40, 2 new) | 2026-09-26 (Q66): the campaign counter is applied through apply_resend_campaign_counter() — marked and incremented in one transaction, once per event, only for the claim holder; the compare-and-set loop remains as the fallback for a database without 0441 | Fresh replay 350/350, probes 58/58; route tests 40/40, including one pinning that the pre-0441 fallback still counts twice | EMAIL-002. See Q66. | -| API-ED756A60DF1B | API | POST /api/webhooks/stripe | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| API-C282DCE0790A | API | POST /api/weekend/discover | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | +| API-7BACE12808B7 | API | POST /api/ab/track | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:422 a!:400 c:422 c!:400 p:422 p!:400 a:422 a!:400] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-972FC3BFEB60 | API | GET /api/admin/benchmarks/export | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:307 c:403 p:403 a:200] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-192980CC87C1 | API | POST /api/admin/marketing/ai | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:307 a!:307 c:403 c!:403 p:403 p!:403 a:503 a!:503] — BLOCKED: every 5xx is an honest 503 "not set up": the credential-dependent path is BLOCKED locally (no provider keys) | API-SWEEP-04: with no AI key answered 500/502 "temporarily unavailable"; now isAIConfigured() first, 503 "not set up" (guard test, red before) | Re-swept on the fixed build: BLOCKED | Remaining: a valid request, its database side effects and response shape, end to end. Live provider path needs credentials. | +| API-77A1BDDE52B2 | API | GET /api/admin/marketing/email/send | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:307 c:403 p:403 a:400] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-68728C68AD8D | API | POST /api/admin/marketing/email/send | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:307 a!:307 c:403 c!:403 p:403 p!:403 a:400 a!:400] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-E1BCA929F06B | API | POST /api/ai/assist | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:307 a!:307 c:503 c!:503 p:503 p!:503 a:503 a!:503] — BLOCKED: every 5xx is an honest 503 "not set up": the credential-dependent path is BLOCKED locally (no provider keys) | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. Live provider path needs credentials. | +| API-E984841AFEA4 | API | POST /api/ai/auto/accident | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:307 a!:307 c:503 c!:503 p:503 p!:503 a:503 a!:503] — BLOCKED: every 5xx is an honest 503 "not set up": the credential-dependent path is BLOCKED locally (no provider keys) | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. Live provider path needs credentials. | +| API-B817F4FD65DE | API | POST /api/ai/briefing | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:307 a!:307 c:403 c!:403 p:403 p!:403 a:200 a!:200] — REVIEW: reviewed: public read-only by design, or an optional body read as empty by readBoundedRequestJsonOrEmpty | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-E8DE1DE9AFA2 | API | POST /api/ai/chat | 🔄 IN PROGRESS | Medium | tests/a-filtered-delete-is-not-a-deletion.test.ts (list measured from pg_policies by docs/audit/gated-write-tables-check.sql) | Conversation title update filtered id alone; Added .eq('family_id', …) + .select('id'); refuses on an empty result (logs on no-op) | Guard green | Browser workflow not yet exercised. See finalaudit.md Q54. API sweep 2026-09-27: OK [a:307 a!:307 c:400 c!:400 p:400 p!:400 a:400 a!:400]. | +| API-1DF9D2390329 | API | POST /api/ai/chef | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:307 a!:307 c:400 c!:400 p:400 p!:400 a:400 a!:400] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-754EB564B7B5 | API | POST /api/ai/flyer | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:307 a!:307 c:400 c!:400 p:400 p!:400 a:400 a!:400] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-4332A474F9DA | API | POST /api/ai/gift | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:400 a!:400 c:400 c!:400 p:400 p!:429 a:429 a!:429] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | API-SWEEP-04: with no AI key the provider throw reached a generic catch (500); now isAIConfigured() first, 503 "not set up" (guard tests/an-ai-route-says-when-the-engine-is-not-set-up.test.ts, red before) | Re-swept on the fixed build: OK | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-A3A4F06659C5 | API | POST /api/ai/habits | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:307 a!:307 c:503 c!:503 p:503 p!:503 a:503 a!:503] — BLOCKED: every 5xx is an honest 503 "not set up": the credential-dependent path is BLOCKED locally (no provider keys) | API-SWEEP-04: with no AI key the provider throw reached a generic catch (500); now isAIConfigured() first, 503 "not set up" (guard tests/an-ai-route-says-when-the-engine-is-not-set-up.test.ts, red before) | Re-swept on the fixed build: BLOCKED | Remaining: a valid request, its database side effects and response shape, end to end. Live provider path needs credentials. | +| API-31CA668A95F5 | API | POST /api/ai/health/coach | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:307 a!:307 c:503 c!:503 p:503 p!:503 a:503 a!:503] — BLOCKED: every 5xx is an honest 503 "not set up": the credential-dependent path is BLOCKED locally (no provider keys) | API-SWEEP-04: decided "not configured" from the environment alone, so a key saved in Admin → AI Engine was reported missing; now isAIConfigured() | Re-swept on the fixed build: BLOCKED | Remaining: a valid request, its database side effects and response shape, end to end. Live provider path needs credentials. | +| API-BE4AC9438408 | API | POST /api/ai/home/diagnose | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:307 a!:307 c:403 c!:403 p:403 p!:403 a:503 a!:503] — BLOCKED: every 5xx is an honest 503 "not set up": the credential-dependent path is BLOCKED locally (no provider keys) | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. Live provider path needs credentials. | +| API-329D84D35085 | API | POST /api/ai/home/find-pro | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:307 a!:307 c:403 c!:403 p:403 p!:403 a:503 a!:503] — BLOCKED: every 5xx is an honest 503 "not set up": the credential-dependent path is BLOCKED locally (no provider keys) | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. Live provider path needs credentials. | +| API-75F7180DAD95 | API | POST /api/ai/home/forecast | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:307 a!:307 c:403 c!:403 p:403 p!:403 a:503 a!:503] — BLOCKED: every 5xx is an honest 503 "not set up": the credential-dependent path is BLOCKED locally (no provider keys) | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. Live provider path needs credentials. | +| API-C216882AA5DE | API | POST /api/ai/home/utility-savings | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:307 a!:307 c:400 c!:400 p:400 p!:400 a:400 a!:400] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-9D988F2B8ACC | API | POST /api/ai/import | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:307 a!:307 c:503 c!:503 p:503 p!:503 a:503 a!:503] — BLOCKED: every 5xx is an honest 503 "not set up": the credential-dependent path is BLOCKED locally (no provider keys) | API-SWEEP-04: with no AI key the provider throw reached a generic catch (500); now isAIConfigured() first, 503 "not set up" (guard tests/an-ai-route-says-when-the-engine-is-not-set-up.test.ts, red before) | Re-swept on the fixed build: BLOCKED | Remaining: a valid request, its database side effects and response shape, end to end. Live provider path needs credentials. | +| API-80AAFA4CCA10 | API | POST /api/ai/insights | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:307 a!:307 c:503 c!:503 p:503 p!:503 a:503 a!:503] — BLOCKED: every 5xx is an honest 503 "not set up": the credential-dependent path is BLOCKED locally (no provider keys) | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. Live provider path needs credentials. | +| API-67B80E5EBF4C | API | POST /api/ai/invest | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:307 a!:307 c:503 c!:503 p:503 p!:503 a:503 a!:503] — BLOCKED: every 5xx is an honest 503 "not set up": the credential-dependent path is BLOCKED locally (no provider keys) | API-SWEEP-04: with no AI key the provider throw reached a generic catch (500); now isAIConfigured() first, 503 "not set up" (guard tests/an-ai-route-says-when-the-engine-is-not-set-up.test.ts, red before) | Re-swept on the fixed build: BLOCKED | Remaining: a valid request, its database side effects and response shape, end to end. Live provider path needs credentials. | +| API-E44B52560D9C | API | POST /api/ai/journal | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:307 a!:307 c:200 c!:200 p:200 p!:200 a:200 a!:200] — REVIEW: reviewed: public read-only by design, or an optional body read as empty by readBoundedRequestJsonOrEmpty | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-5F3292E1BB0A | API | POST /api/ai/meals/nutrition | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:307 a!:307 c:503 c!:503 p:503 p!:503 a:503 a!:503] — BLOCKED: every 5xx is an honest 503 "not set up": the credential-dependent path is BLOCKED locally (no provider keys) | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. Live provider path needs credentials. | +| API-2AD065090629 | API | POST /api/ai/meals/plan | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:307 a!:307 c:503 c!:503 p:503 p!:503 a:503 a!:503] — BLOCKED: every 5xx is an honest 503 "not set up": the credential-dependent path is BLOCKED locally (no provider keys) | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. Live provider path needs credentials. | +| API-E95D3BD32EAD | API | POST /api/ai/notes | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:307 a!:307 c:503 c!:503 p:503 p!:503 a:503 a!:503] — BLOCKED: every 5xx is an honest 503 "not set up": the credential-dependent path is BLOCKED locally (no provider keys) | API-SWEEP-04: with no AI key the provider throw reached a generic catch (500); now isAIConfigured() first, 503 "not set up" (guard tests/an-ai-route-says-when-the-engine-is-not-set-up.test.ts, red before) | Re-swept on the fixed build: BLOCKED | Remaining: a valid request, its database side effects and response shape, end to end. Live provider path needs credentials. | +| API-045500BCF0A1 | API | POST /api/ai/pantry-chef | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:307 a!:307 c:400 c!:400 p:400 p!:400 a:400 a!:400] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-5C9FB320E768 | API | POST /api/ai/relationship | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:307 a!:307 c:503 c!:503 p:503 p!:503 a:503 a!:503] — BLOCKED: every 5xx is an honest 503 "not set up": the credential-dependent path is BLOCKED locally (no provider keys) | API-SWEEP-04: with no AI key the provider throw reached a generic catch (500); now isAIConfigured() first, 503 "not set up" (guard tests/an-ai-route-says-when-the-engine-is-not-set-up.test.ts, red before) | Re-swept on the fixed build: BLOCKED | Remaining: a valid request, its database side effects and response shape, end to end. Live provider path needs credentials. | +| API-A1FF58EB51A2 | API | POST /api/ai/requests | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:307 a!:307 c:400 c!:400 p:400 p!:400 a:400 a!:400] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-31E78F63AA9E | API | POST /api/ai/resolve-conflict | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:307 a!:307 c:403 c!:403 p:403 p!:403 a:503 a!:503] — BLOCKED: every 5xx is an honest 503 "not set up": the credential-dependent path is BLOCKED locally (no provider keys) | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. Live provider path needs credentials. | +| API-D43D567DBE01 | API | GET /api/ai | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:307 c:200 p:200 a:200] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-42F4CE50BBB9 | API | POST /api/ai | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:307 a!:307 c:400 c!:400 p:400 p!:400 a:400 a!:400] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-6855DE440AC2 | API | POST /api/ai/runs/[id]/answer | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:307 a!:307 c:400 c!:400 p:400 p!:400 a:400 a!:400] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-3DB3A40247D5 | API | POST /api/ai/runs/[id]/cancel | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:307 a!:307 c:404 c!:404 p:404 p!:404 a:404 a!:404] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-45439B3E3DBB | API | POST /api/ai/runs/[id]/pause | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:307 a!:307 c:404 c!:404 p:404 p!:404 a:404 a!:404] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-AD1807ED4F6A | API | POST /api/ai/runs/[id]/rerun | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:307 a!:307 c:400 c!:400 p:400 p!:400 a:400 a!:400] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-5DB8F5A367DA | API | POST /api/ai/runs/[id]/resume | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:307 a!:307 c:404 c!:404 p:404 p!:404 a:404 a!:404] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-2DCD34E48CC5 | API | GET /api/ai/runs/[id] | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:307 c:404 p:404 a:404] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-9F45E8EE3CFB | API | POST /api/ai/savings | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:307 a!:307 c:403 c!:403 p:200 p!:200 a:200 a!:200] — REVIEW: reviewed: public read-only by design, or an optional body read as empty by readBoundedRequestJsonOrEmpty | API-SWEEP-05: answered a CHILD with advice built from the family’s transactions, budgets, bills and subscriptions, which the finance service and the privacy export keep for adults; now 403 for non-managers before any read (tests/a-child-is-not-given-the-familys-savings-advice.test.ts, red before) | Re-swept on the fixed build: REVIEW | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-AF853B35E97C | API | POST /api/ai/schedule | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:307 a!:307 c:200 c!:400 p:200 p!:400 a:200 a!:400] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-7597FF39C2ED | API | POST /api/ai/trip | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:307 a!:307 c:400 c!:400 p:400 p!:400 a:400 a!:400] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-41D8A1C2AE43 | API | POST /api/ai/voice/speak | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:307 a!:307 c:503 c!:503 p:503 p!:503 a:503 a!:503] — BLOCKED: every 5xx is an honest 503 "not set up": the credential-dependent path is BLOCKED locally (no provider keys) | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. Live provider path needs credentials. | +| API-8955795CBD4F | API | POST /api/ai/voice/transcribe | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:307 a!:307 c:503 c!:503 p:503 p!:503 a:503 a!:503] — BLOCKED: every 5xx is an honest 503 "not set up": the credential-dependent path is BLOCKED locally (no provider keys) | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. Live provider path needs credentials. | +| API-85D247FBAEB7 | API | POST /api/ai/wallet/child/[childId] | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:307 a!:307 c:503 c!:503 p:503 p!:503 a:503 a!:503] — BLOCKED: every 5xx is an honest 503 "not set up": the credential-dependent path is BLOCKED locally (no provider keys) | API-SWEEP-04: with no AI key the provider throw reached a generic catch (500); now isAIConfigured() first, 503 "not set up" (guard tests/an-ai-route-says-when-the-engine-is-not-set-up.test.ts, red before) | Re-swept on the fixed build: BLOCKED | Remaining: a valid request, its database side effects and response shape, end to end. Live provider path needs credentials. | +| API-525FA07CE158 | API | POST /api/ai/wallet | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:307 a!:307 c:503 c!:503 p:503 p!:503 a:503 a!:503] — BLOCKED: every 5xx is an honest 503 "not set up": the credential-dependent path is BLOCKED locally (no provider keys) | API-SWEEP-04: with no AI key the provider throw reached a generic catch (500); now isAIConfigured() first, 503 "not set up" (guard tests/an-ai-route-says-when-the-engine-is-not-set-up.test.ts, red before) | Re-swept on the fixed build: BLOCKED | Remaining: a valid request, its database side effects and response shape, end to end. Live provider path needs credentials. | +| API-326F6604F14A | API | POST /api/ai/weekly-briefing | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:307 a!:307 c:402 c!:402 p:402 p!:402 a:503 a!:503] — BLOCKED: every 5xx is an honest 503 "not set up": the credential-dependent path is BLOCKED locally (no provider keys) | API-SWEEP-04: with no AI key the provider throw reached a generic catch (500); now isAIConfigured() first, 503 "not set up" (guard tests/an-ai-route-says-when-the-engine-is-not-set-up.test.ts, red before) | Re-swept on the fixed build: BLOCKED | Remaining: a valid request, its database side effects and response shape, end to end. Live provider path needs credentials. | +| API-5DF6AEE61A0B | API | POST /api/autopilot/scan | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:307 a!:307 c:403 c!:403 p:403 p!:403 a:200 a!:200] — REVIEW: reviewed: public read-only by design, or an optional body read as empty by readBoundedRequestJsonOrEmpty | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-DE112DF76F8A | API | POST /api/behavior/insight | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:307 a!:307 c:200 c!:200 p:200 p!:200 a:200 a!:200] — REVIEW: reviewed: public read-only by design, or an optional body read as empty by readBoundedRequestJsonOrEmpty | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-83DBB97C8A0A | API | POST /api/billing/cancel | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:307 a!:307 c:403 c!:403 p:400 p!:400 a:400 a!:400] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | API-SWEEP-02: env-only Stripe key, same as portal; fixed with the same resolution (same test, red before) | Re-swept on the fixed build: OK | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-968F4094B484 | API | POST /api/billing/change-plan | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:307 a!:307 c:403 c!:400 p:400 p!:400 a:400 a!:400] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | API-SWEEP-02: env-only Stripe key, same as portal; fixed with the same resolution (covered by tests/billing-price-verification.test.ts) | Re-swept on the fixed build: OK | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-5328789F6A3F | API | POST /api/billing/checkout | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:307 a!:307 c:403 c!:403 p:400 p!:400 a:400 a!:400] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | API-SWEEP-02: built the Stripe client before reading the body (malformed body = 500) and a missing key = 500; body first, then configured-key-first resolution, 503 "billing isn’t set up" (tests/billing-uses-the-stripe-keys-the-admin-configured.test.ts, red before) | Re-swept on the fixed build: OK | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-78D77493E36C | API | POST /api/billing/portal | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:307 a!:307 c:403 c!:403 p:503 p!:503 a:503 a!:503] — BLOCKED: every 5xx is an honest 503 "not set up": the credential-dependent path is BLOCKED locally (no provider keys) | API-SWEEP-02: used getStripe() (env only), ignoring the key saved in Super Admin → Stripe Setup that checkout uses; now the same resolution, 503 when neither exists (same test, red before) | Re-swept on the fixed build: BLOCKED | Remaining: a valid request, its database side effects and response shape, end to end. Live provider path needs credentials. | +| API-5F170E3A49A7 | API | GET /api/blog/like | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:400 c:400 p:400 a:400] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-1E8AF3D8807C | API | POST /api/blog/like | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:400 a!:400 c:400 c!:400 p:400 p!:400 a:400 a!:400] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-30F8DADE4622 | API | GET /api/blog/save | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:400 c:400 p:400 a:400] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-109685A51F09 | API | POST /api/blog/save | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:401 a!:401 c:400 c!:400 p:400 p!:400 a:400 a!:400] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-98BB54B3E611 | API | POST /api/blog/subscribe | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:400 a!:400 c:400 c!:400 p:400 p!:400 a:400 a!:400] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-FEE8A89E9C00 | API | GET /api/blog/unsubscribe | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:307 c:307 p:307 a:307] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-44E3F76CB80E | API | GET /api/build-info | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:200 c:200 p:200 a:200] — REVIEW: reviewed: public read-only by design, or an optional body read as empty by readBoundedRequestJsonOrEmpty | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-1D27A496479B | API | POST /api/calendar/sync | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:307 a!:307 c:400 c!:400 p:400 p!:400 a:400 a!:400] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-80142EB1C3FC | API | GET /api/concierge-calls/place | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:401 c:401 p:401 a:401] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-1B0FCD08C0B2 | API | POST /api/contact | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:422 a!:400 c:422 c!:400 p:422 p!:429 a:429 a!:429] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-0EB76703163C | API | POST /api/contact-center/email | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:401 a!:401 c:401 c!:401 p:401 p!:401 a:401 a!:401] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-387E2B30BCD7 | API | POST /api/contact-center/sms | 🔄 IN PROGRESS | High | Current 2026-09-19 integration: signed canonical ingress is retained before optional candidate work; timeout/locale failures preserve deterministic filing without a reply, and a late ignored-abort candidate cannot emit. Contact Center, marketing push and social publication regression passes 875 checks across 32 suites. Hosted acceptance of the new ingress fixture and real provider delivery remain pending. Historical evidence: Full application source a61804db passes 15,011 unit tests across 1,202 files with zero failures/skips. Focused helper checks pass 223 cases, concierge checks pass 38, and signed ingress/authorized reader cover new Unicode replay and legacy boundaries. New disposable signed HTTP/PostgreSQL text case is discovered; hosted execution pending. | Retain the signed original ingress before optional context and candidate work; freeze the first reply candidate before any automatic emission; exact one-time final emission reservation; held legacy/uncertain replies; verified deterministic projection. Candidate timeout or locale failure retains deterministic summary/classification and files without an automatic reply. Signed delivery callbacks bind token, account and first provider SID, advance status monotonically and expose safe provider progress in the authorized inbox. New text uses scalar-safe PostgreSQL/XML bounds before candidate fingerprinting; bounded family labels preserve useful fallbacks; old oversized queued replies are held without rewriting stored content. Provider completion receives the candidate cancellation signal. | Application a61804db passes all 15,011 unit tests in 1,202 files, zero failed/skipped, in 43.60 seconds. The exact frozen source was committed without changes during execution. Application a61804db builds 252 pages (44-second compilation). Strict types, lint with four baseline warnings, localization and query audit (491 tables, 77 functions, 144 routes) PASS. See contact-center-text-bounds-cycle.md. Signed delivery publication 0d68bdbb passes CI 34718960420: 1,064 scheduled E2E cases and 1,064 passes in 8.0 minutes, with both authenticated/durable flags enabled; identical tested tree becff049. Web, database, mobile, finance and Vercel PASS. New text source a61804db requires its own hosted execution. See guardian-hosted-acceptance.md. | Private checks and synthetic hosted callbacks are separate from real provider delivery. No SQL, dependency or shared navigation changes; exact source inventory preserved. See docs/final-audit/main-integration-cycle-20260919.md; new hosted ingress acceptance remains pending. API sweep 2026-09-27: OK [a:400 a!:400 c:400 c!:400 p:400 p!:400 a:400 a!:400]. | +| API-97ED864085E2 | API | POST /api/contact-center/voice | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:400 a!:400 c:400 c!:400 p:400 p!:400 a:400 a!:400] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-B4E47347CDDF | API | POST /api/contact-center/voice/transcription | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:400 a!:400 c:400 c!:400 p:400 p!:400 a:400 a!:400] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-96040DFB5635 | API | GET /api/cron/admin-digest | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:401 c:401 p:401 a:401] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-9BCA8F281320 | API | GET /api/cron/ai-runs | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:401 c:401 p:401 a:401] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-8CC89F0675A6 | API | GET /api/cron/automations | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:401 c:401 p:401 a:401] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-C70A0D9938B0 | API | GET /api/cron/autopilot-scan | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:401 c:401 p:401 a:401] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-1AB4E900BF69 | API | GET /api/cron/calendar-feeds | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:401 c:401 p:401 a:401] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-1D509B56AAEE | API | GET /api/cron/checkout-abandoned | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:401 c:401 p:401 a:401] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-F190F9FD81D5 | API | GET /api/cron/chore-reminders | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:401 c:401 p:401 a:401] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-19C785624D88 | API | GET /api/cron/close-auctions | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:401 c:401 p:401 a:401] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-A1B70253196E | API | GET /api/cron/family-routines | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:401 c:401 p:401 a:401] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-9FBC14D60B9D | API | GET /api/cron/feedback-github-sync | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:401 c:401 p:401 a:401] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-24F0D3A37ABD | API | GET /api/cron/guardian-learning | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:401 c:401 p:401 a:401] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-A018B97030A5 | API | GET /api/cron/journey-recovery | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:401 c:401 p:401 a:401] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-B7C1506769BC | API | GET /api/cron/marketing | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:401 c:401 p:401 a:401] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-418A48B840A8 | API | GET /api/cron/marketing-providers | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:401 c:401 p:401 a:401] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-AAC0F7C0EF4E | API | GET /api/cron/model-refresh | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:401 c:401 p:401 a:401] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-3FFBC3414FDB | API | GET /api/cron/network-aggregate | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:401 c:401 p:401 a:401] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-F06602E9AAE7 | API | GET /api/cron/notifications | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:401 c:401 p:401 a:401] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-0F84C6DEF913 | API | GET /api/cron/provider-sync | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:401 c:401 p:401 a:401] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-0FE523360549 | API | GET /api/cron/push-scan | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:401 c:401 p:401 a:401] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-3A5082FCD0B8 | API | GET /api/cron/return-reminders | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:401 c:401 p:401 a:401] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-DCEC08074039 | API | GET /api/cron/wallet-allowance | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:401 c:401 p:401 a:401] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-F536B0FB53D0 | API | GET /api/cron/weekly-digest | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:401 c:401 p:401 a:401] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-1AC88B3A7ED8 | API | POST /api/email/invite | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:307 a!:307 c:400 c!:400 p:400 p!:400 a:400 a!:400] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-F65D59457C92 | API | POST /api/email/welcome | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:401 a!:401 c:401 c!:401 p:401 p!:401 a:401 a!:401] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-A3760E1A3194 | API | POST /api/exit-intent/resolve | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:200 a!:200 c:200 c!:200 p:200 p!:200 a:200 a!:200] — REVIEW: reviewed: public read-only by design, or an optional body read as empty by readBoundedRequestJsonOrEmpty | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-1EA8BE1CCF2A | API | POST /api/exit-intent/track | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:422 a!:400 c:422 c!:400 p:422 p!:400 a:422 a!:400] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-5D86C14BDD41 | API | POST /api/forms/submit | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:422 a!:400 c:422 c!:400 p:422 p!:400 a:422 a!:400] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-8E5BBB079A7B | API | GET /api/gif/search | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:307 c:503 p:503 a:503] — BLOCKED: every 5xx is an honest 503 "not set up": the credential-dependent path is BLOCKED locally (no provider keys) | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. Live provider path needs credentials. | +| API-6C9D5D3559D9 | API | GET /api/google/calendar/auth | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:307 c:307 p:307 a:307] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | API-SWEEP-01: with no Google credentials it threw a blank 500; now returns the parent to /dashboard/calendar?gcal=not_configured with a translated toast (tests/google-calendar-connect-without-credentials.test.ts, red before) | Re-swept on the fixed build: OK | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-F4FA62CD1839 | API | GET /api/google/calendar/callback | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:307 c:307 p:307 a:307] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-C4F20C9B8291 | API | POST /api/google/calendar/sync | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:307 a!:307 c:400 c!:400 p:400 p!:400 a:400 a!:400] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-CDB65ADB1E12 | API | GET /api/google/calendar/sync | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:307 c:200 p:200 a:200] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-DE92C0D3E1A2 | API | POST /api/guardian/escalate | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:401 a!:401 c:401 c!:401 p:401 p!:401 a:401 a!:401] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-4624F4C80639 | API | GET /api/guardian/escalate/twiml | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:503 c:503 p:503 a:503] — BLOCKED: every 5xx is an honest 503 "not set up": the credential-dependent path is BLOCKED locally (no provider keys) | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. Live provider path needs credentials. | +| API-82DE1172411D | API | POST /api/guardian/escalate/twiml | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:400 a!:400 c:400 c!:400 p:400 p!:400 a:400 a!:400] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-BBD0A5DB630F | API | POST /api/guardian/inbound/sms | 🔄 IN PROGRESS | High | Source 237933c1 integrates application and unit files byte-identical to tested isolated eee4ec06. Full unit gate: 1,180 files / 14,151 PASS in42.35s. Focused Guardian groups: 84 policy/rule/caller cases and176 signed SMS/lease/security cases PASS, including four real-pipeline SMS integrations. See guardian-policy-read-checkpoint.md. | Service-owned exact-input/communication/decision receipts authorize replay. Missing proof triggers fresh screening; neutral input survives ledger-read failure. Full conditional decision updates and verified readback precede notification. | Publication fae35e90: CI 34717274616, tested checkout 62d8a325 with tree bb838fce equal to publication; 14,636 units, 252-page build, all 1,062 scheduled browser cases PASS. Database: 300 migrations, zero failures, eleven probes; finance: 66 assertions, mobile and Vercel PASS. See guardian-hosted-acceptance.md. | Guardian service-owned decision authority and autonomous recovery now pass hosted disposable PostgreSQL/HTTP checks. API sweep 2026-09-27: OK [a:400 a!:400 c:400 c!:400 p:400 p!:400 a:400 a!:400]. | +| API-262DB8655008 | API | POST /api/guardian/inbound/voice | 🔄 IN PROGRESS | High | Ten signed actual-pipeline caller cases PASS across voice/WhatsApp, including required policy failures, healthy blocked decisions and invalid signatures. Callback claims are controlled as granted. | Required-policy failures return503 before downstream routing or notification. | Included in full14151-unit and production-build PASS at isolatedeee4ec06, integrated237933c1. | Existing destination-read behavior, unclaimed retry acknowledgement and ten-minute callback reclaim remain unverified; no durable voice/WhatsApp intake claim. API sweep 2026-09-27: OK [a:400 a!:400 c:400 c!:400 p:400 p!:400 a:400 a!:400]. | +| API-90346B8397DA | API | POST /api/guardian/inbound/whatsapp | 🔄 IN PROGRESS | High | Ten signed actual-pipeline caller cases PASS across voice/WhatsApp, including required policy failures, healthy blocked decisions and invalid signatures. Callback claims are controlled as granted. | Required-policy failures return503 before downstream routing or notification. | Included in full14151-unit and production-build PASS at isolatedeee4ec06, integrated237933c1. | Existing destination-read behavior, unclaimed retry acknowledgement and ten-minute callback reclaim remain unverified; no durable voice/WhatsApp intake claim. API sweep 2026-09-27: OK [a:400 a!:400 c:400 c!:400 p:400 p!:400 a:400 a!:400]. | +| API-5A16104BF051 | API | POST /api/guardian/screen | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:400 a!:400 c:400 c!:400 p:400 p!:400 a:400 a!:400] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-A3BF950AE646 | API | POST /api/guardian/status/voicemail | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:400 a!:400 c:400 c!:400 p:400 p!:400 a:400 a!:400] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-2F38298B7BB9 | API | GET /api/health | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:200 c:200 p:200 a:200] — REVIEW: reviewed: public read-only by design, or an optional body read as empty by readBoundedRequestJsonOrEmpty | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-02C7F9F1B8EA | API | POST /api/lp/track | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:422 a!:400 c:422 c!:400 p:422 p!:400 a:422 a!:400] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-E3E78AD03D61 | API | GET /api/marketing/unsubscribe | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:400 c:400 p:400 a:400] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-262A058A148E | API | POST /api/marketing/unsubscribe | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:400 a!:400 c:400 c!:400 p:400 p!:400 a:400 a!:400] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-2C772F6CE84C | API | POST /api/mkt/consent | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:400 a!:400 c:400 c!:400 p:400 p!:400 a:400 a!:400] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-E172ED0D13F4 | API | GET /api/mkt/consent | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:400 c:400 p:400 a:400] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-C06797833926 | API | POST /api/mkt/track | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:400 a!:400 c:400 c!:400 p:400 p!:400 a:400 a!:400] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-A59CC3A2156D | API | POST /api/moving/recalculate | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:307 a!:307 c:403 c!:403 p:403 p!:400 a:403 a!:400] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-FDA0AB66957A | API | POST /api/notifications/generate | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:307 a!:307 c:200 c!:200 p:200 p!:200 a:200 a!:200] — REVIEW: reviewed: public read-only by design, or an optional body read as empty by readBoundedRequestJsonOrEmpty | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-A99EA8197759 | API | POST /api/paperwork/capture | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:307 a!:307 c:400 c!:400 p:400 p!:400 a:400 a!:400] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-C0C3F892998F | API | POST /api/paperwork/link | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:307 a!:307 c:409 c!:400 p:409 p!:400 a:409 a!:400] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-BB4FF5888E65 | API | GET /api/privacy/export | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:307 c:200 p:200 a:200] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-B502B4642DF3 | API | POST /api/push/subscribe | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:307 a!:307 c:400 c!:400 p:400 p!:400 a:400 a!:400] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-818E7ADDB5D4 | API | POST /api/push/test | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:307 a!:307 c:503 c!:503 p:503 p!:503 a:503 a!:503] — BLOCKED: every 5xx is an honest 503 "not set up": the credential-dependent path is BLOCKED locally (no provider keys) | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. Live provider path needs credentials. | +| API-224863753D3F | API | POST /api/push/unsubscribe | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:307 a!:307 c:400 c!:400 p:400 p!:400 a:400 a!:400] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-EECB4738F390 | API | GET /api/recipes/search | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:307 c:200 p:200 a:200] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-D1DF9BBFEFD5 | API | POST /api/recipes/suggest | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:307 a!:307 c:503 c!:503 p:503 p!:503 a:503 a!:503] — BLOCKED: every 5xx is an honest 503 "not set up": the credential-dependent path is BLOCKED locally (no provider keys) | API-SWEEP-04: with no AI key answered 500/502 "temporarily unavailable"; now isAIConfigured() first, 503 "not set up" (guard test, red before) | Re-swept on the fixed build: BLOCKED | Remaining: a valid request, its database side effects and response shape, end to end. Live provider path needs credentials. | +| API-28B5240AADC2 | API | POST /api/recipes/transform | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:307 a!:307 c:503 c!:503 p:503 p!:503 a:503 a!:503] — BLOCKED: every 5xx is an honest 503 "not set up": the credential-dependent path is BLOCKED locally (no provider keys) | API-SWEEP-04: with no AI key answered 500/502 "temporarily unavailable"; now isAIConfigured() first, 503 "not set up" (guard test, red before) | Re-swept on the fixed build: BLOCKED | Remaining: a valid request, its database side effects and response shape, end to end. Live provider path needs credentials. | +| API-A3A1F108621E | API | GET /api/services/descriptions | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:200 c:200 p:200 a:200] — REVIEW: reviewed: public read-only by design, or an optional body read as empty by readBoundedRequestJsonOrEmpty | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-E2E956FFD1B3 | API | POST /api/social/ai | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:307 a!:307 c:403 c!:403 p:400 p!:400 a:400 a!:400] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-F571E7B557A1 | API | GET /api/subscriptions/candidates | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:307 c:403 p:200 a:200] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-95D8CD8A9A26 | API | GET /api/subscriptions/price-history | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:307 c:403 p:400 a:400] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-D7D066AF0B43 | API | GET /api/sync/feeds/[token] | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:404 c:404 p:404 a:404] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-21D5C99CA684 | API | GET /api/sync/google/auth | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:307 c:307 p:307 a:307] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-9C3C1BDFD72F | API | GET /api/sync/google/callback | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:307 c:307 p:307 a:307] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-E9E9049B9B6D | API | POST /api/sync/google/disconnect | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:307 a!:307 c:303 c!:303 p:303 p!:303 a:303 a!:303] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-8E58562A6CBF | API | POST /api/sync/google/sync | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:307 a!:307 c:400 c!:400 p:400 p!:400 a:400 a!:400] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-5F0880D1E5C9 | API | POST /api/sync/run | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:307 a!:307 c:400 c!:400 p:400 p!:400 a:400 a!:400] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-0808E9D1E51D | API | GET /api/sync/[provider]/auth | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:307 c:307 p:307 a:307] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-1C8A03D9F52E | API | GET /api/sync/[provider]/callback | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:307 c:307 p:307 a:307] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-6B7F27956BB2 | API | POST /api/sync/[provider]/disconnect | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:307 a!:307 c:303 c!:303 p:303 p!:303 a:303 a!:303] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-487FF44303FE | API | POST /api/vacations/ai | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:307 a!:307 c:503 c!:503 p:503 p!:503 a:503 a!:503] — BLOCKED: every 5xx is an honest 503 "not set up": the credential-dependent path is BLOCKED locally (no provider keys) | API-SWEEP-04: with no AI key answered 500/502 "temporarily unavailable"; now isAIConfigured() first, 503 "not set up" (guard test, red before) | Re-swept on the fixed build: BLOCKED | Remaining: a valid request, its database side effects and response shape, end to end. Live provider path needs credentials. | +| API-FCD111ED9432 | API | POST /api/vacations/confirmation-import | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:307 a!:307 c:400 c!:400 p:400 p!:400 a:400 a!:400] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-97822A801C67 | API | POST /api/vacations/weather | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:307 a!:307 c:400 c!:400 p:400 p!:400 a:400 a!:400] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | +| API-19260D46CFD1 | API | POST /api/webhooks/money | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:503 a!:503 c:503 c!:503 p:503 p!:503 a:503 a!:503] — BLOCKED: every 5xx is an honest 503 "not set up": the credential-dependent path is BLOCKED locally (no provider keys) | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. Live provider path needs credentials. | +| API-C4A25E75B606 | API | POST /api/webhooks/resend | 🔄 IN PROGRESS | High | docs/audit/email-counter-once-check.sql (a released-and-retaken claim answers already_applied and the counter stays 1; a stale worker counts nothing; unknown campaign left alone; only counter columns; no client can execute) · tests/resend-webhook-execution.test.ts (40, 2 new) | 2026-09-26 (Q66): the campaign counter is applied through apply_resend_campaign_counter() — marked and incremented in one transaction, once per event, only for the claim holder; the compare-and-set loop remains as the fallback for a database without 0441 | Fresh replay 350/350, probes 58/58; route tests 40/40, including one pinning that the pre-0441 fallback still counts twice | EMAIL-002. See Q66. API sweep 2026-09-27: OK [a:401 a!:401 c:401 c!:401 p:401 p!:401 a:401 a!:401]. | +| API-ED756A60DF1B | API | POST /api/webhooks/stripe | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:503 a!:503 c:503 c!:503 p:503 p!:503 a:503 a!:503] — BLOCKED: every 5xx is an honest 503 "not set up": the credential-dependent path is BLOCKED locally (no provider keys) | API-SWEEP-03: read STRIPE_WEBHOOK_SECRET only and built a client with getStripe() to verify, so with the key only in Stripe Setup every real event was answered "signature invalid"; now the Setup secret first, then env, verified with Stripe’s keyless static verifier (real signatures in tests/billing-uses-the-stripe-keys-the-admin-configured.test.ts, red before) | Re-swept on the fixed build: BLOCKED | Remaining: a valid request, its database side effects and response shape, end to end. Live provider path needs credentials. | +| API-C282DCE0790A | API | POST /api/weekend/discover | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:307 a!:307 c:400 c!:400 p:400 p!:400 a:400 a!:400] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Remaining: a valid request, its database side effects and response shape, end to end. | | API-9A739F355ACF | API | GET /auth/callback | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | | API-216181642979 | API | POST /auth/signout | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | | ACTION-A1ED72ED6D21 | ACTION | closeAccountAction | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | @@ -15927,7 +15976,7 @@ not read the same on this page.* | UI-003 | UI | Family display calendar and availability correctness | 🔄 IN PROGRESS | High | docs/final-audit/kitchen-calendar-cycle.md; docs/final-audit/display-state-cycle.md; docs/final-audit/display-clock-cycle.md | Family-zone civil-day/calendar overlap and canonical reminders carry independent read status. Family/user-owned display state adopts idle refreshed settings while preserving edit drafts and local save baselines. A synchronous owner-scoped lock serializes saves/dismissals and blocks false cancellation of in-flight writes. Header,photo frame,event labels and ambienttime use family zone; unavailable data suppresses false availability claims. Eight new UI strings provided in all7base locales. | 10 server/helper/display/reminder suites with 164 tests PASS; 25 display ownership and six clock/photo actual React/Chromium checks PASS, included in the final 108-check component gate. Strict tsc identified an actual Locale object/string mismatch, fixed using .code; corrected fixtures use real locale objects and exercise French dates. Final application source b4d4ad782884066ffebb398ed87425e4f54855b8: production build, strict TypeScript, full 1,136-file / 12,715-test Vitest suite and 108 actual React/Chromium component checks PASS. Generated route types match the strict-check input byte for byte. Full-project lint passes with four unchanged baseline warnings; final changed display files pass scoped lint. See docs/final-audit/verification-checkpoint.md for source/log provenance and limitations. | Hook ownership repaired under DATA-002 first; display/shell and canonical-reminder source follow separately. | | DATA-002 | DATA | Realtime query ownership, request ordering and failure completion | 🔄 IN PROGRESS | High | docs/final-audit/realtime-query-cycle.md; tests/e2e/realtime-query.spec.ts | Query state is tied to table/family/serialized dependencies and mount lifetime. Stale-owner rows are masked during render; request generation fences data/error/loading/cache commits. Captured fetcher and key stay paired; thrown/failed/missing-table reads expose errors. Exact-key caches include valid empty results and expose stale/saved-time state. Central cache-generation invalidation additionally fences pending responses and retained callbacks/setters on auth purge, including failed physical storage deletion. Cache hydration now requires an exact v2 user/session/family/server-access/query identity. All three AppProvider sites use required membership reads, not the optional roster. Legacy/unowned cache is never hydrated by the production hook; callers outside the provider are network-only. Stable semantic dependencies preserve query state through normal token rotation. | 68 actual auth/cache Chromium checks pass, including 32 shared query cases and 22 partition cases. Failed physical deletion, process restart, stale bootstrap, six access dimensions and copied descendant state are exercised with actual SDK/React. Root server identity/degradation gate: 22 tests pass. Combined application source b80e55bf8ebe46b695e412a1104ed45430e1bac9: full 1,140-file / 12,782-test Vitest suite PASS (77.56s), strict TypeScript PASS, 144 Chromium component checks PASS (12.4s), production build PASS, unchanged four baseline lint warnings, query/i18n gates PASS. Generated route types match the strict-check input. Final startup probe: home 2089ms, feature navigation 1631ms, no page errors. Evidence: docs/final-audit/partition-verification-checkpoint.md. | Next cycle active: durable v2 user/session/family/access partition, exact envelope/query identity, and descendant retirement after observed account/access change. Source evidence and scope in auth-cache-partition-proposal.md; master remains IN PROGRESS before implementation. · 2026-09-26 (Q62): the "direct query consumers omit read errors" class re-measured — six consequential reads fixed (trip 404, trip concierge fork, independence milestones, concierge write-backs, Guardian rules, dismissed-insight resurrection) and the remaining nine named with their harmless default in tests/a-read-that-drops-its-error-is-named.test.ts. | | PUSH-004 | PUSH | User and parental consent on every public push sender | 🔄 IN PROGRESS | High | docs/final-audit/push-consent-cycle.md; actual marketing action, own-user test endpoint and public sender execution; stateful policy/failure/recovery fixtures | Every public sender resolves user and parental consent before accessing devices. Policy reads are chunked at200 distinctusers and allchunks resolve before any delivery. Private transport is only invoked with permitted recipients; dispatcher reuses its batch decision. Explicit withheld counts resolve deliberate optouts without treating them as unconfigured retry failures. Marketing persists withheld in existing skipped total and retains separate audit counts. | 13 related suites / 144 tests PASS; final two cron suites / 27 tests PASS; strict types, production build and full 12,715-test suite PASS. Denied user/parental policy sends nothing, policy-read failure marks campaign failed, and recovery/adult/zero-audience cases pass. Provider/device workflow and remaining campaign failure/recovery obligations stay open. | No schema changes or live provider sends. | -| API-9173F41D99C0 | API | GET /api/sync/[provider]/status | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | Incremental source discovery; see discovery/incremental-inventory.json for inspection commit and SHA-256. Workflow verification pending. | +| API-9173F41D99C0 | API | GET /api/sync/[provider]/status | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:307 c:200 p:200 a:200] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Incremental source discovery; see discovery/incremental-inventory.json for inspection commit and SHA-256. Workflow verification pending. Remaining: a valid request, its database side effects and response shape, end to end. | | SUPPORT-2F04FE3C0839 | SUPPORT | docs/runbooks/google-oauth-production.md | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | Incremental source discovery; see discovery/incremental-inventory.json for inspection commit and SHA-256. Workflow verification pending. | | LIBRARY-E4F55CEFC4E6 | LIBRARY | lib/display/calendar.ts | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | Incremental source discovery; see discovery/incremental-inventory.json for inspection commit and SHA-256. Workflow verification pending. | | SERVICE-CE7B98A63052 | SERVICE | displayTimezone | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | Incremental source discovery; see discovery/incremental-inventory.json for inspection commit and SHA-256. Workflow verification pending. | @@ -16006,7 +16055,7 @@ not read the same on this page.* | AUTHZ-003 | AUTHZ | Deleting a restrictive social role must not restore broader household permissions | 🛠 FIXED + PASS | Critical | Reproduced against the replayed schema (docs/audit/social-restriction-delete-check.sql): a `read_only` adult deleted their own row (1 row), resolved to `marketing_manager` and could publish. | Migration 0344 gives social_access_permissions DELETE the UPDATE predicate (is_family_admin or manage_access). The app never deletes these rows, so no legitimate path narrows. | Probe passes after 0344 (restriction holds; parent can still remove it); 0344 re-applies idempotently; 42/42 probes; migration audits and 41 migration-related test files pass. | Also reproduced on this branch as a real authenticated adult on a full replay (docs/audit/social-access-delete-boundary-check.sql, which now asserts main's 0344 — this branch's own 0377 was dropped at the merge as its duplicate, Q68). Repository-verified 2026-09-26 (Pass C1-K). Production carries it only after the F-001 migration-ledger repair; deployed policy unverified. | | DATA-005 | DATA | Medication dose actions must use current verified household and daily state | 🔄 IN PROGRESS | High | See docs/final-audit/medications-ledger-cycle.md and tests/e2e/medications-ledger.spec.ts. | Required ledger gates, owner and per-form opening lifetimes, exactslot conditional writes, midnight/DST review, and opt-in latest committed read confirmation with deferred acknowledged-create completion. | Original33med checks plus10independent form/readback cases and6shared-hook cases verified across focusedruns; full combined frozen-source gate pending. Pinned608c9307: full1151-file/13003-test suite PASS (124.53s, zero unhandled errors), 265 Chromium checks PASS, production245page build PASS, final strict types/lint/query/i18n PASS under isolated Node24.21.0. Browser/build runtime-identical3effbf41; exact provenance and limits in care-verification-checkpoint.md. | Existing canonical surfaces UI-ROUTE-0191, COMPONENT-8DD7D691D391, LIBRARY-47A7FE099D19, LIBRARY-3B7C7846912C, DB-TBL-275/276/277. No SQL or clinical advice/configuration changes. | | SUPPORT-8F67371FBEBC | SUPPORT | app/api/social/x/callback/route.ts | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | Source discovery only; exact hashes and baseline in discovery/social-rewards-inventory.json. Full workflow verification remains separate. | -| API-3B7C407D8AC2 | API | GET /api/social/x/callback | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | Source discovery only; exact hashes and baseline in discovery/social-rewards-inventory.json. Full workflow verification remains separate. | +| API-3B7C407D8AC2 | API | GET /api/social/x/callback | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:307 c:400 p:400 a:400] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Source discovery only; exact hashes and baseline in discovery/social-rewards-inventory.json. Full workflow verification remains separate. Remaining: a valid request, its database side effects and response shape, end to end. | | LIBRARY-90A14267D9AB | LIBRARY | lib/social/account-tokens.ts | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | Source discovery only; exact hashes and baseline in discovery/social-rewards-inventory.json. Full workflow verification remains separate. | | SERVICE-29F92A78FE3C | SERVICE | xFailure | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | Source discovery only; exact hashes and baseline in discovery/social-rewards-inventory.json. Full workflow verification remains separate. | | SERVICE-B2EBE7B38719 | SERVICE | isXId | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | Source discovery only; exact hashes and baseline in discovery/social-rewards-inventory.json. Full workflow verification remains separate. | @@ -16038,7 +16087,7 @@ not read the same on this page.* | PERF-002 | PERF | Production response-stream cancellation and supported runtime | 🔄 IN PROGRESS | High | CI34699583414 E2E logs contain GET/ transformAlgorithm TypeErrors. Independent installed Next15.5.25/localNode22.23.1 cancellation/write reproduction25of25; nativeTransformStream25of25 matches hostedNode22.23.2 stack. Official upstream fix review identifies guarded behavior inNode24.15.0; exact hostedGET/ trigger remains unproven. | MinimumNode24.15<25 packageengine with matching rootlockmetadata; webCI Node24. No dependencyversions/globalruntime changed. | Official portableNode24.21.0 checksum verified in isolatedTemp. Same native and installedNext15.5.25 cancel/write harness:Node22.23.1 internal errors25/25+25/25;Node24.21.0 errors0/25+0/25. Pinned608c9307: full1151-file/13003-test suite PASS (124.53s, zero unhandled errors), 265 Chromium checks PASS, production245page build PASS, final strict types/lint/query/i18n PASS under isolated Node24.21.0. Browser/build runtime-identical3effbf41; exact provenance and limits in care-verification-checkpoint.md. Final production probe:5homepage response cancellations,0internalstreamerrors; home2064ms/navigation1631ms/no pageerrors. Published d36602e (application source608c9307) hosted CI34701809388: all4jobsPASS,13003unit tests,11database probes,697browser tests in5.3minutes, no final flaky summary. Web runtimeNode24.20.0; retrieved log contains no TransformStream/ERR_INVALID_STATE match. Vercel checkPASS. See care-verification-checkpoint.md; live provider/cron/policy proof remains separate. | Finding recorded before configuration changes. Existing dependency tree remains untouched. | | TEST-002 | TEST | Authenticated browser fixture ownership across projects | 🔄 IN PROGRESS | High | Actual unmodified beforeEach hooks executed in two isolated VM project modules with shared synthetic Auth store:chromium createsuser/family;iphone shared-emailcleanup deletesboth and replacesuser. Same resultNode22and24. Exact hostedtoast failure attribution remains unproven. | Fresh project/worker/nonce-owned fixture email, identity-checked cleanup and unchanged fullonboarding/taskpersistence assertions. | 4actualhook interleaving/ownership regressions PASS. Published d36602e (application source608c9307) hosted CI34701809388: all4jobsPASS,13003unit tests,11database probes,697browser tests in5.3minutes, no final flaky summary. Web runtimeNode24.20.0; retrieved log contains no TransformStream/ERR_INVALID_STATE match. Vercel checkPASS. See care-verification-checkpoint.md; live provider/cron/policy proof remains separate. | Recorded before fixture edits. No live user accounts modified. | | SUPPORT-5D639D9E4966 | SUPPORT | app/api/cron/contact-center-urgent/route.ts | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | Source discovery only; exact committed hashes and baseline in discovery/care-delivery-inventory.json. Full workflow verification remains separate. | -| API-62F6F32B2748 | API | GET /api/cron/contact-center-urgent | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | Source discovery only; exact committed hashes and baseline in discovery/care-delivery-inventory.json. Full workflow verification remains separate. | +| API-62F6F32B2748 | API | GET /api/cron/contact-center-urgent | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:401 c:401 p:401 a:401] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Source discovery only; exact committed hashes and baseline in discovery/care-delivery-inventory.json. Full workflow verification remains separate. Remaining: a valid request, its database side effects and response shape, end to end. | | JOB-47B3E5312926 | JOB | /api/cron/contact-center-urgent | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | Source discovery only; exact committed hashes and baseline in discovery/care-delivery-inventory.json. Full workflow verification remains separate. | | SERVICE-7F6A900AF295 | SERVICE | inboundProviderRef | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | Source discovery only; exact committed hashes and baseline in discovery/care-delivery-inventory.json. Full workflow verification remains separate. | | SERVICE-A7E34BED0527 | SERVICE | findInboundMessage | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | Source discovery only; exact committed hashes and baseline in discovery/care-delivery-inventory.json. Full workflow verification remains separate. | @@ -16070,7 +16119,7 @@ not read the same on this page.* | DATA-007 | DATA | Quick Capture current-owner task creation and Undo | 🔄 IN PROGRESS | High | 16 actual component/AppProvider/ToastProvider/cache/SDK characterization cases:4healthy controls,10defect scenarios,2behavioral characterizations;45related unit checks pass. Ordinary Ctrl+Enter bypass of disabledSave produces2POSTs. No attribution to priorCItoastflake. | QuickCapture, CaptureShell and VoiceModule now use synchronous submission guards and current owner/form/lifetime checks. Required list reads, creates and exact mutation/Undo identities are checked. Each SDK request has a15second AbortError deadline and a contained race for noncooperative transport; uncertain mutations hold review rather than blind retry. Confirmed voice capture feedback does not await optional history. | 62helper cases and related5file108test run PASS;39QuickCapture/CaptureShell and9voice Chromium checks PASS. Root combined83capture/social Chromium checks PASS16.3s;10file320focused tests including catalogue/time/action/render/capture PASS. Whole-project types and full lintPASS with4baseline warnings. Pinned910cd271:1156files/13254unit tests PASS128.09s withzero unhandled errors;336Chromium PASS53.0s;246page production build,final strict types,lint/query/i18n PASS under isolatedNode24.21.0. Build/browser/startup runtime-identicalf6e17ef; sole follow-up changes2cronfixtures+evidence. Home2076ms/navigation1647ms/nopageerrors;5responsecancellations/0internalstreamerrors. Exactlogs/provenance/limitations in capture-scheduling-checkpoint.md. | Permanentrefs COMPONENT-97EAFCA06075,CONTROL-C7BEF2FB673A,LIBRARY-3010D3A2884E,DB-TBL-418/419. Explicit Undo of previously created IDs after owner switch and nullable optional roster are behavioral characterizations, not proven defects; preserve their current policy. No shared navigation changes. · 2026-09-26 (Q66 note): the cross-client default-list race was re-read in source — lib/services/tasks ensureTodoList and lib/services/groceries ensureDefaultList create a list only when the family has NO un-archived one, with a lookup-then-insert and no constraint, so two first captures at once can create two lists. A duplicate list, not lost data; closing it needs a serialising RPC (families may legitimately hold many lists, so a unique index is the wrong tool). Filed, not fixed. · 2026-09-26 (Q69): FIXED for every server-side writer by 0443 (a per-family advisory lock around get-or-create, SECURITY INVOKER), proved by a two-session race probe whose lock-less control makes 2 lists where 0443 makes 1. Still open for lib/capture/save.ts (quick capture and voice), whose deadline machinery two E2E harnesses pin request by request — filed as its own change. | | SEC-004 | SEC | Persisted social URLs must not become active-content links | 🔄 IN PROGRESS | High | Independent actual createPostAction plus real React18 PostPage static rendering persists javascript:alert(document.domain) and renders href=javascript despite a React warning. No browser code execution or live exploit claimed. Source trace also finds unvalidated target/feed/comment permalink anchors. | Shared safeSocialLink validates HTTP(S), no credentials/whitespace and4096character limit. Action rejects unsafe incoming links; post, target, feed and inbox rendering validates persisted links, retains escaped text and safe profile fallbacks. | Independent real action andReactSSR tests pass all9URL-specific checks within37presentation and75action cases. Validlinks remain clickable; unsafe legacy javascript/data/credential/whitespace/overlength values do not become anchors; no React warning. Root combined83Chromium and320focused tests pass. No browser exploit/liveDB claim. Pinned910cd271:1156files/13254unit tests PASS128.09s withzero unhandled errors;336Chromium PASS53.0s;246page production build,final strict types,lint/query/i18n PASS under isolatedNode24.21.0. Build/browser/startup runtime-identicalf6e17ef; sole follow-up changes2cronfixtures+evidence. Home2076ms/navigation1647ms/nopageerrors;5responsecancellations/0internalstreamerrors. Exactlogs/provenance/limitations in capture-scheduling-checkpoint.md. | See independent social-scheduling-actions-review.md. No SQL or provider operations. | | SUPPORT-38ACFBD09F34 | SUPPORT | app/api/cron/social-publish/route.ts | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | Source discovery only; exact committed hashes and baseline in discovery/capture-scheduling-inventory.json. Full workflow verification remains separate. | -| API-18FB4D1FF9A8 | API | GET /api/cron/social-publish | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | Source discovery only; exact committed hashes and baseline in discovery/capture-scheduling-inventory.json. Full workflow verification remains separate. | +| API-18FB4D1FF9A8 | API | GET /api/cron/social-publish | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:401 c:401 p:401 a:401] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Source discovery only; exact committed hashes and baseline in discovery/capture-scheduling-inventory.json. Full workflow verification remains separate. Remaining: a valid request, its database side effects and response shape, end to end. | | JOB-3A0C45177B5D | JOB | /api/cron/social-publish | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | Source discovery only; exact committed hashes and baseline in discovery/capture-scheduling-inventory.json. Full workflow verification remains separate. | | SERVICE-9BB09AB06EF2 | SERVICE | assertXCredentials | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | Source discovery only; exact committed hashes and baseline in discovery/capture-scheduling-inventory.json. Full workflow verification remains separate. | | SERVICE-8C21B3472285 | SERVICE | loadScheduledXAccessToken | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | Source discovery only; exact committed hashes and baseline in discovery/capture-scheduling-inventory.json. Full workflow verification remains separate. | @@ -16229,7 +16278,7 @@ not read the same on this page.* | SERVICE-3F2D2F45866E | SERVICE | RecurringAdsPage | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | Source discovery only; exact committed hashes and baseline in discovery/weekly-meal-inventory.json. Full workflow verification remains separate. | | SERVICE-73DB0E2176B9 | SERVICE | createMealAction | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | Source discovery only; exact committed hashes and baseline in discovery/weekly-meal-inventory.json. Full workflow verification remains separate. | | SUPPORT-3650D4324732 | SUPPORT | app/api/cron/marketing-social/route.ts | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | Source discovery only; exact committed hashes and baseline in discovery/weekly-meal-inventory.json. Full workflow verification remains separate. | -| API-9ED269DBDB75 | API | GET /api/cron/marketing-social | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | Source discovery only; exact committed hashes and baseline in discovery/weekly-meal-inventory.json. Full workflow verification remains separate. | +| API-9ED269DBDB75 | API | GET /api/cron/marketing-social | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:401 c:401 p:401 a:401] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Source discovery only; exact committed hashes and baseline in discovery/weekly-meal-inventory.json. Full workflow verification remains separate. Remaining: a valid request, its database side effects and response shape, end to end. | | JOB-F5185B999046 | JOB | /api/cron/marketing-social | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | Source discovery only; exact committed hashes and baseline in discovery/weekly-meal-inventory.json. Full workflow verification remains separate. | | LIBRARY-D8E1C781AB00 | LIBRARY | lib/marketing/recurring-ads-runner.ts | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | Source discovery only; exact committed hashes and baseline in discovery/weekly-meal-inventory.json. Full workflow verification remains separate. | | SERVICE-4936014BD04B | SERVICE | validPlatforms | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | Source discovery only; exact committed hashes and baseline in discovery/weekly-meal-inventory.json. Full workflow verification remains separate. | @@ -16263,8 +16312,8 @@ not read the same on this page.* | AUTHZ-004 | AUTHZ | Guardian replay must verify stored decision authorship | 🔄 IN PROGRESS | High | Two signed-route bypasses reproduced before repair. Hosted CI 34715644176 on the exact tree of publication 0b190659 passes all 1,061 scheduled browser cases, including three PostgreSQL and HTTP Guardian authority cases. See guardian-hosted-acceptance.md. | Service-owned exact-input/communication/decision receipts authorize replay. Missing proof triggers fresh screening; neutral input survives ledger-read failure. Full conditional decision updates and verified readback precede notification. | Publication fae35e90: CI 34717274616, tested checkout 62d8a325 with tree bb838fce equal to publication; 14,636 units, 252-page build, all 1,062 scheduled browser cases PASS. Database: 300 migrations, zero failures, eleven probes; finance: 66 assertions, mobile and Vercel PASS. See guardian-hosted-acceptance.md. | Four Guardian hosted cases pass including the completion/recovery extension. No production provider, policy configuration or scheduler claim. | | SUPPORT-91A56D6A297E | SUPPORT | tests/guardian-policy-callers.test.ts | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | Source discovery only; exact committed hashes and baseline in discovery/guardian-policy-inventory.json. Full workflow verification remains separate. | | SUPPORT-370A0FE646C3 | SUPPORT | tests/guardian-policy-execution.test.ts | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | Source discovery only; exact committed hashes and baseline in discovery/guardian-policy-inventory.json. Full workflow verification remains separate. | -| API-C8B72ACE022A | API | POST /api/assistant | 🔄 IN PROGRESS | High | Exact POST-only middleware exemption verified in source and against production; tests/middleware-public-api-boundary.test.ts pins the class. | Named the two exact paths in the middleware and gated them to POST; the namespace was not opened. | unauthenticated production probes against www.bubaly.com on 2026-09-13 (build f9c4d7a1): POST answers 401 from the handler; GET on the same path answers 307 to /login, and both /api/assistant/link and /api/assistant/other answer 307 on GET and POST — the exemption did not widen to neighbours. | Resolved. Full assistant feature workflow remains separate. Historical scoped evidence retained; current merged-source verification and remaining workflow obligations are pending. | -| API-A2C5302CAE88 | API | POST /api/assistant/alexa | 🔄 IN PROGRESS | High | Exact POST-only middleware exemption verified in source and against production; tests/middleware-public-api-boundary.test.ts pins the class. | Named the two exact paths in the middleware and gated them to POST; the namespace was not opened. | unauthenticated production probes against www.bubaly.com on 2026-09-13 (build f9c4d7a1): POST answers 403 from Amazon signature verification; GET on the same path answers 307 to /login, and both /api/assistant/link and /api/assistant/other answer 307 on GET and POST — the exemption did not widen to neighbours. | Resolved. Full assistant feature workflow remains separate. Historical scoped evidence retained; current merged-source verification and remaining workflow obligations are pending. | +| API-C8B72ACE022A | API | POST /api/assistant | 🔄 IN PROGRESS | High | Exact POST-only middleware exemption verified in source and against production; tests/middleware-public-api-boundary.test.ts pins the class. | Named the two exact paths in the middleware and gated them to POST; the namespace was not opened. | unauthenticated production probes against www.bubaly.com on 2026-09-13 (build f9c4d7a1): POST answers 401 from the handler; GET on the same path answers 307 to /login, and both /api/assistant/link and /api/assistant/other answer 307 on GET and POST — the exemption did not widen to neighbours. | Resolved. Full assistant feature workflow remains separate. Historical scoped evidence retained; current merged-source verification and remaining workflow obligations are pending. API sweep 2026-09-27: OK [a:401 a!:400 c:401 c!:400 p:401 p!:400 a:401 a!:400]. | +| API-A2C5302CAE88 | API | POST /api/assistant/alexa | 🔄 IN PROGRESS | High | Exact POST-only middleware exemption verified in source and against production; tests/middleware-public-api-boundary.test.ts pins the class. | Named the two exact paths in the middleware and gated them to POST; the namespace was not opened. | unauthenticated production probes against www.bubaly.com on 2026-09-13 (build f9c4d7a1): POST answers 403 from Amazon signature verification; GET on the same path answers 307 to /login, and both /api/assistant/link and /api/assistant/other answer 307 on GET and POST — the exemption did not widen to neighbours. | Resolved. Full assistant feature workflow remains separate. Historical scoped evidence retained; current merged-source verification and remaining workflow obligations are pending. API sweep 2026-09-27: OK [a:403 a!:400 c:403 c!:400 p:403 p!:400 a:403 a!:400]. | | AUTHZ-005 | AUTHZ | Guardian contact trust and member profiles require database manager write authority | 🛠 FIXED + PASS | High | Reproduced against the replayed schema (docs/audit/guardian-manager-write-check.sql): a teen raised a caller's trust, created a trusted contact, deleted a contact, and rewrote a routing profile and a suggestion. | Migration 0345: guardian_contacts, guardian_member_profiles and guardian_suggestions writes need can_manage_family (0215's pattern). guardian_communications keeps its member INSERT: the signed-ingress E2E suites rely on it. | Probe fails before 0345 and passes after (parent controls pass, members still read); 0345 re-applies idempotently; 44/44 probes; 26 Guardian test files (636 tests) and the full unit suite pass. | Also reproduced on this branch as a real authenticated child on a full replay ("a child promoted a blocked caller to immediate_family"; docs/audit/guardian-screening-write-boundary-check.sql, which now asserts main's 0318 and 0345 — this branch's own 0376 was dropped at the merge as 0318's duplicate, Q68; deleteContactAction and updateContactTrustAction read back). Repository-verified 2026-09-26 (Pass C1-K, C1-K-17). Production carries it only after the F-001 ledger repair. Cross-family member_id references remain a separate integrity item. | | MIGRATION-828B29F5735B | MIGRATION | supabase/migrations/0282_marketing_recurring_ads.sql | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | Source discovery only; exact committed hashes and baseline in discovery/weekly-meal-inventory.json. Full workflow verification remains separate. | | SUPPORT-EBD58C8E93F0 | SUPPORT | app/(app)/dashboard/assistants/actions.ts | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | Source discovery only; exact committed hashes and baseline in discovery/guardian-replay-inventory.json. Full workflow verification remains separate. | @@ -16401,7 +16450,7 @@ not read the same on this page.* | SUPPORT-92D4AF759E3D | SUPPORT | tests/e2e/kid-login-readiness.spec.ts | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | Source discovery only; exact committed hashes and baseline in discovery/child-readiness-inventory.json. Full workflow verification remains separate. | | SMS-002 | SMS | Guardian SMS recovery after provider retries stop | 🔄 IN PROGRESS | High | Installed SDK recovery/fairness regressions PASS. Hosted real PostgreSQL/HTTP now verifies one signed failed intake followed by cron-only recovery, child receipt denial, untouched unreceipted content, idempotence and a clean continuation sweep. | Shared signed-ingress and service-receipt processing; independent terminal verification; stable notification identity with quiet hours; bounded scheduled recovery with a finite sweep endpoint. Temporary destination removal preserves retained work. Cursor revisions prevent lost progress under competing workers. | Publication fae35e90: CI 34717274616, tested checkout 62d8a325 with tree bb838fce equal to publication; 14,636 units, 252-page build, all 1,062 scheduled browser cases PASS. Database: 300 migrations, zero failures, eleven probes; finance: 66 assertions, mobile and Vercel PASS. See guardian-hosted-acceptance.md. | See guardian-sms-recovery-cycle.md. Production scheduler and provider delivery remain unverified. No SQL, dependency or shared navigation changes. | | SUPPORT-0E0FDE18B6EB | SUPPORT | app/api/cron/guardian-sms-recovery/route.ts | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | Source discovery only; exact committed hashes and baseline in discovery/guardian-sms-recovery-inventory.json. Full workflow verification remains separate. | -| API-C9192C0F02D9 | API | GET /api/cron/guardian-sms-recovery | 🔄 IN PROGRESS | High | Authorization, finite cursor fairness, retry failures and idempotence tested; actual cron-only recovery and clean sweep pass in hosted PostgreSQL/HTTP. | Bounded service-receipt consumer with revisioned finite-sweep cursor; no authority inferred from member communications. | Publication fae35e90: CI 34717274616, tested checkout 62d8a325 with tree bb838fce equal to publication; 14,636 units, 252-page build, all 1,062 scheduled browser cases PASS. Database: 300 migrations, zero failures, eleven probes; finance: 66 assertions, mobile and Vercel PASS. See guardian-hosted-acceptance.md. | Production scheduler and cron-secret deployment remain unverified. | +| API-C9192C0F02D9 | API | GET /api/cron/guardian-sms-recovery | 🔄 IN PROGRESS | High | Authorization, finite cursor fairness, retry failures and idempotence tested; actual cron-only recovery and clean sweep pass in hosted PostgreSQL/HTTP. | Bounded service-receipt consumer with revisioned finite-sweep cursor; no authority inferred from member communications. | Publication fae35e90: CI 34717274616, tested checkout 62d8a325 with tree bb838fce equal to publication; 14,636 units, 252-page build, all 1,062 scheduled browser cases PASS. Database: 300 migrations, zero failures, eleven probes; finance: 66 assertions, mobile and Vercel PASS. See guardian-hosted-acceptance.md. | Production scheduler and cron-secret deployment remain unverified. API sweep 2026-09-27: OK [a:401 c:401 p:401 a:401]. | | JOB-7C2C80C9F161 | JOB | /api/cron/guardian-sms-recovery | 🔄 IN PROGRESS | High | Authorization, finite cursor fairness, retry failures and idempotence tested; actual cron-only recovery and clean sweep pass in hosted PostgreSQL/HTTP. | Bounded service-receipt consumer with revisioned finite-sweep cursor; no authority inferred from member communications. | Publication fae35e90: CI 34717274616, tested checkout 62d8a325 with tree bb838fce equal to publication; 14,636 units, 252-page build, all 1,062 scheduled browser cases PASS. Database: 300 migrations, zero failures, eleven probes; finance: 66 assertions, mobile and Vercel PASS. See guardian-hosted-acceptance.md. | Production scheduler and cron-secret deployment remain unverified. | | LIBRARY-085E4BBF3CE0 | LIBRARY | lib/guardian/sms-deadline.ts | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | Source discovery only; exact committed hashes and baseline in discovery/guardian-sms-recovery-inventory.json. Full workflow verification remains separate. | | SERVICE-BC9ED5463E65 | SERVICE | smsStep | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | Source discovery only; exact committed hashes and baseline in discovery/guardian-sms-recovery-inventory.json. Full workflow verification remains separate. | @@ -16442,7 +16491,7 @@ not read the same on this page.* | SUPPORT-2C87AC076641 | SUPPORT | tests/contact-center-sms-reply.test.ts | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | Source discovery only; exact committed hashes and baseline in discovery/contact-center-sms-reservation-inventory.json. Full workflow verification remains separate. | | SUPPORT-ABE163EE3BE6 | SUPPORT | tests/e2e/contact-center-sms-reservation.spec.ts | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | Source discovery only; exact committed hashes and baseline in discovery/contact-center-sms-reservation-inventory.json. Full workflow verification remains separate. | | SUPPORT-7C1612755C6E | SUPPORT | app/api/contact-center/sms/status/route.ts | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | Source discovery only; exact committed hashes and baseline in discovery/contact-center-sms-status-inventory.json. Full workflow verification remains separate. | -| API-14AB8722BB20 | API | POST /api/contact-center/sms/status | 🔄 IN PROGRESS | High | 85 installed-SDK delivery cases and 53 actual signed route cases PASS; 41 family-authorized reader and 14 React/page cases PASS. Baseline reservation 70 and updated signed ingress 55 cases remain passing. | Signed exact URL/query/form validation, saved emission account and token binding, immutable first provider SID, verified inbox effects, bounded revision CAS, monotonic terminal status and safe family-authorized display. No emission phase is reopened. | Signed delivery publication 0d68bdbb passes CI 34718960420: 1,064 scheduled E2E cases and 1,064 passes in 8.0 minutes, with both authenticated/durable flags enabled; identical tested tree becff049. Web, database, mobile, finance and Vercel PASS. New text source a61804db requires its own hosted execution. See guardian-hosted-acceptance.md. Application a61804db passes all 15,011 unit tests in 1,202 files, zero failed/skipped, in 43.60 seconds. The exact frozen source was committed without changes during execution. Application a61804db builds 252 pages (44-second compilation). Strict types, lint with four baseline warnings, localization and query audit (491 tables, 77 functions, 144 routes) PASS. See contact-center-text-bounds-cycle.md. | Private checks and synthetic hosted callbacks are separate from real provider delivery. No SQL, dependency or shared navigation changes; exact source inventory preserved. | +| API-14AB8722BB20 | API | POST /api/contact-center/sms/status | 🔄 IN PROGRESS | High | 85 installed-SDK delivery cases and 53 actual signed route cases PASS; 41 family-authorized reader and 14 React/page cases PASS. Baseline reservation 70 and updated signed ingress 55 cases remain passing. | Signed exact URL/query/form validation, saved emission account and token binding, immutable first provider SID, verified inbox effects, bounded revision CAS, monotonic terminal status and safe family-authorized display. No emission phase is reopened. | Signed delivery publication 0d68bdbb passes CI 34718960420: 1,064 scheduled E2E cases and 1,064 passes in 8.0 minutes, with both authenticated/durable flags enabled; identical tested tree becff049. Web, database, mobile, finance and Vercel PASS. New text source a61804db requires its own hosted execution. See guardian-hosted-acceptance.md. Application a61804db passes all 15,011 unit tests in 1,202 files, zero failed/skipped, in 43.60 seconds. The exact frozen source was committed without changes during execution. Application a61804db builds 252 pages (44-second compilation). Strict types, lint with four baseline warnings, localization and query audit (491 tables, 77 functions, 144 routes) PASS. See contact-center-text-bounds-cycle.md. | Private checks and synthetic hosted callbacks are separate from real provider delivery. No SQL, dependency or shared navigation changes; exact source inventory preserved. API sweep 2026-09-27: OK [a:400 a!:400 c:400 c!:400 p:400 p!:400 a:400 a!:400]. | | SERVICE-7CAEBB72FF3F | SERVICE | recordSmsReplyDelivery | 🔄 IN PROGRESS | High | 85 installed-SDK delivery cases and 53 actual signed route cases PASS; 41 family-authorized reader and 14 React/page cases PASS. Baseline reservation 70 and updated signed ingress 55 cases remain passing. | Signed exact URL/query/form validation, saved emission account and token binding, immutable first provider SID, verified inbox effects, bounded revision CAS, monotonic terminal status and safe family-authorized display. No emission phase is reopened. | Signed delivery publication 0d68bdbb passes CI 34718960420: 1,064 scheduled E2E cases and 1,064 passes in 8.0 minutes, with both authenticated/durable flags enabled; identical tested tree becff049. Web, database, mobile, finance and Vercel PASS. New text source a61804db requires its own hosted execution. See guardian-hosted-acceptance.md. Application a61804db passes all 15,011 unit tests in 1,202 files, zero failed/skipped, in 43.60 seconds. The exact frozen source was committed without changes during execution. Application a61804db builds 252 pages (44-second compilation). Strict types, lint with four baseline warnings, localization and query audit (491 tables, 77 functions, 144 routes) PASS. See contact-center-text-bounds-cycle.md. | Private checks and synthetic hosted callbacks are separate from real provider delivery. No SQL, dependency or shared navigation changes; exact source inventory preserved. | | SUPPORT-A8866A1C75F0 | SUPPORT | tests/contact-center-sms-delivery.test.ts | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | Source discovery only; exact committed hashes and baseline in discovery/contact-center-sms-status-inventory.json. Full workflow verification remains separate. | | SUPPORT-805EBE44E61C | SUPPORT | tests/contact-center-sms-status.test.ts | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | Source discovery only; exact committed hashes and baseline in discovery/contact-center-sms-status-inventory.json. Full workflow verification remains separate. | @@ -16779,7 +16828,9 @@ not read the same on this page.* | SUPPORT-4CE6A54076E4 | SUPPORT | tests/e2e/family-media-session-ownership.spec.ts | 🛠 FIXED + PASS | Critical | Seven actual React/media-hook/cache-session/browser-adapter/installed-SDK cases: four two-tab ownership cases and three FamilyMediaImg Node SSR/Chromium hydration controls; only synthetic auth/Storage/image transport. | Retains observer-gap, active-switch and both held A/B signing settlement orders for existing SEC-001/Q59. | 7/7PASS in2.6s, fixturee169fe97; valid-cookie hydration warning reproduced then fixed, absent/external and immediate cached SPA controls pass; strict fixture types/lint pass and collector registration guard37/37PASS. Full integrated gates and media deployment pending. | Added2026-09-27 by canonical-path SHA-256 rule; no server, provider operation or private production image. Test coverage is not another product finding. Current2026-09-27: final hydration-aware98d5229 localUTC20722/1623files,146focused,build247/types/lint15PASS; retained browser7/7 and display25/25PASS. Source merged57664cfc, new hosted/deployment pending; row remains IN PROGRESS. Final receipt linux-q59-final/final-gates-receipt.json. Exact main576 public/native deployed acceptancePASS16:50:52-59UTC; full native wrapper1/1PASS2.2s, unchanged507 assertions plus revision/hash guards. No authenticated private-provider image proof; PR595 hosted stillpending. Closure2026-09-27: PR599 CI36335077710 all4SUCCESS; tested564cc64f and reviewed e3/published e70 share treec6f3cd6b. Clean1775/1775 E2E, Web20735 in each3zones/1623files, build247/types/lintPASS. Current media browser11/11 and exact e70/a6 public/native deployment checks pass. FIXED+PASS applies to retained regression coverage only; broader SEC-001 remainsFAIL and no live private-provider image workflow is claimed. Earlier pending statements are historical. | | SUPPORT-2D36C3920F6A | SUPPORT | tests/a-catalogue-says-each-key-once.test.ts | 🔄 IN PROGRESS | Medium | Source reviewed: every catalogue, duplicate-key detection and retained locator failure wording; incoming PR596 regression guard. | Canonical-path SHA-256 inventory registration; no additional product finding. | Incoming combined verification pending; no independent PASS claimed. | Added2026-09-27 while integrating PR596/597 after main e70. Existing permanent IDs/statuses and both audit registers preserved. | -| API-AE3E5F465E18 | API | GET /api/admin/support-tickets/export | 🔄 IN PROGRESS | Medium | New source/coverage inventory; combined integration is not yet published or fully accepted. | Integrate reviewed585/598 fixes with matching regression coverage. | Combined focused unit230/230 and browser44/44 pass; these are suite scopes, not a claim that every new source has a full workflow test. Canonical full gates and hosted acceptance pending. | Added2026-09-27 by canonical path hash; API hashes route:method. No extra product-workflow completion. | +| API-AE3E5F465E18 | API | GET /api/admin/support-tickets/export | 🔄 IN PROGRESS | Medium | New source/coverage inventory; combined integration is not yet published or fully accepted. | Integrate reviewed585/598 fixes with matching regression coverage. | Combined focused unit230/230 and browser44/44 pass; these are suite scopes, not a claim that every new source has a full workflow test. Canonical full gates and hosted acceptance pending. | Added2026-09-27 by canonical path hash; API hashes route:method. No extra product-workflow completion. API sweep 2026-09-27: OK [a:307 c:403 p:403 a:200]. | +| API-9158428527C6 | API | GET /api/blog/search-index | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin [a:200 c:200 p:200 a:200] — REVIEW: reviewed: public read-only by design | None needed at this boundary | Pending (workflow) | Added 2026-09-27: a route on disk with no inventory row, found by the sweep’s own route discovery. Remaining: a valid request, its side effects and response shape, end to end. | +| API-9DB7C4EE8804 | API | GET /api/cron/library-feeds | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin [a:401 c:401 p:401 a:401] — OK: no 5xx, signed-out refused, cron secret required | None needed at this boundary | Pending (workflow) | Added 2026-09-27: a route on disk with no inventory row, found by the sweep’s own route discovery. Remaining: a valid request, its side effects and response shape, end to end. | | COMPONENT-409DF839B3FA | COMPONENT | components/admin/invite-admin-button.tsx | 🔄 IN PROGRESS | Medium | New source/coverage inventory; combined integration is not yet published or fully accepted. | Integrate reviewed585/598 fixes with matching regression coverage. | Combined focused unit230/230 and browser44/44 pass; these are suite scopes, not a claim that every new source has a full workflow test. Canonical full gates and hosted acceptance pending. | Added2026-09-27 by canonical path hash; API hashes route:method. No extra product-workflow completion. | | COMPONENT-28394844D064 | COMPONENT | components/admin/new-ticket-button.tsx | 🔄 IN PROGRESS | Medium | New source/coverage inventory; combined integration is not yet published or fully accepted. | Integrate reviewed585/598 fixes with matching regression coverage. | Combined focused unit230/230 and browser44/44 pass; these are suite scopes, not a claim that every new source has a full workflow test. Canonical full gates and hosted acceptance pending. | Added2026-09-27 by canonical path hash; API hashes route:method. No extra product-workflow completion. | | LIBRARY-03B2D9086BBF | LIBRARY | lib/admin/tickets-csv.ts | 🔄 IN PROGRESS | Medium | New source/coverage inventory; combined integration is not yet published or fully accepted. | Integrate reviewed585/598 fixes with matching regression coverage. | Combined focused unit230/230 and browser44/44 pass; these are suite scopes, not a claim that every new source has a full workflow test. Canonical full gates and hosted acceptance pending. | Added2026-09-27 by canonical path hash; API hashes route:method. No extra product-workflow completion. | From 951dee9ae194d9e3c351e92980c3c70fe1559ac6 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 27 Sep 2026 22:39:52 +0000 Subject: [PATCH 3/4] An email that was not sent is not reported as sent Running every scheduled job on a local build, /api/cron/weekly-digest reported {"sent":4} with no mail provider. sendEmail/sendReactEmail answer { ok: true, skipped: true } without RESEND_API_KEY, a contract the contact form relies on, so each caller that read only `ok` turned "nothing sent" into "sent": the invite, welcome, referral and admin re-send flows told the person their email had gone, and the weekly digest and chore reminders counted skips as sends. Each caller now reads `skipped`: the four person-facing flows answer their existing "couldn't send" message (the referral rolls back its record), and the crons count a skip as a skip. A guard holds every caller that reads the result to read `skipped` too; a behavioural test drives the invite route through the real lib/email. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01776xJyhVe8xJQvTGeNfT9T --- app/(app)/admin/actions.ts | 5 +- app/(app)/referrals/actions.ts | 5 +- app/api/cron/chore-reminders/route.ts | 6 +- app/api/cron/weekly-digest/route.ts | 6 +- app/api/email/invite/route.ts | 4 +- app/api/email/welcome/route.ts | 4 +- lib/blog/subscribe-notice.ts | 1 + lib/server/notification-emails.ts | 8 ++- ...-was-not-sent-is-not-reported-sent.test.ts | 64 +++++++++++++++++++ ...-with-no-mail-provider-is-not-sent.test.ts | 41 ++++++++++++ 10 files changed, 132 insertions(+), 12 deletions(-) create mode 100644 tests/an-email-that-was-not-sent-is-not-reported-sent.test.ts create mode 100644 tests/an-invite-with-no-mail-provider-is-not-sent.test.ts diff --git a/app/(app)/admin/actions.ts b/app/(app)/admin/actions.ts index 52662a844..ab3373071 100644 --- a/app/(app)/admin/actions.ts +++ b/app/(app)/admin/actions.ts @@ -444,7 +444,7 @@ export async function adminResendInviteAction(inviteId: string): Promise ? (await supabase.from('profiles').select('full_name, email').eq('id', invite.invited_by).maybeSingle()).data : null; - const { ok } = await sendReactEmail({ + const { ok, skipped } = await sendReactEmail({ to: invite.email, subject: `Reminder: you’re invited to join ${family?.name ?? 'a family'} on Bubaly`, react: React.createElement(InviteEmail, { @@ -454,7 +454,8 @@ export async function adminResendInviteAction(inviteId: string): Promise role: invite.role, }), }); - if (!ok) return { ok: false, error: t('actions.couldNotSendTheInvite') }; + // `skipped` is no mail provider: nothing went out, so it is not a resend. + if (!ok || skipped) return { ok: false, error: t('actions.couldNotSendTheInvite') }; await adminAuditLog({ familyId: invite.family_id, action: 'resend', resource: 'invites', resourceId: inviteId, metadata: { email: invite.email } }); revalidatePath('/admin/users'); diff --git a/app/(app)/referrals/actions.ts b/app/(app)/referrals/actions.ts index f86addb5d..3592c3157 100644 --- a/app/(app)/referrals/actions.ts +++ b/app/(app)/referrals/actions.ts @@ -79,7 +79,7 @@ export async function sendReferralEmailAction(rawEmail: string): Promise { const email = emailByUserId.get(userId); if (!email) { skipped++; return; } - const { ok } = await sendReactEmail({ + const { ok, skipped: notSent } = await sendReactEmail({ to: email, subject: `${chores.length} chore${chores.length !== 1 ? 's' : ''} coming up this week`, react: React.createElement(ChoreReminderEmail, { memberName, familyName, chores }), }); - if (ok) sent++; + // No provider: sendReactEmail answers ok with `skipped`, and nothing was sent. + if (notSent) skipped++; + else if (ok) sent++; else failed++; }; diff --git a/app/api/cron/weekly-digest/route.ts b/app/api/cron/weekly-digest/route.ts index 5b307534f..24229c1eb 100644 --- a/app/api/cron/weekly-digest/route.ts +++ b/app/api/cron/weekly-digest/route.ts @@ -142,7 +142,7 @@ export async function GET(req: NextRequest) { const adminEmail = emailByUserId.get(adminMember.user_id); if (!adminEmail) { skipped++; return; } - const { ok } = await sendReactEmail({ + const { ok, skipped: notSent } = await sendReactEmail({ to: adminEmail, subject: `${family.name} — your week ahead`, react: React.createElement(WeeklyDigestEmail, { @@ -157,7 +157,9 @@ export async function GET(req: NextRequest) { compareLine: renderCompareLine(await loadCompareLine(supabase, family.id), t), }), }); - if (ok) sent++; + // No provider: sendReactEmail answers ok with `skipped`, and nothing was sent. + if (notSent) skipped++; + else if (ok) sent++; else failed++; }; diff --git a/app/api/email/invite/route.ts b/app/api/email/invite/route.ts index 49f5ffdc1..e2a18c0ca 100644 --- a/app/api/email/invite/route.ts +++ b/app/api/email/invite/route.ts @@ -65,7 +65,7 @@ export async function POST(req: NextRequest) { const inviterName = ctx.active.member.display_name; const familyName = ctx.active.family.name; - const { ok } = await sendReactEmail({ + const { ok, skipped } = await sendReactEmail({ to: invite.email, subject: `${inviterName} invited you to join ${familyName} on Bubaly`, react: React.createElement(InviteEmail, { @@ -76,6 +76,8 @@ export async function POST(req: NextRequest) { }), }); if (!ok) return NextResponse.json({ error: t('invite.failedToSendInvite') }, { status: 502 }); + // No mail provider: the invite was not sent, so the inviter must not be told it was. + if (skipped) return NextResponse.json({ error: t('invite.failedToSendInvite') }, { status: 503 }); return NextResponse.json({ sent: true }); } catch (err) { diff --git a/app/api/email/welcome/route.ts b/app/api/email/welcome/route.ts index 0a84a0fb7..9ebc0a8b9 100644 --- a/app/api/email/welcome/route.ts +++ b/app/api/email/welcome/route.ts @@ -24,12 +24,14 @@ export async function POST(req: NextRequest) { if (!email || !name) return NextResponse.json({ error: t('welcome.missingParams') }, { status: 400 }); if (email.length > 320 || name.length > 120) return NextResponse.json({ error: t('welcome.invalidParams') }, { status: 400 }); - const { ok } = await sendReactEmail({ + const { ok, skipped } = await sendReactEmail({ to: email, subject: 'Welcome to Bubaly 🎉', react: React.createElement(WelcomeEmail, { name }), }); if (!ok) return NextResponse.json({ error: t('welcome.failedToSendWelcomeEmail') }, { status: 502 }); + // No mail provider: nothing was sent, so do not answer `sent: true`. + if (skipped) return NextResponse.json({ error: t('welcome.failedToSendWelcomeEmail') }, { status: 503 }); return NextResponse.json({ sent: true }); } catch (err) { diff --git a/lib/blog/subscribe-notice.ts b/lib/blog/subscribe-notice.ts index b56afdf81..15634a5d9 100644 --- a/lib/blog/subscribe-notice.ts +++ b/lib/blog/subscribe-notice.ts @@ -84,6 +84,7 @@ export async function sendSubscribeNotice( html: subscribeNoticeHtml(outcome, token), }); if (!sent.ok) console.error('[blog-subscribe] notice email was not delivered', { outcome }); + else if (sent.skipped) console.warn('[blog-subscribe] notice email not sent: no mail provider', { outcome }); } catch (error) { console.error('[blog-subscribe] notice email threw', { outcome, error }); } diff --git a/lib/server/notification-emails.ts b/lib/server/notification-emails.ts index e2251d217..33a07cffc 100644 --- a/lib/server/notification-emails.ts +++ b/lib/server/notification-emails.ts @@ -114,12 +114,16 @@ export async function deliverNotificationEmails(supabase: DB): Promise ({ title: n.title, body: n.body, icon: iconForType(n.type) })); - const { ok } = await sendReactEmail({ + const { ok, skipped: notSent } = await sendReactEmail({ to: meta.email, subject: `${notifs.length} family update${notifs.length > 1 ? 's' : ''} · Bubaly`, react: React.createElement(NotificationDigestEmail, { name: meta.name, items }), }); - if (ok) { + if (notSent) { + // No mail provider: nothing was emailed. Leave sent_at null so these go + // out once one is configured, instead of stamping them sent unseen. + skipped += 1; + } else if (ok) { sent++; resolvedIds.push(...ids); } else { diff --git a/tests/an-email-that-was-not-sent-is-not-reported-sent.test.ts b/tests/an-email-that-was-not-sent-is-not-reported-sent.test.ts new file mode 100644 index 000000000..e09c55853 --- /dev/null +++ b/tests/an-email-that-was-not-sent-is-not-reported-sent.test.ts @@ -0,0 +1,64 @@ +import { readFileSync, readdirSync, statSync } from 'node:fs'; +import path from 'node:path'; +import { describe, expect, it } from 'vitest'; + +// Found while running every scheduled job on a local build (the API audit's +// cron pass): /api/cron/weekly-digest reported {"sent":4} on a server with no +// email provider. sendEmail/sendReactEmail answer { ok: true, skipped: true } +// when RESEND_API_KEY is unset — a contract other callers rely on (the contact +// form files a ticket either way; see tests/email-send-contract.test.ts) — so +// a caller that reads only `ok` turns "nothing was sent" into "sent": +// +// - the invite, welcome, referral and admin re-send flows told the person +// their email had gone; +// - (the notification digest was already safe: it returns before sending +// when emailEnabled() is false; it now also refuses to stamp a skip); +// - the weekly digest and chore reminders counted every skip as a send. +// +// Every caller that reads the result must read `skipped` too. Calls whose +// result is deliberately ignored (best-effort mail during onboarding, the +// feedback note) are not reading `ok` at all and are not held to this. + +function files(dir: string): string[] { + return readdirSync(dir).flatMap((name) => { + const full = path.join(dir, name); + if (statSync(full).isDirectory()) return name === 'node_modules' ? [] : files(full); + return /\.tsx?$/.test(name) ? [full.split(path.sep).join('/')] : []; + }); +} + +const SENDERS = /\b(sendEmail|sendReactEmail)\(/; +const callers = [...files('app'), ...files('lib')] + .filter((f) => !['lib/email.ts', 'lib/server/email.ts'].includes(f)) + .filter((f) => SENDERS.test(readFileSync(f, 'utf8'))); + +/** Each call's result binding: `const { ok } = await send…(` or `const x = await send…(`. */ +function readsOkWithoutSkipped(src: string): string[] { + const out: string[] = []; + for (const m of src.matchAll(/const\s+(\{[^}]*\}|[A-Za-z_]\w*)\s*=\s*await\s+(?:sendEmail|sendReactEmail)\(/g)) { + const binding = m[1]; + const after = src.slice(m.index!, m.index! + 2500); + const readsSkipped = binding.startsWith('{') + ? /\bskipped\b/.test(binding) + : new RegExp(`\\b${binding}\\.skipped\\b`).test(after); + if (!readsSkipped) out.push(binding); + } + return out; +} + +describe('an email that was not sent is not reported as sent', () => { + it('finds the callers', () => { + expect(callers.length).toBeGreaterThanOrEqual(10); + }); + + it.each(callers)('%s reads `skipped` wherever it reads the result', (file) => { + expect(readsOkWithoutSkipped(readFileSync(file, 'utf8')), file).toEqual([]); + }); + + it('can tell the two apart', () => { + expect(readsOkWithoutSkipped('const { ok } = await sendReactEmail({});\nif (ok) sent++;')).toEqual(['{ ok }']); + expect(readsOkWithoutSkipped('const { ok, skipped } = await sendReactEmail({});')).toEqual([]); + expect(readsOkWithoutSkipped('const r = await sendEmail({});\nif (r.skipped) x();')).toEqual([]); + expect(readsOkWithoutSkipped('const r = await sendEmail({});\nif (!r.ok) x();')).toEqual(['r']); + }); +}); diff --git a/tests/an-invite-with-no-mail-provider-is-not-sent.test.ts b/tests/an-invite-with-no-mail-provider-is-not-sent.test.ts new file mode 100644 index 000000000..8f55882d7 --- /dev/null +++ b/tests/an-invite-with-no-mail-provider-is-not-sent.test.ts @@ -0,0 +1,41 @@ +import { NextRequest } from 'next/server'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +// The real lib/email, with no RESEND_API_KEY: sendReactEmail answers +// { ok: true, skipped: true } and sends nothing. The invite route read only +// `ok` and answered { sent: true } — the inviter was told their invite had +// gone to someone who never received it. See +// tests/an-email-that-was-not-sent-is-not-reported-sent.test.ts for the class. + +vi.mock('@/lib/i18n/server', () => ({ getTranslations: async () => (key: string) => key })); +vi.mock('@/lib/supabase/auth', () => ({ + requireUserContext: async () => ({ + user: { id: 'u' }, + active: { familyId: 'f', member: { display_name: 'Casey' }, family: { name: 'Rivera family' } }, + }), +})); +vi.mock('@/lib/supabase/server', () => ({ + createServer: async () => ({ from: () => { + const b = { select: () => b, eq: () => b, + maybeSingle: async () => ({ data: { id: 'i', email: 'grandma@example.test', token: 't', role: 'adult', status: 'pending' }, error: null }) }; + return b; + } }), +})); +vi.mock('@/lib/server/request-rate-limit', () => ({ enforceRequestRateLimit: async () => ({ ok: true }) })); + +const saved = process.env.RESEND_API_KEY; +beforeEach(() => { delete process.env.RESEND_API_KEY; }); +afterEach(() => { if (saved === undefined) delete process.env.RESEND_API_KEY; else process.env.RESEND_API_KEY = saved; }); + +describe('an invite on a server with no mail provider', () => { + it('is not reported as sent', async () => { + const { POST } = await import('@/app/api/email/invite/route'); + const res = await POST(new NextRequest('https://www.bubaly.com/api/email/invite', { + method: 'POST', body: JSON.stringify({ inviteId: 'i' }), headers: { 'content-type': 'application/json' }, + })); + const body = await res.json(); + expect(body).not.toEqual({ sent: true }); + expect(res.status).toBe(503); + expect(body).toEqual({ error: 'invite.failedToSendInvite' }); + }); +}); From 2851d350621a6d323a337f1f949bb28912131c0e Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 27 Sep 2026 22:39:52 +0000 Subject: [PATCH 4/4] finalaudit: record the cron pass and API-SWEEP-07 Every job in vercel.json run on a local build with and without its secret: 27/27 refuse without it (401) and run with it (200, zero failures). 25 JOB rows move to IN PROGRESS, one is appended, and the unregistered /api/cron/library-feeds is added. Register B: 14,229 items, 13,663 NOT STARTED, 551 IN PROGRESS. PRODUCTION READY: NO. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01776xJyhVe8xJQvTGeNfT9T --- finalaudit.md | 74 ++++++++++++++++++++++++++++----------------------- 1 file changed, 41 insertions(+), 33 deletions(-) diff --git a/finalaudit.md b/finalaudit.md index dbb8a9cb1..aa476aff1 100644 --- a/finalaudit.md +++ b/finalaudit.md @@ -28,15 +28,22 @@ | API-SWEEP-03 | **High** | `POST /api/webhooks/stripe` | Read `STRIPE_WEBHOOK_SECRET` alone and built a client with `getStripe()` just to check the signature. On a deployment whose key and secret live in Stripe Setup, that threw, and the catch answered "signature invalid" to every real event, so no subscription would ever be recorded. | The Setup secret first, then env, verified with Stripe's keyless static verifier (`constructWebhookEvent` in `lib/stripe.ts`). A wrong secret is still refused. | Same file, with real signatures from Stripe's own test-header generator | | API-SWEEP-04 | Medium | 14 AI routes; `ai/health/coach` | With no AI key, 18 AI routes said "the engine isn't set up" (503). Nine let the provider's throw reach a generic catch (500). Four answered 502 "temporarily unavailable", which tells the family to retry when retrying cannot work. `ai/health/coach` checked only the environment, so a key saved in Admin → AI Engine was reported missing. | Each asks `isAIConfigured()` before calling the provider. Three routes keep their deliberate model-free fallback, named in the guard: `journal`, `savings` and `behavior/insight`. | `tests/an-ai-route-says-when-the-engine-is-not-set-up.test.ts` (15 of 31 red before) | | API-SWEEP-05 | **High** (privacy) | `POST /api/ai/savings` | Answered a signed-in child with advice built from the family's transactions, budgets, unpaid bills and subscriptions. The finance service refuses non-managers ("Family finances are private to the adults in this family"), and the privacy export withholds `finances` from a child. | 403 for non-managers before any read. | `tests/a-child-is-not-given-the-familys-savings-advice.test.ts` (3 of 5 red before) | +| API-SWEEP-07 | Medium | invite, welcome, referral and admin re-send; `cron/weekly-digest`, `cron/chore-reminders` | Found by the cron pass (below): `weekly-digest` reported `{"sent":4}` on a server with no mail provider. `sendEmail`/`sendReactEmail` answer `{ ok: true, skipped: true }` without `RESEND_API_KEY`, a contract the contact form relies on. So every caller that read only `ok` called nothing sent "sent". The invite, welcome, referral and admin re-send flows told the person their email had gone. The two crons counted every skip as a send. (The notification digest was already safe: it stops before sending when email is off.) | Each caller reads `skipped`. The four person-facing flows answer their existing "couldn't send" message (the referral also rolls back its record). The crons count a skip as a skip. | `tests/an-email-that-was-not-sent-is-not-reported-sent.test.ts` (8 callers red before); `tests/an-invite-with-no-mail-provider-is-not-sent.test.ts` (the real `lib/email`, red before) | | API-SWEEP-06 | **High** (privacy) — ⚠️ **OPEN** | database: `transactions`, `budgets`, `bills`, `subscriptions_tracked` | Found while tracing API-SWEEP-05. Probed with a child's own session against the local database: each table's SELECT policy is `is_family_member(family_id)`, so a child reads the family's budgets, bills and tracked subscriptions straight from the REST API. `subscriptions_tracked` also lets any member *write* (`Members manage subscriptions_tracked`, command `*`). Migration 0267 guards only the writes on the first three. The app treats finances as adults-only in three places, and the database does not. | Not fixed here. A manager-only SELECT policy is the obvious fix, but the home page, wallet hub, voting, briefing and agent readers also read these tables. It needs the owner's decision on what a child may see, then a migration, then those readers checked. Production application of any migration is the owner's (PROD-DB-0177). | Proposed probe: `docs/audit/`-style SQL asserting a child JWT reads zero rows from each table | +**Cron pass.** Each of the 27 jobs in `vercel.json` was run on the same build: +- **Without the secret:** every one answered 401. +- **With `Authorization: Bearer $CRON_SECRET`:** every one answered 200 within a second, reporting zero failures, over the local database's nine families. + +`/api/cron/library-feeds` is scheduled but had no JOB row, so it is added. The JOB rows move to 🔄 IN PROGRESS. What remains for them is a run over populated data, provider delivery, and the deployed scheduler, which cannot run in production until the owner sets `CRON_SECRET` (PROD-ENV). + **What each API row now says.** Register B's 142 NOT STARTED API rows move to 🔄 IN PROGRESS, not PASS. Each row records: - the sweep's per-caller statuses; - its verdict; - the fix where there was one; - "Remaining: a valid request, its database side effects and response shape, end to end". -The eleven rows other sessions already had in progress keep their text, with the sweep's result appended. Two routes on disk had no row (`GET /api/blog/search-index`, `GET /api/cron/library-feeds`); they are added as API-9158428527C6 and API-9DB7C4EE8804. Register B: **14,228 items: 13,688 NOT STARTED, 525 IN PROGRESS, 10 FIXED + PASS, 4 BLOCKED, 1 FAIL.** No closure is claimed. +The eleven rows other sessions already had in progress keep their text, with the sweep's result appended. Two routes on disk had no row (`GET /api/blog/search-index`, `GET /api/cron/library-feeds`); they are added as API-9158428527C6 and API-9DB7C4EE8804. Register B after both passes: **14,229 items: 13,663 NOT STARTED, 551 IN PROGRESS, 10 FIXED + PASS, 4 BLOCKED, 1 FAIL.** No closure is claimed. **Re-running it** (local Supabase only; both scripts refuse anything else): @@ -865,19 +872,19 @@ IDs link each route to its row in the Session A register (`PAGE-`) and the Sessi *Different scheme and granularity from the block above; see Register B below.* - Started: 2026-09-12T12:41:52.12Z -- Last Updated: 2026-09-27 22:40Z (API sweep: all 155 route methods × four callers; 142 API rows NOT STARTED → IN PROGRESS, 2 rows added; fixes API-SWEEP-01…05 — see "API sweep — every route under app/api" at the top). Earlier: 82da production verified; b718 framework E2E1FAIL; CSP readiness correction local; fresh602/607 hosted acceptance pending +- Last Updated: 2026-09-27 22:40Z (API sweep: all 155 route methods × four callers, and every scheduled job run with and without its secret; 167 API/JOB rows NOT STARTED → IN PROGRESS, 3 rows added; fixes API-SWEEP-01…05 and 07 — see "API sweep — every route under app/api" at the top). Earlier: 82da production verified; b718 framework E2E1FAIL; CSP readiness correction local; fresh602/607 hosted acceptance pending - Page audit (every page on bubaly.com, shared by all bots): see **"Page audit — every page on bubaly.com"** at the end of this file — batches to claim, findings P-01…P-22, and a status row for all 398 routes. - Released: **#541 merged to `main` at `533554be` on 2026-09-26 18:55Z** (merge commit, 242 commits). `main`'s CI on that head is green in all four jobs — Typecheck · Lint · Test · Build (unit tests on three host zones), Mobile, Database (migration replay, 68 boundary probes, re-apply onto an existing schema) and E2E. Production serves it: `GET https://www.bubaly.com/api/build-info` answered `{"revision":"533554be…"}` at 19:13Z, and `/api/health` answered database, auth and service-role **ok** and `status: degraded` because four feature secrets are unset in the production runtime (see Critical Blockers). **Then #580 merged to `main` at `7e54596d` on 2026-09-26 20:21Z** (the units verified after #541: AUDIT-011's 39 re-controlled probes, SEC-009, m6/m9/m12/m30/m0/m28+m29/m42+m43/m18/m35, migrations 0343 and 0349/0352/0360 unapplied and in the ledger); `main`'s CI on that head failed one E2E case (`phone-auth-http` durable-session close) that passed on the next `main` run untouched, and production answered `{"revision":"7e54596d…"}`. **Then #579 merged at `671c5f6a` on 2026-09-27 00:00Z** (another session's pass C1-K: member-write boundaries 0344–0380, trust fail-safes; recorded by that session in the *Release · #579* section below, with the production-migration blocker at 0177) and #582 at `6ff770da`, its release note. Production answered `{"revision":"6ff770da…"}` at 00:44Z, `/api/health` still `degraded` on the same four missing secrets. **Then #581 merged to `main` at `dcc0b42b` on 2026-09-27 01:41Z** (this branch's second tranche: migrations 0381–0387 unapplied and in the ledger, the remaining SRV-001 medium leads, AUTHZ-011, SEC-008, SEC-009, AUDIT-011's 39 re-controlled probes, money in the reader's locale from billing to the marketplace; merged with #579's 0344–0380 after a renumber). Every check was green on the head `3e0e5fc3` — E2E once re-run after the same phone-login durable-session hang `main` had shown on `7e54596d`, recorded on the PR. When it failed a third time (on #583's `fb033b8e`) it was root-caused rather than re-run: the login form's "Continue with phone" button rendered ENABLED from the server with no handler, and once hydrated its handler refused a not-yet-mounted form, so a tap in that window did nothing — the spec clicked right after `domcontentloaded` and waited 120 s for a phone field that never opened. It is a product defect, not only a test one (a family on a slow phone tapping the primary way in got nothing). The phone button, and the signup form's three entry buttons with the same shape, are now held disabled until the form mounts, as the email fieldset already was; `tests/e2e/login-readiness.spec.ts` pins it (red with the fix reverted: "Expected: disabled, Received: enabled"). Production answered `{"revision":"dcc0b42b…"}` at 01:46Z; `/api/health` database, auth and service-role **ok**, `status: degraded` on the same four unset secrets (PROD-ENV). The owner's "Supabase production migrations" workflow ran on the push and will stop at 0177 as before (PROD-DB-0177). **Then #584 merged at `0306c985` on 2026-09-27 02:20Z** (another session's C1-K-56: a notification for someone else is written by Bubaly, not by a member; `0388` unapplied), and production answered `{"revision":"0306c985…"}` at 02:45Z, as that session recorded. **Then #556 merged at `2eb62151` on 2026-09-27 10:51Z** (the four-worker audit branch: its finding IDs, and migrations `0406`–`0418` less `0412`, `0413` and `0417`, all unapplied and in the ledger). This session closed it out: it merged `main` into the branch, renumbered its migrations out of the range `main` had taken, fixed `releaseNumber`, which still called a deadline wrapper `main` had renamed, and moved two ordering guards onto the shared source-order helper. Every check was green on its head `d99faef0`. Three of the branch's decisions were flagged on the PR for a reviewer and stay the owner's to confirm: `0416` changes who may read a journal entry that is not marked private (its owner, or a manager, per `main`'s 0364 rule, where before any family member could); the fallback phone number now needs a country code; and the behaviour-log policy question (`0417` stays dropped). Production answered `{"revision":"2eb62151…"}` at 11:00Z; `/api/health` database, auth and service-role **ok**, `status: degraded` on the same four unset secrets. `main`'s own CI on `2eb62151` finished with every job green except one E2E case: `phone-auth-http` "held genuine SMS verification cannot replace logout" timed out at 120 s, and on its retry (1,295 passed). It is the pre-hydration phone tap root-caused on #583 (`requestCode` presses "Continue with phone" right after load, and before hydration that tap did nothing). The fix, `1f52f00b`, is on #583 and not yet on `main`, so this red has its fix waiting in the open PR rather than an unknown cause. **This is a deployment, not a readiness declaration**: no migration from `0318` on has been applied to production, and `PRODUCTION READY` stays **NO**. **Then #548 merged to `main` at `338b6b12` on 2026-09-27 ~11:55Z** (another session's PR: member-write boundaries `0426`–`0443`, unapplied; recorded by that session in its own sections). Production answered `{"revision":"338b6b12…"}` at 12:02Z. **Then #588, #589 and #590 merged** (a parallel session's live page audit: 16 blog heroes that 404'd, the review page's seeder text, white on danger at 2.80:1), **and #583 merged to `main` at `06dd3f7e` on 2026-09-27 12:48Z** after every job was green on its head `370fc8a8` (Typecheck · Lint · Test · Build, E2E, Database, Mobile) — which also brings `main` the phone-login fix its E2E had been red on. `main`'s CI on `06dd3f7e` is green. Production answered `{"revision":"06dd3f7e…"}` at 13:02Z; `/api/health` database, auth and service-role ok, `status: degraded` on the unset feature secrets. See *Page Audit* above for the per-page state. **Then #591 merged to `main` at `7563462e769386ad5a3cea53e82337cdfc3d9294` on 2026-09-27 16:01:34Z.** Its source head `e798814125c41465158d762177589df10b1612f2` passed all four jobs in CI run `36329827922` (Web completed 15:54:07Z; E2E completed 15:57:45Z). Those checks cover that incoming source, not the combined #592 worker revision. The incoming page-audit sections and their recorded per-page outcomes are preserved below; exact deployed revision and combined-source gates remain to be verified. - Page-audit release record from PR #593 (merge timestamp reconciled to the GitHub event; its observed production time remains unchanged): **Then #591 merged to `main` at `7563462e` on 2026-09-27 16:01:34Z** (this session's page audit: the three-check ledger and every fix it found — titles, not-found paths, alias redirects, grid overflow, the calendar's realtime channel, relative-time hydration, and the signed-in phone pass: 0 axe violations on all 393 signed-in routes in the third crawl), after every job was green on its head `e7988141`. Production answered `{"revision":"7563462e…"}` at 16:06Z; `/api/health` database, auth and service-role **ok**, `status: degraded` on the unset feature secrets. The signed-out production re-crawl that would move its 358 rows from *fixed, live* to *clean* has not been run: the command was refused by this environment's permission check, and is left for the owner (see *Page Audit*). -- Total Audit Items: 14228 — recounted from the current Register B rows after the API sweep added two routes the inventory had missed (API-9158428527C6, API-9DB7C4EE8804); every prior ID is retained. Ten recorded closures remain source-qualified; the sweep adds none. -- Not Started: 13688 -- In Progress: 525 +- Total Audit Items: 14229 — recounted from the current Register B rows after the API sweep and cron pass added three things the inventory had missed (API-9158428527C6, API-9DB7C4EE8804, and a JOB row for /api/cron/library-feeds); every prior ID is retained. Ten recorded closures remain source-qualified; neither pass adds one. +- Not Started: 13663 +- In Progress: 551 - Passed: 0 - Fixed + Passed: 10 (AUTHZ-002, AUTHZ-003, AUTHZ-005, SEC-005; three worker and three media coverage records). Exact scoped hosted/deployment evidence and all earlier failed checkpoints are retained; no additional product workflows are implied. - Status normalization: MAIN-F-C07, MAIN-F-C09 and MAIN-F-C10 now use IN PROGRESS, matching their pending integrated retests. Their original CLOSED / MITIGATED / MOSTLY MITIGATED verdicts remain verbatim in their Notes and evidence. - Blocked: 4 - Failed: 1 (SEC-001). Repository fixes and replayed-policy checks for AUTHZ-003 and AUTHZ-005 remain recorded as FIXED + PASS with their explicit production-migration limitation. No deployed-policy verification is inferred. -- Overall Completion: **0.07%** (10 recorded closures / 14,228 rows). Four credential/provider rows remain BLOCKED without completion credit. This is audit signoff, not a product-development percentage. +- Overall Completion: **0.07%** (10 recorded closures / 14,229 rows). Four credential/provider rows remain BLOCKED without completion credit. This is audit signoff, not a product-development percentage. > **On these counts, plainly.** *Total Audit Items* is the Codex cycle's > enumeration of the whole target space — every route, table, policy, component @@ -2585,7 +2592,7 @@ not read the same on this page.* hosted-CI/deployed-release evidence this session had no credentials to produce. The two share zero finding IDs. Neither is discarded. --> -# Register B — parallel session inventory (14,228 items) +# Register B — parallel session inventory (14,229 items) - PROD-ENV (owner, measured 2026-09-26 19:13Z): production's `/api/health` reports `CRON_SECRET`, `CHILD_LOGIN_SECRET`, `MARKETING_UNSUB_SECRET` and `GUARDIAN_INTERNAL_SECRET` unset. Per `lib/health/status.ts`, `hasCronAuthorization` is fail-closed, so without `CRON_SECRET` every one of the 24 scheduled jobs in `vercel.json` answers 401 (nightly notifications, wallet allowance, chore reminders, the weekly digest, …); without `CHILD_LOGIN_SECRET` no child in any family can sign in; the other two gate provider ingress and signed links and fail closed. Setting them in the Vercel production environment is the owner's action; the four `ENV-*` rows are BLOCKED on it. Also unset, per the same check: `RESEND_API_KEY`, `INTERNAL_SECRET`, `CONTACT_CENTER_INBOUND_SECRET` were NOT in the missing list, so those are set. - PROD-DB-0177 (owner, measured 2026-09-27 01:15Z on the replayed schema; the failure itself recorded by the #579 session from the owner's "Supabase production migrations" runs on `533554be`, `7e54596d` and `671c5f6a`): production's migration ledger stops at `0176`, so **none of 0177–0387 is applied** and every boundary those migrations close is held in production only by application code. The cause is one statement: `0177_remove_synthetic_auth_users.sql` is a single `DO` block that, for EVERY single-column foreign key to `auth.users`, runs `delete from
where = any()`. On the replayed schema that is **493** foreign keys, **481** of them with no index on the referencing column, so the block is hundreds of sequential scans inside one statement and Supabase's `statement_timeout` cancels it (SQLSTATE 57014) — production at 0176 has fewer keys but the same shape. It is idempotent and best-effort by design (a per-table failure is noticed and skipped), so it is safe to run outside the workflow. **The operator remedy, in order**: (1) in the Supabase SQL editor or `psql` against production, `set statement_timeout = 0;` (session-local) and run the body of 0177 once — expect the `Retired synthetic Auth users: N removed, M still referenced` notice; (2) `supabase migration repair --status applied 0177` so the ledger agrees; (3) re-run the "Supabase production migrations" workflow, which will then apply 0178 onward in order (each already replayed by CI on a fresh database). Alternatively delete the `onb…@seed-onb.bubaly.test` / `person…@seed.bubaly.test` users through the Auth admin API first, which makes 0177 a no-op, then repair and re-run. **No agent applied, retried or altered how the workflow applies anything**, and none will: applying migrations to production is the owner's action, and a change that made 0177 pass inside the workflow would apply 0178–0387 on the next push to `main` by proxy. Until this is done, `PRODUCTION READY` cannot move, whatever the code says. - API-387E2B30BCD7: Current signed ingress retention passes focused tests, including deterministic filing after candidate failure. New integrated ingress hosted acceptance, real provider delivery, controlled old-handler cutover and production configuration remain open; see main-integration-cycle-20260919.md. @@ -13855,28 +13862,28 @@ not read the same on this page.* | MIGRATION-2E00699A65C2 | MIGRATION | supabase/migrations/0279_seed_aeo_question_translations.sql | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | | MIGRATION-401630D8ED40 | MIGRATION | supabase/migrations/0280_aeo_answers_agree_in_number.sql | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | | MIGRATION-6F3D50E93290 | MIGRATION | supabase/migrations/0281_german_clauses_close.sql | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| JOB-4BC14EF80E27 | JOB | /api/cron/feedback-github-sync | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| JOB-F290D7D4DBF1 | JOB | /api/cron/chore-reminders | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| JOB-501B610A6401 | JOB | /api/cron/weekly-digest | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| JOB-947DBEA75C2A | JOB | /api/cron/notifications | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| JOB-90FBAEB0B37F | JOB | /api/cron/push-scan | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| JOB-2D120FC1C5EF | JOB | /api/cron/calendar-feeds | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| JOB-71988A171BC7 | JOB | /api/cron/automations | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| JOB-1C0E9374DE2E | JOB | /api/cron/marketing | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| JOB-F003E81306B4 | JOB | /api/cron/marketing-providers | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| JOB-E2101B7C487E | JOB | /api/cron/checkout-abandoned | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| JOB-195C977F1087 | JOB | /api/cron/autopilot-scan | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| JOB-4065F88A93D9 | JOB | /api/cron/wallet-allowance | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| JOB-D3457248E130 | JOB | /api/cron/model-refresh | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| JOB-6E85CF40F44B | JOB | /api/cron/network-aggregate | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| JOB-7663A237FE3C | JOB | /api/cron/guardian-learning | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| JOB-C5013C344D51 | JOB | /api/cron/provider-sync | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| JOB-9848A06C3416 | JOB | /api/cron/journey-recovery | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| JOB-462EDF9EBA28 | JOB | /api/cron/close-auctions | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| JOB-AAF58B5F5524 | JOB | /api/cron/return-reminders | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| JOB-EF2453D9F633 | JOB | /api/cron/admin-digest | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| JOB-65B62EEA3CA1 | JOB | /api/cron/ai-runs | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | -| JOB-905AB0CFE100 | JOB | /api/cron/family-routines | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | +| JOB-4BC14EF80E27 | JOB | /api/cron/feedback-github-sync | 🔄 IN PROGRESS | Unassessed | Cron pass 2026-09-27 (local prod build, local Supabase): without the secret 401; with `Authorization: Bearer $CRON_SECRET` 200 in <1s, body `{"configured":false,"created":0,"reconciled":0,"changes":[],"errors":0,"ok":true}`; scheduled `15 6 * * *` in vercel.json | None needed | Pending (populated data) | Remaining: a run over populated data, provider delivery where it sends, and the deployed scheduler (production CRON_SECRET is unset — PROD-ENV). | +| JOB-F290D7D4DBF1 | JOB | /api/cron/chore-reminders | 🔄 IN PROGRESS | Unassessed | Cron pass 2026-09-27 (local prod build, local Supabase): without the secret 401; with `Authorization: Bearer $CRON_SECRET` 200 in <1s, body `{"sent":0,"message":"No pending assignments"}`; scheduled `0 18 * * 0` in vercel.json | API-SWEEP-07: same false "sent" count as the weekly digest; fixed the same way (same test, red before) | Re-run after fix: guard test green | Remaining: a run over populated data, provider delivery where it sends, and the deployed scheduler (production CRON_SECRET is unset — PROD-ENV). | +| JOB-501B610A6401 | JOB | /api/cron/weekly-digest | 🔄 IN PROGRESS | Unassessed | Cron pass 2026-09-27 (local prod build, local Supabase): without the secret 401; with `Authorization: Bearer $CRON_SECRET` 200 in <1s, body `{"sent":4,"failed":0,"skipped":5}`; scheduled `0 8 * * 1` in vercel.json | API-SWEEP-07: counted a send skipped for want of a mail provider as sent ({"sent":4} with no RESEND_API_KEY); a skip is now counted as skipped (tests/an-email-that-was-not-sent-is-not-reported-sent.test.ts, red before) | Re-run after fix: guard test green | Remaining: a run over populated data, provider delivery where it sends, and the deployed scheduler (production CRON_SECRET is unset — PROD-ENV). | +| JOB-947DBEA75C2A | JOB | /api/cron/notifications | 🔄 IN PROGRESS | Unassessed | Cron pass 2026-09-27 (local prod build, local Supabase): without the secret 401; with `Authorization: Bearer $CRON_SECRET` 200 in <1s, body `{"ok":true,"families":9,"created":0,"approvals":{"expired":0,"blockedRuns":0,"reminded":0,"remindedFamilies":0},"briefs":{"delivered":0,"families":9,"skipped":9`; scheduled `0 11 * * *` in vercel.json | None needed | Pending (populated data) | Remaining: a run over populated data, provider delivery where it sends, and the deployed scheduler (production CRON_SECRET is unset — PROD-ENV). | +| JOB-90FBAEB0B37F | JOB | /api/cron/push-scan | 🔄 IN PROGRESS | Unassessed | Cron pass 2026-09-27 (local prod build, local Supabase): without the secret 401; with `Authorization: Bearer $CRON_SECRET` 200 in <1s, body `{"ok":true,"families":9,"created":0,"pushed":{"notifications":0,"result":{"sent":0,"skipped":0,"failed":0,"pruned":0,"withheld":0}},"failed":0}`; scheduled `0 12 * * *` in vercel.json | None needed | Pending (populated data) | Remaining: a run over populated data, provider delivery where it sends, and the deployed scheduler (production CRON_SECRET is unset — PROD-ENV). | +| JOB-2D120FC1C5EF | JOB | /api/cron/calendar-feeds | 🔄 IN PROGRESS | Unassessed | Cron pass 2026-09-27 (local prod build, local Supabase): without the secret 401; with `Authorization: Bearer $CRON_SECRET` 200 in <1s, body `{"ok":true,"feeds":0,"synced":0,"imported":0,"failed":0}`; scheduled `0 5 * * *` in vercel.json | None needed | Pending (populated data) | Remaining: a run over populated data, provider delivery where it sends, and the deployed scheduler (production CRON_SECRET is unset — PROD-ENV). | +| JOB-71988A171BC7 | JOB | /api/cron/automations | 🔄 IN PROGRESS | Unassessed | Cron pass 2026-09-27 (local prod build, local Supabase): without the secret 401; with `Authorization: Bearer $CRON_SECRET` 200 in <1s, body `{"workflows":0,"runs":0,"emails":0,"failures":0,"ok":true}`; scheduled `0 13 * * *` in vercel.json | None needed | Pending (populated data) | Remaining: a run over populated data, provider delivery where it sends, and the deployed scheduler (production CRON_SECRET is unset — PROD-ENV). | +| JOB-1C0E9374DE2E | JOB | /api/cron/marketing | 🔄 IN PROGRESS | Unassessed | Cron pass 2026-09-27 (local prod build, local Supabase): without the secret 401; with `Authorization: Bearer $CRON_SECRET` 200 in <1s, body `{"claimed":0,"succeeded":0,"failed":0,"deadLettered":0,"persistenceFailed":0,"ok":true}`; scheduled `0 6 * * *` in vercel.json | None needed | Pending (populated data) | Remaining: a run over populated data, provider delivery where it sends, and the deployed scheduler (production CRON_SECRET is unset — PROD-ENV). | +| JOB-F003E81306B4 | JOB | /api/cron/marketing-providers | 🔄 IN PROGRESS | Unassessed | Cron pass 2026-09-27 (local prod build, local Supabase): without the secret 401; with `Authorization: Bearer $CRON_SECRET` 200 in <1s, body `{"ok":true,"ready":false,"results":{"google_search_console":{"ok":true,"configured":false,"status":"not_configured","rows":0},"bing_webmaster":{"ok":true,"confi`; scheduled `15 7 * * *` in vercel.json | None needed | Pending (populated data) | Remaining: a run over populated data, provider delivery where it sends, and the deployed scheduler (production CRON_SECRET is unset — PROD-ENV). | +| JOB-E2101B7C487E | JOB | /api/cron/checkout-abandoned | 🔄 IN PROGRESS | Unassessed | Cron pass 2026-09-27 (local prod build, local Supabase): without the secret 401; with `Authorization: Bearer $CRON_SECRET` 200 in <1s, body `{"ok":true,"pending":0,"abandoned":0,"fired":0,"failed":0}`; scheduled `0 14 * * *` in vercel.json | None needed | Pending (populated data) | Remaining: a run over populated data, provider delivery where it sends, and the deployed scheduler (production CRON_SECRET is unset — PROD-ENV). | +| JOB-195C977F1087 | JOB | /api/cron/autopilot-scan | 🔄 IN PROGRESS | Unassessed | Cron pass 2026-09-27 (local prod build, local Supabase): without the secret 401; with `Authorization: Bearer $CRON_SECRET` 200 in <1s, body `{"ok":true,"families":9,"entitled":0,"skipped":9,"scanned":0,"autoExecuted":0,"notified":0,"policyCandidates":0,"failures":0}`; scheduled `30 6 * * *` in vercel.json | None needed | Pending (populated data) | Remaining: a run over populated data, provider delivery where it sends, and the deployed scheduler (production CRON_SECRET is unset — PROD-ENV). | +| JOB-4065F88A93D9 | JOB | /api/cron/wallet-allowance | 🔄 IN PROGRESS | Unassessed | Cron pass 2026-09-27 (local prod build, local Supabase): without the secret 401; with `Authorization: Bearer $CRON_SECRET` 200 in <1s, body `{"ok":true,"due":0,"paid":0,"skippedFree":0,"skippedUnauthored":0,"failed":0}`; scheduled `0 7 * * *` in vercel.json | None needed | Pending (populated data) | Remaining: a run over populated data, provider delivery where it sends, and the deployed scheduler (production CRON_SECRET is unset — PROD-ENV). | +| JOB-D3457248E130 | JOB | /api/cron/model-refresh | 🔄 IN PROGRESS | Unassessed | Cron pass 2026-09-27 (local prod build, local Supabase): without the secret 401; with `Authorization: Bearer $CRON_SECRET` 200 in <1s, body `{"ok":true,"families":9,"refreshed":0,"skipped":9,"failures":0}`; scheduled `0 4 * * *` in vercel.json | None needed | Pending (populated data) | Remaining: a run over populated data, provider delivery where it sends, and the deployed scheduler (production CRON_SECRET is unset — PROD-ENV). | +| JOB-6E85CF40F44B | JOB | /api/cron/network-aggregate | 🔄 IN PROGRESS | Unassessed | Cron pass 2026-09-27 (local prod build, local Supabase): without the secret 401; with `Authorization: Bearer $CRON_SECRET` 200 in <1s, body `{"ok":true,"contributors":0,"aggregates":0}`; scheduled `0 3 * * *` in vercel.json | None needed | Pending (populated data) | Remaining: a run over populated data, provider delivery where it sends, and the deployed scheduler (production CRON_SECRET is unset — PROD-ENV). | +| JOB-7663A237FE3C | JOB | /api/cron/guardian-learning | 🔄 IN PROGRESS | Unassessed | Cron pass 2026-09-27 (local prod build, local Supabase): without the secret 401; with `Authorization: Bearer $CRON_SECRET` 200 in <1s, body `{"ok":true,"families":0,"suggestionsCreated":0,"failed":0}`; scheduled `0 2 * * *` in vercel.json | None needed | Pending (populated data) | Remaining: a run over populated data, provider delivery where it sends, and the deployed scheduler (production CRON_SECRET is unset — PROD-ENV). | +| JOB-C5013C344D51 | JOB | /api/cron/provider-sync | 🔄 IN PROGRESS | Unassessed | Cron pass 2026-09-27 (local prod build, local Supabase): without the secret 401; with `Authorization: Bearer $CRON_SECRET` 200 in <1s, body `{"ok":true,"synced":0,"skipped":0,"failed":0,"auditFailed":0,"scanned":0,"details":[]}`; scheduled `15 8 * * *` in vercel.json | None needed | Pending (populated data) | Remaining: a run over populated data, provider delivery where it sends, and the deployed scheduler (production CRON_SECRET is unset — PROD-ENV). | +| JOB-9848A06C3416 | JOB | /api/cron/journey-recovery | 🔄 IN PROGRESS | Unassessed | Cron pass 2026-09-27 (local prod build, local Supabase): without the secret 401; with `Authorization: Bearer $CRON_SECRET` 200 in <1s, body `{"onboardingFired":0,"demoFired":0,"failed":0}`; scheduled `0 15 * * *` in vercel.json | None needed | Pending (populated data) | Remaining: a run over populated data, provider delivery where it sends, and the deployed scheduler (production CRON_SECRET is unset — PROD-ENV). | +| JOB-462EDF9EBA28 | JOB | /api/cron/close-auctions | 🔄 IN PROGRESS | Unassessed | Cron pass 2026-09-27 (local prod build, local Supabase): without the secret 401; with `Authorization: Bearer $CRON_SECRET` 200 in <1s, body `{"ok":true,"closed":0,"sold":0,"unsold":0,"failed":0}`; scheduled `0 10 * * *` in vercel.json | None needed | Pending (populated data) | Remaining: a run over populated data, provider delivery where it sends, and the deployed scheduler (production CRON_SECRET is unset — PROD-ENV). | +| JOB-AAF58B5F5524 | JOB | /api/cron/return-reminders | 🔄 IN PROGRESS | Unassessed | Cron pass 2026-09-27 (local prod build, local Supabase): without the secret 401; with `Authorization: Bearer $CRON_SECRET` 200 in <1s, body `{"ok":true,"reminded":0,"overdue":0,"failed":0}`; scheduled `0 8 * * *` in vercel.json | None needed | Pending (populated data) | Remaining: a run over populated data, provider delivery where it sends, and the deployed scheduler (production CRON_SECRET is unset — PROD-ENV). | +| JOB-EF2453D9F633 | JOB | /api/cron/admin-digest | 🔄 IN PROGRESS | Unassessed | Cron pass 2026-09-27 (local prod build, local Supabase): without the secret 401; with `Authorization: Bearer $CRON_SECRET` 200 in <1s, body `{"ok":true,"sent":0,"skipped":2,"failed":0,"recipients":2,"total":6,"headline":"6 new families."}`; scheduled `30 12 * * *` in vercel.json | None needed | Pending (populated data) | Remaining: a run over populated data, provider delivery where it sends, and the deployed scheduler (production CRON_SECRET is unset — PROD-ENV). | +| JOB-65B62EEA3CA1 | JOB | /api/cron/ai-runs | 🔄 IN PROGRESS | Unassessed | Cron pass 2026-09-27 (local prod build, local Supabase): without the secret 401; with `Authorization: Bearer $CRON_SECRET` 200 in <1s, body `{"ok":true,"claimed":0,"executed":0,"returnedToQueue":0,"elapsedMs":16,"results":[]}`; scheduled `20 9 * * *` in vercel.json | None needed | Pending (populated data) | Remaining: a run over populated data, provider delivery where it sends, and the deployed scheduler (production CRON_SECRET is unset — PROD-ENV). | +| JOB-905AB0CFE100 | JOB | /api/cron/family-routines | 🔄 IN PROGRESS | Unassessed | Cron pass 2026-09-27 (local prod build, local Supabase): without the secret 401; with `Authorization: Bearer $CRON_SECRET` 200 in <1s, body `{"ok":true,"considered":0,"armed":0,"filed":0,"skipped":0,"problems":0,"ms":9}`; scheduled `10 9 * * *` in vercel.json | None needed | Pending (populated data) | Remaining: a run over populated data, provider delivery where it sends, and the deployed scheduler (production CRON_SECRET is unset — PROD-ENV). | | CALLBACK-145967CDCF24 | CALLBACK | /api/contact-center/email | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | | CALLBACK-24807E48E6E6 | CALLBACK | /api/contact-center/sms | 🔄 IN PROGRESS | High | Current 2026-09-19 integration: signed canonical ingress is retained before optional candidate work; timeout/locale failures preserve deterministic filing without a reply, and a late ignored-abort candidate cannot emit. Contact Center, marketing push and social publication regression passes 875 checks across 32 suites. Hosted acceptance of the new ingress fixture and real provider delivery remain pending. Historical evidence: Full application source a61804db passes 15,011 unit tests across 1,202 files with zero failures/skips. Focused helper checks pass 223 cases, concierge checks pass 38, and signed ingress/authorized reader cover new Unicode replay and legacy boundaries. New disposable signed HTTP/PostgreSQL text case is discovered; hosted execution pending. | Retain the signed original ingress before optional context and candidate work; freeze the first reply candidate before any automatic emission; exact one-time final emission reservation; held legacy/uncertain replies; verified deterministic projection. Candidate timeout or locale failure retains deterministic summary/classification and files without an automatic reply. Signed delivery callbacks bind token, account and first provider SID, advance status monotonically and expose safe provider progress in the authorized inbox. New text uses scalar-safe PostgreSQL/XML bounds before candidate fingerprinting; bounded family labels preserve useful fallbacks; old oversized queued replies are held without rewriting stored content. Provider completion receives the candidate cancellation signal. | Application a61804db passes all 15,011 unit tests in 1,202 files, zero failed/skipped, in 43.60 seconds. The exact frozen source was committed without changes during execution. Application a61804db builds 252 pages (44-second compilation). Strict types, lint with four baseline warnings, localization and query audit (491 tables, 77 functions, 144 routes) PASS. See contact-center-text-bounds-cycle.md. Signed delivery publication 0d68bdbb passes CI 34718960420: 1,064 scheduled E2E cases and 1,064 passes in 8.0 minutes, with both authenticated/durable flags enabled; identical tested tree becff049. Web, database, mobile, finance and Vercel PASS. New text source a61804db requires its own hosted execution. See guardian-hosted-acceptance.md. | Private checks and synthetic hosted callbacks are separate from real provider delivery. No SQL, dependency or shared navigation changes; exact source inventory preserved. See docs/final-audit/main-integration-cycle-20260919.md; new hosted ingress acceptance remains pending. | | CALLBACK-A55C76501924 | CALLBACK | /api/contact-center/voice | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | | @@ -16101,7 +16108,7 @@ not read the same on this page.* | TEST-002 | TEST | Authenticated browser fixture ownership across projects | 🔄 IN PROGRESS | High | Actual unmodified beforeEach hooks executed in two isolated VM project modules with shared synthetic Auth store:chromium createsuser/family;iphone shared-emailcleanup deletesboth and replacesuser. Same resultNode22and24. Exact hostedtoast failure attribution remains unproven. | Fresh project/worker/nonce-owned fixture email, identity-checked cleanup and unchanged fullonboarding/taskpersistence assertions. | 4actualhook interleaving/ownership regressions PASS. Published d36602e (application source608c9307) hosted CI34701809388: all4jobsPASS,13003unit tests,11database probes,697browser tests in5.3minutes, no final flaky summary. Web runtimeNode24.20.0; retrieved log contains no TransformStream/ERR_INVALID_STATE match. Vercel checkPASS. See care-verification-checkpoint.md; live provider/cron/policy proof remains separate. | Recorded before fixture edits. No live user accounts modified. | | SUPPORT-5D639D9E4966 | SUPPORT | app/api/cron/contact-center-urgent/route.ts | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | Source discovery only; exact committed hashes and baseline in discovery/care-delivery-inventory.json. Full workflow verification remains separate. | | API-62F6F32B2748 | API | GET /api/cron/contact-center-urgent | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:401 c:401 p:401 a:401] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Source discovery only; exact committed hashes and baseline in discovery/care-delivery-inventory.json. Full workflow verification remains separate. Remaining: a valid request, its database side effects and response shape, end to end. | -| JOB-47B3E5312926 | JOB | /api/cron/contact-center-urgent | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | Source discovery only; exact committed hashes and baseline in discovery/care-delivery-inventory.json. Full workflow verification remains separate. | +| JOB-47B3E5312926 | JOB | /api/cron/contact-center-urgent | 🔄 IN PROGRESS | Unassessed | Cron pass 2026-09-27 (local prod build, local Supabase): without the secret 401; with `Authorization: Bearer $CRON_SECRET` 200 in <1s, body `{"ok":true,"examined":0,"accepted":0,"in_app_only":0,"pending":0,"unknown":0,"rejected":0,"legacy_unknown":0,"failed":0}`; scheduled `0 1 * * *` in vercel.json | None needed | Pending (populated data) | Source discovery only; exact committed hashes and baseline in discovery/care-delivery-inventory.json. Full workflow verification remains separate. Remaining: a run over populated data, provider delivery where it sends, and the deployed scheduler (production CRON_SECRET is unset — PROD-ENV). | | SERVICE-7F6A900AF295 | SERVICE | inboundProviderRef | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | Source discovery only; exact committed hashes and baseline in discovery/care-delivery-inventory.json. Full workflow verification remains separate. | | SERVICE-A7E34BED0527 | SERVICE | findInboundMessage | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | Source discovery only; exact committed hashes and baseline in discovery/care-delivery-inventory.json. Full workflow verification remains separate. | | LIBRARY-C3284E1B6E1F | LIBRARY | lib/contact-center/urgent-delivery.ts | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | Source discovery only; exact committed hashes and baseline in discovery/care-delivery-inventory.json. Full workflow verification remains separate. | @@ -16133,7 +16140,7 @@ not read the same on this page.* | SEC-004 | SEC | Persisted social URLs must not become active-content links | 🔄 IN PROGRESS | High | Independent actual createPostAction plus real React18 PostPage static rendering persists javascript:alert(document.domain) and renders href=javascript despite a React warning. No browser code execution or live exploit claimed. Source trace also finds unvalidated target/feed/comment permalink anchors. | Shared safeSocialLink validates HTTP(S), no credentials/whitespace and4096character limit. Action rejects unsafe incoming links; post, target, feed and inbox rendering validates persisted links, retains escaped text and safe profile fallbacks. | Independent real action andReactSSR tests pass all9URL-specific checks within37presentation and75action cases. Validlinks remain clickable; unsafe legacy javascript/data/credential/whitespace/overlength values do not become anchors; no React warning. Root combined83Chromium and320focused tests pass. No browser exploit/liveDB claim. Pinned910cd271:1156files/13254unit tests PASS128.09s withzero unhandled errors;336Chromium PASS53.0s;246page production build,final strict types,lint/query/i18n PASS under isolatedNode24.21.0. Build/browser/startup runtime-identicalf6e17ef; sole follow-up changes2cronfixtures+evidence. Home2076ms/navigation1647ms/nopageerrors;5responsecancellations/0internalstreamerrors. Exactlogs/provenance/limitations in capture-scheduling-checkpoint.md. | See independent social-scheduling-actions-review.md. No SQL or provider operations. | | SUPPORT-38ACFBD09F34 | SUPPORT | app/api/cron/social-publish/route.ts | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | Source discovery only; exact committed hashes and baseline in discovery/capture-scheduling-inventory.json. Full workflow verification remains separate. | | API-18FB4D1FF9A8 | API | GET /api/cron/social-publish | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:401 c:401 p:401 a:401] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Source discovery only; exact committed hashes and baseline in discovery/capture-scheduling-inventory.json. Full workflow verification remains separate. Remaining: a valid request, its database side effects and response shape, end to end. | -| JOB-3A0C45177B5D | JOB | /api/cron/social-publish | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | Source discovery only; exact committed hashes and baseline in discovery/capture-scheduling-inventory.json. Full workflow verification remains separate. | +| JOB-3A0C45177B5D | JOB | /api/cron/social-publish | 🔄 IN PROGRESS | Unassessed | Cron pass 2026-09-27 (local prod build, local Supabase): without the secret 401; with `Authorization: Bearer $CRON_SECRET` 200 in <1s, body `{"ok":true,"inspected":0,"attempted":0,"published":0,"pending":0,"unknown":0,"failed":0}`; scheduled `5 1 * * *` in vercel.json | None needed | Pending (populated data) | Source discovery only; exact committed hashes and baseline in discovery/capture-scheduling-inventory.json. Full workflow verification remains separate. Remaining: a run over populated data, provider delivery where it sends, and the deployed scheduler (production CRON_SECRET is unset — PROD-ENV). | | SERVICE-9BB09AB06EF2 | SERVICE | assertXCredentials | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | Source discovery only; exact committed hashes and baseline in discovery/capture-scheduling-inventory.json. Full workflow verification remains separate. | | SERVICE-8C21B3472285 | SERVICE | loadScheduledXAccessToken | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | Source discovery only; exact committed hashes and baseline in discovery/capture-scheduling-inventory.json. Full workflow verification remains separate. | | LIBRARY-9399B73C5399 | LIBRARY | lib/social/links.ts | 🔄 IN PROGRESS | High | tests/a-stored-link-is-a-web-link.test.ts asserts safeSocialLink and safeWebLink agree | 2026-09-26 (Q60): delegates to lib/utils/safe-link.ts so SEC-004's rule has one definition | Guard passes | Source discovery only; exact committed hashes and baseline in discovery/capture-scheduling-inventory.json. Full workflow verification remains separate. · See Q60. | @@ -16292,7 +16299,7 @@ not read the same on this page.* | SERVICE-73DB0E2176B9 | SERVICE | createMealAction | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | Source discovery only; exact committed hashes and baseline in discovery/weekly-meal-inventory.json. Full workflow verification remains separate. | | SUPPORT-3650D4324732 | SUPPORT | app/api/cron/marketing-social/route.ts | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | Source discovery only; exact committed hashes and baseline in discovery/weekly-meal-inventory.json. Full workflow verification remains separate. | | API-9ED269DBDB75 | API | GET /api/cron/marketing-social | 🔄 IN PROGRESS | Unassessed | API sweep 2026-09-27 (scripts/api-audit, local prod build): signed-out/child/parent/super-admin × empty/malformed body [a:401 c:401 p:401 a:401] — OK: no 5xx, no internal detail, signed-out refused, admin/cron boundaries held | None needed at this boundary | Pending (workflow) | Source discovery only; exact committed hashes and baseline in discovery/weekly-meal-inventory.json. Full workflow verification remains separate. Remaining: a valid request, its database side effects and response shape, end to end. | -| JOB-F5185B999046 | JOB | /api/cron/marketing-social | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | Source discovery only; exact committed hashes and baseline in discovery/weekly-meal-inventory.json. Full workflow verification remains separate. | +| JOB-F5185B999046 | JOB | /api/cron/marketing-social | 🔄 IN PROGRESS | Unassessed | Cron pass 2026-09-27 (local prod build, local Supabase): without the secret 401; with `Authorization: Bearer $CRON_SECRET` 200 in <1s, body `{"considered":0,"posted":0,"skipped":0,"failures":0,"reasons":{},"ok":true}`; scheduled `45 6 * * *` in vercel.json | None needed | Pending (populated data) | Source discovery only; exact committed hashes and baseline in discovery/weekly-meal-inventory.json. Full workflow verification remains separate. Remaining: a run over populated data, provider delivery where it sends, and the deployed scheduler (production CRON_SECRET is unset — PROD-ENV). | | LIBRARY-D8E1C781AB00 | LIBRARY | lib/marketing/recurring-ads-runner.ts | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | Source discovery only; exact committed hashes and baseline in discovery/weekly-meal-inventory.json. Full workflow verification remains separate. | | SERVICE-4936014BD04B | SERVICE | validPlatforms | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | Source discovery only; exact committed hashes and baseline in discovery/weekly-meal-inventory.json. Full workflow verification remains separate. | | SERVICE-EF004FE9241A | SERVICE | scheduleFromRow | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | Source discovery only; exact committed hashes and baseline in discovery/weekly-meal-inventory.json. Full workflow verification remains separate. | @@ -16464,7 +16471,8 @@ not read the same on this page.* | SMS-002 | SMS | Guardian SMS recovery after provider retries stop | 🔄 IN PROGRESS | High | Installed SDK recovery/fairness regressions PASS. Hosted real PostgreSQL/HTTP now verifies one signed failed intake followed by cron-only recovery, child receipt denial, untouched unreceipted content, idempotence and a clean continuation sweep. | Shared signed-ingress and service-receipt processing; independent terminal verification; stable notification identity with quiet hours; bounded scheduled recovery with a finite sweep endpoint. Temporary destination removal preserves retained work. Cursor revisions prevent lost progress under competing workers. | Publication fae35e90: CI 34717274616, tested checkout 62d8a325 with tree bb838fce equal to publication; 14,636 units, 252-page build, all 1,062 scheduled browser cases PASS. Database: 300 migrations, zero failures, eleven probes; finance: 66 assertions, mobile and Vercel PASS. See guardian-hosted-acceptance.md. | See guardian-sms-recovery-cycle.md. Production scheduler and provider delivery remain unverified. No SQL, dependency or shared navigation changes. | | SUPPORT-0E0FDE18B6EB | SUPPORT | app/api/cron/guardian-sms-recovery/route.ts | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | Source discovery only; exact committed hashes and baseline in discovery/guardian-sms-recovery-inventory.json. Full workflow verification remains separate. | | API-C9192C0F02D9 | API | GET /api/cron/guardian-sms-recovery | 🔄 IN PROGRESS | High | Authorization, finite cursor fairness, retry failures and idempotence tested; actual cron-only recovery and clean sweep pass in hosted PostgreSQL/HTTP. | Bounded service-receipt consumer with revisioned finite-sweep cursor; no authority inferred from member communications. | Publication fae35e90: CI 34717274616, tested checkout 62d8a325 with tree bb838fce equal to publication; 14,636 units, 252-page build, all 1,062 scheduled browser cases PASS. Database: 300 migrations, zero failures, eleven probes; finance: 66 assertions, mobile and Vercel PASS. See guardian-hosted-acceptance.md. | Production scheduler and cron-secret deployment remain unverified. API sweep 2026-09-27: OK [a:401 c:401 p:401 a:401]. | -| JOB-7C2C80C9F161 | JOB | /api/cron/guardian-sms-recovery | 🔄 IN PROGRESS | High | Authorization, finite cursor fairness, retry failures and idempotence tested; actual cron-only recovery and clean sweep pass in hosted PostgreSQL/HTTP. | Bounded service-receipt consumer with revisioned finite-sweep cursor; no authority inferred from member communications. | Publication fae35e90: CI 34717274616, tested checkout 62d8a325 with tree bb838fce equal to publication; 14,636 units, 252-page build, all 1,062 scheduled browser cases PASS. Database: 300 migrations, zero failures, eleven probes; finance: 66 assertions, mobile and Vercel PASS. See guardian-hosted-acceptance.md. | Production scheduler and cron-secret deployment remain unverified. | +| JOB-7C2C80C9F161 | JOB | /api/cron/guardian-sms-recovery | 🔄 IN PROGRESS | High | Authorization, finite cursor fairness, retry failures and idempotence tested; actual cron-only recovery and clean sweep pass in hosted PostgreSQL/HTTP. | Bounded service-receipt consumer with revisioned finite-sweep cursor; no authority inferred from member communications. | Publication fae35e90: CI 34717274616, tested checkout 62d8a325 with tree bb838fce equal to publication; 14,636 units, 252-page build, all 1,062 scheduled browser cases PASS. Database: 300 migrations, zero failures, eleven probes; finance: 66 assertions, mobile and Vercel PASS. See guardian-hosted-acceptance.md. | Production scheduler and cron-secret deployment remain unverified. Cron pass 2026-09-27: 401 without the secret, 200 with it. | +| JOB-430CCD9DD165 | JOB | /api/cron/library-feeds | 🔄 IN PROGRESS | Unassessed | Cron pass 2026-09-27 (local prod build, local Supabase): without the secret 401; with the secret 200, body `{"ok":true,"feeds":0,"refreshed":0,"added":0,"failed":0,"skipped":0}`; scheduled `0 3 * * *` in vercel.json | None needed | Pending (populated data) | Added 2026-09-27: a scheduled job with no inventory row. Remaining: a run over populated data and the deployed scheduler. | | LIBRARY-085E4BBF3CE0 | LIBRARY | lib/guardian/sms-deadline.ts | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | Source discovery only; exact committed hashes and baseline in discovery/guardian-sms-recovery-inventory.json. Full workflow verification remains separate. | | SERVICE-BC9ED5463E65 | SERVICE | smsStep | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | Source discovery only; exact committed hashes and baseline in discovery/guardian-sms-recovery-inventory.json. Full workflow verification remains separate. | | LIBRARY-EC0B659A2F4D | LIBRARY | lib/guardian/sms-notification.ts | ⬜ NOT STARTED | Unassessed | Pending | None | Pending | Source discovery only; exact committed hashes and baseline in discovery/guardian-sms-recovery-inventory.json. Full workflow verification remains separate. |