The official Node.js client for brawsr sessions, checkpoints, rewind, fork, and CDP handoff. It turns asynchronous API operations into one bounded method call; it does not bundle or proxy a browser library.
Published on npm as @brawsr/sdk. Requires Node.js 22+ and uses its built-in
fetch implementation.
This is a server-side SDK. Never expose a brawsr API key in browser code, a public environment variable, or a client-side JavaScript bundle.
npm install @brawsr/sdkInstall your browser client separately, for example:
npm install playwright-core
# or
npm install puppeteer-coreimport { chromium, type Browser } from "playwright-core";
import { BrawsrClient, BrawsrWaitTimeoutError } from "@brawsr/sdk";
const brawsr = new BrawsrClient(); // reads BRAWSR_API_KEY
const session = await brawsr.createSession({
ttlSeconds: 300,
displayLabel: "Checkout retry",
});
let browser: Browser | undefined;
try {
const connection = brawsr.connectCDP(session);
browser = await chromium.connectOverCDP(connection.endpointUrl, {
headers: connection.headers,
});
const context = browser.contexts()[0];
if (!context) throw new Error("browser has no default context");
const page = context.pages()[0] ?? (await context.newPage());
await page.goto("https://example.com");
console.log(await page.title());
} finally {
try {
await browser?.close();
} finally {
await brawsr.closeSession(session.id);
}
}The production endpoint, https://api.brawsr.io, is built in. Pass an explicit
override only for another deployment:
const local = new BrawsrClient({
apiKey: "local-development-key",
baseUrl: "http://localhost:8080",
requestTimeoutMs: 15_000,
});BRAWSR_BASE_URL is intentionally ignored so a process-level setting cannot
silently redirect credentials. The API key can always be passed explicitly.
connectCDP returns immutable connection data. The SDK does not retain the API
key in a browser object or speak CDP itself.
getSession(id) also returns the current stable cdpUrl, so a restarted
process can fetch an active session and pass it directly to connectCDP.
const checkpoint = await brawsr.createCheckpoint(session.id, {
label: "before-submit",
timeoutMs: 30_000,
});
// Continue browser work. This state is intentionally newer than the checkpoint.
await page.fill("textarea", "draft that should be discarded");
// A label, checkpoint ID, Checkpoint, or CheckpointResult is accepted.
const restored = await brawsr.rewind(session.id, checkpoint);
// Rewind replaces the browser process. The old Browser/Page handles are stale.
// Connect again and rediscover the default context and its pages.
const connection = brawsr.connectCDP(restored);
const restoredBrowser = await chromium.connectOverCDP(connection.endpointUrl, {
headers: connection.headers,
});
const restoredContext = restoredBrowser.contexts()[0];
const restoredPages = restoredContext.pages();
// The early rewind result is already CDP-usable. Wait only before another
// lifecycle mutation on this same session (for example checkpoint or close).
await brawsr.waitRewind(restored.operationId);createCheckpoint, rewind, fork, and deleteCheckpoint each send one mutation.
When the API returns 202 Accepted, the SDK polls only the operation resource
and returns the final result. Callers do not need to implement polling.
Checkpoint labels are case-sensitive and unique among live checkpoints in a session. A label that has the checkpoint-ID shape is rejected; use the returned ID when an immutable reference is required.
createSession,getSession,listSessions,iterateSessions,updateSessionLabel,closeSessioncreateCheckpoint,getCheckpoint,listCheckpoints,iterateCheckpoints,listCheckpointAncestry,iterateCheckpointAncestry,deleteCheckpointlistActivity,iterateActivity,getLineage,iterateLineageChildrenrewindfork,closeSessionsgetOperation,waitOperation,waitCheckpoint,waitRewind,waitForkconnectCDP
All iterators are lazy, fetch one page at a time, preserve opaque cursors, and
accept an AbortSignal. Session lists are newest-first. They support exact
status, displayLabel, and sessionId filters, case-insensitive label
search, and inclusive createdFrom / exclusive createdBefore creation-time
bounds. getSession and updateSessionLabel return SessionDetail, including
collection counts, an optional active lifecycle operation, and canonical
collection paths.
listCheckpoints is the session's capture history, including deletion
tombstones. It is useful for audit and dashboard history, but is not a list of
valid rewind targets. Use listCheckpointAncestry for the session's current
rewindable chain. The server still revalidates a checkpoint when rewind starts.
getLineage returns one hop: the selected session, its optional parent edge,
and one cursor-paged set of direct children. Fetch a child's lineage to expand
nested forks. listActivity exposes customer-visible lifecycle and operation
outcomes without internal worker or saga details.
Every waiter accepts a timeout and optional AbortSignal. Stopping a waiter
does not cancel work already admitted by the server. The operation-specific
waiters return the same typed result as the corresponding mutation, including
the restored CDP URL after rewind; callers never need to decode raw operation
results. getOperation and waitOperation remain available for generic
observability.
const forked = await brawsr.fork(session.id, checkpoint, {
n: 3,
ttlSeconds: 300,
timeoutMs: 30_000,
});
for (const child of forked.children) {
const connection = brawsr.connectCDP(child);
// Each child is an independent session. Attach with your browser library.
console.log(child.branchIndex, child.sessionId, connection.endpointUrl);
}
const outcomes = await brawsr.closeSessions(forked, { concurrency: 3 });
for (const outcome of outcomes) {
if (!outcome.ok)
console.error("close failed", outcome.sessionId, outcome.error);
}The returned children are immutable and ordered by branchIndex. The source
session remains open. Children may checkpoint, rewind, or fork again; there is
no browser-state merge. closeSessions never selects a winner, closes the
source, or hides partial failures. It accepts a fork result, child objects, or
session IDs and preserves input order in its outcome list.
BrawsrError: base class for every public SDK error.BrawsrResponseError: the API returned malformed JSON or a response outside the public contract.BrawsrApiError: safe API envelope withstatus, stablecode,requestId, optionaloperationId,retryable, andretryAfterMs.BrawsrOperationError: the server operation reachedfailed.BrawsrWaitTimeoutError/BrawsrWaitCancelledError: local waiting stopped; server work may still complete. For an admitted mutation,operationIdandidempotencyKeyremain available programmatically for recovery.BrawsrTransportError: the transport outcome is ambiguous. ItsidempotencyKeyis available programmatically for recovery but is omitted from the message.
The client does not retry an ambiguous mutation by default. Advanced callers
may provide isPreResponseConnectionError; only a definitely pre-response
failure is retried, at most once, with the same idempotency key.
Every HTTP request has a 30-second transport timeout by default. Configure it
with requestTimeoutMs. A waiter also applies its own overall timeoutMs, so a
hung polling request cannot extend the waiter beyond that deadline. Cancelling
an admitted waiter produces BrawsrWaitCancelledError; aborting an initial
mutation request is transport-ambiguous and preserves its idempotency key in
BrawsrTransportError.
Resume a timed-out rewind without resending it:
try {
await brawsr.rewind(session.id, checkpoint, { timeoutMs: 1_000 });
} catch (error) {
if (!(error instanceof BrawsrWaitTimeoutError) || !error.operationId)
throw error;
const restored = await brawsr.waitRewind(error.operationId, {
timeoutMs: 30_000,
});
const connection = brawsr.connectCDP(restored);
await chromium.connectOverCDP(connection.endpointUrl, {
headers: connection.headers,
});
}Rewind restores browser/client state, not side effects already committed by a remote website. Long-lived WSS, SSE, or WebRTC connections may need application-level reconnection after restore.
See examples/rewind-playwright.ts and examples/rewind-puppeteer.ts for
executable reconnect stories that reject stale page handles and rediscover the
restored pages. A classic Selenium WebDriver session cannot be rebound to the
replacement browser in v0.1; a later WebDriver/BiDi adapter owns that contract.
npm ci
npm run typecheck
npm run typecheck:examples
npm run build
npm test
npm run verify:packageThe package check uses the release-pinned Node 24 + npm 11.5.1 toolchain, builds the archive twice, compares contents and SHA-256, and imports the SDK from a clean temporary archive install. CI tests the installed SDK on Node 22 and Node 24.