Skip to content

Commit 166bdba

Browse files
Acconutremcohaszingmifi
authored
Add method for generating signed Smart CDN URLs (#201)
* Add method for generating signed Smart CDN URLs * Do not URI encode workspace * Revert "Do not URI encode workspace" This reverts commit f645872. * Apply suggestions from code review Co-authored-by: Remco Haszing <remcohaszing@gmail.com> Co-authored-by: Mikael Finstad <finstaden@gmail.com> * Fix implementation * Allow duplicate parameters * Improve checks * Fix formatting * Use absolute expiration time * Test empty parameter * Update docs --------- Co-authored-by: Remco Haszing <remcohaszing@gmail.com> Co-authored-by: Mikael Finstad <finstaden@gmail.com>
1 parent 26e55b3 commit 166bdba

File tree

3 files changed

+114
-0
lines changed

3 files changed

+114
-0
lines changed

README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,33 @@ Calculates a signature for the given `params` JSON object. If the `params` objec
389389

390390
This function returns an object with the key `signature` (containing the calculated signature string) and a key `params`, which contains the stringified version of the passed `params` object (including the set expires and authKey keys).
391391

392+
#### getSignedSmartCDNUrl(params)
393+
394+
Constructs a signed Smart CDN URL, as defined in the [API documentation](https://transloadit.com/docs/topics/signature-authentication/#smart-cdn). `params` must be an object with the following properties:
395+
396+
- `workspace` - Workspace slug (required)
397+
- `template` - Template slug or template ID (required)
398+
- `input` - Input value that is provided as `${fields.input}` in the template (required)
399+
- `urlParams` - Object with additional parameters for the URL query string (optional)
400+
- `expiresAt` - Expiration timestamp of the signature in milliseconds since UNIX epoch. Defaults to 1 hour from now. (optional)
401+
402+
Example:
403+
404+
```js
405+
const client = new Transloadit({ authKey: 'foo_key', authSecret: 'foo_secret' })
406+
const url = client.getSignedSmartCDNUrl({
407+
workspace: 'foo_workspace',
408+
template: 'foo_template',
409+
input: 'foo_input',
410+
urlParams: {
411+
foo: 'bar',
412+
},
413+
})
414+
415+
// url is:
416+
// https://foo_workspace.tlcdn.com/foo_template/foo_input?auth_key=foo_key&exp=1714525200000&foo=bar&sig=sha256:9548915ec70a5f0d05de9497289e792201ceec19a526fe315f4f4fd2e7e377ac
417+
```
418+
392419
### Errors
393420

394421
Errors from Node.js will be passed on and we use [GOT](https://github.com/sindresorhus/got) for HTTP requests and errors from there will also be passed on. When the HTTP response code is not 200, the error will be an `HTTPError`, which is a [got.HTTPError](https://github.com/sindresorhus/got#errors)) with some additional properties:

src/Transloadit.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -626,6 +626,47 @@ export class Transloadit {
626626
return { signature, params: jsonParams }
627627
}
628628

629+
/**
630+
* Construct a signed Smart CDN URL. See https://transloadit.com/docs/topics/signature-authentication/#smart-cdn.
631+
*/
632+
getSignedSmartCDNUrl(opts: SmartCDNUrlOptions): string {
633+
if (opts.workspace == null || opts.workspace === '')
634+
throw new TypeError('workspace is required')
635+
if (opts.template == null || opts.template === '') throw new TypeError('template is required')
636+
if (opts.input == null) throw new TypeError('input is required') // `input` can be an empty string.
637+
638+
const workspaceSlug = encodeURIComponent(opts.workspace)
639+
const templateSlug = encodeURIComponent(opts.template)
640+
const inputField = encodeURIComponent(opts.input)
641+
const expiresAt = opts.expiresAt || Date.now() + 60 * 60 * 1000 // 1 hour
642+
643+
const queryParams = new URLSearchParams()
644+
for (const [key, value] of Object.entries(opts.urlParams || {})) {
645+
if (Array.isArray(value)) {
646+
for (const val of value) {
647+
queryParams.append(key, `${val}`)
648+
}
649+
} else {
650+
queryParams.append(key, `${value}`)
651+
}
652+
}
653+
654+
queryParams.set('auth_key', this._authKey)
655+
queryParams.set('exp', `${expiresAt}`)
656+
// The signature changes depending on the order of the query parameters. We therefore sort them on the client-
657+
// and server-side to ensure that we do not get mismatching signatures if a proxy changes the order of query
658+
// parameters or implementations handle query parameters ordering differently.
659+
queryParams.sort()
660+
661+
const stringToSign = `${workspaceSlug}/${templateSlug}/${inputField}?${queryParams}`
662+
const algorithm = 'sha256'
663+
const signature = createHmac(algorithm, this._authSecret).update(stringToSign).digest('hex')
664+
665+
queryParams.set('sig', `sha256:${signature}`)
666+
const signedUrl = `https://${workspaceSlug}.tlcdn.com/${templateSlug}/${inputField}?${queryParams}`
667+
return signedUrl
668+
}
669+
629670
private _calcSignature(toSign: string, algorithm = 'sha384'): string {
630671
return `${algorithm}:${createHmac(algorithm, this._authSecret)
631672
.update(Buffer.from(toSign, 'utf-8'))
@@ -960,3 +1001,27 @@ export interface PaginationList<T> {
9601001
count: number
9611002
items: T[]
9621003
}
1004+
1005+
export interface SmartCDNUrlOptions {
1006+
/**
1007+
* Workspace slug
1008+
*/
1009+
workspace: string
1010+
/**
1011+
* Template slug or template ID
1012+
*/
1013+
template: string
1014+
/**
1015+
* Input value that is provided as `${fields.input}` in the template
1016+
*/
1017+
input: string
1018+
/**
1019+
* Additional parameters for the URL query string
1020+
*/
1021+
urlParams?: Record<string, boolean | number | string | (boolean | number | string)[]>
1022+
/**
1023+
* Expiration timestamp of the signature in milliseconds since UNIX epoch.
1024+
* Defaults to 1 hour from now.
1025+
*/
1026+
expiresAt?: number
1027+
}

test/unit/test-transloadit-client.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,4 +343,26 @@ describe('Transloadit', () => {
343343
)
344344
})
345345
})
346+
347+
describe('getSignedSmartCDNUrl', () => {
348+
it('should return a signed url', () => {
349+
const client = new Transloadit({ authKey: 'foo_key', authSecret: 'foo_secret' })
350+
351+
const url = client.getSignedSmartCDNUrl({
352+
workspace: 'foo_workspace',
353+
template: 'foo_template',
354+
input: 'foo/input',
355+
urlParams: {
356+
foo: 'bar',
357+
aaa: [42, 21], // Should be sorted before `foo`.
358+
empty: '',
359+
},
360+
expiresAt: 1714525200000,
361+
})
362+
363+
expect(url).toBe(
364+
'https://foo_workspace.tlcdn.com/foo_template/foo%2Finput?aaa=42&aaa=21&auth_key=foo_key&empty=&exp=1714525200000&foo=bar&sig=sha256%3A1ab71ef553df3507a9e2cf7beb8f921538bbef49a13a94a22ff49f2f030a5e9e'
365+
)
366+
})
367+
})
346368
})

0 commit comments

Comments
 (0)