@@ -27,6 +27,7 @@ export interface ApiTokenPayload {
2727 app_id : string ;
2828 scope : string ;
2929 key_version : number ;
30+ sid ?: string ;
3031 // Standard JWT claims
3132 sub : string ; // user_id
3233 aud : string ; // app_id
@@ -55,8 +56,9 @@ export async function createEchoAccessJwtToken(params: {
5556 scope : string ;
5657 expiry : Date ;
5758 keyVersion ?: number ;
59+ sessionId ?: string ;
5860} ) : Promise < string > {
59- const { userId, appId, scope, expiry, keyVersion = 1 } = params ;
61+ const { userId, appId, scope, expiry, keyVersion = 1 , sessionId } = params ;
6062
6163 const tokenId = nanoid ( 16 ) ;
6264 const now = Math . floor ( Date . now ( ) / 1000 ) ; // divide by 1000 to convert to seconds
@@ -67,6 +69,7 @@ export async function createEchoAccessJwtToken(params: {
6769 app_id : appId ,
6870 scope,
6971 key_version : keyVersion ,
72+ ...( sessionId ? { sid : sessionId } : { } ) ,
7073 } )
7174 . setProtectedHeader ( { alg : 'HS256' } )
7275 . setSubject ( userId )
@@ -192,7 +195,8 @@ export interface RefreshTokenError {
192195 * Handle refresh token logic
193196 */
194197export async function handleRefreshToken (
195- refreshToken : string
198+ refreshToken : string ,
199+ metadata ?: { deviceName ?: string ; userAgent ?: string ; ipAddress ?: string }
196200) : Promise < RefreshTokenResponse | RefreshTokenError > {
197201 console . log ( '🔄 Processing refresh token request' ) ;
198202
@@ -204,6 +208,7 @@ export async function handleRefreshToken(
204208 include : {
205209 user : true ,
206210 echoApp : true ,
211+ session : true ,
207212 } ,
208213 } ) ;
209214
@@ -240,15 +245,27 @@ export async function handleRefreshToken(
240245 } ;
241246 }
242247
243- /* 3️⃣ Check if echo app is still active */
248+ /* 3️⃣ Check if session and echo app are still active */
249+ if (
250+ ! echoRefreshTokenRecord . session ||
251+ echoRefreshTokenRecord . session . revokedAt
252+ ) {
253+ return {
254+ error : 'invalid_grant' ,
255+ error_description : 'Session is revoked or invalid' ,
256+ } ;
257+ }
258+
259+ const sessionId = echoRefreshTokenRecord . session . id ;
260+
244261 if ( echoRefreshTokenRecord . echoApp . isArchived ) {
245262 return {
246263 error : 'invalid_grant' ,
247264 error_description : 'Echo app is no longer active' ,
248265 } ;
249266 }
250267
251- /* 4️⃣ Generate new refresh token */
268+ /* 4️⃣ Generate new refresh token (rotate within the same session) */
252269 const newEchoRefreshTokenValue = `refresh_${ nanoid ( 48 ) } ` ;
253270 const newEchoRefreshTokenExpiry = createEchoRefreshTokenExpiry ( ) ;
254271
@@ -266,6 +283,18 @@ export async function handleRefreshToken(
266283 userId : echoRefreshTokenRecord . userId ,
267284 echoAppId : echoRefreshTokenRecord . echoAppId ,
268285 scope : echoRefreshTokenRecord . scope ,
286+ sessionId,
287+ } ,
288+ } ) ;
289+
290+ // Update session last seen
291+ await db . appSession . update ( {
292+ where : { id : sessionId } ,
293+ data : {
294+ lastSeenAt : new Date ( ) ,
295+ ...( metadata ?. userAgent ? { userAgent : metadata . userAgent } : { } ) ,
296+ ...( metadata ?. ipAddress ? { ipAddress : metadata . ipAddress } : { } ) ,
297+ ...( metadata ?. deviceName ? { deviceName : metadata . deviceName } : { } ) ,
269298 } ,
270299 } ) ;
271300
@@ -278,6 +307,7 @@ export async function handleRefreshToken(
278307 scope : echoRefreshTokenRecord . scope ,
279308 keyVersion : 1 ,
280309 expiry : newEchoAccessTokenExpiry ,
310+ sessionId,
281311 } ) ;
282312
283313 /* 6️⃣ Return new tokens with user and app info */
@@ -313,6 +343,9 @@ export interface AuthCodeTokenRequest {
313343 redirect_uri : string ;
314344 client_id : string ;
315345 code_verifier : string ;
346+ deviceName ?: string ;
347+ userAgent ?: string ;
348+ ipAddress ?: string ;
316349}
317350
318351/**
@@ -331,7 +364,15 @@ export type InitialTokenError = RefreshTokenError;
331364export async function handleInitialTokenIssuance (
332365 params : AuthCodeTokenRequest
333366) : Promise < InitialTokenResponse | InitialTokenError > {
334- const { code, redirect_uri, client_id, code_verifier } = params ;
367+ const {
368+ code,
369+ redirect_uri,
370+ client_id,
371+ code_verifier,
372+ deviceName,
373+ userAgent,
374+ ipAddress,
375+ } = params ;
335376
336377 console . log ( '🎫 Processing authorization code token request' ) ;
337378
@@ -473,28 +514,28 @@ export async function handleInitialTokenIssuance(
473514 } ;
474515 }
475516
476- /* 🔟 Generate refresh token */
477- const echoRefreshTokenValue = `refresh_${ nanoid ( 48 ) } ` ;
478- const echoRefreshTokenExpiry = createEchoRefreshTokenExpiry ( ) ;
479-
480- // Deactivate any existing refresh tokens for this app
481- await db . refreshToken . updateMany ( {
482- where : {
517+ /* 🔟 Create an app session and refresh token */
518+ const appSession = await db . appSession . create ( {
519+ data : {
483520 userId : user . id ,
484521 echoAppId : echoApp . id ,
485- isArchived : false ,
522+ deviceName,
523+ userAgent,
524+ ipAddress,
486525 } ,
487- data : { isArchived : true } ,
488526 } ) ;
489527
490- // Create new refresh token
528+ const echoRefreshTokenValue = `refresh_${ nanoid ( 48 ) } ` ;
529+ const echoRefreshTokenExpiry = createEchoRefreshTokenExpiry ( ) ;
530+
491531 const newEchoRefreshToken = await db . refreshToken . create ( {
492532 data : {
493533 token : echoRefreshTokenValue ,
494534 expiresAt : echoRefreshTokenExpiry ,
495535 userId : user . id ,
496536 echoAppId : echoApp . id ,
497537 scope : authData . scope ,
538+ sessionId : appSession . id ,
498539 } ,
499540 } ) ;
500541
@@ -507,6 +548,7 @@ export async function handleInitialTokenIssuance(
507548 scope : authData . scope ,
508549 keyVersion : 1 ,
509550 expiry : newEchoAccessTokenExpiry ,
551+ sessionId : appSession . id ,
510552 } ) ;
511553
512554 console . log ( '🎫 JWT access token generated' ) ;
0 commit comments