Repository navigation
Audit fixes: 16 areas from the security and correctness audit - #989
Conversation
…re failed connect receipts - saveXConnection: accept a stale 'connecting' credential for takeover, run the expiry/permission re-checks before the first write, and on any later failure restore an existing account's token row and status (revoke a new grant on a target that will not connect). - finishXAuthorization retires its claimed receipt on failure; the callback retires an unclaimed receipt on denial/invalid callbacks; createXReceipt sweeps stale unclaimed receipts and retires its account if the token insert fails. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KP9rt5rVQ9jDMpZp2xBy3K
…unlinked, one default list - resolveAssistantLink/lookupAssistantLink refuse an assistant_links row whose created_by is null or differs from user_id: the only minting path writes both from the session user, so such a row was written through PostgREST in a co-parent's name and would outlive the writer's removal (0419 and the owner check follow user_id). The client INSERT/UPDATE grant from 0283 still needs a migration to withdraw. - lookupAssistantLink reports read failures as 'unavailable'; the Alexa route speaks ERROR_SPEECH for that instead of the "link your account" prompt. resolveAssistantLink keeps its null-on-any-failure contract. - ensureList creates a missing default list through 0443's serialised get-or-create (ensureDefaultGroceryListId / ensureTodoListId), so two first captures at once share one list; a list this capture created is removed again, while still empty, when the item insert fails. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KP9rt5rVQ9jDMpZp2xBy3K
…goal writes, report money deletes lost in transit - wallet hub: deleteWalletRowAction removes a transaction via lib/services/finances deleteTransaction, so the role check, zero-row check and household-trail entry match /dashboard/billing. - goals: deleteGoal requires a parent/adult (or system); create/update/progress refuse guest and caregiver. goals-module hides delete from non-managers. RLS goals_* still admits every member; the policy change is left for a migration. - billing, budgets and savings delete handlers go through settleAction, which toasts a rejected server-action call and re-reads the list. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KP9rt5rVQ9jDMpZp2xBy3K
lib/assistant/tools.ts add_goal inserted into `goals` itself, so the role rule 632ce74 added to createGoal (no guest or caregiver) never ran for chat: the hand-written tool shadows the registry's goals.create in mergeToolSets, and RLS on goals admits every member. - AssistantCtx gains a required `role`; the three call sites (chat route, /api/ai listing, assistant-engine) pass the caller's family role. - add_goal calls createGoal with an 'ai' scope built from that role. An unknown role is refused. Return shape and the "Created the goal" summary are unchanged; service refusals surface their message, db errors keep the toolFailure wording. The goal now also gets the service's date check, progress 0 / is_complete false, and the activity entry. - No other direct writes to `goals` remain outside lib/services/goals (the digital-twin page, goals-module and operating-index only read). - tests/the-assistant-cannot-add-a-goal-for-a-guest.test.ts: guest, caregiver and unknown roles are refused with nothing written; parent, adult, teen and child still create the goal. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KP9rt5rVQ9jDMpZp2xBy3K
deleteReminderAction and deleteTodoAction took any active member: the services scope by id + family_id only and the RLS is is_family_member for all, so a guest, caregiver or child could delete a parent's reminder or any family task, contradicting the permission matrix (can_delete on reminders is parent-only) and the view-only guest/caregiver role descriptions. Both actions now allow managers (parent/adult) to delete anything; a teen or child only rows they created, are assigned, or (reminders) that are about them; a guest or caregiver nothing. The matching per-command DELETE RLS policies are left for a migration. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KP9rt5rVQ9jDMpZp2xBy3K
canDeleteReminder and canDeleteTodo let a teen or child delete a row they were assigned or (reminders) that was about them. Those columns are not ownership: updateTodoAction sets assigned_to_id with no role check and the reminder modal updates family_reminders directly, so a teen could reassign a parent's task or reminder to themselves and then delete it. A teen or child may now delete only rows they created: family_reminders by created_by = auth user id (0014), todo_items by created_by = their family_members id (0015, which is what that column references). Managers may still delete any family row; guests and caregivers none. Tests add the reassign-then-delete case for both tables and turn the "about them / assigned to them" cases into refusals. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KP9rt5rVQ9jDMpZp2xBy3K
- materializeConciergePlan reserves the concierge_plan_actions row under its unique key before creating the record; a 23505 loser creates nothing, a failed create releases the reservation. - skipTrust (and the AI actor scope) now require the service-only trust_audit_logs line naming the approval, not the member-writable requested_by_kind; gateAiAction writes that line for rows it files. A member can no longer approve their own member-filed request. - foldIntoRun / expireStale release or block only steps whose own approval_id is the row; plan_step_ids on the row no longer widen what is touched. - A recorded decision whose run fold or tool execution failed is reported as non-retryable; new resumeSettledRuns sweep returns runs parked on a decided or expired approval to the queue. Dependent-step and request updates are checked. - expireStale blocks steps and run before expiring the approval. - expireStale / remindPendingApprovals / resumeSettledRuns return failure counts, and the notifications cron adds them to generationFailures (502). - connectAccount refuses to move an account already connected for another family and refuses an empty external id; both OAuth callbacks fail the connect instead of keying the account on the user id. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KP9rt5rVQ9jDMpZp2xBy3K
…quest check - openApprovalRequest and openApproval share fileOrReusePendingApproval: a pending row under the dedupe key is reused only if its requested_by_kind, requested_by_member_id, domain, capability and payload match the ask. A row planted under the key is never reused (so it cannot collect Bubaly's ai_agent audit line); the ask is filed beside it without a key. - isOwnMemberRequest no longer trusts requested_by_kind: an 'ai' row whose asker is the approver counts as their own unless filedByBubaly proves it (plan_steps rows excepted: they release only server-parked steps). Closes an adult materialising a concierge plan alone. - Tool approvals whose run fold failed re-read the parked steps: none means a standalone approval, executed now; unreadable says so instead of promising a sweep that only scans runs. - connectAccount releases a stale account left in a family the user is no longer active in, and raises AccountConnectedElsewhereError otherwise; the OAuth callbacks map it to error=connected_elsewhere with its own message. - materializeConciergePlan releases a failed reservation through the service writer, so the member DELETE policy on concierge_plan_actions can go. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KP9rt5rVQ9jDMpZp2xBy3K
…llet money step-up - createSavingsGoal: a keyed call whose name-matched probe finds a goal with different money (target/current) answers ALREADY_SAVED instead of handing an older goal back as the approved one. - Savings contribution modal: in-flight guard (ref + loading/disabled button), so a double press posts one delta; a thrown action keeps the modal open. - updateBudget/budgetVsActual: the oldest row is the category's budget for readers and writers; a save that loses the read-then-insert race folds its row into the winner; duplicate category rows no longer double-count spend. - Wallet hub: add/delete on transactions and financial_accounts apply the same aal2 money step-up as the billing actions; the hub routes the refusal through reportRefusal. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KP9rt5rVQ9jDMpZp2xBy3K
…confirm
- SENSITIVE_TERMS: stems (diagnos/therap/pregnan/bank) take \w* and nouns
their plurals, so "diagnosis", "therapist", "pregnant", "medications",
"passwords" and "banking" are classed sensitive.
- Schedule, shopping and travel context slices read facts through a new
recallFactsForContext helper that checks memory_enabled strictly and
yields no facts when memory is off or the setting cannot be read.
- The purchase_advisor insight reads memory_enabled strictly before
querying family_facts and sends none when memory is off.
- confirmFact refuses a non-open (dismissed) card and flips the status
with .eq('status','suggested'), so a concurrent or replayed confirm
rolls back its insert instead of leaving a duplicate fact, and cannot
overwrite a concurrent dismissal.
- Remember/confirm activity titles carry the label, not the value, so
forgetting a fact does not leave its value in the activity feed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KP9rt5rVQ9jDMpZp2xBy3K
…nt SMS budget and recovery - Email auto-reply is reserved once per (family, provider ref) in ai_tool_calls (contact_center.email_reply); redeliveries no longer resend the reply or write another outbound row. - Inbound email refs are stored as email:<sha256(familyId, Message-Id)>, so one Message-Id addressed to two families files for both and a sender can no longer pre-empt another family's message. - Urgent SMS has a per-family budget (5/hour, 20/day) and collapses repeats from one sender within 10 minutes; over budget it stays in-app only (contactUrgent.smsBudget). - Twilio 401/403/404 is 'misconfigured', not 'rejected': the receipt is requeued with drain and no spent attempt, and sent once config is fixed. - A receipt-less inbox row filed non-urgent is queued for escalation when a redelivery classifies it urgent; only rows filed urgent are legacy_unknown. - The post-send receipt write uses its own 5s deadline instead of the shared 25s signal, so an accepted SMS is not stranded in 'dispatching'. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KP9rt5rVQ9jDMpZp2xBy3K
…e out The per-family urgent-SMS budget and the same-sender collapse counted every receipt with a recent locked_at, including claims whose send came back misconfigured, retryable or rejected and sent nothing (misconfigured retries re-claim every 5 minutes, refreshing locked_at). An outage therefore demoted a same-sender follow-up and any backlog beyond 5/hour to in-app only for good, and five Twilio rejections blocked the next genuine urgent text. Both checks now count only receipts whose phase is dispatching, accepted or unknown, filtered in the query (so the limit applies to sent rows) and re-checked in code. Also confirm the email-reply receipt's finishing update (select + row check), which the unconfirmed-write ratchet flagged as a new member. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KP9rt5rVQ9jDMpZp2xBy3K
…consented marketing audience
- push/subscribe: before upserting, remove (service role, row + owner named)
any other account's push_devices row for the same device_key, so a shared
or handed-down device belongs to one account and is buzzed once.
- sign-out: the browser remembers the device key it registered and, on
sign-out, POSTs /api/push/unsubscribe with the leaving session's bearer
token before revoking it; push/unsubscribe accepts that bearer token.
- notify('family') fans out to one row per active member with a login, so
reading a family notice no longer clears it for everyone; a legacy NULL
family row still dedupes for all. markRead (service and screen) and
markAllRead are bounded to the member's own and family-wide rows.
- Marketing push audience requires explicit consent
(user_preferences.notification_prefs.marketingPush === true) and excludes
any account with a child or teen membership, read before dispatch.
- Stuck marketing push campaigns: resolveStalePushCampaignAction records a
stale 'preparing' attempt as preflight_failed (retryable) and a stale
'dispatching' or 'unknown' attempt as reviewed (deletable, never re-sent).
- APNs GOAWAY retires the session gracefully instead of failing streams the
provider may already have accepted.
notify() dedupe race and per-user read state for other writers' NULL rows
need schema changes and are not included.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KP9rt5rVQ9jDMpZp2xBy3K
- middleware: the exact POST /api/push/unsubscribe with a well-formed bearer
joins the bearer-API exception, so the sign-out detach (cookies already
cleared, credentials 'omit') reaches the route's bearer branch instead of a
middleware 401. No other /api/push path or method is opened.
- notify('family'): the per-member fan-out rows are written with sent_at
stamped, the email digest's own "resolved" mark, so a family notice stays
in-app and push only as the NULL row was. Push (pushed_at) and per-member
read state are unchanged.
- Tests drive the detach through the middleware into the route, with a
negative control for other /api/push paths, methods and bad headers, and
prove a family notice sends no email while named/manager notices still do.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KP9rt5rVQ9jDMpZp2xBy3K
…ff the feed, family-day expiry window, guest capture refusal, permanent OCR failures - linkToVacation checks a supplied member_id is an active member of the family (not_found otherwise); resolveAssigneeId validates assignee_id against the family. A 23505 on insert re-reads and returns the existing link (created:false). - readDocument/linkToVacation record "Opened/Attached a private document" for sensitive documents instead of the title on agent_activity. - Document-expiry notifications bound expires_at (DATE) by the family's day keys, not UTC instants. - Guests are refused by /api/paperwork/capture, /api/paperwork/link, captureDocument, captureDocumentLink and authorizeDocumentLink. - extractDocumentText marks refusals, malformed transcriptions and auth/config/model errors non-retryable, so an inbound email with such an attachment is acknowledged (attachment 'skipped') instead of 503-looping. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KP9rt5rVQ9jDMpZp2xBy3K
…rwork filing - extractDocumentText: only failures about this document are permanent (refusal, malformed/empty transcription, HTTP 400/413/422). Missing or rejected keys, unknown models (401/403/404) and everything else stay retryable, so the inbound-email webhook answers 503 and the attachment is redelivered instead of being skipped while the key or model is wrong. A numeric status decides on its own; message wording no longer overrides it. - addPaperworkAction refuses guests with actionRefusal.notAllowed, the same view-only refusal captureDocument/captureDocumentLink give. The other paperwork actions do not create paperwork_items. - resolveAssigneeId logs the member lookup error like the other lookups. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KP9rt5rVQ9jDMpZp2xBy3K
- Feed-imported calendar events (feed_id/external_uid) never auto-prep into reminders, groceries or the family-wide "Autopilot handled" notification. - Accepting a policy suggestion claims the row (open -> approved, fenced on status and updated_at) before writing the policy, and stamps executed only from that claim: two accepts write one policy, and a concurrent dismiss or archive is never overwritten. - The policy-scan refresh is fenced on status 'open' and the read updated_at. - resolveSuggestion re-reads the source row (appointment, event, renewal, insurance, medication) and returns 'changed' when it is gone, inactive, moved, or (appointments) already reminded. - Birthday gift ideas are chosen from all wishes, regardless of purchase state. - The scan reads every live dedupe key (paged) and treats a 23505 on insert as a prior row instead of failing the whole scan. - Reminder times are clamped to an instant still ahead (next family 09:00), in the scan's auto path and in resolveSuggestion. - Renewal drafts carry a real `at` three days before the lapse; renewal and insurance cards expire at the family's local end of day. - A purchase approval whose request association fails is cancelled instead of left pending and undeliverable. - The autopilot cron runs families with bounded concurrency inside a time budget, rotates its start by day, and reports unreached families. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KP9rt5rVQ9jDMpZp2xBy3K
…ches everyone - Event-tied reminders (appointments, calendar events, moment "leave by") are never moved to the next family 09:00. The scan drops any draft whose card has expired, and any event-tied reminder draft whose time has passed, so nothing is created, stored or announced for it. Accepting such a card returns 'changed'. Renewal, insurance and refill reminders (and legacy cards with no source) keep the next-09:00 rule (reminderIsoFor). - The cron start moves along a van der Corput sequence instead of one family a day. No per-family last-scan signal exists, and this needs no estimate of how many families a run reaches: every family is reached within 4*N/C runs (40 for 5,000 families at 500 a run, instead of about 4,500). The start cut-off is now 40 s, and a family is waited on for at most 12 s, so the pass returns inside maxDuration 60. - A clash involving a feed-imported event is pushed without its titles, and imported titles go through sanitizeUntrusted before they reach a card. - A manager's own 'approved' policy claim with no policy behind it, older than two minutes, can be accepted again. It is fenced on the claim time, so two retries still write one policy. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KP9rt5rVQ9jDMpZp2xBy3K
…p side)
- Roster editor: only a parent may grant/change/remove 'parent'
(assignableMemberRoles, canRemoveMember); invite form refuses an invite
role above the sender (canInviteWithRole).
- Removal paths (family-module, settings-module, admin) now write
REMOVED_MEMBER_PATCH {is_active:false, role:'guest'} so accept_invite's
reactivation cannot restore an old parent/adult role.
- /api/email/invite: manager-only, pending + unexpired invites only, and no
parent invite mailed for a non-parent.
- Onboarding finalize: adopt branch requires the progress marker's family,
a parent role and created_by = caller; invite emails capped at 5 and
rate-limited per user and per family; child logins cannot claim a family.
- Child logins: sign-in refuses an inactive linked member; ensureActiveFamily
and onboarding never provision a family for a child login; create refuses
manager targets; link is CAS on user_id IS NULL and rollback unlinks only
its own user.
- Provider-sync cron disables connections whose owner is no longer an active
member instead of syncing them.
- scopeForApprovedWork ignores an inactive asker.
DB-side fixes (fm_update/invites restrictive policies, family_id freeze,
accept_invite ON CONFLICT role, sync/child-login revocation triggers) are
reported separately, not written here.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KP9rt5rVQ9jDMpZp2xBy3K
…nce and whole - closeAccountAction ends every billable Stripe subscription at period end before setting closed_at, refuses the close when a cancel fails or billing is not configured, and resumes what it cancelled if the close does not land. - push-scan, weekly-digest, provider-sync and wallet-allowance skip families with closed_at set (inner-joined in the query for the bounded reads). - commitImport claims a client-minted submission id through rate_limit_hit before reading, so concurrent retries of one commit write the file once; the wizard holds the id across an unanswered retry. - A failed chore_assignments insert takes back the chores just created, so the re-run recreates them with their owners. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KP9rt5rVQ9jDMpZp2xBy3K
…eview follow-ups - Removal: family-module and settings-module now remove through a new removeFamilyMemberAction (app/(app)/family/member-actions.ts). The write still goes through the caller's session, so fm_update and the canRemoveMember check apply. That action and adminRemoveMemberAction then call revokeRemovedChildLogin (lib/server/child-account.ts), which bans a removed member's child-login account with the service role (updateUserById ban_duration). The removal stands if the ban fails; the failure is logged and returned, and the screen shows familyModule.removedButLoginStillActive. A lookup that cannot be answered is reported as failed and bans nobody, because a ban is global. - Onboarding: finalizeOnboardingAction returns unsentInvites (past the cap, refused by a limiter, or rejected or skipped by the mail provider). The Done screen lists them with a Send button that posts to /api/email/invite. The ONBOARDING_INVITE_EMAIL_CAP comment no longer claims Settings re-sends them. - createChildLoginAction: reactivating a removed local child restores role 'child' instead of leaving the 'guest' that REMOVED_MEMBER_PATCH set. - loadSyncExecutionPolicy refuses (ownerUnavailable) an account whose owner is not an active member of its family. That covers every runProviderSync/runGoogleSync caller, so the cron comment is now true. - i18n: the new keys are in en-US and all six base catalogues. en-GB, es-MX, es-US and fr-CA are declared empty placeholder overlays (PLACEHOLDER_LOCALES, enforced by an-empty-locale-says-it-is-a-placeholder.test.ts), so they inherit the keys rather than carry them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KP9rt5rVQ9jDMpZp2xBy3K
- notifications, chore-reminders, calendar-feeds, family-routines (due and
arming reads), autopilot-scan, model-refresh, library-feeds and
guardian-learning skip families with closed_at set: .is('closed_at', null)
on direct families reads, an inner join on families with
.is('family.closed_at', null) on bounded per-row reads.
- The shared delivery paths the notifications and push-scan crons call do the
same: push dispatch, the email digest, morning briefs and AI approval
reminders. So do the contact-center urgent drain, the scheduled social
publisher, network aggregation (whose prune then drops a closed family's
contribution) and scheduled marketing automations.
- ai-runs claims through SQL that cannot be changed without a migration, so a
claimed run of a closed family goes back to the queue a day out instead of
executing; it is not cancelled, so reopening resumes it.
- autopilot-scan's entitlement check already resolves a closed family to level
0, but that skips it only while Autopilot needs a paid tier; the filter
covers a 'free' tier too.
- reopenAccountAction resumes the subscriptions the close cancelled: closing
did not record them, so a subscription counts when Stripe's canceled_at
falls within the close. Anything else still set to end, or a Stripe that
cannot be reached, reopens the account and tells the family to resume it
from billing.
- A refused close whose rollback could not resume a subscription says so in
the error the family reads, not only in the log.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KP9rt5rVQ9jDMpZp2xBy3K
- Webhook: an unknown price no longer blocks a status that ends access (canceled/unpaid lands on the recorded row, plan kept, admin alerted); a second live subscription for a family is flagged instead of overwriting the recorded one; multi-row families are read without maybeSingle failing every retry. - Checkout and change-plan share createSubscriptionCheckout: Stripe-side live-subscription check (change-plan now has it), the service fee on both paths, reuse/expiry of the family's open session, and an idempotency key on the create. - portal/cancel/change-plan/checkout require the AAL2 step-up their page enforces; billing-module, upgrade-modal and the trial paywall route a stepUp refusal through reportRefusal. The paywall opens the portal on a 409 instead of looping into the gated billing page. - Server-side gates honour the trial lock: resolveFamilyEntitlement, resolveFeatureEntitlement 'locked', refuseUnlessEntitled and assertAIAccess answer trial_expired / account_closed. - past_due keeps its paid level while Stripe retries the renewal. - resolveEntitlement returns the fail-open fallback on any read error instead of computing from partial data. - Fridge Chef carries the Smart Kitchen gate on its page and route and records its vision call against the AI allowance. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KP9rt5rVQ9jDMpZp2xBy3K
…ily from context - revokeRemovedChildLogin now records its ban in the auth user's app_metadata (removed_child_ban: true), and leaves alone an account that is already banned without that marker (an admin's ban). - New liftRemovedChildBan lifts a ban (ban_duration 'none') only for this member's own child login (child_logins row for family, member and user) and only when the marker is set. adminSetUserBanAction clears the marker on every ban and unban, so an admin decision is never undone by a re-add. - createChildLoginAction re-adds a REMOVED, non-manager member who still holds this family's child login, instead of refusing with "already has a login". It reactivates the row as 'child' (compare-and-set on user_id and is_active = false), lifts the removal ban, then sets the new PIN through resetChildPinAction. Before this, nothing could bring such a child back, because removal leaves user_id in place. No other path reactivates a member: the other is_active: true writes on family_members only insert or upsert the caller's own parent row. - removeFamilyMemberAction takes the family from the authenticated context rather than from the caller. server-action-authorization.test.ts flagged the caller-supplied family id that b108d66 introduced. - Tests: re-adding lifts only the marked removal ban and restores an active child; an admin ban placed before or after the removal is never lifted; an account without this member's child login is neither unbanned nor relinked. Two guards that already failed at 400dcb6 are fixed: the PRIV-001 sensitive-reach baseline now records child_logins, read through lib/server/child-account.ts with ids only; and an indexOf ordering assert uses at(). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KP9rt5rVQ9jDMpZp2xBy3K
…d alerts - Checkout reuse reads tracked sessions marked 'abandoned' as well as 'pending' (24h window): the cron marks them after an hour but Stripe keeps them payable for 24h, so they were neither reused nor expired. Stripe's status === 'open' still decides. - A tracked session the current Stripe key does not know (resource_missing, e.g. after a key switch) is skipped instead of failing checkout and change-plan with 500. An expire that fails on a session Stripe confirms is no longer open counts as closed. Any other failure still refuses: the session may be open and payable, and creating another beside it is a double charge. - An open session is reused only when its line item is the requested price, not just the same plan key. - past_due keeps its paid level for PAST_DUE_GRACE_DAYS (14) after the renewal that failed. Stripe advances the period on renewal, so that renewal is one plan interval before current_period_end. A lapsed family's paywall checkout answers 409 and opens the billing portal. - Unknown-price alerts are deduped per subscription and status across Stripe retries; the revoke path sends the churn alert the normal path does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KP9rt5rVQ9jDMpZp2xBy3K
… findings - Hand-off: completeHandoffAction goes through loadOrderRole and refuses a non-party, so a sibling who reads the family-visible confirm_code can no longer complete someone else's pickup or close a loan. A child or teen cannot propose or confirm a meeting with another household's member. - Auctions: bids and Buy-It-Now are for parents and adults only; the household that already leads is refused instead of being bid up against itself; Buy-It-Now closes at the first bid (action refuses, panel hides it). - Negotiations and circles: a child or teen cannot accept a cross-family offer, or create or join a circle. - Invest: a child places orders against their own wallet only. - Add funds: refuses a wallet missing a bucket the split would credit, instead of writing completed credits with bucket_id NULL. - Gifts: the gift-waiting notice goes to managers without the giver's name, and /wallet/gift loads pending gifts' name and message for managers only. - close-auctions cron: a scope or notify throw is a counted failure, not a 500 mid-batch; notices are sent once per order/listing and auctions settled in the last 7 days are re-offered, so a lost winner notice goes out later. The SQL halves (RPC party and role checks, proxy-bid leader and reserve rules, BIN in the RPC, buyer_family_id on orders, circle removal and code rotation, circle column exposure, spendable balance counting processing holds, invest_orders RLS) need migrations and are not in this change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KP9rt5rVQ9jDMpZp2xBy3K
… notice re-check reaches every settlement - Negotiations: makeOfferAction and the counter branch of respondToOfferAction refuse a child or teen, like accept. The other household's adult accepting either one creates a confirmed order with the child as buyer or seller. Every negotiation is between two families (0187's distinct_families check), so the accept gate no longer reads the thread for a same-family case that cannot exist, and the comment claiming one is corrected. Decline and withdraw stay open to every party. - Circles: shareListingAction is for parents and adults too, as create and join already were; it puts the listing in front of other households who can bid, buy and arrange a pickup. Unshare and leave stay open. - close-auctions: the notice re-check pages through the 7-day window oldest first (200 per page, at most 25 pages) instead of reading the 50 newest, so an older lost notice is reached. A family whose notice scope is null is logged and counted as `skipped`, not as a failure, so it no longer makes every run return 502 for the whole window; a failed write or a throw still does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KP9rt5rVQ9jDMpZp2xBy3K
…rets, audit config - Every admin gate (assertSuperAdmin, superAdminGate, requireMarketingAdmin, the /admin layout, the /admin action guards, /api/admin exports) now refuses a password-only session of a super-admin with an enrolled authenticator, and fails closed when the assurance level cannot be read (lib/auth/super-admin-assurance.ts). - adminSetUserBanAction refuses to ban a built-in/env or DB super-admin. - saveStripeSettingsAction checks its read error and never writes blank secrets, so a fee change cannot null the stored keys. - /admin/admins revoke/deactivate also remove the super_admins grant (same immutability/self-lockout rules); every roster change is audited. - adminCreateUserAction audits the account before the family join. - Feature-tier set/reset and AI config saves are audited with an actor; the tier save is optimistic on updated_at so a concurrent save is not lost. - Marketplace report resolve refuses a closed report before withdrawing, puts the listing back if the report did not move, and audits withdrawals. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KP9rt5rVQ9jDMpZp2xBy3K
… through the circle view, on both schemas The proposed economy SQL (not applied) adds marketplace_orders.buyer_family_id and replaces the whole-row circle read on marketplace_listings with the view marketplace_circle_listings. The app now works before and after it: - Order reads that were scoped to family_id alone (loadOrderRole, the orders page, setOrderStatusAction, leaveReviewAction, the marketplace home and the trust score card) read family_id OR buyer_family_id through readFamilyOrders, and fall back to the family_id read on 42703/PGRST204. The status update is scoped to the order's own family_id; the pickup calendar event goes on the confirming party's calendar. - The auctions board, community feed, auction panel and the placeBid/buyNow pre-reads read other families' listings from the view (columns checked against what it exposes) and fall back to the table read on PGRST205/42P01. Same-family reads stay on the table. Order titles for won listings come from the view. - Missing-object recognition reuses isMissingSchemaObject from lib/messages/schema-compat.ts, with one warning per object. - New RPC refusals: manager_only maps to actions.onlyAParentGuardianCan16, already_leading to a new key actions.yourMaxAlreadyCoversThat (all 7 full catalogues), and buy-now's not_available/bids_placed to actions.buyItNowIsnT. placeBid keeps refusing the leading household until the SQL lands. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KP9rt5rVQ9jDMpZp2xBy3K
… for step-up audit_logs.resource_id is a uuid, so every audit row keyed by a setting key, an email or 'singleton' was refused by Postgres and only logged to the console. recordAudit/logAudit now move a non-uuid resourceId into metadata.resource_key and send null, and the call sites that did this (tier-features set/reset, AI config, roster invite and grant removal, super-admin grant/revoke, Stripe settings, feature flags) pass null or the real row id with the key in metadata. - The super-admin step-up also gates the feedback status write, the onboarding-funnel and journeys dashboards, and /admin/onboarding's service-role read. - The step-up page asks only for the signed-in user on the way into /admin, so it never provisions a family for an admin. - Activating a super_administrator roster entry restores the super_admins grant that deactivate removed (audited). - A listing that cannot be restored after a lost report resolve has its withdrawal audited; the tier-save conflict and the step-up refusal of tier/AI saves are translated. - The test fake refuses a non-uuid resource_id the way Postgres does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KP9rt5rVQ9jDMpZp2xBy3K
…ntact through the hourly cap Review fixes on 3aa0979 (the Guardian/Locator audit commit). - lib/location/retention.ts: a failed step is described with describeActionError instead of repeating the raw Postgres message; the "no raw .message crossing the response boundary" invariant failed on it. - lib/guardian/sms-processing.ts, app/api/guardian/inbound/whatsapp/route.ts: the 10-per-hour per-sender cap held every trust level, so a trusted family contact's eleventh text in an hour was written `blocked` and never announced, and a retry completed against that. Finding 6 targeted blocked, spam and unknown senders and the model call. Past the per-sender cap a contact at a ring-through trust level (immediate_family, close_family, trusted_friend) now loses only the model call, never the delivery; the per-family cap still holds for everyone. - lib/guardian/sms-processing.ts: the SMS-lane emergency escalation runs under an explicit 30 s step budget instead of smsStep's 5 s default (two Twilio requests per manager, 15 s deadlines each), so a timeout no longer answers 503 while the alerts keep going out detached. - app/api/guardian/screen/route.ts: the emergency fan-out runs in Next's `after()` once the TwiML has gone out (Twilio's 15 s limit), with the inline path kept where `after()` throws outside a request scope. Tests: a trusted contact's eleventh SMS and WhatsApp is still announced for each of the three levels; a known contact's and an unknown sender's eleventh is still held; the family cap still holds for a trusted contact; the screening route answers before the fan-out and the fan-out still runs. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KP9rt5rVQ9jDMpZp2xBy3K
Four held-lane workflows (kid-progress-award, invite-rejoin-role,
guest-household, reward-snapshot) are the owner's negative controls for the
held 0496, 0495, 0498 and 0500: each replays every runnable migration,
requires its held probe to FAIL on the released schema with named breach
lines, applies the held file twice and requires the probe to pass. On this
branch all four died at step 1 ("the held probe failed on the released
schema, but not on: ..."): 0481's kid_progress guard, calendar_feeds guest
guard and reward_redemption_request_shape_guard, and 0482's accept_invite
rewrite, each closed the hole the held file exists to close, so the probe
failed on the wrong error. Decision: the released schema behaves exactly as
main does for those four subjects, each deferred to its held migration.
0481 the kid_progress section (kid_progress_write_is_a_managers(),
trg_kid_progress_manager_write, the three kid_progress_manager_*_guard
policies), the calendar_feeds guest trigger and
reward_redemption_request_shape_guard() with its trigger are removed;
the header records the three deferrals (0496, 0498, 0500 and their
workflows). rls-sweep-check drops those three tables' fixtures and 17
assertions (148 -> 131, all held). reward-redemption-decision-check's
trigger inventory goes back to the four on main.
tests/a-manager-gated-table-is-manager-gated-on-screen.test.ts drops
the kid_progress pin a399516 added.
0482 section C (accept_invite) is removed; A1/A2, B1-B3, D1/D2 and the
self-check stay. 0136's function is the released one; the returning
member's role is 0495's, and the inviter-still-active re-check is a
follow-up after 0495 lands. The probe drops C01-C03 (36/15/20 ->
33/14/18) and moves every fixed uuid into its own 0482000x-...
namespace, which tests/boundary-probes-are-rerunnable.test.ts asked
for (a0000000-/c0000000- ids are also seeded by committing probes).
0484 the DELETE that collapsed duplicate vacation_documents pairs is
replaced, at the coordinator's request, by a fail-closed preflight:
vacation_documents_duplicates_refuse() raises 23505 naming the count
and a sample of the duplicated pairs and deletes nothing; the unique
index is built only when it passes; a second run on a clean schema
succeeds and the self-check also pins the function. New probe
docs/audit/a-trip-links-a-document-once-check.sql proves the refusal
leaves both seeded duplicates in place and that on the released schema
the index exists, a second identical link is 23505 and NULL
document_id rows coexist.
tests tests/a-client-write-reads-what-it-changed.test.ts pins the reminders
helper's family-scoped update; tests/reminder-provenance-ui.test.ts's
fake builder now chains .eq() so the module's .eq('id').eq('family_id')
resolves. docs/PENDING_PROD_MIGRATIONS.md and docs/audit/README.md
record the deferrals, the 0484 refusal and the new probe.
Measured on a throwaway PG16 (initdb as ubuntu, port 54621): 460/460
migrations apply; db:audit:migrations passes (next free 0486); probes
188/188 (0 skipped, 0 failed), rls-sweep 131/131, 0482 probe 14 refused /
18 landed, gated-write 173 tables agree; 0481, 0482 and 0484 re-apply
cleanly. All four held workflows reproduced: each probe fails on the
released schema with every line its yml greps for, each held migration
applies twice, each probe then prints its OK line. vitest: the 37 files
touching these probes, migrations and tests (1404 tests) pass, the three
CI failures included; eslint clean on the changed tests.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KP9rt5rVQ9jDMpZp2xBy3K
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KP9rt5rVQ9jDMpZp2xBy3K
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KP9rt5rVQ9jDMpZp2xBy3K
|
Additional scope hold at2f0dd17da1c0ae4c095a5d95f84309c78e824266, Guardian/Locator lane: vercel.json and scripts/cron-dispatch.mjs now schedule /api/cron/location-retention daily03:40. The handler deletes older location_events and clears coordinates. Source inspection confirms this candidate wiring; no activation or execution is asserted. Bounded next task for this lane's owner: keep the destructive retention sweep unscheduled and outside the deployable candidate pending explicit production-configuration/data-retention authorization. Preserve source/test evidence in a held, non-running lane without removing historical IDs. Do not execute the endpoint, trigger a deployment, run cleanup against real data, or infer authorization from the broader source-audit task. Report exact commit and disposable-test evidence separately. The migration-assembly scope hold6100395136 remains independent and pending. |
|
Guardian/Locator source review at2f0dd17, separate from retention scope-hold6101009975. Bounded repair for this lane's existing owner, source plus synthetic tests only:
These are static findings, not reports of production incidents. Keep provider acceptance distinct from handset delivery. Do not call real recipients, activate schedules, alter production settings, or apply SQL. Preserve existing task IDs, source evidence and authorization/role checks; report exact commits and synthetic test results. |
…retention policy The child-safety audit fix (3aa0979, merged in b3697b7) added app/api/cron/location-retention — a sweep that deletes location_events older than a window and clears coordinates — and scheduled it daily at 03:40 in both vercel.json and scripts/cron-dispatch.mjs. A destructive retention sweep must not run from the deployable candidate until the owner has set a retention policy, so this takes it off both schedulers. - vercel.json: drop the location-retention entry only; the other 28 entries are byte-identical and the JSON still parses. - scripts/cron-dispatch.mjs: drop the SCHEDULES row and leave a comment saying the route is held, why, and where the hold is pinned. - The route, its handler (enforceLocationRetention) and lib/location/ retention.ts are unchanged in behaviour: it stays reachable only with CRON_SECRET, by hand. - tests/cron-schedule-registration.test.ts: the 1:1 disk<->schedule rule stays total; it gains an explicit HELD_UNSCHEDULED list naming /api/cron/location-retention as "held, unscheduled pending a retention decision", and a new test that checks the hold in both directions (the route must still exist and must be in neither scheduler, with a reason naming the pending decision) so the list cannot go stale or become a licence. - tests/a-removed-member-takes-their-location-with-them.test.ts: the "is scheduled, in both places" pin now asserts the hold honestly — absent from vercel.json and the dispatcher table, handler still wired. - lib/location/retention.ts and lib/realtime/published-tables.ts: one line each noting the cron is unscheduled pending the owner's retention decision (and the JSDoc no longer calls it "the scheduled sweep"). docs/PENDING_PROD_MIGRATIONS.md has no decisions section, so no row was added there. Validated: vercel.json parses (28 crons, location-retention absent); node --check scripts/cron-dispatch.mjs; `--dry-run --at 03:40Z` no longer lists the route; eslint --no-ignore clean on all changed .ts/.mjs; vitest 20 files / 224 tests passed (schedule registration, removed-member retention, every test touching location-retention or lib/location, and the cron-dispatch/mirror/catch-up/auth/fan-out/failed-run/secrets suites). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KP9rt5rVQ9jDMpZp2xBy3K
|
Re 6101009975 (retention sweep) and 6101029395 (Guardian/Locator source review):
Generated by Claude Code |
|
Source receipt for6f2853786fd00e1651c5f4095fecfc2de83593d9 / ACK6101072439: removal from both automatic schedules satisfies the unscheduled portion of6101009975. The deployment-scope portion remains open: app/api/cron/location-retention/route.ts is unchanged from2f0 and still directly invokes the destructive sweep on an authorized GET. Tests requiring that route to stay wired establish only the weaker unscheduled condition. Please finish the same bounded hold by excluding the executable endpoint from the deployable candidate and retaining the implementation/evidence in a non-running held source/test location. Preserve history and do not activate, invoke or change production credentials/configuration. Keep the ongoing separate Guardian source/test repair with its current owner; no duplicate assignment is requested. Migration-assembly authorization remains a separate pending question. |
6f28537 took the location-retention sweep off both schedulers, but the executable endpoint app/api/cron/location-retention/route.ts was still in the deployable candidate and ran the destructive sweep (delete old location_events, clear coordinates) on any authorized GET. This finishes the hold: the route file moves, with history (git mv), to held/api/cron/location-retention/route.ts. Next does not route held/, so no deployment exposes it. lib/location/retention.ts and every retention test are unchanged in behaviour. - held/api/cron/location-retention/route.ts: the handler, byte-for-byte, with a header saying why it sits outside app/ and how to restore it (move back, re-add the two schedules, drop the hold from the test). - tests/cron-schedule-registration.test.ts: the disk<->vercel.json 1:1 rule is exact again (no exception needed: a held route is not under app/). HELD_UNSCHEDULED now records where each held route sits and why, and the hold test checks both directions: absent from app/api/cron, present at the held path (which must be under held/), in neither vercel.json nor the dispatcher table, still gated by hasCronAuthorization, reason naming the pending decision. - tests/a-removed-member-takes-their-location-with-them.test.ts: the route pin asserts app/api/cron/location-retention/route.ts no longer exists and reads the held file, which must still wire enforceLocationRetention(createServiceClient()). - scripts/cron-dispatch.mjs, lib/location/retention.ts, lib/realtime/published-tables.ts: the notes from 6f28537 now say the route is held under held/ and not deployed. - package.json: `held` added to the lint dirs. tsconfig already includes **/*.ts, so tsc typechecks held/; linting it too keeps the two consistent (eslint is clean on the held file). Scanner tests were checked for held/: hardcoded-locales uses git ls-files over app/components/lib; shared-secrets walks app+lib and the route reads no *SECRET itself; .tsx-only walkers skip a .ts route; no catalogue test requires every key to be used, so the route's locationRetention.* keys staying in lib/i18n/messages is fine. Validated: vercel.json parses (28 crons, route absent); node --check scripts/cron-dispatch.mjs; eslint --no-ignore clean on every changed .ts/.mjs including the held file; vitest 36 files / 559 tests passed (the 20 from 6f28537 plus both held/-referencing tests and the route-scanning suites: middleware-public-api-boundary, shared-secrets, cron-auth, every-key-a-t-call-names, catalogue-key-rendered-through-t, i18n-catalogue-integrity, notification-write-boundary, the unconfirmed-write and route-write ratchets, whole-table-reads, a-closed-family-is-left-alone-by-the-crons, robots coverage, ai-route-rate-limit, a-filtered-delete, vault, in-filter); NODE_OPTIONS=--max-old-space-size=6144 npx tsc --noEmit -p . exit 0. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KP9rt5rVQ9jDMpZp2xBy3K
|
Re 6101084415: done in Generated by Claude Code |
|
Merged as
The repo's own CI on Generated by Claude Code |
…removal cleanup, model cap) The four Guardian findings raised on #989's thread, each confirmed by two skeptics and fixed with negative controls: - An emergency nobody could reach is retried by a bounded sweep instead of being marked complete after a transient transport failure; a family with no reachable manager is recorded once as unreachable, not retried forever. - The step deadline and request cancellation reach the escalation and its Twilio sends; managers are reached in parallel. - A removal whose location forget failed says so on every removal surface. - The inbound model cap fails closed when its count cannot be read, the WhatsApp lane counts after recording, and both lanes and the screening lookup re-check active membership. Conflicts in lib/location/retention.ts and its test resolved keeping main's hold of the retention sweep alongside the branch's removed-member repair. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KP9rt5rVQ9jDMpZp2xBy3K
#989 fixes the same defects as this branch's sign-in, ensureActiveFamily, sync-policy, invite-email and onboarding changes, so main's code is kept for all of them and this branch's versions, the duplicate tests and the unused locale key are dropped. What stays is the requireUserContext redirect of a kid login with no family to /kid-login, and its test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DXw2nu25BjyRfA6Fg3YiMS
…iews Main's #989 released 0477-0485. 0481 already narrows tax_documents (plus a teen's or child's own), health_visits and immunizations for caregivers and guests; 0482 already refuses a kid login's families_insert by a child_logins row or the synthetic domain. Every held workflow was re-run against the new replay; three changed and are rebaselined here. 0505 (review 6101040142). Remove no longer deletes the photo before it knows the outcome: lib/marketplace/remove-listing.ts deletes the row first, reads back the removed row's photo_url, and deletes that photo only once the row is gone. On 0505's refusal it always asks 0317's locked status change to withdraw and reads "Cannot move listing from withdrawn/ completed" as already kept, so a stale screen or another tab's relist is decided from the row as it is now. Behavioural tests drive refusal, failed withdrawal, already-kept, relist, filtered delete and failed photo cleanup; no photo is lost while a listing is kept. The service-role, null-uid and family-deletion paths are recorded as limits, not a preservation guarantee. 0506 (review 6101355094). Who may log is separated from whose history is shown (lib/health/record-scope.ts): anyone still logs a night, check-in or meal for any member; a manager or the member sees the whole record and its summaries; anyone else sees only what they logged, labelled, with no summary built from part of a record. Same before and after release. The author term is labelled a policy exception needing owner confirmation. Released baseline: teen and child 8 of 8 tables, caregiver and guest 6. 0507 recut. 0482 closed most of it; what remains is the server's app_metadata kid-login mark, which 0495 reads and 0482 does not. 0507 now widens 0482's is_child_login_account() by that test instead of adding a trigger. The service-role provisioning path is recorded as the membership lane's (review 6101404893). 0508 (review 6101849884). On top of 0481 the rows lose the teen/child self-read (the owner's "managers only"), and tax files now answer to the rows' step-up: an enrolled manager at aal1 is withheld the files too. The probe adds enrolled-manager aal1/aal2 controls on existing and new paths, a step-up mutation, and says plainly that no signed URL or download is exercised. rls-sweep-check (main's 0481 probe) reads which rule is installed for the child's own tax document. Checks: all 19 held workflows simulated on the new replay; 188 of 188 released probes; the stack through 0508 applied twice with every held probe and 188 released probes passing; full vitest passes but for the two Node-22 environment tests; tsc, eslint and the i18n gate clean. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KRUgA6hD6QgzmtpSP6TUmP
Main (#989) independently added AssistantCtx.role, for the goals service, and widened isSensitiveMemory's stems. Resolved: - AssistantCtx keeps main's single role field; its doc now also names list_pending_decisions, which reads it here. - Call sites and test contexts take main's spelling (role: ... ?? null). - The assistant engine passes the caller's real role (acting?.role ?? input.role ?? null), not scope.role, which falls back to 'adult' when no role is known: a role nobody has is never a manager, and main's own doc for the field says an unknown role is refused. The source pin follows. - isSensitiveMemory keeps main's regex, plus the forms it still missed: "prescribed" (prescrib\w*), plural "pins" and "passports". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01776xJyhVe8xJQvTGeNfT9T
…mports CI on 964180c (merged with main) failed tests/medications-fixture-module- graph.test.ts: tests/e2e/notification-email-preference.spec.ts, added on main by #989, stubs '@/lib/supabase/errors' with describeDbError and wroteNoRows only, and components/ui/input.tsx (Field) imports localizeDbErrorText from it on this branch. The stub now returns the text unchanged; that fixture is not about language. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TRY21ZKsFrfB3qtoP972A4
#989's money-writes fix landed the same invest-order check this branch made (a non-manager orders only on their own wallet), answering "child wallet not found". Main's action code is kept; this branch's test now pins main's answer, and keeps the Invest page offering Buy / Sell / Request only on cards the viewer may order on, and the allowance/goal same-family check, which main does not carry. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DXw2nu25BjyRfA6Fg3YiMS
Main's #989 already skips closed families in every sending cron and at push and email delivery, so this branch's own closed-family change in the five senders is dropped for main's code. Of its test, only the weekly digest case is kept (a-closed-family-gets-no-weekly-digest), which main's closed-family suite does not drive. That suite's mock of automation-steps now spreads the real module, since the runners also read a retried run's log and count sends through it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DXw2nu25BjyRfA6Fg3YiMS
#989 landed the same own-wallet check for invest orders and answers a sibling's wallet as not found. The reconciling merge kept main's code but left this test's expected error behind; it now asserts main's answer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DXw2nu25BjyRfA6Fg3YiMS
#989 landed the same server-side lock for closed and trial-ended families: resolveFamilyEntitlement, lockedEntitlementCode, and the refusal in assertAIAccess, resolveFeatureEntitlement and refuseUnlessEntitled (codes trial_expired / account_closed). Main's code is kept for all of it; this branch's own version of those gates is dropped. What stays from this branch, on main's primitives: - requireFeature never refuses /dashboard/billing for a locked family (no billing-to-billing redirect) and sends a closed family to plain billing rather than the upgrade view; - /api/ai/chat runs the assistant's feature gate and assertAIAccess; - lib/server/account-standing.ts refuseClosedOrTrialEnded, on the ten ungated Free-tier AI/voice routes, and the gift helper on the gift link's family. Pantry chef is dropped: main gates it as Smart Kitchen. Tests are aligned to main's codes; the route tests warm their imports. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DXw2nu25BjyRfA6Fg3YiMS
Main now refuses a closed or trial-ended family in assertAIAccess, resolveFeatureEntitlement and refuseUnlessEntitled (#989 and later), so the branch's versions of those, its requireFeature billing redirect and their two tests are dropped in favour of main's. Still needed and kept: /api/ai/chat runs refuseUnlessEntitled and assertAIAccess like /api/ai, and the Free-tier AI helpers (habits, journal, notes, relationship, voice, the gift page) refuse a closed or trial-ended family via lib/server/account-standing.ts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DXw2nu25BjyRfA6Fg3YiMS
Summary
Integrates the audit fix branches produced by the finalaudit.md audit programme, each one the result of a five-lens find, two-skeptic verify, worktree fix and independent adversarial review (with re-fix rounds where the reviewer blocked). Base is
mainthrough #995 and #991 (the whole #980–#995 lane merged).Areas (one merge commit each)
buyer_family_idwith fallback; circle reads via view with fallbackDatabase
Migrations 0477–0485, assembled from the audit's tested proposals and verified on a fresh PG16 replay (460 migrations, 188/188 probes, idempotent second apply, ledger audit
next 0486, typecheck clean; CI's Database job green):kid_progress, thecalendar_feedsguest guard and the reward request shape guard are deferred to the held 0496, 0498 and 0500, whose negative-control workflows must keep seeing those defects until the held files landaccept_inviterewrite is deferred to the held 0495; the inviter-still-active re-check is a follow-up once 0495 landsReserved numbers 0488/0490/0492–0503 are untouched and all eleven held-lane negative-control workflows reproduce as their YAML specifies. Nothing is applied by merging: the production-migrations workflow only verifies on
main; applying needs the manualapply=truedispatch. Apply order: the app halves in this PR must be deployed before 0483 is applied (its circle-feed view replaces the dropped read policy);docs/PENDING_PROD_MIGRATIONS.mdrecords this. Two rollout notes: 0482's last-parent rule now also binds the service role; 0483 narrowsgift_paymentsreads for children to completed gifts on their own wallet.Integration repairs
Confirmed writes the audit branches left unconfirmed; fixture mocks for the merged admin step-up, open-family reads and the ban marker; the recall slice fenced and registered; new copy lifted into the catalogues; e2e fixture source lists; the marketplace rail's Trust Score reads through a server action instead of probing the database from the browser, and the notification-email and sign-out fixture loaders resolve the modules the merged views import (the module-graph guard now catches both drifts). The budget duplicate-cleanup outcome lands the requested amount on the canonical row whatever the delete did. Every member-removal path (family screen, settings, admin) does the membership steps and the location forget.
mainis merged in through #991 (one conflict inchild-login-actions.ts, resolved keeping #981's kid-identity and PIN-reset provenance guards alongside this PR's relink CAS, role restore and ban marker; four in the guardian merge, resolved keeping both sides).Validation
Each branch passed
tsc,eslint --no-ignoreand its related vitest files inTZ=UTC,America/Los_AngelesandAsia/Tokyobefore integration; the integrated head8bcf71223passed the full vitest suite locally inTZ=UTC, and the carve-out commit re-ran the full suite (2,087 files; only the two Node-22-only checks fail locally). After each later merge the merge-sensitive files were re-run inTZ=UTC. CI on the previous head had E2E, Database replay and every other job green except the four held-lane controls and three unit tests, all addressed in113eb3b90.Not in this PR
main; applying needs the manualapply=truedispatch, after the rejected service-role key is replaced.guardian_member_profiles,guardian_contacts,guardian_routing_rules) and a database-side location-forget trigger: recorded as proposals for the owner.🤖 Generated with Claude Code
https://claude.ai/code/session_01KP9rt5rVQ9jDMpZp2xBy3K