@@ -87,8 +87,91 @@ async function signToken(req, res) {
8787 }
8888}
8989
90- function createTokenItem ( payload , options = { } ) {
90+ function normalizeExpirationMode ( mode ) {
91+ if ( mode == null || mode === '' ) {
92+ return undefined ;
93+ }
94+ if ( mode === 'duration' || mode === 'datetime' ) {
95+ return mode ;
96+ }
97+ throw new RequestInvalidError (
98+ 'INVALID_EXPIRATION_MODE' ,
99+ `Unsupported expiration mode: ${ mode } ` ,
100+ ) ;
101+ }
102+
103+ function inferLegacyExpirationMode ( options = { } ) {
104+ if ( options ?. expiresIn != null && options . expiresIn !== '' ) {
105+ return 'duration' ;
106+ }
107+ if ( options ?. exp != null && options . exp !== '' ) {
108+ return 'datetime' ;
109+ }
110+ return undefined ;
111+ }
112+
113+ function resolveExactExpiration ( options = { } ) {
114+ const rawExp = options ?. exp ;
115+ if ( typeof rawExp !== 'number' && typeof rawExp !== 'string' ) {
116+ throw new RequestInvalidError (
117+ 'INVALID_EXPIRATION_DATETIME' ,
118+ `Invalid exp option: ${ rawExp } ` ,
119+ ) ;
120+ }
121+
122+ const normalizedRawExp =
123+ typeof rawExp === 'string' ? rawExp . trim ( ) : rawExp ;
124+ if ( normalizedRawExp === '' ) {
125+ throw new RequestInvalidError (
126+ 'INVALID_EXPIRATION_DATETIME' ,
127+ `Invalid exp option: ${ rawExp } ` ,
128+ ) ;
129+ }
130+
131+ const exp = Number ( normalizedRawExp ) ;
132+ if (
133+ ! Number . isSafeInteger ( exp ) ||
134+ exp <= 0 ||
135+ // Require an explicit millisecond Unix timestamp to avoid
136+ // silently accepting second-based values from non-frontend callers.
137+ exp < 1000000000000
138+ ) {
139+ throw new RequestInvalidError (
140+ 'INVALID_EXPIRATION_DATETIME' ,
141+ `Invalid exp option: ${ rawExp } ` ,
142+ ) ;
143+ }
144+ return exp ;
145+ }
146+
147+ function resolveDurationExpiration ( options = { } , { required = false } = { } ) {
148+ const rawExpiresIn = options ?. expiresIn ;
149+ if ( rawExpiresIn == null || rawExpiresIn === '' ) {
150+ if ( required ) {
151+ throw new RequestInvalidError (
152+ 'INVALID_EXPIRES_IN' ,
153+ `Invalid expiresIn option: ${ rawExpiresIn } ` ,
154+ ) ;
155+ }
156+ return null ;
157+ }
158+
91159 const ms = eval ( `require("ms")` ) ;
160+ const expiresIn = ms ( rawExpiresIn ) ;
161+ if ( expiresIn == null || isNaN ( expiresIn ) || expiresIn <= 0 ) {
162+ throw new RequestInvalidError (
163+ 'INVALID_EXPIRES_IN' ,
164+ `Invalid expiresIn option: ${ rawExpiresIn } ` ,
165+ ) ;
166+ }
167+
168+ return {
169+ rawExpiresIn,
170+ exp : Date . now ( ) + expiresIn ,
171+ } ;
172+ }
173+
174+ function createTokenItem ( payload , options = { } ) {
92175 const type = payload ?. type ;
93176 const name = payload ?. name ;
94177 if ( ! type || ! name ) {
@@ -155,15 +238,18 @@ function createTokenItem(payload, options = {}) {
155238 ) ;
156239 }
157240
158- let expiresIn = options ?. expiresIn ;
159- if ( options ?. expiresIn != null ) {
160- expiresIn = ms ( options . expiresIn ) ;
161- if ( expiresIn == null || isNaN ( expiresIn ) || expiresIn <= 0 ) {
162- throw new RequestInvalidError (
163- 'INVALID_EXPIRES_IN' ,
164- `Invalid expiresIn option: ${ options . expiresIn } ` ,
165- ) ;
166- }
241+ const expirationMode =
242+ normalizeExpirationMode ( options ?. mode ) ??
243+ inferLegacyExpirationMode ( options ) ;
244+ let durationExpiration = null ;
245+ let exp ;
246+ if ( expirationMode === 'datetime' ) {
247+ exp = resolveExactExpiration ( options ) ;
248+ } else {
249+ durationExpiration = resolveDurationExpiration ( options , {
250+ required : expirationMode === 'duration' ,
251+ } ) ;
252+ exp = durationExpiration ?. exp ;
167253 }
168254
169255 const nanoid = eval ( `require("nanoid")` ) ;
@@ -180,12 +266,29 @@ function createTokenItem(payload, options = {}) {
180266 )
181267 ) ;
182268 }
269+ const normalizedMode =
270+ expirationMode === 'datetime'
271+ ? 'datetime'
272+ : durationExpiration
273+ ? 'duration'
274+ : undefined ;
275+ const safePayload = { ...payload } ;
276+ delete safePayload . mode ;
277+ delete safePayload . exp ;
278+ delete safePayload . expiresIn ;
183279 const tokenData = {
184- ...payload ,
280+ ...safePayload ,
185281 token,
186282 createdAt : Date . now ( ) ,
187- expiresIn : expiresIn > 0 ? options ?. expiresIn : undefined ,
188- exp : expiresIn > 0 ? Date . now ( ) + expiresIn : undefined ,
283+ ...( normalizedMode ? { mode : normalizedMode } : { } ) ,
284+ ...( normalizedMode === 'datetime'
285+ ? { exp }
286+ : durationExpiration
287+ ? {
288+ expiresIn : durationExpiration . rawExpiresIn ,
289+ exp,
290+ }
291+ : { } ) ,
189292 } ;
190293 insertByPosition ( tokens , tokenData , getCreateItemPosition ( ) ) ;
191294 $ . write ( tokens , TOKENS_KEY ) ;
0 commit comments