Skip to content

feat: payout batching, money-movement audit, anomaly detection, hardened JWT (#1008 #1011 #1013 #1014) - #1301

Merged
github-actions[bot] merged 1 commit into
Smartdevs17:mainfrom
JamesVictor-O:feat/payout-security-hardening
Oct 17, 2026
Merged

github-actions[bot] merged 1 commit into
Smartdevs17:mainfrom
JamesVictor-O:feat/payout-security-hardening

Conversation

@JamesVictor-O

Copy link
Copy Markdown
Contributor

Summary

Resolves four payout and auth issues in backend/.

#1008 — Payout scheduling and batching

New services/payouts/payout-batcher.ts:

  • Scheduling: payouts are queued with a scheduledFor time. A worker started in index.ts, or POST /payouts/scheduled/run, sends the ones that are due.
  • Batching: due payouts are grouped by provider and asset and split into chunks of at most PAYOUT_BATCH_MAX_SIZE. Each chunk is one provider call.
  • Atomic per provider call: a batch settles every item under one transaction hash or fails every item. A provider that acknowledges only some items counts as a failure of the whole batch.
  • Failures are isolated: a failing batch never affects the other batches in the same run. Its items go back to the queue on their own with a retry delay, until maxAttempts is reached.
  • Reports: GET /payouts/batches, /batches/:id and /batches/runs/:runId show status, items, totals by asset, and fees saved by batching (PAYOUT_PROVIDER_FEE_PER_CALL).

#1011 — Audit logging for all money-movement operations

New audit/money-movement-audit.ts:

  • Actor, amount and reference recorded for every balance change, reservation, approval, rejection, settlement, failure, release, schedule, hold and cancellation, in both the payout engine and the batcher.
  • Tamper-evident: append-only hash chain using the existing ImmutableAuditLogEntry and computeAuditHash format, so verifyAuditChain and its tamper alerts work unchanged. Entries are returned as frozen copies. Editing, deleting or reordering an entry is detected.
  • Queryable for compliance: filter by actor, merchant, operation, reference, asset, amount range and time window, with pagination and totals by asset. Exposed as GET /payouts/audit and GET /payouts/audit/verify. An optional durable store (ImmutableAuditStore) is written to in order, without blocking the payout.

#1013 — Anomaly detection for unusual payout volume

New services/payouts/payout-anomaly-detector.ts:

  • Rules: single-payout size, payout count velocity, payout amount velocity, and deviation from the merchant's rolling baseline.
  • Thresholds configurable through PAYOUT_ANOMALY_* env vars or at runtime with PUT /payouts/anomalies/config (admin only, validated as a whole).
  • Alerts emitted to subscribers for every flag. The default sink writes a structured warning; a subscriber that throws can't block a payout.
  • False-positive review path: GET /payouts/anomalies and POST /payouts/anomalies/:id/review with confirmed or false_positive. A false positive can suppress that rule for that merchant for a limited time (capped at 30 days).
  • Wired into money movement:
    • A flagged engine batch always needs approval, and can't be approved until its flags are reviewed (409 until then).
    • A flagged scheduled payout is held out of batches. Clearing every flag as a false positive releases it; confirming a flag cancels it.

#1014 — Harden JWT handling and token lifetimes

Access tokens used to be random at_… strings that were only checked for shape, so any string in the right format was accepted and nothing could be revoked. New auth/jwt.ts and auth/access-token-revocation.ts:

  • Short-lived access tokens: HS256 JWTs with a 15-minute default (JWT_ACCESS_TOKEN_TTL_SECONDS), clamped to 1 minute–1 hour. A signed token whose own lifetime exceeds the cap is rejected.
  • Algorithm enforced: only HS256. none, RS/ES (the algorithm-confusion attack), HS384/512 and crit headers are rejected before the signature is checked.
  • Claims and keys: iss, aud, sub, jti, iat and exp are required, nbf is honoured, clock skew is limited, and signatures are compared in constant time. The secret must be at least 32 characters, and the service won't start in production without JWT_SECRET. JWT_SECRET_PREVIOUS allows key rotation.
  • Refresh rotation: access tokens are bound to their refresh-token family (fid), and each rotation issues a new access token in that family.
  • Revocation list enforced by jti, by family and by user cut-off. Entries are cached locally and mirrored to Redis so revocations reach every instance. Logout, refresh-token replay detection and sign-out-everywhere all revoke the affected access tokens. The list is checked by tokenAuthMiddleware, WebSocket upgrade auth and the refresh routes.
  • tokenStrategy now takes user and tenant from the verified claims, not from headers the caller can set.

Breaking change

Opaque at_… access tokens are no longer accepted. Clients get JWTs from /auth/login and /auth/refresh exactly as before; the responses now also include accessTokenExpiresAt. JWT_SECRET must be set in production.

Also fixed

middleware/logger.ts had a syntax error (?? mixed with || without parentheses). It stopped every module that imports it, token-auth included, from loading.

Tests

  • New: 243 passing tests across 15 affected suites, including new suites for JWT and revocation, token-auth and WebSocket auth, the money-movement audit log, the anomaly detector, the batcher, engine audit/anomaly integration, and the new routes.
  • Full backend suite against main: no new failures in any suite that loaded on main, and 2,262 passing vs 2,152.
  • Already failing upstream, not changed here:
    • 15 payouts.test.ts route tests fail on main too. A stale compiled errorHandler.js sits next to errorHandler.ts, so instanceof AppError fails. The new route tests use a duck-typed error handler to avoid this.
    • After the logger fix, auth.test.ts and the webhookVerification* suites now load, but some of their tests fail on helpers upstream never exported (lookupApiKeyRecord, clearReplayCache, resetWebhookVerificationConfig).
  • tsc: no new errors (one fewer than main). eslint: no new errors in changed files.

New environment variables are documented in ENV_VARS.md.

Closes #1008
Closes #1011
Closes #1013
Closes #1014

…, hardened JWT tokens

Payout scheduling and batching (Smartdevs17#1008)
- PayoutBatcher schedules individual payouts and, when due, groups them by
  provider and asset into batches sent in one provider call each.
- Atomic per call: a batch settles every item under one transaction hash or
  fails every item; partial acknowledgements count as failure.
- Failures are isolated per batch and retried with backoff up to maxAttempts.
- Batch and run reports show status, items, totals and fees saved. A worker
  runs due batches on an interval.

Money-movement audit log (Smartdevs17#1011)
- MoneyMovementAuditLog appends hash-chained, frozen entries recording actor,
  amount, asset and reference for every balance change, reservation,
  approval, rejection, settlement, failure and release in the payout engine
  and batcher. Compatible with verifyAuditChain, so edits, deletions and
  reordering are detected.
- Compliance queries by actor, merchant, operation, reference, asset, amount
  range and time window, plus GET /payouts/audit and /payouts/audit/verify.

Payout anomaly detection (Smartdevs17#1013)
- PayoutAnomalyDetector flags single-payout size, count/amount velocity and
  deviation from the merchant baseline. Thresholds come from PAYOUT_ANOMALY_*
  env vars and can be updated at runtime by admins.
- Every flag is emitted to alert subscribers. Flagged engine batches always
  need approval and cannot be approved until reviewed; flagged scheduled
  payouts are held out of batches.
- Review path: confirm or mark false positive (optionally suppressing the
  rule for the merchant for a bounded time); false positives release held
  payouts, confirmations cancel them.

Hardened JWT handling (Smartdevs17#1014)
- Access tokens are now HS256 JWTs (15 min default, clamped to 1 min-1 h)
  instead of unverifiable opaque strings. The algorithm is pinned (none,
  asymmetric and other HMAC algorithms rejected), iss/aud/sub/jti/iat/exp are
  required, over-long lifetimes are rejected and signatures are compared in
  constant time. Supports key rotation via JWT_SECRET_PREVIOUS and refuses to
  run in production without JWT_SECRET.
- Refresh rotation issues family-bound access tokens. A revocation list
  (local cache mirrored to Redis) is enforced by token-auth, WebSocket auth
  and the refresh routes, and is fed by logout, refresh-token replay and
  sign-out-everywhere.

Also fixes a syntax error in middleware/logger.ts that stopped every module
importing it (including token-auth) from loading.

Closes Smartdevs17#1008
Closes Smartdevs17#1011
Closes Smartdevs17#1013
Closes Smartdevs17#1014
@vercel

vercel Bot commented Oct 17, 2026

Copy link
Copy Markdown

@JamesVictor-O is attempting to deploy a commit to the smartdevs17's projects Team on Vercel.

A member of the Team first needs to authorize it.

@drips-wave

drips-wave Bot commented Oct 17, 2026

Copy link
Copy Markdown

@JamesVictor-O Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@github-actions
github-actions Bot merged commit 8e4a446 into Smartdevs17:main Oct 17, 2026
16 of 29 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant