@corbado/autocapture 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +89 -0
- package/dist/index.d.mts +193 -0
- package/dist/index.d.ts +193 -0
- package/dist/index.js +1 -0
- package/dist/index.mjs +1 -0
- package/package.json +34 -0
package/README.md
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# @corbado/autocapture
|
|
2
|
+
|
|
3
|
+
Observe browser authentication signals without changing application code.
|
|
4
|
+
|
|
5
|
+
`@corbado/autocapture` provides small, focused observers for the browser APIs that authentication
|
|
6
|
+
flows run through. Each observer reports a well-defined stream of events while leaving the
|
|
7
|
+
application — and any other code sharing the same APIs — completely undisturbed. It is designed to
|
|
8
|
+
be dropped into a page it does not own and behave as a careful guest.
|
|
9
|
+
|
|
10
|
+
The package observes three kinds of signal:
|
|
11
|
+
|
|
12
|
+
- **WebAuthn ceremonies** — the lifecycle of `navigator.credentials.get`/`create` passkey
|
|
13
|
+
ceremonies, from invocation to settlement, including silent cancellations.
|
|
14
|
+
- **Network exchanges** — request/response round trips over `XMLHttpRequest` and `fetch`, limited to
|
|
15
|
+
URLs a caller-provided predicate selects.
|
|
16
|
+
- **In-app navigation** — same-document (SPA) route changes.
|
|
17
|
+
|
|
18
|
+
It has no runtime dependencies, ships TypeScript type definitions, and targets ES2019 so it parses
|
|
19
|
+
on older engines.
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
npm install @corbado/autocapture
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
Every observer follows the platform observer pattern (`MutationObserver`, `ResizeObserver`): create
|
|
30
|
+
it with a callback, receive events, and call `disconnect()` when finished.
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
import { captureWebAuthn, captureNetwork, captureNavigation } from "@corbado/autocapture";
|
|
34
|
+
|
|
35
|
+
const webauthn = captureWebAuthn({
|
|
36
|
+
onEvent: (event) => console.log(event.kind, event.phase, event.ceremonyId),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const network = captureNetwork({
|
|
40
|
+
match: (url) => url.origin === location.origin && url.pathname.startsWith("/auth-api/"),
|
|
41
|
+
onExchange: (exchange) => console.log(exchange.method, exchange.path, exchange.status, exchange.outcome),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const navigation = captureNavigation({
|
|
45
|
+
onNavigate: (event) => console.log(event.source, event.path),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// when finished
|
|
49
|
+
webauthn.disconnect();
|
|
50
|
+
network.disconnect();
|
|
51
|
+
navigation.disconnect();
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Each call returns a `CaptureHandle` with a `mode` (`"standard"`, `"compatibility"`, or
|
|
55
|
+
`"unavailable"`) and a `disconnect()` method. Full event and option types ship with the package.
|
|
56
|
+
|
|
57
|
+
## Guarantees
|
|
58
|
+
|
|
59
|
+
The observers run inside application call paths, so their behaviour is defined by what they promise
|
|
60
|
+
never to disturb:
|
|
61
|
+
|
|
62
|
+
- **The application always comes first.** Original browser methods are invoked with their arguments,
|
|
63
|
+
results, and exceptions unchanged. Every observation path is guarded, so a fault in an observer or
|
|
64
|
+
in a caller's own callback can never surface into the application's call.
|
|
65
|
+
- **Coexistence, not ownership.** Password managers, monitoring SDKs, and frameworks routinely
|
|
66
|
+
instrument the same APIs. The observers share those surfaces rather than seize them: other
|
|
67
|
+
integrations keep working regardless of load order, and each handle's `mode` reports how it
|
|
68
|
+
attached. Where attaching cleanly is impossible, an observer reports `mode: "unavailable"` rather
|
|
69
|
+
than throwing.
|
|
70
|
+
- **Exactly-once reporting.** A single application action can pass through several layered
|
|
71
|
+
integrations; each ceremony, exchange, and navigation is reported once, correlated by id where its
|
|
72
|
+
lifecycle spans multiple events.
|
|
73
|
+
- **Clean removal.** `disconnect()` stops delivery immediately and removes the observer's
|
|
74
|
+
instrumentation without disrupting the application or any integration layered on top.
|
|
75
|
+
|
|
76
|
+
## Data handling
|
|
77
|
+
|
|
78
|
+
Network observation is designed for minimal data exposure:
|
|
79
|
+
|
|
80
|
+
- Only URLs a caller's `match` predicate explicitly selects are observed; all other traffic is
|
|
81
|
+
untouched.
|
|
82
|
+
- Exchanges carry **metadata only** (method, URL, status, outcome, duration) by default. Request and
|
|
83
|
+
response bodies are captured only when a caller opts in per direction.
|
|
84
|
+
- When bodies are captured, a built-in credential redactor always runs and cannot be disabled;
|
|
85
|
+
callers may add further redaction but cannot re-expose what the built-in removed. Bodies are bounded
|
|
86
|
+
in size and read from clones, so the application's own body consumption is never disturbed, and
|
|
87
|
+
streaming and binary payloads are never read.
|
|
88
|
+
|
|
89
|
+
WebAuthn payloads are reported in the standard `PublicKeyCredential.toJSON()` shape.
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/** Minimal logging seam. All capture modules are silent unless a logger is provided. */
|
|
2
|
+
type Logger = {
|
|
3
|
+
log: (...args: unknown[]) => void;
|
|
4
|
+
};
|
|
5
|
+
/**
|
|
6
|
+
* Handle returned by every capture function, following the platform observer
|
|
7
|
+
* pattern (MutationObserver, ResizeObserver): create it with a callback,
|
|
8
|
+
* receive events, and `disconnect()` when done.
|
|
9
|
+
*/
|
|
10
|
+
type CaptureHandle = {
|
|
11
|
+
/**
|
|
12
|
+
* Whether and how the capture attached:
|
|
13
|
+
* - `"standard"`: attached and delivering events.
|
|
14
|
+
* - `"compatibility"`: attached via a fallback path (another integration held
|
|
15
|
+
* the primary one) and delivering events.
|
|
16
|
+
* - `"unavailable"`: could not attach (the environment disallows it, or an
|
|
17
|
+
* equivalent capture is already active); no events are delivered and
|
|
18
|
+
* `disconnect()` is a no-op.
|
|
19
|
+
*/
|
|
20
|
+
readonly mode: "standard" | "compatibility" | "unavailable";
|
|
21
|
+
/**
|
|
22
|
+
* Stop event delivery immediately and permanently, removing the capture's
|
|
23
|
+
* instrumentation without disrupting the application or other integrations
|
|
24
|
+
* sharing the same API. Safe to call more than once.
|
|
25
|
+
*/
|
|
26
|
+
disconnect(): void;
|
|
27
|
+
};
|
|
28
|
+
type WebAuthnCeremonyKind = "get" | "create";
|
|
29
|
+
/**
|
|
30
|
+
* One lifecycle event of an observed WebAuthn ceremony
|
|
31
|
+
* (`navigator.credentials.get`/`create` invoked with `publicKey` options).
|
|
32
|
+
* `started` is delivered when the application invokes the API,
|
|
33
|
+
* `completed`/`failed` when the returned promise settles.
|
|
34
|
+
*/
|
|
35
|
+
type WebAuthnCeremonyEvent = {
|
|
36
|
+
kind: WebAuthnCeremonyKind;
|
|
37
|
+
/** Correlates the `started` event with the `completed`/`failed` event of the same ceremony. */
|
|
38
|
+
ceremonyId: string;
|
|
39
|
+
phase: "started" | "completed" | "failed";
|
|
40
|
+
/** Mediation requirement exactly as passed by the application (undefined = platform default). */
|
|
41
|
+
mediation?: string;
|
|
42
|
+
/** Whether transient user activation was active when the ceremony began (when the platform exposes it). */
|
|
43
|
+
userActivation?: boolean;
|
|
44
|
+
/** started, get only: request options as WebAuthn JSON (binary fields base64url-encoded). */
|
|
45
|
+
requestOptionsJson?: string;
|
|
46
|
+
/** started, get only: the request options carried a non-empty allowCredentials list. */
|
|
47
|
+
hasAllowCredentials?: boolean;
|
|
48
|
+
/** completed: the resulting credential as WebAuthn JSON (assertion or attestation shape, by `kind`). */
|
|
49
|
+
credentialJson?: string;
|
|
50
|
+
/** failed: the raw rejection value. */
|
|
51
|
+
error?: unknown;
|
|
52
|
+
/** failed: DOMException name of the rejection, e.g. NotAllowedError / AbortError. */
|
|
53
|
+
errorName?: string;
|
|
54
|
+
/** failed: the ceremony's own AbortSignal was aborted before the promise settled. */
|
|
55
|
+
aborted?: boolean;
|
|
56
|
+
};
|
|
57
|
+
type WebAuthnCaptureOptions = {
|
|
58
|
+
onEvent: (event: WebAuthnCeremonyEvent) => void;
|
|
59
|
+
logger?: Logger;
|
|
60
|
+
};
|
|
61
|
+
type NetworkTransport = "xhr" | "fetch";
|
|
62
|
+
/** One observed request/response round trip on a matched URL. */
|
|
63
|
+
type HttpExchange = {
|
|
64
|
+
transport: NetworkTransport;
|
|
65
|
+
method: string;
|
|
66
|
+
/** Resolved absolute request URL. */
|
|
67
|
+
url: string;
|
|
68
|
+
/** Pathname of the resolved request URL. */
|
|
69
|
+
path: string;
|
|
70
|
+
/** HTTP status; 0 when the request did not produce a response (abort, timeout, network error). */
|
|
71
|
+
status: number;
|
|
72
|
+
ok: boolean;
|
|
73
|
+
/** How the request settled: a normal response, or an abort / timeout / network error. */
|
|
74
|
+
outcome: "load" | "abort" | "timeout" | "error";
|
|
75
|
+
durationMs?: number;
|
|
76
|
+
/** Final response URL when the platform exposes it (differs from `url` after redirects). */
|
|
77
|
+
responseUrl?: string;
|
|
78
|
+
requestBody?: unknown;
|
|
79
|
+
responseBody?: unknown;
|
|
80
|
+
};
|
|
81
|
+
type NetworkCaptureOptions = {
|
|
82
|
+
/**
|
|
83
|
+
* Selects which requests to report. Called with the request URL resolved
|
|
84
|
+
* against the document base URL and the uppercased method. Only matched
|
|
85
|
+
* requests are observed; everything else passes through untouched.
|
|
86
|
+
*/
|
|
87
|
+
match: (url: URL, method: string) => boolean;
|
|
88
|
+
onExchange: (exchange: HttpExchange) => void;
|
|
89
|
+
/** Which transports to observe. Defaults to both. */
|
|
90
|
+
transports?: NetworkTransport[];
|
|
91
|
+
/**
|
|
92
|
+
* Opt in to capturing request and/or response bodies. Both default to
|
|
93
|
+
* `false`: an exchange carries only metadata (method, URL, status, outcome,
|
|
94
|
+
* duration, responseUrl) unless a direction is explicitly enabled here.
|
|
95
|
+
* Matched URLs are auth endpoints whose bodies carry credentials, so body
|
|
96
|
+
* capture is never on by default. When enabled, captured bodies still pass
|
|
97
|
+
* through the built-in credential redactor (always) and any `redact` hook.
|
|
98
|
+
*/
|
|
99
|
+
captureBodies?: {
|
|
100
|
+
request?: boolean;
|
|
101
|
+
response?: boolean;
|
|
102
|
+
};
|
|
103
|
+
/**
|
|
104
|
+
* Converts a raw body into the `requestBody`/`responseBody` value. The
|
|
105
|
+
* default parses JSON strings and reports everything else as undefined.
|
|
106
|
+
* Return undefined to omit the body from the exchange. Only consulted for a
|
|
107
|
+
* direction enabled via `captureBodies`.
|
|
108
|
+
*/
|
|
109
|
+
parseBody?: (raw: unknown, direction: "request" | "response") => unknown;
|
|
110
|
+
/**
|
|
111
|
+
* Additional redaction applied to a parsed body before it is reported. Runs
|
|
112
|
+
* AFTER the built-in credential-key redactor, so it can remove more but can
|
|
113
|
+
* never re-expose what the built-in removed. Return the (possibly mutated)
|
|
114
|
+
* value, or undefined to drop the body entirely.
|
|
115
|
+
*/
|
|
116
|
+
redact?: (body: unknown, direction: "request" | "response") => unknown;
|
|
117
|
+
/**
|
|
118
|
+
* Upper bound on a captured body's size, in characters. Bodies larger than
|
|
119
|
+
* this (by declared content-length or actual length) are omitted rather than
|
|
120
|
+
* buffered and reported. Applies to both transports. Defaults to 65536.
|
|
121
|
+
*/
|
|
122
|
+
maxBodyChars?: number;
|
|
123
|
+
logger?: Logger;
|
|
124
|
+
};
|
|
125
|
+
type NavigationSource = "push" | "replace" | "pop" | "hash";
|
|
126
|
+
/** One in-app (same-document) navigation. */
|
|
127
|
+
type NavigationEvent = {
|
|
128
|
+
path: string;
|
|
129
|
+
search: string;
|
|
130
|
+
hash: string;
|
|
131
|
+
source: NavigationSource;
|
|
132
|
+
};
|
|
133
|
+
type NavigationCaptureOptions = {
|
|
134
|
+
onNavigate: (event: NavigationEvent) => void;
|
|
135
|
+
logger?: Logger;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Observe WebAuthn ceremonies. Each `navigator.credentials.get`/`create` call
|
|
140
|
+
* made with `publicKey` options produces a `started` event when it is invoked
|
|
141
|
+
* and a `completed` or `failed` event when it settles, correlated by
|
|
142
|
+
* `ceremonyId` — including silent cancellations that surface no UI. Calls
|
|
143
|
+
* without `publicKey` (WebOTP, federated or password credentials) are never
|
|
144
|
+
* reported and pass through untouched.
|
|
145
|
+
*
|
|
146
|
+
* Coexists with password managers and other integrations that instrument the
|
|
147
|
+
* same API: the application and any such integration keep working unchanged,
|
|
148
|
+
* and `mode` reports how the observation attached. In the rare case that
|
|
149
|
+
* another integration replaces the credentials container entirely, ceremonies
|
|
150
|
+
* routed solely through it are not observed.
|
|
151
|
+
*
|
|
152
|
+
* Returns a {@link CaptureHandle}; call `disconnect()` to stop.
|
|
153
|
+
*/
|
|
154
|
+
declare function captureWebAuthn(options: WebAuthnCaptureOptions): CaptureHandle;
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Observe request/response round trips over `XMLHttpRequest` and/or `fetch`.
|
|
158
|
+
* Each request accepted by the `match` predicate produces one
|
|
159
|
+
* {@link HttpExchange}; every other request is left untouched. Observation
|
|
160
|
+
* never affects the application's own request — its arguments, result, and
|
|
161
|
+
* errors are unchanged even if a capture callback fails.
|
|
162
|
+
*
|
|
163
|
+
* Exchanges carry metadata only unless body capture is opted in via
|
|
164
|
+
* `captureBodies`; see {@link NetworkCaptureOptions} for the body, redaction,
|
|
165
|
+
* and size controls.
|
|
166
|
+
*
|
|
167
|
+
* Returns a {@link CaptureHandle}; call `disconnect()` to stop.
|
|
168
|
+
*/
|
|
169
|
+
declare function captureNetwork(options: NetworkCaptureOptions): CaptureHandle;
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Observe same-document (SPA) navigations: `pushState`, `replaceState`, and
|
|
173
|
+
* back/forward changes. Each produces one {@link NavigationEvent} carrying the
|
|
174
|
+
* path, search, hash, and how it occurred. The application's router runs first
|
|
175
|
+
* and is unaffected by observation; repeated navigations to the same location
|
|
176
|
+
* are reported once.
|
|
177
|
+
*
|
|
178
|
+
* Returns a {@link CaptureHandle}; call `disconnect()` to stop.
|
|
179
|
+
*/
|
|
180
|
+
declare function captureNavigation(options: NavigationCaptureOptions): CaptureHandle;
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Console-backed logger for development and debugging. The captures run inside
|
|
184
|
+
* application call paths (credentials.get, pushState, fetch), so a
|
|
185
|
+
* page-overridden or unavailable console must never break them — every call is
|
|
186
|
+
* guarded.
|
|
187
|
+
*/
|
|
188
|
+
declare function createConsoleLogger(prefix?: string): Logger;
|
|
189
|
+
|
|
190
|
+
/** Package version, injected at build time; "0.0.0-dev" in unbuilt contexts (tests, watch mode). */
|
|
191
|
+
declare const VERSION: string;
|
|
192
|
+
|
|
193
|
+
export { type CaptureHandle, type HttpExchange, type Logger, type NavigationCaptureOptions, type NavigationEvent, type NavigationSource, type NetworkCaptureOptions, type NetworkTransport, VERSION, type WebAuthnCaptureOptions, type WebAuthnCeremonyEvent, type WebAuthnCeremonyKind, captureNavigation, captureNetwork, captureWebAuthn, createConsoleLogger };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/** Minimal logging seam. All capture modules are silent unless a logger is provided. */
|
|
2
|
+
type Logger = {
|
|
3
|
+
log: (...args: unknown[]) => void;
|
|
4
|
+
};
|
|
5
|
+
/**
|
|
6
|
+
* Handle returned by every capture function, following the platform observer
|
|
7
|
+
* pattern (MutationObserver, ResizeObserver): create it with a callback,
|
|
8
|
+
* receive events, and `disconnect()` when done.
|
|
9
|
+
*/
|
|
10
|
+
type CaptureHandle = {
|
|
11
|
+
/**
|
|
12
|
+
* Whether and how the capture attached:
|
|
13
|
+
* - `"standard"`: attached and delivering events.
|
|
14
|
+
* - `"compatibility"`: attached via a fallback path (another integration held
|
|
15
|
+
* the primary one) and delivering events.
|
|
16
|
+
* - `"unavailable"`: could not attach (the environment disallows it, or an
|
|
17
|
+
* equivalent capture is already active); no events are delivered and
|
|
18
|
+
* `disconnect()` is a no-op.
|
|
19
|
+
*/
|
|
20
|
+
readonly mode: "standard" | "compatibility" | "unavailable";
|
|
21
|
+
/**
|
|
22
|
+
* Stop event delivery immediately and permanently, removing the capture's
|
|
23
|
+
* instrumentation without disrupting the application or other integrations
|
|
24
|
+
* sharing the same API. Safe to call more than once.
|
|
25
|
+
*/
|
|
26
|
+
disconnect(): void;
|
|
27
|
+
};
|
|
28
|
+
type WebAuthnCeremonyKind = "get" | "create";
|
|
29
|
+
/**
|
|
30
|
+
* One lifecycle event of an observed WebAuthn ceremony
|
|
31
|
+
* (`navigator.credentials.get`/`create` invoked with `publicKey` options).
|
|
32
|
+
* `started` is delivered when the application invokes the API,
|
|
33
|
+
* `completed`/`failed` when the returned promise settles.
|
|
34
|
+
*/
|
|
35
|
+
type WebAuthnCeremonyEvent = {
|
|
36
|
+
kind: WebAuthnCeremonyKind;
|
|
37
|
+
/** Correlates the `started` event with the `completed`/`failed` event of the same ceremony. */
|
|
38
|
+
ceremonyId: string;
|
|
39
|
+
phase: "started" | "completed" | "failed";
|
|
40
|
+
/** Mediation requirement exactly as passed by the application (undefined = platform default). */
|
|
41
|
+
mediation?: string;
|
|
42
|
+
/** Whether transient user activation was active when the ceremony began (when the platform exposes it). */
|
|
43
|
+
userActivation?: boolean;
|
|
44
|
+
/** started, get only: request options as WebAuthn JSON (binary fields base64url-encoded). */
|
|
45
|
+
requestOptionsJson?: string;
|
|
46
|
+
/** started, get only: the request options carried a non-empty allowCredentials list. */
|
|
47
|
+
hasAllowCredentials?: boolean;
|
|
48
|
+
/** completed: the resulting credential as WebAuthn JSON (assertion or attestation shape, by `kind`). */
|
|
49
|
+
credentialJson?: string;
|
|
50
|
+
/** failed: the raw rejection value. */
|
|
51
|
+
error?: unknown;
|
|
52
|
+
/** failed: DOMException name of the rejection, e.g. NotAllowedError / AbortError. */
|
|
53
|
+
errorName?: string;
|
|
54
|
+
/** failed: the ceremony's own AbortSignal was aborted before the promise settled. */
|
|
55
|
+
aborted?: boolean;
|
|
56
|
+
};
|
|
57
|
+
type WebAuthnCaptureOptions = {
|
|
58
|
+
onEvent: (event: WebAuthnCeremonyEvent) => void;
|
|
59
|
+
logger?: Logger;
|
|
60
|
+
};
|
|
61
|
+
type NetworkTransport = "xhr" | "fetch";
|
|
62
|
+
/** One observed request/response round trip on a matched URL. */
|
|
63
|
+
type HttpExchange = {
|
|
64
|
+
transport: NetworkTransport;
|
|
65
|
+
method: string;
|
|
66
|
+
/** Resolved absolute request URL. */
|
|
67
|
+
url: string;
|
|
68
|
+
/** Pathname of the resolved request URL. */
|
|
69
|
+
path: string;
|
|
70
|
+
/** HTTP status; 0 when the request did not produce a response (abort, timeout, network error). */
|
|
71
|
+
status: number;
|
|
72
|
+
ok: boolean;
|
|
73
|
+
/** How the request settled: a normal response, or an abort / timeout / network error. */
|
|
74
|
+
outcome: "load" | "abort" | "timeout" | "error";
|
|
75
|
+
durationMs?: number;
|
|
76
|
+
/** Final response URL when the platform exposes it (differs from `url` after redirects). */
|
|
77
|
+
responseUrl?: string;
|
|
78
|
+
requestBody?: unknown;
|
|
79
|
+
responseBody?: unknown;
|
|
80
|
+
};
|
|
81
|
+
type NetworkCaptureOptions = {
|
|
82
|
+
/**
|
|
83
|
+
* Selects which requests to report. Called with the request URL resolved
|
|
84
|
+
* against the document base URL and the uppercased method. Only matched
|
|
85
|
+
* requests are observed; everything else passes through untouched.
|
|
86
|
+
*/
|
|
87
|
+
match: (url: URL, method: string) => boolean;
|
|
88
|
+
onExchange: (exchange: HttpExchange) => void;
|
|
89
|
+
/** Which transports to observe. Defaults to both. */
|
|
90
|
+
transports?: NetworkTransport[];
|
|
91
|
+
/**
|
|
92
|
+
* Opt in to capturing request and/or response bodies. Both default to
|
|
93
|
+
* `false`: an exchange carries only metadata (method, URL, status, outcome,
|
|
94
|
+
* duration, responseUrl) unless a direction is explicitly enabled here.
|
|
95
|
+
* Matched URLs are auth endpoints whose bodies carry credentials, so body
|
|
96
|
+
* capture is never on by default. When enabled, captured bodies still pass
|
|
97
|
+
* through the built-in credential redactor (always) and any `redact` hook.
|
|
98
|
+
*/
|
|
99
|
+
captureBodies?: {
|
|
100
|
+
request?: boolean;
|
|
101
|
+
response?: boolean;
|
|
102
|
+
};
|
|
103
|
+
/**
|
|
104
|
+
* Converts a raw body into the `requestBody`/`responseBody` value. The
|
|
105
|
+
* default parses JSON strings and reports everything else as undefined.
|
|
106
|
+
* Return undefined to omit the body from the exchange. Only consulted for a
|
|
107
|
+
* direction enabled via `captureBodies`.
|
|
108
|
+
*/
|
|
109
|
+
parseBody?: (raw: unknown, direction: "request" | "response") => unknown;
|
|
110
|
+
/**
|
|
111
|
+
* Additional redaction applied to a parsed body before it is reported. Runs
|
|
112
|
+
* AFTER the built-in credential-key redactor, so it can remove more but can
|
|
113
|
+
* never re-expose what the built-in removed. Return the (possibly mutated)
|
|
114
|
+
* value, or undefined to drop the body entirely.
|
|
115
|
+
*/
|
|
116
|
+
redact?: (body: unknown, direction: "request" | "response") => unknown;
|
|
117
|
+
/**
|
|
118
|
+
* Upper bound on a captured body's size, in characters. Bodies larger than
|
|
119
|
+
* this (by declared content-length or actual length) are omitted rather than
|
|
120
|
+
* buffered and reported. Applies to both transports. Defaults to 65536.
|
|
121
|
+
*/
|
|
122
|
+
maxBodyChars?: number;
|
|
123
|
+
logger?: Logger;
|
|
124
|
+
};
|
|
125
|
+
type NavigationSource = "push" | "replace" | "pop" | "hash";
|
|
126
|
+
/** One in-app (same-document) navigation. */
|
|
127
|
+
type NavigationEvent = {
|
|
128
|
+
path: string;
|
|
129
|
+
search: string;
|
|
130
|
+
hash: string;
|
|
131
|
+
source: NavigationSource;
|
|
132
|
+
};
|
|
133
|
+
type NavigationCaptureOptions = {
|
|
134
|
+
onNavigate: (event: NavigationEvent) => void;
|
|
135
|
+
logger?: Logger;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Observe WebAuthn ceremonies. Each `navigator.credentials.get`/`create` call
|
|
140
|
+
* made with `publicKey` options produces a `started` event when it is invoked
|
|
141
|
+
* and a `completed` or `failed` event when it settles, correlated by
|
|
142
|
+
* `ceremonyId` — including silent cancellations that surface no UI. Calls
|
|
143
|
+
* without `publicKey` (WebOTP, federated or password credentials) are never
|
|
144
|
+
* reported and pass through untouched.
|
|
145
|
+
*
|
|
146
|
+
* Coexists with password managers and other integrations that instrument the
|
|
147
|
+
* same API: the application and any such integration keep working unchanged,
|
|
148
|
+
* and `mode` reports how the observation attached. In the rare case that
|
|
149
|
+
* another integration replaces the credentials container entirely, ceremonies
|
|
150
|
+
* routed solely through it are not observed.
|
|
151
|
+
*
|
|
152
|
+
* Returns a {@link CaptureHandle}; call `disconnect()` to stop.
|
|
153
|
+
*/
|
|
154
|
+
declare function captureWebAuthn(options: WebAuthnCaptureOptions): CaptureHandle;
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Observe request/response round trips over `XMLHttpRequest` and/or `fetch`.
|
|
158
|
+
* Each request accepted by the `match` predicate produces one
|
|
159
|
+
* {@link HttpExchange}; every other request is left untouched. Observation
|
|
160
|
+
* never affects the application's own request — its arguments, result, and
|
|
161
|
+
* errors are unchanged even if a capture callback fails.
|
|
162
|
+
*
|
|
163
|
+
* Exchanges carry metadata only unless body capture is opted in via
|
|
164
|
+
* `captureBodies`; see {@link NetworkCaptureOptions} for the body, redaction,
|
|
165
|
+
* and size controls.
|
|
166
|
+
*
|
|
167
|
+
* Returns a {@link CaptureHandle}; call `disconnect()` to stop.
|
|
168
|
+
*/
|
|
169
|
+
declare function captureNetwork(options: NetworkCaptureOptions): CaptureHandle;
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Observe same-document (SPA) navigations: `pushState`, `replaceState`, and
|
|
173
|
+
* back/forward changes. Each produces one {@link NavigationEvent} carrying the
|
|
174
|
+
* path, search, hash, and how it occurred. The application's router runs first
|
|
175
|
+
* and is unaffected by observation; repeated navigations to the same location
|
|
176
|
+
* are reported once.
|
|
177
|
+
*
|
|
178
|
+
* Returns a {@link CaptureHandle}; call `disconnect()` to stop.
|
|
179
|
+
*/
|
|
180
|
+
declare function captureNavigation(options: NavigationCaptureOptions): CaptureHandle;
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Console-backed logger for development and debugging. The captures run inside
|
|
184
|
+
* application call paths (credentials.get, pushState, fetch), so a
|
|
185
|
+
* page-overridden or unavailable console must never break them — every call is
|
|
186
|
+
* guarded.
|
|
187
|
+
*/
|
|
188
|
+
declare function createConsoleLogger(prefix?: string): Logger;
|
|
189
|
+
|
|
190
|
+
/** Package version, injected at build time; "0.0.0-dev" in unbuilt contexts (tests, watch mode). */
|
|
191
|
+
declare const VERSION: string;
|
|
192
|
+
|
|
193
|
+
export { type CaptureHandle, type HttpExchange, type Logger, type NavigationCaptureOptions, type NavigationEvent, type NavigationSource, type NetworkCaptureOptions, type NetworkTransport, VERSION, type WebAuthnCaptureOptions, type WebAuthnCeremonyEvent, type WebAuthnCeremonyKind, captureNavigation, captureNetwork, captureWebAuthn, createConsoleLogger };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";var J=Object.defineProperty;var ae=Object.getOwnPropertyDescriptor;var se=Object.getOwnPropertyNames;var ie=Object.prototype.hasOwnProperty;var ue=(e,t)=>{for(var n in t)J(e,n,{get:t[n],enumerable:!0})},ce=(e,t,n,a)=>{if(t&&typeof t=="object"||typeof t=="function")for(let c of se(t))!ie.call(e,c)&&c!==n&&J(e,c,{get:()=>t[c],enumerable:!(a=ae(t,c))||a.enumerable});return e};var de=e=>ce(J({},"__esModule",{value:!0}),e);var Ce={};ue(Ce,{VERSION:()=>re,captureNavigation:()=>te,captureNetwork:()=>ee,captureWebAuthn:()=>z,createConsoleLogger:()=>ne});module.exports=de(Ce);var I={log:()=>{}},$=Symbol.for("corbado.autocapture.installed");function N(e){try{return typeof e=="function"&&Object.prototype.hasOwnProperty.call(e,$)}catch{return!1}}function B(e,t){var a,c;let n=t;try{Object.defineProperty(e,"name",{value:(a=n.name)!=null?a:"",configurable:!0}),Object.defineProperty(e,"length",{value:(c=n.length)!=null?c:0,configurable:!0}),Object.defineProperty(e,$,{value:t,configurable:!0})}catch{}return e}function T(){return{mode:"unavailable",disconnect:()=>{}}}function X(e,t,n){return a=>{try{n(a)}catch(c){e.log(`${t} listener failed`,c)}}}function z(e){var m;let t=(m=e.logger)!=null?m:I,n=navigator.credentials;if(!n)return t.log("webauthn capture unavailable: navigator.credentials missing"),T();if(N(n.get)||N(n.create))return t.log("webauthn capture unavailable: an equivalent capture is already active"),T();let a=!0,c=0,s=new WeakSet,l=X(t,"webauthn",h=>{a&&e.onEvent(h)}),C=(h,P)=>B(async f=>{var i,R;if(!a||!(f!=null&&f.publicKey))return P(f);let w=f.publicKey;if(s.has(f)||s.has(w))return P(f);s.add(f),s.add(w);let r=`wa-${++c}`,o=typeof f.mediation=="string"?f.mediation:void 0,u=f.signal,g=!1,y=()=>{g=!0};try{(i=u==null?void 0:u.addEventListener)==null||i.call(u,"abort",y,{once:!0})}catch{}let d={kind:h,ceremonyId:r,phase:"started",mediation:o,userActivation:le()};if(h==="get"){let p=f.publicKey;d.requestOptionsJson=pe(p,t),d.hasAllowCredentials=Array.isArray(p.allowCredentials)&&p.allowCredentials.length>0}l(d);try{let p=await P(f);return l({kind:h,ceremonyId:r,phase:"completed",mediation:o,credentialJson:fe(p,t)}),p}catch(p){let b=g||(u==null?void 0:u.aborted)===!0;throw l({kind:h,ceremonyId:r,phase:"failed",mediation:o,error:p,errorName:p instanceof DOMException?p.name:"UnknownError",...b?{aborted:!0}:{}}),p}finally{try{(R=u==null?void 0:u.removeEventListener)==null||R.call(u,"abort",y)}catch{}s.delete(f),s.delete(w)}},{name:h,length:0}),k=n.get.bind(n),A=n.create.bind(n),E=(h,P)=>{let H=C(h,P),f=H,w=()=>f;try{let r=Object.getOwnPropertyDescriptor(n,h);return Object.defineProperty(n,h,{configurable:!0,enumerable:!0,get:w,set:o=>{if(typeof o=="function"){if(N(o)){f=o;return}t.log(`webauthn ${h}: adopted a third-party integration`),f=C(h,o.bind(n))}}}),()=>{try{let o=Object.getOwnPropertyDescriptor(n,h);if((o==null?void 0:o.get)!==w||f!==H)return;r?Object.defineProperty(n,h,r):delete n[h]}catch(o){t.log(`webauthn ${h} detach skipped`,o)}}}catch{return null}},x=E("get",k),L=E("create",A),_,S="standard";if(!x||!L)try{let h=C("get",w=>n.get.call(n,w)),P=C("create",w=>n.create.call(n,w)),H=new Map,f=new Proxy(Object.create(Object.getPrototypeOf(n)),{get(w,r){if(r==="get")return h;if(r==="create")return P;let o=Reflect.get(n,r,n);if(typeof o!="function")return o;let u=H.get(r);if(u&&u.source===o)return u.bound;let g=o.bind(n);return H.set(r,{source:o,bound:g}),g},set(w,r,o){try{Reflect.set(n,r,o)}catch{}return!0},has(w,r){return Reflect.has(n,r)},getOwnPropertyDescriptor(w,r){let o=Reflect.getOwnPropertyDescriptor(n,r);return o?{...o,configurable:!0}:void 0},ownKeys(){return Reflect.ownKeys(n)}});if(Object.defineProperty(navigator,"credentials",{configurable:!0,enumerable:!0,get:()=>f}),navigator.credentials!==f)throw new Error("compatibility layer did not take effect");t.log("webauthn capture attached via compatibility layer (container methods are locked)"),S="compatibility",_=()=>{try{navigator.credentials===f&&delete navigator.credentials}catch(w){t.log("webauthn compatibility layer removal skipped",w)}}}catch(h){if(t.log("webauthn capture unavailable: credentials API locked by another integration",h),!x&&!L)return T();t.log(`webauthn capture: credentials.${x?"create":"get"} is locked and will not be reported`)}return{mode:S,disconnect:()=>{a=!1,x==null||x(),L==null||L(),_==null||_()}}}function le(){try{let e=navigator.userActivation;return typeof(e==null?void 0:e.isActive)=="boolean"?e.isActive:void 0}catch{return}}function fe(e,t){var n,a,c;if(e)try{let s=e;if(typeof s.toJSON=="function")return JSON.stringify(s.toJSON());let l=s.response;return JSON.stringify({id:s.id,rawId:M(s.rawId),type:s.type,authenticatorAttachment:(n=s.authenticatorAttachment)!=null?n:void 0,clientExtensionResults:(c=(a=s.getClientExtensionResults)==null?void 0:a.call(s))!=null?c:{},response:{clientDataJSON:M(l.clientDataJSON),...l.attestationObject?{attestationObject:M(l.attestationObject)}:{},...l.authenticatorData?{authenticatorData:M(l.authenticatorData)}:{},...l.signature?{signature:M(l.signature)}:{},...l.userHandle?{userHandle:M(l.userHandle)}:{}}})}catch(s){t.log("credential serialization failed",s);return}}function pe(e,t){var n;try{return JSON.stringify({...e,challenge:M(e.challenge),allowCredentials:(n=e.allowCredentials)==null?void 0:n.map(a=>({...a,id:M(a.id)}))})}catch(a){t.log("request options serialization failed",a);return}}function M(e){if(!e)return;let t=e instanceof ArrayBuffer?new Uint8Array(e):new Uint8Array(e.buffer,e.byteOffset,e.byteLength),n="";for(let a of t)n+=String.fromCharCode(a);return window.btoa(n).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/g,"")}var K=Symbol.for("corbado.autocapture.request");function Y(e){return e[K]}function he(e,t){try{Object.defineProperty(e,K,{value:t,writable:!0,configurable:!0,enumerable:!1})}catch{e[K]=t}}function ge(e,t,n,a){try{let c=e.responseType;if(c===""||c==="text"){let s=e.responseText;return typeof s=="string"&&s.length>t?void 0:n(s,"response")}if(c==="json"){let s=e.response;if(s==null)return;try{if(JSON.stringify(s).length>t)return}catch{return}return a(s,"response")}return}catch{return}}function ye(e,t,n){if(typeof e=="string"&&!(e.length>t))return n(e,"request")}function be(e){try{return e.responseURL||void 0}catch{return}}function me(e){if(typeof e=="string")try{return JSON.parse(e)}catch{return}}var we=/pass|secret|token|otp|csrf|auth|credential|assertion|session|pin|code|challenge/i,Q="[redacted]";function W(e,t=0){if(t>8||e===null||typeof e!="object")return e;try{if(Array.isArray(e))return e.map(a=>W(a,t+1));let n={};for(let[a,c]of Object.entries(e))n[a]=we.test(a)?Q:W(c,t+1);return n}catch{return Q}}var ve=500,Re=65536;function Oe(e){return/event-stream|octet-stream/i.test(e)?!1:/json|text|urlencoded/i.test(e)}async function Z(e,t,n){var a,c,s;try{let l=Number((s=(c=(a=e.headers)==null?void 0:a.get)==null?void 0:c.call(a,"content-length"))!=null?s:"");if(Number.isFinite(l)&&l>t){n.log("body omitted: content-length exceeds limit",l);return}let C,k=new Promise(E=>{C=setTimeout(()=>E(void 0),ve)}),A=await Promise.race([e.text(),k]);if(C!==void 0&&clearTimeout(C),A===void 0){n.log("body omitted: read timed out");return}if(A.length>t){n.log("body omitted: exceeds size limit",A.length);return}return A}catch{return}}function ee(e){var h,P,H,f,w;let t=(h=e.logger)!=null?h:I,n=(P=e.transports)!=null?P:["xhr","fetch"],a=(H=e.maxBodyChars)!=null?H:Re,c=((f=e.captureBodies)==null?void 0:f.request)===!0,s=((w=e.captureBodies)==null?void 0:w.response)===!0,l=!0,C=X(t,"network",r=>{l&&e.onExchange(r)}),k=r=>r==="request"?c:s,A=(r,o)=>{if(r===void 0)return;let u=W(r);if(!e.redact)return u;try{return e.redact(u,o)}catch(g){t.log("redact hook failed",g);return}},E=(r,o)=>{var g;if(!k(o))return;let u;try{u=((g=e.parseBody)!=null?g:me)(r,o)}catch(y){t.log("body parsing failed",y);return}return A(u,o)},x=(r,o)=>{if(k(o))return A(r,o)},L=(r,o)=>{try{let u=typeof document!="undefined"?document.baseURI:void 0,g=new URL(String(r),u);return e.match(g,o)?{url:g.href,path:g.pathname}:void 0}catch{return}},_=()=>{if(typeof XMLHttpRequest=="undefined")return t.log("xhr capture unavailable: XMLHttpRequest missing"),null;let r=XMLHttpRequest.prototype,o=r.open,u=r.send;if(N(o)||N(u))return t.log("xhr capture unavailable: an equivalent capture is already active"),null;let g=function(d,i,...R){try{let p=Y(this);p&&(p.reported=!0);let b=String(d).toUpperCase(),q=l?L(i,b):void 0;he(this,q?{method:b,url:q.url,path:q.path,reported:!1}:void 0)}catch(p){t.log("xhr open observation failed",p)}return o.call(this,d,i,...R)},y=function(d){try{let i=Y(this);if(i&&!i.reported){i.startedAt=Date.now();let R=b=>()=>{i.outcome||(i.outcome=b)};this.addEventListener("abort",R("abort"),{once:!0}),this.addEventListener("timeout",R("timeout"),{once:!0}),this.addEventListener("error",R("error"),{once:!0});let p=()=>{try{if(this.readyState!==4||i.settledStatus!==void 0)return;let b=this.status;b!==0&&(i.settledStatus=b)}catch{}};this.addEventListener("readystatechange",p),this.addEventListener("loadend",()=>{var b,q;try{try{this.removeEventListener("readystatechange",p)}catch{}if(i.reported)return;i.reported=!0;let v=(b=i.settledStatus)!=null?b:0;if(i.settledStatus===void 0)try{v=this.status}catch{}let F=i.settledStatus!==void 0?"load":(q=i.outcome)!=null?q:"load";C({transport:"xhr",method:i.method,url:i.url,path:i.path,status:v,ok:v>=200&&v<300,outcome:F,durationMs:i.startedAt!==void 0?Date.now()-i.startedAt:void 0,responseUrl:be(this),requestBody:ye(d,a,E),responseBody:ge(this,a,E,x)})}catch(v){t.log("xhr exchange observation failed",v)}},{once:!0})}}catch(i){t.log("xhr send observation failed",i)}return u.call(this,d)};B(g,o),B(y,u);try{r.open=g,r.send=y}catch(d){return r.open===g&&(r.open=o),t.log("xhr capture unavailable: XMLHttpRequest methods are not writable",d),null}return()=>{r.open===g&&(r.open=o),r.send===y&&(r.send=u)}},S=()=>{let r=globalThis,o=r.fetch;if(typeof o!="function")return t.log("fetch capture unavailable: fetch missing"),null;if(N(o))return t.log("fetch capture unavailable: an equivalent capture is already active"),null;let u=async(y,d,i,R,p)=>{try{if(!l)return;let b=typeof R=="string"&&R.length>a?void 0:R!==void 0?E(R,"request"):void 0;if(b===void 0&&p){let v=await Z(p,a,t);v!==void 0&&(b=E(v,"request"))}let q;if(i){let v=await Z(i,a,t);v!==void 0&&(q=E(v,"response"))}C({transport:"fetch",method:y.method,url:y.url,path:y.path,status:d.status,ok:d.ok,outcome:"load",durationMs:Date.now()-y.startedAt,responseUrl:d.url||void 0,requestBody:b,responseBody:q})}catch(b){t.log("fetch exchange observation failed",b)}},g=function(y,d){var F,G;let i,R,p,b;try{if(l){let O=typeof Request!="undefined"&&y instanceof Request,D=String((F=d==null?void 0:d.method)!=null?F:O?y.method:"GET").toUpperCase(),U=O?y.url:y,j=L(U,D);if(j&&(i={method:D,url:j.url,path:j.path,startedAt:Date.now()},b=(G=d==null?void 0:d.signal)!=null?G:O?y.signal:void 0,c)){if(typeof(d==null?void 0:d.body)=="string")R=d.body;else if(O&&!(d!=null&&d.body))try{p=y.clone()}catch{}}}}catch(O){t.log("fetch observation failed",O)}let q=o.call(this!=null?this:globalThis,y,d);if(!i)return q;let v=i;return q.then(O=>{var U,j,V;let D;try{if(l&&s){let oe=(V=(j=(U=O.headers)==null?void 0:U.get)==null?void 0:j.call(U,"content-type"))!=null?V:"";Oe(oe)&&(D=O.clone())}}catch{}u(v,O,D,R,p)},O=>{try{let D=(b==null?void 0:b.aborted)===!0||(O==null?void 0:O.name)==="AbortError"||typeof DOMException!="undefined"&&O instanceof DOMException&&O.code===DOMException.ABORT_ERR;C({transport:"fetch",method:v.method,url:v.url,path:v.path,status:0,ok:!1,outcome:D?"abort":"error",durationMs:Date.now()-v.startedAt})}catch(D){t.log("fetch failure observation failed",D)}}),q};B(g,o);try{r.fetch=g}catch(y){return t.log("fetch capture unavailable: fetch is not writable",y),null}return()=>{r.fetch===g&&(r.fetch=o)}},m=[];if(n.includes("xhr")){let r=_();r&&m.push(r)}if(n.includes("fetch")){let r=S();r&&m.push(r)}return m.length===0?T():{mode:"standard",disconnect:()=>{l=!1;for(let r of m.splice(0))r()}}}function te(e){var _;let t=(_=e.logger)!=null?_:I;if(typeof window=="undefined"||typeof history=="undefined")return t.log("navigation capture unavailable: window/history missing"),T();let n=history.pushState,a=history.replaceState;if(N(n)||N(a))return t.log("navigation capture unavailable: an equivalent capture is already active"),T();let c=!0,s=()=>({path:window.location.pathname,search:window.location.search,hash:window.location.hash}),l=s(),C=X(t,"navigation",S=>{c&&e.onNavigate(S)}),k=S=>{try{let m=s();if(m.path===l.path&&m.search===l.search&&m.hash===l.hash)return;l=m,C({...m,source:S})}catch(m){t.log("navigation observation failed",m)}},A=function(...S){let m=n.apply(this,S);return k("push"),m},E=function(...S){let m=a.apply(this,S);return k("replace"),m};B(A,n),B(E,a);let x=()=>k("pop"),L=()=>k("hash");try{history.pushState=A,history.replaceState=E}catch(S){return history.pushState===A&&(history.pushState=n),t.log("navigation capture unavailable: history methods are not writable",S),T()}return window.addEventListener("popstate",x),window.addEventListener("hashchange",L),{mode:"standard",disconnect:()=>{c=!1,history.pushState===A&&(history.pushState=n),history.replaceState===E&&(history.replaceState=a),window.removeEventListener("popstate",x),window.removeEventListener("hashchange",L)}}}function ne(e="[autocapture]"){return{log:(...t)=>{try{console.log(e,...t)}catch{}}}}var re="0.1.0";0&&(module.exports={VERSION,captureNavigation,captureNetwork,captureWebAuthn,createConsoleLogger});
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var I={log:()=>{}},V=Symbol.for("corbado.autocapture.installed");function N(e){try{return typeof e=="function"&&Object.prototype.hasOwnProperty.call(e,V)}catch{return!1}}function B(e,t){var a,p;let n=t;try{Object.defineProperty(e,"name",{value:(a=n.name)!=null?a:"",configurable:!0}),Object.defineProperty(e,"length",{value:(p=n.length)!=null?p:0,configurable:!0}),Object.defineProperty(e,V,{value:t,configurable:!0})}catch{}return e}function T(){return{mode:"unavailable",disconnect:()=>{}}}function X(e,t,n){return a=>{try{n(a)}catch(p){e.log(`${t} listener failed`,p)}}}function Z(e){var m;let t=(m=e.logger)!=null?m:I,n=navigator.credentials;if(!n)return t.log("webauthn capture unavailable: navigator.credentials missing"),T();if(N(n.get)||N(n.create))return t.log("webauthn capture unavailable: an equivalent capture is already active"),T();let a=!0,p=0,s=new WeakSet,d=X(t,"webauthn",h=>{a&&e.onEvent(h)}),C=(h,P)=>B(async l=>{var i,R;if(!a||!(l!=null&&l.publicKey))return P(l);let w=l.publicKey;if(s.has(l)||s.has(w))return P(l);s.add(l),s.add(w);let r=`wa-${++p}`,o=typeof l.mediation=="string"?l.mediation:void 0,u=l.signal,g=!1,y=()=>{g=!0};try{(i=u==null?void 0:u.addEventListener)==null||i.call(u,"abort",y,{once:!0})}catch{}let c={kind:h,ceremonyId:r,phase:"started",mediation:o,userActivation:ee()};if(h==="get"){let f=l.publicKey;c.requestOptionsJson=ne(f,t),c.hasAllowCredentials=Array.isArray(f.allowCredentials)&&f.allowCredentials.length>0}d(c);try{let f=await P(l);return d({kind:h,ceremonyId:r,phase:"completed",mediation:o,credentialJson:te(f,t)}),f}catch(f){let b=g||(u==null?void 0:u.aborted)===!0;throw d({kind:h,ceremonyId:r,phase:"failed",mediation:o,error:f,errorName:f instanceof DOMException?f.name:"UnknownError",...b?{aborted:!0}:{}}),f}finally{try{(R=u==null?void 0:u.removeEventListener)==null||R.call(u,"abort",y)}catch{}s.delete(l),s.delete(w)}},{name:h,length:0}),k=n.get.bind(n),A=n.create.bind(n),E=(h,P)=>{let H=C(h,P),l=H,w=()=>l;try{let r=Object.getOwnPropertyDescriptor(n,h);return Object.defineProperty(n,h,{configurable:!0,enumerable:!0,get:w,set:o=>{if(typeof o=="function"){if(N(o)){l=o;return}t.log(`webauthn ${h}: adopted a third-party integration`),l=C(h,o.bind(n))}}}),()=>{try{let o=Object.getOwnPropertyDescriptor(n,h);if((o==null?void 0:o.get)!==w||l!==H)return;r?Object.defineProperty(n,h,r):delete n[h]}catch(o){t.log(`webauthn ${h} detach skipped`,o)}}}catch{return null}},x=E("get",k),L=E("create",A),_,S="standard";if(!x||!L)try{let h=C("get",w=>n.get.call(n,w)),P=C("create",w=>n.create.call(n,w)),H=new Map,l=new Proxy(Object.create(Object.getPrototypeOf(n)),{get(w,r){if(r==="get")return h;if(r==="create")return P;let o=Reflect.get(n,r,n);if(typeof o!="function")return o;let u=H.get(r);if(u&&u.source===o)return u.bound;let g=o.bind(n);return H.set(r,{source:o,bound:g}),g},set(w,r,o){try{Reflect.set(n,r,o)}catch{}return!0},has(w,r){return Reflect.has(n,r)},getOwnPropertyDescriptor(w,r){let o=Reflect.getOwnPropertyDescriptor(n,r);return o?{...o,configurable:!0}:void 0},ownKeys(){return Reflect.ownKeys(n)}});if(Object.defineProperty(navigator,"credentials",{configurable:!0,enumerable:!0,get:()=>l}),navigator.credentials!==l)throw new Error("compatibility layer did not take effect");t.log("webauthn capture attached via compatibility layer (container methods are locked)"),S="compatibility",_=()=>{try{navigator.credentials===l&&delete navigator.credentials}catch(w){t.log("webauthn compatibility layer removal skipped",w)}}}catch(h){if(t.log("webauthn capture unavailable: credentials API locked by another integration",h),!x&&!L)return T();t.log(`webauthn capture: credentials.${x?"create":"get"} is locked and will not be reported`)}return{mode:S,disconnect:()=>{a=!1,x==null||x(),L==null||L(),_==null||_()}}}function ee(){try{let e=navigator.userActivation;return typeof(e==null?void 0:e.isActive)=="boolean"?e.isActive:void 0}catch{return}}function te(e,t){var n,a,p;if(e)try{let s=e;if(typeof s.toJSON=="function")return JSON.stringify(s.toJSON());let d=s.response;return JSON.stringify({id:s.id,rawId:M(s.rawId),type:s.type,authenticatorAttachment:(n=s.authenticatorAttachment)!=null?n:void 0,clientExtensionResults:(p=(a=s.getClientExtensionResults)==null?void 0:a.call(s))!=null?p:{},response:{clientDataJSON:M(d.clientDataJSON),...d.attestationObject?{attestationObject:M(d.attestationObject)}:{},...d.authenticatorData?{authenticatorData:M(d.authenticatorData)}:{},...d.signature?{signature:M(d.signature)}:{},...d.userHandle?{userHandle:M(d.userHandle)}:{}}})}catch(s){t.log("credential serialization failed",s);return}}function ne(e,t){var n;try{return JSON.stringify({...e,challenge:M(e.challenge),allowCredentials:(n=e.allowCredentials)==null?void 0:n.map(a=>({...a,id:M(a.id)}))})}catch(a){t.log("request options serialization failed",a);return}}function M(e){if(!e)return;let t=e instanceof ArrayBuffer?new Uint8Array(e):new Uint8Array(e.buffer,e.byteOffset,e.byteLength),n="";for(let a of t)n+=String.fromCharCode(a);return window.btoa(n).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/g,"")}var J=Symbol.for("corbado.autocapture.request");function $(e){return e[J]}function re(e,t){try{Object.defineProperty(e,J,{value:t,writable:!0,configurable:!0,enumerable:!1})}catch{e[J]=t}}function oe(e,t,n,a){try{let p=e.responseType;if(p===""||p==="text"){let s=e.responseText;return typeof s=="string"&&s.length>t?void 0:n(s,"response")}if(p==="json"){let s=e.response;if(s==null)return;try{if(JSON.stringify(s).length>t)return}catch{return}return a(s,"response")}return}catch{return}}function ae(e,t,n){if(typeof e=="string"&&!(e.length>t))return n(e,"request")}function se(e){try{return e.responseURL||void 0}catch{return}}function ie(e){if(typeof e=="string")try{return JSON.parse(e)}catch{return}}var ue=/pass|secret|token|otp|csrf|auth|credential|assertion|session|pin|code|challenge/i,z="[redacted]";function K(e,t=0){if(t>8||e===null||typeof e!="object")return e;try{if(Array.isArray(e))return e.map(a=>K(a,t+1));let n={};for(let[a,p]of Object.entries(e))n[a]=ue.test(a)?z:K(p,t+1);return n}catch{return z}}var ce=500,de=65536;function le(e){return/event-stream|octet-stream/i.test(e)?!1:/json|text|urlencoded/i.test(e)}async function Y(e,t,n){var a,p,s;try{let d=Number((s=(p=(a=e.headers)==null?void 0:a.get)==null?void 0:p.call(a,"content-length"))!=null?s:"");if(Number.isFinite(d)&&d>t){n.log("body omitted: content-length exceeds limit",d);return}let C,k=new Promise(E=>{C=setTimeout(()=>E(void 0),ce)}),A=await Promise.race([e.text(),k]);if(C!==void 0&&clearTimeout(C),A===void 0){n.log("body omitted: read timed out");return}if(A.length>t){n.log("body omitted: exceeds size limit",A.length);return}return A}catch{return}}function fe(e){var h,P,H,l,w;let t=(h=e.logger)!=null?h:I,n=(P=e.transports)!=null?P:["xhr","fetch"],a=(H=e.maxBodyChars)!=null?H:de,p=((l=e.captureBodies)==null?void 0:l.request)===!0,s=((w=e.captureBodies)==null?void 0:w.response)===!0,d=!0,C=X(t,"network",r=>{d&&e.onExchange(r)}),k=r=>r==="request"?p:s,A=(r,o)=>{if(r===void 0)return;let u=K(r);if(!e.redact)return u;try{return e.redact(u,o)}catch(g){t.log("redact hook failed",g);return}},E=(r,o)=>{var g;if(!k(o))return;let u;try{u=((g=e.parseBody)!=null?g:ie)(r,o)}catch(y){t.log("body parsing failed",y);return}return A(u,o)},x=(r,o)=>{if(k(o))return A(r,o)},L=(r,o)=>{try{let u=typeof document!="undefined"?document.baseURI:void 0,g=new URL(String(r),u);return e.match(g,o)?{url:g.href,path:g.pathname}:void 0}catch{return}},_=()=>{if(typeof XMLHttpRequest=="undefined")return t.log("xhr capture unavailable: XMLHttpRequest missing"),null;let r=XMLHttpRequest.prototype,o=r.open,u=r.send;if(N(o)||N(u))return t.log("xhr capture unavailable: an equivalent capture is already active"),null;let g=function(c,i,...R){try{let f=$(this);f&&(f.reported=!0);let b=String(c).toUpperCase(),q=d?L(i,b):void 0;re(this,q?{method:b,url:q.url,path:q.path,reported:!1}:void 0)}catch(f){t.log("xhr open observation failed",f)}return o.call(this,c,i,...R)},y=function(c){try{let i=$(this);if(i&&!i.reported){i.startedAt=Date.now();let R=b=>()=>{i.outcome||(i.outcome=b)};this.addEventListener("abort",R("abort"),{once:!0}),this.addEventListener("timeout",R("timeout"),{once:!0}),this.addEventListener("error",R("error"),{once:!0});let f=()=>{try{if(this.readyState!==4||i.settledStatus!==void 0)return;let b=this.status;b!==0&&(i.settledStatus=b)}catch{}};this.addEventListener("readystatechange",f),this.addEventListener("loadend",()=>{var b,q;try{try{this.removeEventListener("readystatechange",f)}catch{}if(i.reported)return;i.reported=!0;let v=(b=i.settledStatus)!=null?b:0;if(i.settledStatus===void 0)try{v=this.status}catch{}let F=i.settledStatus!==void 0?"load":(q=i.outcome)!=null?q:"load";C({transport:"xhr",method:i.method,url:i.url,path:i.path,status:v,ok:v>=200&&v<300,outcome:F,durationMs:i.startedAt!==void 0?Date.now()-i.startedAt:void 0,responseUrl:se(this),requestBody:ae(c,a,E),responseBody:oe(this,a,E,x)})}catch(v){t.log("xhr exchange observation failed",v)}},{once:!0})}}catch(i){t.log("xhr send observation failed",i)}return u.call(this,c)};B(g,o),B(y,u);try{r.open=g,r.send=y}catch(c){return r.open===g&&(r.open=o),t.log("xhr capture unavailable: XMLHttpRequest methods are not writable",c),null}return()=>{r.open===g&&(r.open=o),r.send===y&&(r.send=u)}},S=()=>{let r=globalThis,o=r.fetch;if(typeof o!="function")return t.log("fetch capture unavailable: fetch missing"),null;if(N(o))return t.log("fetch capture unavailable: an equivalent capture is already active"),null;let u=async(y,c,i,R,f)=>{try{if(!d)return;let b=typeof R=="string"&&R.length>a?void 0:R!==void 0?E(R,"request"):void 0;if(b===void 0&&f){let v=await Y(f,a,t);v!==void 0&&(b=E(v,"request"))}let q;if(i){let v=await Y(i,a,t);v!==void 0&&(q=E(v,"response"))}C({transport:"fetch",method:y.method,url:y.url,path:y.path,status:c.status,ok:c.ok,outcome:"load",durationMs:Date.now()-y.startedAt,responseUrl:c.url||void 0,requestBody:b,responseBody:q})}catch(b){t.log("fetch exchange observation failed",b)}},g=function(y,c){var F,W;let i,R,f,b;try{if(d){let O=typeof Request!="undefined"&&y instanceof Request,D=String((F=c==null?void 0:c.method)!=null?F:O?y.method:"GET").toUpperCase(),U=O?y.url:y,j=L(U,D);if(j&&(i={method:D,url:j.url,path:j.path,startedAt:Date.now()},b=(W=c==null?void 0:c.signal)!=null?W:O?y.signal:void 0,p)){if(typeof(c==null?void 0:c.body)=="string")R=c.body;else if(O&&!(c!=null&&c.body))try{f=y.clone()}catch{}}}}catch(O){t.log("fetch observation failed",O)}let q=o.call(this!=null?this:globalThis,y,c);if(!i)return q;let v=i;return q.then(O=>{var U,j,G;let D;try{if(d&&s){let Q=(G=(j=(U=O.headers)==null?void 0:U.get)==null?void 0:j.call(U,"content-type"))!=null?G:"";le(Q)&&(D=O.clone())}}catch{}u(v,O,D,R,f)},O=>{try{let D=(b==null?void 0:b.aborted)===!0||(O==null?void 0:O.name)==="AbortError"||typeof DOMException!="undefined"&&O instanceof DOMException&&O.code===DOMException.ABORT_ERR;C({transport:"fetch",method:v.method,url:v.url,path:v.path,status:0,ok:!1,outcome:D?"abort":"error",durationMs:Date.now()-v.startedAt})}catch(D){t.log("fetch failure observation failed",D)}}),q};B(g,o);try{r.fetch=g}catch(y){return t.log("fetch capture unavailable: fetch is not writable",y),null}return()=>{r.fetch===g&&(r.fetch=o)}},m=[];if(n.includes("xhr")){let r=_();r&&m.push(r)}if(n.includes("fetch")){let r=S();r&&m.push(r)}return m.length===0?T():{mode:"standard",disconnect:()=>{d=!1;for(let r of m.splice(0))r()}}}function pe(e){var _;let t=(_=e.logger)!=null?_:I;if(typeof window=="undefined"||typeof history=="undefined")return t.log("navigation capture unavailable: window/history missing"),T();let n=history.pushState,a=history.replaceState;if(N(n)||N(a))return t.log("navigation capture unavailable: an equivalent capture is already active"),T();let p=!0,s=()=>({path:window.location.pathname,search:window.location.search,hash:window.location.hash}),d=s(),C=X(t,"navigation",S=>{p&&e.onNavigate(S)}),k=S=>{try{let m=s();if(m.path===d.path&&m.search===d.search&&m.hash===d.hash)return;d=m,C({...m,source:S})}catch(m){t.log("navigation observation failed",m)}},A=function(...S){let m=n.apply(this,S);return k("push"),m},E=function(...S){let m=a.apply(this,S);return k("replace"),m};B(A,n),B(E,a);let x=()=>k("pop"),L=()=>k("hash");try{history.pushState=A,history.replaceState=E}catch(S){return history.pushState===A&&(history.pushState=n),t.log("navigation capture unavailable: history methods are not writable",S),T()}return window.addEventListener("popstate",x),window.addEventListener("hashchange",L),{mode:"standard",disconnect:()=>{p=!1,history.pushState===A&&(history.pushState=n),history.replaceState===E&&(history.replaceState=a),window.removeEventListener("popstate",x),window.removeEventListener("hashchange",L)}}}function he(e="[autocapture]"){return{log:(...t)=>{try{console.log(e,...t)}catch{}}}}var ge="0.1.0";export{ge as VERSION,pe as captureNavigation,fe as captureNetwork,Z as captureWebAuthn,he as createConsoleLogger};
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@corbado/autocapture",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Observer-style capture helpers for browser authentication signals: WebAuthn ceremonies, selected network exchanges, and in-app navigation.",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"module": "./dist/index.mjs",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist"
|
|
10
|
+
],
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "tsup src/index.ts --format cjs,esm --target es2019 --dts --minify --define.__CORBADO_AUTOCAPTURE_VERSION__='\"'${CORBADO_AUTOCAPTURE_VERSION:-0.0.0-dev}'\"'",
|
|
13
|
+
"dev": "tsup src/index.ts --format cjs,esm --target es2019 --watch --dts",
|
|
14
|
+
"lint": "tsc --noEmit",
|
|
15
|
+
"test": "vitest run",
|
|
16
|
+
"test:watch": "vitest",
|
|
17
|
+
"test:e2e": "pnpm build && playwright test --config e2e/playwright.config.ts",
|
|
18
|
+
"check:dist": "es-check es2019 './dist/index.js' && es-check es2019 './dist/index.mjs' --module",
|
|
19
|
+
"format": "prettier --write 'src/**/*.ts' 'e2e/**/*.ts'",
|
|
20
|
+
"format:check": "prettier --check 'src/**/*.ts' 'e2e/**/*.ts'"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@playwright/test": "^1.57.0",
|
|
24
|
+
"@types/node": "^20",
|
|
25
|
+
"es-check": "latest",
|
|
26
|
+
"eslint-config-prettier": "^10.1.8",
|
|
27
|
+
"jsdom": "latest",
|
|
28
|
+
"prettier": "^3.7.4",
|
|
29
|
+
"tsup": "latest",
|
|
30
|
+
"typescript": "latest",
|
|
31
|
+
"typescript-eslint": "^8.55.0",
|
|
32
|
+
"vitest": "^4.1.0"
|
|
33
|
+
}
|
|
34
|
+
}
|