@corbado/autocapture
v0.8.1
Published
Observer-style capture helpers for WebAuthn ceremonies, selected network exchanges, in-app navigation, and composed event paths.
Downloads
515
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 uses @corbado/observe as a required runtime peer, ships TypeScript type definitions, and targets
ES2019 for its own bundle. The host application must also support the Observe SDK.
Installation
npm install @corbado/autocapture @corbado/observe@^0.16.1Usage
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 call (a missing onEvent, onNavigate, match, or onExchange
callback) 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. Keep the predicate from matching the endpoint the consumer reports events to, or the reporting traffic itself becomes observed traffic. - 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 interception and network fallbacks share four helpers exported by @corbado/observe
(and re-exported here for compatibility):
sanitizeCreationOptions, sanitizeCreationResponse, sanitizeRequestOptions, and
sanitizeAssertionResponse. Each accepts the relevant native browser object, a JSON object, or a
JSON string, and returns JSON text or undefined on malformed/unserializable input. Pass the
publicKey options or credential itself, not an application-specific request envelope.
Import these helpers directly from @corbado/observe for new integrations. The blacklist lives
only in Observe’s src/utils/webauthn-payloads.ts:
| Payload | Removed paths |
| --- | --- |
| Creation options | user.name, user.displayName |
| Creation response | clientExtensionResults.prf.results |
| Request options | None |
| Assertion response | response.signature, clientExtensionResults.prf.results |
All other JSON fields are retained, including new/unknown fields and extensions. Binary values are
base64url-encoded. Native WebIDL attributes and response methods are normalized without calling
credential.toJSON(). Inputs are never mutated; blacklisted property getters are not read. Failures
omit the payload without logging raw input or exception messages. This is a path blacklist, not a
content scanner: IDs, user handles, challenges, complete attestation material and non-blacklisted
extension data remain. Encoded attestation/client-data blobs are not decoded or redacted.
started registration events carry creationOptionsJson; authentication events carry
requestOptionsJson. completed events carry credentialJson. All are processed by the helpers
before callbacks run. For a network fallback, extract the credential/options from the endpoint
body and call the corresponding helper in bodyCapture before returning the projected body.
Adapters own endpoint mapping and source deduplication, never copies of the blacklist.
This changes the telemetry contract: assertion signatures are removed, and fields formerly excluded by the old allowlist are retained unless blacklisted. Existing consumers should validate their parsers when upgrading. An adapter pinned to an older release is unaffected until upgraded.
The tested 1Password browser extension exposes locked get and create accessors. By default
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.
captureWebAuthn({ attach: "container" }) opts into a second attachment surface for that case: when
the methods cannot be patched, capture attaches to the navigator.credentials container instead and
observes reads of get/create there. The methods surface is always tried first, so the option
changes nothing where the direct patch works. The integration holding the methods is never written
to and keeps handling every ceremony.
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.
Input validation
captureInputValidation() observes one logical operation's fields and reports at most one local
validation error per submission attempt. Supply a validation-state reader and feed matching request
starts from your existing network capture. Native constraints, custom/auto-submit entry points,
multi-input groups, repeated submissions, server-error suppression, and cleanup are supported.
It does not activate automatically or collect field values/message text.
See the API and evidence contract, including the limits of
asynchronous DOM inference and the beginAttempt(), requestStarted(), and refresh() methods.
Local development
Run pnpm --filter @corbado/autocapture... build from the workspace root before linting or
running unit tests. The workspace development dependency supplies the required Observe peer;
Observe stays external in the published bundles. pnpm test:e2e builds both packages.
When increasing the minimum Observe peer version, publish Observe before autocapture.
