# Arlopass Documentation — Full Content > Complete documentation for the Arlopass AI wallet SDK. > Homepage: https://arlopass.com > Documentation: https://arlopass.com/docs --- ## Overview Overview of the Arlopass UI component primitives — Chat, Message, StreamingText, ProviderPicker, and more. URL: https://arlopass.com/docs/components/overview ## Components Library Headless primitives and copy-paste blocks for building AI chat interfaces Arlopass ships two complementary layers for UI development: | Package | What it is | Install | | -------------------- | ------------------------------------------------------- | --------------------------- | | `@arlopass/react-ui` | Headless compound components — no styles, full control | `pnpm add` | | `@arlopass/ui` | Styled Tailwind blocks — copy into your project via CLI | `pnpm dlx @arlopass/ui add` | ### Install ```bash title="Terminal" pnpm add @arlopass/react-ui ``` ### Quick example A fully functional uncontrolled chat in under 20 lines. The `Chat.Root` manages conversation state internally — just drop it in and go. ```tsx title="Uncontrolled chat" function MyChat() { return ( ))} {streamingContent && } )} ); } ``` ### Primitives All primitives are exported from `@arlopass/react-ui`. Each is a compound component with dot-notation sub-components (e.g. `Chat.Root`, `Chat.Messages`). | Component | Description | | ------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------- | | [Chat](/docs/components/chat) | Compound component for a full chat interface — manages conversation state, message rendering, input, send/stop buttons, and streaming. | | [Message](/docs/components/message) | Renders a single message with role-aware layout, markdown content, and tool-call display slots. | | [StreamingText](/docs/components/streaming-text) | Renders in-progress assistant output with token-by-token display. | | [ProviderPicker](/docs/components/provider-picker) | Compound component for selecting an AI provider and model from the user's configured list. | | [ToolActivity](/docs/components/tool-activity) | Displays tool-call invocations and their results inline within a message. | | [ConnectionStatus](/docs/components/connection-status) | Renders content conditionally based on the Arlopass extension connection state (connected / disconnected / not installed). | ### Data attributes Primitives expose semantic `data-*` attributes on their rendered DOM elements so you can style states with plain CSS — no JavaScript required. | Attribute | Purpose | Example values | | ------------- | ------------------- | -------------------------------------------- | | `data-role` | Message author role | `user`, `assistant`, `system` | | `data-state` | Component state | `idle`, `streaming`, `error` | | `data-status` | Connection status | `connected`, `disconnected`, `not-installed` | ```css title="CSS styling example" /* Style user messages differently using data attributes */ [data-role="user"] { background: #2563eb; color: white; } [data-role="assistant"] { background: #f4f4f5; } [data-state="streaming"] { opacity: 0.8; } [data-status="connected"] { color: #22c55e; } ``` --- ## Chat Chat primitive component for building AI chat interfaces with streaming, tool support, and full state control. URL: https://arlopass.com/docs/components/chat Compound chat interface with messages, streaming, input, and tool support. ```tsx ``` --- ### Parts | Name | Element | Purpose | | ------------------------- | ----------- | ---------------------------------------------------------------------------------------------------------------- | | `Chat.Root` | div | Manages conversation state (uncontrolled) or accepts external state (controlled). Renders the outermost wrapper. | | `Chat.Header` | div | Container for the chat header area (title, controls). Sets `data-part="header"`. | | `Chat.Messages` | render prop | Provides the message list via a render-prop child. Auto-scrolls with rAF pinning during streaming. | | `Chat.Message` | div | Wraps a single message inside Messages. Accepts a TrackedChatMessage. Provides context to child parts. | | `Chat.Avatar` | div | Avatar circle for a message. Accepts a role prop ("user" \| "assistant"). Sets `data-part="avatar"`. | | `Chat.Bubble` | div | Message bubble container. Reads role from Message context. Sets `data-part="bubble"`. | | `Chat.MessageContent` | div | Renders the text content of the current Chat.Message. | | `Chat.MessageMeta` | div | Slot for provider/model attribution below assistant messages. Sets `data-part="message-meta"`. | | `Chat.ToolPills` | div | Renders tool usage pills for assistant messages. Accepts formatToolName prop. Sets `data-part="tool-pills"`. | | `Chat.Input` | textarea | Auto-growing text input wired to Chat.Root state. | | `Chat.SendButton` | button | Sends the current input. Disabled when empty or streaming. | | `Chat.StopButton` | button | Aborts the active stream. Hidden when not streaming. | | `Chat.StreamingIndicator` | span | Visible only while a response is being streamed. | | `Chat.TypingIndicator` | div | Bouncing dots shown when streaming starts before any content arrives. Accepts dotCount prop (default 3). | | `Chat.StreamCursor` | span | Pulsing cursor bar at the end of streaming content. Sets `data-part="stream-cursor"`. | | `Chat.ToolActivity` | div | Live tool execution phases (priming, matched, executing, result). Sets data-phase attribute. | | `Chat.EmptyState` | div | Shown when the message list is empty. | | `Chat.ScrollFade` | div | Gradient fade overlay at the top of the messages area. Controlled via visible prop. | | `Chat.Footer` | div | Footer status bar container. Sets data-state to "streaming" or "idle". | | `Chat.ContextBar` | div | Context window usage indicator with usage levels (normal/warning/critical). Accepts formatTokens prop. | --- ### Uncontrolled usage Wrap in a `
))} )} ); } ``` --- ### Controlled usage Pass `messages` and callbacks to manage state yourself. Useful when you need access to the conversation outside the Chat tree. ```tsx title="Controlled chat" function ControlledChat() { const conv = useConversation({ systemPrompt: "Be concise." }); return ( )) } ); } ``` --- ### Chat.Root — uncontrolled props ### Chat.Root — controlled props --- ## Message Standalone message display component for rendering individual chat messages with role, content, timestamp, and status. URL: https://arlopass.com/docs/components/message Standalone message display component. ```tsx ``` --- ### Parts | Name | Element | Purpose | | ------------------- | ------- | ------------------------------------------------------------ | | `Message.Root` | div | Outermost wrapper. Provides message context to children. | | `Message.Content` | div | Renders the text/markdown content of the message. | | `Message.Role` | span | Displays the message role (user, assistant, system). | | `Message.Timestamp` | time | Renders the message timestamp. | | `Message.Status` | span | Shows message delivery status (pending, sent, error). | | `Message.ToolCalls` | div | Slot for rendering tool-call information within the message. | --- ### Message.Root props --- ### Standalone usage Use `Message` on its own to render a single message outside of a `Chat` context. ```tsx title="Message display" function SingleMessage({ msg }) { return ( ); } ``` ### Inside Chat.Messages Compose with `Chat.Message` inside the `Chat.Messages` render prop for full chat integration. ```tsx title="Inside Chat" )) } ; ``` --- ### Data attributes ### Styling ```css title="CSS" /* Role-based message styling */ [data-role="user"] { justify-content: flex-end; } [data-role="assistant"] { justify-content: flex-start; } [data-status="error"] { border-left: 3px solid red; } ``` --- ## StreamingText Streaming text renderer with a typing cursor for displaying real-time AI responses. URL: https://arlopass.com/docs/components/streaming-text Streaming text renderer with typing cursor. ```tsx ``` --- ### Props --- ### Usage ```tsx title="StreamingText" function StreamingDemo({ content, isStreaming }) { return ( ); } ``` ### With Chat Commonly used inside `Chat.Messages` to render the in-progress assistant response. ```tsx title="Inside Chat" ; ``` --- ### Data attributes ### Styling The component renders a `` element. The cursor character is appended as a text node while streaming. ```css title="CSS" /* Animate the cursor while streaming */ [data-state="streaming"] { /* Cursor blinks */ } [data-state="streaming"]::after { animation: blink 1s steps(2) infinite; } @keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0; } } [data-state="idle"] { /* Final state — no cursor visible */ } ``` --- ## ProviderPicker Provider and model selection compound component for choosing AI providers and models. URL: https://arlopass.com/docs/components/provider-picker Provider and model selection compound component. ```tsx ``` --- ### Parts | Name | Element | Purpose | | ------------------------------- | ------- | ---------------------------------------------------------------------------------- | | `ProviderPicker.Root` | div | Manages provider/model selection state. Reads from useProviders when uncontrolled. | | `ProviderPicker.ProviderSelect` | select | Dropdown for choosing an AI provider. | | `ProviderPicker.ModelSelect` | select | Dropdown for choosing a model from the selected provider. | | `ProviderPicker.SubmitButton` | button | Confirms the selected provider/model pair. | --- ### Uncontrolled usage Inside a ` ); } ``` --- ### Controlled usage Pass props to take full control over the provider list and selection callbacks. ```tsx title="Controlled" function ControlledPicker() { const { providers, selectedProvider, selectProvider, isLoading, error } = useProviders(); return ( ); } ``` ### ProviderPicker.Root — controlled props void", description: "Called when the user selects a different provider.", }, { name: "onModelChange", type: "(modelId: string) => void", description: "Called when the user selects a different model.", }, { name: "onSelect", type: "(providerId: string, modelId: string) => void", description: "Called when the user confirms the selection.", }, ]} /> --- ### Data attributes --- ## ToolActivity Tool call execution display component for rendering function calling status, arguments, and results. URL: https://arlopass.com/docs/components/tool-activity Tool call execution display. ```tsx ``` --- ### Parts | Name | Element | Purpose | | --------------------- | ------- | ---------------------------------------------------------------------- | | `ToolActivity.Root` | div | Wraps tool-call display. Tracks whether any call is still in progress. | | `ToolActivity.Call` | div | Renders a single tool-call invocation. Supports render-prop children. | | `ToolActivity.Result` | div | Renders the result of a tool call. | --- ### ToolActivity.Root props ### ToolActivity.Call props ))} ); } ``` ### Inside a Chat message Compose with `Message` to show tool activity inline within conversation messages. ```tsx title="With Chat.Messages" )} )) } ; ``` --- ### Data attributes --- ## ConnectionStatus Connection state display component for showing real-time WebSocket connection status. URL: https://arlopass.com/docs/components/connection-status Connection state display. ```tsx ``` --- ### Props --- ### Uncontrolled usage Inside a ` ); } ``` ### Controlled usage Pass the `state` prop to override auto-detection. Useful for storybooks, tests, or custom state management. ```tsx title="Controlled" function CustomStatus({ state }) { return ( ); } ``` --- ### Data attributes ### Styling The component renders a plain `
` with a `data-state` attribute. Use CSS selectors to style each state: ```css title="CSS" /* Color-code connection states */ [data-state="connected"] { color: #22c55e; } [data-state="degraded"] { color: #eab308; } [data-state="disconnected"], [data-state="failed"] { color: #ef4444; } [data-state="connecting"], [data-state="reconnecting"] { color: #3b82f6; animation: pulse 1.5s ease-in-out infinite; } @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } } ``` --- ## Block registry Pre-built UI blocks for chat, provider selection, connection status, and more — install via CLI or copy from the registry. URL: https://arlopass.com/docs/components/registry Pre-styled Tailwind blocks you copy into your project with a single command. Blocks are complete, styled UI components built on top of the `@arlopass/react-ui` primitives. Instead of installing them as a dependency, the CLI copies the source files into your project so you have full control over the code. --- ### Install a block ```bash title="Terminal" npx @arlopass/ui add chat ``` --- ### Available blocks | ID | Name | Description | | -------------------- | ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | | `chat` | Chat | Complete chat interface with avatars, message bubbles, typing indicator, streaming cursor, tool activity, context bar, scroll fade, and auto-scroll | | `chatbot` | Chatbot Widget | Floating chatbot bubble with expandable chat panel (depends on chat) | | `provider-picker` | Provider Picker | Styled provider and model selection dropdowns | | `connection-banner` | Connection Banner | Connection status banner with install prompt | | `install-button` | Install Button | Styled button/link to install the Arlopass extension | | `extension-required` | Extension Required Gate | Feature-level gate that shows an install prompt when the extension is missing | | `app-required` | App Required Gate | Full-app gate that blocks the entire UI with an install page when extension is missing | --- ### CLI commands ```bash title="Terminal" # Add a single block npx @arlopass/ui add chat # Add multiple blocks npx @arlopass/ui add chat chatbot provider-picker # List available blocks npx @arlopass/ui list # Overwrite existing files npx @arlopass/ui add chat --force # Preview without writing files npx @arlopass/ui add chat --dry-run # Custom output directory npx @arlopass/ui add chat --out src/ui ``` --- ### Configuration On first run, the CLI creates a `arlopass-ui.json` file in your project root. You can edit it to change the output directory or other settings. ```json title="arlopass-ui.json" // arlopass-ui.json — auto-created on first `add` { "outputDir": "src/components/arlopass", "typescript": true } ``` --- ### Chat block A full chat interface with message list, streaming indicator, input field, and send/stop buttons. Wrap it in an `ArlopassProvider` and you're ready to go. ```tsx title="Chat usage" function App() { return ( ); } ``` --- ### Chatbot widget block A floating chat widget that renders a toggle bubble in the corner of the screen. Opens an expandable panel with the full chat interface. It wraps `ArlopassProvider` and guard components internally — just drop it anywhere. ```tsx title="Chatbot widget" function App() { return (

My App

{/* Floating chat widget — renders a bubble in the bottom-right */}
); } ``` --- ### Provider picker block Styled provider and model selection dropdowns. Fires an `onSelect` callback when the user confirms their choice. ```tsx title="Provider picker" function Settings() { return ( ); } ``` --- ### Extension required gate (feature-level) Wraps a feature that needs the Arlopass extension. When the extension is missing, shows a fallback prompt. When installed, renders the feature normally. Use for individual AI-powered features in apps that also work without Arlopass. ```tsx title="Extension required gate" function App() { return (

My App

{/* This feature only shows when the extension is installed */} {/* This feature hides silently when no extension */}
); } ``` --- ### App required gate (full-app) Blocks the entire app when Arlopass is not installed. Use this when your app cannot function at all without the extension. ```tsx title="App required gate" ArlopassProvider, ArlopassRequiredGate, ArlopassInstallButton, } from "@arlopass/react"; function App() { return ( ); } ``` --- --- ## How Arlopass works Understanding the architecture — extension, SDK, bridge, and adapters. URL: https://arlopass.com/docs/concepts/how-arlopass-works Understanding the architecture — extension, SDK, bridge, and adapters ### The problem Web applications increasingly want AI capabilities — chat, summarization, code generation. But connecting directly to AI providers from frontend code means embedding API keys in JavaScript bundles, managing credentials in localStorage, and trusting every dependency in your supply chain with those secrets. That's not viable. ### The wallet analogy Arlopass works like MetaMask does for Ethereum. MetaMask sits between your web app and the blockchain — it holds your private keys, mediates every transaction, and asks for consent. Your app never touches the keys directly. Arlopass does the same for AI. The browser extension holds API credentials, mediates every request, and the web application never sees a single key. Your app talks to the SDK. The SDK talks to the extension. The extension talks to providers. ### Architecture layers The system has six layers, each with a clear responsibility: 1. **Web App** — your application, using the React SDK (`@arlopass/react`) or Web SDK (`@arlopass/web-sdk`). It calls hooks or client methods. It never manages credentials. 2. **SDK → Extension** — the SDK communicates via `window.arlopass`, a `ArlopassTransport` object injected by the extension's content script. Every message is wrapped in a canonical envelope with timestamps, nonces, and correlation IDs. 3. **Extension** — mediates consent, manages sessions, validates origins, attaches credentials, and enforces rate limits. This is where the user's API keys live. 4. **Bridge** — the extension routes requests to the appropriate provider adapter. The bridge handles protocol translation and connection management. 5. **Vault** — an encrypted file owned by the bridge. It stores credentials (API keys), provider configurations, app connections, and token usage. The extension reads and writes vault data through native messages — it never stores credentials in `chrome.storage`. Because the vault lives on the filesystem, one setup works across Chrome, Edge, and Firefox. 6. **Providers** — Ollama, Claude, OpenAI, Gemini, Amazon Bedrock, Azure, Perplexity, and more. Each has an adapter that normalizes its API into the Arlopass protocol. ```tsx title="SDK usage at each layer" // React SDK — the extension is detected automatically function App() { return ( ); } // Web SDK — you pass the injected transport explicitly const client = new ArlopassClient({ transport: window.arlopass }); await client.connect({ appId: "my-app" }); ``` ### Data flow A typical Arlopass session follows a connect → discover → select → chat → disconnect flow. The state machine enforces this order — you can't chat before connecting, and you can't connect twice. ```ts title="Typical session flow" // 1. Connect — establish a session with the extension await client.connect({ appId: "my-app" }); // state: disconnected → connecting → connected // 2. List providers — discover what's available const providers = await client.listProviders(); // [{ providerId: "ollama", models: [...] }, { providerId: "claude", ... }] // 3. Select a provider await client.selectProvider({ providerId: "ollama", modelId: "llama3" }); // 4. Chat — send messages, stream responses for await (const event of convo.stream("Explain monads")) { // token-by-token streaming } // 5. Disconnect — clean up await client.disconnect(); // state: connected → disconnected ``` ### The security boundary Credentials never touch the web application. The SDK sends a request envelope through `window.arlopass`. The extension validates the envelope, then asks the bridge to read the API key from the vault and forward the request to the provider. The response comes back through the same channel — stripped of any credential material. The web app and extension popup never see the raw API key. Even if your web app is compromised, the attacker gets access to nothing beyond what the user has already consented to in the current session. When a user adds a provider, the credential is encrypted and stored in the vault on the bridge — not in `chrome.storage` or any browser-accessible location. This means credentials are isolated from the browser process entirely, surviving extension updates and working across every browser the bridge is registered with. ### Session lifecycle Calling `connect()` creates a session with a unique `sessionId`. All subsequent operations — listing providers, selecting a model, sending messages — are scoped to that session. Calling `disconnect()` ends it. The extension cleans up resources, and the state machine returns to `disconnected`. If the connection degrades (e.g., the bridge goes down), the state machine transitions through `degraded` and `reconnecting` states automatically. If recovery fails, it lands in `failed`, which can either attempt reconnection or disconnect cleanly. ```ts title="Session and state machine" // Sessions are scoped and ephemeral. // connect() creates a new session with a unique sessionId. // All operations are tied to that sessionId. // disconnect() ends the session — the extension cleans up. // The state machine enforces this: // disconnected → connecting → connected → disconnected // ↓ // degraded → reconnecting → connected // ↓ // failed → reconnecting | disconnected ``` ### Vault setup On first use, the user sets up the vault with a master password or OS keychain integration. The bridge creates the encrypted vault file and all subsequent credential storage flows through it. On subsequent browser opens, the vault is already unlocked for the duration of the bridge process — or prompts once for the master password if the bridge was restarted. --- ## Transport model How the SDK communicates with the extension — transport interface, envelope protocol, and validation. URL: https://arlopass.com/docs/concepts/transport-model How the SDK communicates with the extension ### What is a transport? A transport is the communication layer between the SDK and the Arlopass extension. The `ArlopassTransport` interface is intentionally minimal: `request()` for single request-response operations (like listing providers), and `stream()` for streaming operations (like chat). That's the entire surface area. ```ts title="ArlopassTransport interface" // The ArlopassTransport interface — two methods, that's it. // Send a request, get a single response request; // Web SDK — you reference it explicitly const client = new ArlopassClient({ transport: window.arlopass }); ``` ### Why injected-only? You might wonder why the SDK doesn't let you construct your own transport — say, a WebSocket to your own backend. The reason is security. The entire Arlopass trust model depends on the extension controlling the communication channel. If arbitrary transports were allowed, any code in the page could bypass the extension's consent flow, credential management, and rate limiting. The injected transport is the extension's guarantee that it mediates every interaction. ### The envelope protocol Every message — request or response — is wrapped in a `CanonicalEnvelope`. The SDK constructs these automatically; you never build one by hand. The envelope carries metadata that enables security validation, request correlation, and protocol versioning. ```ts title="Envelope structure" // Every request/response is wrapped in a CanonicalEnvelope. // The SDK builds this automatically — you never construct one manually. const envelope = { protocolVersion: "1.0.0", // Must match — version mismatch rejects early requestId: "uuid-1234", // Unique per request correlationId: "uuid-5678", // Links request → response pairs origin: "https://myapp.com", // From window.location.origin sessionId: "uuid-abcd", // Scoped to this connect() session capability: "chat.stream", // What operation is requested issuedAt: "2025-03-26T...", // When the envelope was created expiresAt: "2025-03-26T...", // TTL — expired envelopes are rejected nonce: "random-value", // Prevents replay attacks payload: { /* request data */ }, }; ``` ### Envelope validation The extension validates every incoming envelope before processing it. This is where Arlopass's replay resistance, expiry enforcement, and correlation checking happen. A stale envelope is rejected. A replayed nonce is rejected. A response that doesn't match the original request's correlation ID is rejected. ```ts title="Validation pipeline" // The extension validates every envelope before processing: // 1. Protocol version must match if (envelope.protocolVersion !== SUPPORTED_VERSION) reject(); // 2. Timestamp check — is issuedAt recent? if (Date.now() - Date.parse(envelope.issuedAt) > TTL) reject(); // 3. Expiry check — has the envelope expired? if (Date.now() > Date.parse(envelope.expiresAt)) reject(); // 4. Nonce check — has this nonce been seen before? if (nonceStore.has(envelope.nonce)) reject(); // replay detected nonceStore.add(envelope.nonce); // 5. Correlation ID — response must reference the original request if (response.correlationId !== request.requestId) reject(); ``` ### Testing with mock transport In tests, you don't have a browser extension. The React SDK ships a `createMockTransport` utility that implements `ArlopassTransport` with configurable providers and responses. It lets you test your components against the full SDK without needing the extension installed. ```tsx title="Mock transport for tests" // For testing, use the mock transport from the React SDK test utilities. // It implements ArlopassTransport without needing the extension. const transport = createMockTransport({ providers: [ { providerId: "mock-provider", models: [{ modelId: "mock-model" }] }, ], }); // Use it in tests exactly like the real transport const client = new ArlopassClient({ transport }); await client.connect({ appId: "test-app" }); ``` --- ## State management How the React SDK stays in sync with the ArlopassClient using ClientStore and useSyncExternalStore. URL: https://arlopass.com/docs/concepts/state-management How the React SDK stays in sync with the ArlopassClient ### The challenge `ArlopassClient` is a plain TypeScript class. It has getters like `.state`, `.sessionId`, and `.selectedProvider` — but they're not reactive. Nothing in the Web SDK knows about React, and that's intentional. The SDK is framework-agnostic. So how does the React SDK keep components in sync with an external, non-reactive object? ```ts title="The problem" // ArlopassClient has internal state — but it's just getters, not reactive. const client = new ArlopassClient({ transport: window.arlopass }); await client.connect({ appId: "my-app" }); client.state; // "connected" — a plain getter client.sessionId; // "uuid-1234" — a plain getter client.selectedProvider; // null — a plain getter // React components won't re-render when these change. // There's no .onChange() callback, no EventEmitter, no observable. // The SDK is deliberately framework-agnostic. ``` ### ClientStore The answer is `ClientStore`. It wraps a `ArlopassClient` and maintains a snapshot — a plain object that represents the client's current state at a point in time. When the store detects a change, it creates a new snapshot object and notifies subscribers. When nothing changes, it keeps the same object reference. ```ts title="ClientStore internals" // ClientStore wraps ArlopassClient and maintains a reactive snapshot. class ClientStore { #client: ArlopassClient; #snapshot: ClientSnapshot; #subscriptions = new Subscriptions(); constructor(client: ArlopassClient) { this.#client = client; this.#snapshot = createInitialSnapshot(); this.#startHeartbeat(); // 500ms safety-net polling } // React's useSyncExternalStore calls these two: getSnapshot(): ClientSnapshot { return this.#snapshot; } subscribe(listener: () => void): () => void { return this.#subscriptions.subscribe(listener); } // Called after every SDK operation refreshSnapshot(): void { const next = buildSnapshot({ state: this.#client.state, sessionId: this.#client.sessionId ?? null, selectedProvider: this.#client.selectedProvider ?? null, providers: this.#providers, error: this.#error, }); // Only notify if something actually changed if (!snapshotsEqual(this.#snapshot, next)) { this.#snapshot = next; this.#subscriptions.notify(); } } } ``` ### useSyncExternalStore React 18 introduced `useSyncExternalStore` specifically for this pattern — reading from an external store that isn't managed by React. It guarantees tear-free reads (no partial state) and works correctly with concurrent features like Suspense and transitions. Every Arlopass hook uses it under the hood. ```ts title="React integration" // React 18's useSyncExternalStore — the bridge between external and React state. // Full snapshot — used internally function useStoreSnapshot(): ClientSnapshot { const { store } = useArlopassContext(); return useSyncExternalStore( (cb) => store.subscribe(cb), () => store.getSnapshot(), () => store.getSnapshot(), // server snapshot (same for SSR safety) ); } // Selective subscription — each hook picks its slice function useStoreSelector ### Snapshot identity The key to avoiding unnecessary re-renders is snapshot identity. The store compares the current snapshot to the next one field-by-field. If nothing changed, it keeps the old object. Since `useSyncExternalStore` uses `Object.is` to compare, same reference means no re-render. ```ts title="snapshotsEqual" // A new snapshot object is created ONLY when values actually differ. function snapshotsEqual(a: ClientSnapshot, b: ClientSnapshot): boolean { return ( a.state === b.state && a.sessionId === b.sessionId && a.selectedProvider === b.selectedProvider && a.providers === b.providers && // referential equality — same array a.error === b.error ); } // If nothing changed, the old snapshot object is kept. // useSyncExternalStore compares by reference (Object.is). // Same reference = no re-render. This is what prevents // the 500ms heartbeat from causing unnecessary re-renders. ``` ### Primary sync and safety-net polling The store uses two complementary strategies. The primary strategy is wrap-and-refresh: every SDK operation goes through the store, which calls `refreshSnapshot()` after completion. The safety net is a 500ms heartbeat that catches changes the store didn't initiate — like the extension unloading or the bridge dropping. The snapshot equality check means the heartbeat is effectively free when nothing has changed. ```ts title="Two sync strategies" // Two complementary sync strategies keep the UI accurate. // 1. PRIMARY: Wrap-and-refresh // Every SDK operation goes through the store, which calls // refreshSnapshot() after the operation completes. // // connect() → client.connect() → refreshSnapshot() // selectProvider() → client.selectProvider() → refreshSnapshot() // stream() → each token → refreshSnapshot() // 2. SAFETY NET: 500ms heartbeat polling // Some state changes happen outside the store's control: // - Extension unloads or crashes // - Bridge connection drops // - Another tab disconnects the same session // // The heartbeat catches these by periodically reading // client.state and comparing to the last snapshot. // The snapshot equality check prevents spurious re-renders. const HEARTBEAT_INTERVAL_MS = 500; this.#heartbeatId = setInterval(() => { this.refreshSnapshot(); // No-op if nothing changed }, HEARTBEAT_INTERVAL_MS); ``` ### Selective subscriptions Each hook subscribes to only the slice of state it needs via `useStoreSelector`. `useConnection()` cares about `state` and `sessionId`. `useProviders()` cares about the provider list. A change to the provider list doesn't re-render components that only use `useConnection()`. ### Streaming optimization During streaming, tokens can arrive hundreds of times per second. Re-rendering on every token would destroy performance. The store uses `requestAnimationFrame` with `setTimeout` microbatching — tokens accumulate, and the store refreshes at most ~60 times per second. Each render sees the latest accumulated content, not individual tokens. ```ts title="Streaming batching" // Streaming tokens arrive very fast — potentially hundreds per second. // Re-rendering on every token would tank performance. // The store uses requestAnimationFrame + setTimeout microbatching: // 1. Token arrives → schedule a RAF callback (if not already scheduled) // 2. RAF fires → batch all accumulated tokens → refreshSnapshot() once // 3. Result: ~60 refreshes/sec max, regardless of token rate // This means the UI stays smooth during streaming, and each render // has the latest accumulated content — not one render per token. ``` --- ## Web SDK vs React SDK When to use each — or both. Side-by-side comparison and migration path. URL: https://arlopass.com/docs/concepts/web-sdk-vs-react When to use each — or both ### Two SDKs, one protocol Arlopass ships two SDKs. The Web SDK (`@arlopass/web-sdk`) is the core — a framework-agnostic TypeScript client that handles connections, state machines, envelope construction, and streaming. The React SDK (`@arlopass/react`) wraps the Web SDK with hooks, providers, error boundaries, and a reactive state layer. They speak the same protocol and use the same transport. ### Side-by-side comparison | Dimension | Web SDK | React SDK | | -------------------- | -------------------------------------------------------- | ------------------------------------------------------ | | **Setup** | Create client, pass transport, manage lifecycle manually | Wrap in ` ); } function Chat() { const { state } = useConnection(); const { providers, select } = useProviders(); const { messages, stream } = useChat({ systemPrompt: "Be helpful." }); // State management, error handling, streaming optimization — // all handled by the hooks. You write the UI. } ``` ### Hook-to-pattern mapping Every React SDK hook replaces a specific Web SDK pattern. If you're familiar with the Web SDK, this mapping shows what each hook encapsulates. ```ts title="What each hook replaces" // Each React hook replaces a manual Web SDK pattern: // useConnection() → client.connect() / client.disconnect() / client.state // useProviders() → client.listProviders() / client.selectProvider() // useChat() → client.chat.send() / client.chat.stream() // useConversation() → new ConversationManager({ client }) // useClient() → escape hatch — returns the raw ArlopassClient // ArlopassChatReadyGate → if (state === "connected" && selectedProvider) ``` ```tsx title="Escape hatch" function AdvancedFeature() { // useClient() gives you the raw ArlopassClient when you need full control. // The React SDK wraps @arlopass/web-sdk — so all web-sdk types // are re-exported from @arlopass/react. const client = useClient(); // You can use client directly for operations not covered by hooks, // but you lose automatic state sync and streaming optimization. // Use sparingly. } ``` ### Using both Because the React SDK wraps the Web SDK, you're already using both. Types like `ProviderDescriptor`, `ClientState`, and `ChatStreamEvent` are re-exported from `@arlopass/react`. You don't need to install `@arlopass/web-sdk` separately unless you're importing something the React SDK doesn't re-export (which is rare). ### Migration path If you started with the Web SDK and want to move to React, the migration is additive. Wrap your app in `ArlopassProvider`, replace imperative client calls with hooks, and remove your manual state management code. Your existing type imports keep working because the React SDK re-exports them. ```ts title="Additive migration" // If you started with the Web SDK and want to add React SDK: // Before — manual state management const client = new ArlopassClient({ transport: window.arlopass }); // ... manage state, re-renders, cleanup yourself // After — wrap in ArlopassProvider, use hooks // The React SDK re-exports web-sdk types, so your existing // type imports still work: // Migration is additive — you don't rewrite anything, // you wrap and replace imperative code with hooks. ``` --- ## Welcome to Arlopass Arlopass is an open-source AI wallet that lets web apps use a user's own AI providers — Ollama, Claude, GPT, Gemini, Bedrock — without touching API keys. URL: https://arlopass.com/docs/getting-started/welcome Arlopass lets users bring their own AI provider to any web app. Instead of locking into a single model or forcing users to trust you with API keys, your app connects to whatever provider the user already has — through a browser extension that acts as a universal AI wallet. ## How it works The architecture has three layers: - **The Arlopass browser extension** holds the user's provider credentials and exposes a secure transport on the page. - **The Web SDK** (`@arlopass/web-sdk`) connects to that transport and gives you a client for sending messages, streaming responses, and calling tools. - **The React SDK** (`@arlopass/react`) wraps the Web SDK in hooks and components so you can build AI-powered UIs with minimal boilerplate. ## Key features | Feature | Description | | ---------------------- | ---------------------------------------------------------------------------------------------------------- | | **Provider Agnostic** | Connect to OpenAI, Anthropic, Google, Ollama, or any provider your users choose. One API, every model. | | **Secure by Default** | API keys never touch your servers. The browser extension manages credentials, so you ship zero secrets. | | **Developer Friendly** | Full TypeScript support, React hooks, streaming out of the box, and guard components for common UI states. | ## Choose your path **Web SDK** — Framework-agnostic. Use with vanilla JS, Svelte, Vue, or any framework. Full control over every call. [Get started with the Web SDK →](/docs/getting-started/quickstart-web-sdk) **React SDK** — Hooks, providers, and guard components. The fastest way to add AI to a React app. [Get started with the React SDK →](/docs/getting-started/quickstart-react) --- ## Installation Install the Arlopass browser extension and SDK packages to start building AI-powered web apps. URL: https://arlopass.com/docs/getting-started/installation Install the SDKs and get your environment ready. ### Prerequisites - **Node.js 18+** — required for both SDKs - **React 18+** — required if using the React SDK - **Arlopass browser extension** — installed in the user's browser to provide AI credentials ### React SDK The React SDK gives you hooks, providers, and guard components for building AI-powered React apps. ```bash title="Terminal" pnpm add @arlopass/react ``` ### Web SDK The Web SDK is framework-agnostic. Use it with vanilla JavaScript, Svelte, Vue, or any other framework. ```bash title="Terminal" pnpm add @arlopass/web-sdk ``` ### Browser extension Arlopass requires the browser extension to be installed in the end user's browser. The extension manages provider credentials and exposes the secure transport that the SDKs connect to. Install the Arlopass browser extension from the Chrome Web Store. ### Verify installation Add one of these import checks to confirm everything is wired up: ```typescript title="React SDK" console.log("React SDK loaded ✓"); ``` ```typescript title="Web SDK" console.log("Web SDK loaded ✓"); ``` ### Components Library The optional components library provides headless React primitives for building chat interfaces. ```bash title="Terminal" pnpm add @arlopass/react-ui ``` You can also use the block registry CLI to copy pre-styled Tailwind blocks into your project: ```bash title="Terminal" npx @arlopass/ui add chat ``` ### TypeScript Both SDKs ship with full TypeScript declarations. No `@types` packages needed — just import and go. --- ## Quickstart: Web SDK Get started with the Arlopass Web SDK in 5 steps — connect, list providers, select a model, and send your first message. URL: https://arlopass.com/docs/getting-started/quickstart-web-sdk Send your first AI message in 5 minutes. ### Step 1 — Install the Web SDK ```bash title="Terminal" pnpm add @arlopass/web-sdk ``` ### Step 2 — Create a client The client needs a transport — the bridge to the browser extension. The extension injects it at `window.arlopass`. ```typescript title="client.ts" const client = new ArlopassClient({ transport: window.arlopass, }); ``` ### Step 3 — Connect to the extension Call `connect()` with your app ID. This handshakes with the extension and verifies the user has it installed. ```typescript title="client.ts" await client.connect({ appId: "my-app" }); console.log("Connected to Arlopass extension"); ``` ### Step 4 — Select a provider List the providers the user has configured, then select one. The extension handles all credential management. ```typescript title="client.ts" const providers = await client.listProviders(); console.log("Available providers:", providers); // Select the first available provider await client.selectProvider(providers[0].id); ``` ### Step 5 — Send a message Send a chat completion request. The response comes back from whatever model the user chose. ```typescript title="client.ts" const response = await client.chat.send({ messages: [ { role: "system", content: "You are a helpful assistant." }, { role: "user", content: "Hello! What can you do?" }, ], }); console.log(response.content); ``` ### Complete example Here's everything together in a single file you can run: ```typescript title="main.ts" async function main() { // 1. Create the client const client = new ArlopassClient({ transport: window.arlopass, }); // 2. Connect to the extension await client.connect({ appId: "my-app" }); // 3. Pick a provider const providers = await client.listProviders(); await client.selectProvider(providers[0].id); // 4. Send a message const response = await client.chat.send({ messages: [ { role: "system", content: "You are a helpful assistant." }, { role: "user", content: "Explain Arlopass in one sentence." }, ], }); console.log(response.content); } main(); ``` --- ## Quickstart: React SDK Build your first AI-powered React app with Arlopass in 5 steps using hooks, providers, and streaming. URL: https://arlopass.com/docs/getting-started/quickstart-react Build your first AI-powered React component in 5 minutes. ### Step 1 — Install the React SDK ```bash title="Terminal" pnpm add @arlopass/react ``` ### Step 2 — Wrap your app in ArlopassProvider The provider connects to the browser extension and manages the AI client lifecycle. ```tsx title="App.tsx" function App() { return ( ); } ``` ### Step 3 — Add ChatReadyGate The gate handles loading, missing-provider, and error states so your chat component only renders when everything is ready. ```tsx title="YourApp.tsx" function YourApp() { return ( ); } ``` ### Step 4 — Use the useConversation hook The hook gives you the message list, a streaming send function, and status flags. No manual state management needed. ```tsx title="Chat.tsx" function Chat() { const { messages, stream, isStreaming } = useConversation(); async function handleSend(text: string) { await stream({ messages: [...messages, { role: "user", content: text }], }); } return /* your UI */; } ``` ### Step 5 — Build the UI Wire up an input and a message list. The hook handles the rest. ```tsx title="Chat.tsx" function Chat() { const { messages, stream, isStreaming } = useConversation(); const [input, setInput] = useState(""); async function handleSubmit(e: React.FormEvent) { e.preventDefault(); if (!input.trim() || isStreaming) return; const text = input; setInput(""); await stream({ messages: [...messages, { role: "user", content: text }], }); } return (
{messages.map((msg, i) => (
{msg.role}: {msg.content}
))}
setInput(e.target.value)} placeholder="Type a message..." disabled={isStreaming} />
); } ``` ### Complete example Here's a full working chat component you can drop into any React app: ```tsx title="App.tsx" ArlopassProvider, ChatReadyGate, useConversation, } from "@arlopass/react"; function Chat() { const { messages, stream, isStreaming } = useConversation(); const [input, setInput] = useState(""); async function handleSubmit(e: React.FormEvent) { e.preventDefault(); if (!input.trim() || isStreaming) return; const text = input; setInput(""); await stream({ messages: [...messages, { role: "user", content: text }], }); } return (
{messages.map((msg, i) => (
{msg.role}: {msg.content}
))}
setInput(e.target.value)} placeholder="Type a message..." disabled={isStreaming} />
); } return ( ); } ``` ### React SDK vs Web SDK The React SDK wraps the same Web SDK calls in hooks and components. Here's the same "send a message" feature in both: #### React SDK ```tsx title="Chat.tsx" function Chat() { const { messages, stream, isStreaming } = useConversation(); async function send(text: string) { await stream({ messages: [...messages, { role: "user", content: text }], }); } } ``` #### Web SDK ```typescript title="main.ts" const client = new ArlopassClient({ transport: window.arlopass }); await client.connect({ appId: "my-app" }); const providers = await client.listProviders(); await client.selectProvider(providers[0].id); const response = await client.chat.send({ messages: [{ role: "user", content: "Hello!" }], }); ``` --- ## Conversation management Manage context windows, pin messages, auto-summarize, and monitor token usage. URL: https://arlopass.com/docs/guides/conversation-management You want to manage conversation history with automatic context window truncation. ### Create a managed conversation The `useConversation` hook wraps a `ConversationManager` from the web SDK. Pass `maxTokens` to set the context window size — the manager automatically evicts the oldest unpinned messages when the window fills up. #### React SDK ```tsx title="Chat.tsx" function Chat() { const { messages, streamingContent, isStreaming, tokenCount, contextWindow, stream, stop, clearMessages, pinMessage, } = useConversation({ systemPrompt: "You are a helpful assistant.", maxTokens: 8192, }); return (

Tokens used: {tokenCount} / 8192

Messages in context window: {contextWindow.length}

{messages.map((msg) => (
{msg.role}: {msg.content}
))} {isStreaming &&
AI: {streamingContent}
}
); } ``` #### Web SDK ```ts title="main.ts" const client = new ArlopassClient({ transport: window.arlopass }); await client.connect({ appId: "my-app" }); const convo = new ConversationManager({ client, systemPrompt: "You are a helpful assistant.", maxTokens: 8192, }); // Send a message and stream the response for await (const event of convo.stream("Hello!")) { if (event.type === "delta") { process.stdout.write(event.content); } } console.log("Tokens used:", convo.getTokenCount()); console.log("Context window:", convo.getContextWindow()); ``` ### Pin messages that should never be evicted Pinned messages survive context window truncation. Use them for critical user facts, instructions, or system context that the AI must always see. Pin on send with `{ pinned: true }` or toggle later with `pinMessage()`. ```tsx title="PinnedMessages.tsx" function Chat() { const { messages, stream, pinMessage } = useConversation({ systemPrompt: "You are a helpful assistant.", maxTokens: 4096, }); async function handleSendPinned() { // Send a message and pin it so it's never evicted const msgId = await stream("My name is Alice. Remember this.", { pinned: true, }); // You can also pin/unpin after the fact: // pinMessage(msgId, true); } async function handleUnpin(messageId: string) { pinMessage(messageId, false); } return (
{messages.map((msg) => (
{msg.role}: {msg.content} {msg.pinned && 📌}
))}
); } ``` ### Auto-summarize evicted messages When `summarize: true` is set, evicted messages aren't just dropped — they're replaced with a summary that preserves key facts and context. This keeps the AI aware of earlier conversation even after truncation. #### React SDK ```tsx title="React SDK" const { messages, tokenCount } = useConversation({ systemPrompt: "You are a helpful assistant.", maxTokens: 4096, summarize: true, // auto-summarize evicted messages }); // When the context window fills up, older unpinned messages // are evicted and replaced with a summary message. // The summary preserves key facts, decisions, and context. ``` #### Web SDK ```ts title="Web SDK" const convo = new ConversationManager({ client, systemPrompt: "You are a helpful assistant.", maxTokens: 4096, summarize: true, summarizationPrompt: "Summarize preserving key facts and decisions.", }); // Evicted messages are summarized automatically. // The summary is added as a system-level message in the context. ``` ### Monitor token usage The `contextInfo` object gives a complete snapshot of context window usage: `usedTokens`, `maxTokens`, `remainingTokens`, and a `usageRatio` (0–1) that's perfect for progress bars. On the web SDK, call `convo.getContextInfo()` for the same data. ```tsx title="TokenMonitoring.tsx" function ChatWithTokenDisplay() { const { messages, stream, contextInfo, contextWindow, isStreaming, streamingContent, } = useConversation({ systemPrompt: "You are a helpful assistant.", maxTokens: 8192, }); const pct = Math.round(contextInfo.usageRatio * 100); return (
Tokens: {contextInfo.usedTokens} / {contextInfo.maxTokens} ({pct}%) {contextInfo.remainingTokens} remaining Context messages: {contextWindow.length} Total messages: {messages.length}
{/* Simple progress bar */}
80 ? "orange" : "#2563eb", borderRadius: 2, transition: "width 200ms", }} />
{pct > 80 && (

Context window is {pct}% full. Older messages will be evicted soon.

)} {messages.map((msg) => (
{msg.role}: {msg.content}
))} {isStreaming &&
AI: {streamingContent}
}
); } ``` ### Clear conversation Call `clearMessages()` to reset the conversation. On the web SDK, use `convo.clear()`. The system prompt is preserved. ```tsx title="ClearConversation.tsx" function Chat() { const { messages, stream, clearMessages, isStreaming } = useConversation({ systemPrompt: "You are a helpful assistant.", }); return (
{messages.map((msg) => (
{msg.role}: {msg.content}
))}
); } ``` ### Complete example A full chat UI with token monitoring, pinning, summarization, and clear: ```tsx title="App.tsx" ArlopassProvider, ChatReadyGate, useConversation, } from "@arlopass/react"; function Chat() { const { messages, streamingContent, isStreaming, tokenCount, contextWindow, stream, stop, clearMessages, pinMessage, } = useConversation({ systemPrompt: "You are a helpful assistant.", maxTokens: 8192, summarize: true, }); const [input, setInput] = useState(""); async function handleSubmit(e: React.FormEvent) { e.preventDefault(); if (!input.trim() || isStreaming) return; const text = input; setInput(""); await stream(text); } const usage = Math.round((tokenCount / 8192) * 100); return (
Tokens: {tokenCount} / 8192 ({usage}%) Context: {contextWindow.length} msgs
{messages.map((msg) => (
{msg.role === "user" ? "You" : "AI"}: {msg.content}
))} {isStreaming && streamingContent && (
AI: {streamingContent}
)}
setInput(e.target.value)} placeholder="Type a message..." disabled={isStreaming} style={{ flex: 1, padding: 8 }} /> {isStreaming && ( )}
); } return ( ); } ``` --- ## Tool calling Configure auto-execute and manual tool modes, handle streaming events, and set maxToolRounds. URL: https://arlopass.com/docs/guides/tool-calling You want the AI model to call functions in your application. ### Define a tool A tool has a `name`, `description`, optional `parameters` (JSON Schema), and an optional `handler`. The handler determines the execution mode. ### Auto-execute mode When a `handler` is provided, the SDK calls it automatically whenever the model invokes the tool. Use this for safe, read-only operations like lookups and searches. #### React SDK ```tsx title="AutoExecute.tsx" function Chat() { const { messages, stream, streamingContent, isStreaming } = useConversation({ systemPrompt: "You are a helpful assistant with access to tools.", tools: [ { name: "get_weather", description: "Get the current weather for a city.", parameters: { type: "object", properties: { city: { type: "string", description: "City name" }, units: { type: "string", description: "Temperature units", enum: ["celsius", "fahrenheit"], }, }, required: ["city"], }, // Handler provided → auto-execute mode handler: async (args) => { const res = await fetch( `/api/weather?city=${args.city}&units=${args.units ?? "celsius"}`, ); const data = await res.json(); return JSON.stringify(data); }, }, ], }); return (
{messages.map((msg) => (
{msg.role}: {msg.content}
))} {isStreaming &&
AI: {streamingContent}
}
); } ``` #### Web SDK ```ts title="auto-execute.ts" const client = new ArlopassClient({ transport: window.arlopass }); await client.connect({ appId: "my-app" }); const convo = new ConversationManager({ client, systemPrompt: "You are a helpful assistant with access to tools.", tools: [ { name: "get_weather", description: "Get the current weather for a city.", parameters: { type: "object", properties: { city: { type: "string", description: "City name" }, units: { type: "string", enum: ["celsius", "fahrenheit"] }, }, required: ["city"], }, handler: async (args) => { const res = await fetch( `/api/weather?city=${args.city}&units=${args.units ?? "celsius"}`, ); return JSON.stringify(await res.json()); }, }, ], }); for await (const event of convo.stream("What's the weather in Tokyo?")) { if (event.type === "delta") process.stdout.write(event.content); if (event.type === "tool_call") console.log("Calling:", event.name); if (event.type === "tool_result") console.log("Result:", event.result); } ``` ### Manual mode Omit the `handler` and the SDK emits a `tool_call` event instead of executing. Your app shows a confirmation UI, then calls `submitToolResult()` to return the result to the model. Use this for destructive or sensitive operations. #### React SDK ```tsx title="ManualMode.tsx" function Chat() { const { messages, stream, submitToolResult, streamingContent, isStreaming } = useConversation({ systemPrompt: "You are a helpful assistant.", tools: [ { name: "create_order", description: "Create a new order. Requires user confirmation.", parameters: { type: "object", properties: { item: { type: "string", description: "Item to order" }, quantity: { type: "number", description: "Number of items" }, }, required: ["item", "quantity"], }, // No handler → manual mode }, ], }); // Listen for tool calls from the subscribe API // or check incoming messages for tool_call events. // When the user confirms, submit the result: function handleConfirmOrder(toolCallId: string, item: string, qty: number) { // Perform the action const orderId = `ORD-${Date.now()}`; submitToolResult(toolCallId, JSON.stringify({ orderId, item, qty })); } return (
{messages.map((msg) => (
{msg.role}: {msg.content}
))} {isStreaming &&
AI: {streamingContent}
}
); } ``` #### Web SDK ```ts title="manual-mode.ts" const client = new ArlopassClient({ transport: window.arlopass }); await client.connect({ appId: "my-app" }); const convo = new ConversationManager({ client, tools: [ { name: "create_order", description: "Create a new order. Requires user confirmation.", parameters: { type: "object", properties: { item: { type: "string" }, quantity: { type: "number" }, }, required: ["item", "quantity"], }, // No handler → manual mode }, ], }); for await (const event of convo.stream("Order 3 widgets")) { if (event.type === "tool_call") { // Show confirmation UI, then submit result const orderId = `ORD-${Date.now()}`; convo.submitToolResult( event.toolCallId, JSON.stringify({ orderId, item: event.arguments.item, quantity: event.arguments.quantity, }), ); } if (event.type === "delta") { process.stdout.write(event.content); } } ``` ### Mixed mode Combine auto-execute and manual tools in the same conversation. Safe read-only tools get handlers; destructive tools don't. The SDK handles both seamlessly. ```tsx title="MixedMode.tsx" function Chat() { const { messages, stream, submitToolResult, isStreaming } = useConversation({ systemPrompt: "You are a helpful assistant.", tools: [ { // Auto-execute: safe, read-only lookup name: "search_products", description: "Search the product catalog.", parameters: { type: "object", properties: { query: { type: "string", description: "Search query" }, }, required: ["query"], }, handler: async (args) => { const res = await fetch(`/api/products?q=${args.query}`); return JSON.stringify(await res.json()); }, }, { // Manual: requires user confirmation before executing name: "place_order", description: "Place an order for a product.", parameters: { type: "object", properties: { productId: { type: "string" }, quantity: { type: "number" }, }, required: ["productId", "quantity"], }, // No handler — manual confirmation needed }, ], maxToolRounds: 3, }); // Auto-execute tools run silently. // Manual tools emit tool_call events for you to handle. return (
{messages.map((msg) => (
{msg.role}: {msg.content}
))}
); } ``` ### Limit tool rounds Set `maxToolRounds` to prevent infinite tool-call loops. The default is 5. After hitting the limit, the SDK stops executing tools and returns the model's text response. #### React SDK ```tsx title="React SDK" const { messages, stream } = useConversation({ tools: [ /* ... */ ], maxToolRounds: 3, // Stop after 3 rounds of tool calls (default: 5) }); // If the model keeps calling tools beyond maxToolRounds, // the SDK stops executing and returns the last text response. ``` #### Web SDK ```ts title="Web SDK" const convo = new ConversationManager({ client, tools: [ /* ... */ ], maxToolRounds: 3, // Stop after 3 rounds (default: 5) }); // Prevents infinite tool-call loops. // After 3 rounds, the stream yields text without further tool calls. ``` ### Subscribe to tool events Use the `subscribe()` function from `useConversation` to listen for `tool_call` and `tool_result` events. This is useful for logging, analytics, or showing tool activity in the UI. ```tsx title="ToolEvents.tsx" function Chat() { const { messages, stream, subscribe, isStreaming, streamingContent } = useConversation({ tools: [ { name: "lookup_user", description: "Look up a user by email.", parameters: { type: "object", properties: { email: { type: "string", description: "User email" }, }, required: ["email"], }, handler: async (args) => { const res = await fetch(`/api/users?email=${args.email}`); return JSON.stringify(await res.json()); }, }, ], }); // Subscribe to tool events for logging or UI updates subscribe("tool_call", (event) => { console.log("Tool called:", event.name, event.arguments); }); subscribe("tool_result", (event) => { console.log("Tool result:", event.name, event.result); }); return (
{messages.map((msg) => (
{msg.role}: {msg.content}
))} {isStreaming &&
AI: {streamingContent}
}
); } ``` ### Complete example A chat app with auto-execute weather lookup and manual reminder creation: ```tsx title="App.tsx" ArlopassProvider, ChatReadyGate, useConversation, } from "@arlopass/react"; const tools = [ { name: "get_weather", description: "Get the current weather for a city.", parameters: { type: "object" as const, properties: { city: { type: "string", description: "City name" }, }, required: ["city"] as const, }, handler: async (args: Record) => { // Simulate an API call return JSON.stringify({ city: args.city, temp: 22, condition: "sunny", }); }, }, { name: "create_reminder", description: "Create a reminder. Needs user confirmation.", parameters: { type: "object" as const, properties: { text: { type: "string", description: "Reminder text" }, time: { type: "string", description: "When to remind" }, }, required: ["text", "time"] as const, }, // No handler — manual mode }, ]; function Chat() { const { messages, streamingContent, isStreaming, stream, stop, submitToolResult, } = useConversation({ systemPrompt: "You are a helpful assistant with tools.", tools, maxToolRounds: 5, }); const [input, setInput] = useState(""); const [pendingTool, setPendingTool] = useState<{ id: string; name: string; args: Record; } | null>(null); async function handleSubmit(e: React.FormEvent) { e.preventDefault(); if (!input.trim() || isStreaming) return; const text = input; setInput(""); await stream(text); } function handleConfirm() { if (!pendingTool) return; const reminderId = `REM-${Date.now()}`; submitToolResult( pendingTool.id, JSON.stringify({ reminderId, ...pendingTool.args }), ); setPendingTool(null); } return (
{messages.map((msg) => (
{msg.role === "user" ? "You" : "AI"}: {msg.content}
))} {isStreaming && streamingContent && (
AI: {streamingContent}
)}
{pendingTool && (

Create reminder: "{pendingTool.args.text}" at{" "} {pendingTool.args.time}?

)}
setInput(e.target.value)} placeholder="Type a message..." disabled={isStreaming} style={{ flex: 1, padding: 8 }} /> {isStreaming && ( )}
); } return ( ); } ``` --- ## Error handling Handle retryable errors, use error boundaries, and reference SDK error codes. URL: https://arlopass.com/docs/guides/error-handling You want to handle errors gracefully — retryable timeouts, fatal auth failures, and everything in between. ### Retryable vs non-retryable errors Every `ArlopassSDKError` has a `retryable` boolean. Timeouts and transient network errors are retryable. Auth failures, policy violations, and state errors are not. Both hooks and the web SDK expose a `retry()` function that replays the last failed operation when the error is retryable. #### React SDK ```tsx title="RetryableErrors.tsx" function Chat() { const connection = useConnection(); const { messages, stream, error, retry, isStreaming, streamingContent } = useConversation({ systemPrompt: "You are a helpful assistant.", }); if (connection.error) { return (

Connection error: {connection.error.message}

{connection.error.retryable && connection.retry && ( )}
); } if (error) { return (

Chat error: {error.message}

Code: {error.machineCode}

{error.retryable && retry && ( )}
); } return (
{messages.map((msg) => (
{msg.role}: {msg.content}
))} {isStreaming &&
AI: {streamingContent}
}
); } ``` #### Web SDK ```ts title="retryable-errors.ts" const client = new ArlopassClient({ transport: window.arlopass }); try { await client.connect({ appId: "my-app" }); } catch (err) { const sdkError = err as ArlopassSDKError; console.error(sdkError.machineCode, sdkError.message); if (sdkError.retryable) { // Safe to retry — transient network issue or timeout await client.connect({ appId: "my-app" }); } else { // Fatal — auth failure, policy violation, etc. throw err; } } const convo = new ConversationManager({ client }); try { for await (const event of convo.stream("Hello!")) { if (event.type === "delta") process.stdout.write(event.content); } } catch (err) { const sdkError = err as ArlopassSDKError; if (sdkError.retryable) { // Retry the stream for await (const event of convo.stream("Hello!")) { if (event.type === "delta") process.stdout.write(event.content); } } } ``` ### ArlopassErrorBoundary Wrap your app (or sections of it) in `ArlopassErrorBoundary` to catch unhandled exceptions. It provides a `fallback` render function and an optional `onError` callback for logging. ```tsx title="ErrorBoundary.tsx" function ErrorFallback({ error, resetErrorBoundary, }: { error: Error; resetErrorBoundary: () => void; }) { return (

Something went wrong

{error.message}

); } return ( ); } ``` ### ArlopassHasError guard `ArlopassHasError` is a negative guard — it only renders when there's an active error. Use it in headers, sidebars, or toast areas to show error state outside the main content area. ```tsx title="ErrorBanner.tsx" function AppHeader() { return (

My App

); } ``` ### Global error callback Pass `onError` to `ArlopassProvider` to receive every SDK error. Use it for error tracking, analytics, or global logging. ```tsx title="GlobalErrorHandler.tsx" function App() { return ( ); } ``` ### Error codes Key error codes and whether they're retryable: ### Complete example A chat app with layered error handling — error boundary, connection gate fallbacks, inline chat errors, and global logging: ```tsx title="App.tsx" ArlopassProvider, ArlopassErrorBoundary, ArlopassChatReadyGate, ArlopassHasError, useConversation, } from "@arlopass/react"; function Chat() { const { messages, streamingContent, isStreaming, stream, stop, error, retry, } = useConversation({ systemPrompt: "You are a helpful assistant.", }); const [input, setInput] = useState(""); async function handleSubmit(e: React.FormEvent) { e.preventDefault(); if (!input.trim() || isStreaming) return; const text = input; setInput(""); await stream(text); } return (
{error && (
Chat error: {error.message} {error.retryable && retry && ( )}
)}
{messages.map((msg) => (
{msg.role === "user" ? "You" : "AI"}: {msg.content}
))} {isStreaming && streamingContent && (
AI: {streamingContent}
)}
setInput(e.target.value)} placeholder="Type a message..." disabled={isStreaming} style={{ flex: 1, padding: 8 }} /> {isStreaming && ( )}
); } return ( ); } ``` --- ## Testing your app Mock transports, test streaming, simulate errors, and verify tool calls. URL: https://arlopass.com/docs/guides/testing You want to write tests for components that use Arlopass hooks. ### Create a mock transport `createMockTransport()` builds a fake transport that simulates the Arlopass extension. Configure responses, errors, latency, and streaming behaviour without a real extension or AI provider. #### React SDK ```tsx title="React component test" createMockTransport, MockArlopassProvider, } from "@arlopass/react/testing"; describe("Chat", () => { it("renders a greeting from the AI", async () => { const transport = createMockTransport({ // Configure the mock response chatResponse: "Hello! How can I help you?", // Simulate 100ms latency latency: 100, // Available providers providers: [ { providerId: "mock", providerName: "Mock", models: ["mock-model"] }, ], }); render( , ); // Your component is now connected to a mock Arlopass backend // that responds with "Hello! How can I help you?" }); }); ``` #### Web SDK ```ts title="Web SDK unit test" describe("ConversationManager", () => { it("streams a response", async () => { const transport = createMockTransport({ streamChunks: ["Hello", " world", "!"], latency: 50, }); const client = new ArlopassClient({ transport }); await client.connect({ appId: "test" }); const convo = new ConversationManager({ client }); let result = ""; for await (const event of convo.stream("Hi")) { if (event.type === "delta") result += event.content; } expect(result).toBe("Hello world!"); }); }); ``` ### MockArlopassProvider `MockArlopassProvider` is a drop-in test wrapper that injects the mock transport into `window.arlopass` and wraps children with `ArlopassProvider`. Use it in every React component test that uses Arlopass hooks. ### Test error scenarios Use `chatError` to simulate chat failures and `failOn` to make specific capabilities fail. This lets you test your error UI and retry logic. ```tsx title="error-tests.tsx" createMockTransport, MockArlopassProvider, } from "@arlopass/react/testing"; describe("Chat error handling", () => { it("shows error when chat fails", async () => { const transport = createMockTransport({ chatError: new Error("Model overloaded"), }); render( , ); // Component should display the error }); it("shows error when a specific capability fails", async () => { const transport = createMockTransport({ failOn: "provider.list", }); render( , ); // Provider listing fails — component should show fallback }); }); ``` ### Test streaming Use `streamChunks` for fine-grained control over chunk delivery, or `streamResponse` for a convenience string that auto-splits. Combine with `latency` to simulate realistic streaming timing. ```tsx title="streaming-tests.tsx" createMockTransport, MockArlopassProvider, } from "@arlopass/react/testing"; describe("Chat streaming", () => { it("shows streaming content chunk by chunk", async () => { const transport = createMockTransport({ streamChunks: ["The ", "answer ", "is ", "42."], latency: 10, }); render( , ); const input = screen.getByPlaceholderText("Type a message..."); const sendBtn = screen.getByText("Send"); await userEvent.type(input, "What is the answer?"); await userEvent.click(sendBtn); await waitFor(() => { expect(screen.getByText(/42/)).toBeInTheDocument(); }); }); it("uses streamResponse for full response mock", async () => { const transport = createMockTransport({ streamResponse: "The answer is 42.", }); render( , ); // streamResponse auto-splits into chunks for streaming }); }); ``` ### Integration tests with window.arlopass For integration tests that mount your full app (not just wrapped components), use `mockWindowArlopass()` and `cleanupWindowArlopass()` to control the global transport. Always clean up in `afterEach`. ```ts title="integration-tests.ts" mockWindowArlopass, cleanupWindowArlopass, } from "@arlopass/react/testing"; describe("Integration tests", () => { afterEach(() => { // Always clean up window.arlopass after each test cleanupWindowArlopass(); }); it("injects transport into window.arlopass", () => { const transport = createMockTransport({ chatResponse: "Hello!", }); // Simulate the extension injecting the transport mockWindowArlopass(transport); // Now window.arlopass is available — your app will // detect the extension as installed expect(window.arlopass).toBeDefined(); }); it("simulates extension not installed", () => { // Don't call mockWindowArlopass — window.arlopass is undefined // Your app's "not installed" UI should render expect(window.arlopass).toBeUndefined(); }); }); ``` ### Complete test example A full test suite with vitest and @testing-library/react covering messaging, streaming, errors, and input state: ```tsx title="Chat.test.tsx" createMockTransport, MockArlopassProvider, } from "@arlopass/react/testing"; // Component under test function Chat() { const { messages, streamingContent, isStreaming, stream, stop, error, retry, } = useConversation({ systemPrompt: "You are a helpful assistant.", }); const [input, setInput] = useState(""); async function handleSubmit(e: React.FormEvent) { e.preventDefault(); if (!input.trim() || isStreaming) return; const text = input; setInput(""); await stream(text); } return (
{error && (
{error.message} {retry && }
)}
{messages.map((msg) => (
{msg.content}
))}
{isStreaming &&
{streamingContent}
}
setInput(e.target.value)} placeholder="Type a message..." disabled={isStreaming} /> {isStreaming && }
); } describe("Chat component", () => { it("sends a message and displays response", async () => { const transport = createMockTransport({ streamChunks: ["Hello", " there", "!"], latency: 10, }); render( , ); const input = screen.getByPlaceholderText("Type a message..."); const sendBtn = screen.getByText("Send"); await userEvent.type(input, "Hi"); await userEvent.click(sendBtn); // User message appears immediately await waitFor(() => { expect(screen.getByTestId("user")).toHaveTextContent("Hi"); }); // AI response appears after streaming completes await waitFor(() => { expect(screen.getByTestId("assistant")).toHaveTextContent("Hello there!"); }); }); it("shows error and allows retry", async () => { const transport = createMockTransport({ chatError: new Error("Timeout"), }); render( , ); const input = screen.getByPlaceholderText("Type a message..."); await userEvent.type(input, "Hi"); await userEvent.click(screen.getByText("Send")); await waitFor(() => { expect(screen.getByTestId("error")).toBeInTheDocument(); }); }); it("disables input while streaming", async () => { const transport = createMockTransport({ streamChunks: ["Thinking", "...", " done"], latency: 100, }); render( , ); const input = screen.getByPlaceholderText("Type a message..."); await userEvent.type(input, "Hello"); await userEvent.click(screen.getByText("Send")); // Input should be disabled while streaming await waitFor(() => { expect(input).toBeDisabled(); }); // After streaming completes, input is re-enabled await waitFor( () => { expect(input).not.toBeDisabled(); }, { timeout: 2000 }, ); }); }); ``` --- ## Guard components Gate UI on connection, provider, and chat readiness with positive and negative guards. URL: https://arlopass.com/docs/guides/guard-components You want to conditionally render UI based on connection, provider, and error states. ### Positive gates Gates render their children only when a condition is met. They accept fallback props for each negative state. **ArlopassConnectionGate** Renders children when connected. Shows `fallback` while connecting, `notInstalledFallback` if the extension isn't detected, and `errorFallback` on failure. ```tsx title="ConnectionGate.tsx" function App() { return ( ); } ``` **ArlopassProviderGate** Renders children when a provider is selected. Shows `fallback` if no provider is chosen yet. ```tsx title="ProviderGate.tsx" function ConnectedApp() { return ( ); } ``` **ArlopassChatReadyGate** Combines connection and provider checks in a single gate. This is the most common gate for chat UIs — it handles not-installed, connecting, no-provider, and error states. ```tsx title="ChatReadyGate.tsx" // ArlopassChatReadyGate combines connection + provider checks in one gate function App() { return ( ); } ``` ### Negative guards Negative guards render only when a specific negative condition is true. Use them in headers, sidebars, or any area outside your main content gates. ```tsx title="NegativeGuards.tsx" ArlopassNotInstalled, ArlopassDisconnected, ArlopassConnected, ArlopassProviderNotReady, ArlopassHasError, ArlopassChatNotReady, ArlopassChatReady, } from "@arlopass/react"; function AppHeader() { return (

My App

{/* Show install prompt when extension is missing */} {/* Show reconnect button when disconnected */} {/* Show green dot when connected */} {/* Prompt to select provider when none is chosen */} {/* Show errors in the header */}
); } ``` ### Nesting gates Compose gates by nesting them. Each layer handles one concern. Put negative guards inside the layout for status indicators that always show. ```tsx title="NestedGates.tsx" ArlopassProvider, ArlopassConnectionGate, ArlopassProviderGate, ArlopassHasError, } from "@arlopass/react"; function App() { return ( ); } ``` ### Render function on ArlopassHasError `ArlopassHasError` uses a render function as children. It receives the error object and an optional `retry` function. Use it to build different UIs for retryable vs fatal errors. ```tsx title="HasErrorRender.tsx" function StatusBar() { return (
); } ``` ### Guards vs manual state checking The React SDK guards replace manual if/else checking against connection and provider state: #### React SDK ```tsx title="React SDK (guards)" ArlopassProvider, ArlopassChatReadyGate, ArlopassHasError, useConversation, } from "@arlopass/react"; function App() { return ( ); } ``` #### Web SDK ```ts title="Web SDK (manual)" const client = new ArlopassClient({ transport: window.arlopass }); if (!window.arlopass) { showInstallPrompt(); } else { try { await client.connect({ appId: "my-app" }); } catch (err) { showError(err); } if (!client.selectedProvider) { showProviderPicker(); } else { startChat(client); } } ``` ### Complete example A full app using connection gate, provider gate, negative guards in a header, and error handling: ```tsx title="App.tsx" ArlopassProvider, ArlopassConnectionGate, ArlopassProviderGate, ArlopassHasError, ArlopassConnected, ArlopassDisconnected, ArlopassNotInstalled, useConversation, } from "@arlopass/react"; function Header() { return (

Chat App

); } function Chat() { const { messages, stream, streamingContent, isStreaming, stop } = useConversation({ systemPrompt: "You are a helpful assistant." }); const [input, setInput] = useState(""); async function handleSubmit(e: React.FormEvent) { e.preventDefault(); if (!input.trim() || isStreaming) return; const text = input; setInput(""); await stream(text); } return (
{messages.map((msg) => (
{msg.role === "user" ? "You" : "AI"}: {msg.content}
))} {isStreaming && streamingContent && (
AI: {streamingContent}
)}
setInput(e.target.value)} placeholder="Type a message..." disabled={isStreaming} style={{ flex: 1, padding: 8 }} /> {isStreaming && ( )}
); } return ( ); } ``` --- ## Security model Understand endpoint validation, origin enforcement, vault encryption, and zero-trust architecture. URL: https://arlopass.com/docs/guides/security You want to understand how Arlopass protects credentials and ensures safe AI access. ### Injected transport only The Arlopass browser extension injects a `ArlopassTransport` object at `window.arlopass`. This is the only communication channel between your app and AI providers. There is no way to construct an arbitrary transport in production — the SDK only accepts what the extension provides. ```ts title="Transport detection" // The Arlopass extension injects window.arlopass — a ArlopassTransport instance. // This is the ONLY way to communicate with AI providers. // React SDK — automatic detection function App() { return ( // ArlopassProvider detects window.arlopass automatically. // If it's not present, the app shows a "not installed" state. ); } // Web SDK — explicit transport reference // The transport comes from the extension — never from user code const client = new ArlopassClient({ transport: window.arlopass }); ``` ### Origin enforcement Every request envelope includes `window.location.origin`. The extension verifies this server-side — it's not configurable by app code. A page on a different origin cannot access your session, and an iframe cannot impersonate your app. ```ts title="Origin verification" // The extension verifies the origin of every message. // It uses window.location.origin — not a configurable value. // // This means: // 1. A malicious page on a different origin cannot access your session // 2. A rogue iframe cannot impersonate your app // 3. The origin check is performed by the extension, not the SDK // // You don't need to do anything — this is automatic. // The SDK adds the origin to every request envelope: // Inside the SDK (you never write this): const envelope = { origin: window.location.origin, // e.g. "https://myapp.com" // ... other fields }; ``` ### Context isolation In the React SDK, hooks are the only access path to the Arlopass client. There's no global state, no static methods, and no way to bypass the state machine. Each component gets an isolated view of the conversation. ```tsx title="Hook isolation" // Hooks are the ONLY access path to the Arlopass client. // There is no global state, no static methods, no direct client access. function Chat() { // Each hook call creates an isolated context. // Components can only interact with Arlopass through these hooks. const { messages, stream } = useConversation({ systemPrompt: "You are a helpful assistant.", }); const { isConnected } = useConnection(); // There's no way to: // - Access the raw transport // - Bypass the state machine // - Read another component's conversation // - Call the client directly } ``` ### Credential isolation The SDK never handles API keys. Users enter credentials in the browser extension, which sends them to the native bridge for encrypted vault storage. The bridge attaches credentials to requests internally — your app code never sees them. Keys don't live in browser storage at all — they're in an encrypted vault file on disk. ```ts title="No credentials in app code" // The SDK NEVER handles API keys, tokens, or credentials. // // Credential flow: // 1. User enters API key in the Arlopass browser extension popup // 2. Extension sends it to the native bridge for vault storage // 3. Bridge encrypts the key (AES-256-GCM) and writes it to the vault file // 4. When a request arrives, the bridge reads from the vault and attaches credentials // 5. SDK sends requests through the transport — never sees keys // // This means: // - Your app code never contains API keys // - Keys can't leak through your app's JavaScript bundle // - Keys can't be extracted via browser DevTools on your page // - A compromised dependency in your app can't steal credentials // - Keys aren't even in browser storage — they live in an encrypted vault file // // Even the Web SDK follows this pattern: const client = new ArlopassClient({ transport: window.arlopass }); await client.connect({ appId: "my-app" }); // No API key needed here — the transport handles auth internally const convo = new ConversationManager({ client }); for await (const event of convo.stream("Hello!")) { // Credentials were attached by the bridge vault, not your code } ``` ### Envelope security Every message uses a structured envelope with timestamps, nonces, TTL, and correlation IDs. Expired envelopes are rejected. Replayed envelopes are detected. Response spoofing is caught by correlation ID matching. ```ts title="Envelope format" // Every message between the SDK and extension uses a secure envelope. // // Envelope fields: // { // protocolVersion: "1.0.0", // Protocol version check // requestId: "uuid", // Unique per request // correlationId: "uuid", // Links request/response pairs // origin: "https://...",// Verified origin // sessionId: "uuid", // Scoped to this session // capability: "chat.stream",// What operation is requested // issuedAt: "ISO-8601", // Timestamp // expiresAt: "ISO-8601", // TTL — expired envelopes are rejected // nonce: "random", // Prevents replay attacks // payload: { ... }, // The actual request data // } // // Security properties: // - Timestamps + TTL: stale requests are rejected // - Nonces: replayed requests are detected and rejected // - Correlation IDs: response spoofing is detected // - Protocol version: incompatible versions are rejected early ``` --- ### Vault-based credential storage Credentials are encrypted at rest in a vault file managed by the native bridge — not in browser storage. The vault uses AES-256-GCM encryption with PBKDF2 key derivation (210,000 iterations). Users choose between a master password or OS keychain (Windows Credential Manager, macOS Keychain, Linux libsecret). The vault is cross-browser: set it up once, and Chrome, Edge, and Firefox all share the same credentials. ```text title="Vault encryption" // Credentials are encrypted at rest in a vault file on the native bridge. // The extension acts as a thin client — all secrets live on the bridge. // // Encryption: // Algorithm: AES-256-GCM (authenticated encryption) // Key derivation: PBKDF2 with 210,000 iterations (SHA-256) // // Two key modes: // // 1. Master Password (PBKDF2) // User picks a password → PBKDF2 derives a 256-bit key → encrypts vault // The password never leaves the machine. // // 2. OS Keychain (platform-native) // A random 32-byte key is generated and stored in: // - Windows: Credential Manager // - macOS: Keychain // - Linux: libsecret (GNOME Keyring / KDE Wallet) // The OS protects the key — unlocking the vault is automatic on login. // // Cross-browser: // The vault is a single file on disk managed by the native bridge. // Set it up once — Chrome, Edge, and Firefox all use the same vault. // No per-browser credential duplication. ``` ### Vault lifecycle The vault follows a predictable lifecycle from first run to auto-lock. On first launch, the extension walks you through vault setup. On subsequent opens, it checks vault status and auto-unlocks if using OS keychain. After 30 minutes of inactivity the vault locks automatically. If a web app triggers an AI request while the vault is locked, the extension popup opens for re-authentication and the SDK retries automatically. ```text title="Vault lifecycle" // Vault lifecycle — what happens at each stage: // // ┌─────────────────────────────────────────────────────────┐ // │ FIRST RUN │ // │ Extension detects no vault → shows vault setup screen │ // │ User chooses: master password or OS keychain │ // │ Bridge creates an empty encrypted vault file │ // └──────────────────────┬──────────────────────────────────┘ // ▼ // ┌─────────────────────────────────────────────────────────┐ // │ BROWSER OPEN │ // │ Extension sends vault status check to bridge │ // │ If locked → unlock screen (password prompt) │ // │ If keychain mode → auto-unlock (OS handles auth) │ // └──────────────────────┬──────────────────────────────────┘ // ▼ // ┌─────────────────────────────────────────────────────────┐ // │ DURING USE │ // │ All provider/credential operations go through vault │ // │ Single persistent native messaging connection to bridge │ // │ Extension reads from vault on demand — never caches keys │ // └──────────────────────┬──────────────────────────────────┘ // ▼ // ┌─────────────────────────────────────────────────────────┐ // │ AUTO-LOCK (30 min inactivity) │ // │ Bridge locks the vault → clears derived key from memory │ // │ Next operation triggers re-authentication │ // └──────────────────────┬──────────────────────────────────┘ // ▼ // ┌─────────────────────────────────────────────────────────┐ // │ WEB APP TRIGGER │ // │ SDK makes request → vault is locked → extension notified │ // │ Extension popup opens → user unlocks → SDK retries │ // │ Retry is automatic — the web app doesn't need to handle │ // │ vault state. Just stream as usual. │ // └─────────────────────────────────────────────────────────┘ ``` ### Zero-knowledge design Web apps never see API keys — the bridge attaches them server-side. The extension popup never holds keys in memory — it reads from the vault on demand. The vault file is encrypted at rest — even if someone copies it, they need the master password or OS keychain access to decrypt it. ```text title="Zero-knowledge boundaries" // Zero-knowledge design — who sees what: // // Web apps: // ❌ Never see API keys // ❌ Never see vault contents // ✅ Send requests through the transport // ✅ Receive AI responses // The bridge attaches credentials server-side — the web app // only sees the conversation, never the auth material. // // Extension popup: // ❌ Never holds keys in memory long-term // ✅ Reads provider list from vault on demand // ✅ Writes new credentials to vault (then forgets them) // The popup is a thin UI layer — it doesn't cache secrets. // // Vault file on disk: // ✅ Encrypted at rest (AES-256-GCM) // ✅ Authenticated (GCM auth tag prevents tampering) // Even if someone copies the vault file, they need the // master password or OS keychain access to decrypt it. // // Native bridge process: // ✅ Holds the derived key in memory while vault is unlocked // ✅ Clears the key on lock or exit // ✅ Single process — no key duplication across browsers ``` ### Key hierarchy In password mode, the master password is run through PBKDF2 (210K iterations, SHA-256) with a random salt to derive an AES-256-GCM key. In keychain mode, a random 32-byte key is stored in the OS keychain and used directly. The vault file has a 64-byte header containing the magic number, version, key mode, salt, and IV, followed by the encrypted JSON payload and a GCM authentication tag. ```text title="Key derivation & vault format" // Key hierarchy — how vault encryption works: // // Password mode: // Master Password // → PBKDF2 (210,000 iterations, SHA-256, random salt) // → 256-bit AES-GCM key // → Encrypts vault JSON payload // // Keychain mode: // OS Keychain // → Stores a random 32-byte key (generated once) // → 256-bit AES-GCM key // → Encrypts vault JSON payload // // Vault file format: // ┌──────────────────────────────────────────────┐ // │ Bytes 0–3: Magic number (0x41524C4F) │ // │ Bytes 4–5: Version (uint16) │ // │ Byte 6: Key mode (0=password, 1=keychain)│ // │ Bytes 7–22: Salt (16 bytes, PBKDF2) │ // │ Bytes 23–34: IV (12 bytes, AES-GCM) │ // │ Bytes 35–98: Reserved / alignment (64-byte hdr)│ // │ Bytes 64+: Encrypted JSON + 16-byte auth tag│ // └──────────────────────────────────────────────┘ // // Properties: // - Salt is unique per vault → same password yields different keys // - IV is unique per write → same data yields different ciphertext // - Auth tag → any file tampering is detected on decrypt ``` --- ### App identity & validation Every app that connects to the extension is identified by an `appId` — a reverse-domain string derived from the page origin (e.g. `com.myapp.chat`). The SDK generates this automatically, and the extension validates it against the actual origin to prevent spoofing. ```tsx title="Auto-derived appId" // The SDK auto-derives an appId from the page origin using reverse-domain notation. // You don't need to provide one explicitly — it's generated for you. // Examples: // https://myapp.com → "com.myapp" // https://chat.example.org → "org.example.chat" // http://localhost:5173 → "localhost" (dev origin — no prefix required) // React SDK — auto-derived, no appId needed: ; // Web SDK — auto-derived: const client = new ArlopassClient({ transport: window.arlopass }); await client.connect({ appSuffix: "chat" }); // → "com.myapp.chat" // Explicit override (must match your domain): await client.connect({ appId: "com.myapp.dashboard" }); ``` #### Origin validation On production domains, the extension checks that the appId starts with the correct reverse-domain prefix. A page on `https://myapp.com` can only claim an appId starting with `com.myapp`. Dev origins (localhost, 127.0.0.1, etc.) are exempt from this check for local development convenience. ```ts title="Validation rules" // The extension validates the appId against the page's actual origin. // Production apps MUST use the correct reverse-domain prefix. // // ✅ On https://myapp.com: // appId: "com.myapp" → valid // appId: "com.myapp.chat" → valid (suffix ok) // // ❌ On https://myapp.com: // appId: "com.otherapp" → REJECTED (wrong domain) // appId: "com.myappx" → REJECTED (must be dot-separated) // // Dev origins (localhost, 127.0.0.1, [::1], *.local) skip this check. // Any appId is accepted during local development. ``` #### App metadata You can pass optional metadata — name, description, and icon — that the extension displays in its connection approval popup. Icon URLs must use HTTPS or a `data:` URI on production; HTTP is only allowed on dev origins. ```tsx title="App metadata" // Pass app metadata during connect for richer extension UI. // The extension shows this info in the connection approval popup. // React SDK: ; // Web SDK: await client.connect({ appSuffix: "chat", appName: "My Chat App", appDescription: "AI-powered customer support", appIcon: "https://myapp.com/icon.png", }); // Icon URL rules: // ✅ https://... — always accepted // ✅ data:image/... — always accepted // ✅ http://... — accepted on dev origins only (localhost, etc.) // ❌ http://... on production — rejected (must be HTTPS) ``` ### Safe defaults The SDK ships with safe defaults: auto-connect, built-in timeouts, state machine enforcement, and no `dangerouslySetInnerHTML` anywhere. AI responses are rendered as plain text nodes — never as raw HTML. ```tsx title="Safe defaults" // The SDK ships with safe defaults that you don't need to configure: // autoConnect: true — connects as soon as the provider mounts // Timeouts — all operations have built-in timeouts // Error boundary — wrap with ArlopassErrorBoundary for crash protection // State machine — invalid state transitions throw immediately // No dangerouslySetInnerHTML anywhere in the SDK. // All user content is rendered as text nodes, never as HTML. // AI responses are treated as plain text by default. function App() { return ( ); } ``` --- ## Playground Live SDK sandbox — connect, list providers, send messages, stream responses. URL: https://arlopass.com/docs/interactive/playground --- ## Connection Configure transport profile, app identity, and manage SDK connections. URL: https://arlopass.com/docs/interactive/connection --- ## Providers Browse available AI providers and select models for chat. URL: https://arlopass.com/docs/interactive/providers --- ## Chat View chat message history and transcripts from SDK operations. URL: https://arlopass.com/docs/interactive/chat --- ## Streaming Real-time streaming demo — watch AI responses arrive chunk by chunk. URL: https://arlopass.com/docs/interactive/streaming --- ## Event log SDK operation audit log — track all connect, list, send, and stream events. URL: https://arlopass.com/docs/interactive/event-log --- ## ArlopassProvider Root context provider for the React SDK — wrap your app to enable all hooks and guard components. URL: https://arlopass.com/docs/reference/react/provider The root context provider. Wrap your app (or the subtree that needs AI) with ` ### Examples **Basic usage** ```tsx function App() { return ( ); } ``` **Auto-select provider and model** ```tsx ``` **App identity** ```tsx ``` **Manual connect with error callback** ```tsx ``` --- ## Hooks React hooks for connection, providers, chat, conversations, and client access. URL: https://arlopass.com/docs/reference/react/hooks All hooks must be called inside a ` --- ### useConversation Full-featured conversation hook built on `ConversationManager`. Adds tool calling, context-window management, token counting, message pinning, and summarization. ```tsx ``` **Options** **Return value** Promise", description: "Send a message. Optionally pin it to survive summarization.", }, { name: "stream", type: "(content: string, options?: { pinned?: boolean }) => Promise", description: "Stream a response. Optionally pin the user message.", }, { name: "stop", type: "() => void", description: "Abort the current stream.", }, { name: "clearMessages", type: "() => void", description: "Clear all messages, reset token count, and clear the ConversationManager.", }, { name: "pinMessage", type: "(messageId: MessageId, pinned: boolean) => void", description: "Toggle pin status for a message. Pinned messages survive context-window summarization.", }, { name: "submitToolResult", type: "(toolCallId: string, result: string) => void", description: "Submit a result for a manual tool call (tools without a handler).", }, { name: "retry", type: "(() => Promise) | null", description: "Retry the last failed operation. Null when not retryable.", }, { name: "subscribe", type: "ChatSubscribe", description: 'Subscribe to events: "response", "stream", "error", "tool_call", "tool_result", "tool_priming_start", "tool_priming_match", "tool_priming_end".', }, ]} /> ```tsx const { messages, stream, tokenCount, contextInfo, pinMessage } = useConversation({ systemPrompt: "You are a helpful assistant.", tools: [ { name: "search", description: "Search the web", handler: async (args) => "results", }, ], }); // Build a usage meter from contextInfo const pct = Math.round(contextInfo.usageRatio * 100); console.log( `${pct}% of context used (${contextInfo.remainingTokens} tokens left)`, ); ``` --- ### useClient Escape hatch to the underlying `ArlopassClient` instance. Returns `null` when the transport is unavailable or the client is in a `"disconnected"` or `"failed"` state. ```tsx ``` ```tsx const client = useClient(); // Use for advanced operations not covered by other hooks ``` --- ## Guard components Conditionally render children based on connection state, provider selection, and error conditions. URL: https://arlopass.com/docs/reference/react/guards Guards conditionally render children based on connection state, provider selection, and error conditions. Import from `@arlopass/react/guards`. ```tsx ArlopassConnectionGate, ArlopassProviderGate, ArlopassChatReadyGate, ArlopassNotInstalled, ArlopassDisconnected, ArlopassConnected, ArlopassProviderNotReady, ArlopassHasError, ArlopassChatNotReady, ArlopassChatReady, ArlopassErrorBoundary, } from "@arlopass/react/guards"; ``` --- ### Positive gates Gates render their children when a condition is met and show a fallback otherwise. #### ArlopassConnectionGate Renders children when the client is connected. Shows fallbacks for loading, error, and not-installed states. ``` #### ArlopassProviderGate Renders children when a provider/model pair is selected. ```tsx }> ``` #### ArlopassChatReadyGate All-in-one gate that checks connection, provider selection, and error state. Renders children only when everything is ready to chat. ``` --- ### Negative guards Negative guards render children when a condition is **not** met. They accept `ReactNode` or a render function as children. ``` --- ### ArlopassErrorBoundary A React error boundary that catches rendering errors in the child tree. Standard class-component boundary with a render-prop fallback. ``` --- ## Types TypeScript types exported from @arlopass/react and re-exported from @arlopass/web-sdk. URL: https://arlopass.com/docs/reference/react/types All types are importable from `@arlopass/react`. This includes React-specific types and re-exports from `@arlopass/web-sdk`. ```tsx MessageId, TrackedChatMessage, ToolCallInfo, SubscriptionEvent, ChatSubscribe, ChatSubscribeNoTools, ArlopassProviderProps, } from "@arlopass/react"; ``` --- ### React SDK types #### MessageId ```tsx type MessageId = string; ``` Opaque string identifier for tracked messages (UUID v4). #### TrackedChatMessage ```tsx type TrackedChatMessage = Readonly<{ id: MessageId; role: ChatRole; content: string; inResponseTo?: MessageId; status: "pending" | "streaming" | "complete" | "error"; pinned: boolean; toolCalls?: readonly ToolCallInfo[]; }>; ``` Immutable message object returned by `useChat` and `useConversation`. Extends the base `ChatMessage` with tracking metadata. #### ToolCallInfo ```tsx type ToolCallInfo = Readonly<{ toolCallId: string; name: string; arguments: Record; result?: string; status: "pending" | "executing" | "complete" | "error"; }>; ``` Attached to a `TrackedChatMessage` when the assistant invokes tools. #### SubscriptionEvent ```tsx type SubscriptionEvent = | "response" | "stream" | "error" | "tool_call" | "tool_result" | "tool_priming_start" | "tool_priming_match" | "tool_priming_end"; ``` #### ChatSubscribe ```tsx type ChatSubscribe = { ( event: "response", messageId: MessageId, handler: (msg: TrackedChatMessage) => void, ): () => void; (event: "response", handler: (msg: TrackedChatMessage) => void): () => void; ( event: "stream", messageId: MessageId, handler: (delta: string, accumulated: string) => void, ): () => void; ( event: "error", handler: (error: ArlopassSDKError, messageId: MessageId | null) => void, ): () => void; ( event: "error", messageId: MessageId, handler: (error: ArlopassSDKError) => void, ): () => void; ( event: "tool_call", handler: ( toolCallId: string, name: string, args: Record, messageId: MessageId, ) => void, ): () => void; ( event: "tool_call", messageId: MessageId, handler: ( toolCallId: string, name: string, args: Record, ) => void, ): () => void; ( event: "tool_result", handler: ( toolCallId: string, name: string, result: string, messageId: MessageId, ) => void, ): () => void; ( event: "tool_result", messageId: MessageId, handler: (toolCallId: string, name: string, result: string) => void, ): () => void; (event: "tool_priming_start", handler: (message: string) => void): () => void; ( event: "tool_priming_match", handler: (tools: readonly string[]) => void, ): () => void; (event: "tool_priming_end", handler: () => void): () => void; }; ``` Overloaded subscribe function returned by `useConversation`. Supports targeted subscriptions by message ID. #### ChatSubscribeNoTools ```tsx type ChatSubscribeNoTools = { ( event: "response", messageId: MessageId, handler: (msg: TrackedChatMessage) => void, ): () => void; (event: "response", handler: (msg: TrackedChatMessage) => void): () => void; ( event: "stream", messageId: MessageId, handler: (delta: string, accumulated: string) => void, ): () => void; ( event: "error", handler: (error: ArlopassSDKError, messageId: MessageId | null) => void, ): () => void; ( event: "error", messageId: MessageId, handler: (error: ArlopassSDKError) => void, ): () => void; }; ``` Restricted subscribe type returned by `useChat` (no tool events). #### ArlopassProviderProps ```tsx type ArlopassProviderProps = Readonly<{ appId?: string; appSuffix?: string; appName?: string; appDescription?: string; appIcon?: string; defaultProvider?: string; defaultModel?: string; autoConnect?: boolean; onError?: (error: ArlopassSDKError) => void; children: React.ReactNode; }>; ``` #### ClientSnapshot ```tsx type ClientSnapshot = Readonly<{ state: ClientState; sessionId: string | null; selectedProvider: Readonly<{ providerId: string; modelId: string }> | null; providers: readonly ProviderDescriptor[]; error: ArlopassSDKError | null; }>; ``` Internal store snapshot exposed by the provider context. --- ### Re-exported from @arlopass/web-sdk These types are re-exported so you only need the `@arlopass/react` package. ```tsx // Re-exported from @arlopass/web-sdk — no need to install web-sdk separately ChatMessage, ChatRole, ClientState, ProviderDescriptor, SelectProviderInput, ChatOperationOptions, ChatStreamEvent, ArlopassSDKError, ArlopassStateError, ArlopassTransport, ToolDefinition, ConversationStreamEvent, ToolCall, ToolResult, ToolCallEvent, ToolResultEvent, ToolPrimingStartEvent, ToolPrimingMatchEvent, ToolPrimingEndEvent, } from "@arlopass/react"; ``` #### ChatMessage ```tsx type ChatMessage = Readonly<{ role: ChatRole; content: string }>; ``` #### ChatRole ```tsx type ChatRole = "system" | "user" | "assistant"; ``` #### ClientState ```tsx type ClientState = | "disconnected" | "connecting" | "connected" | "degraded" | "reconnecting" | "failed"; ``` #### ProviderDescriptor ```tsx type ProviderDescriptor = Readonly<{ providerId: string; providerName: string; models: readonly string[]; }>; ``` #### SelectProviderInput ```tsx type SelectProviderInput = Readonly<{ providerId: string; modelId: string }>; ``` #### ToolDefinition ```tsx type ToolDefinition = Readonly<{ name: string; description: string; parameters?: ToolParameterSchema; handler?: (args: Record) => Promise | string; }>; ``` --- ## Testing utilities Helpers for unit and integration testing with mock transports, providers, and wait utilities. URL: https://arlopass.com/docs/reference/react/testing Helpers for unit and integration testing. Import from `@arlopass/react/testing`. ```tsx createMockTransport, MockArlopassProvider, mockWindowArlopass, cleanupWindowArlopass, simulateExternalDisconnect, waitForSnapshot, waitForChat, waitForStream, waitForState, } from "@arlopass/react/testing"; ``` --- ### createMockTransport Creates a `ArlopassTransport` that responds to protocol capabilities without a real extension. Supports configurable responses, errors, latency, and streaming. , ); ``` --- ### Window mocks Low-level functions for controlling `window.arlopass` in tests. void", description: "Injects a transport as window.arlopass. Use before rendering ArlopassProvider in tests.", }, { name: "cleanupWindowArlopass", type: "() => void", description: "Removes window.arlopass to simulate extension not installed.", }, { name: "simulateExternalDisconnect", type: "(transport: ArlopassTransport) => Promise", description: "Removes window.arlopass and calls transport.disconnect() if available.", }, ]} /> ```tsx // Setup mockWindowArlopass(transport); // Teardown cleanupWindowArlopass(); // Simulate disconnect await simulateExternalDisconnect(transport); ``` --- ### Wait helpers Polling utilities for async test assertions. All accept an optional `timeout` (default 3000ms, poll interval 50ms). boolean, options?: { timeout?: number }) => Promise", description: "Polls the store until the predicate returns true. Default timeout: 3000ms.", }, { name: "waitForChat", type: "(screen: Screen, testId?: string) => Promise", description: 'Waits for an element with the given data-testid (default: "chat-ready") to appear.', }, { name: "waitForStream", type: "(screen: Screen, options?: { timeout?: number }) => Promise", description: 'Waits until the streaming indicator disappears (data-testid="streaming" is removed).', }, { name: "waitForState", type: "(screen: Screen, state: string, options?: { timeout?: number }) => Promise", description: 'Waits until data-testid="state" has the given text content.', }, ]} /> ```tsx // Wait for connected state await waitForState(screen, "connected"); // Wait for streaming to finish await waitForStream(screen, { timeout: 5000 }); ``` --- ## Build your first chat app Build a complete AI chat interface with streaming responses using the Arlopass SDK. URL: https://arlopass.com/docs/tutorials/first-chat-app Create a complete AI chat interface with React in 15 minutes. ### What you'll build A fully functional chat UI with message history, real-time streaming responses, a text input, and a stop button — all powered by any AI provider through the Arlopass extension. ### Step 1 — Set up the provider Wrap your app in ArlopassProvider. This connects to the browser extension and manages the AI client lifecycle. You can optionally set a default provider and model. ```tsx title="App.tsx" function App() { return ( ); } ``` ### Step 2 — Add the chat ready gate ChatReadyGate renders fallback UIs for connecting, missing-provider, and error states. Your chat component only mounts once everything is ready. ```tsx title="ChatApp.tsx" function ChatApp() { return ( ); } ``` ### Step 3 — Create the chat component The useConversation hook manages messages, streaming, and tool calls. Pass a systemPrompt to set the AI's behaviour. ```tsx title="Chat.tsx" function Chat() { const { messages, streamingContent, isStreaming, stream, stop } = useConversation({ systemPrompt: "You are a helpful assistant. Be concise.", }); // We'll build the UI in the next steps return
Chat component
; } ``` ### Step 4 — Render messages Map over the messages array. Each message has an id, role ("user" or "assistant"), and content string. ```tsx title="Chat.tsx (JSX)" { messages.map((msg) => (
{msg.role === "user" ? "You" : "AI"}: {msg.content}
)); } ``` ### Step 5 — Add streaming indicator While isStreaming is true, streamingContent holds the partial response text. Render it below your message list so the user sees the AI typing in real time. ```tsx title="Chat.tsx (JSX)" { isStreaming && streamingContent && (
AI: {streamingContent}
); } ``` ### Step 6 — Add input form Create a controlled input and call stream() on submit. Clear the input immediately so the user can keep typing. Disable the input while streaming. ```tsx title="Chat.tsx" const [input, setInput] = useState(""); async function handleSubmit(e: React.FormEvent) { e.preventDefault(); if (!input.trim() || isStreaming) return; const text = input; setInput(""); await stream(text); } // In your JSX:
setInput(e.target.value)} placeholder="Type a message..." disabled={isStreaming} />
; ``` ### Step 7 — Add stop button Call stop() to abort the current stream. The partial response is kept in the messages array. ```tsx title="Chat.tsx (JSX)" { isStreaming && ; } ``` ### Complete example Here's the full working app — copy it into your project and you're ready to chat: ```tsx title="App.tsx" ArlopassProvider, ChatReadyGate, useConversation, } from "@arlopass/react"; function Chat() { const { messages, streamingContent, isStreaming, stream, stop } = useConversation({ systemPrompt: "You are a helpful assistant. Be concise.", }); const [input, setInput] = useState(""); async function handleSubmit(e: React.FormEvent) { e.preventDefault(); if (!input.trim() || isStreaming) return; const text = input; setInput(""); await stream(text); } return (
{messages.map((msg) => (
{msg.role === "user" ? "You" : "AI"}: {msg.content}
))} {isStreaming && streamingContent && (
AI: {streamingContent}
)}
setInput(e.target.value)} placeholder="Type a message..." disabled={isStreaming} style={{ flex: 1, padding: 8 }} /> {isStreaming && ( )}
); } return ( ); } ``` ### React SDK vs Web SDK The React SDK wraps the Web SDK in hooks and components. Here's the same chat in both approaches: #### React SDK ```tsx title="Chat.tsx" function Chat() { const { messages, stream, streamingContent, isStreaming, stop } = useConversation({ systemPrompt: "You are a helpful assistant." }); return (
{messages.map((msg) => (
{msg.role}: {msg.content}
))} {isStreaming &&
AI: {streamingContent}
} {isStreaming && }
); } ``` #### Web SDK ```typescript title="main.ts" const client = new ArlopassClient({ transport: window.arlopass }); await client.connect({ appId: "my-chat-app" }); const convo = new ConversationManager({ client, systemPrompt: "You are a helpful assistant.", }); for await (const event of convo.stream("Hello!")) { if (event.type === "delta") { process.stdout.write(event.content); } } ``` --- ## Streaming responses Add real-time streaming tokens, typing indicators, and stop functionality to your AI chat. URL: https://arlopass.com/docs/tutorials/streaming-responses Add real-time streaming to see AI responses as they're generated. ### What you'll build A chat interface that shows AI responses word-by-word as they arrive, with a streaming indicator and the ability to stop generation mid-stream. ### Step 1 — Use stream() instead of send() The useConversation hook provides two methods for sending messages: stream() delivers tokens in real time, while send() waits for the complete response. Use stream() for interactive chat. ```tsx title="Chat.tsx" function Chat() { const { messages, stream, isStreaming } = useConversation(); async function handleSend(text: string) { // stream() sends the message and streams the response token-by-token. // The response is automatically appended to the messages array when done. await stream(text); } // send() waits for the full response before updating messages. // Use it when you don't need real-time output. // await send(text); } ``` ### Step 2 — Show streamingContent While a response is being generated, streamingContent holds the partial text. Render it below the completed messages so the user sees the AI "typing" in real time. ```tsx title="Chat.tsx" function Chat() { const { messages, stream, streamingContent, isStreaming } = useConversation(); return (
{/* Completed messages */} {messages.map((msg) => (
{msg.role}: {msg.content}
))} {/* Partial response while streaming */} {isStreaming && streamingContent && (
assistant: {streamingContent}
)}
); } ``` ### Step 3 — Track streaming state isStreaming is true while a response is being generated. Use it to disable inputs and show loading indicators. ```tsx title="ChatInput.tsx" function ChatInput({ onSend }: { onSend: (text: string) => void }) { const { isStreaming } = useConversation(); return (
); } ``` ### Step 4 — Add a stop button Call stop() to abort the current stream. The partial response is preserved in the messages array — nothing is lost. ```tsx title="ChatControls.tsx" function ChatControls() { const { isStreaming, stop } = useConversation(); return (
{isStreaming && }
); } ``` ### Step 5 — Handle the onDelta callback For custom token processing (word counting, syntax highlighting, etc.), subscribe to "stream" events. Each delta gives you the latest chunk of text. ```tsx title="Chat.tsx" function Chat() { const wordCount = useRef(0); const { messages, stream, subscribe } = useConversation(); // Subscribe to stream events for custom processing function handleSend(text: string) { wordCount.current = 0; // Subscribe to stream deltas for this response const unsub = subscribe("stream", (delta: string) => { // Count words as they arrive const words = delta.split(/\s+/).filter(Boolean); wordCount.current += words.length; console.log("Words so far:", wordCount.current); }); stream(text).finally(unsub); } return /* your UI */; } ``` ### Complete example Here's a full streaming chat app with a send/stop toggle button: ```tsx title="App.tsx" ArlopassProvider, ChatReadyGate, useConversation, } from "@arlopass/react"; function Chat() { const { messages, streamingContent, isStreaming, stream, stop } = useConversation({ systemPrompt: "You are a helpful assistant.", }); const [input, setInput] = useState(""); async function handleSubmit(e: React.FormEvent) { e.preventDefault(); if (!input.trim() || isStreaming) return; const text = input; setInput(""); await stream(text); } return (
{messages.map((msg) => (
{msg.role === "user" ? "You" : "AI"}: {msg.content}
))} {isStreaming && streamingContent && (
AI: {streamingContent}
)}
setInput(e.target.value)} placeholder="Type a message..." disabled={isStreaming} style={{ flex: 1, padding: 8 }} /> {isStreaming ? ( ) : ( )}
); } return ( ); } ``` ### React SDK vs Web SDK The React SDK manages streaming state automatically. Here's the comparison: #### React SDK ```tsx title="Chat.tsx" const { messages, stream, streamingContent, isStreaming, stop } = useConversation(); // Stream a message — UI updates automatically await stream("Explain React hooks"); // Stop mid-stream stop(); ``` #### Web SDK ```typescript title="main.ts" const client = new ArlopassClient({ transport: window.arlopass }); await client.connect({ appId: "streaming-demo" }); const convo = new ConversationManager({ client }); // Stream token-by-token for await (const event of convo.stream("Explain React hooks")) { if (event.type === "delta") { document.getElementById("output")!.textContent += event.content; } } // To abort: call convo.stop() ``` --- ## Provider selection UI Build provider and model dropdown selectors using the useProviders hook. URL: https://arlopass.com/docs/tutorials/provider-selection Let users choose their AI provider and model. ### What you'll build A pair of dropdown selectors that let the user pick from available AI providers and models. The chat component stays blocked until a provider is selected. ### Step 1 — Use useProviders The useProviders hook gives you the list of providers, the currently selected provider/model, and a selectProvider function. It automatically fetches providers once connected. ```tsx title="ProviderPicker.tsx" function ProviderPicker() { const { providers, // list of available providers selectedProvider, // { providerId, modelId } or null isLoading, error, selectProvider, // call to switch provider + model } = useProviders(); // We'll build the UI in the next steps return
Provider picker
; } ``` ### Step 2 — Wait for connection Use useConnection to check whether the extension is connected before rendering the provider picker. The providers list is empty until connected. ```tsx title="ProviderPicker.tsx" function ProviderPicker() { const { isConnected, isConnecting } = useConnection(); const { providers, selectedProvider, selectProvider } = useProviders(); if (isConnecting) return

Connecting to extension...

; if (!isConnected) return

Not connected. Is the extension installed?

; return (

Connected! {providers.length} providers available.

{/* Dropdowns go here */}
); } ``` ### Step 3 — Render provider dropdown Map the providers array to select options. When the user picks a provider, auto-select its first model. ```tsx title="ProviderPicker.tsx" function ProviderPicker() { const { isConnected } = useConnection(); const { providers, selectedProvider, selectProvider } = useProviders(); if (!isConnected) return

Connecting...

; const currentProviderId = selectedProvider?.providerId ?? ""; return (
); } ``` ### Step 4 — Render model dropdown Find the selected provider object and list its models. The model dropdown is disabled until a provider is selected. ```tsx title="ProviderPicker.tsx" // Add below the provider dropdown const selectedProviderObj = providers.find( (p) => p.id === selectedProvider?.providerId, ); const models = selectedProviderObj?.models ?? []; ``` ### Step 5 — Handle selection The selectProvider function takes a providerId and modelId. When switching providers, auto-select the first model. ```tsx title="ProviderPicker.tsx" async function handleProviderChange(providerId: string) { const provider = providers.find((p) => p.id === providerId); if (!provider || provider.models.length === 0) return; // Select the first model from the new provider await selectProvider({ providerId: provider.id, modelId: provider.models[0].id, }); } async function handleModelChange(modelId: string) { if (!selectedProvider) return; await selectProvider({ providerId: selectedProvider.providerId, modelId, }); } ``` ### Step 6 — Show ChatReadyGate Place ChatReadyGate below the picker. Its noProvider fallback tells the user to pick a provider. The chat only renders once a provider is active. ```tsx title="App.tsx" function App() { return ( ); } ``` ### Complete example A full app with provider/model dropdowns and a chat component: ```tsx title="App.tsx" ArlopassProvider, ChatReadyGate, useConnection, useProviders, useConversation, } from "@arlopass/react"; function ProviderPicker() { const { isConnected } = useConnection(); const { providers, selectedProvider, selectProvider, isLoading } = useProviders(); if (!isConnected) return

Connecting to extension...

; const selectedProviderObj = providers.find( (p) => p.id === selectedProvider?.providerId, ); const models = selectedProviderObj?.models ?? []; return (
); } function Chat() { const { messages, stream, streamingContent, isStreaming } = useConversation(); const [input, setInput] = useState(""); async function handleSubmit(e: React.FormEvent) { e.preventDefault(); if (!input.trim() || isStreaming) return; const text = input; setInput(""); await stream(text); } return (
{messages.map((msg) => (
{msg.role}: {msg.content}
))} {isStreaming && streamingContent && (
assistant: {streamingContent}
)}
setInput(e.target.value)} placeholder="Type a message..." disabled={isStreaming} style={{ flex: 1, padding: 8 }} />
); } return ( ); } ``` ### React SDK vs Web SDK The React SDK auto-fetches providers on connection and tracks selection state. Here's the comparison: #### React SDK ```tsx title="ProviderPicker.tsx" const { providers, selectedProvider, selectProvider } = useProviders(); // Select a provider + model in one call await selectProvider({ providerId: "ollama", modelId: "llama3", }); ``` #### Web SDK ```typescript title="main.ts" const client = new ArlopassClient({ transport: window.arlopass }); await client.connect({ appId: "my-app" }); // List providers manually const { providers } = await client.listProviders(); // Select provider + model await client.selectProvider({ providerId: providers[0].id, modelId: providers[0].models[0].id, }); ``` --- ## Adding tool calling Configure AI tools with auto-execution, manual mode, and streaming lifecycle events. URL: https://arlopass.com/docs/tutorials/adding-tool-calling Give the AI access to your app's functions. ### What you'll build A chat app where the AI can call a documentation search function and a calculator. You'll learn both auto-execute mode (tools run automatically) and manual mode (you confirm before executing). ### Step 1 — Define tools A ToolDefinition has a name, description, JSON Schema parameters, and an optional handler function. The description tells the model when to use the tool. ```typescript title="tools.ts" const tools: ToolDefinition[] = [ { name: "search_docs", description: "Search the documentation for a given query", parameters: { type: "object", properties: { query: { type: "string", description: "The search query", }, }, required: ["query"], }, handler: async (args) => { // Auto-executed when the model calls this tool const results = await searchDocs(args.query as string); return JSON.stringify(results); }, }, { name: "calculate", description: "Evaluate a math expression", parameters: { type: "object", properties: { expression: { type: "string", description: "The math expression to evaluate, e.g. '2 + 2'", }, }, required: ["expression"], }, handler: async (args) => { const expr = args.expression as string; // Simple evaluation — use a math library in production const result = Function(`"use strict"; return (${expr})`)(); return String(result); }, }, ]; ``` ### Step 2 — Pass tools to useConversation Pass the tools array to useConversation. The hook automatically injects tool descriptions into the system prompt so the model knows what's available. ```tsx title="Chat.tsx" function Chat() { const { messages, streamingContent, isStreaming, stream } = useConversation({ systemPrompt: "You can search docs and do math. Use your tools.", tools, // pass the tools array }); // The hook handles tool execution automatically // when tools have a handler function } ``` ### Step 3 — Auto-execute mode When a tool has a handler, it runs automatically. The SDK parses the model's tool call, runs your handler, feeds the result back, and lets the model continue. ```typescript title="tools.ts" // When a tool has a handler, it runs automatically: const tools: ToolDefinition[] = [ { name: "get_weather", description: "Get current weather for a city", parameters: { type: "object", properties: { city: { type: "string", description: "City name" }, }, required: ["city"], }, // This runs automatically when the model calls get_weather handler: async (args) => { const weather = await fetchWeather(args.city as string); return JSON.stringify(weather); }, }, ]; // The conversation flow: // 1. User: "What's the weather in Paris?" // 2. Model calls get_weather({ city: "Paris" }) // 3. Handler runs automatically, returns result // 4. Model generates final response using the result ``` ### Step 4 — Manual mode Omit the handler for tools that need user confirmation. Subscribe to "tool_call" events and call submitToolResult when ready. ```tsx title="Chat.tsx" function Chat() { const { messages, stream, subscribe, submitToolResult } = useConversation({ tools: [ { name: "approve_purchase", description: "Submit a purchase for approval", parameters: { type: "object", properties: { item: { type: "string", description: "Item name" }, amount: { type: "number", description: "Amount in USD" }, }, required: ["item", "amount"], }, // No handler — manual mode }, ], }); // Listen for tool calls and handle them yourself subscribe("tool_call", (event) => { console.log("Tool called:", event.name, event.arguments); // Show a confirmation dialog, then submit the result if (confirm(`Approve purchase of ${event.arguments.item}?`)) { submitToolResult(event.toolCallId, "Purchase approved"); } else { submitToolResult(event.toolCallId, "Purchase denied by user"); } }); } ``` ### Step 5 — Show tool activity Subscribe to tool_call and tool_result events to show the user what's happening. Messages also include a toolCalls array with call details and results. ```tsx title="Chat.tsx" function Chat() { const { messages, stream, subscribe } = useConversation({ tools }); // Subscribe to tool events for UI updates subscribe("tool_call", (event) => { console.log(`🔧 Calling ${event.name}(${JSON.stringify(event.arguments)})`); }); subscribe("tool_result", (event) => { console.log(`✅ ${event.name} returned: ${event.result}`); }); // Show tool calls within messages return (
{messages.map((msg) => (
{msg.role}: {msg.content} {msg.toolCalls?.map((tc) => (
🔧 {tc.name}({JSON.stringify(tc.arguments)}) {tc.status === "complete" && ` → ${tc.result}`}
))}
))}
); } ``` ### Step 6 — Set maxToolRounds Prevent infinite tool call loops by setting maxToolRounds. The default is 5. After this many rounds, the model must produce a text response. ```tsx title="Chat.tsx" const { messages, stream } = useConversation({ tools, maxToolRounds: 3, // Stop after 3 tool call rounds (default: 5) }); // This prevents infinite loops where the model keeps calling tools. // After maxToolRounds, the model must produce a text response. ``` ### Complete example A full app with search_docs and calculate tools: ```tsx title="App.tsx" ArlopassProvider, ChatReadyGate, useConversation, } from "@arlopass/react"; const tools: ToolDefinition[] = [ { name: "search_docs", description: "Search the documentation for a given query", parameters: { type: "object", properties: { query: { type: "string", description: "The search query", }, }, required: ["query"], }, handler: async (args) => { // Simulate a search return JSON.stringify([ { title: "Getting Started", snippet: "Install with npm..." }, { title: "API Reference", snippet: "useConversation hook..." }, ]); }, }, { name: "calculate", description: "Evaluate a math expression", parameters: { type: "object", properties: { expression: { type: "string", description: "The math expression to evaluate", }, }, required: ["expression"], }, handler: async (args) => { const expr = args.expression as string; const result = Function(`"use strict"; return (${expr})`)(); return String(result); }, }, ]; function Chat() { const { messages, streamingContent, isStreaming, stream, stop } = useConversation({ systemPrompt: "You can search docs and do calculations. Use your tools when appropriate.", tools, maxToolRounds: 3, }); const [input, setInput] = useState(""); async function handleSubmit(e: React.FormEvent) { e.preventDefault(); if (!input.trim() || isStreaming) return; const text = input; setInput(""); await stream(text); } return (
{messages.map((msg) => (
{msg.role === "user" ? "You" : "AI"}: {msg.content} {msg.toolCalls?.map((tc) => (
🔧 {tc.name}({JSON.stringify(tc.arguments)}) {tc.status === "complete" && ( → {tc.result} )}
))}
))} {isStreaming && streamingContent && (
AI: {streamingContent}
)}
setInput(e.target.value)} placeholder="Try: search for hooks, or what is 42 * 17?" disabled={isStreaming} style={{ flex: 1, padding: 8 }} /> {isStreaming ? ( ) : ( )}
); } return ( ); } ``` ### React SDK vs Web SDK The React SDK wraps ConversationManager's tool handling in hooks. Here's the same tool calling in both: #### React SDK ```tsx title="Chat.tsx" const tools: ToolDefinition[] = [ { name: "calculate", description: "Evaluate a math expression", parameters: { type: "object", properties: { expression: { type: "string", description: "Math expression" }, }, required: ["expression"], }, handler: async (args) => String(eval(args.expression as string)), }, ]; const { messages, stream } = useConversation({ tools }); await stream("What is 42 * 17?"); ``` #### Web SDK ```typescript title="main.ts" const client = new ArlopassClient({ transport: window.arlopass }); await client.connect({ appId: "my-app" }); const convo = new ConversationManager({ client, tools: [ { name: "calculate", description: "Evaluate a math expression", parameters: { type: "object", properties: { expression: { type: "string", description: "Math expression" }, }, required: ["expression"], }, handler: async (args) => String(eval(args.expression)), }, ], maxToolRounds: 3, }); for await (const event of convo.stream("What is 42 * 17?")) { if (event.type === "delta") process.stdout.write(event.content); if (event.type === "tool_call") console.log("Tool:", event.name); if (event.type === "tool_result") console.log("Result:", event.result); } ``` --- ## ArlopassClient Core client class — manages connection state, provider selection, and chat operations over the protocol transport. URL: https://arlopass.com/docs/reference/web-sdk/client The core client class. Manages connection state, provider selection, and chat operations over the protocol transport. ```tsx ``` --- ### Constructor options --- ## ConversationManager High-level conversation controller — context-window management, summarization, tool calling, and message pinning. URL: https://arlopass.com/docs/reference/web-sdk/conversation-manager High-level conversation controller built on top of `ArlopassClient`. Handles context-window management, summarization, tool calling, and message pinning. ```tsx ``` --- ### Constructor options --- ### Properties --- ### Methods Promise", description: "Send a user message and receive a complete response. Executes tool calls automatically if handlers are defined.", }, { name: "stream", type: "(content: string, options?: PinOptions) => AsyncIterable", description: "Send a user message and stream the response. Yields chunk, done, tool_call, tool_result, and tool_priming_* events.", }, { name: "addMessage", type: "(message: ChatMessage, options?: PinOptions) => void", description: "Manually add a message to the conversation history.", }, { name: "getMessages", type: "() => readonly ChatMessage[]", description: "Get all messages including the system prompt.", }, { name: "getContextWindow", type: "() => readonly ChatMessage[]", description: "Get the messages that would be sent in the next request (after summarization/truncation).", }, { name: "getTokenCount", type: "() => number", description: "Estimated total tokens in the current context window.", }, { name: "getContextInfo", type: "() => ContextWindowInfo", description: "Returns a snapshot of context window usage: maxTokens, usedTokens, reservedOutputTokens, remainingTokens, and usageRatio (0–1).", }, { name: "setPin", type: "(index: number, pinned: boolean) => void", description: "Toggle pin status of a message by index. Pinned messages survive summarization.", }, { name: "clear", type: "() => void", description: "Remove all messages from the conversation.", }, { name: "submitToolResult", type: "(toolCallId: string, result: string) => void", description: "Submit a result for a manual tool call (tools without a handler).", }, ]} /> --- ### PinOptions Options passed to `send`, `stream`, and `addMessage`. --- ### ConversationStreamEvent Discriminated union yielded by `stream()`. Extends the base `ChatStreamEvent` with tool and priming events. ```tsx type ConversationStreamEvent = | { type: "chunk"; delta: string; index: number; correlationId: string } | { type: "done"; correlationId: string } | { type: "tool_call"; toolCallId: string; name: string; arguments: Record; matchRange: { start: number; end: number }; } | { type: "tool_result"; toolCallId: string; name: string; result: string } | { type: "tool_priming_start"; message: string } | { type: "tool_priming_match"; tools: readonly string[] } | { type: "tool_priming_end" }; ``` --- ### ContextWindowInfo Returned by `getContextInfo()`. Provides a snapshot of context window usage — useful for building token meters, "context full" warnings, and adaptive UI that responds to how much space is left. ```tsx type ContextWindowInfo = Readonly<{ maxTokens: number; // Context window size for the model usedTokens: number; // Tokens currently in the context window reservedOutputTokens: number; // Tokens reserved for model output remainingTokens: number; // Tokens left for new input usageRatio: number; // 0–1 fraction of input budget used }>; ``` --- ### Example ```tsx const manager = new ConversationManager({ client, systemPrompt: "You are a helpful assistant.", tools: [ { name: "search", description: "Search", handler: async () => "results" }, ], }); // Non-streaming const reply = await manager.send("What is the weather?"); // Streaming for await (const event of manager.stream("Tell me more")) { if (event.type === "chunk") process.stdout.write(event.delta); if (event.type === "tool_call") console.log("Tool:", event.name); } // Context window usage const info = manager.getContextInfo(); console.log( `${info.usedTokens}/${info.maxTokens} tokens (${Math.round(info.usageRatio * 100)}%)`, ); console.log(`${info.remainingTokens} tokens remaining for input`); ``` --- ## Types All TypeScript types exported from @arlopass/web-sdk. URL: https://arlopass.com/docs/reference/web-sdk/types All types exported from `@arlopass/web-sdk`. ```tsx ChatMessage, ChatRole, ClientState, ContextWindowInfo, ChatInput, ChatOperationOptions, ChatStreamEvent, ChatSendResult, ConnectOptions, ConnectResult, ProviderDescriptor, SelectProviderInput, SelectProviderResult, ListProvidersResult, SessionId, RequestId, CorrelationId, TransportRequest, TransportResponse, TransportStream, } from "@arlopass/web-sdk"; ``` --- ### Core types #### ChatRole ```tsx type ChatRole = "system" | "user" | "assistant"; ``` #### ChatMessage ```tsx type ChatMessage = Readonly<{ role: ChatRole; content: string; }>; ``` #### ClientState ```tsx type ClientState = | "disconnected" | "connecting" | "connected" | "degraded" | "reconnecting" | "failed"; ``` #### ChatInput ```tsx type ChatInput = Readonly<{ messages: readonly ChatMessage[]; }>; ``` #### ChatOperationOptions ```tsx type ChatOperationOptions = Readonly<{ timeoutMs?: number; signal?: AbortSignal; }>; ``` #### ChatStreamEvent ```tsx type ChatStreamEvent = | Readonly<{ type: "chunk"; delta: string; index: number; correlationId: string; }> | Readonly<{ type: "done"; correlationId: string }>; ``` #### ChatSendResult ```tsx type ChatSendResult = Readonly<{ message: ChatMessage; correlationId: string; }>; ``` #### ContextWindowInfo ```tsx type ContextWindowInfo = Readonly<{ maxTokens: number; // Context window size for the model usedTokens: number; // Estimated tokens in the context window reservedOutputTokens: number; // Tokens reserved for model response remainingTokens: number; // Tokens still available for input usageRatio: number; // 0–1 fraction of input budget used }>; ``` Returned by `ArlopassClient.getContextInfo()` and `ConversationManager.getContextInfo()`. Use `usageRatio` for progress bars and `remainingTokens` for "context full" warnings. #### ID types ```tsx type RequestId = string; type CorrelationId = string; type SessionId = string; ``` --- ### Connection types #### ConnectOptions ```tsx type ConnectOptions = Readonly<{ appId?: string; appSuffix?: string; appName?: string; appDescription?: string; appIcon?: string; origin?: string; timeoutMs?: number; }>; ``` #### ConnectResult ```tsx type ConnectResult = Readonly<{ sessionId: string; capabilities: readonly ProtocolCapability[]; protocolVersion: string; correlationId: string; }>; ``` --- ### Provider types #### ProviderDescriptor ```tsx type ProviderDescriptor = Readonly<{ providerId: string; providerName: string; models: readonly string[]; }>; ``` #### SelectProviderInput ```tsx type SelectProviderInput = Readonly<{ providerId: string; modelId: string; }>; ``` #### SelectProviderResult ```tsx type SelectProviderResult = Readonly<{ providerId: string; modelId: string; correlationId: string; }>; ``` #### ListProvidersResult ```tsx type ListProvidersResult = Readonly<{ providers: readonly ProviderDescriptor[]; correlationId: string; }>; ``` --- ### Transport types ```tsx ``` #### ArlopassTransport ```tsx interface ArlopassTransport { request( request: TransportRequest, ): Promise>; stream( request: TransportRequest, ): Promise>; disconnect?(sessionId: string): Promise; } ``` #### TransportRequest ```tsx type TransportRequest = Readonly<{ envelope: ProtocolEnvelopePayload; timeoutMs?: number; signal?: AbortSignal; }>; ``` #### TransportResponse ```tsx type TransportResponse = Readonly<{ envelope: ProtocolEnvelopePayload; }>; ``` #### TransportStream ```tsx type TransportStream = AsyncIterable< TransportResponse >; ``` --- ### Payload types #### ConnectPayload ```tsx type ConnectPayload = Readonly<{ appId: string; requestedCapabilities: readonly ProtocolCapability[]; appName?: string; appDescription?: string; appIcon?: string; }>; ``` #### ChatSendPayload ```tsx type ChatSendPayload = Readonly<{ messages: readonly ChatMessage[]; }>; ``` #### ChatSendResponsePayload ```tsx type ChatSendResponsePayload = Readonly<{ message: ChatMessage; }>; ``` #### ChatStreamPayload ```tsx type ChatStreamPayload = ChatSendPayload; ``` #### ChatStreamChunkPayload / ChatStreamDonePayload ```tsx type ChatStreamChunkPayload = Readonly<{ type: "chunk"; delta: string; index: number; }>; ``` ```tsx type ChatStreamDonePayload = Readonly<{ type: "done" }>; ``` --- ### Tool types ```tsx ToolDefinition, ToolParameterSchema, ToolCall, ToolResult, ToolCallEvent, ToolResultEvent, ToolPrimingStartEvent, ToolPrimingMatchEvent, ToolPrimingEndEvent, ConversationStreamEvent, } from "@arlopass/web-sdk"; ``` #### ToolParameterSchema ```tsx type ToolParameterSchema = Readonly<{ type: "object"; properties?: Readonly< Record< string, Readonly<{ type: string; description?: string; enum?: readonly string[]; }> > >; required?: readonly string[]; }>; ``` #### ToolDefinition ```tsx type ToolDefinition = Readonly<{ name: string; description: string; parameters?: ToolParameterSchema; handler?: (args: Record) => Promise | string; }>; ``` #### ToolCall ```tsx type ToolCall = Readonly<{ id: string; name: string; arguments: Record; matchRange: Readonly<{ start: number; end: number }>; }>; ``` #### ToolResult ```tsx type ToolResult = Readonly<{ toolCallId: string; name: string; result: string; }>; ``` #### ToolCallEvent ```tsx type ToolCallEvent = Readonly<{ type: "tool_call"; toolCallId: string; name: string; arguments: Record; matchRange: Readonly<{ start: number; end: number }>; }>; ``` #### ToolResultEvent ```tsx type ToolResultEvent = Readonly<{ type: "tool_result"; toolCallId: string; name: string; result: string; }>; ``` #### ToolPrimingStartEvent ```tsx type ToolPrimingStartEvent = Readonly<{ type: "tool_priming_start"; message: string; }>; ``` #### ToolPrimingMatchEvent ```tsx type ToolPrimingMatchEvent = Readonly<{ type: "tool_priming_match"; tools: readonly string[]; }>; ``` #### ToolPrimingEndEvent ```tsx type ToolPrimingEndEvent = Readonly<{ type: "tool_priming_end" }>; ``` #### ConversationStreamEvent ```tsx type ConversationStreamEvent = | ChatStreamEvent | ToolCallEvent | ToolResultEvent | ToolPrimingStartEvent | ToolPrimingMatchEvent | ToolPrimingEndEvent; ``` --- ## Error codes Structured error hierarchy, machine codes, reason codes, and retryable classification. URL: https://arlopass.com/docs/reference/web-sdk/error-codes Every error thrown by the SDK is an instance of `ArlopassSDKError` (or a subclass). Errors carry structured metadata for programmatic handling. ```tsx ArlopassSDKError, ArlopassStateError, ArlopassTransportError, ArlopassTimeoutError, ArlopassProtocolBoundaryError, ArlopassInvalidStateTransitionError, } from "@arlopass/web-sdk"; ``` --- ### Error hierarchy ```text // Error class hierarchy ArlopassSDKError // Base class — all SDK errors extend this ├── ArlopassStateError // Invalid operation for current state ├── ArlopassInvalidStateTransitionError // Illegal state transition ├── ArlopassProtocolBoundaryError // Protocol envelope validation failure ├── ArlopassTransportError // Transport-layer failure (retryable by default) └── ArlopassTimeoutError // Request timed out (retryable by default) ``` ### Error properties All error subclasses inherit these properties from `ArlopassSDKError`.