From 7f84fc4541e9324d4d391ad3286a10baff412a6e Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Tue, 1 Sep 2026 12:34:54 +0200 Subject: [PATCH] Require explicit Smart CDN delivery config --- .../README.md | 5 ++- .../src/app/api/smartcdn/route.ts | 7 ++-- .../src/lib/transloadit-env.ts | 3 +- .../test/e2e/smartcdn.test.ts | 14 +++++++ ...ery-with-transloadit-smartcdn-in-nextjs.md | 2 + .../SKILL.md | 37 ++++++++++++------- 6 files changed, 46 insertions(+), 22 deletions(-) diff --git a/scenarios/integrate-asset-delivery-with-transloadit-smartcdn-in-nextjs/README.md b/scenarios/integrate-asset-delivery-with-transloadit-smartcdn-in-nextjs/README.md index 812b15e..f2c6cf2 100644 --- a/scenarios/integrate-asset-delivery-with-transloadit-smartcdn-in-nextjs/README.md +++ b/scenarios/integrate-asset-delivery-with-transloadit-smartcdn-in-nextjs/README.md @@ -12,12 +12,13 @@ What it proves: Required: - `TRANSLOADIT_KEY` - `TRANSLOADIT_SECRET` - -Optional: - `TRANSLOADIT_SMARTCDN_WORKSPACE` - `TRANSLOADIT_SMARTCDN_TEMPLATE` - `TRANSLOADIT_SMARTCDN_INPUT` +The workspace/template/input triplet must identify a deliberately configured asset-delivery +contract. Do not use a shared Template that accepts an arbitrary origin URL. + This scenario will load `../../.env` automatically as a local-only convenience. Real apps should use their own `.env.local` and normal Next.js env loading. ## Run diff --git a/scenarios/integrate-asset-delivery-with-transloadit-smartcdn-in-nextjs/src/app/api/smartcdn/route.ts b/scenarios/integrate-asset-delivery-with-transloadit-smartcdn-in-nextjs/src/app/api/smartcdn/route.ts index ad2f568..aa95b9b 100644 --- a/scenarios/integrate-asset-delivery-with-transloadit-smartcdn-in-nextjs/src/app/api/smartcdn/route.ts +++ b/scenarios/integrate-asset-delivery-with-transloadit-smartcdn-in-nextjs/src/app/api/smartcdn/route.ts @@ -10,10 +10,9 @@ export async function GET() { const authKey = getRequiredEnv('TRANSLOADIT_KEY'); const authSecret = getRequiredEnv('TRANSLOADIT_SECRET'); - // These values don't need to exist for signing, but real deployments should set them. - const workspace = process.env.TRANSLOADIT_SMARTCDN_WORKSPACE || 'demo'; - const template = process.env.TRANSLOADIT_SMARTCDN_TEMPLATE || 'serve-preview'; - const input = process.env.TRANSLOADIT_SMARTCDN_INPUT || 'example.jpg'; + const workspace = getRequiredEnv('TRANSLOADIT_SMARTCDN_WORKSPACE'); + const template = getRequiredEnv('TRANSLOADIT_SMARTCDN_TEMPLATE'); + const input = getRequiredEnv('TRANSLOADIT_SMARTCDN_INPUT'); const url = getSignedSmartCdnUrl({ workspace, diff --git a/scenarios/integrate-asset-delivery-with-transloadit-smartcdn-in-nextjs/src/lib/transloadit-env.ts b/scenarios/integrate-asset-delivery-with-transloadit-smartcdn-in-nextjs/src/lib/transloadit-env.ts index 32e64d3..4f81f57 100644 --- a/scenarios/integrate-asset-delivery-with-transloadit-smartcdn-in-nextjs/src/lib/transloadit-env.ts +++ b/scenarios/integrate-asset-delivery-with-transloadit-smartcdn-in-nextjs/src/lib/transloadit-env.ts @@ -7,10 +7,9 @@ export function ensureTransloaditEnv(): void { if (loaded) return; loaded = true; - if (process.env.TRANSLOADIT_KEY && process.env.TRANSLOADIT_SECRET) return; - dotenv.config({ path: path.resolve(process.cwd(), '../../.env'), + quiet: true, }); } diff --git a/scenarios/integrate-asset-delivery-with-transloadit-smartcdn-in-nextjs/test/e2e/smartcdn.test.ts b/scenarios/integrate-asset-delivery-with-transloadit-smartcdn-in-nextjs/test/e2e/smartcdn.test.ts index 069bc3d..6320390 100644 --- a/scenarios/integrate-asset-delivery-with-transloadit-smartcdn-in-nextjs/test/e2e/smartcdn.test.ts +++ b/scenarios/integrate-asset-delivery-with-transloadit-smartcdn-in-nextjs/test/e2e/smartcdn.test.ts @@ -6,6 +6,12 @@ import { chromium } from 'playwright'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { ensureTransloaditEnv } from '../../src/lib/transloadit-env'; +const smartCdnConfiguration = { + input: 'images/example.jpg', + template: 'asset-delivery', + workspace: 'skill-e2e', +}; + const getFreePort = async (): Promise => await new Promise((resolve, reject) => { const server = net.createServer(); @@ -44,6 +50,9 @@ describe('Smart CDN signing scenario', () => { env: { ...process.env, NEXT_TELEMETRY_DISABLED: '1', + TRANSLOADIT_SMARTCDN_INPUT: smartCdnConfiguration.input, + TRANSLOADIT_SMARTCDN_TEMPLATE: smartCdnConfiguration.template, + TRANSLOADIT_SMARTCDN_WORKSPACE: smartCdnConfiguration.workspace, }, stdio: 'ignore', }, @@ -88,9 +97,14 @@ describe('Smart CDN signing scenario', () => { expect(text).toBeTruthy(); const payload = JSON.parse(text as string) as Record; + expect(payload).toMatchObject(smartCdnConfiguration); expect(typeof payload.url).toBe('string'); const url = new URL(payload.url as string); expect(url.protocol).toBe('https:'); + expect(url.hostname).toBe(`${smartCdnConfiguration.workspace}.tlcdn.com`); + expect(url.pathname).toBe( + `/${smartCdnConfiguration.template}/${encodeURIComponent(smartCdnConfiguration.input)}`, + ); // Signature parameter name may evolve; check for a common one. expect(url.searchParams.has('sig') || url.searchParams.has('signature')).toBe(true); } finally { diff --git a/scripts/acceptance/integrate-asset-delivery-with-transloadit-smartcdn-in-nextjs.md b/scripts/acceptance/integrate-asset-delivery-with-transloadit-smartcdn-in-nextjs.md index 6154cbb..23b8764 100644 --- a/scripts/acceptance/integrate-asset-delivery-with-transloadit-smartcdn-in-nextjs.md +++ b/scripts/acceptance/integrate-asset-delivery-with-transloadit-smartcdn-in-nextjs.md @@ -1,2 +1,4 @@ - E2E must load `/smartcdn`, read `[data-testid="smartcdn-json"]`, parse JSON, and assert it contains a `url` string. +- The URL host and path must contain the explicitly configured workspace, Template, and input. The + implementation must not fall back to an implicit delivery contract. - The `url` should look signed (do not snapshot secrets; just assert it contains signature-ish markers like `~` or query params). diff --git a/skills/integrate-asset-delivery-with-transloadit-smartcdn-in-nextjs/SKILL.md b/skills/integrate-asset-delivery-with-transloadit-smartcdn-in-nextjs/SKILL.md index c48a035..6a9df8b 100644 --- a/skills/integrate-asset-delivery-with-transloadit-smartcdn-in-nextjs/SKILL.md +++ b/skills/integrate-asset-delivery-with-transloadit-smartcdn-in-nextjs/SKILL.md @@ -1,15 +1,21 @@ --- name: integrate-asset-delivery-with-transloadit-smartcdn-in-nextjs -description: Add Transloadit Smart CDN URL signing to a Next.js App Router project (server-side signing route + optional client demo page). +description: Add server-side Transloadit Smart CDN URL signing for an explicitly configured asset to a Next.js App Router project. --- # Inputs -- Required env (server-only): `TRANSLOADIT_KEY`, `TRANSLOADIT_SECRET` -- Optional env: `TRANSLOADIT_SMARTCDN_WORKSPACE`, `TRANSLOADIT_SMARTCDN_TEMPLATE`, `TRANSLOADIT_SMARTCDN_INPUT` +- Required env (server-only): `TRANSLOADIT_KEY`, `TRANSLOADIT_SECRET`, + `TRANSLOADIT_SMARTCDN_WORKSPACE`, `TRANSLOADIT_SMARTCDN_TEMPLATE`, and + `TRANSLOADIT_SMARTCDN_INPUT` For local dev, put these in `.env.local`. Never expose `TRANSLOADIT_SECRET` to the browser. +Use a Template owned by `TRANSLOADIT_SMARTCDN_WORKSPACE` and give `TRANSLOADIT_SMARTCDN_INPUT` a +fixed asset or storage path. Do not use a shared Template that accepts an arbitrary origin URL. If +a workspace Template imports over HTTP, constrain its origin and path in the Template instead of +accepting an arbitrary URL from the request. + # Install ```bash @@ -32,20 +38,19 @@ import { getSignedSmartCdnUrl } from '@transloadit/utils/node' export const runtime = 'nodejs' -function reqEnv(name: string): string { - const v = process.env[name] - if (!v) throw new Error(`Missing required env var: ${name}`) - return v +function getRequiredEnv(name: string): string { + const value = process.env[name] + if (!value) throw new Error(`Missing required env var: ${name}`) + return value } export async function GET() { try { - const authKey = reqEnv('TRANSLOADIT_KEY') - const authSecret = reqEnv('TRANSLOADIT_SECRET') - - const workspace = process.env.TRANSLOADIT_SMARTCDN_WORKSPACE || 'demo' - const template = process.env.TRANSLOADIT_SMARTCDN_TEMPLATE || 'serve-preview' - const input = process.env.TRANSLOADIT_SMARTCDN_INPUT || 'example.jpg' + const authKey = getRequiredEnv('TRANSLOADIT_KEY') + const authSecret = getRequiredEnv('TRANSLOADIT_SECRET') + const workspace = getRequiredEnv('TRANSLOADIT_SMARTCDN_WORKSPACE') + const template = getRequiredEnv('TRANSLOADIT_SMARTCDN_TEMPLATE') + const input = getRequiredEnv('TRANSLOADIT_SMARTCDN_INPUT') const url = getSignedSmartCdnUrl({ workspace, template, input, authKey, authSecret }) @@ -57,6 +62,10 @@ export async function GET() { } ``` +This route signs one server-configured public asset. Do not replace `workspace`, `template`, or +`input` with unchecked query/body values. For private assets, authenticate and authorize the caller +before returning a short-lived URL. + ## 2) Optional: a tiny demo page Create `app/smartcdn/page.tsx` (or `src/app/smartcdn/page.tsx`): @@ -110,7 +119,7 @@ export default function SmartCdnDemo() { # Quick Check - Start dev server, then open `/smartcdn` or fetch `/api/smartcdn`. -- Expect JSON including a `url` and your `{ workspace, template, input }`. +- Expect JSON including a `url` and the configured `{ workspace, template, input }`. # References (Internal)