Skip to content

Commit 7597b78

Browse files
kvzclaude
andauthored
utils: Smart CDN URL grammar — parse, unsigned build, strip auth, trusted baseUrl (#479)
getSignedSmartCdnUrl already lived here; the rest of the grammar did not, so the Console carried four copies (parser, unsigned builder, auth stripper, dev-origin rewrite) and Uppy a fifth. Add getSmartCdnUrl, parseSmartCdnUrl and stripSmartCdnAuth on the shared prepare/finish core, plus a trusted baseUrl option on both builders (documented: the host is not signed, so it must come from configuration). Round-trip and edge-case vectors; the 4.6.0 known answer is unchanged. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 11b86fc commit 7597b78

6 files changed

Lines changed: 593 additions & 17 deletions

File tree

.changeset/smart-cdn-grammar.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
"@transloadit/utils": minor
3+
---
4+
5+
Add the rest of the Smart CDN URL grammar next to `getSignedSmartCdnUrl`, so applications stop
6+
carrying their own copies: `getSmartCdnUrl` (unsigned builder), `parseSmartCdnUrl` (the inverse of
7+
the builders — decodes once, keeps repeated query parameters, returns `auth_key`/`exp`/`sig` as
8+
`auth`) and `stripSmartCdnAuth` (removes the signature parameters byte-for-byte otherwise). Both
9+
builders accept a trusted `baseUrl` option (for example a local api2's URL Transform endpoint with
10+
a `{workspace}` placeholder); the signature does not cover the host, so it must come from
11+
configuration, never from user input. Exported from the root and the `./node` entry.

packages/utils/README.md

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,14 @@ Everything in the root export runs on WebCrypto, so it works in browsers (secure
1414
`https://` or `localhost`), edge runtimes, and Node.
1515

1616
```ts
17-
import { getSignedSmartCdnUrl, signParams, verifyWebhookSignature } from '@transloadit/utils'
17+
import {
18+
getSignedSmartCdnUrl,
19+
getSmartCdnUrl,
20+
parseSmartCdnUrl,
21+
signParams,
22+
stripSmartCdnAuth,
23+
verifyWebhookSignature,
24+
} from '@transloadit/utils'
1825

1926
const signature = await signParams(paramsString, authSecret)
2027
const verified = await verifyWebhookSignature({
@@ -31,6 +38,29 @@ const url = await getSignedSmartCdnUrl({
3138
})
3239
```
3340

41+
### Smart CDN URL grammar
42+
43+
The URL builders and parser share one grammar, so a URL built here parses back into the options
44+
that built it (and vice versa):
45+
46+
```ts
47+
// Unsigned, for workspaces that do not require signature authentication.
48+
const publicUrl = getSmartCdnUrl({ workspace, template, input, urlParams: { w: 640 } })
49+
50+
// Inverse of the builders: percent-decodes once, keeps repeated params as arrays,
51+
// and returns `auth_key`/`exp`/`sig` separately as `auth`.
52+
const { workspace, template, input, urlParams, auth } = parseSmartCdnUrl(url)
53+
54+
// Drops `auth_key`, `exp`, `sig` (and api2's `hsh`), leaving every other byte untouched.
55+
const unsigned = stripSmartCdnAuth(url)
56+
```
57+
58+
Both builders accept a `baseUrl` that replaces `https://{workspace}.tlcdn.com`, for example a local
59+
api2's URL Transform endpoint `https://api2-devdock.transloadit.dev/file/{workspace}` (a literal
60+
`{workspace}` is substituted). The signature does not cover the host, so treat `baseUrl` as trusted
61+
configuration and never derive it from user input. Pass the same `baseUrl` to `parseSmartCdnUrl` to
62+
parse URLs built with it.
63+
3464
## Node usage
3565

3666
```ts
@@ -70,6 +100,11 @@ for (const source of imageCandidates.sources) {
70100
- `verifyWebhookSignature({ rawBody, signatureHeader, authSecret })`: validates webhook signatures.
71101
- `getSignedSmartCdnUrl(options)`: async, WebCrypto-based Smart CDN URL signer. Byte-identical to
72102
the Node variant below.
103+
- `getSmartCdnUrl(options)`: unsigned Smart CDN URL builder (same options minus credentials/expiry).
104+
- `parseSmartCdnUrl(url, { baseUrl?, workspace? })`: parses a Smart CDN URL into
105+
`{ workspace, template, input, urlParams, auth?, baseUrl? }`; throws on anything else.
106+
- `stripSmartCdnAuth(url)`: removes the signature parameters, byte-for-byte otherwise.
107+
- `baseUrl` (option of both builders): trusted replacement for `https://{workspace}.tlcdn.com`.
73108
- `signParamsSync(paramsString, authSecret, algorithm?)`: Node-only sync signature helper.
74109
- `getSignedSmartCdnUrl(options)` from `@transloadit/utils/node`: synchronous Smart CDN URL signer.
75110
- `getSignedSmartCdnImageCandidates(options)`: deterministic structured, signed AVIF and WebP

packages/utils/src/index.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,16 @@ import { finishSmartCdnUrl, prepareSmartCdnUrl } from './smartCdn.ts'
44

55
export type SignatureAlgorithm = 'sha1' | 'sha256' | 'sha384' | 'sha512'
66

7-
export type { SmartCdnUrlOptions } from './smartCdn.ts'
7+
export type {
8+
ParsedSmartCdnUrl,
9+
ParseSmartCdnUrlOptions,
10+
SmartCdnUnsignedUrlOptions,
11+
SmartCdnUrlOptions,
12+
SmartCdnUrlParams,
13+
} from './smartCdn.ts'
814

915
export * from './assemblyInstructionsCompiler.ts'
16+
export { getSmartCdnUrl, parseSmartCdnUrl, stripSmartCdnAuth } from './smartCdn.ts'
1017

1118
const algorithmMap = {
1219
sha1: 'SHA-1',

packages/utils/src/node.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,15 @@ import { createHmac } from 'node:crypto'
66
import { finishSmartCdnUrl, prepareSmartCdnUrl } from './smartCdn.ts'
77

88
export type { SignatureAlgorithm } from './index.ts'
9-
export type { SmartCdnUrlOptions } from './smartCdn.ts'
9+
export type {
10+
ParsedSmartCdnUrl,
11+
ParseSmartCdnUrlOptions,
12+
SmartCdnUnsignedUrlOptions,
13+
SmartCdnUrlOptions,
14+
SmartCdnUrlParams,
15+
} from './smartCdn.ts'
16+
17+
export { getSmartCdnUrl, parseSmartCdnUrl, stripSmartCdnAuth } from './smartCdn.ts'
1018

1119
export type SignatureAlgorithmInput = SignatureAlgorithm | (string & {})
1220

0 commit comments

Comments
 (0)