@@ -13,11 +13,127 @@ import type {
1313 SolidColorOverlay ,
1414} from './shared' ;
1515import transformationUtils , { safeBtoa } from '../lib/transformation-utils' ;
16+ import { createHmacSha1 } from '../lib/crypto-utils' ;
1617
1718const TRANSFORMATION_PARAMETER = 'tr' ;
19+ const SIGNATURE_PARAMETER = 'ik-s' ;
20+ const TIMESTAMP_PARAMETER = 'ik-t' ;
21+ const DEFAULT_TIMESTAMP = 9999999999 ;
1822const SIMPLE_OVERLAY_PATH_REGEX = new RegExp ( '^[a-zA-Z0-9-._/ ]*$' ) ;
1923const SIMPLE_OVERLAY_TEXT_REGEX = new RegExp ( '^[a-zA-Z0-9-._ ]*$' ) ;
2024
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+
21137function removeTrailingSlash ( str : string ) : string {
22138 if ( typeof str == 'string' && str [ str . length - 1 ] == '/' ) {
23139 str = str . substring ( 0 , str . length - 1 ) ;
@@ -231,7 +347,7 @@ function buildTransformationString(transformation: Transformation[] | undefined)
231347 } else if ( key === 'raw' ) {
232348 parsedTransformStep . push ( currentTransform [ key ] as string ) ;
233349 } else {
234- if ( transformKey === 'di' ) {
350+ if ( transformKey === 'di' || transformKey === 'ff' ) {
235351 value = removeTrailingSlash ( removeLeadingSlash ( ( value as string ) || '' ) ) ;
236352 value = value . replace ( / \/ / g, '@@' ) ;
237353 }
@@ -256,84 +372,46 @@ function buildTransformationString(transformation: Transformation[] | undefined)
256372 return parsedTransforms . join ( transformationUtils . getChainTransformDelimiter ( ) ) ;
257373}
258374
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 ;
263383
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 ;
274386
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+ }
315390
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+ }
326411
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 + '/' ;
338415 }
416+ return str ;
339417}
0 commit comments