feat(nexusswift): zero-trust ISO 20022 pacs.008 cross-border clearing hub - #325
Conversation
… hub
Adds NexusSwift, a self-contained cross-border financial messaging and
settlement engine. It is an independent project living in its own top-level
directory with its own dependencies and test harness; it shares no code with
the rest of this repository and can be extracted with `git subtree split`.
What it implements — the actual technical core of a SWIFT-style hub:
* ISO 20022 pacs.008.001.10 assembly and namespace-aware strict parsing
* RSA-4096 / RSASSA-PSS non-repudiation over invariant signed bytes
* UETR-keyed idempotency enforced by storage constraints, not app checks
* atomic double-entry settlement with a SHA-256 hash-chained journal
* BIC/ISO 9362, ISO 4217 minor units, and per-currency amount precision
Design points that are load-bearing rather than stylistic:
* Money is integer minor units with a looked-up exponent (JPY 0, BHD 3,
CLF 4). An unknown currency is rejected, never defaulted to 2. An amount
with more precision than its currency admits is rejected, not rounded —
rounding would settle a different number than the one that was signed.
* The signed payload is stored verbatim and never re-serialised, so a
settled payment still verifies out of the database years later.
* PSS salt is pinned to 32 bytes rather than MAX_LENGTH, because Java and
most HSMs default to 32 and would reject a 446-byte-salted signature.
* Pipeline order is a security property: idempotency precedes signature
verification so a replay flood cannot be amplified into CPU exhaustion;
authentication precedes every balance read.
Four defects were found by exercising the system rather than by reading it,
and each is fixed with a regression test:
* Unauthenticated rejections were journalled, consuming the UETR. Any
party who observed a UETR in flight could submit it with a garbage
signature and permanently block the genuine payment. Rejections before
authentication are now audit-only; only a cryptographically proven
sender spends its UETR.
* The audit hash chain had a read-modify-write race. Concurrent appends
sharing an actor, event, UETR and head computed identical hashes and
collided on the UNIQUE constraint under a replay burst. Appends now run
under BEGIN IMMEDIATE.
* open_account overwrote an existing account's balance, creating value
from nothing and invalidating journal replay. It now updates terms only.
* aiosqlite worker threads are non-daemon, so an unclosed connection hung
process exit indefinitely — in a container, until the orchestrator
SIGKILLed it past the grace period.
Verification: 80 tests, standard-library unittest only, three consecutive
clean runs. The demo pipeline passes 12/12 runtime checks in-memory and
across three successive runs against a persistent volume, with the ledger
accumulating and reconciling across restarts.
Not verified here: the container image was not built, as this sandbox has
the Docker CLI but no daemon.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019gDSUkVHchizjCoAKmprhg
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
This pull request has been ignored for the connected project Preview Branches by Supabase. |
The Security Audit workflow's `npm audit (prod deps) (compliance-firewall-agent)` leg fails on `browserslist <=4.28.6`: GHSA-c83g-rgw3-j3cx unbounded memory growth (no cache eviction) -> OOM GHSA-73wf-gq98-2v4g uncaught crash / prototype write via untrusted browserslist-stats.json custom stats This is not caused by the NexusSwift change in this branch, which adds a Python project and touches no npm dependency. The same check failed on two unrelated Dependabot PRs on 2026-09-01 at 19:12 and 19:26 UTC, roughly ten hours before this branch existed, and the last scheduled main run (08-31) predates the advisory. The workflow only runs on pull_request and a Monday schedule, so main will not re-run on its own until then; every PR opened in the meantime inherits the red. Ported the remediation rather than waiting: `npm audit fix --package-lock-only --omit=dev`. This is lockfile-only and no-ops once the base branch carries an equivalent bump. browserslist is a transitive dependency, absent from package.json, and all four dependents already accept the new version (@babel/helper-compilation-targets ^4.24.0, autoprefixer ^4.28.6, update-browserslist-db >=4.21.0, webpack ^4.28.1), so no direct dependency version changes -- the same shape as the 2026-08-08 baseline clearance the workflow header documents. browserslist 4.28.6 -> 4.28.8 baseline-browser-mapping 2.10.42 -> 2.11.20 caniuse-lite 1.0.30001806 -> 1.0.30001810 electron-to-chromium 1.5.389 -> 1.5.420 node-releases 2.0.51 -> 2.0.54 update-browserslist-db 1.2.3 -> 1.3.2 Nothing added or removed; package.json untouched. Verified: failure reproduced locally first, then npm audit --omit=dev --audit-level=high reports 0 vulnerabilities; npm ci, npx tsc --noEmit and npm run build all pass; the proxy matrix leg was already clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019gDSUkVHchizjCoAKmprhg
CI:
|
| package | from | to |
|---|---|---|
browserslist |
4.28.6 | 4.28.8 |
baseline-browser-mapping |
2.10.42 | 2.11.20 |
caniuse-lite |
1.0.30001806 | 1.0.30001810 |
electron-to-chromium |
1.5.389 | 1.5.420 |
node-releases |
2.0.51 | 2.0.54 |
update-browserslist-db |
1.2.3 | 1.3.2 |
Nothing added or removed; package.json untouched. browserslist is transitive — absent from package.json — and all four dependents already accept the new version (@babel/helper-compilation-targets ^4.24.0, autoprefixer ^4.28.6, update-browserslist-db >=4.21.0, webpack ^4.28.1), so no direct dependency version changed. Same shape as the 2026-08-08 baseline clearance the workflow comment describes.
Verified before pushing — failure reproduced locally first, then:
| check | result |
|---|---|
npm audit --omit=dev --audit-level=high |
0 vulnerabilities (was 1 high) |
npm ci |
clean, 861 packages |
npx tsc --noEmit |
exit 0 |
npm run build |
exit 0, all routes emitted |
| proxy matrix leg | already 0 vulnerabilities |
No test was skipped, disabled or quarantined, and no re-run was spent — the failure was real and is now fixed at source.
Generated by Claude Code
The Security Audit workflow's `npm audit (prod deps) (compliance-firewall-agent)` leg fails on `browserslist <=4.28.6`: GHSA-c83g-rgw3-j3cx unbounded memory growth (no cache eviction) -> OOM GHSA-73wf-gq98-2v4g uncaught crash / prototype write via untrusted browserslist-stats.json custom stats Not caused by this branch. `git diff --stat HEAD~2 -- package.json package-lock.json` is empty: the two commits here touch three source files and documentation, no dependency. The same check failed on unrelated Dependabot PRs on 2026-09-01 and is red on the base branch, so every PR opened since inherits it. #325 hit the identical failure and reached the identical remediation. Ported rather than waited on, because waiting on another PR to merge is still waiting: `npm audit fix --package-lock-only --omit=dev`. Lockfile only, 4.28.6 -> 4.28.8, a patch bump inside the range the tree already resolves. It no-ops once the base branch carries the same change. Reproduced and verified: before npm audit --omit=dev -> 1 high severity vulnerability after npm audit --omit=dev -> found 0 vulnerabilities Re-validated against the new lockfile rather than the old node_modules: `npm ci` (861 packages, browserslist resolves to 4.28.8), `npx tsc --noEmit` clean, `npm run lint` 0 errors, `npm run test:coverage` 223 files and 3099/3099 passed with the coverage gate met, `npm run build` succeeded. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011UNmNvaCuGcZz418MoTyB5
…conflicts main advanced 3bc6df7 -> 23856c7 while this branch sat ready for review, bringing the NexusSwift addition, the Mode B audit-log hash chain, a readiness gate, and a refactor deleting 29 unreferenced files and 9 runtime dependencies. Three conflicts, resolved on their merits rather than by picking a side: CLAUDE.md — Session Start Protocol, step 3. Both sides rewrote the same line. main replaced the health check with the token-gated `/api/health/ready`, which is strictly better: `/api/health` is a bare liveness probe that returns `{"status":"ok"}` under every failure condition. Taking main's version alone would have dropped a real distinction this branch adds, because `lib/health/service-status.ts` reports `payments` / `payments_webhook` from the same diagnostics the new weekly alert uses — so readiness answers "is the money path CONFIGURED", and only the reconciler answers "was a sale actually missed". Kept main's route, appended that distinction. tasks/todo.md and tasks/lessons.md — append-style logs where each side is a pure addition. Both sides kept; main's dated section opens the lessons block and this branch's entries follow inside it. Verified the deletion refactor does not undercut this branch: none of the nine removed dependencies (@react-three/fiber, @remotion/*, date-fns, next-themes, react-hook-form, react-markdown, remotion, three) is imported by lib/stripe/report-fulfillment.ts, lib/stripe/money-path.ts, app/api/cron/reconcile-orders/route.ts or lib/email/templates/money-path-alert.ts. Gates re-run on the merged tree against the new lockfile, not carried over: npm ci exit 0 · tsc --noEmit clean · 3150 app tests pass (227 files, exit 0) · npm run build exit 0, registering both /api/cron/reconcile-orders and /api/health/ready · eslint 0 errors, none in the new files · npm audit --omit=dev --audit-level=high (the exact CI command) exit 0 · verify-structure PASS · 120 proxy tests pass · proxy bench p99 0.883 ms against a 10 ms budget. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GRc3QBQMgVNecjE7TLY1sG
Summary
Adds NexusSwift, a self-contained cross-border financial messaging and settlement engine that implements the technical core of a SWIFT-style clearing hub: ISO 20022
pacs.008.001.10customer credit transfers, RSA-4096/PSS non-repudiation, UETR-keyed idempotency, and atomic hash-chained double-entry settlement.This is a new, independent project, not a HoundShield change. It lives in its own top-level
nexusswift/directory with its own dependencies, test harness and container, shares no code or imports with the web plane or the proxy, and can be extracted at any time withgit subtree split -P nexusswift. Nothing outsidenexusswift/is touched except two.gitignoreadditions (see below).The one shared-file change is deliberate and necessary: the repo's
.gitignorecarries a blankettests/rule that matches at any depth and would have silently dropped the entire 80-test security harness from every commit. Git cannot re-include a file whose parent directory is excluded, so the directory is un-excluded first and the files second.Closes #
Change type
Impact and operating considerations
Operational impact, threat-model note, and rollback plan:
Operational impact on this repository: none. No existing file is imported, executed, built or deployed differently. The web plane and the proxy do not reference
nexusswift/, and it is not wired into any build, workflow or deploy path. Its SQLite schema is its own database file, entirely separate from Supabase — it introduces no migration into the existingsupabase/migrations/sequence and no new environment variable for any existing service.Rollback: delete the
nexusswift/directory and revert the two.gitignorehunks. There is no state to unwind, no migration to reverse and no configuration to restore.Threat model (internal to the new component). NexusSwift's own boundary assumption is that the transport is already authenticated and encrypted; it defends the message and the ledger, not the channel. Within that boundary the controls are: RSA-PSS over invariant signed bytes (tamper), UETR primary key plus per-sender MsgId unique index (replay), settlement lock plus
BEGIN IMMEDIATE(double-spend), account-to-agent ownership checks (cross-account debits), pinned namespace URI (message-type confusion), pre-parse DTD rejection and a 1 MiB cap (XXE / billion laughs), and a SHA-256 hash chain over the journal (post-hoc mutation).Pipeline order is itself a control and is documented as such: idempotency is checked before signature verification so a replay flood cannot be amplified into CPU exhaustion, and authentication precedes every balance read so an unauthenticated caller cannot probe balances through error codes or timing.
Validation
The web-plane and proxy checks below are listed unchecked because they were not run and do not apply — this PR changes no TypeScript, no React, no proxy code and no schema. Running them would validate nothing about this diff.
npx tsc --noEmit— N/A, no TS changednpm run lint— N/Anpm run test:coverage— N/Anpm run build— N/Anpm run lint— N/Anpm run test:coverage— N/Anpm run bench— N/AResults and manual verification:
Coverage is by behaviour rather than by line: 80 tests across valid clearing, 12 tamper vectors, replay (including a 20-way concurrent burst), liquidity boundaries (including a 50-way concurrent burst), participant control and key rotation, schema/XXE/precision, ledger chain integrity, and the crypto primitives.
Four defects were found by running the system, not by reading it. Each is fixed with a regression test:
test_a_forged_attempt_cannot_burn_a_genuine_uetr.append_auditdid SELECT-head → hash → INSERT without a transaction. Concurrent appends sharing actor, event, UETR and head computed identical hashes and collided on the UNIQUE constraint — nondeterministic, and it surfaced only under a 16-way replay burst on the second persistent run. Now serialised underBEGIN IMMEDIATE. Guarded bytest_concurrent_audit_appends_do_not_collide.open_accountoverwrote an existing account's balance, creating value from nothing and invalidating journal replay — reconciliation correctly reported the drift; the bug was in the reset, not the detector. It now updates terms (status, credit line) only. Guarded bytest_reopening_an_account_never_overwrites_its_balance.aiosqlite0.22 worker threads are non-daemon, so an unclosed connection blocks interpreter exit indefinitely — in a container, until the orchestrator SIGKILLs it past the grace period. Threads are now marked daemon as a backstop, withclose()still the correct path.Not verified: the container image was not built. This sandbox has the Docker CLI but no daemon, so
docker buildcould not run. The Dockerfile is reviewed but unbuilt, and that should be confirmed before any use.Review checklist
nexusswift/README.mddocuments the architecture, every design decision with its rationale, and the full control-to-test mapping; every test name it cites was verified to exist.🤖 Generated with Claude Code
https://claude.ai/code/session_019gDSUkVHchizjCoAKmprhg
Generated by Claude Code