@corbado/observe 1.9.1 → 1.10.1
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 +183 -14
- package/dist/analysis/config.mjs +5 -5
- package/dist/analysis/cross-origin-session.mjs +422 -0
- package/dist/analysis/device.mjs +0 -79
- package/dist/analysis/low-events/grouped-input-tracker.mjs +2 -3
- package/dist/analysis/low-events/input-change-recorder.mjs +35 -12
- package/dist/analysis/low-events/input-tracker.mjs +0 -58
- package/dist/analysis/queue.mjs +9 -0
- package/dist/analysis/readers/network.mjs +5 -0
- package/dist/analysis/readers/page.mjs +4 -0
- package/dist/analysis/transports/fetch.mjs +16 -1
- package/dist/analysis/utils/operation-full.mjs +0 -1
- package/dist/analysis/version.mjs +1 -1
- package/dist/cdn/script.global.js +4 -4
- package/dist/index.d.mts +130 -30
- package/dist/index.d.ts +130 -30
- package/dist/index.js +4 -4
- package/dist/index.mjs +4 -4
- package/package.json +1 -1
- package/dist/analysis/readers/password-manager-dom.mjs +0 -112
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
// src/cross-origin-session.ts
|
|
2
|
+
import { readCookieScope } from "./readers/page.mjs";
|
|
3
|
+
import { cookieJar } from "./readers/storage.mjs";
|
|
4
|
+
var CROSS_ORIGIN_COOKIE_PREFIX = "cbo_xs";
|
|
5
|
+
var CROSS_ORIGIN_COOKIE_VERSION = "1";
|
|
6
|
+
var CROSS_ORIGIN_SEQ_RESERVE = 20;
|
|
7
|
+
var CROSS_ORIGIN_CONTEXT_MAX = 128;
|
|
8
|
+
var CONTEXT_RE = /^[A-Za-z0-9._~-]{1,128}$/;
|
|
9
|
+
var CROSS_ORIGIN_ACTIVITY_WRITE_INTERVAL_MS = 1e3;
|
|
10
|
+
var FUTURE_TOLERANCE_MS = 5 * 60 * 1e3;
|
|
11
|
+
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
12
|
+
var PROJECT_ID_RE = /^[A-Za-z0-9_-]{1,64}$/;
|
|
13
|
+
var DOMAIN_RE = /^[a-z0-9-]+(\.[a-z0-9-]+)+$/;
|
|
14
|
+
var INT_RE = /^\d{1,15}$/;
|
|
15
|
+
var CROSS_ORIGIN_MAX_SEQ_MARK = 1e9;
|
|
16
|
+
var CROSS_ORIGIN_FOREIGN_GAP = 1e3;
|
|
17
|
+
var CROSS_ORIGIN_MAX_SEQ_JUMP = 1e5;
|
|
18
|
+
function isValidCrossOriginContext(value) {
|
|
19
|
+
return typeof value === "string" && CONTEXT_RE.test(value);
|
|
20
|
+
}
|
|
21
|
+
function parseCrossOriginCookie(value) {
|
|
22
|
+
const parts = value.split(".");
|
|
23
|
+
const [version, sessionId, lastActiveAt, seqMark] = parts;
|
|
24
|
+
if (parts.length < 4 || version !== CROSS_ORIGIN_COOKIE_VERSION || !sessionId || !UUID_RE.test(sessionId) || !lastActiveAt || !INT_RE.test(lastActiveAt) || !seqMark || !INT_RE.test(seqMark) || Number(seqMark) > CROSS_ORIGIN_MAX_SEQ_MARK) {
|
|
25
|
+
return void 0;
|
|
26
|
+
}
|
|
27
|
+
const context = parts.length > 4 ? parts.slice(4).join(".") : void 0;
|
|
28
|
+
if (context !== void 0 && !isValidCrossOriginContext(context)) {
|
|
29
|
+
return void 0;
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
sessionId: sessionId.toLowerCase(),
|
|
33
|
+
lastActiveAt: Number(lastActiveAt),
|
|
34
|
+
seqMark: Number(seqMark),
|
|
35
|
+
...context !== void 0 && { context }
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function formatCrossOriginCookie(record) {
|
|
39
|
+
const parts = [
|
|
40
|
+
CROSS_ORIGIN_COOKIE_VERSION,
|
|
41
|
+
record.sessionId,
|
|
42
|
+
String(Math.floor(record.lastActiveAt)),
|
|
43
|
+
String(Math.floor(record.seqMark))
|
|
44
|
+
];
|
|
45
|
+
if (record.context !== void 0) {
|
|
46
|
+
parts.push(record.context);
|
|
47
|
+
}
|
|
48
|
+
return parts.join(".");
|
|
49
|
+
}
|
|
50
|
+
function resolveCrossOriginDomain(raw, hostname) {
|
|
51
|
+
const domain = raw.trim().toLowerCase().replace(/^\./, "");
|
|
52
|
+
const host = hostname.toLowerCase();
|
|
53
|
+
if (!DOMAIN_RE.test(domain)) {
|
|
54
|
+
return void 0;
|
|
55
|
+
}
|
|
56
|
+
return host === domain || host.endsWith(`.${domain}`) ? domain : void 0;
|
|
57
|
+
}
|
|
58
|
+
var CrossOriginSession = class _CrossOriginSession {
|
|
59
|
+
constructor(name, domain, jar, contextJar, deps) {
|
|
60
|
+
this.name = name;
|
|
61
|
+
this.domain = domain;
|
|
62
|
+
this.jar = jar;
|
|
63
|
+
this.contextJar = contextJar;
|
|
64
|
+
this.deps = deps;
|
|
65
|
+
}
|
|
66
|
+
enabled = true;
|
|
67
|
+
/** Session the in-memory reservation, highest seen mark and context belong to. */
|
|
68
|
+
reservedFor;
|
|
69
|
+
/** Seq this page may allocate up to (exclusive) without rewriting the cookie. */
|
|
70
|
+
reservedMark = 0;
|
|
71
|
+
/** Highest plausible mark of {@link reservedFor} this page has seen or written. */
|
|
72
|
+
highestSeen = 0;
|
|
73
|
+
lastWriteAt = 0;
|
|
74
|
+
/** Context of {@link reservedFor} as last known to this page. */
|
|
75
|
+
context;
|
|
76
|
+
/**
|
|
77
|
+
* The feature for this page, or `undefined` when it cannot work here (invalid project id or domain, a
|
|
78
|
+
* domain that is not the page's host or a parent of it, a non-HTTPS page, no document). Never throws.
|
|
79
|
+
*/
|
|
80
|
+
static create(deps) {
|
|
81
|
+
try {
|
|
82
|
+
if (typeof document === "undefined" || typeof window === "undefined") {
|
|
83
|
+
return void 0;
|
|
84
|
+
}
|
|
85
|
+
if (!PROJECT_ID_RE.test(deps.projectId)) {
|
|
86
|
+
deps.logger.debug("Cross-origin session off: project id is not cookie-safe");
|
|
87
|
+
return void 0;
|
|
88
|
+
}
|
|
89
|
+
const scope = deps.scope ? deps.scope() : readCookieScope({
|
|
90
|
+
key: "page:location.cookie-scope",
|
|
91
|
+
title: "Page host name for the shared session cookie",
|
|
92
|
+
accessNote: "location.hostname and location.protocol, compared with the configured shared cookie domain; not sent",
|
|
93
|
+
group: "Observe SDK state",
|
|
94
|
+
origin: "observe-external",
|
|
95
|
+
fields: [
|
|
96
|
+
{
|
|
97
|
+
path: "hostname",
|
|
98
|
+
label: "Host name of the page",
|
|
99
|
+
exposure: "none",
|
|
100
|
+
transform: { note: "only compared with the configured parent domain; never sent or stored" }
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
path: "https",
|
|
104
|
+
label: "Whether the page uses a secure connection",
|
|
105
|
+
exposure: "none",
|
|
106
|
+
transform: { note: "the shared cookie is only used on secure pages; never sent or stored" }
|
|
107
|
+
}
|
|
108
|
+
]
|
|
109
|
+
});
|
|
110
|
+
if (!scope.https) {
|
|
111
|
+
deps.logger.debug("Cross-origin session off: page is not served over HTTPS");
|
|
112
|
+
return void 0;
|
|
113
|
+
}
|
|
114
|
+
const domain = resolveCrossOriginDomain(deps.cookieDomain, scope.hostname);
|
|
115
|
+
if (!domain) {
|
|
116
|
+
deps.logger.warn(`Cross-origin session off: "${deps.cookieDomain}" is not the page's domain or a parent of it`);
|
|
117
|
+
deps.report("info", "cross-origin session: cookieDomain does not match the page");
|
|
118
|
+
return void 0;
|
|
119
|
+
}
|
|
120
|
+
const jar = deps.jar ?? cookieJar({
|
|
121
|
+
key: "storage:sdk.cross-origin-session",
|
|
122
|
+
title: "Observe session shared across your subdomains",
|
|
123
|
+
accessNote: "document.cookie, Observe's own cbo_xs_ cookie on the configured parent domain (Secure, SameSite Lax, Path /, expires after the session inactivity window); the browser sends it with every request to every subdomain of that domain",
|
|
124
|
+
group: "Observe SDK state",
|
|
125
|
+
origin: "observe-internal",
|
|
126
|
+
fields: [
|
|
127
|
+
{
|
|
128
|
+
path: "sessionId",
|
|
129
|
+
label: "Random Observe session id",
|
|
130
|
+
exposure: "token",
|
|
131
|
+
transform: {
|
|
132
|
+
pattern: "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$",
|
|
133
|
+
max: 36,
|
|
134
|
+
note: "generated by the SDK; sent to Corbado as the session id of every batch; the cookie carries it to every subdomain of the configured domain"
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
path: "lastActiveAt",
|
|
139
|
+
label: "Time of the last tracked activity",
|
|
140
|
+
exposure: "none",
|
|
141
|
+
transform: {
|
|
142
|
+
note: "decides whether the session is still active; not sent to Corbado, but the cookie carries it to every subdomain of the configured domain"
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
path: "seqMark",
|
|
147
|
+
label: "Event counter position",
|
|
148
|
+
exposure: "none",
|
|
149
|
+
transform: {
|
|
150
|
+
note: "the next page continues event numbering above it; not sent to Corbado, but the cookie carries it to every subdomain of the configured domain"
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
]
|
|
154
|
+
});
|
|
155
|
+
const contextJar = deps.jar ?? cookieJar({
|
|
156
|
+
key: "storage:sdk.cross-origin-context",
|
|
157
|
+
title: "Reference your integration keeps with the shared session",
|
|
158
|
+
accessNote: "document.cookie, the optional context part of Observe's cbo_xs_ cookie, set and read by the integration",
|
|
159
|
+
group: "Observe SDK state",
|
|
160
|
+
origin: "host-config",
|
|
161
|
+
fields: [
|
|
162
|
+
{
|
|
163
|
+
path: "context",
|
|
164
|
+
label: "Optional reference set by your integration",
|
|
165
|
+
exposure: "none",
|
|
166
|
+
transform: {
|
|
167
|
+
note: "opaque string of at most 128 cookie-safe characters chosen by the integration; not sent to Corbado, but the cookie carries it to every subdomain of the configured domain"
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
]
|
|
171
|
+
});
|
|
172
|
+
return new _CrossOriginSession(`${CROSS_ORIGIN_COOKIE_PREFIX}_${deps.projectId}`, domain, jar, contextJar, deps);
|
|
173
|
+
} catch (error) {
|
|
174
|
+
deps.logger.error("Cross-origin session setup failed", error);
|
|
175
|
+
return void 0;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/** Whether the cookie is in use on this page (false after a failed write check or {@link stop}). */
|
|
179
|
+
isEnabled() {
|
|
180
|
+
return this.enabled;
|
|
181
|
+
}
|
|
182
|
+
/** No cookie access from now on (tracker destroyed). */
|
|
183
|
+
stop() {
|
|
184
|
+
this.enabled = false;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Decide the session of this load before its first event and claim the cookie for it.
|
|
188
|
+
*
|
|
189
|
+
* `local.lastActiveAt` is the local session's last activity BEFORE this load (0 when the local session
|
|
190
|
+
* was created by this load). The cookie's session is adopted when the local one is new or was active
|
|
191
|
+
* earlier than the cookie's; the same session continues at or above the cookie's mark. A local session
|
|
192
|
+
* that was active more recently takes the cookie over. Never throws; `undefined` when the feature is off.
|
|
193
|
+
*/
|
|
194
|
+
boot(local, now = Date.now()) {
|
|
195
|
+
if (!this.enabled) {
|
|
196
|
+
return void 0;
|
|
197
|
+
}
|
|
198
|
+
try {
|
|
199
|
+
const cookie = this.read(this.jar, now);
|
|
200
|
+
let sessionId = local.sessionId;
|
|
201
|
+
let adopted = false;
|
|
202
|
+
if (cookie && cookie.sessionId !== sessionId && (!sessionId || local.lastActiveAt <= 0 || cookie.lastActiveAt > local.lastActiveAt)) {
|
|
203
|
+
sessionId = cookie.sessionId;
|
|
204
|
+
adopted = true;
|
|
205
|
+
}
|
|
206
|
+
let seq = adopted ? cookie?.seqMark ?? 0 : local.seq;
|
|
207
|
+
if (sessionId) {
|
|
208
|
+
const written = this.sync(this.jar, cookie, sessionId, seq, now, true, false);
|
|
209
|
+
seq = written.seq;
|
|
210
|
+
if (!this.isStored(written.value)) {
|
|
211
|
+
this.enabled = false;
|
|
212
|
+
this.deps.logger.debug("Cross-origin session off: the cookie could not be stored");
|
|
213
|
+
this.deps.report("info", "cross-origin session: cookie not stored");
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return { sessionId, seq, adopted };
|
|
217
|
+
} catch (error) {
|
|
218
|
+
this.enabled = false;
|
|
219
|
+
this.deps.logger.error("Cross-origin session boot failed", error);
|
|
220
|
+
this.deps.report("error", "cross-origin session: boot failed");
|
|
221
|
+
return void 0;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Called for every seq allocation with the seq about to be used; returns the seq to use instead. Reads
|
|
226
|
+
* the cookie on every call (a string scan).
|
|
227
|
+
*
|
|
228
|
+
* - Cookie of this session: continue above another page's reservation once one exists above this
|
|
229
|
+
* page's, renew the reservation when it is used up, otherwise refresh the activity at most once per
|
|
230
|
+
* {@link CROSS_ORIGIN_ACTIVITY_WRITE_INTERVAL_MS}. The mark is never lowered.
|
|
231
|
+
* - No (valid) cookie: written as for this session.
|
|
232
|
+
* - Cookie of another session: left alone (see the module notes); the seq stays above the highest mark
|
|
233
|
+
* this page saw for its session.
|
|
234
|
+
*
|
|
235
|
+
* Never throws.
|
|
236
|
+
*/
|
|
237
|
+
claim(sessionId, seq, now = Date.now()) {
|
|
238
|
+
if (!this.enabled || !sessionId) {
|
|
239
|
+
return seq;
|
|
240
|
+
}
|
|
241
|
+
try {
|
|
242
|
+
const cookie = this.read(this.jar, now);
|
|
243
|
+
this.follow(sessionId);
|
|
244
|
+
const same = cookie?.sessionId === sessionId;
|
|
245
|
+
const mark = same ? this.plausibleMark(cookie.seqMark, seq) : void 0;
|
|
246
|
+
if (mark !== void 0) {
|
|
247
|
+
this.highestSeen = Math.max(this.highestSeen, mark);
|
|
248
|
+
}
|
|
249
|
+
if (cookie && !same) {
|
|
250
|
+
if (seq < this.reservedMark) {
|
|
251
|
+
return seq;
|
|
252
|
+
}
|
|
253
|
+
const next = Math.max(seq, this.highestSeen) + CROSS_ORIGIN_FOREIGN_GAP;
|
|
254
|
+
this.reservedMark = next + CROSS_ORIGIN_FOREIGN_GAP;
|
|
255
|
+
this.highestSeen = this.reservedMark;
|
|
256
|
+
return next;
|
|
257
|
+
}
|
|
258
|
+
const renew = seq >= this.reservedMark || mark !== void 0 && mark > this.reservedMark;
|
|
259
|
+
if (!renew && now - this.lastWriteAt < CROSS_ORIGIN_ACTIVITY_WRITE_INTERVAL_MS) {
|
|
260
|
+
return seq;
|
|
261
|
+
}
|
|
262
|
+
return this.sync(this.jar, cookie, sessionId, seq, now, renew, false).seq;
|
|
263
|
+
} catch (error) {
|
|
264
|
+
this.deps.logger.error("Cross-origin session cookie update failed", error);
|
|
265
|
+
return seq;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
/** A new session of this page (`resetSession`): the cookie starts over with it, without context. */
|
|
269
|
+
reset(sessionId, now = Date.now()) {
|
|
270
|
+
if (!this.enabled || !sessionId) {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
try {
|
|
274
|
+
this.reservedFor = void 0;
|
|
275
|
+
this.follow(sessionId);
|
|
276
|
+
this.sync(this.jar, this.read(this.jar, now), sessionId, 0, now, true, true);
|
|
277
|
+
} catch (error) {
|
|
278
|
+
this.deps.logger.error("Cross-origin session cookie reset failed", error);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Store (or with `undefined` clear) the integration context of `sessionId`; this explicit call may take
|
|
283
|
+
* the cookie over from another session. `seq` is the page's best known next seq. Returns the next seq the
|
|
284
|
+
* page must continue at (a renewed reservation can move it up), or `undefined` when nothing was written.
|
|
285
|
+
*/
|
|
286
|
+
setContext(sessionId, seq, context, now = Date.now()) {
|
|
287
|
+
if (!this.enabled || !sessionId) {
|
|
288
|
+
return void 0;
|
|
289
|
+
}
|
|
290
|
+
try {
|
|
291
|
+
const cookie = this.read(this.contextJar, now);
|
|
292
|
+
this.follow(sessionId);
|
|
293
|
+
this.context = context;
|
|
294
|
+
return this.sync(this.contextJar, cookie, sessionId, seq, now, seq >= this.reservedMark, true).seq;
|
|
295
|
+
} catch (error) {
|
|
296
|
+
this.deps.logger.error("Cross-origin session context update failed", error);
|
|
297
|
+
return void 0;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
/** The integration context of `sessionId` (the cookie's when it holds that session, else this page's). */
|
|
301
|
+
getContext(sessionId, now = Date.now()) {
|
|
302
|
+
if (!this.enabled || !sessionId) {
|
|
303
|
+
return void 0;
|
|
304
|
+
}
|
|
305
|
+
try {
|
|
306
|
+
const cookie = this.read(this.contextJar, now);
|
|
307
|
+
if (cookie?.sessionId === sessionId) {
|
|
308
|
+
if (this.reservedFor === sessionId) {
|
|
309
|
+
this.context = cookie.context;
|
|
310
|
+
}
|
|
311
|
+
return cookie.context;
|
|
312
|
+
}
|
|
313
|
+
} catch (error) {
|
|
314
|
+
this.deps.logger.error("Cross-origin session context read failed", error);
|
|
315
|
+
}
|
|
316
|
+
return this.reservedFor === sessionId ? this.context : void 0;
|
|
317
|
+
}
|
|
318
|
+
/** Switch the in-memory state to `sessionId` (fresh reservation, nothing seen, no context) when it changed. */
|
|
319
|
+
follow(sessionId) {
|
|
320
|
+
if (this.reservedFor !== sessionId) {
|
|
321
|
+
this.reservedFor = sessionId;
|
|
322
|
+
this.reservedMark = 0;
|
|
323
|
+
this.highestSeen = 0;
|
|
324
|
+
this.context = void 0;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
/** `mark` when it is plausible next to the page's own counter `seq`; a jump beyond the cap is reported and ignored. */
|
|
328
|
+
plausibleMark(mark, seq) {
|
|
329
|
+
if (mark - seq > CROSS_ORIGIN_MAX_SEQ_JUMP) {
|
|
330
|
+
this.deps.report("info", "cross-origin session: implausible seq mark ignored");
|
|
331
|
+
return void 0;
|
|
332
|
+
}
|
|
333
|
+
return mark;
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Merge the cookie (already read) with this page's state and write it back for `sessionId`.
|
|
337
|
+
*
|
|
338
|
+
* - `renew`: allocate a new reservation from `seq` (jumping above the cookie's plausible mark of the same
|
|
339
|
+
* session and anything seen before). Otherwise the mark only grows.
|
|
340
|
+
* - `ownContext`: this page's context wins (it was just set); otherwise the cookie's context of the same
|
|
341
|
+
* session is taken over.
|
|
342
|
+
*/
|
|
343
|
+
sync(jar, cookie, sessionId, seq, now, renew, ownContext) {
|
|
344
|
+
this.follow(sessionId);
|
|
345
|
+
const same = cookie?.sessionId === sessionId;
|
|
346
|
+
const cookieMark = same ? this.plausibleMark(cookie.seqMark, seq) : void 0;
|
|
347
|
+
if (cookieMark !== void 0) {
|
|
348
|
+
this.highestSeen = Math.max(this.highestSeen, cookieMark);
|
|
349
|
+
}
|
|
350
|
+
if (same && !ownContext) {
|
|
351
|
+
this.context = cookie.context;
|
|
352
|
+
}
|
|
353
|
+
let next = seq;
|
|
354
|
+
let mark;
|
|
355
|
+
if (renew) {
|
|
356
|
+
next = Math.max(seq, cookieMark ?? 0, this.highestSeen);
|
|
357
|
+
mark = next + CROSS_ORIGIN_SEQ_RESERVE;
|
|
358
|
+
} else {
|
|
359
|
+
mark = Math.max(this.reservedMark, this.highestSeen, seq);
|
|
360
|
+
}
|
|
361
|
+
const value = formatCrossOriginCookie({
|
|
362
|
+
sessionId,
|
|
363
|
+
lastActiveAt: now,
|
|
364
|
+
seqMark: mark,
|
|
365
|
+
...this.context !== void 0 && { context: this.context }
|
|
366
|
+
});
|
|
367
|
+
this.write(jar, value);
|
|
368
|
+
if (renew) {
|
|
369
|
+
this.reservedMark = mark;
|
|
370
|
+
}
|
|
371
|
+
this.highestSeen = Math.max(this.highestSeen, mark);
|
|
372
|
+
this.lastWriteAt = now;
|
|
373
|
+
return { seq: next, value };
|
|
374
|
+
}
|
|
375
|
+
maxAgeSeconds() {
|
|
376
|
+
return Math.max(60, Math.ceil(this.deps.inactivityMs() / 1e3));
|
|
377
|
+
}
|
|
378
|
+
write(jar, value) {
|
|
379
|
+
jar.write(
|
|
380
|
+
`${this.name}=${value}; Domain=${this.domain}; Path=/; Max-Age=${this.maxAgeSeconds()}; SameSite=Lax; Secure`
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
/** Raw values of every cookie with this name (another path or a host-only copy can shadow it). */
|
|
384
|
+
rawValues(jar) {
|
|
385
|
+
const prefix = `${this.name}=`;
|
|
386
|
+
return jar.read().split(";").map((part) => part.trim()).filter((part) => part.startsWith(prefix)).map((part) => part.substring(prefix.length));
|
|
387
|
+
}
|
|
388
|
+
isStored(value) {
|
|
389
|
+
try {
|
|
390
|
+
return this.rawValues(this.jar).includes(value);
|
|
391
|
+
} catch {
|
|
392
|
+
return false;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
/** The valid, unexpired record with the latest activity, or `undefined`. Throws when the cookie read throws. */
|
|
396
|
+
read(jar, now) {
|
|
397
|
+
const inactivityMs = this.deps.inactivityMs();
|
|
398
|
+
let best;
|
|
399
|
+
for (const raw of this.rawValues(jar)) {
|
|
400
|
+
const record = parseCrossOriginCookie(raw);
|
|
401
|
+
if (record && now - record.lastActiveAt < inactivityMs && record.lastActiveAt <= now + FUTURE_TOLERANCE_MS && (!best || record.lastActiveAt > best.lastActiveAt)) {
|
|
402
|
+
best = record;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
return best;
|
|
406
|
+
}
|
|
407
|
+
};
|
|
408
|
+
export {
|
|
409
|
+
CROSS_ORIGIN_ACTIVITY_WRITE_INTERVAL_MS,
|
|
410
|
+
CROSS_ORIGIN_CONTEXT_MAX,
|
|
411
|
+
CROSS_ORIGIN_COOKIE_PREFIX,
|
|
412
|
+
CROSS_ORIGIN_COOKIE_VERSION,
|
|
413
|
+
CROSS_ORIGIN_FOREIGN_GAP,
|
|
414
|
+
CROSS_ORIGIN_MAX_SEQ_JUMP,
|
|
415
|
+
CROSS_ORIGIN_MAX_SEQ_MARK,
|
|
416
|
+
CROSS_ORIGIN_SEQ_RESERVE,
|
|
417
|
+
CrossOriginSession,
|
|
418
|
+
formatCrossOriginCookie,
|
|
419
|
+
isValidCrossOriginContext,
|
|
420
|
+
parseCrossOriginCookie,
|
|
421
|
+
resolveCrossOriginDomain
|
|
422
|
+
};
|
package/dist/analysis/device.mjs
CHANGED
|
@@ -7,7 +7,6 @@ import {
|
|
|
7
7
|
readConditionalMediationAvailable,
|
|
8
8
|
readJavaScriptHighEntropy,
|
|
9
9
|
readMaxTouchPoints,
|
|
10
|
-
readUserAgent,
|
|
11
10
|
readUserVerifyingPlatformAuthenticatorAvailable,
|
|
12
11
|
readWebdriver
|
|
13
12
|
} from "./readers/device.mjs";
|
|
@@ -515,84 +514,6 @@ var DeviceCollector = class {
|
|
|
515
514
|
this.logger.error("Failed to report device info timing", error);
|
|
516
515
|
}
|
|
517
516
|
}
|
|
518
|
-
/**
|
|
519
|
-
* Error situations:
|
|
520
|
-
* - navigator is not available: user agent is treated as empty, osName and browserName are "other"
|
|
521
|
-
* - navigator is available but userAgent is null or undefined: same as above
|
|
522
|
-
*/
|
|
523
|
-
static getOsAndBrowserInfo() {
|
|
524
|
-
return {
|
|
525
|
-
osName: this.getOsName(),
|
|
526
|
-
browserName: this.getBrowserName()
|
|
527
|
-
};
|
|
528
|
-
}
|
|
529
|
-
static getUserAgent() {
|
|
530
|
-
return readUserAgent({
|
|
531
|
-
key: "browser-api:device.user-agent",
|
|
532
|
-
title: "Browser and operating system",
|
|
533
|
-
accessNote: "navigator.userAgent",
|
|
534
|
-
group: "Device information",
|
|
535
|
-
origin: "observe-external",
|
|
536
|
-
fields: [
|
|
537
|
-
{
|
|
538
|
-
path: "result",
|
|
539
|
-
label: "Browser and operating system",
|
|
540
|
-
exposure: "class",
|
|
541
|
-
transform: {
|
|
542
|
-
classes: [
|
|
543
|
-
"iOS",
|
|
544
|
-
"Android",
|
|
545
|
-
"Windows",
|
|
546
|
-
"macOS",
|
|
547
|
-
"Linux",
|
|
548
|
-
"ChromeOS",
|
|
549
|
-
"Chrome",
|
|
550
|
-
"Firefox",
|
|
551
|
-
"Safari",
|
|
552
|
-
"Edge",
|
|
553
|
-
"Opera",
|
|
554
|
-
"IE",
|
|
555
|
-
"other"
|
|
556
|
-
]
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
]
|
|
560
|
-
});
|
|
561
|
-
}
|
|
562
|
-
static getOsName() {
|
|
563
|
-
const ua = this.getUserAgent();
|
|
564
|
-
if (ua.includes("iOS")) {
|
|
565
|
-
return "iOS";
|
|
566
|
-
} else if (ua.includes("Android")) {
|
|
567
|
-
return "Android";
|
|
568
|
-
} else if (ua.includes("Windows")) {
|
|
569
|
-
return "Windows";
|
|
570
|
-
} else if (ua.includes("Macintosh")) {
|
|
571
|
-
return "macOS";
|
|
572
|
-
} else if (ua.includes("Linux")) {
|
|
573
|
-
return "Linux";
|
|
574
|
-
} else if (ua.includes("ChromeOS")) {
|
|
575
|
-
return "ChromeOS";
|
|
576
|
-
}
|
|
577
|
-
return "other";
|
|
578
|
-
}
|
|
579
|
-
static getBrowserName() {
|
|
580
|
-
const ua = this.getUserAgent();
|
|
581
|
-
if (ua.includes("Chrome")) {
|
|
582
|
-
return "Chrome";
|
|
583
|
-
} else if (ua.includes("Firefox")) {
|
|
584
|
-
return "Firefox";
|
|
585
|
-
} else if (ua.includes("Safari")) {
|
|
586
|
-
return "Safari";
|
|
587
|
-
} else if (ua.includes("Edge")) {
|
|
588
|
-
return "Edge";
|
|
589
|
-
} else if (ua.includes("Opera")) {
|
|
590
|
-
return "Opera";
|
|
591
|
-
} else if (ua.includes("IE")) {
|
|
592
|
-
return "IE";
|
|
593
|
-
}
|
|
594
|
-
return "other";
|
|
595
|
-
}
|
|
596
517
|
getClientEnvHandle() {
|
|
597
518
|
const existingClientEnvHandle = this.storage.getItem("cbo_client_env_handle");
|
|
598
519
|
if (existingClientEnvHandle) {
|
|
@@ -99,7 +99,7 @@ var LowEventGroupedInputTracker = class {
|
|
|
99
99
|
this.composing ||= composing;
|
|
100
100
|
if (this.changeTimer !== null) return;
|
|
101
101
|
this.changeTime = Date.now();
|
|
102
|
-
this.
|
|
102
|
+
this.changes.observe();
|
|
103
103
|
this.changeTimer = setTimeout(() => this.safe(() => this.flushChanges()), 0);
|
|
104
104
|
},
|
|
105
105
|
focus: (relatedTarget) => {
|
|
@@ -174,7 +174,6 @@ var LowEventGroupedInputTracker = class {
|
|
|
174
174
|
listeners = [];
|
|
175
175
|
changeTimer = null;
|
|
176
176
|
changeTime = 0;
|
|
177
|
-
changeSeq;
|
|
178
177
|
composing = false;
|
|
179
178
|
destroyed = false;
|
|
180
179
|
contains(target) {
|
|
@@ -214,7 +213,7 @@ var LowEventGroupedInputTracker = class {
|
|
|
214
213
|
flushChanges() {
|
|
215
214
|
if (this.changeTimer === null) return;
|
|
216
215
|
this.cancelChange();
|
|
217
|
-
if (!this.paste.pending) this.changes.record(this.length(), this.changeTime, this.composing
|
|
216
|
+
if (!this.paste.pending) this.changes.record(this.length(), this.changeTime, this.composing);
|
|
218
217
|
this.composing = false;
|
|
219
218
|
}
|
|
220
219
|
emit(lowType) {
|
|
@@ -18,9 +18,28 @@ var InputChangeRecorder = class {
|
|
|
18
18
|
autofillTimestamps = [];
|
|
19
19
|
autofillStreak = null;
|
|
20
20
|
idleTimer = null;
|
|
21
|
+
/** Order of the pending run of changes: reserved when it starts, taken by the first event it sends. */
|
|
22
|
+
runSeq;
|
|
21
23
|
markInputObserved() {
|
|
22
24
|
this.prefillSettled = true;
|
|
23
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* Observation of a change begins: reserves the capture order of a new run of changes. One number per
|
|
28
|
+
* run, not per change: every reserved number is sent, so the gaps between sent events never count
|
|
29
|
+
* keystrokes (which would reveal the length of the typed value).
|
|
30
|
+
*/
|
|
31
|
+
observe() {
|
|
32
|
+
if (this.runSeq !== void 0 || this.inputBatch || this.autofillTimestamps.length > 0 || this.autofillStreak) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
this.runSeq = this.reserveCaptureSeq?.();
|
|
36
|
+
}
|
|
37
|
+
/** The run's reserved order for its first event; later events of the run reserve when they are created. */
|
|
38
|
+
takeSeq() {
|
|
39
|
+
const seq = this.runSeq;
|
|
40
|
+
this.runSeq = void 0;
|
|
41
|
+
return seq ?? this.reserveCaptureSeq?.();
|
|
42
|
+
}
|
|
24
43
|
capturePrefill(length, newCapture = false) {
|
|
25
44
|
if (newCapture) this.prefillSettled = false;
|
|
26
45
|
if (this.prefillSettled) return;
|
|
@@ -50,17 +69,17 @@ var InputChangeRecorder = class {
|
|
|
50
69
|
}
|
|
51
70
|
}, INPUT_IDLE_MS);
|
|
52
71
|
}
|
|
53
|
-
extendInputBatch(timestamp, effect = this.currentEffect
|
|
72
|
+
extendInputBatch(timestamp, effect = this.currentEffect) {
|
|
54
73
|
const batch = this.inputBatch;
|
|
55
74
|
if (!batch) {
|
|
56
|
-
this.inputBatch = { firstTimestamp: timestamp, lastTimestamp: timestamp, effect, captureSeq };
|
|
75
|
+
this.inputBatch = { firstTimestamp: timestamp, lastTimestamp: timestamp, effect, captureSeq: this.takeSeq() };
|
|
57
76
|
return;
|
|
58
77
|
}
|
|
59
78
|
batch.lastTimestamp = timestamp;
|
|
60
79
|
}
|
|
61
80
|
drainAutofillRingToBatch() {
|
|
62
81
|
for (const ts of this.autofillTimestamps) {
|
|
63
|
-
this.extendInputBatch(ts
|
|
82
|
+
this.extendInputBatch(ts, this.ringEffect);
|
|
64
83
|
}
|
|
65
84
|
this.autofillTimestamps.length = 0;
|
|
66
85
|
}
|
|
@@ -101,7 +120,7 @@ var InputChangeRecorder = class {
|
|
|
101
120
|
let minInterval = Infinity;
|
|
102
121
|
let maxInterval = -Infinity;
|
|
103
122
|
for (let i = 1; i < ring.length; i++) {
|
|
104
|
-
const interval = ring[i]
|
|
123
|
+
const interval = ring[i] - ring[i - 1];
|
|
105
124
|
if (interval > AUTOFILL_FAST_INTERVAL_MS) {
|
|
106
125
|
return false;
|
|
107
126
|
}
|
|
@@ -114,7 +133,8 @@ var InputChangeRecorder = class {
|
|
|
114
133
|
}
|
|
115
134
|
return maxInterval - minInterval <= AUTOFILL_UNIFORM_SPREAD_MS;
|
|
116
135
|
}
|
|
117
|
-
record(newLength, timestamp = Date.now(), composing = false
|
|
136
|
+
record(newLength, timestamp = Date.now(), composing = false) {
|
|
137
|
+
this.observe();
|
|
118
138
|
this.markInputObserved();
|
|
119
139
|
this.scheduleIdle();
|
|
120
140
|
const oldLength = this.previousLength;
|
|
@@ -123,6 +143,7 @@ var InputChangeRecorder = class {
|
|
|
123
143
|
this.previousLength = newLength;
|
|
124
144
|
if (newLength === 0 && oldLength > 0) {
|
|
125
145
|
this.flush();
|
|
146
|
+
const captureSeq = this.takeSeq();
|
|
126
147
|
this.pushLow({
|
|
127
148
|
...captureSeq === void 0 ? {} : { captureSeq },
|
|
128
149
|
lowType: "input-clear",
|
|
@@ -134,12 +155,13 @@ var InputChangeRecorder = class {
|
|
|
134
155
|
if (composing) {
|
|
135
156
|
this.flushAutofillStreak();
|
|
136
157
|
this.drainAutofillRingToBatch();
|
|
137
|
-
this.extendInputBatch(timestamp, this.currentEffect
|
|
158
|
+
this.extendInputBatch(timestamp, this.currentEffect);
|
|
138
159
|
return;
|
|
139
160
|
}
|
|
140
161
|
if (delta >= BIG_INPUT_DELTA_THRESHOLD) {
|
|
141
162
|
this.flushInputBatch();
|
|
142
163
|
this.flushAutofillStreak();
|
|
164
|
+
const captureSeq = this.takeSeq();
|
|
143
165
|
this.pushLow({
|
|
144
166
|
...captureSeq === void 0 ? {} : { captureSeq },
|
|
145
167
|
lowType: "big-input-add",
|
|
@@ -151,6 +173,7 @@ var InputChangeRecorder = class {
|
|
|
151
173
|
if (delta <= -BIG_INPUT_DELTA_THRESHOLD) {
|
|
152
174
|
this.flushInputBatch();
|
|
153
175
|
this.flushAutofillStreak();
|
|
176
|
+
const captureSeq = this.takeSeq();
|
|
154
177
|
this.pushLow({
|
|
155
178
|
...captureSeq === void 0 ? {} : { captureSeq },
|
|
156
179
|
lowType: "big-input-rem",
|
|
@@ -169,15 +192,15 @@ var InputChangeRecorder = class {
|
|
|
169
192
|
}
|
|
170
193
|
if (delta !== 1) {
|
|
171
194
|
this.drainAutofillRingToBatch();
|
|
172
|
-
this.extendInputBatch(timestamp, this.currentEffect
|
|
195
|
+
this.extendInputBatch(timestamp, this.currentEffect);
|
|
173
196
|
return;
|
|
174
197
|
}
|
|
175
198
|
const ring = this.autofillTimestamps;
|
|
176
199
|
if (ring.length === 0) this.ringEffect = this.currentEffect;
|
|
177
|
-
ring.push(
|
|
200
|
+
ring.push(timestamp);
|
|
178
201
|
if (ring.length > AUTOFILL_STREAK_LENGTH) {
|
|
179
202
|
const graduated = ring.shift();
|
|
180
|
-
this.extendInputBatch(graduated
|
|
203
|
+
this.extendInputBatch(graduated, this.ringEffect);
|
|
181
204
|
this.ringEffect = "modify";
|
|
182
205
|
}
|
|
183
206
|
if (!this.detectAutofillStreak()) {
|
|
@@ -188,9 +211,9 @@ var InputChangeRecorder = class {
|
|
|
188
211
|
ring.length = 0;
|
|
189
212
|
this.flushInputBatch();
|
|
190
213
|
this.autofillStreak = {
|
|
191
|
-
firstTimestamp: streakStart
|
|
192
|
-
captureSeq:
|
|
193
|
-
lastTimestamp: streakLast
|
|
214
|
+
firstTimestamp: streakStart,
|
|
215
|
+
captureSeq: this.takeSeq(),
|
|
216
|
+
lastTimestamp: streakLast,
|
|
194
217
|
effect: this.ringEffect
|
|
195
218
|
};
|
|
196
219
|
}
|