# 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 `