Transport model
import Callout from "../../../components/docs/Callout.astro"; 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. export interface ArlopassTransport { // Send a request, get a single response request<TRequestPayload, TResponsePayload>( request: TransportRequest<TRequestPayload>, ): Promise<TransportResponse<TResponsePayload>>; // Send a request, get a stream of responses stream<TRequestPayload, TResponsePayload>( request: TransportRequest<TRequestPayload>, ): Promise<TransportStream<TResponsePayload>>; // Optional — called on disconnect disconnect?(sessionId: string): Promise<void>; } ``` ### Injected transport The extension's content script injects a `ArlopassTransport` implementation at `window.arlopass` before your app code executes. The React SDK's `ArlopassProvider` detects this automatically and exposes a `transportAvailable` flag. The Web SDK accepts it as a constructor argument. ```tsx title="Transport detection" // The extension's content script injects window.arlopass. // This happens before your app code runs. // React SDK — detects it automatically import { ArlopassProvider } from "@arlopass/react"; <ArlopassProvider appId="my-app"> {/* ArlopassProvider checks window.arlopass on mount */} {/* If missing → transportAvailable: false */} <App /> </ArlopassProvider>; // Web SDK — you reference it explicitly import { ArlopassClient } from "@arlopass/web-sdk"; 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. <Callout type="warning" title="No arbitrary transports"> Constructing a custom `ArlopassTransport` in production code bypasses the extension trust boundary. The SDK accepts `window.arlopass` — nothing else. For testing, use the mock transport from `@arlopass/react/testing`. </Callout> ### 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. import { createMockTransport } from "@arlopass/react/testing"; 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" }); ``` <Callout type="tip" title="Related"> See [How Arlopass Works](/docs/concepts/how-arlopass-works) for the full architecture overview, or [Security Model](/docs/guides/security) for how envelope security protects against attacks. </Callout>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.
// The ArlopassTransport interface — two methods, that's it.
export interface ArlopassTransport {
// Send a request, get a single response
request<TRequestPayload, TResponsePayload>(
request: TransportRequest<TRequestPayload>,
): Promise<TransportResponse<TResponsePayload>>;
// Send a request, get a stream of responses
stream<TRequestPayload, TResponsePayload>(
request: TransportRequest<TRequestPayload>,
): Promise<TransportStream<TResponsePayload>>;
// Optional — called on disconnect
disconnect?(sessionId: string): Promise<void>;
}
Injected transport
The extension’s content script injects a ArlopassTransport implementation at window.arlopass before your app code executes. The React SDK’s ArlopassProvider detects this automatically and exposes a transportAvailable flag. The Web SDK accepts it as a constructor argument.
// The extension's content script injects window.arlopass.
// This happens before your app code runs.
// React SDK — detects it automatically
import { ArlopassProvider } from "@arlopass/react";
<ArlopassProvider appId="my-app">
{/* ArlopassProvider checks window.arlopass on mount */}
{/* If missing → transportAvailable: false */}
<App />
</ArlopassProvider>;
// Web SDK — you reference it explicitly
import { ArlopassClient } from "@arlopass/web-sdk";
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.
// 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.
// 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.
// For testing, use the mock transport from the React SDK test utilities.
// It implements ArlopassTransport without needing the extension.
import { createMockTransport } from "@arlopass/react/testing";
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" });