Skip to content

Commit

Permalink
Add method for generating signed Smart CDN URLs (#201)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
Co-authored-by: Mikael Finstad <[email protected]>

* Fix implementation

* Allow duplicate parameters

* Improve checks

* Fix formatting

* Use absolute expiration time

* Test empty parameter

* Update docs

---------

Co-authored-by: Remco Haszing <[email protected]>
Co-authored-by: Mikael Finstad <[email protected]>
  • Loading branch information
3 people authored Nov 28, 2024
1 parent 26e55b3 commit 166bdba
Show file tree
Hide file tree
Showing 3 changed files with 114 additions and 0 deletions.
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,33 @@ Calculates a signature for the given `params` JSON object. If the `params` objec

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).

#### getSignedSmartCDNUrl(params)

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:

- `workspace` - Workspace slug (required)
- `template` - Template slug or template ID (required)
- `input` - Input value that is provided as `${fields.input}` in the template (required)
- `urlParams` - Object with additional parameters for the URL query string (optional)
- `expiresAt` - Expiration timestamp of the signature in milliseconds since UNIX epoch. Defaults to 1 hour from now. (optional)

Example:

```js
const client = new Transloadit({ authKey: 'foo_key', authSecret: 'foo_secret' })
const url = client.getSignedSmartCDNUrl({
workspace: 'foo_workspace',
template: 'foo_template',
input: 'foo_input',
urlParams: {
foo: 'bar',
},
})

// url is:
// https://foo_workspace.tlcdn.com/foo_template/foo_input?auth_key=foo_key&exp=1714525200000&foo=bar&sig=sha256:9548915ec70a5f0d05de9497289e792201ceec19a526fe315f4f4fd2e7e377ac
```

### Errors

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:
Expand Down
65 changes: 65 additions & 0 deletions src/Transloadit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -626,6 +626,47 @@ export class Transloadit {
return { signature, params: jsonParams }
}

/**
* Construct a signed Smart CDN URL. See https://transloadit.com/docs/topics/signature-authentication/#smart-cdn.
*/
getSignedSmartCDNUrl(opts: SmartCDNUrlOptions): string {
if (opts.workspace == null || opts.workspace === '')
throw new TypeError('workspace is required')
if (opts.template == null || opts.template === '') throw new TypeError('template is required')
if (opts.input == null) throw new TypeError('input is required') // `input` can be an empty string.

const workspaceSlug = encodeURIComponent(opts.workspace)
const templateSlug = encodeURIComponent(opts.template)
const inputField = encodeURIComponent(opts.input)
const expiresAt = opts.expiresAt || Date.now() + 60 * 60 * 1000 // 1 hour

const queryParams = new URLSearchParams()
for (const [key, value] of Object.entries(opts.urlParams || {})) {
if (Array.isArray(value)) {
for (const val of value) {
queryParams.append(key, `${val}`)
}
} else {
queryParams.append(key, `${value}`)
}
}

queryParams.set('auth_key', this._authKey)
queryParams.set('exp', `${expiresAt}`)
// The signature changes depending on the order of the query parameters. We therefore sort them on the client-
// and server-side to ensure that we do not get mismatching signatures if a proxy changes the order of query
// parameters or implementations handle query parameters ordering differently.
queryParams.sort()

const stringToSign = `${workspaceSlug}/${templateSlug}/${inputField}?${queryParams}`
const algorithm = 'sha256'
const signature = createHmac(algorithm, this._authSecret).update(stringToSign).digest('hex')

queryParams.set('sig', `sha256:${signature}`)
const signedUrl = `https://${workspaceSlug}.tlcdn.com/${templateSlug}/${inputField}?${queryParams}`
return signedUrl
}

private _calcSignature(toSign: string, algorithm = 'sha384'): string {
return `${algorithm}:${createHmac(algorithm, this._authSecret)
.update(Buffer.from(toSign, 'utf-8'))
Expand Down Expand Up @@ -960,3 +1001,27 @@ export interface PaginationList<T> {
count: number
items: T[]
}

export interface SmartCDNUrlOptions {
/**
* Workspace slug
*/
workspace: string
/**
* Template slug or template ID
*/
template: string
/**
* Input value that is provided as `${fields.input}` in the template
*/
input: string
/**
* Additional parameters for the URL query string
*/
urlParams?: Record<string, boolean | number | string | (boolean | number | string)[]>
/**
* Expiration timestamp of the signature in milliseconds since UNIX epoch.
* Defaults to 1 hour from now.
*/
expiresAt?: number
}
22 changes: 22 additions & 0 deletions test/unit/test-transloadit-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -343,4 +343,26 @@ describe('Transloadit', () => {
)
})
})

describe('getSignedSmartCDNUrl', () => {
it('should return a signed url', () => {
const client = new Transloadit({ authKey: 'foo_key', authSecret: 'foo_secret' })

const url = client.getSignedSmartCDNUrl({
workspace: 'foo_workspace',
template: 'foo_template',
input: 'foo/input',
urlParams: {
foo: 'bar',
aaa: [42, 21], // Should be sorted before `foo`.
empty: '',
},
expiresAt: 1714525200000,
})

expect(url).toBe(
'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'
)
})
})
})

0 comments on commit 166bdba

Please sign in to comment.