`) is the ONLY default. npm SDK is a fallback.**
- [Code Contract] — mandatory rules every code block must satisfy
- [Exhaustive Modal API List] — only 3 modals exist
- [Quick Use Cases] — when to call which modal
- [Common Session Rule] — session requirements per API
- [Blocking Parameter Rule] — stop and ask for resource / action (IDP modal) when unknown
- [Placeholder Resolution Policy] — RESOLVED_* are required inputs, not final values
- [Index & Tags] — keyword → section mapping
### MCP Server Integration
- MCP Server setup — Claude Desktop, Cursor, Codex
- Available MCP tools — members, roles, resources, audit logs, permissions
- Migration Guide — moving existing users and roles to Transcodes
### 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 — PWA Setup (manifest + sw.js) — CDN static
- PWA — HTML + service worker (required, CDN static, `defer`)
- DO NOT list — prohibited actions
- Required wiring (single canonical snippet — `` + `` to the ``.
- **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. Code Contract Rule (Duplicate, see Rule 7)
- **ALWAYS** wrap API invocations in a `try-catch` block.
- **ALWAYS** use `await` for async SDK methods.
### 5. 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.
---
### [Default Integration Method — CDN STATIC IS THE ONLY DEFAULT]
1. **Always default to CDN static integration.** Add two tags to the HTML `` and create one `public/sw.js`:
```html
```
```javascript
// public/sw.js — entire file, one line
importScripts('https://cdn.transcodes.link/{RESOLVED_PROJECT_ID}/sw.js');
```
2. **The 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 `@bigstrider/transcodes-sdk` by default.** The npm SDK is a **fallback only** for the rare case where CDN static is impossible. Before falling back, ASK the user explicitly:
> "I default to CDN static integration (``**
- Do NOT add `crossorigin` / `crossOrigin` to the `webworker.js` `` in ``. **No `type="module"`. No `crossorigin`.**
**CDN host:** Production typically uses `https://cdn.transcodes.link`. Some environments use `https://dev.transcodes.link` — resolve `CDN_BASE` from Console / env (e.g. `NEXT_PUBLIC_TRANSCODES_CDN_BASE`).
**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('https://cdn.transcodes.link/{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 — `@bigstrider/transcodes-sdk` (npm) — FALLBACK ONLY
**Do not pick this unless the user explicitly asks for it.** Before generating npm code, ASK once:
> "I default to CDN static (`` in ``. Don't use `type="module"`. Don't use `crossorigin`. Don't use `next/script`. Don't install `@bigstrider/transcodes-sdk` 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 (only when user explicitly opts out of CDN static)
Generate these only after the user confirms they cannot use CDN static.
#### Next.js App Router — `@bigstrider/transcodes-sdk` (FALLBACK)
```tsx
// app/providers.tsx — 'use client'
'use client';
import { init } from '@bigstrider/transcodes-sdk';
import { useEffect } from 'react';
export function TranscodesProvider({ children }) {
useEffect(() => {
init({ projectId: process.env.NEXT_PUBLIC_TRANSCODES_PROJECT_ID! });
}, []);
return <>{children}>;
}
```
#### Vue 3 + Vite — `@bigstrider/transcodes-sdk` (FALLBACK)
```typescript
// main.ts
import { init } from '@bigstrider/transcodes-sdk';
await init({ projectId: import.meta.env.VITE_TRANSCODES_PROJECT_ID });
```
---
## Quick start example (CDN static — DEFAULT)
**This is the default. Generate this — not an npm/`init()` variant — unless the user explicitly asks otherwise.**
```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.openAuthLoginModal`, `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 `@bigstrider/transcodes-sdk`, 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 AuthContext pattern (recommended)
The standard pattern for React apps. The structure is identical for both integrations — only the imports change. **Default to the CDN static version**: read everything off the bare `transcodes` global (typed via `transcodes.d.ts`). The npm import shape below is shown for the fallback case only.
**CDN static (DEFAULT) — replace the import block with:**
```tsx
// src/context/AuthContext.tsx — CDN static
// transcodes.d.ts must be installed (see "CDN TypeScript setup")
const sdkIsAuthenticated = () => transcodes.token.isAuthenticated();
const sdkLogin = (params: { webhookNotification?: boolean }) =>
transcodes.openAuthLoginModal(params);
const sdkConsole = () => transcodes.openAuthConsoleModal();
const sdkIdp = (params: { resource: string; action: 'create' | 'read' | 'update' | 'delete' }) =>
transcodes.openAuthIdpModal(params);
const sdkSignOut = () => transcodes.token.signOut();
const on = transcodes.on.bind(transcodes);
// ...rest of AuthProvider below is identical
```
**npm fallback — only when the user has confirmed Option B:**
```tsx
// src/context/AuthContext.tsx
import { createContext, useEffect, useState, type ReactNode } from 'react';
import {
isAuthenticated as sdkIsAuthenticated,
openAuthLoginModal as sdkLogin,
openAuthConsoleModal as sdkConsole,
openAuthIdpModal as sdkIdp,
signOut as sdkSignOut,
on,
} from '@bigstrider/transcodes-sdk';
interface AuthContextValue {
isAuthenticated: boolean;
isLoading: boolean;
memberId: string | null;
openAuthLoginModal: () => Promise;
openAuthConsoleModal: () => Promise;
openAuthIdpModal: (params: {
resource: string;
action: 'create' | 'read' | 'update' | 'delete';
}) => Promise;
signOut: () => Promise;
}
export const AuthContext = createContext({ /* defaults */ } as AuthContextValue);
export function AuthProvider({ children }: { children: ReactNode }) {
const [isAuth, setIsAuth] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [memberId, setMemberId] = useState(null);
useEffect(() => {
sdkIsAuthenticated().then((auth) => {
setIsAuth(auth);
setIsLoading(false);
});
const unsubscribe = on('AUTH_STATE_CHANGED', ({ isAuthenticated, member }) => {
setIsAuth(isAuthenticated);
setMemberId(member?.id ?? null);
});
return () => unsubscribe();
}, []);
return (
{
const result = await sdkLogin({});
if (result.success) setMemberId(result.payload[0]?.member?.id ?? null);
},
openAuthConsoleModal: async () => { await sdkConsole(); },
openAuthIdpModal: async (params) => { await sdkIdp(params); },
signOut: async () => {
await sdkSignOut();
setIsAuth(false);
setMemberId(null);
},
}}>
{children}
);
}
```
Wrap your app in `` and consume with `use(AuthContext)` in components.
---
## Quick start example (`@bigstrider/transcodes-sdk` — React) — npm FALLBACK
**Generate this only when the user has explicitly chosen the npm SDK.** For the default CDN static path, use the bare `transcodes` global directly (see Quick start example above).
```tsx
// src/App.tsx
'use client'; // Next.js
import { openAuthLoginModal, on, getCurrentMember } from '@bigstrider/transcodes-sdk';
import { useEffect, useState } from 'react';
import type { Member } from '@bigstrider/transcodes-sdk';
export default function App() {
const [member, setMember] = useState(null);
useEffect(() => {
getCurrentMember().then((r) => setMember(r.success ? r.member : null));
const unsub = on('AUTH_STATE_CHANGED', (p) => setMember(p.member));
return unsub;
}, []);
const handleLogin = async () => {
const result = await openAuthLoginModal({});
if (result.success) setMember(result.payload[0].member);
};
return member
?
Hello, {member.email}
: ;
}
```
---
## 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. The npm SDK variant uses named imports (see API 4 above) and should only be used if the user explicitly chose Option B.
**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. Request step-up verification
const mfa = await transcodes.openAuthIdpModal({
resource,
action,
});
// SUCCESS CHECKS before using sid
if (!mfa.success || !mfa.payload[0]?.success) return;
// Send session ID to YOUR backend
const sessionId = mfa.payload[0].sid;
await fetch(`/api/users/${userId}`, {
method: 'DELETE',
headers: {
Authorization: `Bearer ${await transcodes.token.getAccessToken()}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ sessionId }),
});
// YOUR backend calls: GET https://api.transcodesapis.com/v1/auth/temp-session/step-up/{sessionId}
// 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. The exact API domains for `connect-src` and `frame-src` are listed in your Transcodes Console → Authentication Cluster → Installation Guide.
```html
```
---
## Release Checklist
Use this checklist before outputting final code:
0. **Integration method = CDN static** — `` in HTML ``, plus `` for PWA, plus `public/sw.js` (one-line `importScripts`). The script tag has `defer` only — **no `type="module"`, no `crossorigin`, no `async`**. The npm SDK (`@bigstrider/transcodes-sdk`) 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 canonical modal APIs are used:
- `openAuthLoginModal({ webhookNotification? })`
- `openAuthConsoleModal()`
- `openAuthIdpModal({ resource, action, forceStepUp?, webhookNotification? })`
3. All async SDK methods use `await`.
4. Every modal result checks outer `result.success` before reading `payload`.
5. `openAuthIdpModal()` checks both outer `result.success` and inner `result.payload[0]?.success` before using `sid`.
6. Step-up flows send `sessionId` to the app backend, and the backend verifies it with:
- `GET https://api.transcodesapis.com/v1/auth/temp-session/step-up/{sessionId}`
- header `x-transcodes-token: ` (server-only JWT issued from the Console; different from `TRANSCODES_TOKEN` used by MCP)
7. Protected modals are preceded by `await isAuthenticated()`.
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. 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. **openAuthLoginModal** — where it is called, which component/page, any options used (e.g. `webhookNotification`)
3. **openAuthConsoleModal** — where it is called, which component/page
4. **openAuthIdpModal** — where it is called, which `resource` and `action` values were used, whether `forceStepUp` is enabled
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
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 `` (React SPA and Next.js — no `next/script`, no `type="module"`, no `crossorigin`); 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 or asks where to put the script tag.
- **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('https://cdn.transcodes.link/{RESOLVED_PROJECT_ID}/sw.js');
```
- **If PWA is not needed**: Remove `` and `` 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 `@bigstrider/transcodes-sdk` (npm) 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 `