Skip to content

feat: moonlight-pay backend with security hardening#53

Merged
AquiGorka merged 11 commits intodevfrom
feat/moonlight-pay-backend
Mar 24, 2026
Merged

feat: moonlight-pay backend with security hardening#53
AquiGorka merged 11 commits intodevfrom
feat/moonlight-pay-backend

Conversation

@AquiGorka
Copy link
Copy Markdown
Contributor

Summary

  • /api/v1/pay/ namespace: 13 endpoints for custodial accounts, KYC, escrow, transactions, self-custodial balance/send, demo deposit
  • 4 DB tables: pay_kyc, pay_custodial_accounts, pay_transactions, pay_escrow
  • Escrow service with atomic claims (FOR UPDATE row locking)
  • Atomic custodial send with double-spend prevention
  • Security hardening: timing-safe passwords, JWT type scoping, input validation, address validation, demo endpoint gating, suspended account checks, KYC ownership
  • 136 PGlite-based tests (zero infra required)

Test plan

  • deno test --allow-all --config src/http/v1/pay/tests/deno.json src/http/v1/pay/ — 136 tests pass
  • Lifecycle E2E (local-dev/lifecycle) — passed in 207s
  • E2E (local-dev/e2e) — passed in 44s
  • Existing deposit/send/receive/withdraw flow unaffected

Three new tables namespaced under pay_* for the moonlight-pay integration:

- pay_kyc: KYC verification status per Stellar address
- pay_custodial_accounts: custodial user accounts with password auth
- pay_transactions: unified transaction history for both self and custodial flows

Each with repository, proper indexes, and soft-delete support.
Migration 0006: creates pay_kyc, pay_custodial_accounts, pay_transactions
tables with enums, indexes, and base audit columns.
10 endpoints namespaced under /api/v1/pay/:

KYC (shared):
- GET /pay/kyc/:address — get KYC status
- POST /pay/kyc — submit KYC

Transactions (shared):
- GET /pay/transactions — list with pagination and status filter

Self-custodial:
- GET /pay/self/balance — UTXO balance summary (placeholder)
- POST /pay/self/send — initiate send (placeholder bundle logic)

Custodial:
- POST /pay/custodial/login — username/password auth → JWT
- POST /pay/custodial/register — create account + deposit address → JWT
- GET /pay/custodial/account — get account info
- POST /pay/custodial/send — send with balance check

Demo:
- POST /pay/demo/simulate-kyc — set KYC to VERIFIED directly

Login/register are public (rate-limited). All others require JWT.
Pay router mounted before bundle router to avoid middleware conflicts.
- pay_escrow entity with HELD/CLAIMED/EXPIRED status
- Escrow repository with findHeldForAddress, findBySender
- Escrow service: createEscrow, claimEscrowForAddress (atomic with
  FOR UPDATE), getEscrowSummary
- Channel service: wraps SDK PrivacyChannel for UTXO balance queries
- GET /pay/escrow/:address — pending escrow count and total
- POST /pay/report — error report ingestion from pay apps
- POST /pay/demo/deposit — demo deposit via PP treasury
- crypto.ts: shared PBKDF2 hashing with timing-safe verification
Security:
- Atomic balance debit with FOR UPDATE row locking (prevents double-spend)
- Timing-safe password verification via HMAC comparison
- JWT type claim (custodial vs sep10) with scope enforcement
- KYC ownership check (address must match session.sub)
- Amount validation (positive integer string via regex)
- Recipient address validation via StrKey.isValidEd25519PublicKey
- Valid Stellar deposit addresses via Keypair.random()
- Suspended account check on login and send
- publicKeys array capped at MAX_UTXO_SLOTS (300)
- Demo endpoints gated behind NETWORK=local|standalone or PAY_DEMO_ENABLED
- Generic registration failure message (no username enumeration)
- KYC read moved outside DB transaction (correct connection usage)

Reliability:
- try/catch on all unprotected handlers
- Status param enum validation on transactions list
- findByDepositAddress added to custodial account repository
- Rate-limit type cast fix
- loadEnv allowEmptyValues removal
Rename 0006 migration and add the pay_escrow table with HELD/CLAIMED/
EXPIRED status enum and indexes.
136 tests, all passing, zero infrastructure required:
- PGlite (in-memory PostgreSQL via WASM) for real SQL/transactions
- Mock JWT and channel service to avoid env var dependencies
- Escrow service: create, claim, concurrent double-claim
- Custodial send: double-spend, escrow, suspended, JWT type
- Self send/balance: validation, limits
- Login/register: auth, credentials, suspended
- KYC get/post: status lookup, ownership check
- Transactions list: pagination, filter
- Escrow summary, report, demo simulate-kyc
- Crypto: password hashing, timing-safe verification
- Validation: amount regex, BigInt, StrKey, demo guard, status enum
The migration was renamed from 0006_clever_mathemanic.sql to
0006_pay_tables.sql but the new file was not staged.
@AquiGorka AquiGorka force-pushed the feat/moonlight-pay-backend branch from 68af65e to 76f35d5 Compare March 24, 2026 14:29
Replace local filesystem reference with published JSR version
(^0.7.0) so Docker builds and CI work without the SDK repo
mounted alongside.
- Version 0.5.3 → 0.5.4
- stellar-sdk bumped to 14.6.1
- Added SDK transitive deps (@noble/curves, @noble/hashes, asn1js, tslib)
- Added @electric-sql/pglite for test infrastructure
@AquiGorka AquiGorka force-pushed the feat/moonlight-pay-backend branch from 2a7a9d1 to 19c0492 Compare March 24, 2026 14:40
@AquiGorka AquiGorka merged commit a80ffc6 into dev Mar 24, 2026
3 checks passed
@AquiGorka AquiGorka deleted the feat/moonlight-pay-backend branch March 24, 2026 14:54
AquiGorka added a commit that referenced this pull request Mar 24, 2026
- Fix test deno.json local path to use JSR registry
- Add JWT type check on self-custodial endpoints (send, balance)
- Move suspended account check after password verification in login
- Add address ownership checks on escrow summary and KYC GET endpoints
- Add amount regex validation on demo deposit endpoint
- Replace ctx.request.url.origin with localhost for internal self-fetch
- Truncate debug fields in error report logging
- Pass status filter to countByAccountId for correct pagination metadata
- Document deposit address design decision and fund-loss risk
AquiGorka added a commit that referenced this pull request Mar 24, 2026
- Fix test deno.json local path to use JSR registry
- Add JWT type check on self-custodial endpoints (send, balance)
- Move suspended account check after password verification in login
- Add address ownership checks on escrow summary and KYC GET endpoints
- Add amount regex validation on demo deposit endpoint
- Replace ctx.request.url.origin with localhost for internal self-fetch
- Truncate debug fields in error report logging
- Pass status filter to countByAccountId for correct pagination metadata
- Document deposit address design decision and fund-loss risk
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant