@@ -26,7 +26,12 @@ import { Counter } from "prom-client";
2626import { env } from "../env.js" ;
2727import { register } from "../metrics.js" ;
2828import {
29+ checkPreviousKeyRunCutoff ,
30+ previousKeyCutoffEnforced ,
31+ previousKeyCutoffMode ,
2932 verifyDeploymentIdHeader ,
33+ type WorkloadAuthTransport ,
34+ type WorkloadTokenKey ,
3035 workloadTokenEnforced ,
3136 workloadTokensEnabled ,
3237} from "../workloadToken.js" ;
@@ -93,6 +98,29 @@ function legacifyCheckpointType<T extends { checkpoint?: { type: string } | null
9398 return item ;
9499}
95100
101+ type SupervisorSocketData = WorkloadClientSocketData & {
102+ workloadTokenKey ?: WorkloadTokenKey ;
103+ workloadTokenClaims ?: WorkloadDeploymentTokenClaims ;
104+ } ;
105+
106+ type PreviousKeyCaller = {
107+ transport : WorkloadAuthTransport ;
108+ remoteAddress ?: string ;
109+ remotePort ?: number ;
110+ runnerId ?: string ;
111+ userAgent ?: string ;
112+ route : string ;
113+ claims ?: WorkloadDeploymentTokenClaims ;
114+ } ;
115+
116+ function runFriendlyIdFromMessage ( message : unknown ) : string {
117+ const friendlyId = ( message as { run ?: { friendlyId ?: unknown } } | undefined ) ?. run ?. friendlyId ;
118+ if ( typeof friendlyId !== "string" ) {
119+ throw new Error ( "Run event without a string run friendlyId" ) ;
120+ }
121+ return friendlyId ;
122+ }
123+
96124type WorkloadServerEvents = {
97125 runConnected : [
98126 {
@@ -139,7 +167,7 @@ export class WorkloadServer extends EventEmitter<WorkloadServerEvents> {
139167 WorkloadClientToServerEvents ,
140168 WorkloadServerToClientEvents ,
141169 DefaultEventsMap ,
142- WorkloadClientSocketData
170+ SupervisorSocketData
143171 > ;
144172
145173 private readonly runSockets = new Map <
@@ -148,7 +176,7 @@ export class WorkloadServer extends EventEmitter<WorkloadServerEvents> {
148176 WorkloadClientToServerEvents ,
149177 WorkloadServerToClientEvents ,
150178 DefaultEventsMap ,
151- WorkloadClientSocketData
179+ SupervisorSocketData
152180 >
153181 > ( ) ;
154182
@@ -224,7 +252,8 @@ export class WorkloadServer extends EventEmitter<WorkloadServerEvents> {
224252 * which is why environmentId stays gated on enforce.
225253 */
226254 private async authorizeWorkloadRequest (
227- req : IncomingMessage
255+ req : IncomingMessage ,
256+ runFriendlyId : string
228257 ) : Promise <
229258 { ok : true ; environmentId ?: string ; claims ?: WorkloadDeploymentTokenClaims } | { ok : false }
230259 > {
@@ -238,6 +267,21 @@ export class WorkloadServer extends EventEmitter<WorkloadServerEvents> {
238267 return { ok : false } ;
239268 }
240269
270+ if (
271+ result . key === "previous" &&
272+ ! this . previousKeyRunAllowed ( runFriendlyId , {
273+ transport : "http" ,
274+ remoteAddress : req . socket . remoteAddress ,
275+ remotePort : req . socket . remotePort ,
276+ runnerId : this . runnerIdFromRequest ( req ) ,
277+ userAgent : this . headerValueFromRequest ( req , "user-agent" ) ,
278+ route : `${ req . method } ${ req . url } ` ,
279+ claims : result . claims ,
280+ } )
281+ ) {
282+ return { ok : false } ;
283+ }
284+
241285 return {
242286 ok : true ,
243287 environmentId :
@@ -248,6 +292,31 @@ export class WorkloadServer extends EventEmitter<WorkloadServerEvents> {
248292 } ;
249293 }
250294
295+ private previousKeyRunAllowed ( runFriendlyId : string , caller : PreviousKeyCaller ) : boolean {
296+ const cutoff = checkPreviousKeyRunCutoff ( runFriendlyId ) ;
297+ const { claims, ...callerFields } = caller ;
298+ const logArgs = {
299+ runId : runFriendlyId ,
300+ key : "previous" ,
301+ cutoffResult : cutoff ,
302+ cutoffMode : previousKeyCutoffMode ,
303+ ...callerFields ,
304+ deployment : claims ?. deployment ,
305+ environmentId : claims ?. environment_id ,
306+ orgId : claims ?. org_id ,
307+ projectId : claims ?. project_id ,
308+ exp : claims ?. exp ,
309+ } ;
310+
311+ if ( cutoff === "pass" ) {
312+ this . logger . debug ( "Previous-key token accepted" , logArgs ) ;
313+ return true ;
314+ }
315+
316+ this . logger . warn ( "Previous-key token used for a run created after the cutoff" , logArgs ) ;
317+ return ! ( workloadTokenEnforced && previousKeyCutoffEnforced ) ;
318+ }
319+
251320 /**
252321 * reclaimCheckpoints asks the checkpoint service to delete a finished run's checkpoint storage.
253322 * Must be called after the reply is sent: it never delays the runner.
@@ -416,7 +485,7 @@ export class WorkloadServer extends EventEmitter<WorkloadServerEvents> {
416485 "POST" ,
417486 async ( ) => {
418487 const { req, reply, params, body } = ctx ;
419- const auth = await this . authorizeWorkloadRequest ( req ) ;
488+ const auth = await this . authorizeWorkloadRequest ( req , params . runFriendlyId ) ;
420489 if ( ! auth . ok ) {
421490 reply . empty ( 401 ) ;
422491 return ;
@@ -462,7 +531,7 @@ export class WorkloadServer extends EventEmitter<WorkloadServerEvents> {
462531 "POST" ,
463532 async ( ) => {
464533 const { req, reply, params, body } = ctx ;
465- const auth = await this . authorizeWorkloadRequest ( req ) ;
534+ const auth = await this . authorizeWorkloadRequest ( req , params . runFriendlyId ) ;
466535 if ( ! auth . ok ) {
467536 reply . empty ( 401 ) ;
468537 return ;
@@ -519,7 +588,7 @@ export class WorkloadServer extends EventEmitter<WorkloadServerEvents> {
519588 "POST" ,
520589 async ( ) => {
521590 const { req, reply, params, body } = ctx ;
522- const auth = await this . authorizeWorkloadRequest ( req ) ;
591+ const auth = await this . authorizeWorkloadRequest ( req , params . runFriendlyId ) ;
523592 if ( ! auth . ok ) {
524593 reply . empty ( 401 ) ;
525594 return ;
@@ -561,7 +630,7 @@ export class WorkloadServer extends EventEmitter<WorkloadServerEvents> {
561630 "GET" ,
562631 async ( ) => {
563632 const { reply, params, req } = ctx ;
564- const auth = await this . authorizeWorkloadRequest ( req ) ;
633+ const auth = await this . authorizeWorkloadRequest ( req , params . runFriendlyId ) ;
565634 if ( ! auth . ok ) {
566635 reply . empty ( 401 ) ;
567636 return ;
@@ -665,7 +734,7 @@ export class WorkloadServer extends EventEmitter<WorkloadServerEvents> {
665734 "GET" ,
666735 async ( ) => {
667736 const { req, reply, params } = ctx ;
668- const auth = await this . authorizeWorkloadRequest ( req ) ;
737+ const auth = await this . authorizeWorkloadRequest ( req , params . runFriendlyId ) ;
669738 if ( ! auth . ok ) {
670739 reply . empty ( 401 ) ;
671740 return ;
@@ -728,7 +797,7 @@ export class WorkloadServer extends EventEmitter<WorkloadServerEvents> {
728797 "GET" ,
729798 async ( ) => {
730799 const { req, reply, params } = ctx ;
731- const auth = await this . authorizeWorkloadRequest ( req ) ;
800+ const auth = await this . authorizeWorkloadRequest ( req , params . runFriendlyId ) ;
732801 if ( ! auth . ok ) {
733802 reply . empty ( 401 ) ;
734803 return ;
@@ -814,15 +883,15 @@ export class WorkloadServer extends EventEmitter<WorkloadServerEvents> {
814883 WorkloadClientToServerEvents ,
815884 WorkloadServerToClientEvents ,
816885 DefaultEventsMap ,
817- WorkloadClientSocketData
886+ SupervisorSocketData
818887 > = io . of ( "/workload" ) ;
819888
820889 websocketServer . on ( "disconnect" , ( socket ) => {
821890 this . logger . verbose ( "[WS] disconnect" , socket . id ) ;
822891 } ) ;
823892 websocketServer . use ( async ( socket , next ) => {
824893 const setSocketDataFromHeader = (
825- dataKey : keyof typeof socket . data ,
894+ dataKey : keyof WorkloadClientSocketData ,
826895 headerName : string ,
827896 required : boolean = true
828897 ) => {
@@ -866,6 +935,9 @@ export class WorkloadServer extends EventEmitter<WorkloadServerEvents> {
866935 return ;
867936 }
868937
938+ socket . data . workloadTokenKey = result . key ;
939+ socket . data . workloadTokenClaims = result . claims ;
940+
869941 // Re-source the deployment id from the verified claim; the raw header may be an opaque token.
870942 // A legacy bare id is itself the friendlyId, so it's safe to keep.
871943 socket . data . deploymentFriendlyId =
@@ -1000,6 +1072,17 @@ export class WorkloadServer extends EventEmitter<WorkloadServerEvents> {
10001072 emitSocketLifecycle ( "run_disconnected" , friendlyId , reason ) ;
10011073 } ;
10021074
1075+ const previousKeySocketMayBind = ( friendlyId : string , eventName : "run:start" | "run:stop" ) =>
1076+ socket . data . workloadTokenKey !== "previous" ||
1077+ this . previousKeyRunAllowed ( friendlyId , {
1078+ transport : "ws" ,
1079+ remoteAddress : socket . handshake . address ,
1080+ runnerId : socket . data . runnerId ,
1081+ userAgent : socket . handshake . headers [ "user-agent" ] ,
1082+ route : eventName ,
1083+ claims : socket . data . workloadTokenClaims ,
1084+ } ) ;
1085+
10031086 socketLogger . debug ( "wsServer socket connected" , { ...getSocketMetadata ( ) } ) ;
10041087
10051088 // FIXME: where does this get set?
@@ -1044,7 +1127,11 @@ export class WorkloadServer extends EventEmitter<WorkloadServerEvents> {
10441127 log . debug ( "Handling run:start" ) ;
10451128
10461129 try {
1047- runConnected ( message . run . friendlyId ) ;
1130+ const friendlyId = runFriendlyIdFromMessage ( message ) ;
1131+ if ( ! previousKeySocketMayBind ( friendlyId , "run:start" ) ) {
1132+ return ;
1133+ }
1134+ runConnected ( friendlyId ) ;
10481135 } catch ( error ) {
10491136 log . error ( "run:start error" , { error } ) ;
10501137 }
@@ -1060,7 +1147,11 @@ export class WorkloadServer extends EventEmitter<WorkloadServerEvents> {
10601147 log . debug ( "Handling run:stop" ) ;
10611148
10621149 try {
1063- runDisconnected ( message . run . friendlyId , "run_stop_message" ) ;
1150+ const friendlyId = runFriendlyIdFromMessage ( message ) ;
1151+ if ( ! previousKeySocketMayBind ( friendlyId , "run:stop" ) ) {
1152+ return ;
1153+ }
1154+ runDisconnected ( friendlyId , "run_stop_message" ) ;
10641155 } catch ( error ) {
10651156 log . error ( "run:stop error" , { error } ) ;
10661157 }
0 commit comments