Add limits and api key handling to dashboard - #1298
Conversation
✅ Deploy Preview for vortexfi ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
✅ Deploy Preview for vrtx-dashboard canceled.
|
✅ Deploy Preview for vortex-sandbox ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
There was a problem hiding this comment.
Pull request overview
Adds dashboard management for monthly corridor limits and user API credentials, backed by new API contracts and services.
Changes:
- Adds unified authenticated limits retrieval and dashboard display.
- Adds credential pairing, creation, listing, and revocation.
- Updates SDK guidance, OpenAPI documentation, security specs, and tests.
Reviewed changes
Copilot reviewed 49 out of 50 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
packages/shared/src/services/brla/schemas.ts |
Validates provider limit periods. |
packages/shared/src/services/brla/schemas.test.ts |
Tests BRLA limit parsing. |
packages/shared/src/endpoints/limits.endpoints.ts |
Defines limits contracts. |
packages/shared/src/endpoints/index.ts |
Exports limits contracts. |
packages/sdk/src/VortexSdk.ts |
Clarifies registration credentials. |
packages/sdk/README.md |
Updates API-key guidance. |
docs/security-spec/07-operations/api-surface.md |
Documents limits endpoint security. |
docs/security-spec/05-integrations/brla.md |
Documents BRL limit handling. |
docs/security-spec/05-integrations/alfredpay.md |
Documents Alfredpay usage aggregation. |
docs/security-spec/01-auth/api-keys.md |
Documents credential pairing. |
docs/api/pages/09-fiat-corridors.md |
Documents corridor limits. |
docs/api/pages/03-authentication-and-partner-keys.md |
Updates key-management documentation. |
docs/api/pages/02-quick-start-with-the-sdk.md |
Updates SDK authentication guidance. |
docs/api/openapi/vortex.openapi.json |
Adds limits and credential schemas. |
docs/api/openapi/vortex.openapi.d.ts |
Regenerates OpenAPI types. |
apps/dashboard/src/services/api/limits.service.ts |
Adds limits API client. |
apps/dashboard/src/services/api/api-keys.service.ts |
Adds credential API client. |
apps/dashboard/src/services/api/api-client.ts |
Supports DELETE request bodies. |
apps/dashboard/src/routeTree.gen.ts |
Registers new dashboard routes. |
apps/dashboard/src/routes/_app/limits.tsx |
Adds limits page. |
apps/dashboard/src/routes/_app/api-keys.tsx |
Adds API-keys page. |
apps/dashboard/src/hooks/useLimits.ts |
Fetches approved-corridor limits. |
apps/dashboard/src/hooks/useApiKeys.ts |
Manages credential queries. |
apps/dashboard/src/domain/api-credentials.ts |
Groups paired key records. |
apps/dashboard/src/domain/api-credentials.test.ts |
Tests credential grouping. |
apps/dashboard/src/components/limits/LimitsCard.tsx |
Displays monthly usage. |
apps/dashboard/src/components/layout/AppSidebar.tsx |
Adds navigation entries. |
apps/dashboard/src/components/api-keys/CreateApiCredentialDialog.tsx |
Implements credential creation. |
apps/dashboard/src/components/api-keys/ApiCredentialsTable.tsx |
Lists and revokes credentials. |
apps/dashboard/e2e/support/mockBackend.ts |
Mocks limits and key APIs. |
apps/dashboard/e2e/limits.spec.ts |
Tests limits UI. |
apps/dashboard/e2e/api-keys.spec.ts |
Tests credential lifecycle. |
apps/api/src/test-utils/fake-world/fake-anchors.ts |
Adds fake limit periods. |
apps/api/src/models/apiKey.model.ts |
Adds credential identifiers. |
apps/api/src/database/migrations/055-add-credential-id-to-api-keys.ts |
Migrates credential pairing. |
apps/api/src/api/services/limits.service.ts |
Resolves unified limits. |
apps/api/src/api/services/limits.service.test.ts |
Tests limit resolution. |
apps/api/src/api/services/alfredpay/alfredpay.helpers.ts |
Aggregates monthly usage. |
apps/api/src/api/services/alfredpay/alfredpay.helpers.test.ts |
Tests usage aggregation. |
apps/api/src/api/services/alfredpay/alfredpay-limits.service.ts |
Adds ARS limits. |
apps/api/src/api/services/alfredpay/alfredpay-limits.service.test.ts |
Tests ARS indexing. |
apps/api/src/api/routes/v1/limits.route.ts |
Adds authenticated limits route. |
apps/api/src/api/routes/v1/index.ts |
Mounts limits routes. |
apps/api/src/api/routes/v1/api-keys.route.ts |
Updates revocation documentation. |
apps/api/src/api/controllers/userApiKeys.controller.ts |
Adds paired credential handling. |
apps/api/src/api/controllers/userApiKeys.controller.test.ts |
Tests key lifecycle changes. |
apps/api/src/api/controllers/limits.controller.ts |
Validates limits requests. |
apps/api/src/api/controllers/limits.controller.test.ts |
Tests request rejection. |
apps/api/src/api/controllers/admin/partnerApiKeys.controller.ts |
Pairs partner credentials. |
.agents/skills/vortex-integration/SKILL.md |
Updates integration guidance. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const { name, expiresAt } = (req.body ?? {}) as CreateApiKeyBody; | ||
|
|
||
| try { | ||
| const baseName = name?.trim() || "API Key"; |
| createdAt: { [Op.gte]: startsAt, [Op.lt]: endsAt }, | ||
| currentPhase: "complete", |
| const activeKeyCount = await ApiKey.count({ where: { isActive: true, partnerId: null, partnerName: null, userId } }); | ||
| if (activeKeyCount + 2 > MAX_ACTIVE_KEYS_PER_USER) { |
| return; | ||
| } | ||
|
|
||
| if (expirationDate.getTime() <= Date.now()) { |
| delete usedLimit.month; | ||
| expect(() => aveniaAccountLimitsSchema.parse(body)).toThrow(); |
| Every authenticated endpoint takes: | ||
| - Header: `X-API-Key: sk_<env>_<32chars>` | ||
| - Body field: `"publicKey": "pk_<env>_<...>"` | ||
| - Quote body field: `"apiKey": "pk_<env>_<...>"` |
| "credentialId": { | ||
| "description": "Shared identifier for the public and secret records. Null only for ambiguous legacy records.", | ||
| "type": ["string", "null"] | ||
| }, |
ebma
left a comment
There was a problem hiding this comment.
Review summary
I reviewed the latest implementation and the production migration implications. The limits work is coherent, and the code head immediately before the documentation-only commit had green test and CI checks. The remaining concern is that an API credential is still modeled as two independently valid rows, with a nullable correlation ID that controllers and clients must interpret correctly. That leaves lifecycle and authorization invariants distributed across the API, dashboard, and migration.
The main actionable findings are inline:
- P1: revocation can disable only one half of a credential.
- P2: public and secret values from different credentials are not rejected at the authentication boundary.
- P2: expired rows still consume the active credential cap.
- P2: the migration infers credential relationships from mutable display names.
I recommend retaining distinct public and secret values but representing them as one credential record with one profile subject, optional managing partner, environment, expiry, and atomic revocation lifecycle. Self-service and delegated credentials should use the same service; users without an interactive signup should receive unique managed profiles, never a shared dummy identity.
I published the full implementation and production rollout plan in docs/plans/api-credential-unification.md. Before production, run the digest backfill, explicitly map or reissue every active pair, provision missing managed profiles, and make deployment fail closed unless active legacy, unpaired, and ownerless rows are all zero. Digest backfill alone does not establish pair or profile ownership.
…-to-dashboard # Conflicts: # apps/api/src/api/services/ramp/ramp.service.ts
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 125 out of 126 changed files in this pull request and generated no new comments.
Suppressed comments (8)
apps/api/src/api/controllers/auth.controller.ts:93
- If
markManagedProfileClaimedhits a database error after Supabase has already minted the session, the outer catch reports “Invalid OTP or OTP expired” and withholds the valid tokens. Keep this post-auth bookkeeping best-effort like entity creation; on failure, skip eager entity creation so a technical profile is not accidentally given one.
const managedSubjectType = await markManagedProfileClaimed(result.user_id);
apps/api/src/api/routes/v1/ramp-info.route.ts:19
- This route does not run optional Supabase auth, and the controller only accepts
req.credential, so the documentedAuthorization: Bearerform always returnsCREDENTIAL_REQUIRED. Add session authentication and resolve the profile from either the session or credential, while defining how mixed identities are handled.
packages/sdk/scripts/delete-api-key.ts:61 - The list endpoint returns
{ credentials: [...] }, not{ apiCredentials: [...] }, so this script crashes before presenting a credential to revoke. Map the wire property to the local name (or rename all accesses).
apps/api/src/api/services/rampInfo.service.ts:29 - This aggregates provider records from every customer entity owned by the profile. Ramp execution resolves only the active/default entity, so an approved inactive individual entity can make
canBuy/canSelltrue while the active business entity is pending and registration will fail. Resolve the same effective entity as registration without creating an entity for technical profiles.
apps/api/src/api/services/rampInfo.service.ts:9 - This duplicates the shared corridor matrix and ignores customer type. For example, an approved AR business record is reported as buy/sell capable even though
packages/shared/src/corridors.ts:11-26defines AR as individual-only. Derive corridor/provider support fromCORRIDOR_CAPABILITIESandisCorridorSupportedForCustomerType, includingcustomerTypein the query.
docs/operations-api-credential-rollout.md:26 - The documented preflight gate is not implemented:
validateManifestverifies ownership and key environment but never compares the two legacyexpiresAtvalues with each other or the manifest, and its partner lookup does not requireisActive. The runbook therefore promises rejection while the migration can silently accept expiry disagreement or an inactive partner; enforce these checks before relying on this rollout gate.
packages/sdk/scripts/fetch-api-keys.ts:55 GET /v1/api-credentialsreturns{ credentials: [...] }, but this cast and the following accesses expectapiCredentials; the first.lengthaccess therefore throws at runtime. Normalize the actual response property before using the existing local name.
apps/dashboard/src/components/limits/LimitsCard.tsx:16- API amount fields are decimal strings, but converting them to
Numbercan lose precision or overflow, producing an incorrect progress percentage for large limits. Compute the bounded ratio with a decimal-safe representation instead of binary floating point.
No description provided.