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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
215 changes: 202 additions & 13 deletions backend/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,31 @@
"type": "string",
"format": "uuid"
}
},
"pageSize": {
"name": "limit",
"in": "query",
"required": false,
"description": "Maximum number of items to return. Hard ceiling is 50 (default 20). A larger value is **rejected**, not clamped, with `400` and `code: \"LIMIT_EXCEEDED\"` β€” the response body repeats the ceiling so clients can self-correct. Use `page` (clamped to 1..1000) to walk the rest.",
"schema": {
"type": "integer",
"minimum": 1,
"maximum": 50,
"default": 20,
"example": 20
}
},
"pageNumber": {
"name": "page",
"in": "query",
"required": false,
"description": "1-based page number for offset pagination. Values outside 1..1000 are clamped rather than rejected, and the effective page is echoed back in `pagination.currentPage`.",
"schema": {
"type": "integer",
"minimum": 1,
"maximum": 1000,
"default": 1
}
}
},
"schemas": {
Expand Down Expand Up @@ -106,6 +131,10 @@
"count": {
"type": "integer"
},
"limit": {
"type": "integer",
"maximum": 50
},
"total": {
"type": "integer"
},
Expand All @@ -118,7 +147,9 @@
"nullable": true
},
"currentPage": {
"type": "integer"
"type": "integer",
"minimum": 1,
"maximum": 1000
},
"totalPages": {
"type": "integer"
Expand All @@ -131,6 +162,62 @@
}
}
},
"Vault": {
"type": "object",
"required": [
"id",
"aum",
"tvlUsd",
"createdAt",
"updatedAt"
],
"properties": {
"id": {
"type": "string",
"format": "uuid"
},
"aum": {
"type": "number",
"example": 0
},
"tvlUsd": {
"type": "string",
"nullable": true,
"example": "1250000.00"
},
"createdAt": {
"type": "string",
"format": "date-time"
},
"updatedAt": {
"type": "string",
"format": "date-time"
}
}
},
"VaultListResponse": {
"type": "object",
"required": [
"data",
"pagination",
"timestamp"
],
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Vault"
}
},
"pagination": {
"$ref": "#/components/schemas/PaginationMeta"
},
"timestamp": {
"type": "string",
"format": "date-time"
}
}
},
"VaultSummary": {
"type": "object",
"properties": {
Expand All @@ -143,15 +230,8 @@
"example": 0
},
"apy": {
"type": ["number", "null"],
"example": 8.45,
"description": "Annualised APY as a decimal percentage. null when the vault has insufficient price history (e.g. zero shares or fewer than 2 snapshots)."
},
"apyStatus": {
"type": "string",
"enum": ["ok", "insufficient_data"],
"example": "ok",
"description": "ok when apy is a valid number; insufficient_data when apy is null."
"type": "number",
"example": 0
},
"timestamp": {
"type": "string",
Expand Down Expand Up @@ -363,11 +443,21 @@
"timestamp": "2024-01-01T00:00:00.000Z",
"uptime": 123.4,
"environment": "production",
"lastIndexedLedger": 12345678,
"checks": {
"api": "up",
"cache": "up",
"stellarRpc": "up",
"databasePrimary": "up",
"databaseReplica": "up",
"prisma": "up",
"jobs": "up",
"indexer": "up"
},
"sorobanCircuitBreaker": {
"state": "closed",
"failures": 0,
"retryAfterMs": 0
}
}
}
Expand Down Expand Up @@ -522,16 +612,26 @@
"application/json": {
"schema": {
"type": "object",
"required": ["apy", "apyStatus", "timestamp"],
"required": [
"apy",
"apyStatus",
"timestamp"
],
"properties": {
"apy": {
"type": ["number", "null"],
"type": [
"number",
"null"
],
"example": 8.45,
"description": "Annualised APY as a decimal percentage. null when insufficient data."
},
"apyStatus": {
"type": "string",
"enum": ["ok", "insufficient_data"],
"enum": [
"ok",
"insufficient_data"
],
"example": "ok"
},
"timestamp": {
Expand All @@ -546,6 +646,95 @@
}
}
},
"/api/v1/vaults": {
"get": {
"tags": [
"Vault"
],
"summary": "List vaults",
"description": "Paginated listing of active (non-deleted) vaults. `limit` is capped at 50 and `page` at 1000; exceeding `limit` fails fast with `400` / `LIMIT_EXCEEDED` instead of silently clamping, so a client that asks for an oversized page is never handed a response that looks complete. Rate limited: global 100 req / 15 min per IP; authenticated APIs 30 req / min per key. `429` responses follow the standard error envelope with a `retryAfter` detail.",
"parameters": [
{
"$ref": "#/components/parameters/pageSize"
},
{
"$ref": "#/components/parameters/pageNumber"
},
{
"$ref": "#/components/parameters/correlationId"
}
],
"responses": {
"200": {
"description": "One page of vaults",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/VaultListResponse"
},
"example": {
"data": [
{
"id": "0f1b3a2c-6d4e-4a1b-9f0e-2c5d7e8b9a01",
"aum": 1250000,
"tvlUsd": "1250000.00",
"createdAt": "2026-01-01T00:00:00.000Z",
"updatedAt": "2026-01-02T00:00:00.000Z"
}
],
"pagination": {
"count": 1,
"limit": 20,
"total": 1,
"nextCursor": null,
"prevCursor": null,
"currentPage": 1,
"totalPages": 1,
"hasNextPage": false,
"hasPrevPage": false
},
"timestamp": "2026-01-01T00:00:00.000Z"
}
}
}
},
"400": {
"description": "`limit` above the published ceiling",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
},
"example": {
"error": "Bad Request",
"status": 400,
"code": "LIMIT_EXCEEDED",
"message": "limit must not exceed 50 (received 100000).",
"retryable": false,
"details": {
"field": "limit",
"requested": 100000,
"maxLimit": 50,
"defaultLimit": 20,
"maxPage": 1000
}
}
}
}
},
"429": {
"description": "Rate limited",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
}
}
},
"/api/v1/vault/deposits": {
"post": {
"tags": [
Expand Down
Binary file modified backend/prisma/dev.db
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
-- AlterTable
ALTER TABLE "Transaction" ADD COLUMN "deletedAt" DATETIME;

-- AlterTable
ALTER TABLE "WebhookEndpoint" ADD COLUMN "tenantId" TEXT;

-- CreateTable
CREATE TABLE "SessionAuditLog" (
"id" TEXT NOT NULL PRIMARY KEY,
"walletAddress" TEXT NOT NULL,
"eventType" TEXT NOT NULL,
"reason" TEXT NOT NULL,
"sessionId" TEXT NOT NULL,
"ipAddress" TEXT,
"userAgent" TEXT,
"metadata" TEXT,
"correlationId" TEXT,
"traceId" TEXT,
"timestamp" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);

-- CreateIndex
CREATE INDEX "SessionAuditLog_walletAddress_timestamp_idx" ON "SessionAuditLog"("walletAddress", "timestamp" DESC);

-- CreateIndex
CREATE INDEX "SessionAuditLog_eventType_idx" ON "SessionAuditLog"("eventType");

-- CreateIndex
CREATE INDEX "SessionAuditLog_timestamp_idx" ON "SessionAuditLog"("timestamp" DESC);

-- CreateIndex
CREATE INDEX "Transaction_tenantId_idx" ON "Transaction"("tenantId");

-- CreateIndex
CREATE INDEX "Transaction_deletedAt_idx" ON "Transaction"("deletedAt");

-- CreateIndex
CREATE INDEX "WebhookEndpoint_tenantId_idx" ON "WebhookEndpoint"("tenantId");
34 changes: 34 additions & 0 deletions backend/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ model Transaction {
tenantId String?
latencyMs Int?
failureReason String?
// Soft-delete marker used by the tenant boundary guard so revoked rows are
// never returned to a tenant-scoped read.
deletedAt DateTime?
timestamp DateTime @default(now())

// Issue #895 – query optimisation indices
Expand All @@ -61,6 +64,8 @@ model Transaction {
@@index([user, type, timestamp(sort: Desc)])
@@index([user, status, timestamp(sort: Desc)])
@@index([vaultId, tenantId, timestamp(sort: Desc)])
@@index([tenantId])
@@index([deletedAt])
}

model Referral {
Expand Down Expand Up @@ -221,6 +226,8 @@ model WebhookEndpoint {
challengeExpiresAt DateTime?
verifiedAt DateTime?
lastVerificationError String?
// Owning tenant, used by the tenant boundary guard for cross-account reads.
tenantId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
Expand All @@ -231,6 +238,7 @@ model WebhookEndpoint {
@@index([createdAt])
@@index([deletedAt])
@@index([verificationStatus])
@@index([tenantId])
}

model WebhookDelivery {
Expand Down Expand Up @@ -675,6 +683,8 @@ model WalletTenantAssociation {

@@index([walletAddress, tenantId])
@@index([tenantId])
}

model IdempotencyKey {
id String @id @default(uuid())
keyId String
Expand All @@ -693,3 +703,27 @@ model IdempotencyKey {
@@unique([keyId, tenantId])
@@index([expiresAt])
}

// ─── Session Audit ────────────────────────────────────────────────────────────

/// Append-only trail of session lifecycle events (login, refresh, logout,
/// expiry) written by `sessionAudit.ts`. Powers incident investigation and
/// the anomaly heuristics in `detectSuspiciousActivity`.
model SessionAuditLog {
id String @id @default(uuid())
walletAddress String
eventType String // created | refreshed | revoked | expired | failed
reason String // user_logout | token_rotation | token_expiration | ...
sessionId String
ipAddress String?
userAgent String?
/// JSON-serialised context attached to the event.
metadata String?
correlationId String?
traceId String?
timestamp DateTime @default(now())

@@index([walletAddress, timestamp(sort: Desc)])
@@index([eventType])
@@index([timestamp(sort: Desc)])
}
Loading
Loading