npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@corbado/autocapture

v2.15.0

Published

Observer-style capture helpers for WebAuthn ceremonies, selected network exchanges, in-app navigation, and composed event paths.

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/create passkey ceremonies, from invocation to settlement, including silent cancellations.
  • Federated identity (FedCM) requests — navigator.credentials.get({ identity }) calls such as Google One Tap on Chromium: the providers requested, the account chosen, or why it failed.
  • Network requests and exchanges — request starts and their eventual outcomes over XMLHttpRequest and fetch, 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.1

Usage

Every observer follows the platform observer pattern (MutationObserver, ResizeObserver): create it with a callback, receive events, and call disconnect() when finished.

import {
  captureFederatedIdentity,
  captureWebAuthn,
  captureNetwork,
  captureNavigation,
  composedPath,
} from "@corbado/autocapture";

const webauthn = captureWebAuthn({
  onEvent: (event) => console.log(event.kind, event.mediation, event.phase, event.ceremonyId),
});

const fedcm = captureFederatedIdentity({
  onEvent: (event) => console.log(event.phase, event.requestId),
});

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();
fedcm.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.

Federated identity events carry the requested providers (configURL, clientId), context, mode and mediation on started; the chosen provider's configURL and isAutoSelected on completed; errorName (plus an IdentityCredentialError code) on failed. Tokens, nonces, provider parameters and login hints are never read. captureWebAuthn and captureFederatedIdentity share one attachment to the credential methods and can start and stop in any order; a later capture joins at the surface the first one attached to, so pass attach: "container" to whichever starts first (see src/federated-identity.contract.md).

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, shared by the WebAuthn and federated identity captures, 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 match predicate 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. onRequest runs after the platform accepts the call; onExchange reports its eventual outcome, and their requestId values 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 from onRequest and present only on onExchange; string bodies passed through RequestInit are 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 undefined fail 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 |

User identifiers (creation user.id, response userHandle) are omitted unless a projector supplies a value. 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: credential IDs, challenges, complete attestation material and non-blacklisted extension data remain. Encoded client-data blobs are not redacted.

An optional synchronous user identifier projector customizes the shared serializers and capture:

import { captureWebAuthn, sanitizeAssertionResponse, type WebAuthnIdentifierProjectors } from "@corbado/autocapture";

const identifierProjectors: WebAuthnIdentifierProjectors = {
  userId: (id) => hashForTelemetry(id),
};
captureWebAuthn({ identifierProjectors, onEvent: (event) => console.log(event) });
const assertionJson = sanitizeAssertionResponse(submittedCredential, identifierProjectors);

A missing callback omits user identifiers. Binary user IDs arrive as base64url strings; JSON string IDs arrive as supplied. Return a string to replace an ID, null to retain explicit null, or undefined to omit it. Exceptions and unsupported return values also omit it, without logging the original ID or callback error. The callback must be synchronous and should be deterministic across payloads. It applies to serialized WebAuthn user IDs, not the tracker's application-level user identity.

User projection covers creation user.id and response userHandle. Credential IDs (id, rawId, allow/exclude descriptor IDs and PRF credential keys) remain unchanged. Encoded attestation and authenticator data are not decoded or rewritten. Original authentication objects remain untouched, and signatures/PRF outputs remain blacklisted. User-handle omission/null is supported by current parsing; replacement user handles must remain base64url-compatible until backend parsing accepts arbitrary projected strings.

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.

Collection spec readers

The declarative specs (defineEndpoints, definePages, defineRoutes, defineElements, defineStores, defineGlobals, defineWebAuthn, also importable side-effect free from @corbado/autocapture/spec) describe every value an integration reads and how it is reduced; the collection manifest is extracted from them. Each reader returns only the reduced value.

A field's data category (f(path, { category }), q/p/h, clearValue/valueToken, r.<source>({ category }), elementAt, a role's category) is one of personal-identifier, account-attribute, protected-input, passkey, device, behavior and technical (SpecCategory); personal-data and secret are accepted as the former names of account-attribute and protected-input. The v1 manifest writes the former names (toManifestSources), so either name gives the same manifest.

defineStores is the exception: the bundle's own storage is an output, not a source. A store declares its area, its key (must start with cbo_; a store without it warns and reads and writes nothing) and an optional plain-language retention, but no fields: what it holds was read elsewhere and is declared there. describeStores(stores) (extraction only, never in a bundle) returns its entries of the manifest's storage section, validateManifestStorage checks them; describe() lists no sources.

Readers that only some integrations need are separate exports, so a bundle that does not use them does not contain them. Reductions of that kind plug into any body field (f(path)), query parameter (q(name)) or endpoint path group (p(group)) through .as(reduction):

| Reader | Returns | Manifest exposure | | --- | --- | --- | | .as(memoryKey()) | MemoryKey: SHA-256 hex of the value as is, for matching in memory (flow ids, credential ids); not meant to be sent | pseudonym | | f(path).as(webauthnPayload("requestOptions" \| "assertion")) | the SDK-sanitized SdkWebAuthnPayload of passkey request options or an authentication credential | raw / redacted | | .pseudonym(scheme, { dropPrefix }) | sha256 / transaction-id pseudonym; dropPrefix removes leading characters after trim + lower case | pseudonym | | f(path).presence("string") | whether the value is a string (the empty one included) | presence | | .as(intCode({ max, pattern })) | an open machine code: a string matching pattern (default digits) or a safe integer as its decimal string, at most max characters | token | | f(path).as(ownKey()) | whether the key exists, null counting as present | presence |

Body readers beyond fixed paths, also separate exports (endpoint field maps):

| Reader | Returns | Manifest | | --- | --- | --- | | deepKey(path, key, { depth, limit, items, spread }).<reduction>() | the reduced values of own key key anywhere below path (default ≤ 5 levels, first 8, 20 items per array; a match is not searched further; spread: n replaces a found array by its first n items) | <path>.**.<key> | | jsonObjects(path, fields, label, { key, maxLength, spread }) | fields projected on every object at path (or found by a key search), JSON-string values decoded first | <path> presence + fields | | bodyShape() | html / json / text / none of the body, also when it is not JSON; content not kept | class on response.body | | responseHeader(name).<reduction>() | one declared response header (read in the projector context) | response.header.<name> | | scalarBody().<reduction>() | a JSON scalar body (true, 42, "x"); other fields of the map need an object / array | the body itself |

f(path, { unwrap: "name" }) reads array items that are a string or an object (["a", { "name": "b" }] → ["a", "b"]; the manifest lists path and path.name).

Endpoint specs take params (query parameters with q(name), capture groups of a RegExp path with p(group), a positive integer), delivered as params on the endpoint signals, and late: false to keep an endpoint out of the autocapture runtime's late-install catch-up.

DOM, store and page-environment readers come as one subpath module per group, so a bundle keeps only the readers it calls:

import * as dom from "@corbado/autocapture/dom"; // find, read, on, handoff, ...
import * as store from "@corbado/autocapture/store"; // available, read, write, remove
import * as pageEnv from "@corbado/autocapture/page-env"; // origin, onDomReady, onPageHide, ...

The grouped objects of the package root (import { dom, store, pageEnv } from "@corbado/autocapture") are deprecated: they keep working with the same call sites, but ship every reader of their group.

Page, element, store and WebAuthn readers:

  • readInitialPage(pages) matches the address the document was loaded with (Navigation Timing), for parameters the site strips right after loading; undefined without a navigation entry.
  • referrerOrigin() reduces the referrer to its origin (or android-app://<package>), none or other.
  • dom.shown(handle, "box"): shown by layout box only (getClientRects()), without the style checks of the default rendered. Also for dom.count({ shown }) and shownBy.
  • findRelated(role, anchor) finds an element by an id derived from the anchor's id (role idTemplate: "{id}--error").
  • attrJson(name, fields, { decode: "astro-island" }) (a read in a role's reads) parses an attribute as JSON, optionally decodes Astro island props, then applies body field readers to it. Attributes longer than max (default 262,144 characters, at most 1,048,576) are not parsed.
  • store.available(stores, name) tells blocked storage (false) from an absent entry.
  • defineWebAuthn({ credentialId: memoryKey() }) adds credentialId (a MemoryKey) to completed ceremony signals; it equals f("authenticationResponse.id").as(memoryKey()) over the same credential.

Readers for hash-routed apps, host globals and declared cleartext (separate exports, so bundles that do not use them do not contain them):

  • defineRoutes({ key: { title, hash: patterns, params: { name: h(group).token(...) } } }) matches the fragment route (#/a/b, without the fragment's own query) per key; readRoute(routes, key) returns the match and its declared capture groups. routeOf(url) gives the route of a URL.
  • defineGlobals({ name: { title, global: "alloy_all", fields: { x: f(path).token(...) } } }) declares a property the host page assigned to window; readGlobal(globals, name) returns only its declared fields. Browser built-ins (document, location, storage, ...) are never read, and fields are read from a bounded copy of the value's plain data only (plain objects, arrays, primitives through data properties): no getter, host object or cycle anywhere in a path. A Proxy cannot be detected.
  • .as(flagKeys({ max, equals, pattern })): the sorted own keys whose value equals equals (default true), e.g. active feature flags. .as(urlOrigin()): a URL reduced to its origin, the same reduction as referrerOrigin().
  • f("[].x") reads into a body whose root is an array; endpoint projections accept such bodies.
  • clearValue({ max, reason }) (in a role's reads) returns a form value as entered. Every readable read (clearValue, f(path).clear, q(name).clear) needs a non-empty reason, listed in the manifest with the reason as its note; max is clamped to 0…65536. Refused (never read): body fields and query parameters whose name looks like a credential, one-time code, PIN or card (pass*, pwd, pin, otp, cvv, card*, cc-*, secret*, token); form controls with a password type (property or attribute, also custom elements), one that had it before on the page, a secret-looking name / id / autocomplete, or -webkit-text-security masking. A field switched to plain text before its first read, with a neutral name, id and autocomplete, cannot be detected.
  • clearValue({ max, reason, category: "personal-identifier", shape: identifierShape({ accept: ["email", "phone"] }) }): an identifier field read that returns the value only when it has one of the accepted shapes, normalised, and nothing otherwise (checked inside the read). Email: trimmed and lower-cased, exactly one @, a non-empty local part, a dotted domain, no whitespace. Phone: spaces, dashes, dots and parentheses removed, a leading 00 as +; only digits with an optional +, 8 to 15 digits with +, 7 to 12 without, not all the same digit; a card number (13 to 19 digits, never with +) does not fit the length. No country or locale input. A normalised value longer than max is not sent. The manifest transform carries shape: { accept, rules }; the autocapture tools write the label from it and require a shape on every personal-identifier clearValue read. A separate module: bundles that do not import identifierShape do not contain it.

URL data, host cookies and value codes (separate exports):

  • Page specs read path groups like endpoint specs: params: { route: p(1).token({ max, pattern }) } (manifest url.path.<n>), next to query parameters (q(name)).
  • Fixed source path: a page spec's sourcePath ({ path: "/security/challenge-me/:route", query: ["errorCode"] }: literal segments and :name placeholders of declared parameters, plus the params keys of query parameters to send; a plain string is a path without query values) and pageSourcePath(pages) give the policy for the @corbado/observe plugin fixedSourcePath(). Masked by default: a matched page with a template sends the template filled with the reduced parameters; any other path sends * per segment; only the listed query parameters keep their reduced value, every other value becomes * (also parameters declared only for reading; keys that are not plain parameter names too). The server's sourcePathQuery config then no longer applies.
    initCore({ projectId, apiBaseUrl, plugins: [fixedSourcePath(pageSourcePath(PAGES))] });
  • readReferrerPage(pages): the referrer's origin (as referrerOrigin()) and the page spec its path matches, with that spec's declared parameters. Page specs match the path only: compare the origin too.
  • .as(urlOriginPath()): a URL (e.g. an OAuth redirect_uri) reduced to origin and path; never query or fragment, path segments that are not plain names become *.
  • defineHostCookies({ name: { title, cookie: "cl_spoid", fields: { id: f("", { category }).pseudonym("sha256") } } }) declares a cookie the host page writes; readHostCookie(cookies, name) returns only its declared fields over the URI-decoded value (at most 4096 characters). Observe's own cbo_ storage is refused (use defineStores). Listed as a storage source with the locator cookie <name>.
  • valueToken({ max, pattern }) (in a role's reads): a form value that is a short code matching pattern (e.g. a numeric error code in a hidden field), listed as token. r.value() never forwards the value; secret fields are refused as for clearValue.

Host page events (separate exports):

  • defineHostEvents({ name: { title, event: "unified-login-decision", fields } }, { namespace, buffer }) declares CustomEvents the host page dispatches on window; fields reduce the event's detail like body fields (f(path, { label, category }), objectsAt("errors", { field: f("field"), … }, label) for lists of objects, .as(epochMs()) for a point in time). Element references in the detail are declared with elementAt(path, { accept: "input", handoff: "sdk:input-tracking", strict }) and arrive as opaque handles; hostEventHandoff(handle) releases a handle of a handoff field to the SDK input tracking (inputHtmlField(s)). Listed as a dom source per event (dom:<namespace>.<name>, mechanism listen, fields under detail. plus the dispatch time, locator window event <type>).
  • onHostEvent(events, name, handler, { onError }): handler({ name, time, fields }) per dispatch on window (time = Date.now() when the listener runs), guarded; returns the remover. projectHostEvent(events, name, event) projects an Event the project already holds.
  • buffer: { title, global: "__cboQ" } declares the host snippet's early-event buffer window.<global> = { v: 1, q: [{ n, ev, t }], dropped, off } (n event type, ev the Event, t its dispatch time in epoch ms, dropped events the snippet could not hold, off() stops the buffering). readHostEventBuffer(events) returns absent, unreadable (not version 1) or the live queue: buffer.at(i) is entry i as { name, time, fields } (undefined when it is not a declared event), iterate up to buffer.size() (it may grow during the replay), then buffer.release(). Listed as a host-config source (host-config:<namespace>.event-buffer, locator window.<global>).
    const read = readHostEventBuffer(EVENTS);
    if (read.kind === "buffer") {
      for (let i = 0; i < read.buffer.size(); i++) {
        const signal = read.buffer.at(i);
        if (signal) route(signal);
      }
      read.buffer.release();
    }
    for (const name of EVENTS.keys) onHostEvent(EVENTS, name, route);

pseudonymOf(scheme, value, { projectId, dropPrefix }) computes the same pseudonyms synchronously for runtimes that wrap them. Pseudonyms and memory keys use a pure SHA-256 implementation, so they are available synchronously, also where WebCrypto is not.

Typed role declarations and gateways (collection manifest v2)

A role declares its fields, each at one collection level, and a category; the gateways attach and read only what is declared, and the manifest follows from the declaration (no auto-added exists / shown).

import { attr, defineElements, empty, interaction, pseudonym, retention, shown, text, validity } from "@corbado/autocapture";
import * as inputs from "@corbado/autocapture/inputs";
import * as elements from "@corbado/autocapture/elements";

const EL = defineElements({
  password: { title: "Password field", locators: 'input[type="password"]',
              input: { category: "secret", fields: { interaction: interaction(), validity: validity({ only: "missing" }) } } },
  email: { title: "Email field", locators: "#email",
           input: { category: "personal-identifier", fields: { interaction: interaction(), tx: pseudonym("transaction-id") } } },
  banner: { title: "Error banner", locators: ".error", category: "technical",
            fields: { kind: text().classify([[/locked/i, "locked"]]), visible: shown() } },
}, { namespace: "vg" });

inputs.onInput(EL.roles.password, (input) => op.bind(input)); // every instance as an InputHandle (replaces the
                                                      // runtime's onInputInstances, deprecated for v2 projects)
inputs.value(EL.roles.email, "tx", undefined, { projectId });
elements.read(EL.roles.banner, handle);               // { kind } — declared values only

| Declared field | Allows | | --- | --- | | any | inputs.present, elements.find / findAll / present | | empty() or interaction() (input) | inputs.isEmpty | | interaction() (input) | inputs.find / findAll / of(role, at?) / onInput(role, cb) (every instance): InputHandles only; inputs.onTyped(role, input, cb): typing without values | | value reductions (pseudonym, value().enum, checked, …) | inputs.value(role, name) | | validity() | inputs.validationAttempts (only: "missing": only valueMissing, allowed on secrets) | | attr(…), text(), read(attrJson(…)) (element) | elements.read | | shown() | elements.shown | | interaction({ events }) (element) | elements.on / onDelegated: click + the declared events |

Categories (protected-input, personal-identifier, opaque-identifier, account-attribute, customer-supplied, device, technical; secret and personal-data are accepted as their former names) allow only their value levels (a protected input none); a level outside the kind's ladder or the category fails to type-check and is dropped at define time. Element readers refuse input, textarea, select and editable elements. The gateways accept only entries created by define*. Stores declare contents (typed references) and retention("PT1H", { enforced: true }); host events declare channel: "page-event" | "post-message" (post-message needs origins; declared only, no reader yet). @corbado/autocapture/manifest holds the ac:element@1 / ac:request@1 descriptors and the v2 role describer for the tools; it is never part of a bundle, and v1-only bundles contain none of the v2 code.

Generated ids (@corbado/autocapture/generated-id)

Random ids the project's own code creates (for example a journey id that links the pages of one sign-in) go through the gateway generatedId(ids, name), and every id is declared in a defineGeneratedIds registry of a spec module (@corbado/autocapture/spec): what it points to, how long it lives, where it is shared and who besides Corbado can see it. The tools list the declared ids under data.outputs.sent.generated (generated:<namespace>.<name>) for bundles that call generatedId (gateway ac:generated-ids, code: ["generatedId"]). generatedId returns a lower-case v4 UUID (crypto.randomUUID, else crypto.getRandomValues; without crypto a time-based hexadecimal fallback), and undefined for a name the registry does not declare or a registry not created by defineGeneratedIds. Never throws. Its own subpath only (not a root export): bundles that create no ids do not contain it.

// src/project/spec/ids.ts
export const IDS = defineGeneratedIds(
  { journey: { label: "Random id of one sign-in", pointsTo: "flow", lifetime: "PT30M", exposedTo: [] } },
  { namespace: "vg" },
);
// project code
const id = generatedId(IDS, "journey");

Late-install catch-up (@corbado/autocapture/late-install)

Requests the host sent before the script installed, part of the request gateway ac:request@1 (its descriptor lists the side effects: the page-wide Response.prototype.json hook, the Resource Timing read, the optional XHR getter hooks). installLateInstallCatchUp, installLateXhrCatchUp, installLateEndpointCatchUp, installLateEndpointXhrCatchUp; telemetry goes to a sink with ac(kind, fields, level) (the autocapture runtime's Telemetry). Moved from the autocapture runtime, which re-exports it.

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.

Probe capture (@corbado/autocapture/probe)

A separate subpath (existing imports of @corbado/autocapture do not load it): a config-driven diagnostic capture for Corbado autocapture bundles. A JSON config selects surfaces (page, ui, act, nav, res, store, net, form, err, console, dl, msg, glob, cev, each off | inventory | shape | values), redaction rules, actions (snapshot, correlate, count), conditions and a flight-recorder trigger. Redacted items go to an injected sink, normally the internal tracker.probe() of Observe ≥ 1.4 (no transport of its own).

import { startProbes, validateProbeConfig } from "@corbado/autocapture/probe";

const handle = startProbes(config, {
  sink: (item) => tracker.probe(item),
  projectId,
  network: { subscribe: (subscriber) => hub.subscribeNetwork(subscriber) }, // else captureNetwork
  navigation: { subscribe: (listener) => hub.subscribeNavigation(listener) }, // else captureNavigation
  webauthn: { subscribe: (listener) => hub.subscribeWebAuthn(listener) }, // taint feed only
  context: () => ({ state, flowType, step }),
  tags: () => flowTags,
  sessionId: () => tracker.getSessionId(),
});
handle.signal("step-error"); // trigger / action events from the host

Defaults are conservative: every value no rule names becomes its class (<email>, <digits:6>, <text:3w>), free text keeps plain words and classes every other token, and clear happens only through an explicit rule. Always-on guards that no config can loosen: field values are read through one path (password, one-time-code and card fields, and inputs of WebAuthn-bound forms yield at most their length), typed secrets, WebAuthn ceremony parts, cookie values and Authorization headers are fingerprinted and replaced wherever they show up, a pattern pass removes JWT/JWE, card numbers, WebAuthn client data, password/WebAuthn keys and high-entropy tokens, a failing guard drops the item, and items are capped at 16 KB (large views are split into several items). One engine per page; startProbes never throws. When injecting no sources, the engine installs captureNetwork / captureNavigation itself, which then occupy those APIs for other captures on the page.

See the probe contract for the config reference and an example config. The guard conformance suite (probeGuardFixtures, runProbeGuardFixture, applyGuards) lives in its own subpath, @corbado/autocapture/probe/fixtures, so bundles that import /probe never carry its fake secrets. The package is marked "sideEffects": false.

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.