@@ -13,11 +13,127 @@ import type {
13
13
SolidColorOverlay ,
14
14
} from './shared' ;
15
15
import transformationUtils , { safeBtoa } from '../lib/transformation-utils' ;
16
+ import { createHmacSha1 } from '../lib/crypto-utils' ;
16
17
17
18
const TRANSFORMATION_PARAMETER = 'tr' ;
19
+ const SIGNATURE_PARAMETER = 'ik-s' ;
20
+ const TIMESTAMP_PARAMETER = 'ik-t' ;
21
+ const DEFAULT_TIMESTAMP = 9999999999 ;
18
22
const SIMPLE_OVERLAY_PATH_REGEX = new RegExp ( '^[a-zA-Z0-9-._/ ]*$' ) ;
19
23
const SIMPLE_OVERLAY_TEXT_REGEX = new RegExp ( '^[a-zA-Z0-9-._ ]*$' ) ;
20
24
25
+ export class Helper extends APIResource {
26
+ constructor ( client : ImageKit ) {
27
+ super ( client ) ;
28
+ }
29
+
30
+ /**
31
+ * Builds a source URL with the given options.
32
+ *
33
+ * @param opts - The options for building the source URL.
34
+ * @returns The constructed source URL.
35
+ */
36
+ buildSrc ( opts : SrcOptions ) : string {
37
+ opts . urlEndpoint = opts . urlEndpoint || '' ;
38
+ opts . src = opts . src || '' ;
39
+ opts . transformationPosition = opts . transformationPosition || 'query' ;
40
+
41
+ if ( ! opts . src ) {
42
+ return '' ;
43
+ }
44
+
45
+ const isAbsoluteURL = opts . src . startsWith ( 'http://' ) || opts . src . startsWith ( 'https://' ) ;
46
+
47
+ var urlObj , isSrcParameterUsedForURL , urlEndpointPattern ;
48
+
49
+ try {
50
+ if ( ! isAbsoluteURL ) {
51
+ urlEndpointPattern = new URL ( opts . urlEndpoint ) . pathname ;
52
+ urlObj = new URL ( pathJoin ( [ opts . urlEndpoint . replace ( urlEndpointPattern , '' ) , opts . src ] ) ) ;
53
+ } else {
54
+ urlObj = new URL ( opts . src ! ) ;
55
+ isSrcParameterUsedForURL = true ;
56
+ }
57
+ } catch ( e ) {
58
+ return '' ;
59
+ }
60
+
61
+ for ( var i in opts . queryParameters ) {
62
+ urlObj . searchParams . append ( i , String ( opts . queryParameters [ i ] ) ) ;
63
+ }
64
+
65
+ var transformationString = this . buildTransformationString ( opts . transformation ) ;
66
+
67
+ if ( transformationString && transformationString . length ) {
68
+ if ( ! transformationUtils . addAsQueryParameter ( opts ) && ! isSrcParameterUsedForURL ) {
69
+ urlObj . pathname = pathJoin ( [
70
+ TRANSFORMATION_PARAMETER + transformationUtils . getChainTransformDelimiter ( ) + transformationString ,
71
+ urlObj . pathname ,
72
+ ] ) ;
73
+ }
74
+ }
75
+
76
+ if ( urlEndpointPattern ) {
77
+ urlObj . pathname = pathJoin ( [ urlEndpointPattern , urlObj . pathname ] ) ;
78
+ } else {
79
+ urlObj . pathname = pathJoin ( [ urlObj . pathname ] ) ;
80
+ }
81
+
82
+ // First, build the complete URL with transformations
83
+ let finalUrl = urlObj . href ;
84
+
85
+ // Add transformation parameter manually to avoid URL encoding
86
+ // URLSearchParams.set() would encode commas and colons in transformation string,
87
+ // It would work correctly but not very readable e.g., "w-300,h-400" is better than "w-300%2Ch-400"
88
+ if ( transformationString && transformationString . length ) {
89
+ if ( transformationUtils . addAsQueryParameter ( opts ) || isSrcParameterUsedForURL ) {
90
+ const separator = urlObj . searchParams . toString ( ) ? '&' : '?' ;
91
+ finalUrl = `${ finalUrl } ${ separator } ${ TRANSFORMATION_PARAMETER } =${ transformationString } ` ;
92
+ }
93
+ }
94
+
95
+ // Then sign the URL if needed
96
+ if ( opts . signed === true || ( opts . expiresIn && opts . expiresIn > 0 ) ) {
97
+ const expiryTimestamp = getSignatureTimestamp ( opts . expiresIn ) ;
98
+
99
+ const urlSignature = getSignature ( {
100
+ privateKey : this . _client . privateAPIKey ,
101
+ url : finalUrl ,
102
+ urlEndpoint : opts . urlEndpoint ,
103
+ expiryTimestamp,
104
+ } ) ;
105
+
106
+ // Add signature parameters to the final URL
107
+ // Use URL object to properly determine if we need ? or & separator
108
+ const finalUrlObj = new URL ( finalUrl ) ;
109
+ const hasExistingParams = finalUrlObj . searchParams . toString ( ) . length > 0 ;
110
+ const separator = hasExistingParams ? '&' : '?' ;
111
+ let signedUrl = finalUrl ;
112
+
113
+ if ( expiryTimestamp && expiryTimestamp !== DEFAULT_TIMESTAMP ) {
114
+ signedUrl += `${ separator } ${ TIMESTAMP_PARAMETER } =${ expiryTimestamp } ` ;
115
+ signedUrl += `&${ SIGNATURE_PARAMETER } =${ urlSignature } ` ;
116
+ } else {
117
+ signedUrl += `${ separator } ${ SIGNATURE_PARAMETER } =${ urlSignature } ` ;
118
+ }
119
+
120
+ return signedUrl ;
121
+ }
122
+
123
+ return finalUrl ;
124
+ }
125
+
126
+ /**
127
+ * Builds a transformation string from the given transformations.
128
+ *
129
+ * @param transformation - The transformations to apply.
130
+ * @returns The constructed transformation string.
131
+ */
132
+ buildTransformationString ( transformation : Transformation [ ] | undefined ) : string {
133
+ return buildTransformationString ( transformation ) ;
134
+ }
135
+ }
136
+
21
137
function removeTrailingSlash ( str : string ) : string {
22
138
if ( typeof str == 'string' && str [ str . length - 1 ] == '/' ) {
23
139
str = str . substring ( 0 , str . length - 1 ) ;
@@ -231,7 +347,7 @@ function buildTransformationString(transformation: Transformation[] | undefined)
231
347
} else if ( key === 'raw' ) {
232
348
parsedTransformStep . push ( currentTransform [ key ] as string ) ;
233
349
} else {
234
- if ( transformKey === 'di' ) {
350
+ if ( transformKey === 'di' || transformKey === 'ff' ) {
235
351
value = removeTrailingSlash ( removeLeadingSlash ( ( value as string ) || '' ) ) ;
236
352
value = value . replace ( / \/ / g, '@@' ) ;
237
353
}
@@ -256,84 +372,46 @@ function buildTransformationString(transformation: Transformation[] | undefined)
256
372
return parsedTransforms . join ( transformationUtils . getChainTransformDelimiter ( ) ) ;
257
373
}
258
374
259
- export class Helper extends APIResource {
260
- constructor ( client : ImageKit ) {
261
- super ( client ) ;
262
- }
375
+ /**
376
+ * Calculates the expiry timestamp for URL signing
377
+ *
378
+ * @param seconds - Number of seconds from now when the URL should expire
379
+ * @returns Unix timestamp for expiry, or DEFAULT_TIMESTAMP if invalid/not provided
380
+ */
381
+ function getSignatureTimestamp ( seconds : number | undefined ) : number {
382
+ if ( ! seconds || seconds <= 0 ) return DEFAULT_TIMESTAMP ;
263
383
264
- /**
265
- * Builds a source URL with the given options.
266
- *
267
- * @param opts - The options for building the source URL.
268
- * @returns The constructed source URL.
269
- */
270
- buildSrc ( opts : SrcOptions ) : string {
271
- opts . urlEndpoint = opts . urlEndpoint || '' ;
272
- opts . src = opts . src || '' ;
273
- opts . transformationPosition = opts . transformationPosition || 'query' ;
384
+ const sec = parseInt ( String ( seconds ) , 10 ) ;
385
+ if ( ! sec || isNaN ( sec ) ) return DEFAULT_TIMESTAMP ;
274
386
275
- if ( ! opts . src ) {
276
- return '' ;
277
- }
278
-
279
- const isAbsoluteURL = opts . src . startsWith ( 'http://' ) || opts . src . startsWith ( 'https://' ) ;
280
-
281
- var urlObj , isSrcParameterUsedForURL , urlEndpointPattern ;
282
-
283
- try {
284
- if ( ! isAbsoluteURL ) {
285
- urlEndpointPattern = new URL ( opts . urlEndpoint ) . pathname ;
286
- urlObj = new URL ( pathJoin ( [ opts . urlEndpoint . replace ( urlEndpointPattern , '' ) , opts . src ] ) ) ;
287
- } else {
288
- urlObj = new URL ( opts . src ! ) ;
289
- isSrcParameterUsedForURL = true ;
290
- }
291
- } catch ( e ) {
292
- return '' ;
293
- }
294
-
295
- for ( var i in opts . queryParameters ) {
296
- urlObj . searchParams . append ( i , String ( opts . queryParameters [ i ] ) ) ;
297
- }
298
-
299
- var transformationString = this . buildTransformationString ( opts . transformation ) ;
300
-
301
- if ( transformationString && transformationString . length ) {
302
- if ( ! transformationUtils . addAsQueryParameter ( opts ) && ! isSrcParameterUsedForURL ) {
303
- urlObj . pathname = pathJoin ( [
304
- TRANSFORMATION_PARAMETER + transformationUtils . getChainTransformDelimiter ( ) + transformationString ,
305
- urlObj . pathname ,
306
- ] ) ;
307
- }
308
- }
309
-
310
- if ( urlEndpointPattern ) {
311
- urlObj . pathname = pathJoin ( [ urlEndpointPattern , urlObj . pathname ] ) ;
312
- } else {
313
- urlObj . pathname = pathJoin ( [ urlObj . pathname ] ) ;
314
- }
387
+ const currentTimestamp = Math . floor ( new Date ( ) . getTime ( ) / 1000 ) ;
388
+ return currentTimestamp + sec ;
389
+ }
315
390
316
- if ( transformationString && transformationString . length ) {
317
- if ( transformationUtils . addAsQueryParameter ( opts ) || isSrcParameterUsedForURL ) {
318
- if ( urlObj . searchParams . toString ( ) !== '' ) {
319
- // In 12 node.js .size was not there. So, we need to check if it is an object or not.
320
- return `${ urlObj . href } &${ TRANSFORMATION_PARAMETER } =${ transformationString } ` ;
321
- } else {
322
- return `${ urlObj . href } ?${ TRANSFORMATION_PARAMETER } =${ transformationString } ` ;
323
- }
324
- }
325
- }
391
+ /**
392
+ * Generates an HMAC-SHA1 signature for URL signing
393
+ *
394
+ * @param opts - Options containing private key, URL, endpoint, and expiry timestamp
395
+ * @returns Hex-encoded signature, or empty string if required params missing
396
+ */
397
+ function getSignature ( opts : {
398
+ privateKey : string ;
399
+ url : string ;
400
+ urlEndpoint : string ;
401
+ expiryTimestamp : number ;
402
+ } ) : string {
403
+ if ( ! opts . privateKey || ! opts . url || ! opts . urlEndpoint ) return '' ;
404
+
405
+ // Create the string to sign: relative path + expiry timestamp
406
+ const stringToSign =
407
+ opts . url . replace ( addTrailingSlash ( opts . urlEndpoint ) , '' ) + String ( opts . expiryTimestamp ) ;
408
+
409
+ return createHmacSha1 ( opts . privateKey , stringToSign ) ;
410
+ }
326
411
327
- return urlObj . href ;
328
- }
329
-
330
- /**
331
- * Builds a transformation string from the given transformations.
332
- *
333
- * @param transformation - The transformations to apply.
334
- * @returns The constructed transformation string.
335
- */
336
- buildTransformationString ( transformation : Transformation [ ] | undefined ) : string {
337
- return buildTransformationString ( transformation ) ;
412
+ function addTrailingSlash ( str : string ) : string {
413
+ if ( typeof str === 'string' && str [ str . length - 1 ] !== '/' ) {
414
+ str = str + '/' ;
338
415
}
416
+ return str ;
339
417
}
0 commit comments