@@ -2,10 +2,10 @@ import type { LoaderFunctionArgs } from "@remix-run/server-runtime";
22import { json } from "@remix-run/server-runtime" ;
33import { generateJWT as internal_generateJWT } from "@trigger.dev/core/v3" ;
44import { resolveJwtSigningKey , scopesWithinAbility } from "@trigger.dev/rbac" ;
5- import parseDuration from "parse-duration" ;
65import { z } from "zod" ;
76import { authenticateBearerWithTelemetry } from "~/services/authTelemetry.server" ;
87import { isApiKeyInGraceWindow , presentedBearerToken } from "~/services/apiKeyGraceWindow.server" ;
8+ import { validateJwtExpiration } from "~/services/jwtExpiration.server" ;
99import { logger } from "~/services/logger.server" ;
1010import { publicTokenSubject } from "~/services/publicTokenSubject.server" ;
1111
@@ -18,35 +18,6 @@ const RequestBodySchema = z.object({
1818 expirationTime : z . union ( [ z . number ( ) , z . string ( ) ] ) . optional ( ) ,
1919} ) ;
2020
21- const DEFAULT_EXPIRY = "1h" ;
22- // A minted public JWT may be valid for at most 24 hours. A caller-controlled,
23- // uncapped `expirationTime` ("100y") would otherwise mint a token that no key
24- // rotation can revoke — public JWTs are not invalidated by rotating the
25- // environment's key. The cap is the compensating control for that.
26- const MAX_EXPIRY_SECONDS = 24 * 60 * 60 ;
27-
28- // A requested `expirationTime` above this (epoch seconds, ~2001) is an absolute
29- // timestamp; a smaller number is a relative offset in seconds.
30- const EXPIRY_EPOCH_THRESHOLD_SECONDS = 1_000_000_000 ;
31-
32- // Resolve the requested expiry to an absolute epoch-second timestamp so it can
33- // be capped. Returns undefined when a string can't be parsed as a duration.
34- function resolveRequestedExpirySeconds (
35- expirationTime : number | string | undefined ,
36- nowSec : number
37- ) : number | undefined {
38- if ( typeof expirationTime === "number" ) {
39- return expirationTime > EXPIRY_EPOCH_THRESHOLD_SECONDS
40- ? expirationTime
41- : nowSec + expirationTime ;
42- }
43- const durationMs = parseDuration ( expirationTime ?? DEFAULT_EXPIRY ) ;
44- if ( durationMs == null ) {
45- return undefined ;
46- }
47- return nowSec + Math . floor ( durationMs / 1000 ) ;
48- }
49-
5021export async function action ( { request } : LoaderFunctionArgs ) {
5122 try {
5223 // Authenticate through the RBAC bearer controller (not the legacy
@@ -108,15 +79,9 @@ export async function action({ request }: LoaderFunctionArgs) {
10879 }
10980
11081 const nowSec = Math . floor ( Date . now ( ) / 1000 ) ;
111- const requestedAbsSec = resolveRequestedExpirySeconds ( parsedBody . data . expirationTime , nowSec ) ;
112- if ( requestedAbsSec === undefined ) {
113- return json ( { error : "Invalid expiration time" } , { status : 400 } ) ;
114- }
115- if ( requestedAbsSec <= nowSec ) {
116- return json ( { error : "Expiration time must be in the future" } , { status : 400 } ) ;
117- }
118- if ( requestedAbsSec - nowSec > MAX_EXPIRY_SECONDS ) {
119- return json ( { error : "Expiration time cannot exceed 24 hours" } , { status : 400 } ) ;
82+ const expiry = validateJwtExpiration ( parsedBody . data . expirationTime , nowSec ) ;
83+ if ( expiry . error !== undefined ) {
84+ return json ( { error : expiry . error } , { status : 400 } ) ;
12085 }
12186
12287 const claims = {
@@ -132,7 +97,7 @@ export async function action({ request }: LoaderFunctionArgs) {
13297 const jwt = await internal_generateJWT ( {
13398 secretKey : resolveJwtSigningKey ( authenticationResult . environment ) ,
13499 payload : claims ,
135- expirationTime : requestedAbsSec ,
100+ expirationTime : expiry . expirationTime ,
136101 } ) ;
137102
138103 return json ( { token : jwt } ) ;
0 commit comments