uid: T-203 title: Money-movement authorization + audit trail (payment endpoints fail closed, every payment on the record) status: done area: bank-access created: 2026-08-28 updated: 2026-08-28 owner: girafeev1 assignee: Bank Access (Diagnosticcs) related: T-167, I-039
T-203 β Payment authorization + audit trail¶
Why (owner, 2026-08-28)¶
"I've always been concerning about the lack of identity verification and security of it, since there's no additional login required, and there're nothing tracking who's logged in and performed what action exactly⦠Does Airwallex provide any solution in this API access foundation for account specific login, or does it really just let the web app hold a token for access we configure? If so, do you think using authenticators would be one way to go?"
Owner then directed: "Proceed with 1 & 2" β the permission gate and the audit trail, with step-up MFA (item 3) deliberately deferred.
The constraint that shapes this (researched 2026-08-28, Airwallex docs)¶
Airwallex API auth is a machine credential, not a human one: client_id + API key β a 30-minute
access token, belonging to the organization, not a person. Their docs never address per-user
attribution for API actions because there is none β every API-created transfer is simply "created via
API" in their ledger. Their human-identity controls (user roles, sign-in security, the transfer
approval workflow) govern web-app users only.
Therefore identity is structurally ours to enforce. Airwallex authenticates the application; only we can authenticate the person. This is the correct division, not a defect β but it means the app must carry the whole burden, and before this task it carried none of it.
Relevant because the owner relaxed "require API-initiated transfers to go through the approval workflow" during T-167: the human gate was deliberately moved from Airwallex's side to ours. That trade is only sound if our side is actually strong.
What shipped¶
1. Authorization β payments fail closed.
canMoveMoney(role, status) in lib/rbac/permissions.ts, gating /transfers/create and
/transfers/queue. Deliberately independent of RBAC_ENABLED: canPerform short-circuits to
true while enforcement is rolled out, a sane default for ordinary features but never for a payment
endpoint β the fallback state must not be "any signed-in session can move money."
Queuing is gated identically to sending, because a queued payment fires for real on signature β
gating only /create would leave the open door.
2. Audit β every money movement on the record.
bank_transfer added to AuditEntity (+ admin audit-viewer label, which the type gate caught).
Audited: direct send, queue-for-signature, signature-triggered release, release failure, and
refusals. Entries carry actor, role, amount, currency, beneficiary, reference, idempotency
request_id, resulting transfer id and Airwallex status, plus IP / user-agent / path β queryable and
exportable from /admin/audit.
The unattended release has no request context, so it is attributed to the creator who authorized
those exact amounts, with the triggering signature recorded alongside; createdByRole is now stored
on the queue entry to make that possible.
Verification¶
- All 5 existing accounts hold a qualifying role (super_admin Γ1, admin Γ2, auditing_accountant Γ2),
so no lockout risk β confirmed against Firestore before writing a fail-closed gate. Two of them
are
suspendedand are now correctly refused. - Session
role/statusare populated in production β confirmed independently by live audit entries carryingrole=super_admin. - Audit pipeline confirmed live and writing; before this change it held zero bank-related entries.
- Gate: fresh
npm ci+tsc --noEmit --incremental false, clean.
Follow-ups (owner-gated β do NOT act without a command)¶
- Step-up MFA (TOTP) at the confirm step. The real remaining layer: proves who, where the gate proves whether allowed and the audit log proves what happened. Needs enrollment UI, secret storage, recovery codes, re-prompt window. Owner deferred pending a design proposal.
- Role membership question for the owner:
bank_accounts:writeis currently held by bookkeeper and auditing_accountant as well as the admin tier. Enforced as-is here rather than silently redesigning the role model β worth deciding whether those two roles should be able to move money. - Airwallex-side (owner action, their dashboard): replace the admin API key with a scoped key (Transfers Write + needed reads) β Airwallex explicitly discourages admin keys for new integrations; and rotate the key. IP allowlisting is supported by Airwallex but likely unavailable to us (Vercel serverless has no stable egress IPs outside Enterprise Secure Compute) β verify before promising it.
- Activation requires a production deploy (π· owner-commanded only).
Log¶
- 2026-08-28 β Opened and shipped from the owner's security question + "Proceed with 1 & 2". Researched the Airwallex side before touching code (no per-user API identity exists, by design). Diagnosis recorded in I-039. Source: Bank Access (Diagnosticcs) Β· https://claude.ai/code/session_01S4ztAsDZfR97qXjhPdwLn4