@@ -3,6 +3,7 @@ import { prisma } from '../lib/prisma.js';
33import { asyncHandler } from '../middleware/errorHandler.js' ;
44import { AppError } from '../middleware/errorHandler.js' ;
55import { quotaManagerService } from '../services/keys/quota-manager.js' ;
6+ import { rotateApiKeyWithGracePeriod , settleGracePeriod } from '../services/keys/rotation.js' ;
67import { randomBytes , createHash } from 'node:crypto' ;
78
89export const apiKeysRouter = Router ( ) ;
@@ -37,7 +38,8 @@ apiKeysRouter.get('/', asyncHandler(async (req, res) => {
3738 quota : true ,
3839 } ,
3940 } ) ;
40- res . json ( { keys } ) ;
41+ const settled = await Promise . all ( keys . map ( ( key ) => settleGracePeriod ( key ) ) ) ;
42+ res . json ( { keys : settled } ) ;
4143} ) ) ;
4244
4345apiKeysRouter . get ( '/:keyId' , asyncHandler ( async ( req , res ) => {
@@ -47,7 +49,8 @@ apiKeysRouter.get('/:keyId', asyncHandler(async (req, res) => {
4749 include : { quota : true } ,
4850 } ) ;
4951 if ( ! key || key . tenantId !== tenantId ) throw new AppError ( 404 , 'API key not found' , 'KEY_NOT_FOUND' ) ;
50- res . json ( key ) ;
52+ const settled = await settleGracePeriod ( key ) ;
53+ res . json ( settled ) ;
5154} ) ) ;
5255
5356apiKeysRouter . delete ( '/:keyId' , asyncHandler ( async ( req , res ) => {
@@ -62,8 +65,20 @@ apiKeysRouter.get('/:keyId/usage', asyncHandler(async (req, res) => {
6265 const tenantId = resolveTenant ( req ) ;
6366 const key = await prisma . apiKey . findUnique ( { where : { keyId : req . params . keyId } } ) ;
6467 if ( ! key || key . tenantId !== tenantId ) throw new AppError ( 404 , 'API key not found' , 'KEY_NOT_FOUND' ) ;
68+ const settled = await settleGracePeriod ( key ) ;
6569 const summary = await quotaManagerService . getUsageSummary ( req . params . keyId ) ;
66- res . json ( summary ) ;
70+ res . json ( {
71+ ...summary ,
72+ rotation : {
73+ rotatedAt : settled . rotatedAt ,
74+ gracePeriodEndsAt : settled . gracePeriodEndsAt ,
75+ predecessorKeyId : settled . predecessorKeyId ,
76+ successorKeyId : settled . successorKeyId ,
77+ inGracePeriod : Boolean (
78+ settled . isActive && settled . gracePeriodEndsAt && settled . gracePeriodEndsAt . getTime ( ) > Date . now ( ) ,
79+ ) ,
80+ } ,
81+ } ) ;
6782} ) ) ;
6883
6984apiKeysRouter . get ( '/:keyId/usage/daily' , asyncHandler ( async ( req , res ) => {
@@ -93,19 +108,24 @@ apiKeysRouter.post('/:keyId/rotate', asyncHandler(async (req, res) => {
93108 const tenantId = resolveTenant ( req ) ;
94109 const key = await prisma . apiKey . findUnique ( { where : { keyId : req . params . keyId } } ) ;
95110 if ( ! key || key . tenantId !== tenantId ) throw new AppError ( 404 , 'API key not found' , 'KEY_NOT_FOUND' ) ;
111+ if ( ! key . isActive ) throw new AppError ( 409 , 'API key is not active' , 'KEY_INACTIVE' ) ;
96112
97- await prisma . apiKey . update ( { where : { keyId : req . params . keyId } , data : { isActive : false , revokedAt : new Date ( ) } } ) ;
113+ const { gracePeriodHours } = req . body as { gracePeriodHours ?: number } ;
114+ const { previousKey, newKey, gracePeriodEndsAt } = await rotateApiKeyWithGracePeriod ( {
115+ tenantId,
116+ keyId : key . keyId ,
117+ gracePeriodHours,
118+ } ) ;
98119
99- const newKeyId = `ak_ ${ Date . now ( ) . toString ( 36 ) } _ ${ Math . random ( ) . toString ( 36 ) . slice ( 2 , 8 ) } ` ;
100- const newKey = await prisma . apiKey . create ( {
101- data : {
102- tenantId ,
103- keyId : newKeyId ,
104- description : key . description ? ` ${ key . description } (rotated)` : 'Rotated key' ,
105- expiresAt : key . expiresAt ,
120+ res . status ( 201 ) . json ( {
121+ keyId : newKey . keyId ,
122+ description : newKey . description ,
123+ rotatedFrom : previousKey . keyId ,
124+ gracePeriod : {
125+ predecessorKeyId : previousKey . keyId ,
126+ gracePeriodEndsAt ,
106127 } ,
107128 } ) ;
108- res . status ( 201 ) . json ( { keyId : newKey . keyId , description : newKey . description , rotatedFrom : key . keyId } ) ;
109129} ) ) ;
110130
111131apiKeysRouter . post ( '/:keyId/revoke' , asyncHandler ( async ( req , res ) => {
0 commit comments