@corbado/observe 1.7.2 → 1.9.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/dist/analysis/client.mjs +1877 -0
- package/dist/analysis/config-manager.mjs +108 -0
- package/dist/analysis/config.mjs +216 -0
- package/dist/analysis/device-info-manager.mjs +168 -0
- package/dist/analysis/device.mjs +640 -0
- package/dist/analysis/handoff.mjs +7 -0
- package/dist/analysis/index.mjs +118 -0
- package/dist/analysis/logger.mjs +50 -0
- package/dist/analysis/low-events/capture-sequence.mjs +11 -0
- package/dist/analysis/low-events/ceremony-initiator.mjs +26 -0
- package/dist/analysis/low-events/form-input-binding.mjs +46 -0
- package/dist/analysis/low-events/grouped-input-tracker.mjs +248 -0
- package/dist/analysis/low-events/input-change-recorder.mjs +200 -0
- package/dist/analysis/low-events/input-tracker.mjs +302 -0
- package/dist/analysis/low-events/otp-input-binding.mjs +67 -0
- package/dist/analysis/low-events/shared-window-observer.mjs +296 -0
- package/dist/analysis/low-events/sink.mjs +0 -0
- package/dist/analysis/low-events/viewport-batcher.mjs +42 -0
- package/dist/analysis/low-events/window-tracker.mjs +40 -0
- package/dist/analysis/queue.mjs +954 -0
- package/dist/analysis/readers/device.mjs +73 -0
- package/dist/analysis/readers/input-paste.mjs +91 -0
- package/dist/analysis/readers/input.mjs +61 -0
- package/dist/analysis/readers/loader-global.mjs +15 -0
- package/dist/analysis/readers/meta.mjs +18 -0
- package/dist/analysis/readers/network.mjs +14 -0
- package/dist/analysis/readers/page.mjs +30 -0
- package/dist/analysis/readers/password-manager-dom.mjs +112 -0
- package/dist/analysis/readers/password-manager-extensions.mjs +247 -0
- package/dist/analysis/readers/private-mode.mjs +9 -0
- package/dist/analysis/readers/storage.mjs +28 -0
- package/dist/analysis/readers/web-lock.mjs +56 -0
- package/dist/analysis/readers/window-events.mjs +48 -0
- package/dist/analysis/storage/index.mjs +249 -0
- package/dist/analysis/storage/migration.mjs +56 -0
- package/dist/analysis/store/event-store.mjs +132 -0
- package/dist/analysis/transport.mjs +0 -0
- package/dist/analysis/transports/fetch.mjs +150 -0
- package/dist/analysis/types.mjs +25 -0
- package/dist/analysis/utils/appconfirmation-operation-full.mjs +19 -0
- package/dist/analysis/utils/blocked-user-agents.mjs +37 -0
- package/dist/analysis/utils/captcha-operation-full.mjs +17 -0
- package/dist/analysis/utils/email-link-continuity.mjs +50 -0
- package/dist/analysis/utils/email-link-operation-full.mjs +162 -0
- package/dist/analysis/utils/email-otp-operation-full.mjs +38 -0
- package/dist/analysis/utils/form-operation-full.mjs +80 -0
- package/dist/analysis/utils/key-operation-full.mjs +38 -0
- package/dist/analysis/utils/normalize-error.mjs +29 -0
- package/dist/analysis/utils/operation-full-provide-identifier-with-cui.mjs +46 -0
- package/dist/analysis/utils/operation-full.mjs +235 -0
- package/dist/analysis/utils/passkey-deletion-operation-full.mjs +17 -0
- package/dist/analysis/utils/passkey-enrollment-operation-full.mjs +35 -0
- package/dist/analysis/utils/passkey-login-operation-full.mjs +39 -0
- package/dist/analysis/utils/password-enrollment-operation-full.mjs +27 -0
- package/dist/analysis/utils/password-login-operation-full.mjs +65 -0
- package/dist/analysis/utils/password-manager-sampler.mjs +170 -0
- package/dist/analysis/utils/provide-data-operation-full.mjs +28 -0
- package/dist/analysis/utils/serialize-raw-error.mjs +205 -0
- package/dist/analysis/utils/sms-otp-operation-full.mjs +38 -0
- package/dist/analysis/utils/social-operation-login.mjs +17 -0
- package/dist/analysis/utils/source-path.mjs +39 -0
- package/dist/analysis/utils/sso-operation-login.mjs +37 -0
- package/dist/analysis/utils/totp-operation-full.mjs +34 -0
- package/dist/analysis/utils/trusted-device-operation-full.mjs +100 -0
- package/dist/analysis/utils/webauthn-payloads.mjs +101 -0
- package/dist/analysis/version.mjs +7 -0
- package/dist/cdn/script.global.js +4 -4
- package/dist/index.d.mts +285 -21
- package/dist/index.d.ts +285 -21
- package/dist/index.js +4 -4
- package/dist/index.mjs +4 -4
- package/package.json +10 -3
- package/tools/reader-meta-text.d.mts +2 -0
- package/tools/reader-meta-text.mjs +26 -0
- package/tools/strip-reader-meta.d.mts +33 -0
- package/tools/strip-reader-meta.mjs +131 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// src/transports/fetch.ts
|
|
2
|
+
import { parseReliabilityConfig } from "../config.mjs";
|
|
3
|
+
import { beaconSender, sendFetch } from "../readers/network.mjs";
|
|
4
|
+
var MAX_KEEPALIVE_BODY_BYTES = 6e4;
|
|
5
|
+
var FETCH_TIMEOUT_MS = 6e4;
|
|
6
|
+
var FetchTransport = class {
|
|
7
|
+
constructor(options) {
|
|
8
|
+
this.options = options;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Error situations:
|
|
12
|
+
* - JSON.stringify(batch) throws: log "Failed to send event" and return empty TransportMakeRequestResponse
|
|
13
|
+
* - fetch throws or rejects: log "Failed to send event" and return empty TransportMakeRequestResponse
|
|
14
|
+
* - Reading the response (e.g. status or headers) throws: log "Failed to send event" and return empty TransportMakeRequestResponse
|
|
15
|
+
*
|
|
16
|
+
* When `extraHeaders` requests the SDK config (HTTP 200 with a body), the parsed config is returned
|
|
17
|
+
* in `config`. A 204 (legacy/back-compat) carries no body and leaves `config` undefined.
|
|
18
|
+
*/
|
|
19
|
+
async send(batch, extraHeaders) {
|
|
20
|
+
try {
|
|
21
|
+
batch.meta = {
|
|
22
|
+
...batch.meta,
|
|
23
|
+
sent: Date.now(),
|
|
24
|
+
transport: "fetch"
|
|
25
|
+
};
|
|
26
|
+
const body = JSON.stringify(batch);
|
|
27
|
+
const controller = typeof AbortController !== "undefined" ? new AbortController() : void 0;
|
|
28
|
+
const timeout = controller ? setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS) : void 0;
|
|
29
|
+
try {
|
|
30
|
+
const requestOptions = {
|
|
31
|
+
method: "POST",
|
|
32
|
+
headers: {
|
|
33
|
+
"Content-Type": "application/json",
|
|
34
|
+
...this.options.headers,
|
|
35
|
+
...extraHeaders
|
|
36
|
+
},
|
|
37
|
+
body,
|
|
38
|
+
keepalive: new Blob([body]).size <= MAX_KEEPALIVE_BODY_BYTES,
|
|
39
|
+
...controller ? { signal: controller.signal } : {}
|
|
40
|
+
};
|
|
41
|
+
const response = await sendFetch(
|
|
42
|
+
{
|
|
43
|
+
key: "network:observe.events",
|
|
44
|
+
title: "Sending events to Corbado Observe",
|
|
45
|
+
accessNote: "fetch (keepalive) to the Observe ingestion API",
|
|
46
|
+
group: "Event delivery",
|
|
47
|
+
origin: "observe-internal",
|
|
48
|
+
fields: [
|
|
49
|
+
{ path: "response.status", label: "Response code", exposure: "raw" },
|
|
50
|
+
{ path: "response.header.retry-after", label: "Retry delay requested by the server", exposure: "raw" },
|
|
51
|
+
{
|
|
52
|
+
path: "response.header.x-corbado-observe-accepted",
|
|
53
|
+
label: "Whether Observe accepted the events",
|
|
54
|
+
exposure: "enum",
|
|
55
|
+
transform: { values: ["1", "other"] }
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
path: "response.body",
|
|
59
|
+
label: "Observe settings in the response",
|
|
60
|
+
exposure: "raw",
|
|
61
|
+
transform: { note: "SDK reliability config (HTTP 200 only)" }
|
|
62
|
+
}
|
|
63
|
+
]
|
|
64
|
+
},
|
|
65
|
+
this.options.url,
|
|
66
|
+
requestOptions
|
|
67
|
+
);
|
|
68
|
+
const result = {
|
|
69
|
+
statusCode: response.status,
|
|
70
|
+
headers: {
|
|
71
|
+
"retry-after": response.headers.get("Retry-After")
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
if (response.headers.get("X-Corbado-Observe-Accepted") === "1") result.deliveryAccepted = true;
|
|
75
|
+
if (response.status === 200) {
|
|
76
|
+
try {
|
|
77
|
+
const config = parseReliabilityConfig(await response.json());
|
|
78
|
+
if (config) {
|
|
79
|
+
result.config = config;
|
|
80
|
+
}
|
|
81
|
+
} catch (error) {
|
|
82
|
+
this.options.logger.error("Failed to parse SDK config response", error);
|
|
83
|
+
this.options.logger.reportTelemetry?.("error", "failed to parse SDK config response");
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return result;
|
|
87
|
+
} finally {
|
|
88
|
+
if (timeout) {
|
|
89
|
+
clearTimeout(timeout);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
} catch (error) {
|
|
93
|
+
this.options.logger.error("Failed to send event", error);
|
|
94
|
+
return {};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Error situations:
|
|
99
|
+
* - navigator.sendBeacon is not a function: return false
|
|
100
|
+
* - JSON.stringify(batch) throws: log "Failed to send event via beacon" and return false
|
|
101
|
+
* - Blob construction throws: log "Failed to send event via beacon" and return false
|
|
102
|
+
* - navigator.sendBeacon throws: log "Failed to send event via beacon" and return false
|
|
103
|
+
*/
|
|
104
|
+
sendBeacon(batch) {
|
|
105
|
+
try {
|
|
106
|
+
this.options.logger.debug(`Sending ${batch.events.length} events via beacon`);
|
|
107
|
+
const send = beaconSender({
|
|
108
|
+
key: "network:observe.events-beacon",
|
|
109
|
+
title: "Sending events to Corbado Observe when the page closes",
|
|
110
|
+
accessNote: "navigator.sendBeacon to the Observe ingestion API",
|
|
111
|
+
group: "Event delivery",
|
|
112
|
+
origin: "observe-internal",
|
|
113
|
+
fields: [
|
|
114
|
+
{
|
|
115
|
+
path: "result",
|
|
116
|
+
label: "Whether the browser accepted the data for sending",
|
|
117
|
+
exposure: "enum",
|
|
118
|
+
transform: { values: [true, false], note: "whether the browser queued the beacon" }
|
|
119
|
+
}
|
|
120
|
+
]
|
|
121
|
+
});
|
|
122
|
+
if (!send) {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
batch.meta = {
|
|
126
|
+
...batch.meta,
|
|
127
|
+
sent: Date.now(),
|
|
128
|
+
transport: "beacon"
|
|
129
|
+
};
|
|
130
|
+
const blob = new Blob([JSON.stringify(batch)], {
|
|
131
|
+
// Using text/plain is recommended as it skips CORS handling. The ingestion API
|
|
132
|
+
// can handle both text/plain and application/json.
|
|
133
|
+
type: "text/plain"
|
|
134
|
+
});
|
|
135
|
+
return send(this.options.url, blob);
|
|
136
|
+
} catch (error) {
|
|
137
|
+
this.options.logger.error("Failed to send event via beacon", error);
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* This transport does not buffer requests; always resolves with true. The `timeout` argument is unused.
|
|
143
|
+
*/
|
|
144
|
+
flush(timeout) {
|
|
145
|
+
return Promise.resolve(true);
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
export {
|
|
149
|
+
FetchTransport
|
|
150
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// src/types.ts
|
|
2
|
+
var AuthEventName = /* @__PURE__ */ ((AuthEventName2) => {
|
|
3
|
+
AuthEventName2["FlowStarted"] = "flow_started";
|
|
4
|
+
AuthEventName2["FlowDecided"] = "flow_decided";
|
|
5
|
+
AuthEventName2["FlowFinished"] = "flow_finished";
|
|
6
|
+
AuthEventName2["FlowReset"] = "flow_reset";
|
|
7
|
+
AuthEventName2["AuthMethodDecisionStarted"] = "auth_method_decision_started";
|
|
8
|
+
AuthEventName2["AuthMethodDecisionFinished"] = "auth_method_decision_finished";
|
|
9
|
+
AuthEventName2["FlowAutoFinished"] = "flow_auto_finished";
|
|
10
|
+
AuthEventName2["AuthDecisionStarted"] = "auth_decision_started";
|
|
11
|
+
AuthEventName2["AuthDecisionFinished"] = "auth_decision_finished";
|
|
12
|
+
AuthEventName2["SubflowStepStarted"] = "subflow_step_started";
|
|
13
|
+
AuthEventName2["SubflowStepFinished"] = "subflow_step_finished";
|
|
14
|
+
AuthEventName2["SubflowStepError"] = "subflow_step_error";
|
|
15
|
+
AuthEventName2["SubflowTrigger"] = "subflow_trigger";
|
|
16
|
+
AuthEventName2["SubflowError"] = "subflow_error";
|
|
17
|
+
AuthEventName2["SubflowStarted"] = "subflow_started";
|
|
18
|
+
AuthEventName2["SubflowFinished"] = "subflow_finished";
|
|
19
|
+
AuthEventName2["FlowEnriched"] = "flow_enriched";
|
|
20
|
+
AuthEventName2["Conversion"] = "conversion";
|
|
21
|
+
return AuthEventName2;
|
|
22
|
+
})(AuthEventName || {});
|
|
23
|
+
export {
|
|
24
|
+
AuthEventName
|
|
25
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// src/utils/appconfirmation-operation-full.ts
|
|
2
|
+
import { OperationFull } from "./operation-full.mjs";
|
|
3
|
+
var AppConfirmationOperationFull = class extends OperationFull {
|
|
4
|
+
// One app confirmation, on the same device, reached through a QR handoff, or pushed to an enrolled device.
|
|
5
|
+
ceremony;
|
|
6
|
+
retry;
|
|
7
|
+
/** Optional separate browser exchange after approval; omit when the final poll already proves acceptance. */
|
|
8
|
+
postResponse;
|
|
9
|
+
constructor(tracker, config = {}) {
|
|
10
|
+
super(tracker, "app-confirmation");
|
|
11
|
+
this.ceremony = this.defineStep("ceremony");
|
|
12
|
+
this.retry = this.defineStep("retry");
|
|
13
|
+
this.postResponse = this.defineStep("post-response");
|
|
14
|
+
void this.subflowStart({ explicitSpecType: config.explicitSpecType });
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
export {
|
|
18
|
+
AppConfirmationOperationFull
|
|
19
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// src/utils/blocked-user-agents.ts
|
|
2
|
+
var BLOCKED_UA_SUBSTRINGS = [
|
|
3
|
+
"ahrefsbot",
|
|
4
|
+
"ahrefssiteaudit",
|
|
5
|
+
"amazonbot",
|
|
6
|
+
"baiduspider",
|
|
7
|
+
"bingbot",
|
|
8
|
+
"bingpreview",
|
|
9
|
+
"chrome-lighthouse",
|
|
10
|
+
"facebookexternal",
|
|
11
|
+
"petalbot",
|
|
12
|
+
"pinterest",
|
|
13
|
+
"screaming frog",
|
|
14
|
+
"yahoo! slurp",
|
|
15
|
+
"yandex",
|
|
16
|
+
"adsbot-google",
|
|
17
|
+
"apis-google",
|
|
18
|
+
"duplexweb-google",
|
|
19
|
+
"feedfetcher-google",
|
|
20
|
+
"google favicon",
|
|
21
|
+
"google web preview",
|
|
22
|
+
"google-read-aloud",
|
|
23
|
+
"googlebot",
|
|
24
|
+
"googleweblight",
|
|
25
|
+
"mediapartners-google",
|
|
26
|
+
"storebot-google"
|
|
27
|
+
];
|
|
28
|
+
function isBlockedUserAgent(userAgent) {
|
|
29
|
+
if (!userAgent) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
const normalizedUa = userAgent.toLowerCase();
|
|
33
|
+
return BLOCKED_UA_SUBSTRINGS.some((blockedUa) => normalizedUa.includes(blockedUa));
|
|
34
|
+
}
|
|
35
|
+
export {
|
|
36
|
+
isBlockedUserAgent
|
|
37
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// src/utils/captcha-operation-full.ts
|
|
2
|
+
import { OperationFull } from "./operation-full.mjs";
|
|
3
|
+
var CaptchaOperationFull = class extends OperationFull {
|
|
4
|
+
/** Step representing the captcha challenge ceremony. */
|
|
5
|
+
ceremony;
|
|
6
|
+
/** Submission of the captcha token and the server's verdict on it. */
|
|
7
|
+
postResponse;
|
|
8
|
+
constructor(tracker, config = {}) {
|
|
9
|
+
super(tracker, "captcha");
|
|
10
|
+
this.ceremony = this.defineStep("ceremony");
|
|
11
|
+
this.postResponse = this.defineStep("post-response");
|
|
12
|
+
void this.subflowStart({ explicitSpecType: config.explicitSpecType });
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
export {
|
|
16
|
+
CaptchaOperationFull
|
|
17
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// src/utils/email-link-continuity.ts
|
|
2
|
+
var EMAIL_LINK_STARTS_STORAGE_KEY = "cbo_email_link_starts";
|
|
3
|
+
var EMAIL_LINK_START_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
4
|
+
var MAX_ENTRIES = 10;
|
|
5
|
+
function read(storage, now) {
|
|
6
|
+
const raw = storage.getItem(EMAIL_LINK_STARTS_STORAGE_KEY);
|
|
7
|
+
if (!Array.isArray(raw)) return [];
|
|
8
|
+
return raw.filter(
|
|
9
|
+
(entry) => !!entry && typeof entry === "object" && typeof entry.transactionId === "string" && typeof entry.anchor === "string" && typeof entry.sessionId === "string" && typeof entry.startedAt === "number" && now - entry.startedAt < EMAIL_LINK_START_TTL_MS
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
function rememberEmailLinkStart(storage, entry) {
|
|
13
|
+
if (!storage) return;
|
|
14
|
+
try {
|
|
15
|
+
const entries = read(storage, entry.startedAt).filter((e) => e.transactionId !== entry.transactionId);
|
|
16
|
+
entries.push(entry);
|
|
17
|
+
storage.setItem(EMAIL_LINK_STARTS_STORAGE_KEY, entries.slice(-MAX_ENTRIES));
|
|
18
|
+
} catch {
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function findEmailLinkStart(storage, query, now = Date.now()) {
|
|
22
|
+
if (!storage) return void 0;
|
|
23
|
+
try {
|
|
24
|
+
const entries = read(storage, now);
|
|
25
|
+
if (query.transactionId) return entries.find((e) => e.transactionId === query.transactionId);
|
|
26
|
+
const pending = entries.filter(
|
|
27
|
+
(e) => !e.continued && (!query.specType || !e.specType || e.specType === query.specType)
|
|
28
|
+
);
|
|
29
|
+
return pending[pending.length - 1];
|
|
30
|
+
} catch {
|
|
31
|
+
return void 0;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function markEmailLinkContinued(storage, transactionId) {
|
|
35
|
+
if (!storage) return;
|
|
36
|
+
try {
|
|
37
|
+
const entries = read(storage, Date.now()).map(
|
|
38
|
+
(e) => e.transactionId === transactionId ? { ...e, continued: true } : e
|
|
39
|
+
);
|
|
40
|
+
storage.setItem(EMAIL_LINK_STARTS_STORAGE_KEY, entries);
|
|
41
|
+
} catch {
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export {
|
|
45
|
+
EMAIL_LINK_STARTS_STORAGE_KEY,
|
|
46
|
+
EMAIL_LINK_START_TTL_MS,
|
|
47
|
+
findEmailLinkStart,
|
|
48
|
+
markEmailLinkContinued,
|
|
49
|
+
rememberEmailLinkStart
|
|
50
|
+
};
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// src/utils/email-link-operation-full.ts
|
|
2
|
+
import { v7 as uuidv7 } from "uuid";
|
|
3
|
+
import { OperationFull } from "./operation-full.mjs";
|
|
4
|
+
import { findEmailLinkStart, markEmailLinkContinued, rememberEmailLinkStart } from "./email-link-continuity.mjs";
|
|
5
|
+
import { listenWindow } from "../readers/page.mjs";
|
|
6
|
+
var EmailLinkOperationFull = class extends OperationFull {
|
|
7
|
+
send;
|
|
8
|
+
postResponse;
|
|
9
|
+
resend;
|
|
10
|
+
/** Transaction ID of this email link; hand it to the page that continues it. */
|
|
11
|
+
transactionId;
|
|
12
|
+
constructor(tracker, config = {}) {
|
|
13
|
+
super(tracker, "email-link");
|
|
14
|
+
this.send = this.defineStep("send");
|
|
15
|
+
this.postResponse = this.defineStep("post-response");
|
|
16
|
+
this.resend = this.defineStep("resend");
|
|
17
|
+
this.transactionId = config.transactionId || uuidv7();
|
|
18
|
+
void this.subflowStart(
|
|
19
|
+
{ explicitSpecType: config.explicitSpecType },
|
|
20
|
+
{ userReference: { crossEnvironmentTransactionIDStart: this.transactionId } }
|
|
21
|
+
);
|
|
22
|
+
const anchor = this.captureReference;
|
|
23
|
+
const sessionId = this.captureSession;
|
|
24
|
+
if (anchor && sessionId) {
|
|
25
|
+
rememberEmailLinkStart(tracker.getPersistentStorage(), {
|
|
26
|
+
transactionId: this.transactionId,
|
|
27
|
+
anchor,
|
|
28
|
+
sessionId,
|
|
29
|
+
specType: config.explicitSpecType,
|
|
30
|
+
startedAt: Date.now()
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
var EmailLinkContinuationOperation = class extends OperationFull {
|
|
36
|
+
postResponse;
|
|
37
|
+
resend;
|
|
38
|
+
mode;
|
|
39
|
+
continuationTracker;
|
|
40
|
+
specType;
|
|
41
|
+
createdAt = Date.now();
|
|
42
|
+
transaction;
|
|
43
|
+
started = false;
|
|
44
|
+
deferred = [];
|
|
45
|
+
onPageHide = () => this.flush();
|
|
46
|
+
removePageHide;
|
|
47
|
+
constructor(tracker, config = {}) {
|
|
48
|
+
super(tracker, "email-link");
|
|
49
|
+
this.continuationTracker = tracker;
|
|
50
|
+
this.specType = config.explicitSpecType;
|
|
51
|
+
this.transaction = config.transactionId;
|
|
52
|
+
this.postResponse = this.deferrable(this.defineStep("post-response"), true);
|
|
53
|
+
this.resend = this.deferrable(this.defineStep("resend"));
|
|
54
|
+
const storage = tracker.getPersistentStorage();
|
|
55
|
+
const entry = findEmailLinkStart(storage, {
|
|
56
|
+
transactionId: config.transactionId,
|
|
57
|
+
specType: config.explicitSpecType
|
|
58
|
+
});
|
|
59
|
+
if (entry && entry.sessionId === tracker.getSessionId()) {
|
|
60
|
+
this.mode = "attached";
|
|
61
|
+
this.transaction = config.transactionId ?? entry.transactionId;
|
|
62
|
+
this.adoptCapture(entry.anchor, entry.sessionId, entry.specType ?? config.explicitSpecType);
|
|
63
|
+
this.started = true;
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
this.mode = "detached";
|
|
67
|
+
if (this.transaction) {
|
|
68
|
+
this.startDetached(this.createdAt);
|
|
69
|
+
} else if (typeof window !== "undefined") {
|
|
70
|
+
this.removePageHide = listenWindow(
|
|
71
|
+
{
|
|
72
|
+
key: "page:lifecycle.pagehide",
|
|
73
|
+
title: "Leaving the page",
|
|
74
|
+
accessNote: "pagehide event on window",
|
|
75
|
+
group: "Page lifecycle",
|
|
76
|
+
origin: "observe-external",
|
|
77
|
+
fields: [
|
|
78
|
+
{
|
|
79
|
+
path: "event.pagehide",
|
|
80
|
+
label: "Leaving the page",
|
|
81
|
+
exposure: "presence",
|
|
82
|
+
transform: { note: "flushes a detached email-link continuation on unload" }
|
|
83
|
+
}
|
|
84
|
+
]
|
|
85
|
+
},
|
|
86
|
+
"pagehide",
|
|
87
|
+
this.onPageHide
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/** Transaction ID of the continued email link, when known. */
|
|
92
|
+
get transactionId() {
|
|
93
|
+
return this.transaction;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Supply the transaction ID once it is known. In detached mode this emits the deferred
|
|
97
|
+
* continuation start (back-dated to the first deferred step, or construction) and replays the
|
|
98
|
+
* deferred steps. Has no effect once a detached start was emitted without an ID.
|
|
99
|
+
*/
|
|
100
|
+
setTransactionId(transactionId) {
|
|
101
|
+
if (!transactionId || this.transaction) return;
|
|
102
|
+
this.transaction = transactionId;
|
|
103
|
+
if (this.mode === "detached" && !this.started) this.startDetached(this.firstDeferredAt());
|
|
104
|
+
}
|
|
105
|
+
/** Emit a pending detached start (without transaction ID) and its deferred steps now. */
|
|
106
|
+
flush() {
|
|
107
|
+
if (this.mode === "detached" && !this.started) this.startDetached(this.firstDeferredAt());
|
|
108
|
+
}
|
|
109
|
+
/** Flush anything pending and release the page-hide listener. */
|
|
110
|
+
destroy() {
|
|
111
|
+
this.flush();
|
|
112
|
+
this.removePageHideListener();
|
|
113
|
+
}
|
|
114
|
+
firstDeferredAt() {
|
|
115
|
+
return this.deferred[0]?.at ?? this.createdAt;
|
|
116
|
+
}
|
|
117
|
+
startDetached(timestamp) {
|
|
118
|
+
this.started = true;
|
|
119
|
+
this.removePageHideListener();
|
|
120
|
+
this.subflowStart(
|
|
121
|
+
{ explicitSpecType: this.specType, continuation: true },
|
|
122
|
+
{
|
|
123
|
+
explicitTimestamp: timestamp,
|
|
124
|
+
...this.transaction ? { userReference: { crossEnvironmentTransactionIDFinish: this.transaction } } : {}
|
|
125
|
+
}
|
|
126
|
+
);
|
|
127
|
+
const calls = this.deferred;
|
|
128
|
+
this.deferred = [];
|
|
129
|
+
for (const call of calls) call.run(call.at);
|
|
130
|
+
try {
|
|
131
|
+
this.continuationTracker.flushKeepalive();
|
|
132
|
+
} catch {
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
removePageHideListener() {
|
|
136
|
+
if (typeof window !== "undefined") this.removePageHide?.();
|
|
137
|
+
}
|
|
138
|
+
/** Defer step calls until the detached start is emitted; mark the continued link on completion. */
|
|
139
|
+
deferrable(step, completesLink = false) {
|
|
140
|
+
const call = (run, options) => {
|
|
141
|
+
if (this.started) {
|
|
142
|
+
run(options?.explicitTimestamp ?? Date.now());
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
this.deferred.push({ at: options?.explicitTimestamp ?? Date.now(), run });
|
|
146
|
+
};
|
|
147
|
+
return {
|
|
148
|
+
start: (data, options) => call((explicitTimestamp) => step.start(data, { ...options, explicitTimestamp }), options),
|
|
149
|
+
finished: (data, options) => {
|
|
150
|
+
call((explicitTimestamp) => step.finished(data, { ...options, explicitTimestamp }), options);
|
|
151
|
+
if (completesLink && this.transaction) {
|
|
152
|
+
markEmailLinkContinued(this.continuationTracker.getPersistentStorage(), this.transaction);
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
error: (error, options) => call((explicitTimestamp) => step.error(error, { ...options, explicitTimestamp }), options)
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
export {
|
|
160
|
+
EmailLinkContinuationOperation,
|
|
161
|
+
EmailLinkOperationFull
|
|
162
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// src/utils/email-otp-operation-full.ts
|
|
2
|
+
import { OTPInputBinding } from "../low-events/otp-input-binding.mjs";
|
|
3
|
+
import { OperationFull } from "./operation-full.mjs";
|
|
4
|
+
var EmailOtpOperationFull = class extends OperationFull {
|
|
5
|
+
lowEventTracker;
|
|
6
|
+
send;
|
|
7
|
+
postResponse;
|
|
8
|
+
resend;
|
|
9
|
+
constructor(tracker, config = {}) {
|
|
10
|
+
super(tracker, "email-otp");
|
|
11
|
+
this.send = this.defineStep("send");
|
|
12
|
+
this.postResponse = this.defineStep("post-response");
|
|
13
|
+
this.resend = this.defineStep("resend");
|
|
14
|
+
void this.subflowStart({ explicitSpecType: config.explicitSpecType, emailDomain: config.emailDomain });
|
|
15
|
+
this.lowEventTracker = this.createLowCollector(
|
|
16
|
+
() => new OTPInputBinding(
|
|
17
|
+
tracker.getLogger(),
|
|
18
|
+
this.lowEventSink("input", "otp"),
|
|
19
|
+
this.lowEventSink("input-group", "otp")
|
|
20
|
+
)
|
|
21
|
+
);
|
|
22
|
+
if (config.inputHtmlFields?.length) {
|
|
23
|
+
this.setInputFields(config.inputHtmlFields);
|
|
24
|
+
} else if (config.inputHtmlField) {
|
|
25
|
+
this.lowEventTracker?.setFields([config.inputHtmlField], false);
|
|
26
|
+
}
|
|
27
|
+
this.registerLowFlusher(() => this.lowEventTracker?.flushPending());
|
|
28
|
+
}
|
|
29
|
+
setInputFields(fields) {
|
|
30
|
+
this.lowEventTracker?.setFields(fields);
|
|
31
|
+
}
|
|
32
|
+
destroy() {
|
|
33
|
+
this.lowEventTracker?.destroy();
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
export {
|
|
37
|
+
EmailOtpOperationFull
|
|
38
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// src/utils/form-operation-full.ts
|
|
2
|
+
import { FormInputBinding } from "../low-events/form-input-binding.mjs";
|
|
3
|
+
import { OperationFull } from "./operation-full.mjs";
|
|
4
|
+
var FORM_FIELDS_MAX_COUNT = 50;
|
|
5
|
+
var FORM_STARTED_AT_MAX_AGE_MS = 6e4;
|
|
6
|
+
var FORM_FIELD_NAME_MAX_LENGTH = 40;
|
|
7
|
+
var FORM_FIELD_NAME_PATTERN = /^[A-Za-z0-9._-]+$/;
|
|
8
|
+
var FormOperationFull = class extends OperationFull {
|
|
9
|
+
/** The application accepts the submitted form. */
|
|
10
|
+
submit;
|
|
11
|
+
/** The user leaves the form forward without submitting it. */
|
|
12
|
+
skip;
|
|
13
|
+
inputBinding;
|
|
14
|
+
constructor(tracker, config) {
|
|
15
|
+
super(tracker, "form");
|
|
16
|
+
this.submit = this.defineStep("submit");
|
|
17
|
+
this.skip = this.defineStep("skip");
|
|
18
|
+
void this.subflowStart(formStartData(tracker, config), formStartOptions(tracker, config));
|
|
19
|
+
this.inputBinding = this.createLowCollector(
|
|
20
|
+
() => new FormInputBinding(tracker.getLogger(), () => this.lowEventSink("input", "data"))
|
|
21
|
+
);
|
|
22
|
+
this.registerLowFlusher(() => this.inputBinding?.flushPending());
|
|
23
|
+
if (config?.inputHtmlFields?.length) this.setInputFields(config.inputHtmlFields);
|
|
24
|
+
}
|
|
25
|
+
setInputFields(fields) {
|
|
26
|
+
this.inputBinding?.setFields(fields);
|
|
27
|
+
}
|
|
28
|
+
destroy() {
|
|
29
|
+
this.inputBinding?.destroy();
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
function formStartOptions(tracker, config) {
|
|
33
|
+
const startedAt = config?.startedAt;
|
|
34
|
+
if (startedAt === void 0) return void 0;
|
|
35
|
+
const now = Date.now();
|
|
36
|
+
if (typeof startedAt !== "number" || !Number.isFinite(startedAt) || startedAt > now || now - startedAt > FORM_STARTED_AT_MAX_AGE_MS) {
|
|
37
|
+
warn(tracker, "Form: implausible startedAt ignored; using the current time");
|
|
38
|
+
return void 0;
|
|
39
|
+
}
|
|
40
|
+
return { explicitTimestamp: startedAt };
|
|
41
|
+
}
|
|
42
|
+
function formStartData(tracker, config) {
|
|
43
|
+
const data = {};
|
|
44
|
+
const name = config?.name;
|
|
45
|
+
if (typeof name === "string" && name.trim() !== "") {
|
|
46
|
+
data.name = name;
|
|
47
|
+
} else {
|
|
48
|
+
warn(tracker, "Form: missing name; the form will not be counted");
|
|
49
|
+
}
|
|
50
|
+
const fields = config?.fields;
|
|
51
|
+
if (fields === void 0) {
|
|
52
|
+
return data;
|
|
53
|
+
}
|
|
54
|
+
if (!Array.isArray(fields)) {
|
|
55
|
+
warn(tracker, "Form: fields must be an array of field names; omitted from telemetry");
|
|
56
|
+
return data;
|
|
57
|
+
}
|
|
58
|
+
const valid = fields.filter(
|
|
59
|
+
(field) => typeof field === "string" && field.length <= FORM_FIELD_NAME_MAX_LENGTH && FORM_FIELD_NAME_PATTERN.test(field)
|
|
60
|
+
);
|
|
61
|
+
if (valid.length < fields.length) {
|
|
62
|
+
warn(tracker, "Form: unsupported field names omitted from telemetry");
|
|
63
|
+
}
|
|
64
|
+
if (valid.length > FORM_FIELDS_MAX_COUNT) {
|
|
65
|
+
warn(tracker, `Form: more than ${FORM_FIELDS_MAX_COUNT} field names; the rest is omitted from telemetry`);
|
|
66
|
+
}
|
|
67
|
+
if (valid.length > 0) {
|
|
68
|
+
data.fields = valid.slice(0, FORM_FIELDS_MAX_COUNT);
|
|
69
|
+
}
|
|
70
|
+
return data;
|
|
71
|
+
}
|
|
72
|
+
function warn(tracker, message) {
|
|
73
|
+
try {
|
|
74
|
+
tracker.getLogger().warn(message);
|
|
75
|
+
} catch {
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
export {
|
|
79
|
+
FormOperationFull
|
|
80
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// src/utils/key-operation-full.ts
|
|
2
|
+
import { OperationFull } from "./operation-full.mjs";
|
|
3
|
+
function specType(tracker, config) {
|
|
4
|
+
if (config.explicitSpecType === void 0) return void 0;
|
|
5
|
+
try {
|
|
6
|
+
tracker.getLogger().warn("Unsupported key operation spec type; omitting it");
|
|
7
|
+
} catch {
|
|
8
|
+
}
|
|
9
|
+
return void 0;
|
|
10
|
+
}
|
|
11
|
+
var KeySigningOperationFull = class extends OperationFull {
|
|
12
|
+
/** Resolve an eligible key and prepare the input for an actual signing attempt. */
|
|
13
|
+
prepare;
|
|
14
|
+
/** Sign and assemble the usable signed artifact. Does not include submitting it. */
|
|
15
|
+
sign;
|
|
16
|
+
constructor(tracker, config = {}) {
|
|
17
|
+
super(tracker, "key-signing");
|
|
18
|
+
this.prepare = this.defineStep("prepare");
|
|
19
|
+
this.sign = this.defineStep("sign");
|
|
20
|
+
void this.subflowStart({ actor: "system", explicitSpecType: specType(tracker, config) });
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
var KeyRegistrationOperationFull = class extends OperationFull {
|
|
24
|
+
/** Build the request that will establish the key's authority. */
|
|
25
|
+
prepare;
|
|
26
|
+
/** Submit and await acceptance: finish only when the key is authorized and usable. */
|
|
27
|
+
postResponse;
|
|
28
|
+
constructor(tracker, config = {}) {
|
|
29
|
+
super(tracker, "key-registration");
|
|
30
|
+
this.prepare = this.defineStep("prepare");
|
|
31
|
+
this.postResponse = this.defineStep("post-response");
|
|
32
|
+
void this.subflowStart({ actor: "system", explicitSpecType: specType(tracker, config) });
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
export {
|
|
36
|
+
KeyRegistrationOperationFull,
|
|
37
|
+
KeySigningOperationFull
|
|
38
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// src/utils/normalize-error.ts
|
|
2
|
+
function normalizeError(error) {
|
|
3
|
+
if (error === void 0 || error === null) {
|
|
4
|
+
return void 0;
|
|
5
|
+
}
|
|
6
|
+
if (error instanceof Error) {
|
|
7
|
+
return {
|
|
8
|
+
name: error.name,
|
|
9
|
+
message: error.message
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
if (typeof error === "string") {
|
|
13
|
+
return { message: error };
|
|
14
|
+
}
|
|
15
|
+
if (typeof error === "object") {
|
|
16
|
+
const obj = error;
|
|
17
|
+
if (typeof obj.message === "string" || typeof obj.code === "string") {
|
|
18
|
+
return {
|
|
19
|
+
name: typeof obj.name === "string" ? obj.name : void 0,
|
|
20
|
+
message: typeof obj.message === "string" ? obj.message : void 0,
|
|
21
|
+
code: typeof obj.code === "string" ? obj.code : void 0
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return { message: String(error) };
|
|
26
|
+
}
|
|
27
|
+
export {
|
|
28
|
+
normalizeError
|
|
29
|
+
};
|