@loxel.dev/pharos-browser 0.6.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.
@@ -0,0 +1,844 @@
1
+ // src/privacy/mask.ts
2
+ var MASK_TOKEN = "••••";
3
+ function maskText(_text) {
4
+ return MASK_TOKEN;
5
+ }
6
+
7
+ // src/privacy/markers.ts
8
+ var EXCLUDE_CLASS = "pharos-exclude";
9
+ var MASK_CLASS = "pharos-mask";
10
+ function hasMarkerAncestor(el, cls) {
11
+ let node = el;
12
+ while (node) {
13
+ if (node.classList?.contains(cls))
14
+ return true;
15
+ node = node.parentElement;
16
+ }
17
+ return false;
18
+ }
19
+ function shouldExclude(el) {
20
+ return hasMarkerAncestor(el, EXCLUDE_CLASS);
21
+ }
22
+ function shouldMaskByMarker(el) {
23
+ return hasMarkerAncestor(el, MASK_CLASS);
24
+ }
25
+
26
+ // src/privacy/sensitivity.ts
27
+ var SENSITIVE_INPUT_TYPES = new Set(["password", "email", "tel"]);
28
+ var SENSITIVE_AUTOCOMPLETE = new Set([
29
+ "username",
30
+ "current-password",
31
+ "new-password",
32
+ "one-time-code",
33
+ "email",
34
+ "tel",
35
+ "given-name",
36
+ "family-name",
37
+ "name",
38
+ "street-address",
39
+ "address-line1",
40
+ "address-line2",
41
+ "address-line3",
42
+ "postal-code",
43
+ "cc-number",
44
+ "cc-exp",
45
+ "cc-csc",
46
+ "cc-name",
47
+ "bday",
48
+ "bday-day",
49
+ "bday-month",
50
+ "bday-year",
51
+ "sex"
52
+ ]);
53
+ var SENSITIVE_WORDS = [
54
+ "password",
55
+ "passwd",
56
+ "secret",
57
+ "token",
58
+ "otp",
59
+ "username",
60
+ "userid",
61
+ "email",
62
+ "mail",
63
+ "phone",
64
+ "tel",
65
+ "mobile",
66
+ "firstname",
67
+ "lastname",
68
+ "fullname",
69
+ "fname",
70
+ "lname",
71
+ "surname",
72
+ "maidenname",
73
+ "address",
74
+ "street",
75
+ "postcode",
76
+ "postalcode",
77
+ "zip",
78
+ "birthday",
79
+ "birthdate",
80
+ "dob",
81
+ "dateofbirth",
82
+ "ssn",
83
+ "socialsecurity",
84
+ "nationalid",
85
+ "taxid",
86
+ "creditcard",
87
+ "cardnumber",
88
+ "cvv",
89
+ "cvc",
90
+ "iban",
91
+ "passport",
92
+ "driverslicense"
93
+ ];
94
+ var NAME_ATTRS = ["name", "id", "aria-label"];
95
+ function normalize(value) {
96
+ return value.toLowerCase().replace(/[^a-z0-9]/g, "");
97
+ }
98
+ function isSensitiveField(el) {
99
+ if (el.tagName.toLowerCase() === "input") {
100
+ const type = (el.getAttribute("type") ?? "text").toLowerCase();
101
+ if (SENSITIVE_INPUT_TYPES.has(type))
102
+ return true;
103
+ }
104
+ const autocomplete = el.getAttribute("autocomplete");
105
+ if (autocomplete) {
106
+ for (const token of autocomplete.toLowerCase().split(/\s+/)) {
107
+ if (SENSITIVE_AUTOCOMPLETE.has(token))
108
+ return true;
109
+ }
110
+ }
111
+ for (const attr of NAME_ATTRS) {
112
+ const raw = el.getAttribute(attr);
113
+ if (!raw)
114
+ continue;
115
+ const normalized = normalize(raw);
116
+ if (SENSITIVE_WORDS.some((word) => normalized.includes(word)))
117
+ return true;
118
+ }
119
+ return false;
120
+ }
121
+
122
+ // src/privacy/attributes.ts
123
+ var URL_ATTRS = new Set([
124
+ "href",
125
+ "src",
126
+ "action",
127
+ "formaction",
128
+ "poster",
129
+ "xlink:href",
130
+ "data",
131
+ "ping",
132
+ "cite"
133
+ ]);
134
+ var VALUE_BEARING_ATTRS = new Set(["value", "placeholder", "title", "alt", "aria-label"]);
135
+ function scrubUrl(value) {
136
+ let url;
137
+ try {
138
+ url = new URL(value, document.baseURI);
139
+ } catch {
140
+ return MASK_TOKEN;
141
+ }
142
+ if (url.protocol !== "http:" && url.protocol !== "https:")
143
+ return MASK_TOKEN;
144
+ const wasAbsolute = /^[a-z][a-z0-9+.-]*:/i.test(value) || value.startsWith("//");
145
+ return wasAbsolute ? `${url.protocol}//${url.host}${url.pathname}` : url.pathname;
146
+ }
147
+ function scrubSrcset(value) {
148
+ const candidates = value.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
149
+ if (candidates.length === 0)
150
+ return MASK_TOKEN;
151
+ const scrubbed = [];
152
+ for (const candidate of candidates) {
153
+ const parts = candidate.split(/\s+/);
154
+ if (parts.length < 1 || parts.length > 2 || !parts[0])
155
+ return MASK_TOKEN;
156
+ const [url, descriptor] = parts;
157
+ const scrubbedUrl = scrubUrl(url);
158
+ scrubbed.push(descriptor ? `${scrubbedUrl} ${descriptor}` : scrubbedUrl);
159
+ }
160
+ return scrubbed.join(", ");
161
+ }
162
+ var CSS_URL_RE = /url\(\s*(['"]?)([^'")]*)\1\s*\)/gi;
163
+ var UNTERMINATED_URL_RE = /url\((?![^)]*\))/i;
164
+ var DECLARATION_RE = /^(\s*)([a-zA-Z-]+)(\s*:\s*)([\s\S]*?)(\s*)$/;
165
+ function findStringEnd(text, start, quote) {
166
+ let i = start + 1;
167
+ while (i < text.length) {
168
+ const ch = text[i];
169
+ if (ch === "\\") {
170
+ i += 2;
171
+ continue;
172
+ }
173
+ if (ch === quote)
174
+ return i;
175
+ i++;
176
+ }
177
+ return -1;
178
+ }
179
+ function splitDeclarations(value) {
180
+ const declarations = [];
181
+ let current = "";
182
+ let i = 0;
183
+ while (i < value.length) {
184
+ const ch = value[i];
185
+ if (ch === '"' || ch === "'") {
186
+ const end = findStringEnd(value, i, ch);
187
+ if (end === -1) {
188
+ current += value.slice(i);
189
+ i = value.length;
190
+ } else {
191
+ current += value.slice(i, end + 1);
192
+ i = end + 1;
193
+ }
194
+ } else if (ch === ";") {
195
+ declarations.push(current);
196
+ current = "";
197
+ i++;
198
+ } else {
199
+ current += ch;
200
+ i++;
201
+ }
202
+ }
203
+ declarations.push(current);
204
+ return declarations;
205
+ }
206
+ var QUOTED_VALUE_ALLOWLIST = new Set([
207
+ "font-family",
208
+ "font",
209
+ "grid-template-areas",
210
+ "grid-template"
211
+ ]);
212
+ function maskQuotedStrings(text, allowed) {
213
+ let result = "";
214
+ let i = 0;
215
+ while (i < text.length) {
216
+ if (/^url\(/i.test(text.slice(i))) {
217
+ const closeIdx = text.indexOf(")", i);
218
+ if (closeIdx === -1) {
219
+ result += MASK_TOKEN;
220
+ return result;
221
+ }
222
+ result += text.slice(i, closeIdx + 1);
223
+ i = closeIdx + 1;
224
+ continue;
225
+ }
226
+ const ch = text[i];
227
+ if (ch === '"' || ch === "'") {
228
+ const quote = ch;
229
+ const closeIdx = findStringEnd(text, i, quote);
230
+ if (closeIdx === -1) {
231
+ result += MASK_TOKEN;
232
+ return result;
233
+ }
234
+ result += allowed ? text.slice(i, closeIdx + 1) : `${quote}${MASK_TOKEN}${quote}`;
235
+ i = closeIdx + 1;
236
+ } else {
237
+ result += ch;
238
+ i++;
239
+ }
240
+ }
241
+ return result;
242
+ }
243
+ function scrubDeclaration(declaration) {
244
+ const match = DECLARATION_RE.exec(declaration);
245
+ if (!match) {
246
+ return maskQuotedStrings(declaration, false);
247
+ }
248
+ const [, leadingWs, name, sep, rawValue, trailingWs] = match;
249
+ const allowed = QUOTED_VALUE_ALLOWLIST.has(name.trim().toLowerCase());
250
+ return `${leadingWs}${name}${sep}${maskQuotedStrings(rawValue, allowed)}${trailingWs}`;
251
+ }
252
+ function scrubStyle(value) {
253
+ const withUrls = /url\(/i.test(value) ? value.replace(CSS_URL_RE, (_match, quote, inner) => `url(${quote}${scrubUrl(inner)}${quote})`) : value;
254
+ const cut = withUrls.search(UNTERMINATED_URL_RE);
255
+ if (cut !== -1) {
256
+ const head = splitDeclarations(withUrls.slice(0, cut)).map(scrubDeclaration).join(";");
257
+ return `${head}${MASK_TOKEN}`;
258
+ }
259
+ return splitDeclarations(withUrls).map(scrubDeclaration).join(";");
260
+ }
261
+ function scrubAttribute(el, name, value) {
262
+ const attr = name.toLowerCase();
263
+ if (attr === "style")
264
+ return scrubStyle(value);
265
+ if (attr === "srcset")
266
+ return scrubSrcset(value);
267
+ if (URL_ATTRS.has(attr))
268
+ return scrubUrl(value);
269
+ if (isSensitiveField(el) && (VALUE_BEARING_ATTRS.has(attr) || attr.startsWith("data-"))) {
270
+ return MASK_TOKEN;
271
+ }
272
+ return value;
273
+ }
274
+
275
+ // src/privacy/policy.ts
276
+ var NEVER_RECORDED_TAGS = new Set(["script"]);
277
+ function isAlwaysMaskedInput(el) {
278
+ if (el.tagName.toLowerCase() !== "input")
279
+ return false;
280
+ const type = (el.getAttribute("type") ?? "text").toLowerCase();
281
+ return type === "password" || type === "hidden";
282
+ }
283
+ function decide(el) {
284
+ if (NEVER_RECORDED_TAGS.has(el.tagName.toLowerCase()))
285
+ return "exclude";
286
+ if (shouldExclude(el))
287
+ return "exclude";
288
+ if (isAlwaysMaskedInput(el))
289
+ return "mask";
290
+ if (shouldMaskByMarker(el))
291
+ return "mask";
292
+ if (isSensitiveField(el))
293
+ return "mask";
294
+ return "record";
295
+ }
296
+ function isEditableByAttribute(el) {
297
+ let node = el;
298
+ while (node) {
299
+ const raw = node.getAttribute("contenteditable");
300
+ if (raw !== null) {
301
+ const state = raw.toLowerCase();
302
+ if (state === "true" || state === "" || state === "plaintext-only")
303
+ return true;
304
+ if (state === "false")
305
+ return false;
306
+ }
307
+ node = node.parentElement;
308
+ }
309
+ return false;
310
+ }
311
+ function masksValue(el) {
312
+ const tag = el.tagName.toLowerCase();
313
+ if (tag === "input" || tag === "textarea")
314
+ return true;
315
+ if (el.isContentEditable === true)
316
+ return true;
317
+ return isEditableByAttribute(el);
318
+ }
319
+
320
+ // src/replay/config.ts
321
+ var MAX_BUFFER_BYTES = 30 * 1024 * 1024;
322
+ var MIN_BUFFER_BYTES = 64 * 1024;
323
+ var RECORDER_DEFAULTS = {
324
+ preRollMs: 30000,
325
+ postRollMs: 1e4,
326
+ pointerHz: 20
327
+ };
328
+ function clamp(value, fallback, min, max) {
329
+ if (typeof value !== "number" || !Number.isFinite(value))
330
+ return fallback;
331
+ return Math.min(max, Math.max(min, value));
332
+ }
333
+ function clampMaxBytes(value) {
334
+ if (typeof value !== "number" || !Number.isFinite(value))
335
+ return MAX_BUFFER_BYTES;
336
+ if (value <= 0)
337
+ return MIN_BUFFER_BYTES;
338
+ return Math.min(MAX_BUFFER_BYTES, value);
339
+ }
340
+ function resolveConfig(opts) {
341
+ const preRollMs = clamp(opts.preRollMs, RECORDER_DEFAULTS.preRollMs, 1000, 300000);
342
+ const postRollMs = clamp(opts.postRollMs, RECORDER_DEFAULTS.postRollMs, 0, 120000);
343
+ const pointerHz = clamp(opts.pointerHz, RECORDER_DEFAULTS.pointerHz, 1, 60);
344
+ const maxBytes = clampMaxBytes(opts.maxBytes);
345
+ return {
346
+ preRollMs,
347
+ postRollMs,
348
+ maxBytes,
349
+ pointerHz,
350
+ mousemoveWaitMs: Math.round(1000 / pointerHz),
351
+ checkpointIntervalMs: Math.max(1000, Math.round(preRollMs / 2))
352
+ };
353
+ }
354
+
355
+ // src/replay/interactions.ts
356
+ var ACTIVATION_MODALITY_TTL_MS = 500;
357
+ var ACTIVATION_EVENT_TAG = "pharos-activation";
358
+ function resolveActivationMode(input) {
359
+ if (input.detail > 0)
360
+ return "pointer";
361
+ const fresh = input.modality !== null && input.at - input.modalityAt <= ACTIVATION_MODALITY_TTL_MS;
362
+ if (fresh && input.modality === "keyboard")
363
+ return "keyboard";
364
+ if (fresh && input.modality === "pointer")
365
+ return "pointer";
366
+ return "programmatic";
367
+ }
368
+ function attachInteractions(opts) {
369
+ const state = { modality: null, modalityAt: 0, pointerAt: Number.NEGATIVE_INFINITY, work: 0 };
370
+ let pointerIntervalMs = opts.pointerIntervalMs;
371
+ const onKeyDown = () => {
372
+ state.modality = "keyboard";
373
+ state.modalityAt = opts.now();
374
+ state.work += 1;
375
+ };
376
+ const onPointerDown = () => {
377
+ state.modality = "pointer";
378
+ state.modalityAt = opts.now();
379
+ state.work += 1;
380
+ };
381
+ const onPointerMove = () => {
382
+ const t = opts.now();
383
+ if (t - state.pointerAt < pointerIntervalMs)
384
+ return;
385
+ state.pointerAt = t;
386
+ state.modality = "pointer";
387
+ state.modalityAt = t;
388
+ state.work += 1;
389
+ };
390
+ const onClick = (event) => {
391
+ const detail = event.detail ?? 0;
392
+ opts.emit({
393
+ mode: resolveActivationMode({
394
+ modality: state.modality,
395
+ modalityAt: state.modalityAt,
396
+ at: opts.now(),
397
+ detail
398
+ })
399
+ });
400
+ };
401
+ opts.target.addEventListener("keydown", onKeyDown, true);
402
+ opts.target.addEventListener("pointerdown", onPointerDown, true);
403
+ opts.target.addEventListener("pointermove", onPointerMove, true);
404
+ opts.target.addEventListener("click", onClick, true);
405
+ return {
406
+ state,
407
+ setPointerIntervalMs(ms) {
408
+ pointerIntervalMs = ms;
409
+ },
410
+ detach() {
411
+ opts.target.removeEventListener("keydown", onKeyDown, true);
412
+ opts.target.removeEventListener("pointerdown", onPointerDown, true);
413
+ opts.target.removeEventListener("pointermove", onPointerMove, true);
414
+ opts.target.removeEventListener("click", onClick, true);
415
+ }
416
+ };
417
+ }
418
+
419
+ // src/replay/buffer.ts
420
+ var EVENT_TYPE_FULL_SNAPSHOT = 2;
421
+ var EVENT_TYPE_META = 4;
422
+ function beginsWithSnapshot(events) {
423
+ if (events.length === 0)
424
+ return false;
425
+ if (events[0].type === EVENT_TYPE_FULL_SNAPSHOT)
426
+ return true;
427
+ return events[0].type === EVENT_TYPE_META && events[1]?.type === EVENT_TYPE_FULL_SNAPSHOT;
428
+ }
429
+ var MAX_PENDING_META_AGE_MS = 1000;
430
+ function utf8Length(s) {
431
+ let bytes = 0;
432
+ for (let i = 0;i < s.length; i++) {
433
+ const code = s.charCodeAt(i);
434
+ if (code < 128) {
435
+ bytes += 1;
436
+ } else if (code < 2048) {
437
+ bytes += 2;
438
+ } else if (code >= 55296 && code <= 56319 && i + 1 < s.length) {
439
+ const low = s.charCodeAt(i + 1);
440
+ if (low >= 56320 && low <= 57343) {
441
+ bytes += 4;
442
+ i += 1;
443
+ continue;
444
+ }
445
+ bytes += 3;
446
+ } else {
447
+ bytes += 3;
448
+ }
449
+ }
450
+ return bytes;
451
+ }
452
+ function defaultSizeOf(event) {
453
+ return utf8Length(JSON.stringify(event));
454
+ }
455
+
456
+ class ReplayBuffer {
457
+ maxBytes;
458
+ preRollMs;
459
+ sizeOf;
460
+ onCeilingEviction;
461
+ onSegmentClosed;
462
+ segments = [];
463
+ pendingMeta = null;
464
+ newestTimestamp = 0;
465
+ totalBytes = 0;
466
+ evictedForAge = 0;
467
+ evictedForBytes = 0;
468
+ droppedBeforeFirstSnapshot = 0;
469
+ constructor(opts) {
470
+ this.maxBytes = opts.maxBytes;
471
+ this.preRollMs = opts.preRollMs;
472
+ this.sizeOf = opts.sizeOf ?? defaultSizeOf;
473
+ this.onCeilingEviction = opts.onCeilingEviction;
474
+ this.onSegmentClosed = opts.onSegmentClosed;
475
+ }
476
+ append(event) {
477
+ if (event.timestamp > this.newestTimestamp)
478
+ this.newestTimestamp = event.timestamp;
479
+ if (event.type === EVENT_TYPE_META) {
480
+ this.pendingMeta = event;
481
+ return;
482
+ }
483
+ if (event.type === EVENT_TYPE_FULL_SNAPSHOT) {
484
+ const meta = this.pendingMeta !== null && Math.abs(event.timestamp - this.pendingMeta.timestamp) <= MAX_PENDING_META_AGE_MS ? this.pendingMeta : null;
485
+ const events = meta ? [meta, event] : [event];
486
+ const bytes2 = events.reduce((sum, e) => sum + this.sizeOf(e), 0);
487
+ this.pendingMeta = null;
488
+ const closing = this.segments[this.segments.length - 1];
489
+ this.segments.push({ startedAt: event.timestamp, events, bytes: bytes2 });
490
+ this.totalBytes += bytes2;
491
+ this.evict();
492
+ if (closing)
493
+ this.onSegmentClosed?.([...closing.events], closing.startedAt);
494
+ return;
495
+ }
496
+ const current = this.segments[this.segments.length - 1];
497
+ if (!current) {
498
+ this.droppedBeforeFirstSnapshot += 1;
499
+ return;
500
+ }
501
+ const bytes = this.sizeOf(event);
502
+ current.events.push(event);
503
+ current.bytes += bytes;
504
+ this.totalBytes += bytes;
505
+ this.evict();
506
+ }
507
+ dropOldestSegment() {
508
+ const dropped = this.segments.shift();
509
+ if (dropped)
510
+ this.totalBytes -= dropped.bytes;
511
+ }
512
+ evict() {
513
+ while (this.segments.length > 1) {
514
+ const next = this.segments[1];
515
+ if (next.startedAt > this.newestTimestamp - this.preRollMs)
516
+ break;
517
+ this.dropOldestSegment();
518
+ this.evictedForAge += 1;
519
+ }
520
+ while (this.totalBytes > this.maxBytes && this.segments.length > 1) {
521
+ this.dropOldestSegment();
522
+ this.evictedForBytes += 1;
523
+ this.onCeilingEviction?.();
524
+ }
525
+ }
526
+ extract(triggeredAt, until) {
527
+ if (this.segments.length === 0)
528
+ return null;
529
+ const boundary = triggeredAt - this.preRollMs;
530
+ let start = 0;
531
+ for (let i = 0;i < this.segments.length; i++) {
532
+ if (this.segments[i].startedAt <= boundary)
533
+ start = i;
534
+ else
535
+ break;
536
+ }
537
+ const out = [];
538
+ for (let i = start;i < this.segments.length; i++) {
539
+ const segment = this.segments[i];
540
+ for (const event of segment.events) {
541
+ const isOpeningHead = i === start && (event.type === EVENT_TYPE_META || event.type === EVENT_TYPE_FULL_SNAPSHOT);
542
+ if (!isOpeningHead && event.timestamp > until)
543
+ continue;
544
+ out.push(event);
545
+ }
546
+ }
547
+ if (!beginsWithSnapshot(out))
548
+ return null;
549
+ return out;
550
+ }
551
+ currentSegment() {
552
+ const segment = this.segments[this.segments.length - 1];
553
+ if (!segment)
554
+ return null;
555
+ return { events: [...segment.events], startedAt: segment.startedAt };
556
+ }
557
+ clear() {
558
+ this.segments = [];
559
+ this.pendingMeta = null;
560
+ this.totalBytes = 0;
561
+ this.newestTimestamp = 0;
562
+ this.evictedForAge = 0;
563
+ this.evictedForBytes = 0;
564
+ this.droppedBeforeFirstSnapshot = 0;
565
+ }
566
+ stats() {
567
+ let events = 0;
568
+ for (const segment of this.segments)
569
+ events += segment.events.length;
570
+ return {
571
+ bytes: this.totalBytes,
572
+ segments: this.segments.length,
573
+ events,
574
+ evictedForAge: this.evictedForAge,
575
+ evictedForBytes: this.evictedForBytes,
576
+ droppedBeforeFirstSnapshot: this.droppedBeforeFirstSnapshot
577
+ };
578
+ }
579
+ }
580
+
581
+ // src/replay/drops.ts
582
+ function emptyBuckets() {
583
+ return {
584
+ oversized: 0,
585
+ delivery: 0,
586
+ quota: 0,
587
+ storage: 0,
588
+ "no-snapshot": 0,
589
+ "fabricated-window": 0
590
+ };
591
+ }
592
+ function createDropLedger() {
593
+ let buckets = emptyBuckets();
594
+ return {
595
+ note(reason) {
596
+ buckets[reason] += 1;
597
+ },
598
+ total() {
599
+ let sum = 0;
600
+ for (const key of Object.keys(buckets))
601
+ sum += buckets[key];
602
+ return sum;
603
+ },
604
+ byReason() {
605
+ return { ...buckets };
606
+ },
607
+ reset() {
608
+ buckets = emptyBuckets();
609
+ }
610
+ };
611
+ }
612
+
613
+ // src/replay/upload.ts
614
+ var TRANSPORT_MAX_BODY_BYTES = 8388608;
615
+ var TRANSPORT_HEADER_ALLOWANCE = 65536;
616
+ var MAX_WINDOW_BYTES = Math.floor((TRANSPORT_MAX_BODY_BYTES - TRANSPORT_HEADER_ALLOWANCE) * 3 / 4);
617
+ function encodeRaw(w) {
618
+ return new TextEncoder().encode(JSON.stringify(w));
619
+ }
620
+ async function encodeWindow(w) {
621
+ if (typeof CompressionStream === "undefined") {
622
+ return { body: encodeRaw(w), compressed: false };
623
+ }
624
+ const stream = new CompressionStream("gzip");
625
+ const writer = stream.writable.getWriter();
626
+ writer.write(encodeRaw(w));
627
+ writer.close();
628
+ const gzipped = await new Response(stream.readable).arrayBuffer();
629
+ return { body: new Uint8Array(gzipped), compressed: true };
630
+ }
631
+ function toBase64(bytes) {
632
+ const CHUNK = 32768;
633
+ let binary = "";
634
+ for (let i = 0;i < bytes.length; i += CHUNK) {
635
+ binary += String.fromCharCode(...bytes.subarray(i, i + CHUNK));
636
+ }
637
+ return btoa(binary);
638
+ }
639
+ function randomSessionId() {
640
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
641
+ return crypto.randomUUID();
642
+ }
643
+ return `sess-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
644
+ }
645
+ async function postEnvelope(body, meta, target) {
646
+ const fetchImpl = target.fetchImpl ?? fetch;
647
+ const res = await fetchImpl(target.endpoint, {
648
+ method: "POST",
649
+ headers: {
650
+ "Content-Type": "application/json",
651
+ Authorization: `Bearer ${target.appKey}`
652
+ },
653
+ body: JSON.stringify({ ...meta, blob: toBase64(body) })
654
+ });
655
+ return res.ok;
656
+ }
657
+ function envelopeMetaFor(window, sessionId, compressed, droppedWindows) {
658
+ return {
659
+ sessionId,
660
+ trigger: window.trigger,
661
+ degraded: window.degraded,
662
+ config: window.config,
663
+ compressed,
664
+ droppedWindows
665
+ };
666
+ }
667
+ async function encodeForUpload(window, compress) {
668
+ return compress === false ? { body: encodeRaw(window), compressed: false } : await encodeWindow(window);
669
+ }
670
+ function createUploadSink(opts) {
671
+ const sessionId = opts.sessionId && opts.sessionId.length > 0 ? opts.sessionId : randomSessionId();
672
+ const drops = opts.drops ?? createDropLedger();
673
+ const onFailure = async (window) => {
674
+ if (opts.onDeliveryFailure) {
675
+ try {
676
+ await opts.onDeliveryFailure(window);
677
+ return;
678
+ } catch {}
679
+ }
680
+ drops.note("delivery");
681
+ };
682
+ const uploadOne = async (window) => {
683
+ try {
684
+ const { body, compressed } = await encodeForUpload(window, opts.compress);
685
+ if (body.byteLength > MAX_WINDOW_BYTES) {
686
+ drops.note("oversized");
687
+ return;
688
+ }
689
+ const ok = await postEnvelope(body, envelopeMetaFor(window, sessionId, compressed, drops.total()), opts);
690
+ if (!ok)
691
+ await onFailure(window);
692
+ } catch {
693
+ await onFailure(window);
694
+ }
695
+ };
696
+ return (window) => {
697
+ const inflight = uploadOne(window);
698
+ opts.track?.(inflight);
699
+ };
700
+ }
701
+
702
+ // src/replay/persist.ts
703
+ var DB_NAME = "pharos-replay";
704
+ var DB_VERSION = 1;
705
+ var STORE_NAME = "windows";
706
+ var LOCAL_TTL_MS = 24 * 60 * 60 * 1000;
707
+ var defaultDrops = createDropLedger();
708
+ function quotaDroppedWindows() {
709
+ return defaultDrops.byReason().quota;
710
+ }
711
+ function resetQuotaDroppedWindows() {
712
+ defaultDrops.reset();
713
+ }
714
+ function defaultFactory() {
715
+ const factory = globalThis.indexedDB;
716
+ if (!factory) {
717
+ throw new Error("persist.ts: no IndexedDB available (pass an indexedDB factory explicitly)");
718
+ }
719
+ return factory;
720
+ }
721
+ function openDb(factory) {
722
+ return new Promise((resolve, reject) => {
723
+ const req = factory.open(DB_NAME, DB_VERSION);
724
+ req.onupgradeneeded = () => {
725
+ const db = req.result;
726
+ if (!db.objectStoreNames.contains(STORE_NAME)) {
727
+ db.createObjectStore(STORE_NAME, { keyPath: "id", autoIncrement: true });
728
+ }
729
+ };
730
+ req.onsuccess = () => resolve(req.result);
731
+ req.onerror = () => reject(req.error);
732
+ });
733
+ }
734
+ function addRecord(db, record) {
735
+ return new Promise((resolve, reject) => {
736
+ const tx = db.transaction(STORE_NAME, "readwrite");
737
+ const req = tx.objectStore(STORE_NAME).add(record);
738
+ req.onsuccess = () => resolve();
739
+ req.onerror = () => reject(req.error);
740
+ });
741
+ }
742
+ function getAllRecords(db) {
743
+ return new Promise((resolve, reject) => {
744
+ const tx = db.transaction(STORE_NAME, "readonly");
745
+ const req = tx.objectStore(STORE_NAME).getAll();
746
+ req.onsuccess = () => resolve(req.result);
747
+ req.onerror = () => reject(req.error);
748
+ });
749
+ }
750
+ function deleteRecord(db, id) {
751
+ return new Promise((resolve, reject) => {
752
+ const tx = db.transaction(STORE_NAME, "readwrite");
753
+ const req = tx.objectStore(STORE_NAME).delete(id);
754
+ req.onsuccess = () => resolve();
755
+ req.onerror = () => reject(req.error);
756
+ });
757
+ }
758
+ function clearStore(db) {
759
+ return new Promise((resolve, reject) => {
760
+ const tx = db.transaction(STORE_NAME, "readwrite");
761
+ const req = tx.objectStore(STORE_NAME).clear();
762
+ req.onsuccess = () => resolve();
763
+ req.onerror = () => reject(req.error);
764
+ });
765
+ }
766
+ function isQuotaExceeded(err) {
767
+ return err instanceof Object && err.name === "QuotaExceededError";
768
+ }
769
+ async function purgeExpired(db, now) {
770
+ const records = await getAllRecords(db);
771
+ const cutoff = now();
772
+ let purged = 0;
773
+ for (const rec of records) {
774
+ if (rec.id === undefined)
775
+ continue;
776
+ if (cutoff - rec.insertedAt > LOCAL_TTL_MS) {
777
+ await deleteRecord(db, rec.id);
778
+ purged += 1;
779
+ }
780
+ }
781
+ return purged;
782
+ }
783
+ async function persistWindow(body, meta, now = Date.now, indexedDB = defaultFactory(), drops = defaultDrops) {
784
+ const db = await openDb(indexedDB);
785
+ try {
786
+ await purgeExpired(db, now);
787
+ const record = { insertedAt: now(), body, meta };
788
+ try {
789
+ await addRecord(db, record);
790
+ } catch (err) {
791
+ if (!isQuotaExceeded(err))
792
+ throw err;
793
+ const existing = await getAllRecords(db);
794
+ if (existing.length > 0) {
795
+ const oldest = existing[0];
796
+ if (oldest.id !== undefined) {
797
+ await deleteRecord(db, oldest.id);
798
+ drops.note("quota");
799
+ }
800
+ }
801
+ await addRecord(db, record);
802
+ }
803
+ } finally {
804
+ db.close();
805
+ }
806
+ }
807
+ async function drainPersisted(send, now = Date.now, indexedDB = defaultFactory()) {
808
+ const db = await openDb(indexedDB);
809
+ try {
810
+ const records = await getAllRecords(db);
811
+ let drained = 0;
812
+ for (const rec of records) {
813
+ if (rec.id === undefined)
814
+ continue;
815
+ if (now() - rec.insertedAt > LOCAL_TTL_MS) {
816
+ await deleteRecord(db, rec.id);
817
+ continue;
818
+ }
819
+ let ok = false;
820
+ try {
821
+ ok = await send(rec.body, rec.meta);
822
+ } catch {
823
+ ok = false;
824
+ }
825
+ if (ok) {
826
+ await deleteRecord(db, rec.id);
827
+ drained += 1;
828
+ }
829
+ }
830
+ return drained;
831
+ } finally {
832
+ db.close();
833
+ }
834
+ }
835
+ async function clearPersisted(indexedDB = defaultFactory()) {
836
+ const db = await openDb(indexedDB);
837
+ try {
838
+ await clearStore(db);
839
+ } finally {
840
+ db.close();
841
+ }
842
+ }
843
+
844
+ export { MASK_TOKEN, maskText, EXCLUDE_CLASS, MASK_CLASS, shouldExclude, shouldMaskByMarker, isSensitiveField, scrubUrl, scrubAttribute, decide, masksValue, MAX_BUFFER_BYTES, RECORDER_DEFAULTS, resolveConfig, ACTIVATION_EVENT_TAG, attachInteractions, EVENT_TYPE_FULL_SNAPSHOT, EVENT_TYPE_META, beginsWithSnapshot, ReplayBuffer, createDropLedger, MAX_WINDOW_BYTES, encodeWindow, randomSessionId, postEnvelope, envelopeMetaFor, encodeForUpload, createUploadSink, LOCAL_TTL_MS, quotaDroppedWindows, resetQuotaDroppedWindows, isQuotaExceeded, persistWindow, drainPersisted, clearPersisted };