|
| 1 | +#!/usr/bin/env tsx |
| 2 | +// Reference Smart CDN (https://transloadit.com/services/content-delivery/) Signature implementation |
| 3 | +// And CLI tester to see if our SDK's implementation |
| 4 | +// matches Node's |
| 5 | + |
| 6 | +/// <reference types="node" /> |
| 7 | + |
| 8 | +import { createHash, createHmac } from 'crypto' |
| 9 | + |
| 10 | +interface SmartCDNParams { |
| 11 | + workspace: string |
| 12 | + template: string |
| 13 | + input: string |
| 14 | + expire_at_ms?: number |
| 15 | + auth_key?: string |
| 16 | + auth_secret?: string |
| 17 | + url_params?: Record<string, any> |
| 18 | +} |
| 19 | + |
| 20 | +function signSmartCDNUrl(params: SmartCDNParams): string { |
| 21 | + const { |
| 22 | + workspace, |
| 23 | + template, |
| 24 | + input, |
| 25 | + expire_at_ms, |
| 26 | + auth_key, |
| 27 | + auth_secret, |
| 28 | + url_params = {}, |
| 29 | + } = params |
| 30 | + |
| 31 | + if (!workspace) throw new Error('workspace is required') |
| 32 | + if (!template) throw new Error('template is required') |
| 33 | + if (input === null || input === undefined) |
| 34 | + throw new Error('input must be a string') |
| 35 | + if (!auth_key) throw new Error('auth_key is required') |
| 36 | + if (!auth_secret) throw new Error('auth_secret is required') |
| 37 | + |
| 38 | + const workspaceSlug = encodeURIComponent(workspace) |
| 39 | + const templateSlug = encodeURIComponent(template) |
| 40 | + const inputField = encodeURIComponent(input) |
| 41 | + |
| 42 | + const expireAt = expire_at_ms ?? Date.now() + 60 * 60 * 1000 // 1 hour default |
| 43 | + |
| 44 | + const queryParams: Record<string, string[]> = {} |
| 45 | + |
| 46 | + // Handle url_params |
| 47 | + Object.entries(url_params).forEach(([key, value]) => { |
| 48 | + if (value === null || value === undefined) return |
| 49 | + if (Array.isArray(value)) { |
| 50 | + value.forEach((val) => { |
| 51 | + if (val === null || val === undefined) return |
| 52 | + ;(queryParams[key] ||= []).push(String(val)) |
| 53 | + }) |
| 54 | + } else { |
| 55 | + queryParams[key] = [String(value)] |
| 56 | + } |
| 57 | + }) |
| 58 | + |
| 59 | + queryParams.auth_key = [auth_key] |
| 60 | + queryParams.exp = [String(expireAt)] |
| 61 | + |
| 62 | + // Sort parameters to ensure consistent ordering |
| 63 | + const sortedParams = Object.entries(queryParams) |
| 64 | + .sort() |
| 65 | + .map(([key, values]) => |
| 66 | + values.map((v) => `${encodeURIComponent(key)}=${encodeURIComponent(v)}`) |
| 67 | + ) |
| 68 | + .flat() |
| 69 | + .join('&') |
| 70 | + |
| 71 | + const stringToSign = `${workspaceSlug}/${templateSlug}/${inputField}?${sortedParams}` |
| 72 | + const signature = createHmac('sha256', auth_secret) |
| 73 | + .update(stringToSign) |
| 74 | + .digest('hex') |
| 75 | + |
| 76 | + const finalParams = `${sortedParams}&sig=${encodeURIComponent( |
| 77 | + `sha256:${signature}` |
| 78 | + )}` |
| 79 | + return `https://${workspaceSlug}.tlcdn.com/${templateSlug}/${inputField}?${finalParams}` |
| 80 | +} |
| 81 | + |
| 82 | +// Read JSON from stdin |
| 83 | +let jsonInput = '' |
| 84 | +process.stdin.on('data', (chunk) => { |
| 85 | + jsonInput += chunk |
| 86 | +}) |
| 87 | + |
| 88 | +process.stdin.on('end', () => { |
| 89 | + const params = JSON.parse(jsonInput) |
| 90 | + console.log(signSmartCDNUrl(params)) |
| 91 | +}) |
0 commit comments