@corbado/observe 1.7.1 → 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 +164 -11
- package/dist/index.d.ts +164 -11
- package/dist/index.js +4 -4
- package/dist/index.mjs +4 -4
- package/package.json +7 -2
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// src/utils/password-manager-sampler.ts
|
|
2
|
+
import {
|
|
3
|
+
readPasswordManagerExtensions
|
|
4
|
+
} from "../readers/password-manager-extensions.mjs";
|
|
5
|
+
var LOAD_RESAMPLE_DELAY_MS = 3e3;
|
|
6
|
+
var INPUT_FOCUS_SAMPLE_DELAY_MS = 500;
|
|
7
|
+
var MAX_FOCUS_SAMPLES = 6;
|
|
8
|
+
var IDLE_TIMEOUT_MS = 1e3;
|
|
9
|
+
var PasswordManagerSampler = class {
|
|
10
|
+
constructor(logger, isEnabled, windowObserver) {
|
|
11
|
+
this.logger = logger;
|
|
12
|
+
this.isEnabled = isEnabled;
|
|
13
|
+
if (typeof window === "undefined" || typeof document === "undefined") return;
|
|
14
|
+
this.sample();
|
|
15
|
+
if (document.readyState === "complete") {
|
|
16
|
+
this.sampleAfter(LOAD_RESAMPLE_DELAY_MS);
|
|
17
|
+
} else {
|
|
18
|
+
const onLoad = () => {
|
|
19
|
+
this.sampleWhenIdle();
|
|
20
|
+
this.sampleAfter(LOAD_RESAMPLE_DELAY_MS);
|
|
21
|
+
};
|
|
22
|
+
window.addEventListener("load", onLoad, { once: true });
|
|
23
|
+
this.removers.push(() => window.removeEventListener("load", onLoad));
|
|
24
|
+
}
|
|
25
|
+
const removeFocus = windowObserver.onInputFocus((field) => {
|
|
26
|
+
if (!this.isEnabled() || this.sampledFields.has(field)) return;
|
|
27
|
+
this.sampledFields.add(field);
|
|
28
|
+
if (++this.focusSamples >= MAX_FOCUS_SAMPLES) removeFocus();
|
|
29
|
+
this.sampleAfter(INPUT_FOCUS_SAMPLE_DELAY_MS);
|
|
30
|
+
});
|
|
31
|
+
this.removers.push(removeFocus);
|
|
32
|
+
}
|
|
33
|
+
detected = /* @__PURE__ */ new Map();
|
|
34
|
+
timers = /* @__PURE__ */ new Set();
|
|
35
|
+
idleCallbacks = /* @__PURE__ */ new Set();
|
|
36
|
+
/** Registered fields whose first focus already scheduled a sample (weak, so removed forms are not retained). */
|
|
37
|
+
sampledFields = /* @__PURE__ */ new WeakSet();
|
|
38
|
+
focusSamples = 0;
|
|
39
|
+
removers = [];
|
|
40
|
+
destroyed = false;
|
|
41
|
+
/** Per session, how many managers the last reported set held; the set only grows, so a count suffices. */
|
|
42
|
+
reportedCounts = /* @__PURE__ */ new Map();
|
|
43
|
+
/** Managers detected on this page so far, in detection order. */
|
|
44
|
+
getDetected() {
|
|
45
|
+
return [...this.detected.values()];
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* The full set of detected manager names when it grew since the last report for `sessionId`, otherwise
|
|
49
|
+
* undefined. Each session's first event after a detection carries the set (also after `resetSession()`),
|
|
50
|
+
* later events only when a sample added a manager. Signed-in upgrades are not reported, so they never
|
|
51
|
+
* count as growth. A lost event that carried the set is not repaired by later events of the same page.
|
|
52
|
+
*/
|
|
53
|
+
takeReport(sessionId) {
|
|
54
|
+
if (!this.isEnabled() || this.detected.size <= (this.reportedCounts.get(sessionId) ?? 0)) return void 0;
|
|
55
|
+
this.reportedCounts.set(sessionId, this.detected.size);
|
|
56
|
+
return this.getDetected().map(({ name }) => ({ name }));
|
|
57
|
+
}
|
|
58
|
+
destroy() {
|
|
59
|
+
this.destroyed = true;
|
|
60
|
+
for (const remove of this.removers) remove();
|
|
61
|
+
for (const timer of this.timers) clearTimeout(timer);
|
|
62
|
+
this.timers.clear();
|
|
63
|
+
for (const handle of this.idleCallbacks) cancelIdleCallback(handle);
|
|
64
|
+
this.idleCallbacks.clear();
|
|
65
|
+
}
|
|
66
|
+
sampleAfter(delayMs) {
|
|
67
|
+
const timer = setTimeout(() => {
|
|
68
|
+
this.timers.delete(timer);
|
|
69
|
+
this.sampleWhenIdle();
|
|
70
|
+
}, delayMs);
|
|
71
|
+
this.timers.add(timer);
|
|
72
|
+
}
|
|
73
|
+
sampleWhenIdle() {
|
|
74
|
+
if (typeof requestIdleCallback !== "function") {
|
|
75
|
+
this.sample();
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const handle = requestIdleCallback(
|
|
79
|
+
() => {
|
|
80
|
+
this.idleCallbacks.delete(handle);
|
|
81
|
+
this.sample();
|
|
82
|
+
},
|
|
83
|
+
{ timeout: IDLE_TIMEOUT_MS }
|
|
84
|
+
);
|
|
85
|
+
this.idleCallbacks.add(handle);
|
|
86
|
+
}
|
|
87
|
+
sample() {
|
|
88
|
+
if (this.destroyed || !this.isEnabled()) return;
|
|
89
|
+
try {
|
|
90
|
+
const found = readPasswordManagerExtensions({
|
|
91
|
+
key: "browser-api:password-managers.extensions",
|
|
92
|
+
fields: [
|
|
93
|
+
{
|
|
94
|
+
path: "result[].name",
|
|
95
|
+
exposure: "enum",
|
|
96
|
+
transform: {
|
|
97
|
+
values: [
|
|
98
|
+
"1password",
|
|
99
|
+
"bitwarden",
|
|
100
|
+
"dashlane",
|
|
101
|
+
"kaspersky",
|
|
102
|
+
"keeper",
|
|
103
|
+
"lastpass",
|
|
104
|
+
"nordpass",
|
|
105
|
+
"protonpass"
|
|
106
|
+
]
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
// Read to merge samples, never reported (takeReport keeps only the name).
|
|
110
|
+
{ path: "result[].signedIn", exposure: "none" }
|
|
111
|
+
],
|
|
112
|
+
dataConfig: "pmd"
|
|
113
|
+
});
|
|
114
|
+
for (const extension of found) {
|
|
115
|
+
const known = this.detected.get(extension.name);
|
|
116
|
+
if (!known || extension.signedIn && !known.signedIn) {
|
|
117
|
+
this.detected.set(extension.name, extension);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
} catch (error) {
|
|
121
|
+
this.logger.error("Failed to detect password manager extensions", error);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
export {
|
|
126
|
+
INPUT_FOCUS_SAMPLE_DELAY_MS,
|
|
127
|
+
LOAD_RESAMPLE_DELAY_MS,
|
|
128
|
+
MAX_FOCUS_SAMPLES,
|
|
129
|
+
PasswordManagerSampler
|
|
130
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// src/utils/provide-data-operation-full.ts
|
|
2
|
+
import { LowEventInputTracker } from "../low-events/input-tracker.mjs";
|
|
3
|
+
import { OperationFull } from "./operation-full.mjs";
|
|
4
|
+
var ProvideDataOperationFull = class extends OperationFull {
|
|
5
|
+
clientValidation;
|
|
6
|
+
postResponse;
|
|
7
|
+
lowEventTracker;
|
|
8
|
+
constructor(tracker, config = {}) {
|
|
9
|
+
super(tracker, "provide-data");
|
|
10
|
+
this.clientValidation = this.defineStep("client-validation");
|
|
11
|
+
this.postResponse = this.defineStep("post-response");
|
|
12
|
+
const { inputHtmlField, ...startData } = config;
|
|
13
|
+
void this.subflowStart(startData);
|
|
14
|
+
if (!inputHtmlField) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
this.lowEventTracker = this.createLowCollector(
|
|
18
|
+
() => new LowEventInputTracker(tracker.getLogger(), inputHtmlField, this.lowEventSink("input", "data"))
|
|
19
|
+
);
|
|
20
|
+
this.registerLowFlusher(() => this.lowEventTracker?.flushPending());
|
|
21
|
+
}
|
|
22
|
+
destroy() {
|
|
23
|
+
this.lowEventTracker?.destroy();
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
export {
|
|
27
|
+
ProvideDataOperationFull
|
|
28
|
+
};
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
// src/utils/serialize-raw-error.ts
|
|
2
|
+
var RAW_ERROR_BOUNDS = {
|
|
3
|
+
maxStackFrames: { min: 1, max: 100, default: 50 },
|
|
4
|
+
depth: { min: 0, max: 10, default: 3 },
|
|
5
|
+
maxBreadth: { min: 1, max: 1e3, default: 30 },
|
|
6
|
+
maxValueLength: { min: 1, max: 1e4, default: 1024 },
|
|
7
|
+
maxCauseDepth: { min: 0, max: 10, default: 3 },
|
|
8
|
+
maxBytes: { min: 0, max: 65536, default: 32768 }
|
|
9
|
+
};
|
|
10
|
+
var MAX_NODES = 1024;
|
|
11
|
+
var RawErrorBudgetExceeded = class extends Error {
|
|
12
|
+
};
|
|
13
|
+
var ERROR_FIELDS = ["name", "message", "code", "errors", "stack"];
|
|
14
|
+
var HAS_MULTI_BYTE_CHARACTERS = /[^\x20-\x7E]/;
|
|
15
|
+
var tag = (value) => Object.prototype.toString.call(value).slice(8, -1);
|
|
16
|
+
var typeOf = (value) => value === null ? "null" : typeof value === "object" ? tag(value) : typeof value;
|
|
17
|
+
var isErrorLike = (value) => value instanceof Error || tag(value) === "Error" || tag(value) === "DOMException";
|
|
18
|
+
function byteLength(json) {
|
|
19
|
+
return HAS_MULTI_BYTE_CHARACTERS.test(json) ? new TextEncoder().encode(json).byteLength : json.length;
|
|
20
|
+
}
|
|
21
|
+
function clamp(name, value) {
|
|
22
|
+
const bounds = RAW_ERROR_BOUNDS[name];
|
|
23
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return bounds.default;
|
|
24
|
+
return Math.min(bounds.max, Math.max(bounds.min, Math.floor(value)));
|
|
25
|
+
}
|
|
26
|
+
function resolveRawErrorLimits(options = {}) {
|
|
27
|
+
return {
|
|
28
|
+
stack: options.stack === true,
|
|
29
|
+
maxStackFrames: clamp("maxStackFrames", options.maxStackFrames),
|
|
30
|
+
depth: clamp("depth", options.depth),
|
|
31
|
+
maxBreadth: clamp("maxBreadth", options.maxBreadth),
|
|
32
|
+
maxValueLength: clamp("maxValueLength", options.maxValueLength),
|
|
33
|
+
maxCauseDepth: clamp("maxCauseDepth", options.maxCauseDepth),
|
|
34
|
+
maxBytes: clamp("maxBytes", options.maxBytes)
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function serializeRawError(value, options = {}) {
|
|
38
|
+
if (value === void 0) return void 0;
|
|
39
|
+
let limits = resolveRawErrorLimits();
|
|
40
|
+
const fits = (out) => {
|
|
41
|
+
const json = JSON.stringify(out);
|
|
42
|
+
return json === void 0 || byteLength(json) <= limits.maxBytes;
|
|
43
|
+
};
|
|
44
|
+
try {
|
|
45
|
+
limits = resolveRawErrorLimits(options);
|
|
46
|
+
if (limits.maxBytes === 0) return void 0;
|
|
47
|
+
const type = truncate(typeOf(value), 1024);
|
|
48
|
+
for (let depth = limits.depth; depth >= 0; depth--) {
|
|
49
|
+
const ctx = { limits, causeBudget: limits.maxCauseDepth, seen: /* @__PURE__ */ new Set(), nodes: 0, textBytes: 0 };
|
|
50
|
+
try {
|
|
51
|
+
const out = { type, value: walk(value, depth, ctx) };
|
|
52
|
+
if (fits(out)) return out;
|
|
53
|
+
} catch (error) {
|
|
54
|
+
if (!(error instanceof RawErrorBudgetExceeded)) throw error;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const fallback = { type, truncated: true };
|
|
58
|
+
return fits(fallback) ? fallback : void 0;
|
|
59
|
+
} catch {
|
|
60
|
+
const fallback = { type: "unserializable" };
|
|
61
|
+
return fits(fallback) ? fallback : void 0;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function accountText(text, ctx) {
|
|
65
|
+
if (text.length > ctx.limits.maxBytes) throw new RawErrorBudgetExceeded();
|
|
66
|
+
ctx.textBytes += byteLength(JSON.stringify(text));
|
|
67
|
+
if (ctx.textBytes > ctx.limits.maxBytes) throw new RawErrorBudgetExceeded();
|
|
68
|
+
return text;
|
|
69
|
+
}
|
|
70
|
+
function walk(value, depth, ctx) {
|
|
71
|
+
if (++ctx.nodes > MAX_NODES) throw new RawErrorBudgetExceeded();
|
|
72
|
+
const out = walkValue(value, depth, ctx);
|
|
73
|
+
return typeof out === "string" ? accountText(out, ctx) : out;
|
|
74
|
+
}
|
|
75
|
+
function walkValue(value, depth, ctx) {
|
|
76
|
+
switch (typeof value) {
|
|
77
|
+
case "string":
|
|
78
|
+
return truncate(value, ctx.limits.maxValueLength);
|
|
79
|
+
case "number":
|
|
80
|
+
return Number.isFinite(value) ? value : String(value);
|
|
81
|
+
case "boolean":
|
|
82
|
+
case "undefined":
|
|
83
|
+
return value;
|
|
84
|
+
case "bigint":
|
|
85
|
+
case "symbol":
|
|
86
|
+
return truncate(String(value), ctx.limits.maxValueLength);
|
|
87
|
+
case "function":
|
|
88
|
+
return `[Function: ${value.name || "anonymous"}]`;
|
|
89
|
+
}
|
|
90
|
+
if (value === null) return null;
|
|
91
|
+
const obj = value;
|
|
92
|
+
if (ctx.seen.has(obj)) return "[Circular]";
|
|
93
|
+
if (depth < 0) return `[${tag(obj)}]`;
|
|
94
|
+
ctx.seen.add(obj);
|
|
95
|
+
try {
|
|
96
|
+
return walkObject(obj, depth, ctx);
|
|
97
|
+
} finally {
|
|
98
|
+
ctx.seen.delete(obj);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function truncate(value, max) {
|
|
102
|
+
return value.length > max ? value.slice(0, max) + "\u2026" : value;
|
|
103
|
+
}
|
|
104
|
+
function truncateStack(stack, maxFrames, maxValueLength) {
|
|
105
|
+
const textTruncated = stack.length > maxValueLength;
|
|
106
|
+
stack = truncate(stack, maxValueLength);
|
|
107
|
+
const lines = stack.split("\n");
|
|
108
|
+
if (lines.length <= maxFrames + 1) return truncate(stack, maxValueLength);
|
|
109
|
+
const kept = lines.slice(0, maxFrames + 1).join("\n");
|
|
110
|
+
const marker = textTruncated ? "[truncated]" : `[+${lines.length - maxFrames - 1} frames]`;
|
|
111
|
+
return truncate(`${kept}
|
|
112
|
+
${marker}`, maxValueLength);
|
|
113
|
+
}
|
|
114
|
+
function walkObject(obj, depth, ctx) {
|
|
115
|
+
const { limits } = ctx;
|
|
116
|
+
const kind = tag(obj);
|
|
117
|
+
const next = (v) => walk(v, depth - 1, ctx);
|
|
118
|
+
if (kind === "Date") return Number.isNaN(obj.getTime()) ? "[Invalid Date]" : obj.toISOString();
|
|
119
|
+
if (kind === "RegExp") return truncate(String(obj), limits.maxValueLength);
|
|
120
|
+
if (kind === "ArrayBuffer") return `[ArrayBuffer(${obj.byteLength})]`;
|
|
121
|
+
if (ArrayBuffer.isView(obj)) return `[${kind}(${obj.byteLength})]`;
|
|
122
|
+
if (kind === "Map" || kind === "Set") {
|
|
123
|
+
const collection = obj;
|
|
124
|
+
const items = [];
|
|
125
|
+
for (const value of collection) {
|
|
126
|
+
if (items.length === limits.maxBreadth) break;
|
|
127
|
+
items.push(next(value));
|
|
128
|
+
}
|
|
129
|
+
if (collection.size > items.length) items.push(`[+${collection.size - items.length} more]`);
|
|
130
|
+
return items;
|
|
131
|
+
}
|
|
132
|
+
if (kind === "Promise" || kind === "WeakMap" || kind === "WeakSet" || kind === "Window") return `[${kind}]`;
|
|
133
|
+
if (typeof Node !== "undefined" && obj instanceof Node) return `[${kind}]`;
|
|
134
|
+
if (Array.isArray(obj)) {
|
|
135
|
+
const items = obj.slice(0, limits.maxBreadth).map(next);
|
|
136
|
+
if (obj.length > limits.maxBreadth) items.push(`[+${obj.length - limits.maxBreadth} more]`);
|
|
137
|
+
return items;
|
|
138
|
+
}
|
|
139
|
+
if (typeof Response !== "undefined" && obj instanceof Response) {
|
|
140
|
+
return {
|
|
141
|
+
status: next(obj.status),
|
|
142
|
+
statusText: next(obj.statusText),
|
|
143
|
+
url: next(obj.url),
|
|
144
|
+
redirected: next(obj.redirected)
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
if (typeof Event !== "undefined" && obj instanceof Event) {
|
|
148
|
+
const ev = obj;
|
|
149
|
+
const out2 = { type: next(ev.type) };
|
|
150
|
+
for (const key of ["message", "filename", "lineno", "colno"]) {
|
|
151
|
+
if (key in ev) out2[key] = next(ev[key]);
|
|
152
|
+
}
|
|
153
|
+
if ("error" in ev && ev.error != null) out2.error = next(ev.error);
|
|
154
|
+
return out2;
|
|
155
|
+
}
|
|
156
|
+
const out = /* @__PURE__ */ Object.create(null);
|
|
157
|
+
const record = obj;
|
|
158
|
+
const errorLike = isErrorLike(obj);
|
|
159
|
+
if (errorLike) {
|
|
160
|
+
for (const field of ERROR_FIELDS) {
|
|
161
|
+
if (field === "stack" && !limits.stack) continue;
|
|
162
|
+
const v = record[field];
|
|
163
|
+
if (v === void 0 || v === null || typeof v === "function") continue;
|
|
164
|
+
if (field === "stack") {
|
|
165
|
+
if (limits.stack && typeof v === "string")
|
|
166
|
+
out.stack = accountText(truncateStack(v, limits.maxStackFrames, limits.maxValueLength), ctx);
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
out[field] = next(v);
|
|
170
|
+
}
|
|
171
|
+
const cause = record.cause;
|
|
172
|
+
if (cause !== void 0 && cause !== null && typeof cause !== "function") {
|
|
173
|
+
out.cause = walkCause(cause, depth - 1, ctx);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
let keys = 0;
|
|
177
|
+
const ownKeys = Object.keys(record);
|
|
178
|
+
for (const key of ownKeys) {
|
|
179
|
+
if (key in out || key === "stack" && !limits.stack || errorLike && key === "cause") continue;
|
|
180
|
+
if (++keys > limits.maxBreadth) {
|
|
181
|
+
out["\u2026"] = `[+${ownKeys.length - limits.maxBreadth} keys]`;
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
const v = record[key];
|
|
185
|
+
if (typeof v === "function") continue;
|
|
186
|
+
out[accountText(key, ctx)] = key === "stack" && typeof v === "string" ? accountText(truncateStack(v, limits.maxStackFrames, limits.maxValueLength), ctx) : next(v);
|
|
187
|
+
}
|
|
188
|
+
if (!errorLike && kind !== "Object" && keys === 0) return `[${kind}]`;
|
|
189
|
+
return out;
|
|
190
|
+
}
|
|
191
|
+
function walkCause(cause, depth, ctx) {
|
|
192
|
+
if (typeof cause !== "object" || cause === null) return walk(cause, depth, ctx);
|
|
193
|
+
if (ctx.causeBudget <= 0) return accountText(`[${tag(cause)}]`, ctx);
|
|
194
|
+
ctx.causeBudget--;
|
|
195
|
+
try {
|
|
196
|
+
return walk(cause, depth, ctx);
|
|
197
|
+
} finally {
|
|
198
|
+
ctx.causeBudget++;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
export {
|
|
202
|
+
RAW_ERROR_BOUNDS,
|
|
203
|
+
resolveRawErrorLimits,
|
|
204
|
+
serializeRawError
|
|
205
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// src/utils/sms-otp-operation-full.ts
|
|
2
|
+
import { OTPInputBinding } from "../low-events/otp-input-binding.mjs";
|
|
3
|
+
import { OperationFull } from "./operation-full.mjs";
|
|
4
|
+
var SmsOtpOperationFull = class extends OperationFull {
|
|
5
|
+
lowEventTracker;
|
|
6
|
+
send;
|
|
7
|
+
postResponse;
|
|
8
|
+
resend;
|
|
9
|
+
constructor(tracker, config = {}) {
|
|
10
|
+
super(tracker, "sms-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 });
|
|
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
|
+
SmsOtpOperationFull
|
|
39
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// src/utils/social-operation-login.ts
|
|
2
|
+
import { OperationFull } from "./operation-full.mjs";
|
|
3
|
+
var SocialLoginOperationFull = class extends OperationFull {
|
|
4
|
+
getRedirectUrl;
|
|
5
|
+
exchangeCode;
|
|
6
|
+
constructor(tracker, config = {}) {
|
|
7
|
+
super(tracker, "social-login");
|
|
8
|
+
this.getRedirectUrl = this.defineStep(
|
|
9
|
+
"get-redirect-url"
|
|
10
|
+
);
|
|
11
|
+
this.exchangeCode = this.defineStep("exchange-code");
|
|
12
|
+
void this.subflowStart({ explicitSpecType: config.explicitSpecType, entry: config.entry });
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
export {
|
|
16
|
+
SocialLoginOperationFull
|
|
17
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// src/utils/source-path.ts
|
|
2
|
+
var MASKED_QUERY_VALUE = "*";
|
|
3
|
+
function decodeKey(raw) {
|
|
4
|
+
try {
|
|
5
|
+
return decodeURIComponent(raw.replace(/\+/g, " "));
|
|
6
|
+
} catch {
|
|
7
|
+
return raw;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
function isAllowed(key, allow) {
|
|
11
|
+
const lower = key.toLowerCase();
|
|
12
|
+
return allow.some((pattern) => {
|
|
13
|
+
const entry = pattern.toLowerCase();
|
|
14
|
+
return entry.endsWith("*") ? lower.startsWith(entry.slice(0, -1)) : lower === entry;
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
function applySourcePathQuery(search, mode, allow) {
|
|
18
|
+
if (mode !== "allowlist" || search === "" || search === "?") {
|
|
19
|
+
return search;
|
|
20
|
+
}
|
|
21
|
+
const query = search.startsWith("?") ? search.slice(1) : search;
|
|
22
|
+
const masked = query.split("&").map((pair) => {
|
|
23
|
+
const separator = pair.indexOf("=");
|
|
24
|
+
if (separator === -1) {
|
|
25
|
+
return pair;
|
|
26
|
+
}
|
|
27
|
+
const key = pair.slice(0, separator);
|
|
28
|
+
const value = pair.slice(separator + 1);
|
|
29
|
+
if (value === "" || isAllowed(decodeKey(key), allow)) {
|
|
30
|
+
return pair;
|
|
31
|
+
}
|
|
32
|
+
return `${key}=${MASKED_QUERY_VALUE}`;
|
|
33
|
+
});
|
|
34
|
+
return `?${masked.join("&")}`;
|
|
35
|
+
}
|
|
36
|
+
export {
|
|
37
|
+
MASKED_QUERY_VALUE,
|
|
38
|
+
applySourcePathQuery
|
|
39
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// src/utils/sso-operation-login.ts
|
|
2
|
+
import { OperationFull } from "./operation-full.mjs";
|
|
3
|
+
var SsoLoginOperationFull = class extends OperationFull {
|
|
4
|
+
getRedirectUrl;
|
|
5
|
+
exchangeCode;
|
|
6
|
+
constructor(tracker, config = {}) {
|
|
7
|
+
super(tracker, "sso-login");
|
|
8
|
+
this.getRedirectUrl = this.defineStep(
|
|
9
|
+
"get-redirect-url"
|
|
10
|
+
);
|
|
11
|
+
this.exchangeCode = this.defineStep("exchange-code");
|
|
12
|
+
const { explicitSpecType, organizationDomain, actor } = validateConfig(config ?? {}, tracker);
|
|
13
|
+
void this.subflowStart({ explicitSpecType, organizationDomain, actor });
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
function validateConfig(config, tracker) {
|
|
17
|
+
const result = { ...config };
|
|
18
|
+
const warn = (field) => {
|
|
19
|
+
try {
|
|
20
|
+
tracker.getLogger().warn(`SSO login: unsupported ${field}; omitted from telemetry`);
|
|
21
|
+
} catch {
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
const domain = result.organizationDomain;
|
|
25
|
+
if (domain !== void 0 && (typeof domain !== "string" || domain.includes("@") || domain.length > 253)) {
|
|
26
|
+
delete result.organizationDomain;
|
|
27
|
+
warn("organizationDomain");
|
|
28
|
+
}
|
|
29
|
+
if (result.actor !== void 0 && result.actor !== "user" && result.actor !== "system") {
|
|
30
|
+
delete result.actor;
|
|
31
|
+
warn("actor");
|
|
32
|
+
}
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
export {
|
|
36
|
+
SsoLoginOperationFull
|
|
37
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// src/utils/totp-operation-full.ts
|
|
2
|
+
import { OTPInputBinding } from "../low-events/otp-input-binding.mjs";
|
|
3
|
+
import { OperationFull } from "./operation-full.mjs";
|
|
4
|
+
var TotpOperationFull = class extends OperationFull {
|
|
5
|
+
lowEventTracker;
|
|
6
|
+
postResponse;
|
|
7
|
+
constructor(tracker, config = {}) {
|
|
8
|
+
super(tracker, "totp");
|
|
9
|
+
this.postResponse = this.defineStep("post-response");
|
|
10
|
+
void this.subflowStart({ explicitSpecType: config.explicitSpecType });
|
|
11
|
+
this.lowEventTracker = this.createLowCollector(
|
|
12
|
+
() => new OTPInputBinding(
|
|
13
|
+
tracker.getLogger(),
|
|
14
|
+
this.lowEventSink("input", "otp"),
|
|
15
|
+
this.lowEventSink("input-group", "otp")
|
|
16
|
+
)
|
|
17
|
+
);
|
|
18
|
+
if (config.inputHtmlFields?.length) {
|
|
19
|
+
this.setInputFields(config.inputHtmlFields);
|
|
20
|
+
} else if (config.inputHtmlField) {
|
|
21
|
+
this.lowEventTracker?.setFields([config.inputHtmlField], false);
|
|
22
|
+
}
|
|
23
|
+
this.registerLowFlusher(() => this.lowEventTracker?.flushPending());
|
|
24
|
+
}
|
|
25
|
+
/** Rebind the ordered code input group without starting a new subflow. An empty list detaches it. */
|
|
26
|
+
setInputFields(fields) {
|
|
27
|
+
this.lowEventTracker?.setFields(fields);
|
|
28
|
+
}
|
|
29
|
+
destroy() {
|
|
30
|
+
this.lowEventTracker?.destroy();
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
export {
|
|
34
|
+
TotpOperationFull
|
|
35
|
+
};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// src/utils/trusted-device-operation-full.ts
|
|
2
|
+
import { normalizeError } from "./normalize-error.mjs";
|
|
3
|
+
import { OperationFull } from "./operation-full.mjs";
|
|
4
|
+
function stampIdentity(data, identity, tracker) {
|
|
5
|
+
if (data === null || typeof data !== "object") return data;
|
|
6
|
+
try {
|
|
7
|
+
const out = { ...data };
|
|
8
|
+
if (out.purpose === void 0 && identity.purpose !== void 0) out.purpose = identity.purpose;
|
|
9
|
+
if (out.storage === void 0 && identity.storage !== void 0) out.storage = identity.storage;
|
|
10
|
+
if (out.trustName === void 0 && identity.trustName !== void 0) out.trustName = identity.trustName;
|
|
11
|
+
for (const [field, limit] of [
|
|
12
|
+
["bindingReference", 255],
|
|
13
|
+
["trustName", 128]
|
|
14
|
+
]) {
|
|
15
|
+
if (typeof out[field] === "string" && [...out[field]].length > limit) {
|
|
16
|
+
delete out[field];
|
|
17
|
+
try {
|
|
18
|
+
tracker.getLogger().debug(`Trusted device: omitted ${field} exceeding ${limit} characters`);
|
|
19
|
+
} catch {
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return out;
|
|
24
|
+
} catch {
|
|
25
|
+
return data;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function withIdentity(step, identity, tracker) {
|
|
29
|
+
return {
|
|
30
|
+
...step,
|
|
31
|
+
start: (data, options) => step.start(stampIdentity(data, identity, tracker), options),
|
|
32
|
+
finished: (data, options) => step.finished(stampIdentity(data, identity, tracker), options)
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
var TrustedDeviceCheckOperationFull = class extends OperationFull {
|
|
36
|
+
getOptions;
|
|
37
|
+
ceremony;
|
|
38
|
+
postResponse;
|
|
39
|
+
constructor(tracker, config) {
|
|
40
|
+
super(tracker, "trusted-device-check");
|
|
41
|
+
config = validateConfig(config, tracker);
|
|
42
|
+
const identity = { purpose: config.purpose, storage: config.storage, trustName: config.trustName };
|
|
43
|
+
this.getOptions = this.checkStep("get-options", identity, tracker);
|
|
44
|
+
this.ceremony = this.checkStep("ceremony", identity, tracker);
|
|
45
|
+
this.postResponse = this.checkStep("post-response", identity, tracker);
|
|
46
|
+
this.subflowStart({ explicitSpecType: config.explicitSpecType, actor: "system", ignoreAsInteraction: true });
|
|
47
|
+
}
|
|
48
|
+
checkStep(name, identity, tracker) {
|
|
49
|
+
const step = this.defineStep(name, true);
|
|
50
|
+
return {
|
|
51
|
+
...withIdentity(step, identity, tracker),
|
|
52
|
+
errorTyped: (data, options) => {
|
|
53
|
+
step.errorTyped(
|
|
54
|
+
stampIdentity({ code: data?.code, error: normalizeError(data?.error) }, identity, tracker),
|
|
55
|
+
options
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
var TrustedDeviceEnrollmentOperationFull = class extends OperationFull {
|
|
62
|
+
getOptions;
|
|
63
|
+
ceremony;
|
|
64
|
+
postResponse;
|
|
65
|
+
constructor(tracker, config) {
|
|
66
|
+
super(tracker, "trusted-device-enrollment");
|
|
67
|
+
config = validateConfig(config, tracker);
|
|
68
|
+
const identity = { purpose: config.purpose, storage: config.storage, trustName: config.trustName };
|
|
69
|
+
this.getOptions = withIdentity(this.defineStep("get-options"), identity, tracker);
|
|
70
|
+
this.ceremony = withIdentity(this.defineStep("ceremony"), identity, tracker);
|
|
71
|
+
this.postResponse = withIdentity(
|
|
72
|
+
this.defineStep("post-response"),
|
|
73
|
+
identity,
|
|
74
|
+
tracker
|
|
75
|
+
);
|
|
76
|
+
this.subflowStart({ explicitSpecType: config.explicitSpecType });
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
function validateConfig(config, tracker) {
|
|
80
|
+
const result = { ...config };
|
|
81
|
+
const warn = (field) => {
|
|
82
|
+
try {
|
|
83
|
+
tracker.getLogger().warn(`Trusted device: missing or unsupported ${field}; omitted from telemetry`);
|
|
84
|
+
} catch {
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
if (result.purpose !== "additional-verification" && result.purpose !== "mfa-exemption") {
|
|
88
|
+
delete result.purpose;
|
|
89
|
+
warn("purpose");
|
|
90
|
+
}
|
|
91
|
+
if (!["cookie", "indexeddb", "localstore", "sessionstore"].includes(result.storage)) {
|
|
92
|
+
delete result.storage;
|
|
93
|
+
warn("storage");
|
|
94
|
+
}
|
|
95
|
+
return result;
|
|
96
|
+
}
|
|
97
|
+
export {
|
|
98
|
+
TrustedDeviceCheckOperationFull,
|
|
99
|
+
TrustedDeviceEnrollmentOperationFull
|
|
100
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// src/utils/web-lock.ts
|
|
2
|
+
function withWebLock(name, fn, options = {}) {
|
|
3
|
+
const locks = typeof navigator !== "undefined" ? navigator.locks : void 0;
|
|
4
|
+
if (locks?.request) {
|
|
5
|
+
const timeoutMs = options.timeoutMs;
|
|
6
|
+
const controller = timeoutMs !== void 0 && typeof AbortController !== "undefined" ? new AbortController() : null;
|
|
7
|
+
if (timeoutMs !== void 0 && !controller) {
|
|
8
|
+
return Promise.resolve().then(fn);
|
|
9
|
+
}
|
|
10
|
+
try {
|
|
11
|
+
let ran = false;
|
|
12
|
+
let timer;
|
|
13
|
+
const clearAcquisitionTimer = () => {
|
|
14
|
+
if (timer !== void 0) {
|
|
15
|
+
clearTimeout(timer);
|
|
16
|
+
timer = void 0;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
const callback = () => {
|
|
20
|
+
ran = true;
|
|
21
|
+
clearAcquisitionTimer();
|
|
22
|
+
return fn();
|
|
23
|
+
};
|
|
24
|
+
const granted = controller ? locks.request(name, { signal: controller.signal }, callback) : locks.request(name, callback);
|
|
25
|
+
if (controller && timeoutMs !== void 0 && !ran) {
|
|
26
|
+
timer = setTimeout(() => {
|
|
27
|
+
timer = void 0;
|
|
28
|
+
controller.abort();
|
|
29
|
+
try {
|
|
30
|
+
options.onTimeout?.();
|
|
31
|
+
} catch {
|
|
32
|
+
}
|
|
33
|
+
}, timeoutMs);
|
|
34
|
+
}
|
|
35
|
+
return granted.catch((error) => {
|
|
36
|
+
if (ran) {
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
return fn();
|
|
40
|
+
}).finally(clearAcquisitionTimer);
|
|
41
|
+
} catch {
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
return Promise.resolve(fn());
|
|
46
|
+
} catch (error) {
|
|
47
|
+
return Promise.reject(error);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export {
|
|
51
|
+
withWebLock
|
|
52
|
+
};
|