`) is the ONLY supported default. npm SDK is deprecated.**
- **[MCP Runtime Boundary] — MCP is an AI-tool transport, NOT a customer app backend.**
- [Code Contract] — mandatory rules every code block must satisfy
- [Public Auth API Surface] — hosted auth redirects + typed modal compatibility
- [Quick Use Cases] — when to call each auth API
- [Common Session Rule] — session requirements per API
- [Blocking Parameter Rule] — stop and ask for resource / action (hosted step-up) when unknown
- [Placeholder Resolution Policy] — RESOLVED_* are required inputs, not final values
- [Index & Tags] — keyword → section mapping
### MCP Server Integration
- MCP Server setup — install in Claude Desktop, Cursor, or Codex for project setup/admin assistance
- Available MCP tools — members, roles, resources, audit logs, permissions for AI-assisted operations
- Migration Guide — moving existing users and roles to Transcodes via AI-tool MCP, not customer runtime code
### How It Works & Architecture
- Core security model — WebAuthn, DPoP, browser memory tokens, RP ID binding
- System Context: Disney World Scenario — entity mapping and authentication flow analogy
### Section 1 — Optional PWA/Web App Kit Setup (manifest + sw.js) — CDN static
- PWA — HTML + service worker (required only for installable PWA, CDN static, `defer`)
- DO NOT list — prohibited actions
- Required PWA wiring (`` + `` to the ``.
- **PWA/Web App Kit assets are conditional:** Add `` and `public/sw.js` only when the user asks for installable PWA/Web App Kit.
- **REQUIRED ATTRIBUTE:** The script tag **MUST** use `defer`. Do NOT use `type="module"`, `crossorigin`, or `async`.
### 2. TypeScript Rule (Mandatory for CDN Static)
- **`transcodes.d.ts` is MANDATORY** for any TypeScript project using the CDN static method.
- **DO NOT ASSUME IT EXISTS.** Verify its existence. If missing, you MUST create it by fetching from `https://www.transcodes.io/instructions/types`.
- **`tsconfig.json` MUST BE UPDATED.** You MUST add the directory containing `transcodes.d.ts` to **BOTH** `include` AND `typeRoots`. Skipping this will cause a compile error.
### 3. Framework Access Pattern Rule (CSR vs SSR)
- **CSR (Vite, CRA, Vue SPA):** Use the bare `transcodes.*` global everywhere.
- **SSR/RSC (Next.js, Remix, Astro, SvelteKit):** You **MUST** guard SDK calls. Calling `transcodes.*` at the module top-level or in server renders will throw a `ReferenceError`.
- **Rule:** Only call `transcodes.*` inside browser-only paths (`useEffect`, event handlers) OR guard it with `if (typeof window !== 'undefined' && window.transcodes)`.
### 4. Parameter Resolution Rule
- If a required parameter (like `projectId`, `resource`, or `action`) is missing, **STOP AND ASK** the user. Do NOT hallucinate or guess security parameters.
- If Transcodes MCP is available, use it to resolve resources/roles first.
### 5. MCP Runtime Boundary Rule
- **MCP is NOT a customer application backend.** `http://localhost:/mcp`, `/mcp`, `tools/list`, and `tools/call` are MCP client transports for AI tools such as Claude, Cursor, and Codex. They are not browser, Vue, React, Next.js, or app-server integration endpoints.
- **NEVER generate customer app code that calls MCP HTTP endpoints.** Do not put `/mcp`, `tools/list`, `tools/call`, or `http://localhost:/mcp` in browser code, framework code, API routes, server actions, backend controllers, or fetch helpers.
- **NEVER expose `TRANSCODES_TOKEN` to a customer app.** `TRANSCODES_TOKEN` belongs only in the MCP host configuration. Browser apps use the CDN static SDK; app backends use server-only Transcodes API tokens such as `MEMBER_API_TOKEN` when a backend verification call is required.
- For requests such as "build a Vue todo app", "add auth to a React app", or "wire Transcodes into Next.js", generate CDN static SDK integration: resolve the real `projectId`, resolve the CDN base, install `transcodes.d.ts`, add ``, and call `transcodes.*` APIs in client-safe code.
- Use MCP only as an AI-tool helper to inspect or manage project state (`get_project`, `get_resources`, `get_roles`, member/RBAC tools). If MCP returns 401 or is unavailable, do not guess values; ask the user to confirm the real project ID, CDN base, resource, role, or Console setting.
---
### [Default Integration Method — CDN STATIC IS THE ONLY DEFAULT]
1. **Always default to CDN static integration.** For Authentication-only, add one native script tag to the HTML ``:
```html
```
Do **not** add a manifest link or create `public/sw.js` for Authentication-only.
If the user explicitly needs installable PWA/Web App Kit, then also add the manifest and one-line service worker:
```html
```
```javascript
// public/sw.js — entire file, one line
importScripts('{RESOLVED_TRANSCODES_CDN_BASE}/{RESOLVED_PROJECT_ID}/sw.js');
```
2. **The SDK script tag MUST use `defer`.** Do NOT use `type="module"`. Do NOT add `crossorigin` / `crossOrigin`. Do NOT use `async`. Do NOT use Next.js `` only.
3. **CDN static REQUIRES `transcodes.d.ts` for any TypeScript project. Skipping any sub-step here is a compile error.** This is a strict 3-step procedure — execute it as a sequence, do not assume any step is already done:
**Step 3a — Check if `transcodes.d.ts` exists in the repo** (e.g. `types/transcodes.d.ts`, `src/types/transcodes.d.ts`, or anywhere on the `tsconfig.json` `include` path).
- **If the file is MISSING → you MUST create it** by fetching from `https://www.transcodes.io/instructions/types` (or download/copy the same file from the Transcodes Console). Do not skip this step because the file "should be there" — verify, then create if absent:
```bash
mkdir -p types
curl -o types/transcodes.d.ts https://www.transcodes.io/instructions/types
```
- **If the file EXISTS → still confirm it is up-to-date** by re-running `curl` (or comparing the version comment).
**Step 3b — TypeScript projects MUST update `tsconfig.json`** so the compiler picks up the file. **Without this edit, every `transcodes.*` (or `window.transcodes.*`) call site will throw a TypeScript compile error** (`Cannot find name 'transcodes'` for the bare form, or `Property 'transcodes' does not exist on type 'Window & typeof globalThis'` for the explicit form). Add the directory containing `transcodes.d.ts` to **both** `include` AND `typeRoots`:
```jsonc
{
"compilerOptions": {
"typeRoots": ["./node_modules/@types", "./types"]
},
"include": ["src", "types"]
}
```
**Step 3c — Verify by running `tsc --noEmit`** (or your framework's type-check command). If `window.transcodes` is still untyped, `tsconfig.json` was not edited correctly — go back to Step 3b.
For non-TypeScript (plain JS) projects you can skip Step 3b/3c, but Step 3a is still recommended so editors can offer autocomplete via JSDoc.
4. **Do NOT call `init()` on the CDN static path.** `webworker.js` bootstraps `window.transcodes` automatically — `projectId` is encoded in the URL.
5. **Do NOT install the npm SDK package.** The npm package is **deprecated for new integrations** until the SDK is reintroduced after stabilization. If the user asks for npm, explain that the supported path is CDN static (``**
- Do NOT add `crossorigin` / `crossOrigin` to the `webworker.js` `` in ``. **No `type="module"`. No `crossorigin`.**
**CDN host:** `{RESOLVED_TRANSCODES_CDN_BASE}` varies per environment. The MCP server auto-substitutes when `TRANSCODES_CDN_BASE_URL` is set; otherwise resolve from the project's env (`NEXT_PUBLIC_TRANSCODES_CDN_BASE`) / Console — do not hardcode.
**TypeScript (CDN) — MANDATORY (3 strict steps):**
1. **Verify `transcodes.d.ts` exists** in the repo. If it is MISSING, create it now from `https://www.transcodes.io/instructions/types` (do not skip — the file will not appear by itself):
```bash
mkdir -p types && curl -o types/transcodes.d.ts https://www.transcodes.io/instructions/types
```
2. **Edit `tsconfig.json`** — add the directory to **both** `include` and `typeRoots`. **Without this edit, every `transcodes.*` call site is a TypeScript compile error** (`Cannot find name 'transcodes'`).
3. **Run `npx tsc --noEmit`** to confirm no type errors remain.
See **CDN TypeScript setup** for the full procedure and the exact `tsconfig.json` snippet. **Do not** call `init()` on the CDN static path.
#### React (Vite / CRA) / Vue.js
Add to `index.html` ``, then create `public/sw.js`:
```html
```
`public/sw.js` (entire file — one line):
```javascript
importScripts('{RESOLVED_TRANSCODES_CDN_BASE}/{RESOLVED_PROJECT_ID}/sw.js');
```
#### Next.js App Router
Use a **native `
```
After the deferred script runs, all APIs are available via `window.transcodes` (or just `transcodes`).
**Why this is the default:** No bundler config, no `init()` race conditions, no SSR pitfalls, no extra dependency, single source of truth (the CDN URL), instant SDK updates without redeploying your app.
### Option B — npm SDK fallback — DEPRECATED / LEGACY ONLY
**Do not generate this path for new integrations.** The npm package is deprecated until the SDK is reintroduced after stabilization. If the user asks for npm, explain that CDN static is the supported path:
> "The npm SDK fallback is deprecated for new integrations. Use CDN static (`` in ``. Don't use `type="module"`. Don't use `crossorigin`. Don't use `next/script`. Don't install the npm SDK package unless the user explicitly requests the npm fallback.
### React + Vite — CDN static (DEFAULT)
> **Access pattern:** Vite is CSR-only — bare `transcodes.*` and explicit `window.transcodes.*` are equivalent. Use the bare form everywhere for brevity. See **Framework-specific access pattern** below.
Put the script in **`index.html` ``** (native `
```
```bash
# .env
VITE_TRANSCODES_PROJECT_ID=proj_abc123xyz
```
**Then complete the TypeScript wiring (mandatory — skipping this is a compile error):**
```bash
# 1. Create types/transcodes.d.ts if it does not already exist
mkdir -p types
curl -o types/transcodes.d.ts https://www.transcodes.io/instructions/types
```
```jsonc
// 2. tsconfig.json — add types/ to BOTH typeRoots and include
{
"compilerOptions": {
"typeRoots": ["./node_modules/@types", "./types"]
},
"include": ["src", "types"]
}
```
```bash
# 3. Verify
npx tsc --noEmit
```
### Next.js App Router — CDN static (DEFAULT)
> **Access pattern (CRITICAL):** Next.js renders `'use client'` components **on the server first** during SSR/RSC. Calling `transcodes.*` (or `window.transcodes.*`) at module top level or directly in a render body throws `ReferenceError` at SSR — **and `tsc` will not warn you**. Defer every SDK call into `useEffect`, an event handler, or behind a `typeof window !== 'undefined' && window.transcodes` guard. See **Framework-specific access pattern** below.
Use a **native `
```
**Then complete the TypeScript wiring (mandatory — skipping this is a compile error):**
```bash
# 1. Create types/transcodes.d.ts if it does not already exist
mkdir -p types
curl -o types/transcodes.d.ts https://www.transcodes.io/instructions/types
```
```jsonc
// 2. tsconfig.json — add types/ to BOTH typeRoots and include
{
"compilerOptions": {
"typeRoots": ["./node_modules/@types", "./types"]
},
"include": ["src", "types"]
}
```
```bash
# 3. Verify
npx tsc --noEmit
```
---
### Framework-specific access pattern — when to use `transcodes` vs `window.transcodes`
Both `transcodes.*` (bare) and `window.transcodes.*` (explicit) resolve to the **same object at runtime** in the browser, and both are typed by `transcodes.d.ts` (`var transcodes: Window['transcodes']`). **The choice depends entirely on where the code runs.**
> ⚠️ **TypeScript will NOT catch SSR-time absence.** The `.d.ts` declares the global as always-present. The type checker happily green-lights both forms even when they would throw `ReferenceError` at runtime on the server. This makes the framework decision below a **runtime concern that the type system cannot enforce** — you must reason about it explicitly.
| Framework / runtime | Module top-level / RSC / first SSR render | `useEffect` / event handler / `onClick` | Recommended access form |
| ---------------------------------------- | ----------------------------------------------------- | --------------------------------------------------- | ---------------------------------------------------------------------- |
| **Vite** (React, Vue, Svelte) — CSR only | ✅ both work — no SSR pass | ✅ both work | Use bare `transcodes.*` everywhere. Equivalent and shorter. |
| **Create React App** / Parcel — CSR only | ✅ both work | ✅ both work | Use bare `transcodes.*` everywhere. |
| **Next.js App Router** (RSC + SSR) | ❌ both throw `ReferenceError` (renders in Node first) | ✅ both work (browser only) | Inside `'use client'` + `useEffect` / handler: bare `transcodes.*`. Outside that scope: guard with `typeof window !== 'undefined' && window.transcodes`. |
| **Next.js Pages Router** (`getServerSideProps`, `getStaticProps`) | ❌ both throw on the server | ✅ both work in `useEffect` / handlers | Same as App Router. Never call SDK in `getServerSideProps` / `getStaticProps`. |
| **Remix / React Router (Framework Mode)** | ❌ both throw in loaders / actions / SSR | ✅ both work in component effects | Same as Next.js — gate behind effects or feature-detection. |
| **Astro / Qwik** (islands + SSR) | ❌ both throw at SSR | ✅ both work inside client islands | Use bare `transcodes.*` inside client-only directives (`client:load`, `client:idle`, etc.). |
**Decision rule for code samples:**
1. **Pure CSR project (Vite / CRA / Vue SPA / static HTML)** → use bare `transcodes.*` everywhere. Concise and equivalent.
2. **Any framework with SSR / RSC / static-generation phase (Next.js / Remix / Astro / SvelteKit)** → bare `transcodes.*` is fine **only inside browser-only code paths** (`useEffect`, `useLayoutEffect`, event handlers, components after `'use client'` + post-mount). Anywhere else, use the explicit `window.transcodes` form **with an SSR guard** so `tsc` and bundler don't mask the runtime hazard.
**Idiomatic SSR-safe access patterns:**
```tsx
// Next.js App Router — 'use client' + useEffect (preferred)
'use client';
import { useEffect } from 'react';
export function LoginButton() {
// ❌ WRONG — runs at first SSR pass, transcodes is undefined on the server
// const isAuth = await transcodes.token.isAuthenticated();
useEffect(() => {
// ✅ OK — useEffect only runs in the browser
transcodes.token.isAuthenticated().then((isAuth) => {
// ...
});
}, []);
return ;
}
```
```ts
// Anywhere outside an effect / handler in an SSR framework — feature-detect first.
// The window.transcodes form pairs naturally with `typeof window !== 'undefined'`.
function isTranscodesReady(): boolean {
return typeof window !== 'undefined' && Boolean(window.transcodes);
}
if (isTranscodesReady()) {
// Inside the guarded block, bare `transcodes.*` is safe to call.
await transcodes.token.isAuthenticated();
}
```
```ts
// AVOID — the bare form silently breaks SSR because TS doesn't see the gap.
// In Next.js / Remix / Astro this throws at SSR build / render time.
const isAuth = await transcodes.token.isAuthenticated(); // ❌ at module top level
```
**Why prefer `window.transcodes` for the guard but `transcodes.*` for the call?**
- `typeof window !== 'undefined' && window.transcodes` is the canonical SSR feature-detection idiom — every Next.js / Remix codebase has it. Reviewers immediately recognise the intent.
- After the guard establishes browser context, switching to bare `transcodes.*` for the actual calls keeps the call sites short and matches the rest of the documentation.
- TypeScript narrows neither form (the `.d.ts` always promises `transcodes` exists), so the guard is purely a **runtime safety contract**, not a type-narrowing trick.
---
### npm fallback variants (legacy exception only)
Do not generate npm fallback variants from this guide. If the user explicitly opts out of CDN static, verify the current package API first and keep that legacy implementation separate from the CDN static path.
---
## Quick start example (CDN static — Authentication-only DEFAULT)
**This is the default. Generate this — not an npm/`init()` variant — unless the user explicitly asks otherwise.**
For installable PWA, add the manifest and `public/sw.js` from Section 1 after this Authentication-only setup.
```html
Not signed in
```
---
## CDN TypeScript setup — MANDATORY
The CDN static path is the default integration method, and **`transcodes.d.ts` is a required part of it for any TypeScript project.** Without it, `window.transcodes` is `any` and the SDK call sites do not type-check — worse, with `strict` mode enabled the project will fail to compile (`Property 'transcodes' does not exist on type 'Window'`).
**Do not assume the file is already in the repo. Verify, then create it if missing.**
### Step 1 — Check `transcodes.d.ts` exists; if not, create it
Search the repo for an existing `transcodes.d.ts` (typical locations: `types/transcodes.d.ts`, `src/types/transcodes.d.ts`, or any path on `tsconfig.json` `include`).
- **If the file is MISSING → create it now.** This is mandatory, not optional. The canonical source is `https://www.transcodes.io/instructions/types` (you can also download/copy the same file from the Transcodes Console):
```bash
mkdir -p types
curl -o types/transcodes.d.ts https://www.transcodes.io/instructions/types
```
- **If the file EXISTS → confirm it is current** by re-running the `curl` (it overwrites the local copy with the latest CDN version). Stale `.d.ts` files cause subtle "method not found" errors when the SDK adds new APIs.
### Step 2 — Edit `tsconfig.json` (TypeScript projects only — REQUIRED)
**Skipping this step causes compile errors.** The TypeScript compiler will not pick up `transcodes.d.ts` unless its directory is on `include` and/or `typeRoots`. Update `tsconfig.json` like this:
```jsonc
{
"compilerOptions": {
// include the project-local types directory in addition to node_modules/@types
"typeRoots": ["./node_modules/@types", "./types"]
},
// ensure the types/ directory is part of the compilation (not just typeRoots)
"include": ["src", "types"]
}
```
If you used a different folder name in Step 1 (e.g. `src/types/`), match it in both `typeRoots` and `include`.
For Next.js projects, add the same paths to the **root** `tsconfig.json`. Do **not** rely on auto-generated `next-env.d.ts` to pick up `transcodes.d.ts` — it will not.
### Step 3 — Verify the compiler is happy
Run a no-emit type check to confirm the wiring:
```bash
npx tsc --noEmit
```
If `window.transcodes` is still untyped, `tsconfig.json` was edited incorrectly — re-check that the directory containing `transcodes.d.ts` appears in **both** `include` AND `typeRoots`, and that the file path is exactly `/transcodes.d.ts`.
After Steps 1–3, `window.transcodes`, `transcodes.redirectToSignIn`, `transcodes.token.getCurrentMember()`, etc. are fully typed.
### Notes for the CDN static path
- **Do NOT call `init()`.** `webworker.js` bootstraps `window.transcodes` automatically. The `.d.ts` may still declare `init` (it is shared with the npm package) — ignore that export on the CDN static path.
- Re-run `curl` periodically (or as part of CI) so the local `transcodes.d.ts` stays in sync with the CDN.
### npm fallback only
When the user has explicitly chosen the npm SDK fallback, TypeScript types ship with the package and **no** extra `transcodes.d.ts` file is needed (and no `tsconfig.json` edit either). This exemption applies only to the npm fallback — not to the default CDN static path.
---
## React SDK helper aliases (recommended)
Use small local helper aliases in React apps when that makes components easier to read. **Default to CDN static**: read everything off the bare `transcodes` global (typed via `transcodes.d.ts`). Hosted redirect APIs do not share the old modal return shape, so do not reuse modal payload handling for CDN static integrations.
**CDN static (DEFAULT):**
```tsx
// src/lib/transcodes-client.ts — CDN static
// transcodes.d.ts must be installed (see "CDN TypeScript setup")
const sdkIsAuthenticated = () => transcodes.token.isAuthenticated();
const redirectToHostedSignIn = (redirectUri: string) =>
transcodes.redirectToSignIn({ redirectUri });
const redirectToHostedConsole = (redirectUri: string) =>
transcodes.redirectToConsole({ redirectUri });
const redirectToHostedStepUp = (params: { resource: string; action: 'create' | 'read' | 'update' | 'delete' }) =>
transcodes.redirectToStepUp(params);
const sdkSignOut = () => transcodes.token.signOut();
const on = transcodes.on.bind(transcodes);
// Use these helpers inside browser-only React handlers/effects.
```
No npm React helper variant is provided because generated integrations should use CDN static.
---
## npm fallback examples — omitted for generated integrations
The npm SDK is deprecated for new integrations. Do not generate React quick starts from the npm SDK fallback; use the CDN static quick start above.
---
## Step-up MFA example (CDN static — DEFAULT)
Use before any sensitive action. This example shows the **default CDN static** flow via the bare `transcodes` global. No npm step-up variant is provided because generated integrations should use CDN static.
**CDN step-up with all mandatory steps:**
```typescript
// CDN step-up implementation
// Resolve resource/action from MCP / Console first. If unavailable, ask the user and stop.
async function deleteUser(userId: string) {
try {
const resource = resolvedResource; // resolved via get_resources or explicit user input
const action = resolvedAction; // resolved from the protected operation
// 1. RBAC gate + hosted step-up when permission level is 2
const gate = await transcodes.redirectToStepUp({
resource,
action,
redirectUri: `${window.location.origin}/auth/stepup-callback`,
});
// SUCCESS CHECKS before using sid
if (!gate.success || !gate.payload[0]) {
throw new Error(gate.error || 'Step-up failed');
}
const decision = gate.payload[0];
if (decision.decision === 'deny') {
throw new Error('RBAC denied');
}
if (decision.decision === 'allow') {
throw new Error('Step-up was not required by RBAC; expected permission level 2');
}
if (decision.status !== 'verified' || !decision.sid) {
throw new Error('Step-up was not verified');
}
// Send verified sid to YOUR backend
await fetch(`/api/users/${userId}`, {
method: 'DELETE',
headers: {
Authorization: `Bearer ${await transcodes.token.getAccessToken()}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ sid: decision.sid }),
});
// YOUR backend calls: GET https://api.transcodesapis.com/v1/auth/temp-session/step-up/{sid}
// with header `x-transcodes-token: ` to verify before executing the action.
// 4. Audit log success
await transcodes.trackUserAction({
tag: 'user:delete',
severity: 'high',
status: true,
metadata: { userId },
});
} catch (err) {
// 5. Audit log failure
await transcodes.trackUserAction({
tag: 'user:delete',
severity: 'high',
status: false,
error: (err as Error).message,
metadata: { userId },
});
}
}
```
---
## Server-side JWT verification
When you create an Authentication Cluster, you receive a `public_key.json` file (EC P-256 / ES256). Use this key to verify JWTs issued by Transcodes on your server. No JWKS endpoint or remote key fetch is needed.
Flow:
1. Client calls `await getAccessToken()` to get the JWT
2. Client sends the token to your backend via `Authorization: Bearer `
3. Your backend verifies the token locally using `public_key.json`
`public_key.json` format (download from Console → Authentication Cluster):
```json
{
"kty": "EC",
"x": "...",
"y": "...",
"crv": "P-256",
"alg": "ES256",
"kid": "..."
}
```
If you regenerate the public key in the Console, the previous key becomes invalid. Update `public_key.json` on your server immediately.
### Node.js / Next.js (jose)
```typescript
// npm install jose
import { importJWK, jwtVerify, type JWTPayload } from 'jose';
// Paste your public_key.json content here, or import from a file
const TRANSCODES_PUBLIC_JWK = {
kty: 'EC',
x: '...',
y: '...',
crv: 'P-256',
alg: 'ES256',
kid: '...',
};
let _publicKey: Awaited> | null = null;
async function getPublicKey() {
if (!_publicKey) {
_publicKey = await importJWK(TRANSCODES_PUBLIC_JWK, 'ES256');
}
return _publicKey;
}
async function verifyToken(token: string): Promise {
const publicKey = await getPublicKey();
const { payload } = await jwtVerify(token, publicKey);
return payload;
}
```
Express middleware:
```typescript
app.use(async (req, res, next) => {
const auth = req.headers.authorization;
if (!auth?.startsWith('Bearer ')) return res.status(401).json({ error: 'No token' });
try {
req.member = await verifyToken(auth.slice(7));
next();
} catch {
res.status(401).json({ error: 'Invalid or expired token' });
}
});
```
Next.js middleware / route handler:
```typescript
import { importJWK, jwtVerify, type JWTPayload } from 'jose';
import { NextRequest, NextResponse } from 'next/server';
const TRANSCODES_PUBLIC_JWK = { /* paste public_key.json */ };
let _publicKey: Awaited> | null = null;
async function getPublicKey() {
if (!_publicKey) _publicKey = await importJWK(TRANSCODES_PUBLIC_JWK, 'ES256');
return _publicKey;
}
export async function verifyAuth(req: NextRequest): Promise<{ payload: JWTPayload } | NextResponse> {
const authHeader = req.headers.get('authorization');
if (!authHeader?.startsWith('Bearer ')) {
return NextResponse.json({ error: 'No token' }, { status: 401 });
}
try {
const publicKey = await getPublicKey();
const { payload } = await jwtVerify(authHeader.slice(7), publicKey);
return { payload };
} catch {
return NextResponse.json({ error: 'Invalid or expired token' }, { status: 401 });
}
}
```
### Python (PyJWT)
```python
# pip install pyjwt cryptography
import jwt
PUBLIC_KEY_JWK = {
"kty": "EC",
"x": "...",
"y": "...",
"crv": "P-256",
"alg": "ES256",
"kid": "...",
}
_public_key = jwt.algorithms.ECAlgorithm.from_jwk(PUBLIC_KEY_JWK)
def verify_token(token: str) -> dict:
return jwt.decode(token, _public_key, algorithms=["ES256"])
```
## CSP (Content Security Policy)
At minimum, allow the CDN for the SDK script (`script-src`) and the REST API host for SDK fetches (`connect-src`). Hosted sign-in, console, and step-up navigate to the auth app as a top-level redirect or a new tab; they do **not** require `frame-src` in the current hosted redirect flow. If your app already uses the CSP `navigate-to` directive, include `{RESOLVED_TRANSCODES_AUTH_ORIGIN}` there.
```html
```
---
## Hosted WebAuthn and Redirect Origins
**Current NestJS SSOT (2026-06):** In the hosted auth architecture, passkey/authenticator WebAuthn ceremonies run on the dedicated auth app (`https://auth.transcodes.io` in prod, dev override supported). Customer apps do not create, serve, or verify a WebAuthn related-origins file for the normal hosted sign-in, console, or step-up flows.
`authentication.related_origins` is used by `TempSessionService` as the allow-list for hosted sign-in `redirect_uri` callback origins. Treat it as sign-in redirect configuration only. Do not use it to build WebAuthn ceremony origin lists.
### Agent rules
- Do **not** create or rewrite `/.well-known/webauthn` in the customer repository for normal generated integrations.
- Do **not** synthesize an `origins` array from `project.domain_url`, `https://auth.transcodes.io`, and `authentication.related_origins`.
- If `redirectToSignIn()` must return to a preview URL, alias, subdomain, custom domain, or localhost URL, add that full callback origin in Console -> Authentication -> Related origins.
- Never remove or rewrite existing `authentication.related_origins` entries unless the user explicitly asks. Removing them can break hosted sign-in callback validation.
- If a user explicitly asks for customer-domain WebAuthn setup, stop and verify the current backend contract first. This frontend integration guide does not require or describe that setup.
### Integration record
In `.transcodes/integration.md`, record only this:
- Hosted WebAuthn: `handled by https://auth.transcodes.io` (or the configured dev auth app).
- Customer `/.well-known/webauthn`: `not generated for hosted redirect flow`.
- Hosted sign-in callback origins: list `project.domain_url` plus any saved `authentication.related_origins` values that are needed for sign-in callbacks.
If `authentication.related_origins` is absent or empty, record `[]`. This is valid and does not require any customer WebAuthn file.
---
## Release Checklist
Use this checklist before outputting final code:
0. **Integration method = CDN static** — Authentication-only uses one `` in HTML ``. Add `` and `public/sw.js` (one-line `importScripts`) only for installable PWA/Web App Kit. The script tag has `defer` only — **no `type="module"`, no `crossorigin`, no `async`**. The npm SDK fallback is used **only** if the user explicitly opted out of CDN static.
0a. **`transcodes.d.ts` is installed AND `tsconfig.json` is wired** for any TypeScript project on the CDN static path. All three sub-steps must be done — verify, do not assume:
- `transcodes.d.ts` exists in the repo (e.g. `types/transcodes.d.ts`). If it was missing, you fetched it from `https://www.transcodes.io/instructions/types` (not from memory).
- `tsconfig.json` includes the directory in **both** `include` AND `typeRoots`. **Skipping this is a guaranteed compile error.**
- `npx tsc --noEmit` passes (or the project's equivalent type-check command).
0b. **No `init()` call** on the CDN static path. (`init({ projectId })` appears only in confirmed npm fallback code.)
1. `projectId`, JWT issuer, and server-side Transcodes token (`x-transcodes-token`) are resolved or explicitly requested from the user.
2. Only the hosted redirect APIs are generated by default:
- `redirectToSignIn(options?)`
- `redirectToConsole(options?)`
- `redirectToStepUp({ resource, action, redirectUri?, comment? })`
Direct `openAuth*` calls are used only after verifying the current entry binds the matching modal factory.
3. All async SDK methods use `await`.
4. Every SDK result checks outer `result.success` before reading `payload`.
5. `redirectToStepUp()` checks outer `result.success`, then branches on `result.payload[0]?.decision`; a verified `sid` is required before executing the protected operation.
6. Step-up flows send the verified `sid` to the app backend, and the backend verifies it with:
- `GET https://api.transcodesapis.com/v1/auth/temp-session/step-up/{sid}`
- header `x-transcodes-token: ` (server-only JWT issued from the Console; different from `TRANSCODES_TOKEN` used by MCP)
7. Protected console and step-up flows are preceded by `await isAuthenticated()` or an equivalent app session guard.
8. Event subscriptions return and use `unsubscribe` on cleanup.
9. Audit log tags use `entity:action` format.
10. Sensitive operations log both success and failure.
11. `resource` is resolved via `get_resources` or explicit user input; never guessed.
12. **No customer WebAuthn endpoint** — hosted WebAuthn ceremonies run on the auth app, and `authentication.related_origins` is only for hosted sign-in callback validation. Do not generate `/.well-known/webauthn` for normal integrations.
13. Final code contains no unresolved placeholders.
---
## [Agent Rule — .transcodes Integration Record]
### After completing Transcodes frontend integration
When the initial installation and configuration of Transcodes is finished, **you MUST create a `.transcodes/` folder** in the project root and generate a documentation file that records how each Transcodes API was used.
**Create:** `.transcodes/integration.md`
The file must contain:
1. **Project info** — Project ID, integration method (`CDN static` is the default; record `npm` only if the user explicitly chose it), framework
2. **redirectToSignIn** — where it is called, which component/page, any options used (e.g. `redirectUri`)
3. **redirectToConsole** — where it is called, which component/page, any options used
4. **redirectToStepUp** — where it is called, which `resource` and `action` values were used, callback path, and backend verification endpoint
5. **Transcodes MCP** — if used: which tools (e.g. members, roles, audit logs), and how the project is configured
6. **trackUserAction** — every call site, the `tag` used, `severity`, what `metadata` is passed
7. **Events** — which events are subscribed to (`AUTH_STATE_CHANGED`, `TOKEN_EXPIRED`, etc.) and where
8. **Server-side** — whether JWT verification is implemented, which endpoint, which library
9. **Hosted WebAuthn / redirect origins** — record that hosted WebAuthn is handled by `https://auth.transcodes.io`, customer `/.well-known/webauthn` was not generated, and `authentication.related_origins` is used only as the hosted sign-in callback allow-list (`[]` if empty / absent).
Format each entry as a table or bullet list with: **file path**, **function/component name**, **parameters**, and **purpose**.
Example structure:
```
# Transcodes Integration Record
## Project
- Project ID: proj_abc123
- Method: CDN static (`` in `` for Authentication-only; add `` and `public/sw.js` only for installable PWA/Web App Kit; fetch **https://www.transcodes.io/instructions/types**, save as `transcodes.d.ts`, wire `tsconfig.json`; the CDN static path needs no `init()`.
- **When to show**: When the user is setting up the default CDN static integration, asks where to put the script tag, or asks what extra assets PWA needs.
- **Link**: [Watch Video](https://player.mux.com/PQCWSkHiBYjhWkJXlsHu7w5Ha9U2oWnDGtAjbaCo1Go?metadata-video-title=pwa_auth_installation&video-title=pwa_auth_installation)
- **PWA Installation Simulation**
- **Description**: Simulates the user experience of installing the PWA.
- **When to show**: When the user wants to understand the install prompt flow.
- **Link**: [Watch Video](https://player.mux.com/HAp2zjaue756ndCwdQ02PsJGjZk01Ft75r101hhDSeE93Y?metadata-video-title=pwa_installation&video-title=pwa_installation)
- **Rich Install Screenshots Setup**
- **Description**: How to upload and configure screenshots that appear in the PWA install prompt.
- **When to show**: When the user asks how to add images to the install prompt or configure rich install UI.
- **Link**: [Watch Video](https://player.mux.com/blZ00ZnVCN00RMow02DL4fMwCBTdjSXNtJRPbO7jEFm4jU?metadata-video-title=pwa_screenshot&video-title=pwa_screenshot)
- **Widget Component Configuration**
- **Description**: How to configure the floating PWA install widget.
- **When to show**: When the user asks about the install button or widget configuration.
- **Link**: [Watch Video](https://player.mux.com/QfE008xqjTzHLjoGabizmqU00fdb7ZGrwve1QgpFeJU6g?metadata-video-title=pwa_widget&video-title=pwa_widget)
- **Manifest Configuration**
- **Description**: How to set up the PWA manifest details (name, short name, theme color, display mode) in the Console.
- **When to show**: When the user asks how to change the app name, colors, or display settings for the PWA.
- **Link**: [Watch Video](https://player.mux.com/JEm5F02GQfAWlNWxujqt8B9ScTXSCvs00RzraM9YHF01Ds?metadata-video-title=pwa_configuration&video-title=pwa_configuration)
- **Custom Icon Setup**
- **Description**: How to upload a base icon and have Transcodes automatically generate all required manifest icon sizes.
- **When to show**: When the user asks how to set the app icon for the home screen.
- **Link**: [Watch Video](https://player.mux.com/W2Gs4TJThlEkOW3m017RtP00ZsbkP01ZP6y7JoCrkTe5no?metadata-video-title=pwa_icon&video-title=pwa_icon)
- **PWA Cache Settings**
- **Description**: How to configure offline caching strategies via the Transcodes Console.
- **When to show**: When the user asks about offline support or caching static assets.
- **Link**: [Watch Video](https://player.mux.com/39ieyO856vgEacy0136q3qHEQEEQaokRl9vgPS8hizgE?metadata-video-title=pwa_cache&video-title=pwa_cache)
---
## Troubleshooting
Use this section when something is not working. Each entry maps **symptom → cause → fix**.
---
## PWA / sw.js
### GET /sw.js → 404
- **Cause**: `public/sw.js` does not exist in the repo, but the browser is requesting it because Transcodes PWA wiring is (or was) active.
- **Diagnosis**: Check if `public/sw.js` is present. Check browser DevTools → Application → Service Workers for stale registrations.
- **Fix**: Create `public/sw.js` with exactly one line:
```javascript
importScripts('{RESOLVED_TRANSCODES_CDN_BASE}/{RESOLVED_PROJECT_ID}/sw.js');
```
- **If PWA is not needed**: Remove `` and stale service worker registration/imports, but keep `` if Authentication-only is still used. Clear stale SW via DevTools → Application → Service Workers → Unregister.
### PWA install prompt never appears
- **Cause 1**: `public/sw.js` is missing → browser cannot register the service worker.
- **Cause 2**: `` is missing from ``.
- **Cause 3**: Site is not served over HTTPS (required for WebAuthn / PWA).
- **Fix**: Confirm all three: manifest link in ``, `sw.js` served at `/sw.js`, HTTPS in production.
### manifest.json → 404
- **Cause**: Using a local `manifest.json` path that does not exist instead of the CDN URL.
- **Fix**: Use `href="{RESOLVED_TRANSCODES_CDN_BASE}/{RESOLVED_PROJECT_ID}/manifest.json"` — do not create a local file.
---
## SDK loading (CDN static)
### Should I use the npm SDK instead?
- **Default answer: NO.** Use CDN static (`` in `` + `transcodes.d.ts`). The npm SDK is a fallback used only when the user explicitly opts out of CDN static — for example, an environment with no controllable HTML ``.
- **If you already wired npm without asking**: Switch to CDN static unless the user has confirmed they need npm.
### `window.transcodes` is undefined
- **Cause 1**: `` inside ``. Gate usage on `window.transcodes` existing, on `DOMContentLoaded`, or subscribe to `AUTH_STATE_CHANGED` instead of reading on load.
### `ReferenceError: transcodes is not defined` (or `window is not defined`) on the server
- **Symptom**: Build/runtime error during Next.js SSR/RSC, Remix loader, Astro SSR, SvelteKit prerender, or `next build` — the stack trace points at `transcodes.*` or `window.transcodes.*`.
- **Cause**: The SDK call ran in a server context (Node) where neither the `transcodes` global nor `window` exists. Common triggers:
- Bare `transcodes.token.isAuthenticated()` at the top of a `'use client'` file (still rendered on the server during SSR pass).
- SDK call in a Server Component / RSC body.
- SDK call in `getServerSideProps`, `getStaticProps`, Remix `loader` / `action`, SvelteKit `+page.server.ts`, or Astro frontmatter.
- SDK call in module top-level of any file imported by an SSR route.
- **Why TypeScript missed it**: `transcodes.d.ts` declares `var transcodes: Window['transcodes']` as always-present. The type system has no way to know the global is missing on the server.
- **Fix**:
- **Move the call into a browser-only path** — `useEffect`, `useLayoutEffect`, an event handler, or a post-mount callback.
- **Or guard the call**: `if (typeof window !== 'undefined' && window.transcodes) { ... }` — works in module top-level too.
- For Next.js, ensure the file starts with `'use client'` AND the call is inside `useEffect` (not the render body).
- See **Framework-specific access pattern** for the per-framework matrix.
### Script tag has `type="module"` or `crossorigin` / `crossOrigin`
- **Cause**: Old documentation or muscle memory from the prior recommendation.
- **Fix**: Remove both attributes. The canonical tag is `` — `defer` is the only attribute besides `src`.
### TypeScript compile error: `Property 'transcodes' does not exist on type 'Window & typeof globalThis'`
- **Cause A**: `transcodes.d.ts` is not in the repo at all. **This file is mandatory for the CDN static path — you must create it if it is missing.**
- **Cause B**: `transcodes.d.ts` exists, but `tsconfig.json` was never edited, so the compiler does not pick it up.
- **Cause C**: `tsconfig.json` was edited but the directory is on `typeRoots` only OR `include` only — both are required.
- **Fix (sequence — execute every step, do not skip)**:
1. `mkdir -p types && curl -o types/transcodes.d.ts https://www.transcodes.io/instructions/types`
2. Edit `tsconfig.json` so `types` appears in **both** `include` AND `typeRoots`:
```jsonc
{
"compilerOptions": { "typeRoots": ["./node_modules/@types", "./types"] },
"include": ["src", "types"]
}
```
3. `npx tsc --noEmit` — if the error is gone, you are done.
- See **CDN TypeScript setup** for the canonical procedure.
### TypeScript error: `window.transcodes` has no type / implicit `any` (non-strict mode)
- **Cause**: Same as above (missing `transcodes.d.ts` or unedited `tsconfig.json`). Without `strict`/`noImplicitAny` the compiler does not crash, but every SDK call site falls back to `any` — you lose autocomplete and type checks.
- **Fix**: Same 3-step sequence as the compile error above.
### `init is not a function` or calling `init()` breaks the CDN static flow
- **Cause**: `init()` is only for the npm SDK fallback path. The CDN static path bootstraps automatically — `init()` must NOT be called.
- **Fix**: Remove the `init()` call. Confirm the integration is CDN static (a `