@reticlehq/browser 3.2.0 → 3.3.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/actions/actions-dom.d.ts +1 -1
- package/dist/actions/actions-dom.js +5 -4
- package/dist/actions/actions.d.ts +0 -1
- package/dist/actions/actions.js +4 -2
- package/dist/actions/value-input.js +14 -5
- package/dist/connect-options.d.ts +1 -0
- package/dist/dom/addressing/source.d.ts +2 -1
- package/dist/dom/dom-ignore.d.ts +1 -0
- package/dist/dom/dom-ignore.js +13 -0
- package/dist/dom/query.js +6 -4
- package/dist/dom/snapshot.d.ts +1 -1
- package/dist/observers/console.js +38 -13
- package/dist/observers/dom.js +3 -1
- package/dist/observers/download.js +3 -2
- package/dist/observers/error-repeats.d.ts +18 -0
- package/dist/observers/error-repeats.js +41 -0
- package/dist/observers/field.d.ts +2 -0
- package/dist/observers/field.js +86 -0
- package/dist/observers/frames.js +3 -4
- package/dist/observers/install-all.js +2 -0
- package/dist/observers/net-detail/network-body.d.ts +2 -0
- package/dist/observers/net-detail/network-body.js +24 -7
- package/dist/observers/route.js +4 -4
- package/dist/observers/state.js +18 -8
- package/dist/presenter/carousel/carousel.d.ts +13 -0
- package/dist/presenter/carousel/carousel.js +126 -0
- package/dist/presenter/carousel/offer-card.d.ts +21 -0
- package/dist/presenter/carousel/offer-card.js +55 -0
- package/dist/presenter/carousel/panel-slides.d.ts +7 -0
- package/dist/presenter/carousel/panel-slides.js +10 -0
- package/dist/presenter/carousel/talk-card.d.ts +8 -0
- package/dist/presenter/carousel/talk-card.js +20 -0
- package/dist/presenter/chrome/presenter-log.d.ts +2 -1
- package/dist/presenter/chrome/presenter-log.js +14 -3
- package/dist/presenter/chrome/presenter-report-copy.d.ts +0 -1
- package/dist/presenter/chrome/presenter-report-copy.js +1 -1
- package/dist/presenter/hud-telemetry.d.ts +3 -0
- package/dist/presenter/hud-telemetry.js +87 -0
- package/dist/presenter/presenter-account.js +1 -1
- package/dist/presenter/presenter-config.d.ts +5 -1
- package/dist/presenter/presenter-config.js +4 -1
- package/dist/presenter/presenter-controls.d.ts +1 -0
- package/dist/presenter/presenter-controls.js +12 -1
- package/dist/presenter/presenter-report.js +6 -6
- package/dist/presenter/presenter-settings-styles.js +2 -1
- package/dist/presenter/presenter-settings.d.ts +2 -4
- package/dist/presenter/presenter-settings.js +13 -4
- package/dist/presenter/presenter-shell-styles.js +4 -0
- package/dist/presenter/presenter-shell.d.ts +4 -1
- package/dist/presenter/presenter-shell.js +49 -11
- package/dist/presenter/presenter-styles.js +5 -1
- package/dist/presenter/presenter-workspace.d.ts +0 -7
- package/dist/presenter/presenter-workspace.js +7 -7
- package/dist/presenter/presenter.js +43 -10
- package/dist/presenter/tour/tour.d.ts +3 -0
- package/dist/presenter/tour/tour.js +5 -0
- package/dist/recorder/recorder.js +25 -22
- package/dist/reticle-inject.d.ts +2 -0
- package/dist/reticle-inject.js +1337 -0
- package/dist/reticle.js +6 -0
- package/dist/review/annotator.d.ts +3 -0
- package/dist/review/annotator.js +37 -37
- package/dist/review/marks-for-agent.d.ts +9 -0
- package/dist/review/marks-for-agent.js +12 -0
- package/dist/util/captured-method.d.ts +0 -1
- package/dist/util/captured-method.js +1 -1
- package/package.json +8 -4
- package/dist/presenter/presenter-offer.d.ts +0 -25
- package/dist/presenter/presenter-offer.js +0 -87
|
@@ -16,11 +16,12 @@ function pointerEventFor(el, type, init) {
|
|
|
16
16
|
? new PointerEvent(type, full)
|
|
17
17
|
: new MouseEvent(type, full);
|
|
18
18
|
}
|
|
19
|
-
export async function fireClickSequence(el, hold) {
|
|
19
|
+
export async function fireClickSequence(el, hold, detail) {
|
|
20
20
|
const doc = el.ownerDocument;
|
|
21
21
|
const from = doc.activeElement ?? doc.body;
|
|
22
|
+
const init = { bubbles: true, cancelable: true, detail: detail ?? 0 };
|
|
22
23
|
firePointer(el, 'pointerdown', from);
|
|
23
|
-
asSyntheticInput(() => el.dispatchEvent(mouseEventFor(el, 'mousedown',
|
|
24
|
+
asSyntheticInput(() => el.dispatchEvent(mouseEventFor(el, 'mousedown', init)));
|
|
24
25
|
if (el.tabIndex >= 0 && 'function' === typeof el.focus)
|
|
25
26
|
el.focus();
|
|
26
27
|
let heldMs = 0;
|
|
@@ -30,8 +31,8 @@ export async function fireClickSequence(el, hold) {
|
|
|
30
31
|
heldMs = hold.now() - startedAt;
|
|
31
32
|
}
|
|
32
33
|
firePointer(el, 'pointerup', from);
|
|
33
|
-
asSyntheticInput(() => el.dispatchEvent(mouseEventFor(el, 'mouseup',
|
|
34
|
-
const notPrevented = asSyntheticInput(() => el.dispatchEvent(mouseEventFor(el, 'click',
|
|
34
|
+
asSyntheticInput(() => el.dispatchEvent(mouseEventFor(el, 'mouseup', init)));
|
|
35
|
+
const notPrevented = asSyntheticInput(() => el.dispatchEvent(mouseEventFor(el, 'click', init)));
|
|
35
36
|
return { prevented: !notPrevented, heldMs };
|
|
36
37
|
}
|
|
37
38
|
function isMeasurable(rect) {
|
|
@@ -34,7 +34,6 @@ interface ActionResult {
|
|
|
34
34
|
};
|
|
35
35
|
warning?: string;
|
|
36
36
|
}
|
|
37
|
-
export declare const MAX_HOLD_MS = 30000;
|
|
38
37
|
export declare const ZOOM_NEEDS_REAL_BROWSER_MSG: string;
|
|
39
38
|
export declare function executeAction(ref: string, action: string, args?: Record<string, unknown>): Promise<ActionResult>;
|
|
40
39
|
export declare function dispatchWebMcp(tool: string, params: Record<string, unknown>, confirmDangerous?: boolean): Promise<unknown>;
|
package/dist/actions/actions.js
CHANGED
|
@@ -141,7 +141,7 @@ function activeRef(el) {
|
|
|
141
141
|
return null;
|
|
142
142
|
return refs.refFor(active);
|
|
143
143
|
}
|
|
144
|
-
|
|
144
|
+
const MAX_HOLD_MS = 30_000;
|
|
145
145
|
function clampHold(raw) {
|
|
146
146
|
if ('number' !== typeof raw || !Number.isFinite(raw) || raw <= 0)
|
|
147
147
|
return 0;
|
|
@@ -167,7 +167,9 @@ function asFiniteNumber(raw) {
|
|
|
167
167
|
async function dispatchOther(el, action, args) {
|
|
168
168
|
switch (action) {
|
|
169
169
|
case ActionType.DBLCLICK:
|
|
170
|
-
|
|
170
|
+
await fireClickSequence(el, undefined, 1);
|
|
171
|
+
await fireClickSequence(el, undefined, 2);
|
|
172
|
+
return !asSyntheticInput(() => el.dispatchEvent(mouseEventFor(el, 'dblclick', { bubbles: true, cancelable: true, detail: 2 })));
|
|
171
173
|
case ActionType.HOVER: {
|
|
172
174
|
const doc = el.ownerDocument;
|
|
173
175
|
const from = doc.activeElement ?? doc.body;
|
|
@@ -13,12 +13,21 @@ export function setNativeValue(el, value) {
|
|
|
13
13
|
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
14
14
|
return !notPrevented;
|
|
15
15
|
}
|
|
16
|
-
|
|
16
|
+
function isRichText(el) {
|
|
17
17
|
const flag = el.getAttribute('contenteditable');
|
|
18
|
-
if (el.isContentEditable || (
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
if (el.isContentEditable || (null !== flag && 'false' !== flag))
|
|
19
|
+
return true;
|
|
20
|
+
if ('textbox' === el.getAttribute('role'))
|
|
21
|
+
return true;
|
|
22
|
+
if (!('editContext' in el))
|
|
23
|
+
return false;
|
|
24
|
+
const surface = el;
|
|
25
|
+
return null !== surface.editContext && undefined !== surface.editContext;
|
|
26
|
+
}
|
|
27
|
+
export function assertNotRichText(el, action) {
|
|
28
|
+
if (!isRichText(el))
|
|
29
|
+
return;
|
|
30
|
+
throw new Error(`cannot ${action} a contenteditable or EditContext editor; use press or an input`);
|
|
22
31
|
}
|
|
23
32
|
export function assertEditable(el, action) {
|
|
24
33
|
if (el.disabled) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
interface SourceLocation {
|
|
2
2
|
file: string;
|
|
3
3
|
line: number;
|
|
4
4
|
}
|
|
@@ -7,3 +7,4 @@ export declare function sourceFromDom(el: Element): SourceLocation | undefined;
|
|
|
7
7
|
export declare function sourceFor(el: Element, adapterSource: SourceLocation | undefined): SourceLocation | undefined;
|
|
8
8
|
export declare function documentHasSourceStamps(doc: Document): boolean;
|
|
9
9
|
export declare function formatSource(source: SourceLocation | undefined): string | undefined;
|
|
10
|
+
export {};
|
package/dist/dom/dom-ignore.d.ts
CHANGED
|
@@ -5,3 +5,4 @@ export declare function setIgnoreSelectors(selectors: string[]): void;
|
|
|
5
5
|
export declare function isReticleOverlay(el: Element): boolean;
|
|
6
6
|
export declare function isReticleUi(node: Element | null): boolean;
|
|
7
7
|
export declare function isIgnored(el: Element): boolean;
|
|
8
|
+
export declare function appModalOpen(doc: Document): boolean;
|
package/dist/dom/dom-ignore.js
CHANGED
|
@@ -22,3 +22,16 @@ export function isIgnored(el) {
|
|
|
22
22
|
const sel = [ours, DEV_OVERLAYS, extraIgnore].filter((part) => part.length > 0).join(',');
|
|
23
23
|
return el.closest(sel) !== null;
|
|
24
24
|
}
|
|
25
|
+
const APP_MODAL = 'dialog[open], [aria-modal="true"]';
|
|
26
|
+
export function appModalOpen(doc) {
|
|
27
|
+
for (const el of Array.from(doc.querySelectorAll(APP_MODAL))) {
|
|
28
|
+
if (el.closest(RETICLE_OVERLAY) !== null)
|
|
29
|
+
continue;
|
|
30
|
+
if (el instanceof HTMLElement && (el.hidden || null !== el.closest('[hidden]')))
|
|
31
|
+
continue;
|
|
32
|
+
if (el instanceof HTMLElement && 'none' === getComputedStyle(el).display)
|
|
33
|
+
continue;
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
return false;
|
|
37
|
+
}
|
package/dist/dom/query.js
CHANGED
|
@@ -409,15 +409,16 @@ function buildEmptyHint(query) {
|
|
|
409
409
|
const container = resolveContainer(query.scope).container ?? document.body;
|
|
410
410
|
const all = container.querySelectorAll(`[${TESTID_ATTR}]`);
|
|
411
411
|
const present = [];
|
|
412
|
+
const seen = new Set();
|
|
412
413
|
for (const el of Array.from(all)) {
|
|
413
414
|
if (isIgnored(el))
|
|
414
415
|
continue;
|
|
415
416
|
const id = el.getAttribute(TESTID_ATTR);
|
|
416
|
-
if (id
|
|
417
|
+
if (null === id || 0 === id.length || seen.has(id))
|
|
418
|
+
continue;
|
|
419
|
+
seen.add(id);
|
|
420
|
+
if (present.length < MAX_PRESENT_TESTIDS)
|
|
417
421
|
present.push(id);
|
|
418
|
-
if (present.length >= MAX_PRESENT_TESTIDS)
|
|
419
|
-
break;
|
|
420
|
-
}
|
|
421
422
|
}
|
|
422
423
|
const registered = declaredTestids();
|
|
423
424
|
const knownEmptyState = present.some((id) => registered.includes(id));
|
|
@@ -425,6 +426,7 @@ function buildEmptyHint(query) {
|
|
|
425
426
|
const hint = {
|
|
426
427
|
route,
|
|
427
428
|
presentTestids: present,
|
|
429
|
+
...(seen.size > present.length ? { presentTestidsTotal: seen.size } : {}),
|
|
428
430
|
presentRegions: buildPresentRegions(query),
|
|
429
431
|
knownEmptyState,
|
|
430
432
|
};
|
package/dist/dom/snapshot.d.ts
CHANGED
|
@@ -2,6 +2,9 @@ import { EventType, TRANSPORT_LIMITS } from '@reticlehq/core';
|
|
|
2
2
|
import { observeSafely } from './types.js';
|
|
3
3
|
import { safeStringify } from '../security/serialization.js';
|
|
4
4
|
import { requireCapturedMethod } from '../util/captured-method.js';
|
|
5
|
+
import { createRepeatLimiter } from './error-repeats.js';
|
|
6
|
+
const CSP_VIOLATION_EVENT = 'securitypolicyviolation';
|
|
7
|
+
const REPORT_ONLY = 'report';
|
|
5
8
|
const METHOD_EVENT = {
|
|
6
9
|
log: EventType.CONSOLE_LOG,
|
|
7
10
|
warn: EventType.CONSOLE_WARN,
|
|
@@ -54,6 +57,16 @@ export function installConsole(emit) {
|
|
|
54
57
|
patched.set(method, wrapper);
|
|
55
58
|
console[method] = wrapper;
|
|
56
59
|
}
|
|
60
|
+
const repeats = createRepeatLimiter();
|
|
61
|
+
const emitError = (payload) => {
|
|
62
|
+
const decision = repeats.admit(payload);
|
|
63
|
+
if (!decision.emit)
|
|
64
|
+
return;
|
|
65
|
+
emit(EventType.ERROR_UNCAUGHT, {
|
|
66
|
+
...payload,
|
|
67
|
+
...(undefined === decision.repeats ? {} : { repeats: decision.repeats }),
|
|
68
|
+
});
|
|
69
|
+
};
|
|
57
70
|
const onError = (event) => {
|
|
58
71
|
if (!(event instanceof ErrorEvent)) {
|
|
59
72
|
const target = event.target;
|
|
@@ -62,43 +75,55 @@ export function installConsole(emit) {
|
|
|
62
75
|
const tag = target.tagName.toLowerCase();
|
|
63
76
|
const url = target.getAttribute('src') ?? target.getAttribute('href') ?? '';
|
|
64
77
|
observeSafely(() => {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
kind: 'resource',
|
|
68
|
-
...(0 === url.length ? {} : { source: url }),
|
|
69
|
-
});
|
|
78
|
+
const message = `<${tag}> failed to load${0 === url.length ? '' : `: ${url}`}`;
|
|
79
|
+
emitError({ message, kind: 'resource', ...(0 === url.length ? {} : { source: url }) });
|
|
70
80
|
});
|
|
71
81
|
return;
|
|
72
82
|
}
|
|
73
83
|
observeSafely(() => {
|
|
74
84
|
const stack = capStack(event.error instanceof Error ? event.error.stack : undefined);
|
|
75
|
-
|
|
85
|
+
const payload = {
|
|
76
86
|
message: event.message,
|
|
77
87
|
source: event.filename,
|
|
78
88
|
line: event.lineno,
|
|
79
89
|
...(stack === undefined ? {} : { stack }),
|
|
80
|
-
}
|
|
90
|
+
};
|
|
91
|
+
emitError(payload);
|
|
81
92
|
});
|
|
82
93
|
};
|
|
83
94
|
const onRejection = (event) => {
|
|
84
95
|
observeSafely(() => {
|
|
85
96
|
const reason = event.reason;
|
|
86
97
|
const stack = capStack(reason instanceof Error ? reason.stack : undefined);
|
|
87
|
-
|
|
98
|
+
const payload = {
|
|
88
99
|
message: reason instanceof Error ? reason.message : String(reason),
|
|
89
100
|
kind: 'unhandledrejection',
|
|
90
101
|
...(stack === undefined ? {} : { stack }),
|
|
91
|
-
}
|
|
102
|
+
};
|
|
103
|
+
emitError(payload);
|
|
104
|
+
});
|
|
105
|
+
};
|
|
106
|
+
const onViolation = (event) => {
|
|
107
|
+
const what = 0 === event.blockedURI.length ? 'inline content' : event.blockedURI;
|
|
108
|
+
const reportOnly = REPORT_ONLY === event.disposition;
|
|
109
|
+
emit(reportOnly ? EventType.CONSOLE_WARN : EventType.CONSOLE_ERROR, {
|
|
110
|
+
message: reportOnly
|
|
111
|
+
? `[Report Only] Content Security Policy directive: ${event.violatedDirective} would block ${what}`
|
|
112
|
+
: `Content Security Policy directive: ${event.violatedDirective} blocked ${what}`,
|
|
113
|
+
kind: CSP_VIOLATION_EVENT,
|
|
114
|
+
...(0 === event.blockedURI.length ? {} : { source: event.blockedURI }),
|
|
92
115
|
});
|
|
93
116
|
};
|
|
94
|
-
|
|
95
|
-
|
|
117
|
+
const listeners = new AbortController();
|
|
118
|
+
const { signal } = listeners;
|
|
119
|
+
window.addEventListener('error', onError, { capture: true, signal });
|
|
120
|
+
window.addEventListener('unhandledrejection', onRejection, { signal });
|
|
121
|
+
window.addEventListener(CSP_VIOLATION_EVENT, onViolation, { capture: true, signal });
|
|
96
122
|
return () => {
|
|
97
123
|
for (const [method, original] of originals) {
|
|
98
124
|
if (console[method] === patched.get(method))
|
|
99
125
|
console[method] = original;
|
|
100
126
|
}
|
|
101
|
-
|
|
102
|
-
window.removeEventListener('unhandledrejection', onRejection);
|
|
127
|
+
listeners.abort();
|
|
103
128
|
};
|
|
104
129
|
}
|
package/dist/observers/dom.js
CHANGED
|
@@ -153,8 +153,10 @@ export function installDom(emit) {
|
|
|
153
153
|
dropped += 1;
|
|
154
154
|
continue;
|
|
155
155
|
}
|
|
156
|
-
if (isReticleOverlay(node)
|
|
156
|
+
if (isReticleOverlay(node) ||
|
|
157
|
+
(isElement(record.target) && isReticleOverlay(record.target))) {
|
|
157
158
|
continue;
|
|
159
|
+
}
|
|
158
160
|
const gone = describeMount(node);
|
|
159
161
|
if (gone === undefined)
|
|
160
162
|
continue;
|
|
@@ -7,6 +7,7 @@ const PREVIEW_CHARS = 2000;
|
|
|
7
7
|
export function installDownload(emit, opts = {}) {
|
|
8
8
|
if (typeof URL.createObjectURL !== 'function')
|
|
9
9
|
return () => undefined;
|
|
10
|
+
const ac = new AbortController();
|
|
10
11
|
const objectUrl = URL.createObjectURL.bind(URL);
|
|
11
12
|
const known = new Map();
|
|
12
13
|
const originalCreate = captureMethod(URL, 'createObjectURL');
|
|
@@ -58,7 +59,7 @@ export function installDownload(emit, opts = {}) {
|
|
|
58
59
|
report(anchor.getAttribute('href') ?? '', anchor.getAttribute('download') ?? undefined);
|
|
59
60
|
});
|
|
60
61
|
};
|
|
61
|
-
document.addEventListener('click', onClick, true);
|
|
62
|
+
document.addEventListener('click', onClick, { capture: true, signal: ac.signal });
|
|
62
63
|
const originalClick = captureMethod(HTMLAnchorElement.prototype, 'click');
|
|
63
64
|
const patchedClick = function reticleObservedAnchorClick() {
|
|
64
65
|
const pending = observeValue(() => this.hasAttribute('download')
|
|
@@ -75,7 +76,7 @@ export function installDownload(emit, opts = {}) {
|
|
|
75
76
|
};
|
|
76
77
|
HTMLAnchorElement.prototype.click = patchedClick;
|
|
77
78
|
return () => {
|
|
78
|
-
|
|
79
|
+
ac.abort();
|
|
79
80
|
if (URL.createObjectURL === patchedCreate)
|
|
80
81
|
URL.createObjectURL = originalCreate;
|
|
81
82
|
if (HTMLAnchorElement.prototype.click === patchedClick) {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export declare const REPEAT_BURST = 3;
|
|
2
|
+
export declare const MAX_TRACKED_FINGERPRINTS = 256;
|
|
3
|
+
export interface RepeatDecision {
|
|
4
|
+
readonly emit: boolean;
|
|
5
|
+
readonly repeats?: number;
|
|
6
|
+
}
|
|
7
|
+
export interface ErrorIdentity {
|
|
8
|
+
readonly message: string;
|
|
9
|
+
readonly source?: string;
|
|
10
|
+
readonly line?: number;
|
|
11
|
+
readonly kind?: string;
|
|
12
|
+
readonly stack?: string;
|
|
13
|
+
}
|
|
14
|
+
export declare function fingerprintError(identity: ErrorIdentity): string;
|
|
15
|
+
export interface RepeatLimiter {
|
|
16
|
+
admit(identity: ErrorIdentity): RepeatDecision;
|
|
17
|
+
}
|
|
18
|
+
export declare function createRepeatLimiter(burst?: number, maxTracked?: number): RepeatLimiter;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { fnv1a } from '@reticlehq/core';
|
|
2
|
+
export const REPEAT_BURST = 3;
|
|
3
|
+
export const MAX_TRACKED_FINGERPRINTS = 256;
|
|
4
|
+
const PASS = { emit: true };
|
|
5
|
+
const DROP = { emit: false };
|
|
6
|
+
function isCheckpoint(count) {
|
|
7
|
+
if (count < 10)
|
|
8
|
+
return false;
|
|
9
|
+
let n = count;
|
|
10
|
+
while (0 === n % 10)
|
|
11
|
+
n /= 10;
|
|
12
|
+
return 1 === n;
|
|
13
|
+
}
|
|
14
|
+
export function fingerprintError(identity) {
|
|
15
|
+
return fnv1a([
|
|
16
|
+
identity.message,
|
|
17
|
+
identity.source ?? '',
|
|
18
|
+
undefined === identity.line ? '' : String(identity.line),
|
|
19
|
+
identity.kind ?? '',
|
|
20
|
+
identity.stack ?? '',
|
|
21
|
+
].join('\x00'));
|
|
22
|
+
}
|
|
23
|
+
export function createRepeatLimiter(burst = REPEAT_BURST, maxTracked = MAX_TRACKED_FINGERPRINTS) {
|
|
24
|
+
const counts = new Map();
|
|
25
|
+
return {
|
|
26
|
+
admit(identity) {
|
|
27
|
+
const key = fingerprintError(identity);
|
|
28
|
+
const seen = (counts.get(key) ?? 0) + 1;
|
|
29
|
+
counts.delete(key);
|
|
30
|
+
if (counts.size >= maxTracked) {
|
|
31
|
+
const oldest = counts.keys().next();
|
|
32
|
+
if (!(oldest.done ?? false))
|
|
33
|
+
counts.delete(oldest.value);
|
|
34
|
+
}
|
|
35
|
+
counts.set(key, seen);
|
|
36
|
+
if (seen <= burst)
|
|
37
|
+
return PASS;
|
|
38
|
+
return isCheckpoint(seen) ? { emit: true, repeats: seen } : DROP;
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { EventType, isSensitiveKey } from '@reticlehq/core';
|
|
2
|
+
import { getAccessibleName } from '../dom/a11y.js';
|
|
3
|
+
const MAX_VALUE_LEN = 160;
|
|
4
|
+
const BURST_MS = 150;
|
|
5
|
+
const PAYMENT_AUTOCOMPLETE = /^(cc-|current-password|new-password)/i;
|
|
6
|
+
function isField(target) {
|
|
7
|
+
return (target instanceof HTMLInputElement ||
|
|
8
|
+
target instanceof HTMLTextAreaElement ||
|
|
9
|
+
target instanceof HTMLSelectElement);
|
|
10
|
+
}
|
|
11
|
+
function fieldName(element) {
|
|
12
|
+
const testid = element.getAttribute('data-testid');
|
|
13
|
+
if (null !== testid && testid.length > 0)
|
|
14
|
+
return testid;
|
|
15
|
+
if (element.name.length > 0)
|
|
16
|
+
return element.name;
|
|
17
|
+
const accessible = getAccessibleName(element);
|
|
18
|
+
if (accessible.length > 0)
|
|
19
|
+
return accessible;
|
|
20
|
+
const id = element.getAttribute('id');
|
|
21
|
+
return null !== id && id.length > 0 ? id : element.tagName.toLowerCase();
|
|
22
|
+
}
|
|
23
|
+
function isSecret(element) {
|
|
24
|
+
if (element instanceof HTMLInputElement && 'password' === element.type)
|
|
25
|
+
return true;
|
|
26
|
+
const hint = element.getAttribute('autocomplete');
|
|
27
|
+
if (null !== hint && PAYMENT_AUTOCOMPLETE.test(hint))
|
|
28
|
+
return true;
|
|
29
|
+
for (const key of [
|
|
30
|
+
element.name,
|
|
31
|
+
element.getAttribute('data-testid'),
|
|
32
|
+
element.getAttribute('id'),
|
|
33
|
+
]) {
|
|
34
|
+
if (null !== key && key.length > 0 && isSensitiveKey(key))
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
export function installField(emit) {
|
|
40
|
+
const ac = new AbortController();
|
|
41
|
+
const { signal } = ac;
|
|
42
|
+
const pending = new Map();
|
|
43
|
+
const report = (element, kind) => {
|
|
44
|
+
const value = element.value;
|
|
45
|
+
const secret = isSecret(element);
|
|
46
|
+
emit(EventType.FIELD_CHANGE, {
|
|
47
|
+
field: fieldName(element),
|
|
48
|
+
kind,
|
|
49
|
+
length: value.length,
|
|
50
|
+
...(secret
|
|
51
|
+
? { redacted: true }
|
|
52
|
+
: { value: value.length > MAX_VALUE_LEN ? `${value.slice(0, MAX_VALUE_LEN)}…` : value }),
|
|
53
|
+
});
|
|
54
|
+
};
|
|
55
|
+
const clear = (element) => {
|
|
56
|
+
const timer = pending.get(element);
|
|
57
|
+
if (timer !== undefined)
|
|
58
|
+
clearTimeout(timer);
|
|
59
|
+
pending.delete(element);
|
|
60
|
+
};
|
|
61
|
+
const onInput = (event) => {
|
|
62
|
+
const element = event.target;
|
|
63
|
+
if (!isField(element))
|
|
64
|
+
return;
|
|
65
|
+
clear(element);
|
|
66
|
+
pending.set(element, setTimeout(() => {
|
|
67
|
+
pending.delete(element);
|
|
68
|
+
report(element, 'input');
|
|
69
|
+
}, BURST_MS));
|
|
70
|
+
};
|
|
71
|
+
const onChange = (event) => {
|
|
72
|
+
const element = event.target;
|
|
73
|
+
if (!isField(element))
|
|
74
|
+
return;
|
|
75
|
+
clear(element);
|
|
76
|
+
report(element, 'change');
|
|
77
|
+
};
|
|
78
|
+
document.addEventListener('input', onInput, { signal, capture: true });
|
|
79
|
+
document.addEventListener('change', onChange, { signal, capture: true });
|
|
80
|
+
return () => {
|
|
81
|
+
for (const timer of pending.values())
|
|
82
|
+
clearTimeout(timer);
|
|
83
|
+
pending.clear();
|
|
84
|
+
ac.abort();
|
|
85
|
+
};
|
|
86
|
+
}
|
package/dist/observers/frames.js
CHANGED
|
@@ -24,6 +24,7 @@ export function sameOriginFrameBodies(root) {
|
|
|
24
24
|
return found;
|
|
25
25
|
}
|
|
26
26
|
export function observeSameOriginFrames(attach) {
|
|
27
|
+
const ac = new AbortController();
|
|
27
28
|
const attachAll = () => {
|
|
28
29
|
for (const body of sameOriginFrameBodies(document))
|
|
29
30
|
attach(body);
|
|
@@ -33,8 +34,6 @@ export function observeSameOriginFrames(attach) {
|
|
|
33
34
|
if (event.target instanceof HTMLIFrameElement)
|
|
34
35
|
attachAll();
|
|
35
36
|
};
|
|
36
|
-
document.addEventListener('load', onLoad, true);
|
|
37
|
-
return () =>
|
|
38
|
-
document.removeEventListener('load', onLoad, true);
|
|
39
|
-
};
|
|
37
|
+
document.addEventListener('load', onLoad, { capture: true, signal: ac.signal });
|
|
38
|
+
return () => ac.abort();
|
|
40
39
|
}
|
|
@@ -2,6 +2,7 @@ import { installDom } from './dom.js';
|
|
|
2
2
|
import { installStorage } from './storage.js';
|
|
3
3
|
import { installStoreState } from './state.js';
|
|
4
4
|
import { installFocus } from './focus.js';
|
|
5
|
+
import { installField } from './field.js';
|
|
5
6
|
import { installBlindSpots } from './blind-spots.js';
|
|
6
7
|
import { installDownload } from './download.js';
|
|
7
8
|
import { installContextOpen } from './context-open.js';
|
|
@@ -59,6 +60,7 @@ export function installAllObservers(emit, options) {
|
|
|
59
60
|
guard(emit, SdkSite.STORE_ADAPTER, () => installStorage(emit)),
|
|
60
61
|
guard(emit, SdkSite.STORE_ADAPTER, () => installStoreState(emit)),
|
|
61
62
|
guard(emit, SdkSite.DOM_OBSERVER, () => installFocus(emit)),
|
|
63
|
+
guard(emit, SdkSite.DOM_OBSERVER, () => installField(emit)),
|
|
62
64
|
guard(emit, SdkSite.NETWORK_OBSERVER, () => installDownload(emit, { capturePreview: options.captureBodies })),
|
|
63
65
|
guard(emit, SdkSite.DOM_OBSERVER, () => installBlindSpots(emit)),
|
|
64
66
|
guard(emit, SdkSite.HEALTH_OBSERVER, () => installHealth(emit)),
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export declare function isStreamingBody(contentType: string | null, contentLength: string | null): boolean;
|
|
2
2
|
export declare function withBodyDeadline(read: Promise<string>): Promise<string | undefined>;
|
|
3
3
|
export declare function isCapturableType(contentType: string | null): boolean;
|
|
4
|
+
export declare function setNetworkBodyMaxChars(chars: number): void;
|
|
5
|
+
export declare function networkBodyMaxChars(): number;
|
|
4
6
|
export declare function shouldCaptureErrorBody(status: number, enabled: boolean): boolean;
|
|
5
7
|
export declare function projectErrorBody(rawText: string, contentType: string | null): {
|
|
6
8
|
body: string;
|
|
@@ -32,8 +32,23 @@ export function isCapturableType(contentType) {
|
|
|
32
32
|
return false;
|
|
33
33
|
return CAPTURABLE_CONTENT.test(contentType);
|
|
34
34
|
}
|
|
35
|
-
const
|
|
36
|
-
const
|
|
35
|
+
const DEFAULT_BODY_CHARS = 8192;
|
|
36
|
+
const MAX_CONFIGURABLE_BODY_CHARS = 262144;
|
|
37
|
+
const MIN_CONFIGURABLE_BODY_CHARS = 256;
|
|
38
|
+
let bodyMaxChars = DEFAULT_BODY_CHARS;
|
|
39
|
+
export function setNetworkBodyMaxChars(chars) {
|
|
40
|
+
if (!Number.isFinite(chars))
|
|
41
|
+
return;
|
|
42
|
+
bodyMaxChars = Math.min(MAX_CONFIGURABLE_BODY_CHARS, Math.max(MIN_CONFIGURABLE_BODY_CHARS, Math.floor(chars)));
|
|
43
|
+
}
|
|
44
|
+
export function networkBodyMaxChars() {
|
|
45
|
+
return bodyMaxChars;
|
|
46
|
+
}
|
|
47
|
+
const REDACT_SCAN_CEILING = DEFAULT_BODY_CHARS * 2;
|
|
48
|
+
function scanWidth(redacting) {
|
|
49
|
+
const generous = bodyMaxChars * 2;
|
|
50
|
+
return redacting ? Math.min(generous, REDACT_SCAN_CEILING) : generous;
|
|
51
|
+
}
|
|
37
52
|
const AUTH_SCHEME_TOKEN = /\b(Bearer|Basic)\s+(?=[A-Za-z0-9._~+/=-]{16,})(?=[A-Za-z0-9._~+/=-]*[\d._~+/=-])[A-Za-z0-9._~+/=-]+/gi;
|
|
38
53
|
function redactText(text) {
|
|
39
54
|
return (text
|
|
@@ -51,10 +66,12 @@ export function projectErrorBody(rawText, contentType) {
|
|
|
51
66
|
return { body: projected.body.slice(0, ERROR_BODY_CHARS), truncated: true };
|
|
52
67
|
}
|
|
53
68
|
export function projectBody(rawText, contentType) {
|
|
54
|
-
const
|
|
55
|
-
const
|
|
69
|
+
const isJson = contentType !== null && /json|graphql/i.test(contentType);
|
|
70
|
+
const scanChars = scanWidth(!isJson);
|
|
71
|
+
const oversized = rawText.length > scanChars;
|
|
72
|
+
const text = oversized ? rawText.slice(0, scanChars) : rawText;
|
|
56
73
|
let out;
|
|
57
|
-
if (
|
|
74
|
+
if (isJson) {
|
|
58
75
|
try {
|
|
59
76
|
out = safeStringify(JSON.parse(text));
|
|
60
77
|
}
|
|
@@ -66,6 +83,6 @@ export function projectBody(rawText, contentType) {
|
|
|
66
83
|
out = redactText(text);
|
|
67
84
|
}
|
|
68
85
|
out = scrubKnownSecrets(out);
|
|
69
|
-
const truncated = oversized || out.length >
|
|
70
|
-
return { body: out.length >
|
|
86
|
+
const truncated = oversized || out.length > bodyMaxChars;
|
|
87
|
+
return { body: out.length > bodyMaxChars ? out.slice(0, bodyMaxChars) : out, truncated };
|
|
71
88
|
}
|
package/dist/observers/route.js
CHANGED
|
@@ -10,6 +10,7 @@ function snapshotLocation() {
|
|
|
10
10
|
};
|
|
11
11
|
}
|
|
12
12
|
export function installRoute(emit) {
|
|
13
|
+
const ac = new AbortController();
|
|
13
14
|
const origPush = captureMethod(history, 'pushState');
|
|
14
15
|
const origReplace = captureMethod(history, 'replaceState');
|
|
15
16
|
const callPush = origPush.bind(history);
|
|
@@ -45,14 +46,13 @@ export function installRoute(emit) {
|
|
|
45
46
|
const onNav = () => {
|
|
46
47
|
fire(lastHref);
|
|
47
48
|
};
|
|
48
|
-
window.addEventListener('popstate', onNav);
|
|
49
|
-
window.addEventListener('hashchange', onNav);
|
|
49
|
+
window.addEventListener('popstate', onNav, { signal: ac.signal });
|
|
50
|
+
window.addEventListener('hashchange', onNav, { signal: ac.signal });
|
|
50
51
|
return () => {
|
|
51
52
|
if (history.pushState === patchedPush)
|
|
52
53
|
history.pushState = origPush;
|
|
53
54
|
if (history.replaceState === patchedReplace)
|
|
54
55
|
history.replaceState = origReplace;
|
|
55
|
-
|
|
56
|
-
window.removeEventListener('hashchange', onNav);
|
|
56
|
+
ac.abort();
|
|
57
57
|
};
|
|
58
58
|
}
|
package/dist/observers/state.js
CHANGED
|
@@ -19,6 +19,16 @@ export function diffState(prev, next) {
|
|
|
19
19
|
function project(path, value) {
|
|
20
20
|
return isSensitiveKey(path) ? REDACTED_VALUE : sanitizeForTransport(value);
|
|
21
21
|
}
|
|
22
|
+
function samePresentedValue(oldValue, newValue) {
|
|
23
|
+
if (Object.is(oldValue, newValue))
|
|
24
|
+
return true;
|
|
25
|
+
try {
|
|
26
|
+
return JSON.stringify(oldValue) === JSON.stringify(newValue);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
22
32
|
function safeRead(getter) {
|
|
23
33
|
try {
|
|
24
34
|
return getter();
|
|
@@ -39,15 +49,15 @@ export function installStoreState(emit) {
|
|
|
39
49
|
active.set(name, subscribe(() => {
|
|
40
50
|
observeSafely(() => {
|
|
41
51
|
const next = safeRead(getter);
|
|
42
|
-
|
|
43
|
-
emit(EventType.STATE_CHANGE, {
|
|
44
|
-
name,
|
|
45
|
-
path: change.path,
|
|
46
|
-
value: project(change.path, change.new),
|
|
47
|
-
old: project(change.path, change.old),
|
|
48
|
-
});
|
|
49
|
-
}
|
|
52
|
+
const changes = diffState(last, next);
|
|
50
53
|
last = next;
|
|
54
|
+
for (const change of changes) {
|
|
55
|
+
const value = project(change.path, change.new);
|
|
56
|
+
const old = project(change.path, change.old);
|
|
57
|
+
if (!isSensitiveKey(change.path) && samePresentedValue(old, value))
|
|
58
|
+
continue;
|
|
59
|
+
observeSafely(() => emit(EventType.STATE_CHANGE, { name, path: change.path, value, old }));
|
|
60
|
+
}
|
|
51
61
|
});
|
|
52
62
|
}));
|
|
53
63
|
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare const CAROUSEL_ATTR = "data-reticle-carousel";
|
|
2
|
+
export declare const CAROUSEL_SLIDE_ATTR = "data-reticle-carousel-slide";
|
|
3
|
+
export declare const CAROUSEL_DOT_ATTR = "data-reticle-carousel-dot";
|
|
4
|
+
export declare const CAROUSEL_CLOSE_ATTR = "data-reticle-carousel-close";
|
|
5
|
+
export declare const CAROUSEL_DISMISSED_KEY = "reticle.carousel.dismissed";
|
|
6
|
+
export declare const ROTATE_MS = 6000;
|
|
7
|
+
export interface Slide {
|
|
8
|
+
id: string;
|
|
9
|
+
html: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function carouselHtml(slides: readonly Slide[], active: number): string;
|
|
12
|
+
export declare function paintCarousel(log: HTMLElement, slides: readonly Slide[], storage: Pick<Storage, 'getItem' | 'setItem'> | undefined): void;
|
|
13
|
+
export declare const CAROUSEL_CSS = "\n.reticle-carousel{position:relative;margin:8px 10px;padding:10px 12px 8px;border:1px solid var(--reticle-line,#2a2f3a);border-radius:10px;background:var(--reticle-surface-inset,rgba(255,255,255,.03));}\n.reticle-carousel-track{height:100px;overflow:hidden;}\n.reticle-carousel-slide .reticle-offer,.reticle-carousel-slide .reticle-talk{margin:0;padding:0;border:0;background:none;}\n.reticle-carousel-close{position:absolute;top:4px;right:6px;width:20px;height:20px;padding:0;border:0;background:none;color:inherit;opacity:.55;cursor:pointer;font-size:14px;line-height:20px;}\n.reticle-carousel-close:hover{opacity:1;}\n.reticle-carousel-dots{display:flex;justify-content:center;gap:6px;margin-top:6px;}\n.reticle-carousel-dot{width:6px;height:6px;padding:0;border:0;border-radius:999px;background:currentColor;opacity:.3;cursor:pointer;}\n.reticle-carousel-dot[aria-current=\"true\"]{opacity:.9;}\n";
|