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 a1a5899..e7e6ce3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,16 +9,24 @@ "version": "1.0.0", "license": "MIT", "dependencies": { + "@prisma/client": "^7.8.0", + "@stellar/stellar-sdk": "^16.0.1", + "@types/node-cron": "^3.0.11", + "@types/supertest": "^7.2.0", "cors": "^2.8.5", "dotenv": "^16.3.1", "express": "^4.18.2", "ioredis": "^5.11.1" }, "devDependencies": { + "@aws-sdk/client-ssm": "^3.1075.0", "@types/cors": "^2.8.17", "@types/express": "^4.17.21", "@types/jest": "^29.5.10", + "@types/js-yaml": "^4.0.9", + "@types/jsonwebtoken": "^9.0.10", "@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", @@ -35,6 +43,364 @@ "npm": ">=9.0.0" } }, + "node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-ssm": { + "version": "3.1076.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-ssm/-/client-ssm-3.1076.0.tgz", + "integrity": "sha512-Y8NTU3gBd5UrkhNePn1TGLkjFosi/btwv75QgTpkJwvd4np5fhW72L6IK1Aw9QjT5pvXOib4LLBNMzwdlBRy9w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.974.24", + "@aws-sdk/credential-provider-node": "^3.972.59", + "@aws-sdk/types": "^3.973.14", + "@smithy/core": "^3.27.0", + "@smithy/fetch-http-handler": "^5.6.0", + "@smithy/node-http-handler": "^4.9.0", + "@smithy/types": "^4.15.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/core": { + "version": "3.974.24", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.974.24.tgz", + "integrity": "sha512-vWB/qJl21vxGKBkBN8fKPTVXgm14v/bUQWTtR5oikrfAZbIN2bxuSiCY5rRAMR4gs3vtR2Vw0aTfVDU4tdfIPg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.14", + "@aws-sdk/xml-builder": "^3.972.32", + "@aws/lambda-invoke-store": "^0.2.2", + "@smithy/core": "^3.27.0", + "@smithy/signature-v4": "^5.5.3", + "@smithy/types": "^4.15.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.972.50", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.50.tgz", + "integrity": "sha512-l8bWzhPFTi9tDcvtURxeMlfsboul5/0sEN3SwwXxdpYudVB9+EuQcxo2pwlTzXwDo4Gm2VLGyiZ8zti3nfdOLw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.24", + "@aws-sdk/types": "^3.973.14", + "@smithy/core": "^3.27.0", + "@smithy/types": "^4.15.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.972.52", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.52.tgz", + "integrity": "sha512-FjAlnsIvemWzO3JTM3ObuuxpqCyrqkXOewlYY2+NiR1MYO1JuFYSIJ8SJN5Q2KD1jkL5lIuab8awjb/AxsvjiQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.24", + "@aws-sdk/types": "^3.973.14", + "@smithy/core": "^3.27.0", + "@smithy/fetch-http-handler": "^5.6.0", + "@smithy/node-http-handler": "^4.9.0", + "@smithy/types": "^4.15.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.972.57", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.57.tgz", + "integrity": "sha512-8qwNhQ0sK/1KaOpVEFC7TFxrWP3fxzJV1K049MzjouiMIbvTDvIGDEUtj5ND5aTmlHVK/YZxjoYnLCeV/GZU0w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.24", + "@aws-sdk/credential-provider-env": "^3.972.50", + "@aws-sdk/credential-provider-http": "^3.972.52", + "@aws-sdk/credential-provider-login": "^3.972.56", + "@aws-sdk/credential-provider-process": "^3.972.50", + "@aws-sdk/credential-provider-sso": "^3.972.56", + "@aws-sdk/credential-provider-web-identity": "^3.972.56", + "@aws-sdk/nested-clients": "^3.997.24", + "@aws-sdk/types": "^3.973.14", + "@smithy/core": "^3.27.0", + "@smithy/credential-provider-imds": "^4.4.3", + "@smithy/types": "^4.15.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login": { + "version": "3.972.56", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.56.tgz", + "integrity": "sha512-S36dCrDaafakFMlaCVGAF4advbQKoJuMcyMtNWVBpUz65uqhbIAsUfvAyp+djA+jkzaEfgZGd+AELjIGzTqyhw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.24", + "@aws-sdk/nested-clients": "^3.997.24", + "@aws-sdk/types": "^3.973.14", + "@smithy/core": "^3.27.0", + "@smithy/types": "^4.15.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.972.59", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.59.tgz", + "integrity": "sha512-LkczBXaEsdManijlEZwbKfEoo1C98Yri3LHF8gQI7CYWv+uFkmpS3OZH3BSew8g1A2ppKsScdPUSlhI6NV7a9g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "^3.972.50", + "@aws-sdk/credential-provider-http": "^3.972.52", + "@aws-sdk/credential-provider-ini": "^3.972.57", + "@aws-sdk/credential-provider-process": "^3.972.50", + "@aws-sdk/credential-provider-sso": "^3.972.56", + "@aws-sdk/credential-provider-web-identity": "^3.972.56", + "@aws-sdk/types": "^3.973.14", + "@smithy/core": "^3.27.0", + "@smithy/credential-provider-imds": "^4.4.3", + "@smithy/types": "^4.15.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.972.50", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.50.tgz", + "integrity": "sha512-ARBEVkOQzmowTU0a35smGVyldJ9FN/f57XIGrPatrul4mYN+vvOKxoc1njDOX3nugVze+0sHzQZWJ8kPARAtUA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.24", + "@aws-sdk/types": "^3.973.14", + "@smithy/core": "^3.27.0", + "@smithy/types": "^4.15.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.972.56", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.56.tgz", + "integrity": "sha512-LvbWiFcLI/D5RPaT68TrpLLHyv7x5X+dm59wJ5dFizyGPZggBC7OdgJTlP0X1bVjiSSAgE1u1oxxcBps0GCEnA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.24", + "@aws-sdk/nested-clients": "^3.997.24", + "@aws-sdk/token-providers": "3.1076.0", + "@aws-sdk/types": "^3.973.14", + "@smithy/core": "^3.27.0", + "@smithy/types": "^4.15.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.972.56", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.56.tgz", + "integrity": "sha512-OV3JxmqMphVGMLWupYD2UhZxX07ATk1NwyYk7RgCnAEh0y3owHmtEnkWZ3ciCZ6liiFEwS8dYQpJGmKsR6ml4Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.24", + "@aws-sdk/nested-clients": "^3.997.24", + "@aws-sdk/types": "^3.973.14", + "@smithy/core": "^3.27.0", + "@smithy/types": "^4.15.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients": { + "version": "3.997.24", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.24.tgz", + "integrity": "sha512-+wFVfVofxeiXdRhUjRwYISB2mVfBCdiCq1wThkRipTeOc10Kyr+LS9QJTjgZuhWsna7jyLMPndrCnzLGWWvZXg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.974.24", + "@aws-sdk/signature-v4-multi-region": "^3.996.36", + "@aws-sdk/types": "^3.973.14", + "@smithy/core": "^3.27.0", + "@smithy/fetch-http-handler": "^5.6.0", + "@smithy/node-http-handler": "^4.9.0", + "@smithy/types": "^4.15.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.996.36", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.36.tgz", + "integrity": "sha512-VSOWIPkI+g3a7NkxIBCO24HnsR0BZXJAi3wrKaGIZwVKyrMtNRdHxPrQI/igazgla5J9FhDzmg4RgnOSr6UQBw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.14", + "@smithy/signature-v4": "^5.5.3", + "@smithy/types": "^4.15.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/token-providers": { + "version": "3.1076.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1076.0.tgz", + "integrity": "sha512-4rTHETRKe2JWAsFUMo5ENmlzc3i9FD4KqBVXgoaF8DLTADjGid8SA+1LR2nJWjefoafvKAHcQH9F2iKa8uHc6Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.24", + "@aws-sdk/nested-clients": "^3.997.24", + "@aws-sdk/types": "^3.973.14", + "@smithy/core": "^3.27.0", + "@smithy/types": "^4.15.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.973.14", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.14.tgz", + "integrity": "sha512-vH4pEu9YBEwr67yT+GVcmKX0GzfIrIYUn+MF5vXg9OspouVnAekuyVyawFvZHEK7WlcwVDwNrqI3ZBDUAiyu9A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.15.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.965.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.8.tgz", + "integrity": "sha512-uUbMs1cBZPafD0ohUj6EwNf0fPZ534NvBxHox4hjX+0Rxq5paSYUem7+hi833pYrzrcnBATKIYpR02MDXT5M9g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/xml-builder": { + "version": "3.972.32", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.32.tgz", + "integrity": "sha512-2loKuOMRFDg1nwdni5AtJ9S5juVbRNPNsPC7tWTfkHyycPwACMhxepspUHi8GhvfNlL2cQo3sPMod1uib+KZ0w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.15.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws/lambda-invoke-store": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.4.tgz", + "integrity": "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", @@ -705,7 +1071,7 @@ "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", "deprecated": "Use @eslint/config-array instead", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "dependencies": { "@humanwhocodes/object-schema": "^2.0.3", "debug": "^4.3.1", @@ -1274,7 +1640,7 @@ "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { "expect": "^29.7.0", "jest-snapshot": "^29.7.0" @@ -2131,6 +2497,15 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@noble/ed25519": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@noble/ed25519/-/ed25519-3.1.0.tgz", + "integrity": "sha512-pfcObRY3CtvwfaG9Mt5XqZdKmAQppl37tHUeuBhDUbiwJBCVY4/A4lbMvb1xKhMDx96AqAqZpMWuBX1HulhX4g==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@noble/hashes": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", @@ -2192,6 +2567,36 @@ "@noble/hashes": "^1.1.5" } }, + "node_modules/@prisma/client": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-7.8.0.tgz", + "integrity": "sha512-HFp3Dawv/3sU3JtlPha90IB+48lS7zHiH4LKZPjmcE8YH5P9DOXGPvo8dqOtO7MqLDd1p2hOWMcFlRT1DMblHw==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/client-runtime-utils": "7.8.0" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24.0" + }, + "peerDependencies": { + "prisma": "*", + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@prisma/client-runtime-utils": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/client-runtime-utils/-/client-runtime-utils-7.8.0.tgz", + "integrity": "sha512-5NQZztQ0oY/ADFkmd9gPuweH5A1/CCY8YQPorLLO0Mu6a87mY5gsnDkzmFmIHs9NFaLnZojzgddFVN4RpKYrdw==", + "license": "Apache-2.0" + }, "node_modules/@sinonjs/commons": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", @@ -2212,6 +2617,182 @@ "@sinonjs/commons": "^3.0.0" } }, + "node_modules/@smithy/core": { + "version": "3.28.0", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.28.0.tgz", + "integrity": "sha512-N/LoLG8pZ1zv5cIWpdF6vmSjtZtXKK9G0OqT5yYCOZU+CzPq1+nYA95VoKJBGWRScs7YbMugZ7lZx8Fj1vdHoA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.15.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.4.4.tgz", + "integrity": "sha512-jT0WrDaM88L5na9FX1xRNywCS3B1n75wPY5Ksasjo0PHUtuI7d8FclksN1BbOSYTiaiKxUDqU23nUymH/V+AaQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.28.0", + "@smithy/types": "^4.15.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.6.1.tgz", + "integrity": "sha512-fW6l9rWoyk1iyzfuZaERnZLNjB6WIojgGm6Bo9Hpfpy3RUpltjLikNlxTsS/YtxVobcfbCGBuAncREYqT4hvqQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.28.0", + "@smithy/types": "^4.15.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.9.1.tgz", + "integrity": "sha512-m/f15di58P6NtLQ7eVEb5N19NdJWn+4c7zfkFHMT/i3JH7U8UtknpPoy8o2tm2R3OdliYvsvQhZHIfACQDqT+Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.28.0", + "@smithy/types": "^4.15.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.6.0.tgz", + "integrity": "sha512-IkPHQdbyoebSwBCuMTzJ/2oIhKVqiZZAZxQYSlpDZqq/WhJUpmdgbHvP7ItddxsPzcDUJeI0V4PNMSNtlZ0aqA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.28.0", + "@smithy/types": "^4.15.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "4.15.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.15.0.tgz", + "integrity": "sha512-Z5TAOxygoFvybJV3igo5SloFflSokHx2hu1eFA+DxDTcn+FtKxUSui+rbTRG1pAafMA888Z3MVvCWUuvCrTXjg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@stellar/js-xdr": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@stellar/js-xdr/-/js-xdr-4.0.0.tgz", + "integrity": "sha512-+NmNa7Tk5BI5XFdy/6xGTqAN4J9a9KgCrCGhj2uEUTCBhLkch0M+QbKzNH8zEnejWe0p8w+0q5hUVX6L3OzoVA==", + "license": "Apache-2.0", + "engines": { + "node": ">=20.0.0", + "pnpm": ">=9.0.0" + } + }, + "node_modules/@stellar/stellar-sdk": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/@stellar/stellar-sdk/-/stellar-sdk-16.0.1.tgz", + "integrity": "sha512-bxKohaiyKVqoudRhbOOHeHhHIaeYV5Zab4rCjxhP4Ty1h1ozTLBOv8lWFnZz9ilBzXG8Bb7usQI3rlEcfvUynA==", + "license": "Apache-2.0", + "dependencies": { + "@noble/ed25519": "^3.1.0", + "@noble/hashes": "^2.2.0", + "@stellar/js-xdr": "4.0.0", + "axios": "1.16.1", + "base32.js": "^0.1.0", + "bignumber.js": "^11.1.1", + "buffer": "^6.0.3", + "commander": "^14.0.3", + "eventsource": "^4.1.0", + "feaxios": "^0.0.23", + "smol-toml": "^1.6.1", + "uint8array-extras": "^1.5.0" + }, + "bin": { + "stellar-js": "bin/stellar-js" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@stellar/stellar-sdk/node_modules/@noble/hashes": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz", + "integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@tsconfig/node10": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", @@ -2306,6 +2887,12 @@ "@types/node": "*" } }, + "node_modules/@types/cookiejar": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", + "integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==", + "license": "MIT" + }, "node_modules/@types/cors": { "version": "2.8.19", "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", @@ -2397,6 +2984,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", @@ -2404,6 +2998,23 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, + "node_modules/@types/methods": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", + "integrity": "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==", + "license": "MIT" + }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", @@ -2411,16 +3022,28 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "20.19.43", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", - "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.21.0" } }, + "node_modules/@types/node-cron": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/node-cron/-/node-cron-3.0.11.tgz", + "integrity": "sha512-0ikrnug3/IyneSHqCBeslAhlK2aBfYek1fGo4bP4QnZPmiqSGRK+Oy7ZMisLWkesffJvQ1cqAcBnJC+8+nxIAg==", + "license": "MIT" + }, "node_modules/@types/qs": { "version": "6.15.1", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", @@ -2482,6 +3105,39 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/superagent": { + "version": "8.1.10", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.10.tgz", + "integrity": "sha512-nbt4IWXABhW0jGmmpRzCFNlbmwCTzZ2gTUsNIr+X+ItdqPms+PAJZbWsNzpS2USqXjcoNLQcO6nXo60zcPQiIg==", + "license": "MIT", + "dependencies": { + "@types/cookiejar": "^2.1.5", + "@types/methods": "^1.1.4", + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/@types/supertest": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-7.2.0.tgz", + "integrity": "sha512-uh2Lv57xvggst6lCqNdFAmDSvoMG7M/HDtX4iUCquxQ5EGPtaPM5PL5Hmi7LCvOG8db7YaCPNJEeoI8s/WzIQw==", + "license": "MIT", + "dependencies": { + "@types/methods": "^1.1.4", + "@types/superagent": "^8.1.0" + } + }, + "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", @@ -2786,6 +3442,18 @@ "node": ">=0.4.0" } }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/ajv": { "version": "6.15.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", @@ -2926,9 +3594,20 @@ "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true, "license": "MIT" }, + "node_modules/axios": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.1.tgz", + "integrity": "sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, "node_modules/babel-preset-current-node-syntax": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", @@ -2962,14 +3641,39 @@ "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "dev": true, "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, "engines": { "node": "18 || 20 || >=22" } }, + "node_modules/base32.js": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/base32.js/-/base32.js-0.1.0.tgz", + "integrity": "sha512-n3TkB02ixgBOhTvANakDb4xaMXnYUVkNoRFJjQflcqMQhyEKxEHdj3E6N8t8sUQ0mjH/3/JxzlXuz3ul/J90pQ==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/baseline-browser-mapping": { "version": "2.10.0", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", @@ -2983,6 +3687,12 @@ "node": ">=6.0.0" } }, + "node_modules/bignumber.js": { + "version": "11.1.4", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-11.1.4.tgz", + "integrity": "sha512-AJ9dSeaUGj2xu7tEwmdqb51dqdb633xo4njI9K8ZFfcLrNr0XN8/EPkkZUNaF9fkCblGt2zVwZymesUdGynEkQ==", + "license": "MIT" + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -3035,6 +3745,13 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/bowser": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", + "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==", + "dev": true, + "license": "MIT" + }, "node_modules/brace-expansion": { "version": "5.0.6", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", @@ -3118,6 +3835,30 @@ "node-int64": "^0.4.0" } }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -3388,7 +4129,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "license": "MIT", "dependencies": { "delayed-stream": "~1.0.0" @@ -3397,6 +4137,15 @@ "node": ">= 0.8" } }, + "node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, "node_modules/component-emitter": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", @@ -3634,10 +4383,7 @@ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } + "license": "MIT" }, "node_modules/deepmerge": { "version": "4.3.1", @@ -3653,7 +4399,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.4.0" @@ -3820,7 +4565,7 @@ "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", "dependencies": { "is-arrayish": "^0.2.1" } @@ -3859,7 +4604,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -4230,6 +4974,27 @@ "node": ">= 0.6" } }, + "node_modules/eventsource": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-4.1.0.tgz", + "integrity": "sha512-2GuF51iuHX6A9xdTccMTsNb7VO0lHZihApxhvQzJB5A03DvHDd2FQepodbMaztPBmBcE/ox7o2gqaxGhYB9LhQ==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.0.tgz", + "integrity": "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -4268,22 +5033,13 @@ "dev": true, "engines": { "node": ">= 0.8.0" - } - }, - "node_modules/expect": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", - "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, "license": "MIT", "dependencies": { "@jest/expect-utils": "^29.7.0", @@ -4494,33 +5250,21 @@ "bser": "2.1.1" } }, + "node_modules/feaxios": { + "version": "0.0.23", + "resolved": "https://registry.npmjs.org/feaxios/-/feaxios-0.0.23.tgz", + "integrity": "sha512-eghR0A21fvbkcQBgZuMfQhrXxJzC0GNUGC9fXhBge33D+mFDTwl0aJ35zoQQn575BhyjQitRc5N4f+L4cP708g==", + "license": "MIT", + "dependencies": { + "is-retry-allowed": "^3.0.0" + } + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", "dev": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-safe-stringify": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", - "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", - "dev": true, - "license": "MIT" - }, - "node_modules/fastq": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", - "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", - "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { "flat-cache": "^3.0.4" }, @@ -4533,15 +5277,6 @@ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, - "license": "Apache-2.0", - "dependencies": { - "bser": "2.1.1" - } - }, - "node_modules/feaxios": { - "version": "0.0.23", - "resolved": "https://registry.npmjs.org/feaxios/-/feaxios-0.0.23.tgz", - "integrity": "sha512-eghR0A21fvbkcQBgZuMfQhrXxJzC0GNUGC9fXhBge33D+mFDTwl0aJ35zoQQn575BhyjQitRc5N4f+L4cP708g==", "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" @@ -4689,11 +5424,30 @@ "dev": true, "license": "ISC" }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/form-data": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", - "dev": true, "license": "MIT", "dependencies": { "asynckit": "^0.4.0", @@ -4727,13 +5481,6 @@ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - }, "engines": { "node": ">= 0.6" } @@ -4902,7 +5649,7 @@ "slash": "^3.0.0" }, "engines": { - "node": ">= 0.4" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -4982,7 +5729,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" @@ -5028,8 +5774,22 @@ "engines": { "node": ">= 0.8" }, - "optionalDependencies": { - "uglify-js": "^3.1.4" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" } }, "node_modules/human-signals": { @@ -5054,15 +5814,32 @@ "node": ">=0.10.0" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, "engines": { "node": ">= 4" } @@ -5279,6 +6056,18 @@ "node": ">=8" } }, + "node_modules/is-retry-allowed": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-3.0.0.tgz", + "integrity": "sha512-9xH0xvoggby+u0uGF7cZXdrutWiBiaFG8ZT4YFPXL8NzkyAwX3AKGLeFQLvzDpM430+nDFBZ1LHkie/8ocL06A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -5544,12 +6333,6 @@ "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], "license": "MIT", "dependencies": { "@sinclair/typebox": "^0.27.8" @@ -5922,10 +6705,7 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } + "license": "MIT" }, "node_modules/jest-config/node_modules/brace-expansion": { "version": "1.1.15", @@ -6000,7 +6780,7 @@ "semver": "^6.3.0" }, "engines": { - "node": "*" + "node": ">=8" } }, "node_modules/jest-config/node_modules/jest-haste-map": { @@ -6346,12 +7126,6 @@ "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], "license": "MIT", "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -6646,9 +7420,6 @@ } ], "license": "MIT", - "dependencies": { - "@sinclair/typebox": "^0.27.8" - }, "engines": { "node": ">=8" } @@ -6856,7 +7627,7 @@ "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", "dev": true, - "license": "ISC" + "license": "MIT" }, "node_modules/jest-runner/node_modules/babel-plugin-istanbul": { "version": "6.1.1", @@ -6872,7 +7643,7 @@ "test-exclude": "^6.0.0" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + "node": ">=8" } }, "node_modules/jest-runner/node_modules/ci-info": { @@ -6887,9 +7658,6 @@ } ], "license": "MIT", - "dependencies": { - "@sinclair/typebox": "^0.27.8" - }, "engines": { "node": ">=8" } @@ -6900,25 +7668,8 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, "license": "MIT", - "dependencies": { - "@babel/core": "^7.11.6", - "@jest/types": "^29.6.3", - "@jridgewell/trace-mapping": "^0.3.18", - "babel-plugin-istanbul": "^6.1.1", - "chalk": "^4.0.0", - "convert-source-map": "^2.0.0", - "fast-json-stable-stringify": "^2.1.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "jest-regex-util": "^29.6.3", - "jest-util": "^29.7.0", - "micromatch": "^4.0.4", - "pirates": "^4.0.4", - "slash": "^3.0.0", - "write-file-atomic": "^4.0.2" - }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=8" } }, "node_modules/jest-runner/node_modules/istanbul-lib-instrument": { @@ -6969,21 +7720,7 @@ "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", "dev": true, - "license": "MIT" - }, - "node_modules/jest-runtime/node_modules/babel-plugin-istanbul": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", - "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@babel/helper-plugin-utils": "^7.0.0", - "@istanbuljs/load-nyc-config": "^1.0.0", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-instrument": "^5.0.4", - "test-exclude": "^6.0.0" - }, + "license": "MIT", "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } @@ -7166,12 +7903,10 @@ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, "node_modules/jest-runtime/node_modules/@sinclair/typebox": { @@ -7329,14 +8064,7 @@ "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", "dev": true, - "license": "MIT" - }, - "node_modules/jest-snapshot/node_modules/babel-plugin-istanbul": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", - "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", - "dev": true, - "license": "BSD-3-Clause", + "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", @@ -7354,12 +8082,6 @@ "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], "license": "MIT", "dependencies": { "@types/node": "*", @@ -7406,7 +8128,7 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, - "license": "BSD-3-Clause", + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -7577,11 +8299,7 @@ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, - "license": "ISC", - "dependencies": { - "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.7" - }, + "license": "MIT", "engines": { "node": ">=8" } @@ -7822,39 +8540,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-watcher": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", - "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/test-result": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", - "emittery": "^0.13.1", - "jest-util": "^29.7.0", - "string-length": "^4.0.1" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-watcher/node_modules/@jest/schemas": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", - "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sinclair/typebox": "^0.27.8" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/jest-watcher/node_modules/@jest/types": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", @@ -7892,11 +8577,8 @@ } ], "license": "MIT", - "dependencies": { - "@sinclair/typebox": "^0.27.8" - }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=8" } }, "node_modules/jest-watcher/node_modules/jest-util": { @@ -8752,6 +9434,15 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/pstree.remy": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", @@ -9664,6 +10355,13 @@ } } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -9764,7 +10462,6 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, "license": "MIT" }, "node_modules/unpipe": { diff --git a/package.json b/package.json index 325c14f..adcc9c4 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": [ @@ -47,8 +49,10 @@ "@types/cors": "^2.8.17", "@types/express": "^4.17.21", "@types/jest": "^29.5.10", + "@types/js-yaml": "^4.0.9", "@types/jsonwebtoken": "^9.0.10", "@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')