@mochabug/adapt-web
Version:
Browser client library for Adapt automation platform
576 lines (575 loc) • 19.4 kB
TypeScript
import type { Output, SignalValue, StatusJson } from "@mochabug/adapt-core";
import type { AdaptTheme } from "./theme.js";
export type { Output, SignalValue, StatusJson } from "@mochabug/adapt-core";
export type { AdaptTheme } from "./theme.js";
/**
* Options for session persistence across page refreshes.
*/
export interface PersistOptions {
/** Storage type: 'session' (default) uses sessionStorage, 'local' uses localStorage. */
storage?: "session" | "local";
/** Max TTL in seconds (default: 3600). Capped by the session's own expiresAt. */
ttl?: number;
/** Custom storage key suffix to distinguish multiple embeds of the same automation. */
key?: string;
}
/**
* Discriminated union for fork display configuration.
*
* - `"side-by-side"`: Shows main and fork iframes side by side
* - `"dialog"`: Shows fork in a floating group
*/
export type ForkDisplay = {
mode: "side-by-side";
split?: number;
} | {
mode: "dialog";
};
/**
* Custom label overrides for the Cap.js widget.
* All fields are optional — unspecified labels use Cap.js defaults.
*
* These map to `data-cap-i18n-*` attributes on the underlying `<cap-widget>`.
*
* @example
* ```typescript
* i18n: {
* initialState: "Verify you're human",
* verifyingLabel: "Checking...",
* solvedLabel: "Verified!",
* errorLabel: "Failed"
* }
* ```
*/
export interface CapWidgetI18n {
/** Text shown while the proof-of-work is being solved. Default: `"Verifying..."` */
verifyingLabel?: string;
/** Text shown before the user clicks the checkbox. Default: `"Verify you're human"` */
initialState?: string;
/** Text shown after a successful solve. Default: `"You're a human"` */
solvedLabel?: string;
/** Text shown when solving fails. Default: `"Error. Try again."` */
errorLabel?: string;
/** Text shown for the blocked-user troubleshooting link. Default: `"Troubleshoot"` */
troubleshootingLabel?: string;
/** Text shown when WebAssembly is unavailable. */
wasmDisabled?: string;
/** ARIA label for the initial interactive state. */
verifyAriaLabel?: string;
/** ARIA label while verification is running. */
verifyingAriaLabel?: string;
/** ARIA label after successful verification. */
verifiedAriaLabel?: string;
/** Validation text when the widget is required in a form. */
requiredLabel?: string;
/** ARIA label when an error occurs. */
errorAriaLabel?: string;
}
/**
* Options for the Cap.js widget when used via `AdaptWebClient` with `requiresChallenge: true`.
*
* @example
* ```typescript
* capWidgetOptions: {
* workerCount: 4,
* i18n: { initialState: "Click to verify" }
* }
* ```
*/
export interface CapWidgetOptions {
/** Custom text labels for the widget. */
i18n?: CapWidgetI18n;
/** Number of Web Workers for solving. @default navigator.hardwareConcurrency || 8 */
workerCount?: number;
/** Name of the hidden token input generated by Cap.js. @default `"cap-token"` */
hiddenFieldName?: string;
/** Custom URL for Cap.js's troubleshooting link when instrumentation blocks a user. */
troubleshootingUrl?: string;
/** Disable Cap.js haptic feedback for this widget. */
disableHaptics?: boolean;
}
/**
* Challenge information returned from createChallenge
*/
export interface ChallengeInfo {
/** Number of challenges to solve */
count: number;
/** Salt size in bytes */
size: number;
/** Difficulty target (hex prefix length) */
difficulty: number;
/** When the challenge expires */
expires: Date;
/** Hex-encoded seed for Cap.js PRNG derivation (64 chars) */
token: string;
/** Encrypted verification token for server */
verificationToken: string;
/** Optional Cap.js instrumentation payload to execute before redeeming */
instrumentation?: string;
}
export interface RedeemChallengeOptions {
/** Successful Cap.js instrumentation result sent as `instr` by the widget */
instrumentation?: Record<string, unknown>;
/** True when Cap.js reports that instrumentation detected an automated browser */
instrumentationBlocked?: boolean;
/** True when Cap.js reports that instrumentation timed out */
instrumentationTimeout?: boolean;
}
/**
* Redeemed challenge returned from redeemChallenge
*/
export interface RedeemedChallenge {
/** The redeemed token to pass to startSession/runSession */
token: string;
/** When the redeemed token expires */
expires: Date;
}
/**
* Configuration options for AdaptWebClient
*
* @example
* ```typescript
* // Basic usage
* const client = new AdaptWebClient({
* container: 'my-container',
* automationId: 'automation-123',
* authToken: 'optional-auth-token'
* });
*
* // With fork display options
* const client = new AdaptWebClient({
* container: 'my-container',
* automationId: 'automation-123',
* forkDisplay: { mode: 'dialog' }
* });
*
* // With custom styling
* const client = new AdaptWebClient({
* container: 'my-container',
* automationId: 'automation-123',
* classNames: {
* root: 'my-custom-class',
* iframe: 'my-iframe-class'
* },
* styles: {
* height: '600px'
* }
* });
* ```
*/
export interface AdaptWebClientOptions {
/**
* HTML element or ID of the element where the client will render.
* The element must exist in the DOM before creating the client.
*
* @example
* ```html
* <div id="my-container"></div>
* ```
* ```typescript
* container: 'my-container'
* // or
* container: document.getElementById('my-container')
* ```
*/
container: string | HTMLElement;
/**
* Automation ID to connect to.
* This ID is provided by the Adapt automation platform.
*/
automationId: string;
/**
* Pre-created session token from server-side session creation.
* When provided, skips client-side startSession/inheritSession calls entirely.
*
* Use this for SSR frameworks (Next.js, Nuxt, etc.) where the authToken
* should remain on the server for security.
*
* @example
* ```typescript
* // Server-side (e.g., Next.js API route or server component)
* const { token } = await startSession({ id: 'automation-123' }, authToken);
*
* // Client-side
* const client = new AdaptWebClient({
* container: 'my-container',
* automationId: 'automation-123',
* sessionToken: token // Use pre-created session
* });
* ```
*/
sessionToken?: string;
/**
* Authentication token for starting a new session client-side.
* Use this for SPA applications where the token is available on the frontend.
*
* Ignored if sessionToken is provided.
*/
authToken?: string;
/**
* The transmitter to start from (optional).
* Use this when your automation has multiple transmitters and you want to
* specify which one to start from. Defaults to the first transmitter if not specified.
*
* Ignored if sessionToken is provided.
*
* @example
* ```typescript
* transmitter: 'my-transmitter-name'
* ```
*/
transmitter?: string;
/**
* Initial signals to pass to the transmitter.
* The signals must align with the transmitter's JTD schema.
*
* Ignored if sessionToken is provided.
*
* @example
* ```typescript
* signals: {
* 'input': { mimeType: 'text/plain', data: btoa('Hello World') }
* }
* ```
*/
signals?: {
[key: string]: SignalValue;
};
/**
* Pre-solved challenge token for automations requiring proof-of-work.
* When provided, skips the Cap.js widget and uses this token directly.
*
* Ignored if sessionToken is provided.
*
* @example
* ```typescript
* // From server-side pre-solving
* challengeToken: presolvedToken
* ```
*/
challengeToken?: string;
/**
* Show a Cap.js proof-of-work widget before starting the session.
* The widget handles challenge creation, solving, and redemption automatically.
* It is centered inside the container and auto-removes after a successful solve.
*
* **Styling:** Customize via `--mb-adapt-cap-*` CSS variables on the `.mb-adapt` root.
* Dark mode colors are applied automatically when `darkMode: true`.
* Never use `::part()` selectors on the inner `<cap-widget>` — this breaks
* Cap.js's Shadow DOM animations (checkbox, spinner, label transitions).
*
* Ignored when `sessionToken` or `challengeToken` is provided.
*
* @default false
*/
requiresChallenge?: boolean;
/**
* Options for the Cap.js widget (labels and worker count).
* Only used when `requiresChallenge` is `true`.
*
* @example
* ```typescript
* capWidgetOptions: {
* workerCount: 4,
* i18n: { initialState: "Verify you're human" }
* }
* ```
*/
capWidgetOptions?: CapWidgetOptions;
/**
* Token to inherit an existing session.
* When provided, calls inheritSession() instead of startSession().
*
* Ignored if sessionToken is provided.
*/
inheritToken?: string;
/**
* Auto-parse inherit token from URL.
* Token is removed from URL after parsing (via history.replaceState).
* Takes precedence over inheritToken if token found in URL.
*
* @example
* ```typescript
* // Parse from hash: example.com#mb_session=xxx
* inheritFrom: { hash: 'mb_session' }
*
* // Parse from query param: example.com?token=xxx
* inheritFrom: { param: 'token' }
* ```
*/
inheritFrom?: {
hash: string;
} | {
param: string;
};
/**
* How to display fork URLs when automation creates sub-sessions.
*
* - `{ mode: "side-by-side", split?: number }`: Shows main and fork tabs docked to the right
* - `{ mode: "dialog" }`: Shows fork tabs in a floating group
*
* @default { mode: "side-by-side" }
* @example
* ```typescript
* // Side-by-side with 60/40 split
* forkDisplay: { mode: "side-by-side", split: 60 }
*
* // Dialog (floating group)
* forkDisplay: { mode: "dialog" }
* ```
*/
forkDisplay?: ForkDisplay;
/**
* Whether users can pop out / float panels.
* When false, hides "Pop out" buttons and blocks user-initiated floating.
* Does NOT affect `forkDisplay="dialog"` — that's the developer's chosen initial placement.
*
* @default true
*/
allowFloating?: boolean;
/**
* Whether users can minimize panels (both docked and floating).
* When false, hides "Minimize" buttons on docked group headers and floating toolbars.
*
* @default true
*/
allowMinimize?: boolean;
/**
* Whether users can maximize panels (both docked and floating).
* When false, hides "Maximize" buttons on docked group headers and floating toolbars.
*
* @default true
*/
allowMaximize?: boolean;
/**
* Whether users can dock floating panels back to the main layout.
* When false, hides "Dock" button on floating toolbar and minimized tabs.
* Does NOT affect `forkDisplay="side-by-side"` — that's the developer's chosen initial placement.
*
* @default true
*/
allowDocking?: boolean;
/**
* Whether users can create splits inside floating dialog overlays by dragging tabs.
* When false, tabs dragged inside a floating overlay will merge into existing groups
* instead of creating new split panes.
*
* @default true
*/
allowDialogDocking?: boolean;
/**
* Auto-resize floating overlays based on iframe content height.
* When true, floating overlays auto-resize based on iframe content (same mechanism
* as `autoResizing` for the main panel, but targeting floating overlays).
* Independent of the main `autoResizing` flag.
*
* @default false
*/
floatingAutoResize?: boolean;
/**
* Enable dark mode. When changed, sends message to iframes via postMessage.
*
* @default false
*/
darkMode?: boolean;
/**
* Auto-resize iframe height based on content.
* When true, listens for resize messages from iframes and adjusts height.
* Width remains at 100%.
*
* @default false
*/
autoResizing?: boolean;
/**
* Prompt the user before closing panels that haven't completed, and before
* navigating away from the page while a session is active.
*
* When true:
* - Closing a fork tab that isn't marked complete shows a styled confirmation dialog.
* - Navigating away (or refreshing) while a session is running triggers
* the browser's native `beforeunload` prompt.
*
* The confirmation dialog can be styled via CSS variables on `.mb-adapt`:
* --mb-adapt-confirm-overlay-bg, --mb-adapt-confirm-bg, --mb-adapt-confirm-text,
* --mb-adapt-confirm-btn-bg, --mb-adapt-confirm-btn-text,
* --mb-adapt-confirm-cancel-bg, --mb-adapt-confirm-cancel-text
*
* @default false
*/
confirmOnClose?: boolean;
/**
* Callback invoked when session state changes.
* Receives the status and optional fork identifier.
*
* @param status - Current session status (e.g., 'STATUS_RUNNING', 'STATUS_COMPLETED')
* @param fork - Optional fork identifier (present for forked sessions)
*
* @example
* ```typescript
* onSession: (status, fork) => {
* console.log('Session status:', status);
* if (fork) console.log('Fork:', fork);
* }
* ```
*/
onSession?: (status: StatusJson, fork?: string) => void;
/**
* Callback invoked when automation produces output.
* Receives Output objects containing logs, data, or other results.
*
* @example
* ```typescript
* onOutput: (output) => {
* console.log('Output:', output);
* }
* ```
*/
onOutput?: (output: Output) => void;
/**
* Callback invoked when a fork becomes visually active or inactive.
* Active means the fork is taking up visual space (side-by-side non-collapsed, or dialog open).
*
* @param active - Whether a fork is visually active
*/
onForkActive?: (active: boolean) => void;
/**
* Callback invoked when the automation encounters an error.
* Receives a structured error with classification, message, and retriability.
* Fires for both init-phase errors (e.g. invalid inheritToken) and streaming errors.
*
* @example
* ```typescript
* onError: (error) => {
* if (error.retriable) {
* console.log('Retriable error:', error.kind, error.message);
* } else {
* console.error('Fatal error:', error.kind, error.message);
* }
* }
* ```
*/
onError?: (error: AdaptError) => void;
/**
* Custom CSS class names to override default styling.
* All properties are optional. Unspecified classes use defaults.
*
* @example
* ```typescript
* // Using custom CSS classes
* classNames: {
* root: 'my-adapt-container',
* iframe: 'my-iframe'
* }
* ```
*
* @example
* ```typescript
* // Using Tailwind classes
* classNames: {
* root: 'w-full h-screen',
* iframe: 'rounded-lg shadow-xl'
* }
* ```
*/
classNames?: {
/** Root container class. Default: `'mb-adapt'` */
root?: string;
/** Iframe element class. Default: `'mb-adapt__iframe'` */
iframe?: string;
/** Status message overlay container. Default: `'mb-adapt__status-message'` */
statusMessage?: string;
/** Status card inside the overlay. Default: `'mb-adapt__status-card'` */
statusCard?: string;
};
/**
* Theme configuration for semantic theming.
* Applies CSS custom properties to the root element based on semantic tokens.
* Use `theme.vars` for direct per-variable overrides.
*
* @example
* ```typescript
* // Simple — match your brand
* theme: { primary: '#FFF700' }
*
* // Full design system
* theme: {
* mode: 'dark',
* primary: '#FFF700',
* background: '#121212',
* surface: '#1e1e1e',
* text: '#fff',
* border: 'rgba(255,255,255,0.12)',
* font: 'Inter, sans-serif',
* }
* ```
*/
theme?: AdaptTheme;
/**
* Inline styles to apply to the root element.
* Useful for quick positioning or sizing adjustments.
*
* @example
* ```typescript
* styles: {
* width: '100%',
* height: '600px',
* maxWidth: '1200px',
* margin: '0 auto'
* }
* ```
*/
styles?: Partial<CSSStyleDeclaration>;
/**
* Enable session persistence across page refreshes.
*
* When enabled, the session token and URL state are stored in browser storage.
* On page load, the client reconnects to the existing session instead of creating a new one.
*
* - `true` — use sessionStorage with 3600s TTL
* - `PersistOptions` — customize storage type and TTL
*
* @default undefined (disabled)
*/
persist?: boolean | PersistOptions;
/**
* Enable debug logging to the browser console.
* When true, logs all session, persistence, URL, fork, and layout operations
* with the `[Adapt]` prefix. Also activates debug logging in the core client.
*
* @default false
*/
debug?: boolean;
/**
* Custom text overrides for status messages shown to the user.
* Each property is optional — unspecified messages use built-in defaults.
*/
text?: StatusText;
}
/**
* Custom text overrides for status messages and error states.
*/
export interface StatusText {
/** Shown when the session is stopped. Default: `"This session has been stopped"` */
stopped?: string;
/** Shown when the session token has expired. Default: `"Your session has expired."` */
sessionExpired?: string;
/** Shown on ResourceExhausted. Default: `"This automation is currently at capacity. Please try again later."` */
resourceExhausted?: string;
/** Shown on NotFound when automation doesn't exist. Default: `"This automation could not be found."` */
notFound?: string;
/** Shown on NotFound when session doesn't exist (timed out). Default: `"This session has timed out or been stopped."` */
sessionNotFound?: string;
/** Shown on PermissionDenied or Unauthenticated. Default: `"You don't have permission to run this automation."` */
permissionDenied?: string;
/** Shown on any other non-retriable error. Default: `"Something went wrong. Please try again later."` */
error?: string;
/** Label for the restart/try-again button on status cards. Default: `"Try again"` */
restartButton?: string;
}
export type AdaptErrorKind = 'auth' | 'expired' | 'not_found' | 'session_not_found' | 'capacity' | 'error';
export interface AdaptError {
kind: AdaptErrorKind;
message: string;
retriable: boolean;
cause?: unknown;
}