Repository navigation
feat: harden API — wallet auth, validation, tracing, error handling, CI, OpenAPI - #7
Conversation
diiabblo
left a comment
There was a problem hiding this comment.
Review: API hardening
Thanks for this -- the auth flow (nonce + Ed25519 verify + JWT), request tracing, error handling, and CI pipeline are well-structured. Before merging, a few issues need attention:
Critical
verifyWalletSignature public key extraction is off by one byte (src/middleware/auth.ts:70)
The raw Ed25519 key is extracted with bytes.slice(2, 34) but Stellar encoded keys place the 32-byte raw key at bytes[1] through bytes[32]. The version byte is at [0], checksum at [33..34]. Using slice(2, 34) skips the first byte of the actual key and includes a checksum byte instead. The correct extraction is bytes.slice(1, 33).
I verified this with a real Stellar address -- the two slices produce different hex output, meaning the derived public key is wrong and signature verification fails for all real keys. This code path is also not covered by the test suite; the auth tests mock Redis but never exercise verifyWalletSignature with a real signature.
High
jwtSecret defaults to "change-me-in-production" (src/config.ts:23)
If JWT_SECRET is not set, anyone who reads this source can forge valid JWT tokens. The app should refuse to start in production when JWT_SECRET is unset, rather than falling through to a hardcoded default.
vesting.ts conflicts with PR #6 (src/routes/vesting.ts:49-50)
This changes totalAmount and vestedAmount to use Number(schedule.amount), but PR #6 migrates those columns from Float to Prisma.Decimal. Number() on a Decimal object loses precision -- exactly the bug PR #6 fixes. Both PRs modify the same lines. Coordinate the merge so the final version uses Decimal.mul() and toString() serialization from PR #6, while keeping the totalDuration > 0 guard and next(error) pattern from this PR.
Medium
CI Node version is 20 but other PRs need 22 (.github/workflows/ci.yml:26)
PR #5 adds @stellar/stellar-sdk which requires Node >=22. Since this PR adds the CI pipeline that will gate all other PRs, bump node-version to "22" now.
Coverage threshold gates this PR (jest.config.js:13)
coverageThreshold: { global: 60% } with tests covering only ~4 of ~18 source files means CI will fail on this PR. Either lower the threshold or add tests for the uncovered modules before merging.
Minor
globals config is deprecated (jest.config.js:6)
ts-jest 29 emits a deprecation warning for globals config. Tests still pass (I verified jest@30 + ts-jest@29 works), but migrating to the transform config format would eliminate the warning:
transform: {
"^.+\\.tsx?$": ["ts-jest", { tsconfig: "./tsconfig.test.json" }]
}
|
Thanks for the thorough review @diiabblo — all points addressed in the latest commit: Critical — key extraction off-by-one: Fixed. High — hardcoded JWT_SECRET fallback: Fixed. The config now throws High — vesting.ts Decimal precision: Fixed. Imported Medium — Node 20 → 22 in CI: Fixed in Medium — coverage threshold: Lowered to 30% to match the current test scope. The threshold will be raised as coverage grows. Minor — ts-jest globals deprecation: Migrated to the |
…nAPI (closes OBP-ORG#3) - Wallet nonce/verify auth with single-use Redis nonces (replay protection) - HMAC-SHA256 JWT (native crypto, no extra deps) with configurable TTL - requireAuth, requireOrgAdmin, requireOrgMember middleware with cross-org deny logging - Request ID middleware (x-request-id header + all log lines) - Structured JSON logger (requestId, no secrets or raw signatures) - Input validation: Stellar address regex, UUID cursor, bounded pagination (max 100) - Body field allowlist to reject unknown keys - Standardised error envelope {error, message, requestId} on all routes - express.json() body size cap (64 kb) - trust proxy config for real client IPs behind load balancer - Redis socket connect timeout; Ed25519 key import via SPKI DER wrapper - Decimal-safe vesting amount arithmetic (Number() cast for Prisma Decimal) - openapi.yaml covering all public and auth-gated routes - GitHub Actions CI: type-check → 22 Jest tests → build → npm audit --audit-level=high - Jest + ts-jest + supertest devDeps; jest.config.js with 60% coverage threshold
- Fix off-by-one in verifyWalletSignature: slice(2,34) → slice(1,33) to correctly extract the 32-byte Ed25519 key (bytes[1..32], not bytes[2..33]) - Refuse to start in production when JWT_SECRET is unset instead of falling back to the hardcoded 'change-me-in-production' default - Use Prisma.Decimal arithmetic in vesting progress endpoint so the code is forward-compatible with the Float→Decimal migration in PR OBP-ORG#6 - Bump CI Node version from 20 to 22 (required by @stellar/stellar-sdk) - Lower jest coverage threshold from 60% to 30% to match current test scope; threshold will be raised as coverage expands - Migrate ts-jest config from deprecated globals to transform format
10b1d38 to
f5b2b54
Compare
diiabblo
left a comment
There was a problem hiding this comment.
Re-review: all fixes verified
All issues from the previous review are addressed:
- verifyWalletSignature:
bytes.slice(2,34)->bytes.slice(1,33)-- correct Ed25519 key extraction - jwtSecret: now throws in production via lazy IIFE when
JWT_SECRETis unset - vesting.ts: uses
new Prisma.Decimal()for arithmetic with stable@prisma/clientimport -- compatible with PR #6 Decimal migration - CI:
node-version: "22"-- supports@stellar/stellar-sdkrequirement - jest.config.js: migrated from deprecated
globalstotransformformat, coverage threshold lowered to 30% - .env.example: all new config vars documented
TypeScript compiles clean, tests pass. This is the right PR to merge first -- the CI pipeline it adds will gate all subsequent PRs with automated checks.
Merge note
PR #6 (Decimal migration) also touches src/routes/vesting.ts. Since this PR now uses Prisma.Decimal arithmetic, the merge should resolve cleanly -- #6 just needs to change .toNumber() to .toString() in the response serialization to match.
Closes #3
What changed
/auth/nonce+ POST/auth/verifywith Ed25519 signature verification; Redis single-use nonces (TTL 300 s, atomicgetDel); HMAC-SHA256 JWT issued on successrequireOrgMember/requireOrgAdminmiddlewares; role validated from DB on every protected request/^G[A-Z2-7]{55}$/), cursor/page pagination bounded to 1–100,bodyFieldsallowlist blocks unknown keys,express.json({ limit: '64kb' })x-request-idheader propagated end-to-end; structured JSON logs vialogger.ts; all error responses includerequestIdnotFound+errorHandlermiddlewares; every route usesnext(error)instead of inline 500sparsePagination(cursor) andparsePagePagination(offset) enforcelimit ≤ 100limit_reqconfig already in docker-compose;trust proxyset in Express soX-Forwarded-Foris trustedconnectTimeout+commandTimeout; Prisma query timeout viadbQueryTimeoutMsopenapi.yamlcovering all public + authenticated routes with request/response schemastsc --noEmit→jest --runInBand→tsc -p tsconfig.json→npm audit --audit-level=highrequireAuthmiddleware, nonce flow, rate-limit header propagationWhy
Full security hardening as requested in issue #3: replay-protected auth, org-scoped RBAC, input sanitisation, observability, and automated quality gates.
How to test
Or with the full stack: