@corbado/observe 1.7.2 → 1.8.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 +1851 -0
- package/dist/analysis/config-manager.mjs +83 -0
- package/dist/analysis/config.mjs +216 -0
- package/dist/analysis/device-info-manager.mjs +153 -0
- package/dist/analysis/device.mjs +562 -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 +133 -0
- package/dist/analysis/low-events/input-change-recorder.mjs +200 -0
- package/dist/analysis/low-events/input-paste-observer.mjs +91 -0
- package/dist/analysis/low-events/input-tracker.mjs +223 -0
- package/dist/analysis/low-events/otp-input-binding.mjs +67 -0
- package/dist/analysis/low-events/shared-window-observer.mjs +225 -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 +887 -0
- package/dist/analysis/readers/device.mjs +73 -0
- package/dist/analysis/readers/meta.mjs +18 -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/storage/index.mjs +197 -0
- package/dist/analysis/storage/migration.mjs +56 -0
- package/dist/analysis/store/event-store.mjs +114 -0
- package/dist/analysis/transport.mjs +0 -0
- package/dist/analysis/transports/fetch.mjs +108 -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 +142 -0
- package/dist/analysis/utils/email-otp-operation-full.mjs +39 -0
- package/dist/analysis/utils/form-operation-full.mjs +81 -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 +32 -0
- package/dist/analysis/utils/passkey-login-operation-full.mjs +34 -0
- package/dist/analysis/utils/password-enrollment-operation-full.mjs +27 -0
- package/dist/analysis/utils/password-login-operation-full.mjs +61 -0
- package/dist/analysis/utils/password-manager-sampler.mjs +130 -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 +39 -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 +35 -0
- package/dist/analysis/utils/trusted-device-operation-full.mjs +100 -0
- package/dist/analysis/utils/web-lock.mjs +52 -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 +156 -4
- package/dist/index.d.ts +156 -4
- package/dist/index.js +4 -4
- package/dist/index.mjs +4 -4
- package/package.json +7 -2
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// src/storage/migration.ts
|
|
2
|
+
import { namespacedKey } from "./index.mjs";
|
|
3
|
+
var STORAGE_SCHEMA_VERSION = 1;
|
|
4
|
+
var STORAGE_SCHEMA_KEY = "cbo_storage_schema";
|
|
5
|
+
var LOCAL_KEYS = [
|
|
6
|
+
"cbo_seq",
|
|
7
|
+
"cbo_sdk_config",
|
|
8
|
+
"cbo_experiments",
|
|
9
|
+
"cbo_client_env_handle",
|
|
10
|
+
"cbo_outbox",
|
|
11
|
+
"cbo_visibility_change_debug",
|
|
12
|
+
"cbo_pagehide_debug",
|
|
13
|
+
"cbo_session"
|
|
14
|
+
];
|
|
15
|
+
var SESSION_KEYS = ["cbo_session_id", "cbo_tab_id"];
|
|
16
|
+
var COOKIE_KEYS = ["cbo_client_env_handle"];
|
|
17
|
+
function writeVerified(storage, key, value) {
|
|
18
|
+
storage.setItem(key, value);
|
|
19
|
+
return JSON.stringify(storage.getItem(key)) === JSON.stringify(value);
|
|
20
|
+
}
|
|
21
|
+
function migrateArea(storage, projectId, keys) {
|
|
22
|
+
const schema = storage.getItem(STORAGE_SCHEMA_KEY);
|
|
23
|
+
if (schema !== null) {
|
|
24
|
+
if (schema.version !== STORAGE_SCHEMA_VERSION || schema.projectId !== projectId) return false;
|
|
25
|
+
if (schema.complete === true) return true;
|
|
26
|
+
if (schema.complete !== false) return false;
|
|
27
|
+
} else {
|
|
28
|
+
if (keys.some((key) => storage.getItem(namespacedKey(key, projectId)) !== null)) {
|
|
29
|
+
return writeVerified(storage, STORAGE_SCHEMA_KEY, { version: STORAGE_SCHEMA_VERSION, projectId, complete: true });
|
|
30
|
+
}
|
|
31
|
+
if (!writeVerified(storage, STORAGE_SCHEMA_KEY, { version: STORAGE_SCHEMA_VERSION, projectId, complete: false })) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
for (const key of keys) {
|
|
36
|
+
const target = namespacedKey(key, projectId);
|
|
37
|
+
if (storage.getItem(target) !== null) continue;
|
|
38
|
+
const legacy = storage.getItem(key);
|
|
39
|
+
if (legacy !== null && !writeVerified(storage, target, legacy)) return false;
|
|
40
|
+
}
|
|
41
|
+
return writeVerified(storage, STORAGE_SCHEMA_KEY, { version: STORAGE_SCHEMA_VERSION, projectId, complete: true });
|
|
42
|
+
}
|
|
43
|
+
function migrateLegacyStorage(projectId, local, session, cookie, logger) {
|
|
44
|
+
try {
|
|
45
|
+
if (!migrateArea(local, projectId, LOCAL_KEYS)) return;
|
|
46
|
+
migrateArea(session, projectId, SESSION_KEYS);
|
|
47
|
+
if (cookie) migrateArea(cookie, projectId, COOKIE_KEYS);
|
|
48
|
+
} catch (error) {
|
|
49
|
+
logger.error("Failed to migrate legacy Observe storage", error);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export {
|
|
53
|
+
STORAGE_SCHEMA_KEY,
|
|
54
|
+
STORAGE_SCHEMA_VERSION,
|
|
55
|
+
migrateLegacyStorage
|
|
56
|
+
};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// src/store/event-store.ts
|
|
2
|
+
import { withWebLock } from "../utils/web-lock.mjs";
|
|
3
|
+
var OUTBOX_STORAGE_KEY = "cbo_outbox";
|
|
4
|
+
var OUTBOX_LOCK_NAME = "cbo_outbox";
|
|
5
|
+
var DEFAULT_MAX_ENTRIES = 500;
|
|
6
|
+
var DEFAULT_MAX_SERIALIZED_LENGTH = 512 * 1024;
|
|
7
|
+
function isOutboxEntry(value) {
|
|
8
|
+
if (!value || typeof value !== "object") {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
const entry = value;
|
|
12
|
+
return typeof entry.id === "string" && entry.id !== "" && typeof entry.sessionId === "string" && typeof entry.flushAfter === "number" && Number.isFinite(entry.flushAfter) && typeof entry.attempts === "number" && Number.isFinite(entry.attempts) && typeof entry.event === "object" && entry.event !== null;
|
|
13
|
+
}
|
|
14
|
+
var EventStore = class {
|
|
15
|
+
constructor(storage, logger, maxEntries = DEFAULT_MAX_ENTRIES, maxSerializedLength = DEFAULT_MAX_SERIALIZED_LENGTH, lockName = OUTBOX_LOCK_NAME, storageKey = OUTBOX_STORAGE_KEY) {
|
|
16
|
+
this.storage = storage;
|
|
17
|
+
this.logger = logger;
|
|
18
|
+
this.maxEntries = maxEntries;
|
|
19
|
+
this.maxSerializedLength = maxSerializedLength;
|
|
20
|
+
this.lockName = lockName;
|
|
21
|
+
this.storageKey = storageKey;
|
|
22
|
+
this.entries = this.read();
|
|
23
|
+
}
|
|
24
|
+
entries = [];
|
|
25
|
+
/** Malformed entries are dropped on every read; report only once per instance to avoid log spam. */
|
|
26
|
+
reportedMalformed = false;
|
|
27
|
+
read() {
|
|
28
|
+
const raw = this.storage.getItem(this.storageKey);
|
|
29
|
+
if (!Array.isArray(raw)) {
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
const valid = raw.filter(isOutboxEntry);
|
|
33
|
+
if (valid.length < raw.length && !this.reportedMalformed) {
|
|
34
|
+
this.reportedMalformed = true;
|
|
35
|
+
this.logger.error(`EventStore dropped ${raw.length - valid.length} malformed outbox entries`);
|
|
36
|
+
this.logger.reportTelemetry?.("error", "event store dropped malformed outbox entries");
|
|
37
|
+
}
|
|
38
|
+
return valid;
|
|
39
|
+
}
|
|
40
|
+
mutate(fn) {
|
|
41
|
+
return withWebLock(this.lockName, () => {
|
|
42
|
+
try {
|
|
43
|
+
let next = fn(this.read());
|
|
44
|
+
if (next.length > this.maxEntries) {
|
|
45
|
+
const evicted = next.length - this.maxEntries;
|
|
46
|
+
next = next.slice(next.length - this.maxEntries);
|
|
47
|
+
this.logger.error(`EventStore evicted ${evicted} oldest entries (cap ${this.maxEntries})`);
|
|
48
|
+
this.logger.reportTelemetry?.("error", "event store evicted entries (cap)");
|
|
49
|
+
}
|
|
50
|
+
next = this.enforceSizeBudget(next);
|
|
51
|
+
this.entries = next;
|
|
52
|
+
this.storage.setItem(this.storageKey, next);
|
|
53
|
+
} catch (error) {
|
|
54
|
+
this.logger.error("Failed to mutate event store", error);
|
|
55
|
+
this.logger.reportTelemetry?.("error", "failed to mutate event store");
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
/** Evict oldest entries until the serialized outbox fits {@link maxSerializedLength}. */
|
|
60
|
+
enforceSizeBudget(next) {
|
|
61
|
+
const sizes = next.map((entry) => JSON.stringify(entry).length);
|
|
62
|
+
let total = sizes.reduce((sum, size) => sum + size, next.length + 1);
|
|
63
|
+
let evict = 0;
|
|
64
|
+
while (total > this.maxSerializedLength && evict < next.length) {
|
|
65
|
+
total -= sizes[evict] + 1;
|
|
66
|
+
evict++;
|
|
67
|
+
}
|
|
68
|
+
if (evict === 0) {
|
|
69
|
+
return next;
|
|
70
|
+
}
|
|
71
|
+
this.logger.error(`EventStore evicted ${evict} oldest entries (size budget ${this.maxSerializedLength})`);
|
|
72
|
+
this.logger.reportTelemetry?.("error", "event store evicted entries (size budget)");
|
|
73
|
+
return next.slice(evict);
|
|
74
|
+
}
|
|
75
|
+
/** Append an entry. No-op if an entry with the same id already exists. */
|
|
76
|
+
add(entry) {
|
|
77
|
+
return this.mutate((current) => current.some((e) => e.id === entry.id) ? current : [...current, entry]);
|
|
78
|
+
}
|
|
79
|
+
/** Re-read from storage and return all entries (memory mirror refreshed). */
|
|
80
|
+
all() {
|
|
81
|
+
this.entries = this.read();
|
|
82
|
+
return this.entries;
|
|
83
|
+
}
|
|
84
|
+
/** Entries whose `flushAfter` has elapsed, in insertion order. */
|
|
85
|
+
getDue(now) {
|
|
86
|
+
return this.all().filter((entry) => entry.flushAfter <= now);
|
|
87
|
+
}
|
|
88
|
+
/** Remove entries by id (acknowledged or permanently dropped). */
|
|
89
|
+
remove(ids) {
|
|
90
|
+
if (ids.length === 0) {
|
|
91
|
+
return Promise.resolve();
|
|
92
|
+
}
|
|
93
|
+
const idSet = new Set(ids);
|
|
94
|
+
return this.mutate((current) => current.filter((entry) => !idSet.has(entry.id)));
|
|
95
|
+
}
|
|
96
|
+
/** Replace existing entries (matched by id) with updated copies, e.g. after bumping backoff. */
|
|
97
|
+
update(updated) {
|
|
98
|
+
if (updated.length === 0) {
|
|
99
|
+
return Promise.resolve();
|
|
100
|
+
}
|
|
101
|
+
const byId = new Map(updated.map((entry) => [entry.id, entry]));
|
|
102
|
+
return this.mutate((current) => current.map((entry) => byId.get(entry.id) ?? entry));
|
|
103
|
+
}
|
|
104
|
+
size() {
|
|
105
|
+
return this.all().length;
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
export {
|
|
109
|
+
DEFAULT_MAX_ENTRIES,
|
|
110
|
+
DEFAULT_MAX_SERIALIZED_LENGTH,
|
|
111
|
+
EventStore,
|
|
112
|
+
OUTBOX_LOCK_NAME,
|
|
113
|
+
OUTBOX_STORAGE_KEY
|
|
114
|
+
};
|
|
File without changes
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// src/transports/fetch.ts
|
|
2
|
+
import { parseReliabilityConfig } from "../config.mjs";
|
|
3
|
+
var MAX_KEEPALIVE_BODY_BYTES = 6e4;
|
|
4
|
+
var FETCH_TIMEOUT_MS = 6e4;
|
|
5
|
+
var FetchTransport = class {
|
|
6
|
+
constructor(options) {
|
|
7
|
+
this.options = options;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Error situations:
|
|
11
|
+
* - JSON.stringify(batch) throws: log "Failed to send event" and return empty TransportMakeRequestResponse
|
|
12
|
+
* - fetch throws or rejects: log "Failed to send event" and return empty TransportMakeRequestResponse
|
|
13
|
+
* - Reading the response (e.g. status or headers) throws: log "Failed to send event" and return empty TransportMakeRequestResponse
|
|
14
|
+
*
|
|
15
|
+
* When `extraHeaders` requests the SDK config (HTTP 200 with a body), the parsed config is returned
|
|
16
|
+
* in `config`. A 204 (legacy/back-compat) carries no body and leaves `config` undefined.
|
|
17
|
+
*/
|
|
18
|
+
async send(batch, extraHeaders) {
|
|
19
|
+
try {
|
|
20
|
+
batch.meta = {
|
|
21
|
+
...batch.meta,
|
|
22
|
+
sent: Date.now(),
|
|
23
|
+
transport: "fetch"
|
|
24
|
+
};
|
|
25
|
+
const body = JSON.stringify(batch);
|
|
26
|
+
const controller = typeof AbortController !== "undefined" ? new AbortController() : void 0;
|
|
27
|
+
const timeout = controller ? setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS) : void 0;
|
|
28
|
+
try {
|
|
29
|
+
const requestOptions = {
|
|
30
|
+
method: "POST",
|
|
31
|
+
headers: {
|
|
32
|
+
"Content-Type": "application/json",
|
|
33
|
+
...this.options.headers,
|
|
34
|
+
...extraHeaders
|
|
35
|
+
},
|
|
36
|
+
body,
|
|
37
|
+
keepalive: new Blob([body]).size <= MAX_KEEPALIVE_BODY_BYTES,
|
|
38
|
+
...controller ? { signal: controller.signal } : {}
|
|
39
|
+
};
|
|
40
|
+
const response = await fetch(this.options.url, requestOptions);
|
|
41
|
+
const result = {
|
|
42
|
+
statusCode: response.status,
|
|
43
|
+
headers: {
|
|
44
|
+
"retry-after": response.headers.get("Retry-After")
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
if (response.headers.get("X-Corbado-Observe-Accepted") === "1") result.deliveryAccepted = true;
|
|
48
|
+
if (response.status === 200) {
|
|
49
|
+
try {
|
|
50
|
+
const config = parseReliabilityConfig(await response.json());
|
|
51
|
+
if (config) {
|
|
52
|
+
result.config = config;
|
|
53
|
+
}
|
|
54
|
+
} catch (error) {
|
|
55
|
+
this.options.logger.error("Failed to parse SDK config response", error);
|
|
56
|
+
this.options.logger.reportTelemetry?.("error", "failed to parse SDK config response");
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return result;
|
|
60
|
+
} finally {
|
|
61
|
+
if (timeout) {
|
|
62
|
+
clearTimeout(timeout);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
} catch (error) {
|
|
66
|
+
this.options.logger.error("Failed to send event", error);
|
|
67
|
+
return {};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Error situations:
|
|
72
|
+
* - navigator.sendBeacon is not a function: return false
|
|
73
|
+
* - JSON.stringify(batch) throws: log "Failed to send event via beacon" and return false
|
|
74
|
+
* - Blob construction throws: log "Failed to send event via beacon" and return false
|
|
75
|
+
* - navigator.sendBeacon throws: log "Failed to send event via beacon" and return false
|
|
76
|
+
*/
|
|
77
|
+
sendBeacon(batch) {
|
|
78
|
+
try {
|
|
79
|
+
this.options.logger.debug(`Sending ${batch.events.length} events via beacon`);
|
|
80
|
+
if (typeof navigator?.sendBeacon !== "function") {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
batch.meta = {
|
|
84
|
+
...batch.meta,
|
|
85
|
+
sent: Date.now(),
|
|
86
|
+
transport: "beacon"
|
|
87
|
+
};
|
|
88
|
+
const blob = new Blob([JSON.stringify(batch)], {
|
|
89
|
+
// Using text/plain is recommended as it skips CORS handling. The ingestion API
|
|
90
|
+
// can handle both text/plain and application/json.
|
|
91
|
+
type: "text/plain"
|
|
92
|
+
});
|
|
93
|
+
return navigator.sendBeacon(this.options.url, blob);
|
|
94
|
+
} catch (error) {
|
|
95
|
+
this.options.logger.error("Failed to send event via beacon", error);
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* This transport does not buffer requests; always resolves with true. The `timeout` argument is unused.
|
|
101
|
+
*/
|
|
102
|
+
flush(timeout) {
|
|
103
|
+
return Promise.resolve(true);
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
export {
|
|
107
|
+
FetchTransport
|
|
108
|
+
};
|
|
@@ -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,142 @@
|
|
|
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
|
+
var EmailLinkOperationFull = class extends OperationFull {
|
|
6
|
+
send;
|
|
7
|
+
postResponse;
|
|
8
|
+
resend;
|
|
9
|
+
/** Transaction ID of this email link; hand it to the page that continues it. */
|
|
10
|
+
transactionId;
|
|
11
|
+
constructor(tracker, config = {}) {
|
|
12
|
+
super(tracker, "email-link");
|
|
13
|
+
this.send = this.defineStep("send");
|
|
14
|
+
this.postResponse = this.defineStep("post-response");
|
|
15
|
+
this.resend = this.defineStep("resend");
|
|
16
|
+
this.transactionId = config.transactionId || uuidv7();
|
|
17
|
+
void this.subflowStart(
|
|
18
|
+
{ explicitSpecType: config.explicitSpecType },
|
|
19
|
+
{ userReference: { crossEnvironmentTransactionIDStart: this.transactionId } }
|
|
20
|
+
);
|
|
21
|
+
const anchor = this.captureReference;
|
|
22
|
+
const sessionId = this.captureSession;
|
|
23
|
+
if (anchor && sessionId) {
|
|
24
|
+
rememberEmailLinkStart(tracker.getPersistentStorage(), {
|
|
25
|
+
transactionId: this.transactionId,
|
|
26
|
+
anchor,
|
|
27
|
+
sessionId,
|
|
28
|
+
specType: config.explicitSpecType,
|
|
29
|
+
startedAt: Date.now()
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
var EmailLinkContinuationOperation = class extends OperationFull {
|
|
35
|
+
postResponse;
|
|
36
|
+
resend;
|
|
37
|
+
mode;
|
|
38
|
+
continuationTracker;
|
|
39
|
+
specType;
|
|
40
|
+
createdAt = Date.now();
|
|
41
|
+
transaction;
|
|
42
|
+
started = false;
|
|
43
|
+
deferred = [];
|
|
44
|
+
onPageHide = () => this.flush();
|
|
45
|
+
constructor(tracker, config = {}) {
|
|
46
|
+
super(tracker, "email-link");
|
|
47
|
+
this.continuationTracker = tracker;
|
|
48
|
+
this.specType = config.explicitSpecType;
|
|
49
|
+
this.transaction = config.transactionId;
|
|
50
|
+
this.postResponse = this.deferrable(this.defineStep("post-response"), true);
|
|
51
|
+
this.resend = this.deferrable(this.defineStep("resend"));
|
|
52
|
+
const storage = tracker.getPersistentStorage();
|
|
53
|
+
const entry = findEmailLinkStart(storage, {
|
|
54
|
+
transactionId: config.transactionId,
|
|
55
|
+
specType: config.explicitSpecType
|
|
56
|
+
});
|
|
57
|
+
if (entry && entry.sessionId === tracker.getSessionId()) {
|
|
58
|
+
this.mode = "attached";
|
|
59
|
+
this.transaction = config.transactionId ?? entry.transactionId;
|
|
60
|
+
this.adoptCapture(entry.anchor, entry.sessionId, entry.specType ?? config.explicitSpecType);
|
|
61
|
+
this.started = true;
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
this.mode = "detached";
|
|
65
|
+
if (this.transaction) {
|
|
66
|
+
this.startDetached(this.createdAt);
|
|
67
|
+
} else if (typeof window !== "undefined") {
|
|
68
|
+
window.addEventListener("pagehide", this.onPageHide);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/** Transaction ID of the continued email link, when known. */
|
|
72
|
+
get transactionId() {
|
|
73
|
+
return this.transaction;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Supply the transaction ID once it is known. In detached mode this emits the deferred
|
|
77
|
+
* continuation start (back-dated to the first deferred step, or construction) and replays the
|
|
78
|
+
* deferred steps. Has no effect once a detached start was emitted without an ID.
|
|
79
|
+
*/
|
|
80
|
+
setTransactionId(transactionId) {
|
|
81
|
+
if (!transactionId || this.transaction) return;
|
|
82
|
+
this.transaction = transactionId;
|
|
83
|
+
if (this.mode === "detached" && !this.started) this.startDetached(this.firstDeferredAt());
|
|
84
|
+
}
|
|
85
|
+
/** Emit a pending detached start (without transaction ID) and its deferred steps now. */
|
|
86
|
+
flush() {
|
|
87
|
+
if (this.mode === "detached" && !this.started) this.startDetached(this.firstDeferredAt());
|
|
88
|
+
}
|
|
89
|
+
/** Flush anything pending and release the page-hide listener. */
|
|
90
|
+
destroy() {
|
|
91
|
+
this.flush();
|
|
92
|
+
this.removePageHideListener();
|
|
93
|
+
}
|
|
94
|
+
firstDeferredAt() {
|
|
95
|
+
return this.deferred[0]?.at ?? this.createdAt;
|
|
96
|
+
}
|
|
97
|
+
startDetached(timestamp) {
|
|
98
|
+
this.started = true;
|
|
99
|
+
this.removePageHideListener();
|
|
100
|
+
this.subflowStart(
|
|
101
|
+
{ explicitSpecType: this.specType, continuation: true },
|
|
102
|
+
{
|
|
103
|
+
explicitTimestamp: timestamp,
|
|
104
|
+
...this.transaction ? { userReference: { crossEnvironmentTransactionIDFinish: this.transaction } } : {}
|
|
105
|
+
}
|
|
106
|
+
);
|
|
107
|
+
const calls = this.deferred;
|
|
108
|
+
this.deferred = [];
|
|
109
|
+
for (const call of calls) call.run(call.at);
|
|
110
|
+
try {
|
|
111
|
+
this.continuationTracker.flushKeepalive();
|
|
112
|
+
} catch {
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
removePageHideListener() {
|
|
116
|
+
if (typeof window !== "undefined") window.removeEventListener("pagehide", this.onPageHide);
|
|
117
|
+
}
|
|
118
|
+
/** Defer step calls until the detached start is emitted; mark the continued link on completion. */
|
|
119
|
+
deferrable(step, completesLink = false) {
|
|
120
|
+
const call = (run, options) => {
|
|
121
|
+
if (this.started) {
|
|
122
|
+
run(options?.explicitTimestamp ?? Date.now());
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
this.deferred.push({ at: options?.explicitTimestamp ?? Date.now(), run });
|
|
126
|
+
};
|
|
127
|
+
return {
|
|
128
|
+
start: (data, options) => call((explicitTimestamp) => step.start(data, { ...options, explicitTimestamp }), options),
|
|
129
|
+
finished: (data, options) => {
|
|
130
|
+
call((explicitTimestamp) => step.finished(data, { ...options, explicitTimestamp }), options);
|
|
131
|
+
if (completesLink && this.transaction) {
|
|
132
|
+
markEmailLinkContinued(this.continuationTracker.getPersistentStorage(), this.transaction);
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
error: (error, options) => call((explicitTimestamp) => step.error(error, { ...options, explicitTimestamp }), options)
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
export {
|
|
140
|
+
EmailLinkContinuationOperation,
|
|
141
|
+
EmailLinkOperationFull
|
|
142
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
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
|
+
/** Rebind the ordered OTP group without starting a new subflow. An empty list detaches it. */
|
|
30
|
+
setInputFields(fields) {
|
|
31
|
+
this.lowEventTracker?.setFields(fields);
|
|
32
|
+
}
|
|
33
|
+
destroy() {
|
|
34
|
+
this.lowEventTracker?.destroy();
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
export {
|
|
38
|
+
EmailOtpOperationFull
|
|
39
|
+
};
|