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
15 changes: 15 additions & 0 deletions db/migrations/20260628200000_add_api_key_usage_tracking.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
exports.up = async function up(knex) {
await knex.schema.alterTable('api_keys', (table) => {
table.timestamp('last_used_at', { useTz: true });
table.integer('request_count').notNullable().defaultTo(0);
table.text('last_ip');
});
};

exports.down = async function down(knex) {
await knex.schema.alterTable('api_keys', (table) => {
table.dropColumn('last_used_at');
table.dropColumn('request_count');
table.dropColumn('last_ip');
});
};
5 changes: 5 additions & 0 deletions docs/VAULT_API.md
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,11 @@ invalid `verifier` value:
- Authorization checks for vault cancellation (creator or admin only)
- Input validation for all parameters
- Idempotency support for vault creation
- **Stellar Destination Address & Memo Hardening**:
- Accepts both classic (`G...`) and muxed (`M...`) account addresses.
- Rejects contract addresses (`C...`) where user/escrow accounts are required (e.g. verifier and destinations).
- Rejects unsafe zero/burn and all-ones addresses to prevent routing slashed funds irrecoverably.
- Enforces muxed addresses (`M...`) for known exchange destinations requiring a memo.

## Testing

Expand Down
29 changes: 29 additions & 0 deletions docs/api-keys.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,35 @@ curl -X POST "http://localhost:3000/api/api-keys/<api-key-id>/revoke" \
-H "x-user-id: user-123"
```

### Usage Analytics

`GET /api/orgs/:id/api-keys/usage`

Retrieves per-key usage analytics (last-used timestamp, total request counter, and last-seen IP) for all keys under the organization. Restricted to organization owners and admins. Never exposes secrets or hashes.

```bash
curl "http://localhost:3000/api/orgs/org-123/api-keys/usage" \
-H "x-user-id: user-123"
```

Response:
```json
{
"usage": [
{
"id": "a1b2c3d4...",
"label": "read-only analytics key",
"scopes": ["read:analytics"],
"createdAt": "2026-06-28T12:00:00.000Z",
"revokedAt": null,
"lastUsedAt": "2026-06-28T18:25:00.000Z",
"requestCount": 42,
"lastIp": "192.168.1.10"
}
]
}
```

## Using a key

Use the issued secret in the `x-api-key` header on API-key protected endpoints.
Expand Down
85 changes: 63 additions & 22 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion src/app-bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ import { adminRouter } from './routes/admin.js'
import { adminVerifiersRouter } from './routes/adminVerifiers.js'
import { adminWebhooksRouter } from './routes/adminWebhooks.js'
import { verificationsRouter } from './routes/verifications.js'
import { apiKeysRouter } from './routes/apiKeys.js'
import { apiKeysRouter, getApiKeyUsageHandler } from './routes/apiKeys.js'
import { requireUserAuth } from './middleware/auth.js'
import { requireOrgAccess } from './middleware/orgAuth.js'
import { notificationsRouter } from './routes/notifications.js'
import { notificationPreferencesRouter } from './routes/notificationPreferences.js'
import { webhooksRouter } from './routes/webhooks.js'
Expand Down Expand Up @@ -75,6 +77,7 @@ export function bootstrapApp(options: BootstrapOptions = {}) {
app.use('/api/admin/verifiers', adminVerifiersRouter)
app.use('/api/admin/webhooks', adminWebhooksRouter)
app.use('/api/verifications', verificationsRouter)
app.get('/api/orgs/:orgId/api-keys/usage', requireUserAuth, requireOrgAccess('owner', 'admin'), getApiKeyUsageHandler)
app.use('/api/api-keys', apiKeysRouter)
app.use('/api/notifications', notificationsRouter)
app.use('/api/users/me/notification-preferences', notificationPreferencesRouter)
Expand Down
6 changes: 3 additions & 3 deletions src/config/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@

// ── Misc / Limits ───────────────────────────────────────
MAX_JSON_BODY_SIZE: z.string().default("500kb"),
NOTIFICATION_PROVIDER: z.string().optional(),

Check failure on line 192 in src/config/env.ts

View workflow job for this annotation

GitHub Actions / test-and-migrate

An object literal cannot have multiple properties with the same name.
HORIZON_LAG_THRESHOLD: nonNegativeInt(10),
HORIZON_SHUTDOWN_TIMEOUT_MS: positiveInt(30_000),

Expand Down Expand Up @@ -275,9 +275,9 @@
*/
export function getEnv(): Env {
if (!_validated) {
throw new Error("Environment not validated yet — call initEnv() first");
initEnv()
}
return _validated;
return _validated!;
}

/** Reset internal state — exposed for tests only. */
Expand Down Expand Up @@ -343,7 +343,7 @@
for (const { key, sentinel } of insecureDefaults) {
if (validated[key] === sentinel) {
const w: EnvWarning = {
variable: key,

Check failure on line 346 in src/config/env.ts

View workflow job for this annotation

GitHub Actions / test-and-migrate

Object literal may only specify known properties, and 'variable' does not exist in type 'EnvWarning'.
message: `${key} is using its insecure default value`,
};
warnings.push(w);
Expand Down Expand Up @@ -376,7 +376,7 @@
);
if (present.length > 0 && present.length < sorobanVars.length) {
const w: EnvWarning = {
variable: "SOROBAN_*",

Check failure on line 379 in src/config/env.ts

View workflow job for this annotation

GitHub Actions / test-and-migrate

Object literal may only specify known properties, and 'variable' does not exist in type 'EnvWarning'.
message:
"Partial Soroban configuration detected; submit mode will be disabled",
};
Expand Down Expand Up @@ -436,7 +436,7 @@
return { env: result.data, warnings };
}

/** Returns parsed JWT keys from the environment. */
/** Warnings emitted during validation (not hard failures). */
export function getJwtKeys(env: Env): JwtKey[] {
return (env as any).JWT_KEYS as JwtKey[];
}
Loading
Loading