@corbado/autocapture
v0.2.0
Published
Observer-style capture helpers for WebAuthn ceremonies, selected network exchanges, in-app navigation, and composed event paths.
Keywords
Readme
@corbado/autocapture
Observe browser authentication signals without changing application code.
@corbado/autocapture provides small, focused observers for the browser APIs that authentication
flows run through. Each observer reports a well-defined stream of events while leaving the
application — and other code sharing the same APIs — as safely as browser monkey-patching permits.
It is designed to be dropped into a page it does not own and fail closed when a surface cannot be
instrumented safely. The remaining cases where observation cannot be perfectly transparent are
called out in the safety invariants and data handling sections below.
The package observes three kinds of signal:
- WebAuthn ceremonies — the lifecycle of
navigator.credentials.get/createpasskey ceremonies, from invocation to settlement, including silent cancellations. - Network requests and exchanges — request starts and their eventual outcomes over
XMLHttpRequestandfetch, limited to URLs a caller-provided predicate selects. A page-local request ID correlates both callbacks. - In-app navigation — same-document (SPA) route changes.
It also exposes composedPath() for selector matching across visible shadow-DOM boundaries.
It has no runtime dependencies, ships TypeScript type definitions, and targets ES2019 so it parses on older engines.
Installation
npm install @corbado/autocaptureUsage
Every observer follows the platform observer pattern (MutationObserver, ResizeObserver): create
it with a callback, receive events, and call disconnect() when finished.
import { captureWebAuthn, captureNetwork, captureNavigation, composedPath } from "@corbado/autocapture";
const webauthn = captureWebAuthn({
onEvent: (event) => console.log(event.kind, event.mediation, event.phase, event.ceremonyId),
});
const network = captureNetwork({
match: (url) => url.origin === location.origin && url.pathname.startsWith("/auth-api/"),
bodyCapture: {
request: (body) => {
const value = typeof body === "string" ? JSON.parse(body) : body;
return value && typeof value === "object" ? { type: "type" in value ? value.type : undefined } : undefined;
},
response: (body) => {
const value = typeof body === "string" ? JSON.parse(body) : body;
return value && typeof value === "object" ? { status: "status" in value ? value.status : undefined } : undefined;
},
},
onRequest: (request) => console.log(request.requestId, request.method, request.path),
onExchange: (exchange) => console.log(exchange.method, exchange.path, exchange.status, exchange.outcome),
});
const navigation = captureNavigation({
onNavigate: (event) => console.log(event.source, event.path),
});
document.addEventListener("click", (event) => {
const clickedButton = composedPath(event).find(
(entry) => entry instanceof HTMLButtonElement,
);
if (clickedButton) console.log(clickedButton);
});
// when finished
webauthn.disconnect();
network.disconnect();
navigation.disconnect();Each capture call returns a CaptureHandle with a mode ("standard" or "unavailable") and a
disconnect() method. Environments that cannot be instrumented report "unavailable" rather than
throwing; a misconfigured captureNetwork call (missing match or onExchange) throws a
TypeError instead, since that is a programming error, not a degraded environment. Full event and
option types ship with the package.
WebAuthn events use a protocol-native discriminated union. Authentication phases carry normalized
mediation, uiMode, and credentialSet ("allowlisted" or "discoverable"); registration
phases carry normalized mediation. The same profile is present on every phase of one ceremony.
Autocapture deliberately does not infer product concepts such as conditional-UI spec types,
known-identifier state, enrollment intent, or the user/system actor. Integrations map those facts in
their own state context.
Safety invariants
The observers run inside application call paths, so their behaviour is defined by what they promise never to disturb:
- Exact host delegation. Patched methods call the delegate once with the original receiver and
argument vector, and preserve its return value and synchronous exception. Non-WebAuthn Credential
Management calls are literal pass-throughs. Two Promise-observation choices are unavoidable:
public-key ceremonies return a derived Promise (identity and microtask timing change), and fetch
settlement is observed on the platform Promise itself, so an ignored failed fetch no longer
surfaces as an
unhandledrejection. - Observation is firewalled. Synchronous callback, projector, serializer, and logger faults are contained; rejected asynchronous event callbacks are consumed and logged. The firewall covers first-party faults on a real browser engine — hostile polyfills and patched built-ins are outside the threat model.
- Coexistence over ownership. Existing delegates are preserved, slots are restored only while still owned by the capture, and later wrappers are never overwritten. A buried wrapper becomes an inert pass-through after disconnect. Locked WebAuthn methods are never bypassed through a container proxy; the entire WebAuthn attachment is rolled back and reported as unavailable.
- Exactly-once reporting. Each patched surface holds a single capture wrapper, so one application action passes through observation once. A later integration that overwrites a wrapped method without forwarding through it displaces capture for that surface: the application call still succeeds, telemetry is lost. A package wrapper hidden below an unknown third-party wrapper cannot currently be detected by another copy.
- Conditional removal.
disconnect()stops delivery immediately, releases active listeners, and restores only slots still owned by the capture. If another integration sits above it, the inert pass-through is deliberately left in that chain.
These are implementation invariants, not a claim that monkey-patching is undetectable or has zero timing, memory, privacy, or Promise-semantics impact.
Data handling
Network observation is designed for minimal data exposure:
- Only URLs a caller's
matchpredicate explicitly selects are observed; all other traffic is ignored. The Corbado/observe/events/{projectID}sink is always excluded to prevent recursion. - Requests and exchanges carry metadata only by default.
onRequestruns after the platform accepts the call;onExchangereports its eventual outcome, and theirrequestIdvalues match. A request or response body is read only when the caller configures a projector for that direction. - Projected XHR request bodies are available at request time. For
fetch(new Request(...)), reading the request clone is asynchronous, so the request body may be absent fromonRequestand present only ononExchange; string bodies passed throughRequestInitare available synchronously. - Autocapture performs no generic body parsing or redaction. The endpoint-specific projector is the
privacy boundary: it receives the matched raw body and must return only the fields the consumer is
allowed to observe. The exact projected value is attached to the exchange without generic
serialization, cloning, or size limiting. Projector failures, promises, and
undefinedfail closed by omitting the body. - Readable bodies come from isolated clones read within a 500 ms window; the source body is never directly consumed, and streaming and binary payloads are never read. Body cloning still has inherent stream tee/backpressure costs, so enabling it is a security and performance decision.
Network metadata currently includes the resolved full URL and final response URL. Query strings can
contain secrets or PII; callers should restrict match accordingly until a URL-redaction policy is
chosen.
Navigation events likewise contain the raw search and hash. OAuth responses, password-reset
links, and application routes can put secrets or PII there; no navigation redaction policy is
currently applied.
WebAuthn responses use an explicit serializer rather than credential.toJSON(). The allowlist keeps
the assertion and attestation fields the Observe backend parses for credential, authenticator,
backup-state, AAGUID, transport, and discoverability insight. Client-extension output is limited to
credProps.rk; PRF and unrelated extension results are omitted. The payload therefore still contains
credential IDs, user handles, challenges, signatures, authenticator data, and attestation material.
The tested 1Password browser extension exposes locked get and create accessors. WebAuthn capture
therefore reports mode: "unavailable"; passkey operations continue through 1Password unchanged but
produce no autocaptured WebAuthn events. Password managers that wrap the writable methods before
autocapture are delegated to; one that assigns its wrapper afterwards keeps working, and its
ceremonies stay observed when the wrapper forwards through the method it replaced (the surveyed
managers do). A later assignment that does not forward displaces WebAuthn telemetry.
Runtime support
The published bundle targets ES2019 and assumes ES2019-era runtime primitives such as Promise,
WeakMap, WeakSet, Symbol, Reflect, and URL. Body capture additionally relies on the
Streams API and TextDecoder; these are part of the assumed baseline, not feature-detected at
runtime. The current real-browser suite covers current
Chromium, Firefox, and WebKit, with password-manager simulation and virtual WebAuthn in Chromium.
That is not yet evidence for old iOS, Android WebView, legacy Edge, or arbitrary polyfills; expanding
the declared browser floor and test matrix is an open release decision.
