# Transcodes AI Integration Guide --- ## [Table of Contents] ### Agent directives & rules - [Global Agent Directives] — security, parameter resolution, video references - [Global Implementation Rules] — try-catch, loading state, await - [Code Contract] — mandatory rules every code block must satisfy - [Exhaustive Modal API List] — only 4 modals exist - [Quick Use Cases] — when to call which modal - [Common Session Rule] — session requirements per API - [Blocking Parameter Rule] — stop and ask for allowedRoles / resource / action - [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 ### 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) - PWA — HTML + service worker (required) - DO NOT list — prohibited actions - Required wiring - React (Vite / CRA) / Vue.js - Next.js App Router — native ` ``` `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 load, all APIs are available via `window.transcodes` (or just `transcodes`). ### Integration Option B — @bigstrider/transcodes-sdk Install the npm package. Call `init()` only on the client. ```bash npm install @bigstrider/transcodes-sdk ``` ```typescript import { init } from '@bigstrider/transcodes-sdk'; // Call once at client entry (e.g. main.tsx, layout.tsx with 'use client') await init({ projectId: 'RESOLVED_PROJECT_ID' }); // Optional: await init({ projectId: '...', customUserId: 'uid_xxx', debug: true }); ``` Then use **named exports** — not `window.transcodes`: ```typescript import { init, // await init({ projectId, customUserId?, debug? }) openAuthLoginModal, // await openAuthLoginModal({ webhookNotification? }) openAuthConsoleModal, // await openAuthConsoleModal() openAuthAdminModal, // await openAuthAdminModal({ allowedRoles }) openAuthIdpModal, // await openAuthIdpModal({ resource, action }) isAuthenticated, // await isAuthenticated() → boolean (ASYNC!) getCurrentMember, // await getCurrentMember() → Promise { success, member, error? } getAccessToken, // await getAccessToken() → string | null hasToken, // hasToken() → boolean (SYNC) signOut, // await signOut({ webhookNotification? }) getMember, // await getMember({ email?, memberId? }) → ApiResponse on, off, // on('AUTH_STATE_CHANGED', cb) → unsubscribeFn trackUserAction, // await trackUserAction({ tag, severity?, ... }, opts?) isPwaInstalled, // isPwaInstalled() → boolean (SYNC) } from '@bigstrider/transcodes-sdk'; ``` **CDN vs @bigstrider/transcodes-sdk naming:** | Action | CDN (window.transcodes) | @bigstrider/transcodes-sdk (named export) | | ---------------------- | ------------------------------------- | ------------------------ | | Init | **Do not call `init()`** — not required | `init({ projectId })` (required on client) | | Login modal | `transcodes.openAuthLoginModal({})` | `openAuthLoginModal({})` | | Is authenticated? | `transcodes.token.isAuthenticated()` | `isAuthenticated()` | | Get current member | `transcodes.token.getCurrentMember()` → `GetCurrentMemberResult` | `getCurrentMember()` — same; check `success` then `member` | | Get access token | `transcodes.token.getAccessToken()` | `getAccessToken()` | | Sign out | `transcodes.token.signOut()` | `signOut()` | | Lookup member | `transcodes.member.get({ email })` | `getMember({ email })` | | Subscribe to events | `transcodes.on('EVENT', cb)` | `on('EVENT', cb)` | --- ## TranscodesInitOptions (@bigstrider/transcodes-sdk only) ```typescript interface TranscodesInitOptions { projectId: string; // Required — from Transcodes Console customUserId?: string; // Optional — associate with your own user ID debug?: boolean; // Optional — enable debug logging } ``` --- ## Dynamic SDK utility methods (CDN and @bigstrider/transcodes-sdk) ```typescript // Check if SDK is already initialized (avoid double-init) transcodes.isInitialized(): boolean // Update config at runtime (e.g. set memberId after login) transcodes.setConfig({ memberId?: string }): void // Build info for debugging transcodes.getBuildInfo(): { buildTimestamp: string } ``` ```javascript // CDN path: do NOT call transcodes.init() — webworker.js initializes the SDK from the URL. // npm path: use init({ projectId }) once at client entry (see Integration Option B). // Optional — link your own member ID after login (CDN or npm) transcodes.setConfig({ memberId: 'your_internal_user_id' }); ``` --- ## 1. Core API: openAuthLoginModal (Login & Signup) ### Usage - Showing sign in / sign up - Restoring session after expiry - Guarding protected routes ### [Parameters] | Param | Type | Req | Note | | :--- | :--- | :--- | :--- | | webhookNotification | boolean | N | Send Slack webhook on login. Default: false | | projectId | string | N | Optional if inferred. | ### [Agent Instructions: Implementation Logic] - **Mandatory Check**: Always check `result.success` before processing the payload. - **Token Extraction**: Extract the JWT token and member object from `result.payload[0]`. - **State Update**: After a successful login, update the application's authentication state using the returned `member` data. ### [Framework Integration: Must-Know] All four Transcodes modals are Web Components rendered inside Shadow DOM. When calling them from any framework (React, Vue, Angular, etc.) combined with UI component libraries, apply these rules: 1. **Close parent overlays before opening a modal.** If the trigger button is inside a dropdown, popover, drawer, or dialog, close it first. These components implement focus traps that block Shadow DOM elements from receiving keyboard input. ```typescript const handleOpenLogin = async () => { setMenuOpen(false); // close the parent dropdown / dialog / popover await new Promise(r => setTimeout(r, 0)); // one tick — lets the focus trap unmount await window.transcodes.openAuthLoginModal({}); }; ``` 2. **Use `composedPath()` for event targets.** `e.target` is retargeted to the custom element host at the shadow boundary. Use `e.nativeEvent.composedPath()[0]` (React) or `e.composedPath()[0]` (vanilla) to get the real inner element. 3. **Await `el.updateComplete` before reading Lit shadow DOM.** React and Lit have independent render cycles. Setting a property on a Lit element and immediately reading its shadow DOM returns stale data. 4. **Isolate inherited CSS.** Shadow DOM does not block inherited properties (`font-family`, `color`, etc.). If global resets or `!important` rules break modal appearance, apply `:host { all: initial; }` in the Lit component's styles, then selectively restore needed properties. ### [Implementation] ```typescript // Login flow implementation async function handleLogin() { let isLoading = true; try { const result = await openAuthLoginModal({}); // check success before accessing payload if (!result.success) { // User cancelled or auth failed — do NOT access payload return; } // SAFE: only access payload after success check const { token, member } = result.payload[0]; // token = JWT string, member = { id, email, role, ... } // Update your app state with member data here } catch (err) { console.error('Login error:', err); // Show user-friendly error message } finally { isLoading = false; } } ``` ```typescript // Login guard implementation async function ensureAuthenticated() { try { const isAuth = await isAuthenticated(); if (isAuth) return true; const result = await openAuthLoginModal({}); if (!result.success) return false; // user cancelled return true; } catch (err) { console.error('Auth check failed:', err); return false; } } ``` ```typescript // Re-auth on session expiry on('TOKEN_EXPIRED', async () => { try { const result = await openAuthLoginModal({}); if (result.success) window.location.reload(); } catch (err) { console.error('Re-auth failed:', err); window.location.href = '/login?reason=session_expired'; } }); ``` ### Server-side JWT Verification When you create an Authentication Cluster in the Transcodes Console, you can download or copy a `public_key.json` file. Place this key on your server to verify JWTs. The algorithm is `ES256` (ECDSA P-256). `public_key.json` format: ```json { "kty": "EC", "x": "V5UGkr1FKv3TGd1OO3lC9YDHfizkkpO0bOjQQLBr-wc", "y": "fqhfcdO6mgmZwoEuaIeVKCAr-sdEa4Ghyn1-ESvEl1g", "crv": "P-256", "alg": "ES256", "kid": "64aaaf01-65d0-4c22-9dcd-3c1f9b3acb7f" } ``` **Option A — File-based:** Save `public_key.json` to your server project and read it at startup. ```typescript import * as jose from 'jose'; import fs from 'fs'; const jwk = JSON.parse(fs.readFileSync('./public_key.json', 'utf-8')); const publicKey = await jose.importJWK(jwk, 'ES256'); async function verifyToken(token: string) { const { payload } = await jose.jwtVerify(token, publicKey); return payload; } ``` **Option B — Env variable or inline:** Embed the `public_key.json` contents in an environment variable or directly in code. ```typescript import * as jose from 'jose'; const jwk = { kty: 'EC', x: 'V5UGkr1FKv3TGd1OO3lC9YDHfizkkpO0bOjQQLBr-wc', y: 'fqhfcdO6mgmZwoEuaIeVKCAr-sdEa4Ghyn1-ESvEl1g', crv: 'P-256', alg: 'ES256', kid: '64aaaf01-65d0-4c22-9dcd-3c1f9b3acb7f', }; const publicKey = await jose.importJWK(jwk, 'ES256'); async function verifyToken(token: string) { const { payload } = await jose.jwtVerify(token, publicKey); return payload; } ``` **Express middleware example:** ```typescript async function authMiddleware(req, res, next) { const token = req.headers.authorization?.replace('Bearer ', ''); if (!token) return res.status(401).json({ error: 'No token' }); try { req.user = await verifyToken(token); next(); } catch { return res.status(401).json({ error: 'Invalid token' }); } } app.use('/api/', authMiddleware); ``` The public key is available for download or copy when creating an Authentication Cluster in the Console. The JWK values above are samples — replace them with your project's actual key. --- ## 2. Core API: openAuthConsoleModal (Member Account Management) ### Usage - Opening user settings / profile - Managing passkeys and backup auth methods - Letting members self-manage identity data ### [Parameters] | Param | Type | Req | Note | | :--- | :--- | :--- | :--- | | projectId | string | N | Optional if inferred. | ### [Agent Instructions: Implementation Logic] - **Auth Requirement**: Ensure the user is authenticated (`isAuthenticated() === true`) before calling this modal. If not, prompt them to log in first. - **NEVER call `openAuthConsoleModal()` without an auth guard.** It will fail silently or show an error if the user has no session. ### [Framework Integration: Must-Know] Same rules as `openAuthLoginModal` — close parent overlays before opening, use `composedPath()` for events, await `updateComplete` for Lit DOM reads, isolate inherited CSS. See `openAuthLoginModal` → [Framework Integration: Must-Know] for full details and code examples. ### [Implementation] ```typescript // Console modal implementation async function openSettings() { let isLoading = true; try { // auth guard const isAuth = await isAuthenticated(); if (!isAuth) { const login = await openAuthLoginModal({}); if (!login.success) return; // user cancelled login } await openAuthConsoleModal(); // Modal closed — user may have updated passkeys, name, etc. // RECOMMENDED: refresh member data after console closes const current = await getCurrentMember(); if (current.success && current.member?.email) { const fresh = await getMember({ email: current.member.email }); if (fresh.success) { // Update your app state with fresh.payload[0] } } } catch (err) { console.error('Console modal error:', err); } finally { isLoading = false; } } ``` ## 3. Core API: openAuthAdminModal (Admin Management Panel) ### Usage - Opening team settings / admin settings - Inviting members - Changing member roles - Viewing organization directory ### [Parameters] | Param | Type | Req | Note | | :--- | :--- | :--- | :--- | | allowedRoles | string[] | Y | Existing Console role names only | | projectId | string | N | Optional if inferred. | ### [What is `allowedRoles`?] `allowedRoles` is the open-gate for the admin modal. - Values must exactly match role names that already exist in Console → RBAC → Roles - If the member role is not included, the modal does not open - `allowedRoles` controls **who may open** - RBAC permissions control **what they may do after opening** ### [Agent Instructions: Implementation Logic] - **DO NOT hardcode 'allowedRoles'** unless the user explicitly specifies which roles. - **Mandatory Question**: Before generating code, STOP and ask: *"Which roles in your Transcodes Console project should be allowed to access this admin panel?"* - **Contextual Mapping**: If the user says "only for owners" or "supervisors," map this to the role name they actually created in the Console (e.g. `allowedRoles: ['admin']`). - **Pre-requisite Check**: Remind the user that roles must be pre-configured in the Console → RBAC → Roles. Passing a non-existent role name will always block access. - **MCP First**: If Transcodes MCP / Console access is available, resolve roles there first (`get_roles`) before asking the user. - **Blocking Rule**: If `allowedRoles` is still unknown, output only the clarifying question and stop. Do not generate placeholder code, pseudo-code, or guessed defaults. ### [Framework Integration: Must-Know] Same rules as `openAuthLoginModal` — close parent overlays before opening, use `composedPath()` for events, await `updateComplete` for Lit DOM reads, isolate inherited CSS. See `openAuthLoginModal` → [Framework Integration: Must-Know] for full details and code examples. ### [Implementation] ```typescript // Admin modal implementation // Resolve allowedRoles from MCP / Console first. If unavailable, ask the user and stop. async function openAdminPanel() { let isLoading = true; try { // auth guard const isAuth = await isAuthenticated(); if (!isAuth) { const login = await openAuthLoginModal({}); if (!login.success) return; } const allowedRoles = resolvedAllowedRoles; // resolved via get_roles or explicit user input // client-side role check before opening modal const memberRes = await getCurrentMember(); if (!memberRes.success || !memberRes.member) { // Show permission denied message alert('You do not have permission to access this panel.'); return; } // allowedRoles must match roles that exist in Console await openAuthAdminModal({ allowedRoles }); } catch (err) { console.error('Admin panel error:', err); } finally { isLoading = false; } } ``` ## 4. Core API: openAuthIdpModal (Step-up MFA) ### Usage - Protecting sensitive operations - Requiring MFA before action - Enforcing RBAC + step-up on privileged flows ### [Parameters] | Param | Type | Req | Note | | :--- | :--- | :--- | :--- | | resource | string | Y | Existing Console resource key only | | action | 'create'\|'read'\|'update'\|'delete' | Y | 'create'\|'read'\|'update'\|'delete' | | forceStepUp | boolean | N | Force MFA regardless of RBAC. Default: false | | webhookNotification | boolean | N | Send Slack webhook on result. Default: false | | projectId | string | N | Optional if inferred. | ### [What are `role` and `resource`?] #### RBAC meaning - `resource` = Console RBAC resource key such as `'system'`, `'billing'`, `'reports'` - `action` = `create | read | update | delete` - `role` = current member role from JWT / `getCurrentMember()` when `result.success` and `result.member` is set - `openAuthIdpModal` reads the current member role automatically; you only pass `resource` + `action` - Permission levels: - `0` = deny - `1` = allow - `2` = allow + step-up required #### RBAC example ``` Console RBAC setup: Role: admin Resource: system create → permission: 2 (allow + step-up required) read → permission: 1 (allow, no step-up) update → permission: 1 delete → permission: 2 (allow + step-up required) ``` Result: - `admin × system × create = 2` → step-up required - `admin × system × read = 1` → allowed without step-up - On success, client sends `sid` to its backend, and the backend verifies it before executing the action **IMPORTANT:** `resource` must match a resource key created in the Console. Passing an unknown resource key will return a deny result. If `forceStepUp: true` is passed, step-up is always triggered regardless of RBAC permission level. ### [Agent Instructions: Implementation Logic] - **Mandatory Question**: Before generating code, STOP and ask: *"What is the `resource` key (defined in your Console RBAC) and what `action` are you protecting?"* - **Success Check**: You must check outer `result.success` before accessing `result.payload[0]`, and you must check `result.payload[0]?.success` before using `sid`. - **Backend Verification**: The returned `sid` (session ID) must be sent to the user's own backend, which then calls the Transcodes API (`https://api.transcodesapis.com/v1/auth/temp-session/step-up/{sid}`) with the project's API key to verify it. Client-side `sid` alone is NOT sufficient — server-side verification is mandatory. - **Runtime Gate**: For step-up MFA, use a two-step gate: outer `ApiResponse.success` first, then `result.payload[0]?.success`. - **Audit Trail**: After a successful step-up operation, you SHOULD log the action with `trackUserAction` for compliance. - **MCP First**: If Transcodes MCP / Console access is available, resolve resources there first (`get_resources`) before asking the user. - **Blocking Rule**: If `resource` or `action` is still unknown, output only the clarifying question and stop. Do not generate placeholder code, pseudo-code, or guessed defaults. ### [Framework Integration: Must-Know] Same rules as `openAuthLoginModal` — close parent overlays before opening, use `composedPath()` for events, await `updateComplete` for Lit DOM reads, isolate inherited CSS. See `openAuthLoginModal` → [Framework Integration: Must-Know] for full details and code examples. ### [Implementation] This is the canonical pattern for step-up MFA. Every step is mandatory. **Client-side (browser):** ```typescript // Step-up MFA flow implementation // Resolve resource/action from MCP / Console first. If unavailable, ask the user and stop. async function protectedAction(userId: string) { let isLoading = true; try { const resource = resolvedResource; // resolved via get_resources or explicit user input const action = resolvedAction; // resolved from the protected operation // STEP 1: Request step-up verification const mfa = await openAuthIdpModal({ resource, action, }); // SUCCESS CHECKS before using sid if (!mfa.success || !mfa.payload[0]?.success) { // User cancelled, failed verification, or was denied by RBAC return; } // Send session ID + JWT to YOUR backend for verification const sessionId = mfa.payload[0].sid; const token = await getAccessToken(); await fetch(`/api/users/${userId}`, { method: 'DELETE', headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ sessionId }), }); // STEP 4: Audit log on success await trackUserAction({ tag: 'user:delete', severity: 'high', status: true, metadata: { userId }, }); } catch (err) { // STEP 5: Error handling + audit log on failure console.error('Step-up action failed:', err); await trackUserAction({ tag: 'user:delete', severity: 'high', status: false, error: (err as Error).message, metadata: { userId }, }); } finally { isLoading = false; } } ``` **Server-side (your backend) — verify session ID against Transcodes API:** ```typescript // Backend verification example async function handleDeleteUser(req, res) { try { const { sessionId } = req.body; // Verify the step-up session ID with Transcodes API const verification = await fetch( `https://api.transcodesapis.com/v1/auth/temp-session/step-up/${sessionId}`, { headers: { 'x-api-key': process.env.TRANSCODES_API_KEY, // resolved project API key }, } ); if (!verification.ok) { return res.status(403).json({ error: 'Step-up verification failed' }); } // Verification passed — safe to execute the sensitive action await deleteUser(req.params.userId); return res.status(200).json({ success: true }); } catch (err) { return res.status(500).json({ error: 'Verification request failed' }); } } ``` ``` Step-up verification endpoint: URL: https://api.transcodesapis.com/v1/auth/temp-session/step-up/{sessionId} Method: GET Header: x-api-key: Response: 200 = valid session, non-200 = invalid/expired ``` ```typescript // Canonical step-up call shape: await openAuthIdpModal({ resource: resolvedResource, action: resolvedAction }); ``` Full `IdpAuthResponse` type reference: ```typescript interface IdpAuthResponse { success: boolean; // step-up result inside the payload item sid?: string; // step-up session ID (present on success) error?: string; // error message (present on failure) timestamp: number; // Unix timestamp (ms) action?: 'create' | 'read' | 'update' | 'delete'; // the requested action } // Returned as ApiResponse // Runtime gate: check outer ApiResponse.success first, then check result.payload[0]?.success, then use sid ``` // Permission matrix (set per role × resource × action in Console): // 0 = deny 1 = allow (no step-up) 2 = allow + step-up required // openAuthIdpModal reads the current member's role and checks this matrix automatically. --- ## 5. Core API: Token API (Session Management) ### Usage - Sending JWT to your backend - Guarding protected routes - Reading current member from JWT - Signing out ### [Methods] | Method | Returns | Note | | :--- | :--- | :--- | | `getCurrentMember()` | `Promise` | `{ success, member, error? }` — on success `member` is set; on failure `member` is null and `error` is a `GetCurrentMemberFailureReason`. | | `getAccessToken()` | `Promise` | Gets valid JWT, refreshing if needed. | | `hasToken()` | `boolean` | Sync check if token exists in memory. | | `isAuthenticated()` | `Promise` | Async full validity check. | | `signOut(options?)` | `Promise` | Clears session. `options.webhookNotification?: boolean` | ### [Agent Instructions: Implementation Logic] - **getCurrentMember**: Always read `const r = await getCurrentMember()` then branch on `r.success` before using `r.member`. Do not treat the return value as `Member | null`. - **Header Injection**: When making fetch calls to a backend, always use `await getAccessToken()` to ensure the token is fresh. - **isAuthenticated vs hasToken**: Use `hasToken()` for fast UI rendering decisions. Use `isAuthenticated()` for security-critical decisions (it verifies token validity, not just existence). - **NEVER use `isAuthenticated()` without `await`** — it returns a Promise, which is always truthy. This is the #1 most common bug. ### [Implementation] **Authorized Fetch Wrapper — use this for ALL backend API calls:** ```typescript async function authorizedFetch(url: string, options: RequestInit = {}) { const token = await getAccessToken(); if (!token) throw new Error('Not authenticated'); return fetch(url, { ...options, headers: { ...options.headers, Authorization: `Bearer ${token}` }, }); } ``` **Route Guard — use `isAuthenticated()` (async) for security decisions:** ```typescript const isAuth = await isAuthenticated(); if (!isAuth) { window.location.href = '/login'; return; } ``` **UI Rendering — use `hasToken()` (sync) for non-security UI toggle:** ```typescript if (hasToken()) { showDashboard(); } else { showLandingPage(); } ``` **Sign Out:** ```typescript try { await signOut(); window.location.href = '/'; } catch (err) { console.error('Sign out failed:', err); } ``` **Get Current User:** ```typescript const r = await getCurrentMember(); if (r.success && r.member) { const { member } = r; console.log(`${member.name} (${member.email}) — role: ${member.role}`); } else if (!r.success) { // r.member is null; r.error is GetCurrentMemberFailureReason when applicable } ``` --- ## 6. Core API: Member API (User Data) ### Usage - Refreshing profile after console changes - Re-checking the latest role from the API - Looking up a member by email or ID ### [Parameters] | Param | Type | Req | Note | | :--- | :--- | :--- | :--- | | projectId | string | N | Optional if inferred. | | memberId | string | N | Specific member ID to fetch. | | email | string | N | Specific email to fetch. | | fields | string | N | Comma-separated field names to return. | ### [Implementation] ```typescript // getMember implementation async function lookupMember(email: string) { try { const res = await getMember({ email }); // check success AND payload length if (!res.success || !res.payload?.length) { console.log('Member not found or request failed'); return null; } return res.payload[0]; // { id, email, role, name, ... } } catch (err) { console.error('Member lookup failed:', err); return null; } } ``` ```typescript // getCurrentMember() returns { success, member, error? }; member comes from JWT when success (may be stale) // getMember() fetches from API (always fresh) // Use getMember() when you need the latest role after a change const res = await getMember({ email: 'alice@example.com' }); if (res.success && res.payload.length > 0) { const role = res.payload[0].role; } ``` --- ## 7. Core API: Events API (Real-time State) ### Usage - Syncing auth state into app state - Handling token expiry - Monitoring SDK errors ### [Events] | Event | Payload | Note | | :--- | :--- | :--- | | `AUTH_STATE_CHANGED` | `{ isAuthenticated, accessToken, expiresAt, member }` | Fired on login, logout, restore. | | `TOKEN_REFRESHED` | `{ accessToken, expiresAt }` | Fired when new JWT is issued. | | `TOKEN_EXPIRED` | `{ expiredAt }` | Fired when session fully expires. | | `ERROR` | `{ code, message, context? }` | Fired on background errors. | ### [Implementation] **Always store and call the unsubscribe function on cleanup.** Failing to unsubscribe causes memory leaks and duplicate event handlers. ```typescript // Subscribe with proper cleanup (React example) useEffect(() => { const unsub = on('AUTH_STATE_CHANGED', (payload) => { if (payload.isAuthenticated) { setUser(payload.member); } else { setUser(null); } }); // unsubscribe on unmount return () => unsub(); }, []); ``` ```typescript // Auto-re-auth on token expiry with error handling const unsub = on('TOKEN_EXPIRED', async () => { try { const result = await openAuthLoginModal({}); if (!result.success) { window.location.href = '/login?reason=session_expired'; } } catch (err) { console.error('Re-auth failed:', err); window.location.href = '/login?reason=error'; } }); ``` ```typescript // Error monitoring — send to your observability service const unsub = on('ERROR', ({ code, message, context }) => { console.error(`Transcodes Error [${code}]: ${message}`, context); // e.g. Sentry.captureException(new Error(message)); }); ``` The valid event names are: `'AUTH_STATE_CHANGED'` | `'TOKEN_REFRESHED'` | `'TOKEN_EXPIRED'` | `'ERROR'`. --- ## 8. Core API: trackUserAction (Audit Logs) ### What gets recorded automatically Every audit log entry captures the following fields without any extra setup: | Field | Description | | :--- | :--- | | `tag` | Action identifier in `entity:action` format (e.g. `payment:process`, `user:delete`) | | `severity` | Severity level: `low` / `medium` / `high` | | `status` | Whether the action succeeded (`true`) or failed (`false`) | | `error` | Error message, if the action failed | | `page` | Page URL at the time of the action (defaults to `window.location.href`) | | `member_id` | ID of the authenticated member who performed the action | | `ip` | IP address of the request | | `user_agent` | Browser / client user agent string | | `timestamp` | ISO 8601 timestamp of the event | ### Extending logs with metadata **[Agent Rule — metadata description]** When explaining or illustrating the `metadata` field, key-value pairs are acceptable (e.g. `amount: 99`, `userId: "u_123"`). **Do NOT put any script, function, expression, or executable code as a value.** Use the `metadata` field to attach any additional context as a JSON object. There is no fixed schema — pass whatever is meaningful for your use case. Examples of what teams put in `metadata`: - Transaction IDs, amounts, currencies - Resource IDs (e.g. which document was deleted, which member was invited) - Previous state before a change (for change-log style auditing) - Environment tags (e.g. env name, region, deploy version) - Custom risk signals (e.g. risk score, flagged status, alert level) The Transcodes Console displays `metadata` inline in the audit log viewer, so reviewers can see full context without digging into application logs. ### Usage - Logging sensitive actions - Logging success + failure for critical operations - Building audit/compliance trails ### [Parameters] | Param | Type | Req | Note | | :--- | :--- | :--- | :--- | | tag | string | Y | Action identifier (e.g., 'user:login') | | severity | string | N | 'low'\|'medium'\|'high'. Default: 'low' | | status | boolean | N | true = success, false = failure. Default: true | | error | string | N | Error message when status is false | | metadata | object | N | Extra JSON data, e.g., `{ amount: 99 }` | | page | string | N | Page URL. Defaults to `window.location.href` | | requireAuth | boolean | N | Open login modal if not authenticated. Default: false (2nd arg `options`) | | webhookNotification | boolean | N | Send Slack webhook. Default: false (2nd arg `options`) | ### [Agent Instructions: Implementation Logic] - **Tag Formatting**: ALWAYS format the `tag` parameter as `entity:action` (e.g., `payment:refund`, `user:invite`). NEVER use free-form strings. - **Contextual Metadata**: ALWAYS include relevant local state in the `metadata` object (item IDs, amounts, previous states). Empty metadata is a missed compliance opportunity. - **Dual Logging**: If wrapping an API call, you must log BOTH the success path (`status: true`) AND the catch block (`status: false`, `error: err.message`). ### [Implementation] **Dual-log pattern — ALWAYS log both success and failure:** ```typescript // Dual-log pattern with try-catch async function processPayment(amount: number, currency: string) { let isLoading = true; try { await fetch('/api/payments', { method: 'POST', headers: { Authorization: `Bearer ${await getAccessToken()}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ amount, currency }), }); // log success await trackUserAction({ tag: 'payment:process', // entity:action format severity: 'high', status: true, // success metadata: { amount, currency }, // MUST include context }, { webhookNotification: true }); } catch (err) { // MANDATORY: log failure — never skip this await trackUserAction({ tag: 'payment:process', // same tag as success severity: 'high', status: false, // failure error: (err as Error).message, metadata: { amount, currency }, }, { webhookNotification: true }); } finally { isLoading = false; } } ``` Use `entity:action` tags such as `payment:process`, `user:delete`, `member:role-change`. Always log both success and failure paths for sensitive operations. --- ## Type Definitions ```typescript // --- Core Types --- interface Member { id?: string; projectId?: string; name?: string; email?: string; role?: string; metadata?: Record; createdAt?: Date | string; updatedAt?: Date | string; } interface AuthResult { token: string; // JWT access token member: Member; } interface ApiResponse { success: boolean; payload: T; error?: string; message?: string; status?: number; } // --- Init --- interface TranscodesInitOptions { projectId: string; // Required — from Transcodes Console customUserId?: string; // Optional — associate with your own user ID debug?: boolean; // Optional — enable debug logging } interface TranscodesBuildInfo { buildTimestamp: string; // ISO timestamp when SDK bundle was built } // --- IDP (Step-up MFA) --- interface IdpOpenParams { resource: string; // Resource key from Console RBAC action: 'create' | 'read' | 'update' | 'delete'; forceStepUp?: boolean; // Force step-up regardless of permission level webhookNotification?: boolean; // Send Slack webhook on result } interface IdpAuthResponse { success: boolean; // step-up result inside the payload item sid?: string; // Step-up session ID (present on success) error?: string; // Error message (present on failure) timestamp: number; // Unix timestamp (ms) action?: string; // The requested action } // --- Modal Return Types --- // openAuthLoginModal(params) → Promise> // openAuthConsoleModal(params?) → Promise> // openAuthAdminModal(params) → Promise> // openAuthIdpModal(params) → Promise> // --- Token API --- // Matches public/transcodes.d.ts (also exported from @bigstrider/transcodes-sdk types) export enum GetCurrentMemberFailureReason { // If member is not authenticated, the error is 'unauthenticated' UNAUTHENTICATED = 'unauthenticated', // If member is not found, the error is 'not_found' NOT_FOUND = 'not_found', // If member is revoked, the error is 'revoked' REVOKED = 'revoked', // If member is transient network error, the error is 'transient' TRANSIENT = 'transient', // If member is other error, the error is 'error' ERROR = 'error', } export interface GetCurrentMemberResult { success: boolean; member: Member | null; error?: GetCurrentMemberFailureReason; } interface TokenAPI { getCurrentMember(): Promise; getAccessToken(): Promise; hasToken(): boolean; // SYNC isAuthenticated(): Promise; // ASYNC — always use await signOut(options?: { webhookNotification?: boolean }): Promise; } // --- Member API --- interface PublicMemberAPI { get(params: { projectId?: string; memberId?: string; email?: string; fields?: string; // comma-separated field names }): Promise>; } // --- Audit Log --- // trackUserAction(event, options?) → Promise // event: // tag: string — required, entity:action format (e.g. 'payment:process') // severity?: 'low' | 'medium' | 'high' — default: 'low' // status?: boolean — default: true (true = success, false = failure) // error?: string — error message when status is false // metadata?: Record — extra context // page?: string — page URL (defaults to window.location.href) // options: // requireAuth?: boolean — open login modal if not authenticated (default: false) // webhookNotification?: boolean — send Slack webhook (default: false) // --- Events --- type TranscodesEventName = | 'AUTH_STATE_CHANGED' | 'TOKEN_REFRESHED' | 'TOKEN_EXPIRED' | 'ERROR'; interface AuthStateChangedPayload { isAuthenticated: boolean; accessToken: string | null; expiresAt: number | null; member: Member | null; } interface TokenRefreshedPayload { accessToken: string; expiresAt: number; } interface TokenExpiredPayload { expiredAt: number; } interface ErrorPayload { code: string; message: string; context?: string; } // on(event: TranscodesEventName, callback: (payload) => void): () => void — returns unsubscribe fn // off(event: TranscodesEventName, callback): void ``` --- ## Framework setup (quick reference) ### React + Vite Put the script in **`index.html` ``** (native ` ``` ```bash # .env VITE_TRANSCODES_PROJECT_ID=proj_abc123xyz ``` ### Next.js App Router (CDN path) Use a **native `

Not signed in

``` --- ## CDN TypeScript setup When you integrate **authentication via CDN** (`webworker.js` in HTML), fetch **https://www.transcodes.io/transcodes.d.ts**, save the response as a `transcodes.d.ts` file in your repo (e.g. `types/transcodes.d.ts`), and **add `types/` to `tsconfig.json` when needed** (`include` / `typeRoots`) so the compiler picks up the definitions — then `window.transcodes` and SDK calls type-check. You can also copy the same file from the Transcodes Console. ```bash curl -o types/transcodes.d.ts https://www.transcodes.io/transcodes.d.ts ``` Example `tsconfig.json` wiring: ```json { "compilerOptions": { "typeRoots": ["./node_modules/@types", "./types"] }, "include": ["src", "types"] } ``` **CDN + `webworker.js` does not use `init()`** — types may still declare `init` for the npm package; ignore `init` for pure CDN loading. When using `@bigstrider/transcodes-sdk`, TypeScript types ship with the package — no extra `transcodes.d.ts` file needed. --- ## React AuthContext pattern (recommended) The standard pattern for React apps — works with both CDN and `@bigstrider/transcodes-sdk`. ```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) ```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 version) Use before any sensitive action. This example shows the CDN (`window.transcodes`) variant. The SDK variant uses named imports (see API 4 above). **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-api-key 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: 1. `projectId`, JWT issuer, and Transcodes API key are resolved or explicitly requested from the user. 2. Only the canonical modal APIs are used: - `openAuthLoginModal({ webhookNotification? })` - `openAuthConsoleModal()` - `openAuthAdminModal({ allowedRoles })` - `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-api-key: ` 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. `allowedRoles` is resolved via `get_roles` or explicit user input; never guessed. 12. `resource` is resolved via `get_resources` or explicit user input; never guessed. 13. Final code contains no unresolved placeholders. --- ## Browser support | Browser | Minimum | | ------- | ------- | | Chrome | 67+ | | Safari | 14+ | | Firefox | 60+ | | Edge | 79+ | WebAuthn requires **HTTPS** in production. `localhost` / `127.0.0.1` work for development. --- ## Scope: Transcodes vs Your App | Transcodes SDK handles | Your app should implement | | ----------------------------------- | --------------------------------------- | | Passkey credential storage | XSS / CSRF protection | | JWT issuance and refresh | Application-level authorization logic | | WebAuthn ceremony (challenge/verify)| Server-side JWT verification | | UI modals (login, console, admin) | HTTPS in production | | Token memory management | Rate limiting on your API | ### Recommended frontend implementations **XSS Protection:** - Sanitize user input before rendering (`DOMPurify` or framework-native escaping) - Set `Content-Security-Policy` headers (see CSP section above) - Avoid `innerHTML` / `dangerouslySetInnerHTML` with untrusted data **CSRF Protection:** - Use `SameSite=Strict` or `SameSite=Lax` on cookies - Add CSRF tokens on state-mutating requests if using cookie-based sessions - Transcodes JWT is stored in memory (not cookies), so XSS is the primary vector to guard against **HTTPS:** - Required for WebAuthn in production (`localhost` is exempt for development) - Use reverse proxy (nginx, Cloudflare, Vercel) or platform-managed TLS **Rate Limiting:** ```typescript // Example: express-rate-limit import rateLimit from 'express-rate-limit'; const limiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 100, // 100 requests per window per IP }); app.use('/api/', limiter); ``` --- ## Links - [Full docs](https://www.transcodes.io/docs) - [MCP Server Setup](https://www.transcodes.io/docs/mcp-server) - [API Reference](https://www.transcodes.io/docs/api-reference) - [Quick Integration](https://www.transcodes.io/docs/quick-integration) - [Console](https://app.transcodes.io) --- ## Video Tutorials **[Agent Instruction for Videos]** Append the relevant video link when generating complex setups. ### General Setup & Demos - **Organization & Project Creation** - **Description**: Shows how to create an organization and a project from the Transcodes Console. - **When to show**: When the user is just starting out, asks how to create an account, or needs to set up their first workspace. - **Link**: [Watch Video](https://player.mux.com/wS301Khp4BQzGhcNGqeRcDRtDqN600HdEXThbIIqTSK8w?metadata-video-title=getting_started&video-title=getting_started) - **Creating Auth & PWA Clusters** - **Description**: Demonstrates how to create and configure Authentication and PWA clusters within a project. - **When to show**: When the user asks how to enable auth/PWA, or is stuck on the "Console prerequisite" step. - **Link**: [Watch Video](https://player.mux.com/74dnHn2e8BRKw99r1nCH7qG7YasJjDdaeXYezgdWqBY?metadata-video-title=project_setup_node&video-title=project_setup_node) - **Official Authentication Demo** - **Description**: A complete walkthrough of the Transcodes authentication flow (passkey, step-up MFA) from an end-user perspective. - **When to show**: When the user asks "what does the login look like?" or wants to see the final product in action. - **Link**: [Watch Video](https://player.mux.com/c9GY3lWAi2E3gZzLkolhil87rg28S8dfA1SLBSc8N00k?metadata-video-title=official_demo_complete&video-title=official_demo_complete) - **Official PWA Demo** - **Description**: Shows the PWA installation process and how the app looks when installed on a device. - **When to show**: When the user asks "how does the PWA install work?" or wants to see the PWA UX. - **Link**: [Watch Video](https://player.mux.com/PA00U9vVASatqGU3iw5sFemijDKi1vskPFUjWmoXx902Q?metadata-video-title=getting_started_pwa&video-title=getting_started_pwa) ### Authentication Kit - **Customizing Modal Branding** - **Description**: How to change the logo, colors, and styling of the authentication modals via the Console. - **When to show**: When the user asks how to change the modal color, add their logo, or customize the UI. - **Link**: [Watch Video](https://player.mux.com/DbWrDD5TOC17mH2POv1Ib01Azez2fZobXDMmElMXDhbY?metadata-video-title=auth_branding&video-title=auth_branding) - **Setting up RBAC (Role-Based Access Control)** - **Description**: How to define roles, permissions, and assign them to members in the Console. - **When to show**: When the user asks about role-based access control, how to restrict access, or how to use `allowedRoles`. - **Link**: [Watch Video](https://player.mux.com/1RDl5VEY602DNguYqjVDK6PaQ00B9CD01yDcYD6HiF5mi8?metadata-video-title=auth_rbac&video-title=auth_rbac) - **Configuring Webhooks** - **Description**: How to set up Slack/Discord webhooks to get notified on user logins or actions. - **When to show**: When the user asks about notifications, Slack integrations, or webhook parameters in the API. - **Link**: [Watch Video](https://player.mux.com/YeyzMIwwN800v9zYVc8dKKrvCTy02EJ713mZJfZ1soBnw?metadata-video-title=auth_webhook&video-title=auth_webhook) - **Viewing Audit Logs** - **Description**: How to view and filter user action logs in the Transcodes Console. - **When to show**: When the user asks where to see the logs generated by `trackUserAction` or wants to monitor user activity. - **Link**: [Watch Video](https://player.mux.com/HK00WvGEok500415CmQ7TavCStVG3t3YgNHNih8K02Z9028?metadata-video-title=auth_audit&video-title=auth_audit) - **Server-side Auth (JWT Verification)** - **Description**: How to verify Transcodes JWTs on a backend server using `public_key.json` (ES256 / `jose`). - **When to show**: When the user asks how to protect their backend API or verify tokens server-side. - **Link**: [Watch Video](https://player.mux.com/qtgRGW1DB006D6JTNYIyWy01uGy014YXOufbCUYr00p1oZ4?metadata-video-title=auth_json&video-title=auth_json) - **Authentication Backup Methods** - **Description**: Setting up fallback authentication methods (like email or TOTP) when passkeys aren't available. - **When to show**: When the user asks "what if the user loses their passkey?" or wants to enable email/TOTP login. - **Link**: [Watch Video](https://player.mux.com/I4OctRguqJvaqRFE9uL6oQTIjZNBWqhrBpNzFfkJJO4?metadata-video-title=auth_backup&video-title=auth_backup) ### PWA / Web App Kit - **PWA & Auth CDN Installation (index.html)** - **Description**: How to add `manifest` + native `