Push notifications for low-balance and security-finding alerts - #4771
Conversation
…ecurity-finding pushes
|
(bot) E2E evidence (iOS simulator, local stack, push sink): Low-balance happy path (full product path: org-billed kilo-auto/efficient call → gateway → schedule → worker): org balance 10.00 == threshold, one message → microdollars_used 0→4883, exactly one agent_push_sink_payload with idempotencyKey low-balance:, dataType low_balance. Opt-out on second org: balanceAlerts=false → usage crossed (4872) with zero sink lines. Security finding: POST → sent + one security-finding: sink line; repeat POST → sent, no second line (DO idempotency). Push tap routing (simctl push, Expo body wrapper): warm tap on low_balance → Org A credit-activity with selection persisted; foreign org id → Organization unavailable with selection untouched; security_finding → finding detail; both new toggle rows render and persist. Cold-start tap (app terminated → Notification Center cell activation) is tooling-blocked on this host (Maestro cannot activate SpringBoard NC cells for a terminated app; two documented rounds). Compensating evidence: delivery without tap correctly does NOT navigate (icon launch → Home); the cold mechanism (getLastNotificationResponse → pending link) shares parseNotificationData/notificationPathForData with the warm path; reconcile/path logic is unit-tested. |
|
(bot) Standin review of head FindingsHigh1. Security-finding route awaits push dispatch, exceeding the sweep's 10s timeout → duplicate user-facing emails — The route Low2. No route-level coverage for the new 3. Security-finding test fixture uses a non-producer-realistic 4. Stale eslint-disable justification — 5. Unrelated lockfile churn — Repairs for all five are being dispatched now; a follow-up comment will record what changed, and a fresh standin review will cover the new head. |
Schedule the security-finding push via next/server after() so push latency cannot extend the route response beyond the security-sync sweep's 10s budget and cause duplicate emails on sweep retry. Add route-level tests for POST /internal/v1/dispatch: missing, empty, and wrong secrets (equal and different length), malformed and schema-invalid bodies, and the happy path into the dispatch core. Use a producer-realistic scope fixture, correct the stale toggle-count comment, and revert unrelated lockfile churn.
|
(bot) Repair pushed: head is now What changed
Checks on the repair: worker dispatch suites 7/7 and 12/12, web security-agent route tests 14/14, typecheck/lint/format clean in all touched packages, A fresh standin review of the new head |
|
(bot) E2E rerun evidence for the repair head
Stack stopped, fixtures deleted, device slot released. |
Summary
Two alerts that today are email-only now also dispatch mobile push notifications, reusing the existing per-user Durable Object push pipeline (idempotency, presence, rate limiting) and the per-category preference model:
settings.minimum_balanceand the alert email is scheduled, every configured recipient email that maps to a Kilo user who is an active member of the alerting org (and has the category enabled) receives exactly one push per crossing. Tapping lands on that org's credit-activity screen with the org reconciled from the deep link.security_finding_notificationsrow's email is sent, the recipient also receives a push (new finding / SLA warning / SLA breach copy). Tapping opens the finding detail screen.Changes
packages/db: two newuser_notification_preferencescolumns (balance_alerts_enabled,security_findings_enabled, both default ON like every category) + generated migration. Email delivery is never affected by these toggles.packages/notifications: two newpushDataSchemamembers (low_balance,security_finding) +internalDispatchRequestSchemafor the internal endpoint.services/notifications: newPOST /internal/v1/dispatchendpoint (X-Internal-Secret auth with timing-safe compare against theINTERNAL_API_SECRETsecrets-store binding, config-only addition). The worker owns copy/payload construction and gates each recipient on the new preference categories before the DO call (fail-closed), then dispatches throughNotificationChannelDO.dispatchPush(presenceContext: null,badge: null). Idempotency keys:low-balance:<orgId>(a second crossing of the same org within the DO's 1h TTL is intentionally push-suppressed as anti-spam; email unaffected),security-finding:<notificationId>(covers sweep retry/crash windows).apps/web: fire-safenotifications-worker-client.ts(never throws into the email path; log-and-skip when unconfigured). Low-balance push shares the email schedule predicate; recipients are resolved email→user and filtered to active members of the alerting org (the tap target requires membership; unmatched/non-member emails stay email-only). Security-finding push dispatches only aftersendEmailsucceeded and can never change the route's response (no email double-send via sweep retries). tRPCgetNotificationPreferences/setNotificationPreferencesexposebalanceAlerts/securityFindings.apps/mobile: both payload types route vianotificationPathForData; credit-activity screen honors a?org=deep-link param with an explicit reconcile algorithm (param-only sourcing, no query keyed on the pre-tap org, persist selection only for validated memberships after SecureStore hydration, foreign/stale param → "Organization unavailable" without mutating the selection); two new preference toggle rows on the Notifications screen.iOS App Store rule intact: push copy is informational only, no purchase CTA; no landing-screen changes that add external-purchase CTAs.
Deploy-time env addition (action required)
NOTIFICATIONS_WORKER_URLmust be added to the web app's prod and preview env (whereverSESSION_INGEST_WORKER_URLis managed):https://notifications.kiloapps.iopnpm dev:env(@url notifications)Missing URL is safe by design: dispatch logs and skips, the email path is unaffected.
Test plan
internal-dispatch-push.test.ts);pnpm testinservices/notifications— 173/173.organization-usage.test.ts,route.test.ts,user-router.test.ts) — 111/111.org-deep-link.test.ts,notification-path.test.ts,agent-push-preference.test.ts) — 1885/1885.