From f870a79377d0d000a6e16b060fe3984ae350391a Mon Sep 17 00:00:00 2001 From: charliechinedu19-netizen Date: Tue, 30 Jun 2026 00:22:27 +0100 Subject: [PATCH] docs: publish versioned OpenAPI specification --- README.md | 27 +- docs/openapi.yaml | 2332 ++++++++++++++++++++++++++++++++++++++------- package-lock.json | 26 +- package.json | 6 +- src/index.ts | 34 + 5 files changed, 2064 insertions(+), 361 deletions(-) diff --git a/README.md b/README.md index a5f0494..f0c125e 100644 --- a/README.md +++ b/README.md @@ -20,22 +20,39 @@ It covers: |---|---|---| | health | `/health` | None | | auth | `/api/auth` | None (issues JWT) | +| agent | `/api/agent` | Internal token | +| whatsapp | `/api/whatsapp` | Twilio signature | | portfolio | `/api/portfolio` | Bearer JWT | | transactions | `/api/transactions` | Bearer JWT | +| protocols | `/api/protocols` | None | | deposit | `/api/deposit` | Bearer JWT | | withdraw | `/api/withdraw` | Bearer JWT | -| vault | `/api/vault` | Bearer JWT | -| admin | `/api/admin` | `X-Admin-Token` header | +| vault | `/api/vault` | Mixed (public + Bearer JWT) | +| analytics | `/api/analytics` | Mixed (public + Bearer JWT) | +| stellar | `/api/stellar` | None | +| admin | `/api/admin` | Admin API key (Bearer or `X-Admin-Token`) | +| metrics | `/metrics` | Internal token (strict) | -### Viewing the docs locally +### Viewing the docs + +**Swagger UI** (available when the server is running): + +| URL | Description | +|---|---| +| `http://localhost:3000/api/v1/docs` | Interactive API explorer | +| `http://localhost:3000/docs` | Alias for the above | +| `http://localhost:3000/api/v1/openapi.yaml` | Raw spec YAML | +| `http://localhost:3000/openapi.yaml` | Alias for the above | + +### Validating the spec ```bash -npx @redocly/cli preview-docs docs/openapi.yaml +npm run validate:spec ``` ### Updating the spec -When you add or change a route, update `docs/openapi.yaml` in the same PR. The `api-contract` CI job will lint the spec and run smoke tests automatically. +When you add or change a route, update `docs/openapi.yaml` in the same PR. Run `npm run validate:spec` to ensure the spec is valid before committing. The `prebuild` step also validates and copies the spec to `dist/docs/openapi.yaml` as a build artifact. ### Breaking-change policy diff --git a/docs/openapi.yaml b/docs/openapi.yaml index 44cd2b6..5879245 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -7,19 +7,18 @@ info: REST API for the NeuroWealth platform — AI-assisted portfolio management backed by Stellar smart contracts. - **Breaking-change policy:** This API follows semantic versioning. Breaking - changes (removed fields, changed response shapes, new required params) - increment the major version and are announced in CHANGELOG.md at least two - weeks before release. - **Versioning:** All endpoints are served under an explicit version prefix (`/api/v1/*`). The legacy unversioned paths (`/api/*`) remain available as deprecated aliases and return `Deprecation`/`Sunset` headers. Every response includes an `X-API-Version` header. See `docs/api-versioning.md`. + **Breaking-change policy:** This API follows semantic versioning. Breaking + changes (removed fields, changed response shapes, new required parameters) + increment the major version and are announced at least two weeks before release. + servers: - url: http://localhost:{port} - description: Local development (versioned base path included in each route) + description: Local development variables: port: default: '3000' @@ -31,181 +30,32 @@ tags: description: Liveness and readiness probes - name: auth description: Wallet-based authentication (Stellar SIWE challenge/verify) + - name: agent + description: Agent loop control and monitoring + - name: whatsapp + description: Twilio WhatsApp webhook integration - name: portfolio - description: Portfolio positions, performance, and NLP queries + description: Portfolio positions, performance, and earnings - name: transactions description: Transaction history and details + - name: protocols + description: Protocol rates and agent status - name: deposit - description: Deposit flows + description: On-chain deposit operations - name: withdraw - description: Withdrawal flows + description: On-chain withdrawal operations - name: vault description: Vault / savings product + - name: analytics + description: Analytics — APY history, user yield, protocol performance + - name: stellar + description: Stellar event listener metrics - name: admin description: Admin-only management endpoints + - name: metrics + description: Prometheus-compatible metrics endpoint -# ── Security schemes ───────────────────────────────────────────────────────── -components: - securitySchemes: - BearerAuth: - type: http - scheme: bearer - bearerFormat: JWT - description: JWT issued by `/api/v1/auth/verify`. Include as `Authorization: Bearer `. - AdminToken: - type: apiKey - in: header - name: X-Admin-Token - description: Static admin token set via `ADMIN_SECRET` env var. Required for all `/api/v1/admin/*` routes. - InternalToken: - type: apiKey - in: header - name: X-Internal-Token - description: Service-to-service token for internal calls (e.g. Stellar event relayer). - - # ── Reusable schemas ─────────────────────────────────────────────────────── - schemas: - ErrorResponse: - type: object - required: [error] - properties: - error: - type: string - example: Unauthorized - details: - description: Optional structured detail (array or object) - oneOf: - - type: array - items: {} - - type: object - - HealthResponse: - type: object - required: [status, timestamp, version, environment] - properties: - status: - type: string - enum: [ok, degraded] - timestamp: - type: string - format: date-time - version: - type: string - example: 1.0.0 - environment: - type: string - enum: [development, test, production] - - AuthChallengeResponse: - type: object - required: [nonce, expiresAt] - properties: - nonce: - type: string - description: Random one-time nonce the wallet must sign - example: a3f9e2c1d0b84756 - expiresAt: - type: string - format: date-time - - AuthVerifyRequest: - type: object - required: [publicKey, signature, nonce] - properties: - publicKey: - type: string - description: Stellar G-address (Ed25519 public key) - example: GDQP2KPQGKIHYJGXNUIYOMHARUARCA7DJT5FO2FFOOKY3B2WSQHG4W37 - signature: - type: string - description: Base64-encoded signature over the nonce - nonce: - type: string - - AuthVerifyResponse: - type: object - required: [token] - properties: - token: - type: string - description: JWT for use in subsequent requests - expiresIn: - type: integer - description: Seconds until the token expires - example: 86400 - - Portfolio: - type: object - required: [address, totalValueUsdc, positions] - properties: - address: - type: string - totalValueUsdc: - type: string - description: Total portfolio value in USDC (string to avoid float precision loss) - example: '12450.75' - pnlPercent: - type: string - example: '4.23' - positions: - type: array - items: - $ref: '#/components/schemas/Position' - - Position: - type: object - required: [asset, amount, valueUsdc] - properties: - asset: - type: string - example: XLM - amount: - type: string - example: '5000.0000000' - valueUsdc: - type: string - example: '600.00' - pnlPercent: - type: string - - Transaction: - type: object - required: [id, type, amount, asset, status, createdAt] - properties: - id: - type: string - type: - type: string - enum: [deposit, withdrawal, swap, vault_deposit, vault_withdraw] - amount: - type: string - asset: - type: string - status: - type: string - enum: [pending, confirmed, failed] - stellarTxHash: - type: string - createdAt: - type: string - format: date-time - - VaultPosition: - type: object - required: [balance, yieldEarned, lockUntil] - properties: - balance: - type: string - yieldEarned: - type: string - lockUntil: - type: string - format: date-time - nullable: true - -# ── Paths ───────────────────────────────────────────────────────────────────── paths: - # ── Health ───────────────────────────────────────────────────────────────── /health: get: @@ -220,6 +70,11 @@ paths: application/json: schema: $ref: '#/components/schemas/HealthResponse' + example: + status: ok + timestamp: '2026-06-29T12:00:00.000Z' + version: 1.0.0 + environment: production /health/ready: get: @@ -233,27 +88,67 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/HealthResponse' + type: object + properties: + ready: + type: boolean + enum: [true] + subsystems: + type: object + timestamp: + type: string + format: date-time + example: + ready: true + subsystems: + database: healthy + eventListener: connected + agentLoop: running + timestamp: '2026-06-29T12:00:00.000Z' '503': description: One or more dependencies unavailable content: application/json: schema: - $ref: '#/components/schemas/ErrorResponse' + type: object + properties: + ready: + type: boolean + enum: [false] + subsystems: + type: object + timestamp: + type: string + format: date-time + example: + ready: false + subsystems: + database: healthy + eventListener: disconnected + agentLoop: running + timestamp: '2026-06-29T12:00:00.000Z' # ── Auth ─────────────────────────────────────────────────────────────────── /api/v1/auth/challenge: - get: + post: tags: [auth] operationId: getAuthChallenge summary: Request a sign-in challenge nonce - parameters: - - in: query - name: publicKey - required: true - schema: - type: string - description: Stellar G-address requesting a nonce + description: | + Generates a one-time nonce that the caller must sign with their Stellar + keypair. The nonce expires after a configurable TTL (default 5 min). + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [stellarPubKey] + properties: + stellarPubKey: + type: string + description: Stellar G-address (Ed25519 public key) + example: GDQP2KPQGKIHYJGXNUIYOMHARUARCA7DJT5FO2FFOOKY3B2WSQHG4W37 responses: '200': description: Challenge nonce issued @@ -261,43 +156,56 @@ paths: application/json: schema: $ref: '#/components/schemas/AuthChallengeResponse' + example: + nonce: nw-auth-a3f9e2c1d0b84756... + expiresAt: '2026-06-29T12:05:00.000Z' '400': - description: Invalid public key - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' + description: Invalid Stellar public key + $ref: '#/components/responses/BadRequest' /api/v1/auth/verify: post: tags: [auth] operationId: verifyAuthSignature summary: Verify wallet signature and issue JWT + description: | + Verifies the Stellar signature over the nonce obtained from `/challenge`. + On success creates a new user if one does not exist and issues an + access token + refresh token pair with rotation. requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/AuthVerifyRequest' + example: + stellarPubKey: GDQP2KPQGKIHYJGXNUIYOMHARUARCA7DJT5FO2FFOOKY3B2WSQHG4W37 + signature: AQAAAAA... responses: '200': - description: Signature valid — JWT issued + description: Signature valid — token pair issued content: application/json: schema: $ref: '#/components/schemas/AuthVerifyResponse' + example: + accessToken: eyJhbGciOiJIUzI1NiIs... + refreshToken: nw-ref-a3f9e2c1d0b8... + userId: a1b2c3d4-e5f6-7890-abcd-ef1234567890 + expiresAt: '2026-06-29T12:15:00.000Z' + refreshExpiresAt: '2026-07-06T12:00:00.000Z' '401': - description: Invalid or expired signature - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' + description: Invalid or expired signature / nonce + $ref: '#/components/responses/Unauthorized' /api/v1/auth/logout: post: tags: [auth] operationId: logout - summary: Invalidate the current JWT + summary: Invalidate the current session + description: | + Revokes the session associated with the provided Bearer token. + Both the access token and refresh token are invalidated. security: - BearerAuth: [] responses: @@ -308,151 +216,582 @@ paths: schema: type: object properties: - success: - type: boolean - example: true + message: + type: string + example: Logged out successfully '401': $ref: '#/components/responses/Unauthorized' - # ── Portfolio ────────────────────────────────────────────────────────────── - /api/v1/portfolio: + # ── Agent ────────────────────────────────────────────────────────────────── + /api/v1/agent/status: get: - tags: [portfolio] - operationId: getPortfolio - summary: Get the authenticated user's portfolio + tags: [agent] + operationId: getAgentStatus + summary: Get agent loop status + description: | + Returns current agent health and operational status. + Protected by internal auth (X-Internal-Token, IP allowlist, or admin Bearer token). security: - - BearerAuth: [] + - InternalToken: [] responses: '200': - description: Portfolio data + description: Agent status content: application/json: schema: - $ref: '#/components/schemas/Portfolio' - '401': - $ref: '#/components/responses/Unauthorized' + $ref: '#/components/schemas/AgentStatusResponse' + example: + success: true + data: + isRunning: true + lastRebalanceAt: '2026-06-29T11:30:00.000Z' + currentProtocol: Aave + currentApy: 4.23 + nextScheduledCheck: '2026-06-29T12:30:00.000Z' + lastError: null + healthStatus: healthy + timestamp: '2026-06-29T12:00:00.000Z' + '403': + $ref: '#/components/responses/Forbidden' - /api/v1/portfolio/query: + # ── WhatsApp ─────────────────────────────────────────────────────────────── + /api/v1/whatsapp/webhook: + get: + tags: [whatsapp] + operationId: whatsappWebhookHealth + summary: WhatsApp webhook health check + description: Twilio webhook endpoint health check. Returns a simple text response. + responses: + '200': + description: Webhook is alive + content: + text/plain: + schema: + type: string + example: WhatsApp webhook is alive post: - tags: [portfolio] - operationId: queryPortfolio - summary: Natural-language portfolio query (NLP) - security: - - BearerAuth: [] + tags: [whatsapp] + operationId: handleWhatsAppMessage + summary: Handle incoming WhatsApp message + description: | + Receives incoming WhatsApp messages from Twilio via webhook. + Validates the x-twilio-signature header. Returns TwiML response. requestBody: required: true content: - application/json: + application/x-www-form-urlencoded: schema: type: object - required: [query] + required: [From, Body] properties: - query: + From: type: string - example: What is my XLM allocation? + description: WhatsApp sender number + example: '+1234567890' + Body: + type: string + description: Message body + example: What is my portfolio? + responses: + '200': + description: TwiML response + content: + text/xml: + schema: + type: string + example: 'Your portfolio value is 12,450.75 USDC' + '403': + description: Missing or invalid Twilio signature + content: + text/plain: + schema: + type: string + example: 'Forbidden: invalid Twilio signature' + + # ── Portfolio ────────────────────────────────────────────────────────────── + /api/v1/portfolio/{userId}: + get: + tags: [portfolio] + operationId: getPortfolio + summary: Get portfolio positions + description: | + Returns the user's portfolio including all active positions, + total balance, and total earnings. The authenticated user can + only access their own portfolio (enforceUserAccess). + security: + - BearerAuth: [] + parameters: + - in: path + name: userId + required: true + schema: + type: string + format: uuid + description: User ID (UUID v4) responses: '200': - description: NLP answer + description: Portfolio data content: application/json: schema: - type: object - required: [answer] - properties: - answer: - type: string + $ref: '#/components/schemas/PortfolioResponse' + example: + userId: a1b2c3d4-e5f6-7890-abcd-ef1234567890 + totalBalance: 12450.75 + totalEarnings: 523.40 + activePositions: 3 + positions: + - id: pos-001 + protocolName: Aave + assetSymbol: USDC + currentValue: 10000.00 + yieldEarned: 423.15 + status: ACTIVE + - id: pos-002 + protocolName: Compound + assetSymbol: XLM + currentValue: 2000.00 + yieldEarned: 85.50 + status: ACTIVE + - id: pos-003 + protocolName: unassigned + assetSymbol: USDC + currentValue: 450.75 + yieldEarned: 14.75 + status: ACTIVE '401': $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/NotFound' - # ── Transactions ─────────────────────────────────────────────────────────── - /api/v1/transactions: + /api/v1/portfolio/{userId}/history: get: - tags: [transactions] - operationId: listTransactions - summary: List the authenticated user's transactions + tags: [portfolio] + operationId: getPortfolioHistory + summary: Get portfolio yield history + description: Returns historical yield snapshots for the user's positions over a given period. security: - BearerAuth: [] parameters: - - in: query - name: limit - schema: - type: integer - default: 20 - maximum: 100 - - in: query - name: cursor + - in: path + name: userId + required: true schema: type: string - description: Pagination cursor (transaction ID) + format: uuid + description: User ID (UUID v4) - in: query - name: type + name: period schema: type: string - enum: [deposit, withdrawal, swap, vault_deposit, vault_withdraw] + enum: [7d, 30d, 90d] + default: 30d + description: Lookback period responses: '200': - description: Transaction list + description: Portfolio history content: application/json: schema: type: object - required: [data] properties: - data: + userId: + type: string + format: uuid + period: + type: string + enum: [7d, 30d, 90d] + points: type: array items: - $ref: '#/components/schemas/Transaction' - nextCursor: - type: string - nullable: true + type: object + properties: + date: + type: string + format: date + yieldAmount: + type: number + example: + userId: a1b2c3d4-e5f6-7890-abcd-ef1234567890 + period: 30d + points: + - date: 2026-06-01 + yieldAmount: 12.50 + - date: 2026-06-02 + yieldAmount: 13.20 '401': $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/NotFound' - # ── Deposit ──────────────────────────────────────────────────────────────── - /api/v1/deposit: - post: - tags: [deposit] - operationId: initiateDeposit - summary: Initiate a deposit + /api/v1/portfolio/{userId}/earnings: + get: + tags: [portfolio] + operationId: getPortfolioEarnings + summary: Get portfolio earnings summary + description: Returns total and period earnings with average APY for the user. security: - BearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - type: object - required: [asset, amount] - properties: - asset: - type: string - example: USDC - amount: - type: string - example: '100.0000000' + parameters: + - in: path + name: userId + required: true + schema: + type: string + format: uuid + description: User ID (UUID v4) responses: - '201': - description: Deposit initiated + '200': + description: Portfolio earnings content: application/json: schema: type: object - required: [transaction] properties: - transaction: - $ref: '#/components/schemas/Transaction' - '400': - $ref: '#/components/responses/BadRequest' + userId: + type: string + format: uuid + totalEarnings: + type: number + periodEarnings: + type: number + averageApy: + type: number + example: + userId: a1b2c3d4-e5f6-7890-abcd-ef1234567890 + totalEarnings: 523.40 + periodEarnings: 85.20 + averageApy: 4.23 '401': $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/NotFound' - # ── Withdraw ─────────────────────────────────────────────────────────────── - /api/v1/withdraw: - post: - tags: [withdraw] - operationId: initiateWithdrawal - summary: Initiate a withdrawal - security: + # ── Transactions ─────────────────────────────────────────────────────────── + /api/v1/transactions/detail/{txHash}: + get: + tags: [transactions] + operationId: getTransactionDetail + summary: Get transaction detail + description: Returns full detail for a single transaction owned by the authenticated user. + security: + - BearerAuth: [] + parameters: + - in: path + name: txHash + required: true + schema: + type: string + description: Transaction hash + responses: + '200': + description: Transaction detail + content: + application/json: + schema: + type: object + properties: + transaction: + $ref: '#/components/schemas/Transaction' + example: + transaction: + id: tx-001 + txHash: a1b2c3d4e5f6... + type: DEPOSIT + status: CONFIRMED + amount: 1000.00 + assetSymbol: USDC + protocolName: Aave + createdAt: '2026-06-28T10:00:00.000Z' + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/NotFound' + + /api/v1/transactions/{userId}: + get: + tags: [transactions] + operationId: listTransactions + summary: List user transactions + description: Returns a paginated list of transactions for the given user. + security: + - BearerAuth: [] + parameters: + - in: path + name: userId + required: true + schema: + type: string + format: uuid + description: User ID (UUID v4) + - in: query + name: page + schema: + type: integer + minimum: 1 + default: 1 + description: Page number (1-indexed) + - in: query + name: limit + schema: + type: integer + minimum: 1 + maximum: 50 + default: 5 + description: Items per page + responses: + '200': + description: Transaction list + content: + application/json: + schema: + type: object + properties: + page: + type: integer + limit: + type: integer + total: + type: integer + transactions: + type: array + items: + $ref: '#/components/schemas/Transaction' + example: + page: 1 + limit: 5 + total: 42 + transactions: + - id: tx-001 + txHash: a1b2c3d4e5f6... + type: DEPOSIT + status: CONFIRMED + amount: 1000.00 + assetSymbol: USDC + protocolName: Aave + createdAt: '2026-06-28T10:00:00.000Z' + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/NotFound' + + /api/v1/transactions/{id}/events: + get: + tags: [transactions] + operationId: getTransactionEvents + summary: Get transaction event history + description: Returns the ordered event history for a transaction. + security: + - BearerAuth: [] + parameters: + - in: path + name: id + required: true + schema: + type: string + format: uuid + description: Transaction ID (UUID v4) + responses: + '200': + description: Transaction events + content: + application/json: + schema: + type: object + properties: + transactionId: + type: string + events: + type: array + items: + type: object + example: + transactionId: tx-001 + events: + - id: evt-001 + eventType: SUBMITTED + occurredAt: '2026-06-28T10:00:05.000Z' + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/NotFound' + + # ── Protocols ────────────────────────────────────────────────────────────── + /api/v1/protocols/rates: + get: + tags: [protocols] + operationId: getProtocolRates + summary: Get current protocol rates + description: Returns the latest supply/borrow APY rates across all protocols. + responses: + '200': + description: Protocol rates + content: + application/json: + schema: + type: object + properties: + rates: + type: array + items: + $ref: '#/components/schemas/ProtocolRate' + example: + rates: + - protocolName: Aave + assetSymbol: USDC + supplyApy: 4.23 + borrowApy: 6.15 + tvl: 125000000 + network: stellar + fetchedAt: '2026-06-29T12:00:00.000Z' + + /api/v1/protocols/agent/status: + get: + tags: [protocols] + operationId: getProtocolAgentStatus + summary: Get rebalancing agent status + description: Returns agent loop health and status information for the rebalancing agent. + responses: + '200': + description: Agent status + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + data: + type: object + properties: + isRunning: + type: boolean + healthStatus: + type: string + lastRebalanceAt: + type: string + format: date-time + nullable: true + currentProtocol: + type: string + nullable: true + currentApy: + type: number + nullable: true + nextScheduledCheck: + type: string + format: date-time + lastError: + type: string + nullable: true + latestLog: + type: object + nullable: true + properties: + status: + type: string + action: + type: string + createdAt: + type: string + format: date-time + timestamp: + type: string + format: date-time + example: + success: true + data: + isRunning: true + healthStatus: healthy + lastRebalanceAt: '2026-06-29T11:30:00.000Z' + currentProtocol: Aave + currentApy: 4.23 + nextScheduledCheck: '2026-06-29T12:30:00.000Z' + lastError: null + latestLog: + status: success + action: REBALANCE + createdAt: '2026-06-29T11:30:00.000Z' + timestamp: '2026-06-29T12:00:00.000Z' + + # ── Deposit ──────────────────────────────────────────────────────────────── + /api/v1/deposit: + post: + tags: [deposit] + operationId: initiateDeposit + summary: Initiate an on-chain deposit + description: | + Submits a deposit transaction to the active protocol on behalf of the + authenticated user. The user must match the userId in the request body. + security: + - BearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [userId, amount, assetSymbol] + properties: + userId: + type: string + format: uuid + description: User ID (UUID v4) + amount: + type: number + exclusiveMinimum: 0 + description: Deposit amount + assetSymbol: + type: string + minLength: 1 + description: Asset symbol (e.g. USDC, XLM) + protocolName: + type: string + description: Target protocol (optional, uses active protocol if omitted) + memo: + type: string + maxLength: 280 + description: Optional memo + example: + userId: a1b2c3d4-e5f6-7890-abcd-ef1234567890 + amount: 1000.00 + assetSymbol: USDC + protocolName: Aave + responses: + '201': + description: Deposit initiated + content: + application/json: + schema: + $ref: '#/components/schemas/TransactionResponse' + example: + txHash: a1b2c3d4e5f6... + status: CONFIRMED + transaction: + id: tx-001 + txHash: a1b2c3d4e5f6... + status: CONFIRMED + amount: 1000.00 + assetSymbol: USDC + protocolName: Aave + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '409': + description: Duplicate transaction hash + $ref: '#/components/responses/Conflict' + + # ── Withdraw ─────────────────────────────────────────────────────────────── + /api/v1/withdraw: + post: + tags: [withdraw] + operationId: initiateWithdrawal + summary: Initiate an on-chain withdrawal + description: | + Submits a withdrawal transaction from the active protocol on behalf of + the authenticated user. The user must match the userId in the request body. + security: - BearerAuth: [] requestBody: required: true @@ -460,71 +799,879 @@ paths: application/json: schema: type: object - required: [asset, amount, destinationAddress] + required: [userId, amount, assetSymbol] properties: - asset: + userId: type: string - example: XLM + format: uuid amount: + type: number + exclusiveMinimum: 0 + assetSymbol: type: string - example: '50.0000000' - destinationAddress: + minLength: 1 + protocolName: type: string + memo: + type: string + maxLength: 280 + example: + userId: a1b2c3d4-e5f6-7890-abcd-ef1234567890 + amount: 500.00 + assetSymbol: USDC responses: '201': description: Withdrawal initiated content: application/json: schema: - type: object - required: [transaction] - properties: - transaction: - $ref: '#/components/schemas/Transaction' + $ref: '#/components/schemas/TransactionResponse' + example: + txHash: f6e5d4c3b2a1... + status: CONFIRMED + transaction: + id: tx-002 + txHash: f6e5d4c3b2a1... + status: CONFIRMED + amount: 500.00 + assetSymbol: USDC + protocolName: Aave '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' + '409': + $ref: '#/components/responses/Conflict' # ── Vault ────────────────────────────────────────────────────────────────── - /api/v1/vault: + /api/v1/vault/state: get: tags: [vault] - operationId: getVaultPosition - summary: Get the user's vault position + operationId: getVaultState + summary: Get global vault state + description: Returns current vault APY and the active protocol name. No auth required. + responses: + '200': + description: Vault state + content: + application/json: + schema: + type: object + properties: + apy: + type: number + description: Current vault APY + activeProtocol: + type: string + description: Active protocol name + example: + apy: 4.23 + activeProtocol: Aave + + /api/v1/vault/balance: + get: + tags: [vault] + operationId: getVaultBalance + summary: Get user's vault balance + description: Returns the authenticated user's on-chain vault balance and shares. + security: + - BearerAuth: [] + responses: + '200': + description: Vault balance + content: + application/json: + schema: + type: object + properties: + balance: + type: number + shares: + type: number + example: + balance: 5000.00 + shares: 4950.00 + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/NotFound' + + /api/v1/vault/build-transaction: + post: + tags: [vault] + operationId: buildVaultTransaction + summary: Build unsigned vault transaction XDR (non-custodial) + description: | + Builds an unsigned Stellar XDR transaction for vault deposit or withdrawal. + The user signs the XDR client-side — the backend never holds private keys. security: - BearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [type, amount, assetSymbol] + properties: + type: + type: string + enum: [deposit, withdraw] + amount: + type: number + exclusiveMinimum: 0 + assetSymbol: + type: string + minLength: 1 + example: + type: deposit + amount: 1000.00 + assetSymbol: USDC responses: '200': - description: Vault position + description: Unsigned XDR built content: application/json: schema: - $ref: '#/components/schemas/VaultPosition' + type: object + properties: + xdr: + type: string + description: Unsigned Stellar transaction XDR + type: + type: string + enum: [deposit, withdraw] + amount: + type: number + walletAddress: + type: string + example: + xdr: AAAAAA... + type: deposit + amount: 1000.00 + walletAddress: GDQP2KPQGKIHYJGXNUIYOMHARUARCA7DJT5FO2FFOOKY3B2WSQHG4W37 + '400': + $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' - /api/v1/vault/deposit: + # ── Analytics ────────────────────────────────────────────────────────────── + /api/v1/analytics/apy-history: + get: + tags: [analytics] + operationId: getApyHistory + summary: Get APY history + description: Returns APY snapshots over time for the authenticated user's positions (graph-ready). + security: + - BearerAuth: [] + parameters: + - in: query + name: period + schema: + type: string + enum: [7d, 30d, 90d] + default: 30d + description: Lookback period + responses: + '200': + description: APY history + content: + application/json: + schema: + type: object + properties: + userId: + type: string + period: + type: string + points: + type: array + items: + type: object + properties: + date: + type: string + format: date + apy: + type: number + positionId: + type: string + example: + userId: a1b2c3d4-e5f6-7890-abcd-ef1234567890 + period: 30d + points: + - date: 2026-06-01 + apy: 4.10 + positionId: pos-001 + - date: 2026-06-02 + apy: 4.15 + positionId: pos-001 + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + + /api/v1/analytics/user-yield: + get: + tags: [analytics] + operationId: getUserYield + summary: Get user yield summary + description: Returns cumulative and period yield earned by the authenticated user. + security: + - BearerAuth: [] + parameters: + - in: query + name: period + schema: + type: string + enum: [7d, 30d, 90d] + default: 30d + responses: + '200': + description: User yield + content: + application/json: + schema: + type: object + properties: + userId: + type: string + period: + type: string + totalYield: + type: number + periodYield: + type: number + averageApy: + type: number + points: + type: array + items: + type: object + properties: + date: + type: string + format: date + yieldAmount: + type: number + apy: + type: number + example: + userId: a1b2c3d4-e5f6-7890-abcd-ef1234567890 + period: 30d + totalYield: 523.40 + periodYield: 85.20 + averageApy: 4.23 + points: + - date: 2026-06-01 + yieldAmount: 12.50 + apy: 4.10 + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + + /api/v1/analytics/protocol-performance: + get: + tags: [analytics] + operationId: getProtocolPerformance + summary: Get protocol APY performance + description: Returns historical APY rates per protocol (graph-ready). No auth required. + parameters: + - in: query + name: period + schema: + type: string + enum: [7d, 30d, 90d] + default: 30d + responses: + '200': + description: Protocol performance + content: + application/json: + schema: + type: object + properties: + period: + type: string + protocols: + type: array + items: + type: object + properties: + protocol: + type: string + asset: + type: string + network: + type: string + points: + type: array + items: + type: object + properties: + date: + type: string + format: date + apy: + type: number + tvl: + type: number + nullable: true + example: + period: 30d + protocols: + - protocol: Aave + asset: USDC + network: stellar + points: + - date: 2026-06-01 + apy: 4.10 + tvl: 125000000 + '400': + $ref: '#/components/responses/BadRequest' + + # ── Stellar ──────────────────────────────────────────────────────────────── + /api/v1/stellar/metrics: + get: + tags: [stellar] + operationId: getStellarMetrics + summary: Get Stellar event-processing metrics + description: Returns current event-processing metrics from the Stellar event listener. + responses: + '200': + description: Stellar metrics + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + data: + type: object + properties: + totalProcessed: + type: integer + totalErrors: + type: integer + processingRatePerMinute: + type: number + errorRate: + type: number + ledgerLag: + type: integer + lastDbOperationMs: + type: number + lastUpdated: + type: string + format: date-time + example: + success: true + data: + totalProcessed: 15234 + totalErrors: 23 + processingRatePerMinute: 45.2 + errorRate: 0.15 + ledgerLag: 2 + lastDbOperationMs: 42 + lastUpdated: '2026-06-29T12:00:00.000Z' + + # ── Admin ────────────────────────────────────────────────────────────────── + /api/v1/admin/users: + get: + tags: [admin] + operationId: adminListUsers + summary: List all users + description: Returns a paginated list of all users. Requires admin authentication. + security: + - AdminToken: [] + parameters: + - in: query + name: limit + schema: + type: integer + default: 50 + maximum: 500 + - in: query + name: cursor + schema: + type: string + responses: + '200': + description: User list + content: + application/json: + schema: + type: object + properties: + data: + type: array + items: + type: object + properties: + address: + type: string + createdAt: + type: string + format: date-time + nextCursor: + type: string + nullable: true + example: + data: + - address: GDQP2KPQGKIHYJGXNUIYOMHARUARCA7DJT5FO2FFOOKY3B2WSQHG4W37 + createdAt: '2026-01-15T10:00:00.000Z' + nextCursor: null + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + + /api/v1/admin/stats: + get: + tags: [admin] + operationId: adminGetStats + summary: Platform statistics + description: Returns platform-wide statistics. Requires admin authentication. + security: + - AdminToken: [] + responses: + '200': + description: Platform stats + content: + application/json: + schema: + type: object + properties: + totalUsers: + type: integer + totalVolumeUsdc: + type: string + activeVaults: + type: integer + example: + totalUsers: 1284 + totalVolumeUsdc: '12500000.00' + activeVaults: 856 + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + + /api/v1/admin/stellar/metrics: + get: + tags: [admin] + operationId: adminGetStellarMetrics + summary: Stellar event processing metrics (admin) + description: | + Returns detailed event processing metrics including processing rate, + error rate, and ledger lag. Requires admin scope `metrics:read`. + security: + - AdminToken: [] + responses: + '200': + description: Stellar metrics + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + data: + type: object + properties: + totalProcessed: + type: integer + totalErrors: + type: integer + processingRatePerMinute: + type: number + errorRate: + type: string + ledgerLag: + type: integer + lastDbOperationMs: + type: number + lastUpdated: + type: string + format: date-time + timestamp: + type: string + format: date-time + example: + success: true + data: + totalProcessed: 15234 + totalErrors: 23 + processingRatePerMinute: 45.2 + errorRate: 0.15% + ledgerLag: 2 + lastDbOperationMs: 42 + lastUpdated: '2026-06-29T12:00:00.000Z' + timestamp: '2026-06-29T12:00:00.000Z' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + + /api/v1/admin/dlq/inspect: + get: + tags: [admin] + operationId: adminInspectDLQ + summary: Inspect dead-letter queue + description: | + Returns current DLQ contents with filtering and pagination. + Requires admin scope `dlq:read`. + security: + - AdminToken: [] + parameters: + - in: query + name: status + schema: + type: string + enum: [PENDING, RETRIED, RESOLVED] + description: Filter by event status + - in: query + name: eventType + schema: + type: string + description: Filter by event type + - in: query + name: retryCountMin + schema: + type: integer + default: 0 + description: Minimum retry count + - in: query + name: retryCountMax + schema: + type: integer + description: Maximum retry count + - in: query + name: timeRangeStart + schema: + type: string + format: date-time + description: Earliest event timestamp + - in: query + name: timeRangeEnd + schema: + type: string + format: date-time + description: Latest event timestamp + - in: query + name: limit + schema: + type: integer + default: 50 + maximum: 500 + - in: query + name: offset + schema: + type: integer + default: 0 + responses: + '200': + description: DLQ contents + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + data: + type: object + properties: + totalInQueue: + type: integer + filteredCount: + type: integer + returnedCount: + type: integer + pagination: + type: object + properties: + offset: + type: integer + limit: + type: integer + hasMore: + type: boolean + items: + type: array + items: + type: object + properties: + id: + type: string + contractId: + type: string + txHash: + type: string + eventType: + type: string + ledger: + type: integer + status: + type: string + retryCount: + type: integer + error: + type: string + nullable: true + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + timestamp: + type: string + format: date-time + example: + success: true + data: + totalInQueue: 15 + filteredCount: 10 + returnedCount: 10 + pagination: + offset: 0 + limit: 50 + hasMore: false + items: + - id: dlq-001 + contractId: CA123... + txHash: a1b2c3d4... + eventType: DEPOSIT + ledger: 1234567 + status: PENDING + retryCount: 3 + error: 'Insufficient gas' + createdAt: '2026-06-29T10:00:00.000Z' + updatedAt: '2026-06-29T11:00:00.000Z' + timestamp: '2026-06-29T12:00:00.000Z' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + + /api/v1/admin/dlq/retry: + post: + tags: [admin] + operationId: adminRetryDLQ + summary: Retry all pending DLQ events + description: | + Manually retries all pending dead-letter events. + Requires admin scope `dlq:write`. + security: + - AdminToken: [] + requestBody: + content: + application/json: + schema: + type: object + properties: + dryRun: + type: boolean + description: If true, returns what would be retried without executing + default: false + responses: + '200': + description: Retry initiated or dry-run result + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + data: + type: object + properties: + resolved: + type: integer + failed: + type: integer + totalRemaining: + type: integer + timestamp: + type: string + format: date-time + example: + success: true + data: + resolved: 8 + failed: 2 + totalRemaining: 5 + timestamp: '2026-06-29T12:00:00.000Z' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + + /api/v1/admin/dlq/resolve: + post: + tags: [admin] + operationId: adminResolveDLQEvent + summary: Manually resolve a DLQ event + description: | + Marks a specific DLQ event as resolved. + Requires admin scope `dlq:write`. + security: + - AdminToken: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [eventId] + properties: + eventId: + type: string + description: DLQ event ID + responses: + '200': + description: Event resolved + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + data: + type: object + properties: + eventId: + type: string + status: + type: string + enum: [RESOLVED] + timestamp: + type: string + format: date-time + example: + success: true + data: + eventId: dlq-001 + status: RESOLVED + timestamp: '2026-06-29T12:00:00.000Z' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' + + /api/v1/admin/dlq/replay: + post: + tags: [admin] + operationId: adminReplayDLQEvents + summary: Replay selected DLQ events + description: | + Safely replays selected DLQ events back into the processing pipeline. + Only retries events in PENDING or RETRIED status. + Requires admin scope `dlq:write`. + security: + - AdminToken: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [eventIds] + properties: + eventIds: + type: array + items: + type: string + maxItems: 1000 + description: DLQ event IDs to replay + dryRun: + type: boolean + default: false + responses: + '200': + description: Replay result + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + data: + type: object + properties: + requestedCount: + type: integer + replayedCount: + type: integer + blockedCount: + type: integer + resolved: + type: integer + failed: + type: integer + timestamp: + type: string + format: date-time + example: + success: true + data: + requestedCount: 5 + replayedCount: 5 + blockedCount: 0 + resolved: 4 + failed: 1 + timestamp: '2026-06-29T12:00:00.000Z' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' + + /api/v1/admin/stellar/backfill: post: - tags: [vault] - operationId: depositToVault - summary: Move funds into the vault + tags: [admin] + operationId: adminBackfillStellarEvents + summary: Backfill Stellar events for a ledger range + description: | + Manually triggers event backfill for a ledger range. + Requires admin scope `backfill:write`. security: - - BearerAuth: [] + - AdminToken: [] requestBody: required: true content: application/json: schema: type: object - required: [amount] + required: [startLedger] properties: - amount: - type: string - example: '200.0000000' + startLedger: + type: integer + minimum: 0 + description: Starting ledger sequence number + endLedger: + type: integer + description: Ending ledger sequence number (optional, defaults to latest) + example: + startLedger: 1234000 + endLedger: 1235000 responses: - '201': - description: Vault deposit recorded + '200': + description: Backfill initiated content: application/json: schema: @@ -532,31 +1679,144 @@ paths: properties: success: type: boolean - transaction: - $ref: '#/components/schemas/Transaction' + data: + type: object + properties: + startLedger: + type: integer + endLedger: + type: string + status: + type: string + message: + type: string + timestamp: + type: string + format: date-time + example: + success: true + data: + startLedger: 1234000 + endLedger: latest + status: backfill_initiated + message: 'Backfill operation initiated. Check logs for progress.' + timestamp: '2026-06-29T12:00:00.000Z' + '400': + $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' - /api/v1/vault/withdraw: + /api/v1/admin/keys: + get: + tags: [admin] + operationId: adminListKeys + summary: List admin API keys + description: | + Lists all admin API key metadata (no hashes or tokens returned). + Requires admin scope `keys:read`. + security: + - AdminToken: [] + responses: + '200': + description: List of admin keys + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + data: + type: array + items: + type: object + properties: + id: + type: string + name: + type: string + role: + type: string + scopes: + type: array + items: + type: string + expiresAt: + type: string + format: date-time + nullable: true + revokedAt: + type: string + format: date-time + nullable: true + lastUsedAt: + type: string + format: date-time + nullable: true + createdAt: + type: string + format: date-time + timestamp: + type: string + format: date-time + example: + success: true + data: + - id: key-001 + name: CI Pipeline + role: service + scopes: [read, write] + expiresAt: null + revokedAt: null + lastUsedAt: '2026-06-29T11:00:00.000Z' + createdAt: '2026-01-01T00:00:00.000Z' + timestamp: '2026-06-29T12:00:00.000Z' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' post: - tags: [vault] - operationId: withdrawFromVault - summary: Move funds out of the vault + tags: [admin] + operationId: adminCreateKey + summary: Create a new admin API key + description: | + Issues a new scoped admin API key. The raw token is returned once + and will never be stored in plaintext. Requires admin scope `keys:write`. security: - - BearerAuth: [] + - AdminToken: [] requestBody: required: true content: application/json: schema: type: object - required: [amount] + required: [name, role, scopes] properties: - amount: + name: + type: string + description: Human-readable key name + role: + type: string + description: Key role (e.g. service, admin) + scopes: + type: array + items: + type: string + enum: [read, write, wallet, agent, super] + minItems: 1 + expiresAt: type: string + format: date-time + description: Optional expiration date + example: + name: Monitor Service + role: service + scopes: [read, metrics:read] responses: '201': - description: Vault withdrawal recorded + description: Key created content: application/json: schema: @@ -564,88 +1824,435 @@ paths: properties: success: type: boolean - transaction: - $ref: '#/components/schemas/Transaction' + data: + type: object + properties: + id: + type: string + name: + type: string + role: + type: string + scopes: + type: array + items: + type: string + expiresAt: + type: string + format: date-time + nullable: true + createdAt: + type: string + format: date-time + token: + type: string + description: The raw API key token (shown once) + warning: + type: string + timestamp: + type: string + format: date-time + example: + success: true + data: + id: key-002 + name: Monitor Service + role: service + scopes: [read, metrics:read] + expiresAt: null + createdAt: '2026-06-29T12:00:00.000Z' + token: a1b2c3d4e5f6... + warning: 'Store this token securely. It will not be shown again.' + timestamp: '2026-06-29T12:00:00.000Z' + '400': + $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '409': + description: Key name already exists + $ref: '#/components/responses/Conflict' - # ── Admin ────────────────────────────────────────────────────────────────── - /api/v1/admin/users: - get: + /api/v1/admin/keys/{id}: + delete: tags: [admin] - operationId: adminListUsers - summary: List all users (admin only) + operationId: adminRevokeKey + summary: Revoke an admin API key + description: | + Immediately revokes an admin API key by setting its revokedAt timestamp. + Requires admin scope `keys:write`. security: - AdminToken: [] parameters: - - in: query - name: limit - schema: - type: integer - default: 50 - - in: query - name: cursor + - in: path + name: id + required: true schema: type: string + description: Admin key ID responses: '200': - description: User list + description: Key revoked content: application/json: schema: type: object properties: + success: + type: boolean data: - type: array - items: - type: object - properties: - address: - type: string - createdAt: - type: string - format: date-time - nextCursor: + type: object + properties: + id: + type: string + status: + type: string + timestamp: type: string - nullable: true + format: date-time + example: + success: true + data: + id: key-002 + status: revoked + timestamp: '2026-06-29T12:00:00.000Z' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' + '409': + description: Key already revoked + $ref: '#/components/responses/Conflict' - /api/v1/admin/stats: + /api/v1/admin/wallets/rotation-status: get: tags: [admin] - operationId: adminGetStats - summary: Platform statistics (admin only) + operationId: adminGetWalletRotationStatus + summary: Get wallet key rotation status + description: | + Reports the progress of wallet key rotation across custodial wallets. + Requires admin scope `keys:read`. security: - AdminToken: [] responses: '200': - description: Platform stats + description: Rotation status content: application/json: schema: type: object properties: - totalUsers: - type: integer - totalVolumeUsdc: + success: + type: boolean + data: + type: object + properties: + totalWallets: + type: integer + v1Wallets: + type: integer + v2Wallets: + type: integer + percentV1: + type: number + isRotationComplete: + type: boolean + timestamp: type: string - activeVaults: - type: integer + format: date-time + example: + success: true + data: + totalWallets: 150 + v1Wallets: 15 + v2Wallets: 135 + percentV1: 10 + isRotationComplete: false + timestamp: '2026-06-29T12:00:00.000Z' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' - # ── Reusable responses ──────────────────────────────────────────────────── - responses: {} + # ── Metrics ──────────────────────────────────────────────────────────────── + /metrics: + get: + tags: [metrics] + operationId: getPrometheusMetrics + summary: Prometheus metrics endpoint + description: | + Returns Prometheus-compatible metrics for observability. + Protected by strict internal auth (returns 404 if unauthorized for info hiding). + security: + - InternalToken: [] + responses: + '200': + description: Prometheus metrics + content: + text/plain: + schema: + type: string + example: | + # HELP neuro_event_processed_total Total events processed + # TYPE neuro_event_processed_total counter + neuro_event_processed_total{status="success"} 15234 + neuro_event_processed_total{status="error"} 23 + '404': + description: Not found (info hiding for unauthorized requests) components: + securitySchemes: + BearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + description: > + JWT access token issued by `POST /api/v1/auth/verify`. + Include as `Authorization: Bearer `. + AdminToken: + type: http + scheme: bearer + bearerFormat: API-Key + description: > + Admin API key. Can be provided either as + `Authorization: Bearer ` or as the legacy + `X-Admin-Token` header. Issued via `POST /api/v1/admin/keys`. + InternalToken: + type: apiKey + in: header + name: X-Internal-Token + description: > + Service-to-service token for internal endpoints (e.g., agent status, + Prometheus metrics). Alternative auth methods include IP allowlisting + or admin Bearer token. + + schemas: + # ── Auth ────────────────────────────────────────────────────────────── + AuthChallengeResponse: + type: object + required: [nonce, expiresAt] + properties: + nonce: + type: string + description: 'One-time nonce the wallet must sign (prefix: nw-auth-)' + example: nw-auth-a3f9e2c1d0b84756... + expiresAt: + type: string + format: date-time + + AuthVerifyRequest: + type: object + required: [stellarPubKey, signature] + properties: + stellarPubKey: + type: string + description: Stellar G-address (Ed25519 public key) + example: GDQP2KPQGKIHYJGXNUIYOMHARUARCA7DJT5FO2FFOOKY3B2WSQHG4W37 + signature: + type: string + description: Base64-encoded Stellar signature over the nonce + + AuthVerifyResponse: + type: object + required: [accessToken, refreshToken, userId] + properties: + accessToken: + type: string + description: JWT access token (short-lived, default 15 min) + refreshToken: + type: string + description: Opaque refresh token (single-use, default 7 day TTL) + userId: + type: string + format: uuid + expiresAt: + type: string + format: date-time + description: Access token expiration + refreshExpiresAt: + type: string + format: date-time + description: Refresh token expiration + + # ── Health & Status ─────────────────────────────────────────────────── + HealthResponse: + type: object + required: [status, timestamp, version, environment] + properties: + status: + type: string + enum: [ok, degraded] + timestamp: + type: string + format: date-time + version: + type: string + example: 1.0.0 + environment: + type: string + enum: [development, test, production] + + AgentStatusResponse: + type: object + properties: + success: + type: boolean + data: + type: object + properties: + isRunning: + type: boolean + lastRebalanceAt: + type: string + format: date-time + nullable: true + currentProtocol: + type: string + nullable: true + currentApy: + type: string + nullable: true + nextScheduledCheck: + type: string + format: date-time + lastError: + type: string + nullable: true + healthStatus: + type: string + enum: [healthy, degraded, stopped] + timestamp: + type: string + format: date-time + + # ── Portfolio ───────────────────────────────────────────────────────── + PortfolioResponse: + type: object + properties: + userId: + type: string + format: uuid + totalBalance: + type: number + totalEarnings: + type: number + activePositions: + type: integer + positions: + type: array + items: + $ref: '#/components/schemas/Position' + + Position: + type: object + properties: + id: + type: string + protocolName: + type: string + assetSymbol: + type: string + example: USDC + currentValue: + type: number + yieldEarned: + type: number + status: + type: string + enum: [ACTIVE, CLOSED] + + # ── Transaction ─────────────────────────────────────────────────────── + Transaction: + type: object + properties: + id: + type: string + txHash: + type: string + type: + type: string + enum: [DEPOSIT, WITHDRAWAL, SWAP, VAULT_DEPOSIT, VAULT_WITHDRAW] + status: + type: string + enum: [PENDING, CONFIRMED, FAILED] + amount: + type: number + assetSymbol: + type: string + protocolName: + type: string + nullable: true + createdAt: + type: string + format: date-time + + TransactionResponse: + type: object + properties: + txHash: + type: string + status: + type: string + transaction: + type: object + properties: + id: + type: string + txHash: + type: string + status: + type: string + amount: + type: number + assetSymbol: + type: string + protocolName: + type: string + nullable: true + + # ── Protocol ────────────────────────────────────────────────────────── + ProtocolRate: + type: object + properties: + protocolName: + type: string + assetSymbol: + type: string + supplyApy: + type: number + borrowApy: + type: number + nullable: true + tvl: + type: number + nullable: true + network: + type: string + fetchedAt: + type: string + format: date-time + + # ── Error ───────────────────────────────────────────────────────────── + ErrorResponse: + type: object + required: [error] + properties: + error: + type: string + example: Unauthorized + details: + description: Optional structured detail (array or object) + oneOf: + - type: array + items: {} + - type: object + responses: Unauthorized: - description: Missing or invalid authentication token + description: Missing or invalid authentication content: application/json: schema: @@ -666,3 +2273,24 @@ components: application/json: schema: $ref: '#/components/schemas/ErrorResponse' + example: + error: Validation error + details: + - field: amount + message: Must be a positive number + NotFound: + description: The requested resource was not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + error: Not found + Conflict: + description: Resource conflict (e.g. duplicate) + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + error: Conflict diff --git a/package-lock.json b/package-lock.json index ad14fc1..d965ec4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,9 @@ "@types/cors": "^2.8.17", "@types/express": "^4.17.21", "@types/jest": "^29.5.10", + "@types/js-yaml": "^4.0.9", "@types/node": "^20.10.6", + "@types/swagger-ui-express": "^4.1.8", "@typescript-eslint/eslint-plugin": "^6.16.0", "@typescript-eslint/parser": "^6.16.0", "eslint": "^8.56.0", @@ -2397,6 +2399,13 @@ "pretty-format": "^29.0.0" } }, + "node_modules/@types/js-yaml": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", + "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -2482,6 +2491,17 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/swagger-ui-express": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.8.tgz", + "integrity": "sha512-AhZV8/EIreHFmBV5wAs0gzJUNq9JbbSXgJLQubCC0jtIo6prnI9MIRRxnU4MZX9RB9yXxF1V4R7jtLl/Wcj31g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/serve-static": "*" + } + }, "node_modules/@types/yargs": { "version": "17.0.35", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", @@ -9540,9 +9560,9 @@ } }, "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", + "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", "dev": true, "license": "Apache-2.0", "bin": { diff --git a/package.json b/package.json index 440a40a..86d5156 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,9 @@ "format": "prettier --write \"src/**/*.ts\" \"tests/**/*.ts\"", "typecheck": "tsc --noEmit", "clean": "rm -rf dist coverage", - "prebuild": "npm run clean", + "validate:spec": "npx @apidevtools/swagger-parser validate docs/openapi.yaml", + "build:spec": "mkdir -p dist/docs && cp docs/openapi.yaml dist/docs/openapi.yaml && npm run validate:spec", + "prebuild": "npm run clean && npm run build:spec", "prestart": "npm run build" }, "keywords": [ @@ -42,7 +44,9 @@ "@types/cors": "^2.8.17", "@types/express": "^4.17.21", "@types/jest": "^29.5.10", + "@types/js-yaml": "^4.0.9", "@types/node": "^20.10.6", + "@types/swagger-ui-express": "^4.1.8", "@typescript-eslint/eslint-plugin": "^6.16.0", "@typescript-eslint/parser": "^6.16.0", "eslint": "^8.56.0", diff --git a/src/index.ts b/src/index.ts index 9e1e5fd..fdf7005 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,8 +16,12 @@ import './telemetry/sentry' // 2. Sentry — error reporting // ── Standard imports ────────────────────────────────────────────────────────── import { type Server } from 'node:http' +import fs from 'node:fs' +import path from 'node:path' import express, { Request, Response } from 'express' import * as Sentry from '@sentry/node' +import swaggerUi from 'swagger-ui-express' +import yaml from 'js-yaml' import { config } from './config/env' import { errorHandler } from './middleware/errorHandler' import { correlationIdMiddleware } from './middleware/correlationId' @@ -164,6 +168,36 @@ app.use((_req: Request, res: Response, next) => { next() }) +// ── OpenAPI / Swagger UI ────────────────────────────────────────────────────── + +let swaggerSpec: Record | null = null +const specPath = path.join(process.cwd(), 'docs', 'openapi.yaml') + +try { + const specFile = fs.readFileSync(specPath, 'utf8') + swaggerSpec = yaml.load(specFile) as Record + logger.info(`[OpenAPI] Spec loaded from ${specPath}`) +} catch (error) { + logger.error('[OpenAPI] Failed to load spec', { error: error instanceof Error ? error.message : String(error) }) +} + +if (swaggerSpec) { + const swaggerOpts = { + customSiteTitle: 'NeuroWealth API Docs', + customfavIcon: '/favicon.ico', + } + app.use('/api/v1/docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec, swaggerOpts)) + app.use('/docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec, swaggerOpts)) + app.use('/api/v1/openapi.yaml', (_req: Request, res: Response) => { + res.setHeader('Content-Type', 'text/yaml; charset=utf-8') + res.sendFile(specPath) + }) + app.use('/openapi.yaml', (_req: Request, res: Response) => { + res.setHeader('Content-Type', 'text/yaml; charset=utf-8') + res.sendFile(specPath) + }) +} + // Marks legacy unversioned /api/* responses as deprecated. function deprecatedApiWarning(req: Request, res: Response, next: express.NextFunction): void { res.setHeader('Deprecation', 'true')