Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
107 commits
Select commit Hold shift + click to select a range
4edd272
Three boundaries proven by running the database, not reading it
claude Sep 27, 2026
0bfd1af
A feature that could only ever be refused, and the check that found it
claude Sep 27, 2026
6245805
Two rules the code kept and the table did not
claude Sep 27, 2026
46d7ce6
The probe was watching the dead feature, and CI could not see it
claude Sep 27, 2026
9a74b38
Keep the vote-owner probe measuring ownership, not the single-choice …
claude Sep 27, 2026
effba0b
A deleted family leaves nothing, and the probe that proves it
claude Sep 27, 2026
7f49514
A guard that stripped comments before looking for comment markers
claude Sep 27, 2026
c1a9390
SEC-001, proven: no credentials, HTTP 200, the bytes
claude Sep 27, 2026
1e16efd
Two probes were living in the family everything else is measured against
claude Sep 27, 2026
dfed345
What else does the app call that nothing exercises?
claude Sep 27, 2026
8600baa
An HMAC keyed on the empty string is computable by anyone
claude Sep 27, 2026
2029ff9
Guard the admin action surface against an ungated 139th action (ADMIN…
claude Sep 27, 2026
080a993
Close four open Pass-E findings: F-E04, F-E07, F-E08, F-E09
claude Sep 27, 2026
062e855
Close the feedback-attachment bucket to the internet (F-E05, SEC-012)
claude Sep 27, 2026
8e01f37
Make step-up MFA reach the data, for the three vaults it can (F-E02, …
claude Sep 27, 2026
8b6625e
Make eleven callers act on the truncation signal F-F01 raises (DATA-011)
claude Sep 27, 2026
bb2d940
Put "today" back where the family is, on the surfaces that show one (…
claude Sep 27, 2026
620c181
Sign the approval queue's proof photos in one round trip, not 240 (F-…
claude Sep 27, 2026
819736f
Let a keyboard open, use and leave the photo lightbox (F-D01, A11Y-001)
claude Sep 27, 2026
8324693
Switch on the a11y rule that was never on, and count what it cannot s…
claude Sep 27, 2026
6e53434
Make three test-infrastructure claims checkable (F-F05, F-F06, F-F12)
claude Sep 27, 2026
3c8a2d5
Give the drive-time fan-out a width, and make the ledger rule checkable
claude Sep 27, 2026
8918b15
Replace the last test whose whole argument was an absence (F-F13)
claude Sep 27, 2026
66955a4
Same family is not the same person, in four money-area actions (F-F07)
claude Sep 27, 2026
0782472
No control apologises for itself (UX-001, closes F-F10)
claude Sep 27, 2026
f9152ad
A billing write that worked once per family (DATA-014)
claude Sep 27, 2026
368729b
A page boundary decided by the query plan (DATA-015)
claude Sep 27, 2026
12010d2
The AI deny-list was checked one file deep (PRIV-001, verified healthy)
claude Sep 27, 2026
0cec603
An open redirect inside the function written to prevent one (SEC-014)
claude Sep 27, 2026
29d66e4
The refused-write guard stopped at components/ (AUTHZ-010, verified h…
claude Sep 27, 2026
ebbdfd2
Deleting a secure document made it readable by everyone (SEC-015)
claude Sep 27, 2026
05de657
One rule for ten storage removes, and a photo that was not deleted (S…
claude Sep 27, 2026
1e109b9
A proxy bid's ceiling was readable by the people bidding against it (…
claude Sep 27, 2026
2154d3c
A child could decide where a gift went, and what a goal said (SEC-017)
claude Sep 27, 2026
642040f
Guardian's screening decisions were anyone's (closes AUTHZ-005)
claude Sep 27, 2026
5fb1248
A stranger could onboard into your family as its parent (SEC-018, cri…
claude Sep 27, 2026
35505a0
The reset action laundered an unverified family pointer (SEC-018 hard…
claude Sep 27, 2026
e877195
A burst of PIN guesses got past a child's sign-in lock (SEC-019)
claude Sep 27, 2026
5b12aee
Every per-client limit was unbounded for an IPv6 client (SEC-020)
claude Sep 27, 2026
06b266d
Two retries of an abandoned AI action both ran it (SEC-021)
claude Sep 27, 2026
ca8d348
A child's PIN lockout never escalated: every lock was the first (SEC-…
claude Sep 27, 2026
9fc1d06
The lockout a child sees was English in every language (I18N-001)
claude Sep 27, 2026
093e555
A chore paid its XP every time it was approved (DATA-016)
claude Sep 27, 2026
4e933d2
Declining a gift could mark a credited gift "cancelled" (DATA-017)
claude Sep 27, 2026
9e01471
Every concierge run a parent decided stayed on "Needs your decision" …
claude Sep 27, 2026
9270b78
Pass the family zone and clock the ported chore test left out
claude Sep 27, 2026
418be20
61 server actions handed the database's own error text to the user (S…
claude Sep 27, 2026
8ca4833
Server-only functions callable with the anon key; no auction could cl…
claude Sep 27, 2026
725f41d
Merge main (#584) and move this port's migrations up one
claude Sep 27, 2026
3f70458
Bring three main tests in line with fixes ported earlier
claude Sep 27, 2026
74de5f1
Keep the marketplace working before 0397 reaches production
claude Sep 27, 2026
fc8dab3
Record the port: PORT-001 in finalaudit.md, 0389-0402 in the pending …
claude Sep 27, 2026
54dd51d
The hardcoded-string ratchet now sees template-literal messages (I18N…
claude Sep 27, 2026
763f1be
Translate the Family Wallet's template messages (I18N-002, 11 of 89)
claude Sep 27, 2026
487b841
Translate sixteen module toasts (I18N-002, 27 of 89)
claude Sep 27, 2026
69bbd82
Only the server links a login to a family member (SEC-025)
claude Sep 27, 2026
800738d
Choose a user's active family the same way everywhere (DATA-019)
claude Sep 27, 2026
fcb2c0c
End a removed member's profile visibility with their membership (PRIV…
claude Sep 27, 2026
e554c00
Leave SEC-026 to the owner, and give the owner what it reaches
claude Sep 27, 2026
d62db50
Translate 24 more module and trip toasts (I18N-002, 51 of 89)
claude Sep 27, 2026
6d119a1
Translate the AI-limit, sync, weather, medical and upload messages (I…
claude Sep 27, 2026
2dbca56
Leave the SEC-001 bucket design to the owner, and point at the ready …
claude Sep 27, 2026
11d8025
Leave PUSH-003 and EMAIL-002 to #548, which carries both
claude Sep 27, 2026
b11230a
Translate household-policy refusals, reason included (I18N-002, 77 of…
claude Sep 27, 2026
9bc5a04
Translate the calendar-sync and two admin count toasts (I18N-002, 80 …
claude Sep 27, 2026
90bead1
Localize the Guardian dashboard as a whole (I18N-002, 81 of 89)
claude Sep 27, 2026
9ca9b6a
Localize the care log's type labels (I18N-002, 82 of 89)
claude Sep 27, 2026
cb8f315
Localize move statuses (I18N-002, 83 of 89)
claude Sep 27, 2026
98a2466
Translate the last scanner-visible templates; close I18N-002
claude Sep 27, 2026
e1d7ffe
Count copy written as a template in an attribute or a JSX child (I18N…
claude Sep 27, 2026
3693c28
Make the audit header its rows' count; stop a probe granting dblink t…
claude Sep 27, 2026
8ccf008
Guard the thirteen MAIN fixes that nothing held in place
claude Sep 27, 2026
0f2ad56
Name every icon button and attach every label; document the env vars
claude Sep 27, 2026
80b02c7
Fix three guards a full run found after the last ports
claude Sep 27, 2026
fbab215
Let the body-resolve probe read a presence-guarded field as guarded
claude Sep 27, 2026
499b5fb
Name every select, and make lint fail on a warning
claude Sep 27, 2026
4a7cd1d
Make every clickable row reachable from the keyboard (MAIN-F-D06)
claude Sep 27, 2026
4b47b5f
Let only a manager take a chore off the board (MAIN-F20, 0341)
claude Sep 27, 2026
9d74e31
Stop superseded async effects overwriting newer state (MAIN-F-D09)
claude Sep 27, 2026
6725a9f
Give every signed-in page a heading (MAIN-F-D05); record I18N-004
claude Sep 27, 2026
1acd913
Translate the app's navigation (I18N-004)
claude Sep 27, 2026
1e0058d
Audit the mobile app's dependencies in CI; record the boot-policy dec…
claude Sep 27, 2026
cfe8cba
Ask before a delete in the family's language (I18N-005)
claude Sep 27, 2026
2fe2640
Translate 63 attribute templates through shared item-action keys (I18…
claude Sep 27, 2026
7582214
Translate 70 more screen-reader labels and tooltips (I18N-003)
claude Sep 27, 2026
6d94d11
Translate medical records; read a birthday as a calendar day (I18N-00…
claude Sep 27, 2026
a6daafe
Read a date column as the day it names, not a UTC instant (DATA-021)
claude Sep 27, 2026
44d4c5d
Format dates and times in the family's locale, not always US English …
claude Sep 27, 2026
e6cee2e
Translate the Insurance Hub and Trip Planner labels (I18N-003)
claude Sep 27, 2026
cf20181
See English sentence templates anywhere, and translate the first 25 (…
claude Sep 27, 2026
7371202
Translate 35 more English sentence templates (I18N-003)
claude Sep 27, 2026
24514b8
Greet by the family's hour, not the server's, in their language (DATA…
claude Sep 27, 2026
6862c4c
Translate 30 more English sentence templates (I18N-003)
claude Sep 27, 2026
f8d218d
Translate the last 27 sentence templates; guard at zero (I18N-003, TE…
claude Sep 27, 2026
5c6882c
Translate 248 page titles; drop the doubled brand (I18N-004)
claude Sep 27, 2026
6da7809
Match and render the command bar in the family's language (I18N-004)
claude Sep 27, 2026
88bd086
Fix the four CI failures the port introduced on main's guards
claude Sep 27, 2026
30c93bd
Record the finished port (PORT-001) and 0404's pending entry
claude Sep 27, 2026
57563d9
Merge main (after #583) into the port, and fix what the merge turned up
claude Sep 27, 2026
fef99d1
Merge main (after #591–#600) into the port
claude Sep 27, 2026
cfa20b7
Merge main (#603) into the port
claude Sep 27, 2026
c698de8
Merge main (through #606) into the port
claude Sep 27, 2026
a230bce
Merge main (through #601) into the port
claude Sep 27, 2026
689b477
Merge main (#608) into the port
claude Sep 27, 2026
d719f4e
Merge main (#609) into the port; drop 0446 for main's identical 0444
claude Sep 27, 2026
2cbbd04
Merge main (through #598 and #604) into the port
claude Sep 27, 2026
687fa91
Merge main into the port (no conflicts)
claude Sep 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
217 changes: 0 additions & 217 deletions $F

This file was deleted.

61 changes: 61 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,23 @@ TWILIO_ACCOUNT_SID=
TWILIO_AUTH_TOKEN=
TWILIO_PHONE_NUMBER=

# Leave this UNSET everywhere except a local machine.
#
# Every Twilio-facing webhook (Guardian inbound SMS/voice/WhatsApp, screening,
# voicemail status, escalation TwiML, and the Contact Center's voice and SMS
# callbacks) is on the middleware PUBLIC allowlist: the caller is Twilio, not a
# signed-in person, so the request signature is the only thing standing between
# those endpoints and anyone at all. lib/server/twilio-ingress.ts therefore
# verifies on every request, in every environment, and refuses with 503 when
# TWILIO_AUTH_TOKEN is absent and it cannot verify at all.
#
# Setting this to 1 turns that refusal into an acceptance — but only while
# TWILIO_AUTH_TOKEN is unset; it cannot switch off a check that can actually
# run. It exists so local work and the test suite can post unsigned callbacks.
# In a deployment it means unauthenticated writes to the guardian tables and
# outbound SMS and voice at your expense.
ALLOW_UNSIGNED_TWILIO_WEBHOOKS=

# ---- Google OAuth (Sign-in + Calendar sync) ----
# Create credentials at https://console.cloud.google.com/apis/credentials
# Authorised redirect URIs: https://your-project-ref.supabase.co/auth/v1/callback
Expand Down Expand Up @@ -306,3 +323,47 @@ APNS_ENVIRONMENT=production
# Capacitor native shells load this hosted URL (defaults to production). Point at
# a LAN dev server (e.g. http://192.168.1.20:3000) for on-device development.
CAP_SERVER_URL=https://www.bubaly.com

# ---- Contact Center: inbound family email ----
# Shared secret the inbound-parse provider presents when it POSTs a message to
# /api/contact-center/email. Send it in the `x-inbound-secret` header; a `?key=`
# query parameter is accepted only for providers that cannot set headers, and a
# secret in a URL lands in proxy and CDN logs (MAIN-F-E06). Unset, the route
# refuses every message. Setup and MX routing:
# docs/runbooks/family-contact-center-routing.md
# CONTACT_CENTER_INBOUND_SECRET=

# ---- Feedback board -> GitHub issues (optional) ----
# Mirrors ideas and bugs to a GitHub repository. A no-op unless BOTH a token and
# an "owner/repo" resolve. Token: GITHUB_TOKEN, else GITHUB_FEEDBACK_TOKEN.
# Repository: GITHUB_FEEDBACK_REPO, else GITHUB_REPO. Use a fine-grained token
# limited to Issues on that one repository.
# GITHUB_FEEDBACK_TOKEN=
# GITHUB_FEEDBACK_REPO=owner/repo
# GITHUB_TOKEN=
# GITHUB_REPO=owner/repo

# ---- X (Twitter) posting from the social studio (optional) ----
# OAuth 2.0 client from the X developer portal. Both are required, AND
# SYNC_TOKEN_KEY must be a 32-byte key (64 hex characters or 44 base64), or the
# X connection reports "setup required" instead of starting.
# X_CLIENT_ID=
# X_CLIENT_SECRET=

# ---- Apple Calendar (iCloud CalDAV) sync — off by default ----
# The provider stays dark until APPLE_SYNC_ENABLED is exactly "true" (an owner
# decision). APPLE_CALDAV_BASE_URL overrides the CalDAV host and defaults to
# https://caldav.icloud.com; only change it to point at a test server.
# APPLE_SYNC_ENABLED=true
# APPLE_CALDAV_BASE_URL=https://caldav.icloud.com

# ---- Set by the build or the test harness: do not set these by hand ----
# BUBALY_BUILD_REVISION is written by next.config.mjs from VERCEL_GIT_COMMIT_SHA
# and served by /api/build-info. NEXT_PUBLIC_BUILD_ID is an optional label the
# display error screen shows when no commit SHA is available.
# AI_PROVIDER_STUB_DIR selects a scripted-AI fixture directory for e2e runs; it
# is honoured only under tests/, and the stub never runs on a Vercel production
# deployment (VERCEL_ENV=production).
# BUBALY_BUILD_REVISION=
# NEXT_PUBLIC_BUILD_ID=
# AI_PROVIDER_STUB_DIR=tests/ai-eval/scripts
2 changes: 1 addition & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"extends": "next/core-web-vitals",
"ignorePatterns": ["mobile/**", "supabase/**", "node_modules/**", ".next/**"],
"rules": {
"jsx-a11y/label-has-associated-control": ["warn", { "depth": 3 }],
"jsx-a11y/label-has-associated-control": ["error", { "assert": "either", "depth": 4 }],
"jsx-a11y/click-events-have-key-events": "warn",
"jsx-a11y/no-static-element-interactions": "warn"
}
Expand Down
23 changes: 23 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,14 @@ jobs:
- name: Install dependencies
run: npm ci

# The web app's audit runs at `moderate` (above). The Expo app had none at
# all (MAIN-F-C10). It runs at `high` for now: on 2026-09-27 the lockfile
# carried 14 moderate advisories, all transitive through @expo/config and
# @expo/config-plugins, whose fix is a breaking Expo upgrade. None high or
# critical, so this passes today and stops anything worse arriving.
- name: Dependency audit (production dependencies)
run: npm audit --omit=dev --audit-level=high

- name: Typecheck
run: npm run typecheck

Expand Down Expand Up @@ -234,6 +242,21 @@ jobs:
- name: ON CONFLICT targets are inferable
run: node scripts/check-conflict-targets.mjs

# `plpgsql-bodies-resolve-check.sql` resolves every plpgsql body against
# the real catalogue, which needs plpgsql_check. The image does not ship
# it, and WITHOUT THIS STEP that probe skips — which is how the class it
# catches stayed invisible in the first place. The package is installed
# into the running service container; run-probes.sh fails any skip unless
# PROBES_ALLOW_SKIP=1 is set, so if this step ever stops working the suite says
# so instead of quietly running one probe fewer.
- name: Install plpgsql_check into the database container
run: |
docker exec ${{ job.services.postgres.id }} bash -c '
apt-get update -qq
apt-get install -y --no-install-recommends postgresql-16-plpgsql-check
'
psql -Atc "select 1 from pg_available_extensions where name = 'plpgsql_check'" | grep -q 1

# Each probe asserts with RAISE EXCEPTION under ON_ERROR_STOP=1. Covers
# both directions of the 0118 failure: another family reading your rows,
# and your own family losing access to them.
Expand Down
10 changes: 9 additions & 1 deletion app/(app)/admin/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -508,8 +508,16 @@ export async function adminDeleteDocumentAction(documentId: string, _storagePath
if (docError) return actionFailure(docError, t('actions.couldNotLoadThatDocument'));
if (!doc) return { ok: false, error: t('actions.documentNotFound') };

const { error: storageError } = await supabase.storage.from('documents').remove([doc.storage_path]);
// SEC-015: a refused delete and an absent object are both `error: null,
// data: []`, so the returned list is what says the object is gone.
const { data: removed, error: storageError } = await supabase.storage.from('documents').remove([doc.storage_path]);
if (storageError) return actionFailure(storageError, t('actions.couldNotRemoveTheDocument'));
if (!removed?.some((object) => object.name === doc.storage_path)) {
const listed = await supabase.storage.from('documents').list(doc.storage_path.slice(0, doc.storage_path.lastIndexOf('/')), { search: doc.storage_path.slice(doc.storage_path.lastIndexOf('/') + 1), limit: 100 });
if (listed.data?.some((object) => object.name === doc.storage_path.slice(doc.storage_path.lastIndexOf('/') + 1))) {
return { ok: false, error: t('actions.couldNotRemoveTheDocument') };
}
}

const { data: deleted, error } = await supabase
.from('documents').delete().eq('id', documentId).select('id').maybeSingle();
Expand Down
6 changes: 3 additions & 3 deletions app/(app)/admin/ai-activity/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,9 +98,9 @@ export default async function AdminAIActivityPage({ searchParams }: Params) {
</div>

<div className="grid gap-3 sm:grid-cols-3">
<SummaryTile label={`Failed · last ${SUMMARY_WINDOW_HOURS}h`} value={summary.failed} tone={summary.failed > 0 ? 'danger' : 'neutral'} />
<SummaryTile label={`In flight · last ${SUMMARY_WINDOW_HOURS}h`} value={summary.inFlight} tone="brand" />
<SummaryTile label={`Total · last ${SUMMARY_WINDOW_HOURS}h`} value={summary.total} tone="neutral" />
<SummaryTile label={t('adminAiActivity.failedLastHours', { hours: SUMMARY_WINDOW_HOURS })} value={summary.failed} tone={summary.failed > 0 ? 'danger' : 'neutral'} />
<SummaryTile label={t('adminAiActivity.inFlightLastHours', { hours: SUMMARY_WINDOW_HOURS })} value={summary.inFlight} tone="brand" />
<SummaryTile label={t('adminAiActivity.totalLastHours', { hours: SUMMARY_WINDOW_HOURS })} value={summary.total} tone="neutral" />
</div>

<SignalPrecisionCard result={precision} retryHref="/admin/ai-activity" allFamilies />
Expand Down
Loading
Loading