@@ -28,8 +28,10 @@ import {
2828 type WorkerInstanceGroupType ,
2929} from "@trigger.dev/database" ;
3030import { acceptsWorkerStamp } from "./workerGroupAccess" ;
31+ import { matchesAnyManagedWorkerSecret } from "./managedWorkerSecret.server" ;
32+ import { sanitizeWorkerHeaders } from "./sanitizeWorkerHeaders" ;
3133import { json } from "@remix-run/server-runtime" ;
32- import { createHash , timingSafeEqual } from "crypto" ;
34+ import { createHash } from "crypto" ;
3335import { customAlphabet } from "nanoid" ;
3436import { z } from "zod" ;
3537import { env } from "~/env.server" ;
@@ -129,51 +131,108 @@ export class WorkerGroupTokenService extends WithRunEngine {
129131 const workerGroup = await this . _prisma . workerInstanceGroup . findFirst ( {
130132 where : {
131133 token : {
132- tokenHash,
134+ OR : [
135+ { tokenHash } ,
136+ { previousTokenHash : tokenHash , previousTokenValidUntil : { gt : new Date ( ) } } ,
137+ ] ,
133138 } ,
134139 } ,
140+ include : {
141+ token : { select : { tokenHash : true , previousTokenValidUntil : true } } ,
142+ } ,
135143 } ) ;
136144
137145 if ( ! workerGroup ) {
138- logger . warn ( "[WorkerGroupTokenService] No matching worker group found" , { token } ) ;
146+ logger . warn ( "[WorkerGroupTokenService] No matching worker group found" ) ;
139147 return null ;
140148 }
141149
142- return workerGroup ;
150+ const { token : matchedToken , ...group } = workerGroup ;
151+ const tokenValidUntil =
152+ matchedToken . tokenHash === tokenHash
153+ ? undefined
154+ : ( matchedToken . previousTokenValidUntil ?? undefined ) ;
155+
156+ return { ...group , tokenValidUntil } ;
143157 }
144158
145- async rotateToken ( { workerGroupId } : { workerGroupId : string } ) {
159+ async rotateToken ( { workerGroupId, graceMs } : { workerGroupId : string ; graceMs : number } ) {
146160 const workerGroup = await this . _prisma . workerInstanceGroup . findFirst ( {
147161 where : {
148162 id : workerGroupId ,
149163 } ,
164+ include : {
165+ token : true ,
166+ } ,
150167 } ) ;
151168
152169 if ( ! workerGroup ) {
153170 logger . error ( "[WorkerGroupTokenService] WorkerGroup not found" , { workerGroupId } ) ;
154- return ;
171+ return { outcome : "not_found" as const } ;
172+ }
173+
174+ const activeGraceUntil = workerGroup . token . previousTokenValidUntil ;
175+ if ( activeGraceUntil && activeGraceUntil . getTime ( ) > Date . now ( ) ) {
176+ logger . error ( "[WorkerGroupTokenService] Previous token still within grace" , {
177+ workerGroupId,
178+ previousTokenValidUntil : activeGraceUntil ,
179+ } ) ;
180+ return { outcome : "grace_active" as const , previousTokenValidUntil : activeGraceUntil } ;
155181 }
156182
157183 const rawToken = await this . generateToken ( ) ;
184+ const previousTokenValidUntil = new Date ( Date . now ( ) + graceMs ) ;
158185
159- const workerGroupToken = await this . _prisma . workerGroupToken . update ( {
186+ const updated = await this . _prisma . workerGroupToken . updateMany ( {
160187 where : {
161188 id : workerGroup . tokenId ,
189+ tokenHash : workerGroup . token . tokenHash ,
162190 } ,
163191 data : {
164192 tokenHash : rawToken . hash ,
193+ previousTokenHash : workerGroup . token . tokenHash ,
194+ previousTokenValidUntil,
195+ } ,
196+ } ) ;
197+
198+ if ( updated . count === 0 ) {
199+ logger . error ( "[WorkerGroupTokenService] Concurrent token rotation detected" , {
200+ workerGroupId,
201+ } ) ;
202+ return { outcome : "conflict" as const } ;
203+ }
204+
205+ return {
206+ outcome : "rotated" as const ,
207+ plaintext : rawToken . plaintext ,
208+ previousTokenValidUntil,
209+ } ;
210+ }
211+
212+ async revokePreviousToken ( { workerGroupId } : { workerGroupId : string } ) {
213+ const workerGroup = await this . _prisma . workerInstanceGroup . findFirst ( {
214+ where : {
215+ id : workerGroupId ,
165216 } ,
166217 } ) ;
167218
168- if ( ! workerGroupToken ) {
169- logger . error ( "[WorkerGroupTokenService] WorkerGroupToken not found" , { workerGroupId } ) ;
219+ if ( ! workerGroup ) {
220+ logger . error ( "[WorkerGroupTokenService] WorkerGroup not found" , { workerGroupId } ) ;
170221 return ;
171222 }
172223
224+ const workerGroupToken = await this . _prisma . workerGroupToken . update ( {
225+ where : {
226+ id : workerGroup . tokenId ,
227+ } ,
228+ data : {
229+ previousTokenHash : null ,
230+ previousTokenValidUntil : null ,
231+ } ,
232+ } ) ;
233+
173234 return {
174235 id : workerGroupToken . id ,
175- tokenHash : workerGroupToken . tokenHash ,
176- plaintext : rawToken . plaintext ,
177236 } ;
178237 }
179238
@@ -203,7 +262,6 @@ export class WorkerGroupTokenService extends WithRunEngine {
203262
204263 if ( ! token . startsWith ( this . tokenPrefix ) ) {
205264 logger . error ( "[WorkerGroupTokenService] Token does not start with expected prefix" , {
206- token,
207265 prefix : this . tokenPrefix ,
208266 } ) ;
209267 return ;
@@ -227,22 +285,13 @@ export class WorkerGroupTokenService extends WithRunEngine {
227285 return ;
228286 }
229287
230- const encoder = new TextEncoder ( ) ;
231-
232- const a = encoder . encode ( managedWorkerSecret ) ;
233- const b = encoder . encode ( env . MANAGED_WORKER_SECRET ) ;
234-
235- if ( a . byteLength !== b . byteLength ) {
236- logger . error ( "[WorkerGroupTokenService] Managed secret length mismatch" , {
237- managedWorkerSecret,
238- headers : this . sanitizeHeaders ( request ) ,
239- } ) ;
240- return ;
241- }
242-
243- if ( ! timingSafeEqual ( a , b ) ) {
288+ if (
289+ ! matchesAnyManagedWorkerSecret ( managedWorkerSecret , [
290+ env . MANAGED_WORKER_SECRET ,
291+ env . MANAGED_WORKER_SECRET_PREVIOUS ,
292+ ] )
293+ ) {
244294 logger . error ( "[WorkerGroupTokenService] Managed secret mismatch" , {
245- managedWorkerSecret,
246295 headers : this . sanitizeHeaders ( request ) ,
247296 } ) ;
248297 return ;
@@ -256,7 +305,7 @@ export class WorkerGroupTokenService extends WithRunEngine {
256305 const workerGroup = await this . findWorkerGroup ( { token } ) ;
257306
258307 if ( ! workerGroup ) {
259- logger . warn ( "[WorkerGroupTokenService] Worker group not found" , { token } ) ;
308+ logger . warn ( "[WorkerGroupTokenService] Worker group not found" , { instanceName } ) ;
260309 return ;
261310 }
262311
@@ -285,6 +334,7 @@ export class WorkerGroupTokenService extends WithRunEngine {
285334 workloadType : workerGroup . workloadType ,
286335 allowedSubscriptions : env . RUN_ENGINE_WORKER_QUEUE_SUBSCRIPTIONS [ workerGroup . id ] ?? [ ] ,
287336 mixedRuntime : env . RUN_ENGINE_MIXED_RUNTIME_WORKER_GROUPS . includes ( workerGroup . id ) ,
337+ tokenValidUntil : workerGroup . tokenValidUntil ,
288338 } ) ;
289339 }
290340 ) ;
@@ -296,6 +346,15 @@ export class WorkerGroupTokenService extends WithRunEngine {
296346 return ;
297347 }
298348
349+ if ( result . val ?. tokenValidUntil && result . val . tokenValidUntil . getTime ( ) <= Date . now ( ) ) {
350+ await authenticatedWorkerInstanceCache . authenticatedWorkerInstance . remove ( cacheKey . join ( "-" ) ) ;
351+ logger . warn ( "[WorkerGroupTokenService] Previous worker group token has expired" , {
352+ instanceName,
353+ workerGroupId : result . val . workerGroupId ,
354+ } ) ;
355+ return ;
356+ }
357+
299358 return result . val ;
300359 }
301360
@@ -368,16 +427,8 @@ export class WorkerGroupTokenService extends WithRunEngine {
368427 }
369428 }
370429
371- private sanitizeHeaders ( request : Request , skipHeaders = [ "authorization" ] ) {
372- const sanitizedHeaders : Partial < Record < string , string > > = { } ;
373-
374- for ( const [ key , value ] of request . headers . entries ( ) ) {
375- if ( ! skipHeaders . includes ( key . toLowerCase ( ) ) ) {
376- sanitizedHeaders [ key ] = value ;
377- }
378- }
379-
380- return sanitizedHeaders ;
430+ private sanitizeHeaders ( request : Request ) {
431+ return sanitizeWorkerHeaders ( request . headers ) ;
381432 }
382433}
383434
@@ -391,6 +442,7 @@ export type AuthenticatedWorkerInstanceOptions = WithRunEngineOptions<
391442 WorkerQueueConsumerOptions & {
392443 type : WorkerInstanceGroupType ;
393444 name : string ;
445+ tokenValidUntil ?: Date ;
394446 }
395447> ;
396448
@@ -400,6 +452,7 @@ export class AuthenticatedWorkerInstance extends WithRunEngine {
400452 readonly workerGroupId : string ;
401453 readonly workerInstanceId : string ;
402454 readonly masterQueue : string ;
455+ readonly tokenValidUntil ?: Date ;
403456 private readonly queueConsumer : WorkerQueueConsumer ;
404457
405458 // FIXME: Required for unmanaged workers
@@ -413,6 +466,7 @@ export class AuthenticatedWorkerInstance extends WithRunEngine {
413466 this . workerGroupId = opts . workerGroupId ;
414467 this . workerInstanceId = opts . workerInstanceId ;
415468 this . masterQueue = opts . masterQueue ;
469+ this . tokenValidUntil = opts . tokenValidUntil ;
416470 this . queueConsumer = createWorkerQueueConsumer ( opts ) ;
417471
418472 if ( refusedSubscriptions ( opts ) . length > 0 && ! warnedRefusedGroups . has ( opts . workerGroupId ) ) {
0 commit comments