Skip to content

Fix/sr push webhook admin kyc gaps - #1414

Merged
GoodnessJohn merged 5 commits into
Haroldwonder:mainfrom
celina005:fix/sr-push-webhook-admin-kyc-gaps
Aug 28, 2026
Merged

Fix/sr push webhook admin kyc gaps#1414
GoodnessJohn merged 5 commits into
Haroldwonder:mainfrom
celina005:fix/sr-push-webhook-admin-kyc-gaps

Conversation

@celina005

Copy link
Copy Markdown
Contributor

Summary

  1. fix(push): delete permanently-invalid Expo tokens and poll delivery receipts (backend/src/device-token-service.ts, backend/src/scheduler.ts)
    sendExpoPush() now inspects ticket.details.error and deregisters tokens reporting DeviceNotRegistered immediately. Successful tickets are persisted to a new expo_push_tickets table, and a new 15-minute scheduler job polls Expo's /getReceipts to catch delivery failures that never surfaced at the ticket phase and prune those tokens too.

  2. fix(webhooks): bound inline retry wall-clock time and parallelize dispatch (dispatcher.ts, webhook-handler.ts, scheduler.ts)
    dispatch() now fans out to subscribers via Promise.allSettled instead of sequentially. attemptDelivery() caps in-process retrying at WEBHOOK_INLINE_MAX_WALL_CLOCK_MS (10s default), deferring the rest to a new 2-minute background job. The inbound /webhooks/anchor handler no longer awaits the outbound dispatch call, so it responds immediately.

  3. fix(admin): wire AdminConfirmationService to real confirm/initiate routes (routes/admin.ts)
    Added POST /api/admin/actions/:op/initiate, POST /api/admin/actions/:id/confirm, and GET /api/admin/actions/pending, gated by a new ADMIN_ACTIONS_API_KEY credential. Since no execution call site exists yet for withdraw_fees/remove_agent/update_fee, confirmation authorizes out-of-band execution rather than triggering it directly (documented in the README).

  4. fix(kyc): gate the real POST /api/remittance endpoint with transferGuard (routes/remittance.ts)
    The real money-movement endpoint now runs createTransferGuard(kycUpsertService) before inserting into transactions, closing the gap where the only KYC-gated route was a no-op stub.

Linked Issue

Closes #441
Closes #442
Closes #443
Closes #444

Type of Change

  • Bug fix
  • New feature
  • Refactor
  • Documentation
  • Chore / infra

Checklist

  • Tests added or updated for the change
  • Documentation updated (README, API docs, guides) where relevant
  • Changeset added (sdk/) if this touches a published package
  • Linked issue referenced above
  • This PR introduces a breaking change (if checked, describe migration steps below)

Breaking Change Notes

…eceipts

sendExpoPush() only logged ticket.status === 'error' and never inspected
ticket.details.error, so tokens Expo reports as DeviceNotRegistered were
never deleted and kept being resent on every notification indefinitely.
The two-phase Expo flow (ticket now, receipt later) also meant real
delivery failures were never observed at all.

DeviceTokenService now deregisters tokens immediately on a
DeviceNotRegistered ticket, records successful ticket IDs, and a new
15-minute scheduler job polls /getReceipts to prune tokens whose receipt
reports DeviceNotRegistered.

Claude-Session: https://claude.ai/code/session_017KdUwWCFg1x9ezdbFs9zVB
…patch

attemptDelivery() retried failed subscriber deliveries in-place with up to
5 attempts at up to 300s backoff each, and dispatch() awaited subscribers
sequentially. handleRemittanceCreated() awaited dispatch() inline before
the inbound /webhooks/anchor handler sent its 200, so one down subscriber
could hold the anchor's webhook request open for minutes, risking the
anchor's own delivery timeout/retry.

Dispatch now runs subscribers in parallel via Promise.allSettled, inline
retries are capped by WEBHOOK_INLINE_MAX_WALL_CLOCK_MS (default 10s) and
deferred to a new scheduled job otherwise, and the inbound handler no
longer awaits the outbound fan-out dispatch call.

Claude-Session: https://claude.ai/code/session_017KdUwWCFg1x9ezdbFs9zVB
…utes

AdminConfirmationService's two-step confirmation flow for withdraw_fees,
remove_agent, and update_fee was referenced only by its own file and test
- no route ever constructed or called it, so the dual-admin safety control
it was built to provide did not exist in practice and
pending_admin_actions was never populated.

Adds POST /api/admin/actions/:op/initiate, POST /api/admin/actions/:id/confirm,
and GET /api/admin/actions/pending to routes/admin.ts, gated by a dedicated
ADMIN_ACTIONS_API_KEY credential distinct from the identity-only x-user-id
header used elsewhere in the router.

Claude-Session: https://claude.ai/code/session_017KdUwWCFg1x9ezdbFs9zVB
createTransferGuard() was only wired to the /api/transfer no-op stub in
routes/kyc.ts. The actual money-movement endpoint, POST /api/remittance
in routes/remittance.ts, used a separate authMiddleware that only checked
for a non-empty x-user-id header and never applied any KYC check, so it
was reachable regardless of a user's KYC status, expiry, or
re-verification-pending state.

routes/remittance.ts now constructs its own KycUpsertService (same
pattern as routes/kyc.ts) and applies transferGuard before the handler
that inserts into transactions.

Claude-Session: https://claude.ai/code/session_017KdUwWCFg1x9ezdbFs9zVB
@vercel

vercel Bot commented Aug 28, 2026

Copy link
Copy Markdown

@celina005 is attempting to deploy a commit to the Harold's projects Team on Vercel.

A member of the Team first needs to authorize it.

@GoodnessJohn
GoodnessJohn merged commit 772c811 into Haroldwonder:main Aug 28, 2026
1 check failed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants