Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/node-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ jobs:
- name: Run format check
run: npm run format:check

- name: Check OpenAPI spec
run: npm run openapi:check

# Build the project
- name: Build project
run: npm run build
Expand Down
7 changes: 6 additions & 1 deletion docs/API_REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ Comprehensive reference for all backend endpoints defined in src/routes.
- Base URL (local): http://localhost:3001
- Content type: application/json unless otherwise specified
- Auth header format: Authorization: Bearer <token>
- OpenAPI JSON: GET /openapi.json in non-production with a valid bearer token
- Swagger UI: GET /docs in non-production

## Authentication and Authorization

Expand Down Expand Up @@ -685,4 +687,7 @@ Response 404:
- protocols.ts: GET /api/protocols/rates, GET /api/protocols/agent/status
- deposit.ts: POST /api/deposit
- withdraw.ts: POST /api/withdraw
- vault.ts: GET /api/vault/state, GET /api/vault/balance
- vault.ts: GET /api/vault/state, GET /api/vault/balance, POST /api/vault/build-transaction
- analytics.ts: GET /api/analytics/apy-history, GET /api/analytics/user-yield, GET /api/analytics/protocol-performance
- stellar.ts: GET /api/stellar/metrics
- admin.ts: GET /api/admin/stellar/metrics, GET /api/admin/dlq/inspect, POST /api/admin/dlq/retry, POST /api/admin/dlq/resolve, POST /api/admin/stellar/backfill
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"build": "tsc",
"start": "node dist/index.js",
"lint": "npm run lint:types && npm run lint:style",
"openapi:check": "ts-node --transpile-only scripts/check-openapi.ts",
"lint:types": "tsc --noEmit",
"lint:style": "eslint \"src/**/*.ts\" \"tests/**/*.ts\" \"prisma/**/*.ts\" \"jest.config.ts\"",
"format": "prettier --write .github/workflows/node-ci.yml package.json .prettierrc.json eslint.config.mjs src/nlp/parser.ts src/stellar/dlq.ts src/whatsapp/handler.ts src/whatsapp/userManager.ts tests/helpers/testDb.ts tests/integration/stellar/events.test.ts tests/unit/nlp/parser.test.ts tests/unit/whatsapp/handler.test.ts",
Expand Down
88 changes: 88 additions & 0 deletions scripts/check-openapi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import fs from 'node:fs'
import path from 'node:path'
import { openApiSpec } from '../src/openapi/spec'

type RouteMount = {
file: string
prefix: string
}

const routeMounts: RouteMount[] = [
{ file: 'agent.ts', prefix: '/api/agent' },
{ file: 'analytics.ts', prefix: '/api/analytics' },
{ file: 'admin.ts', prefix: '/api/admin' },
{ file: 'auth.ts', prefix: '/api/auth' },
{ file: 'deposit.ts', prefix: '/api/deposit' },
{ file: 'portfolio.ts', prefix: '/api/portfolio' },
{ file: 'protocols.ts', prefix: '/api/protocols' },
{ file: 'stellar.ts', prefix: '/api/stellar' },
{ file: 'transactions.ts', prefix: '/api/transactions' },
{ file: 'vault.ts', prefix: '/api/vault' },
{ file: 'whatsapp.ts', prefix: '/api/whatsapp' },
{ file: 'withdraw.ts', prefix: '/api/withdraw' },
]

function collectDiscoveredOperations(): Set<string> {
const operations = new Set<string>()
const routesDir = path.join(process.cwd(), 'src', 'routes')

for (const mount of routeMounts) {
const filePath = path.join(routesDir, mount.file)
const source = fs.readFileSync(filePath, 'utf8')
const regex = /router\.(get|post|put|delete|patch)\s*\(\s*(['"])(.*?)\2/gs

for (const match of source.matchAll(regex)) {
const method = match[1].toLowerCase()
const routePath = match[3]
const normalizedPath = `${mount.prefix}${routePath}`
.replace(/\/+/g, '/')
.replace(/:([A-Za-z0-9_]+)/g, '{$1}')
.replace(/\/$/, '') || '/'
operations.add(`${method} ${normalizedPath}`)
}
}

return operations
}

function collectSpecOperations(): Set<string> {
const operations = new Set<string>()

for (const [routePath, pathItem] of Object.entries(openApiSpec.paths)) {
for (const method of Object.keys(pathItem)) {
if (['get', 'post', 'put', 'patch', 'delete'].includes(method)) {
operations.add(`${method} ${routePath}`)
}
}
}

return operations
}

const discovered = collectDiscoveredOperations()
const documented = collectSpecOperations()

const missing = [...discovered].filter((operation) => !documented.has(operation)).sort()
const extra = [...documented].filter((operation) => !discovered.has(operation)).sort()

if (missing.length || extra.length) {
console.error('OpenAPI spec is out of sync with the route table.')

if (missing.length) {
console.error('\nMissing from spec:')
for (const operation of missing) {
console.error(` - ${operation}`)
}
}

if (extra.length) {
console.error('\nExtra in spec:')
for (const operation of extra) {
console.error(` - ${operation}`)
}
}

process.exit(1)
}

console.log('OpenAPI spec is in sync with the route table.')
4 changes: 3 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import analyticsRouter from './routes/analytics'
import adminRouter from './routes/admin'
import metricsRouter from './routes/metrics'
import stellarRouter from './routes/stellar'
import docsRouter from './routes/docs'
import { corsMiddleware, jsonBodyParser, payloadSizeErrorHandler, urlencodedBodyParser } from './middleware/corsandbody'

// ── Readiness state ───────────────────────────────────────────────────────────
Expand Down Expand Up @@ -120,6 +121,7 @@ app.use('/api/withdraw', withdrawRouter)
app.use('/api/vault', vaultRouter)
app.use('/api/analytics', analyticsRouter)
app.use('/api/stellar', stellarRouter)
app.use('/', docsRouter)

app.use('/metrics', metricsRouter)
// Admin routes (protected, strictest rate limit)
Expand Down Expand Up @@ -303,4 +305,4 @@ if (require.main === module) {
}

export default app
export { serviceStatus, allServicesReady }
export { serviceStatus, allServicesReady }
Loading