@@ -91,7 +91,7 @@ function normalizeExpirationMode(mode) {
9191 if ( mode == null || mode === '' ) {
9292 return undefined ;
9393 }
94- if ( mode === 'duration' || mode === 'datetime' ) {
94+ if ( mode === 'duration' || mode === 'datetime' || mode === 'count' ) {
9595 return mode ;
9696 }
9797 throw new RequestInvalidError (
@@ -171,6 +171,38 @@ function resolveDurationExpiration(options = {}, { required = false } = {}) {
171171 } ;
172172}
173173
174+ function resolveCountExpiration ( options = { } ) {
175+ const rawCount = options ?. count ;
176+ if ( rawCount == null || rawCount === '' ) {
177+ throw new RequestInvalidError (
178+ 'INVALID_SHARE_COUNT' ,
179+ `Invalid count option: ${ rawCount } ` ,
180+ ) ;
181+ }
182+
183+ const count = Number ( rawCount ) ;
184+ if ( ! Number . isSafeInteger ( count ) || count <= 0 ) {
185+ throw new RequestInvalidError (
186+ 'INVALID_SHARE_COUNT' ,
187+ `Invalid count option: ${ rawCount } ` ,
188+ ) ;
189+ }
190+
191+ const rawUsedCount = options ?. usedCount ?? 0 ;
192+ const usedCount = Number ( rawUsedCount ) ;
193+ if ( ! Number . isSafeInteger ( usedCount ) || usedCount < 0 || usedCount > count ) {
194+ throw new RequestInvalidError (
195+ 'INVALID_SHARE_USED_COUNT' ,
196+ `Invalid usedCount option: ${ rawUsedCount } ` ,
197+ ) ;
198+ }
199+
200+ return {
201+ count,
202+ usedCount,
203+ } ;
204+ }
205+
174206function createTokenItem ( payload , options = { } ) {
175207 const type = payload ?. type ;
176208 const name = payload ?. name ;
@@ -243,8 +275,11 @@ function createTokenItem(payload, options = {}) {
243275 inferLegacyExpirationMode ( options ) ;
244276 let durationExpiration = null ;
245277 let exp ;
278+ let countExpiration = null ;
246279 if ( expirationMode === 'datetime' ) {
247280 exp = resolveExactExpiration ( options ) ;
281+ } else if ( expirationMode === 'count' ) {
282+ countExpiration = resolveCountExpiration ( options ) ;
248283 } else {
249284 durationExpiration = resolveDurationExpiration ( options , {
250285 required : expirationMode === 'duration' ,
@@ -269,20 +304,29 @@ function createTokenItem(payload, options = {}) {
269304 const normalizedMode =
270305 expirationMode === 'datetime'
271306 ? 'datetime'
307+ : expirationMode === 'count'
308+ ? 'count'
272309 : durationExpiration
273310 ? 'duration'
274311 : undefined ;
275312 const safePayload = { ...payload } ;
276313 delete safePayload . mode ;
277314 delete safePayload . exp ;
278315 delete safePayload . expiresIn ;
316+ delete safePayload . count ;
317+ delete safePayload . usedCount ;
279318 const tokenData = {
280319 ...safePayload ,
281320 token,
282321 createdAt : Date . now ( ) ,
283322 ...( normalizedMode ? { mode : normalizedMode } : { } ) ,
284323 ...( normalizedMode === 'datetime'
285324 ? { exp }
325+ : normalizedMode === 'count'
326+ ? {
327+ count : countExpiration . count ,
328+ usedCount : countExpiration . usedCount ,
329+ }
286330 : durationExpiration
287331 ? {
288332 expiresIn : durationExpiration . rawExpiresIn ,
@@ -314,6 +358,61 @@ function deleteTokenItem(token, type, name) {
314358 return match ;
315359}
316360
361+ function matchesShareToken ( item , { token, type, name, pathname } ) {
362+ return (
363+ item . token === token &&
364+ ( type ? item . type === type : true ) &&
365+ ( name ? item . name === name : true ) &&
366+ ( pathname
367+ ? `/share/${ item . type } /${ item . name } ` === pathname ||
368+ pathname . startsWith ( `/share/${ item . type } /${ item . name } /` )
369+ : true )
370+ ) ;
371+ }
372+
373+ function isShareTokenUsable ( item ) {
374+ if ( item . exp != null && item . exp <= Date . now ( ) ) {
375+ return false ;
376+ }
377+
378+ if ( item . mode === 'count' ) {
379+ const count = Number ( item . count ) ;
380+ const usedCount = item . usedCount == null ? 0 : Number ( item . usedCount ) ;
381+ return (
382+ Number . isSafeInteger ( count ) &&
383+ count > 0 &&
384+ Number . isSafeInteger ( usedCount ) &&
385+ usedCount >= 0 &&
386+ usedCount < count
387+ ) ;
388+ }
389+
390+ return true ;
391+ }
392+
393+ function consumeShareToken ( query ) {
394+ const allTokens = $ . read ( TOKENS_KEY ) || [ ] ;
395+ const tokenIndex = allTokens . findIndex (
396+ ( item ) => matchesShareToken ( item , query ) && isShareTokenUsable ( item ) ,
397+ ) ;
398+ if ( tokenIndex < 0 ) {
399+ return null ;
400+ }
401+
402+ const token = allTokens [ tokenIndex ] ;
403+ if ( token . mode !== 'count' ) {
404+ return token ;
405+ }
406+
407+ const nextToken = {
408+ ...token ,
409+ usedCount : Number ( token . usedCount ?? 0 ) + 1 ,
410+ } ;
411+ allTokens [ tokenIndex ] = nextToken ;
412+ $ . write ( allTokens , TOKENS_KEY ) ;
413+ return nextToken ;
414+ }
415+
317416function shouldArchiveDeletion ( mode ) {
318417 if ( mode == null || mode === '' || mode === 'permanent' ) {
319418 return false ;
@@ -327,4 +426,4 @@ function shouldArchiveDeletion(mode) {
327426 ) ;
328427}
329428
330- export { createTokenItem , deleteTokenItem } ;
429+ export { createTokenItem , deleteTokenItem , consumeShareToken } ;
0 commit comments