@web-auto/camo 0.1.19 → 0.1.20

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@web-auto/camo",
3
- "version": "0.1.19",
3
+ "version": "0.1.20",
4
4
  "description": "Camoufox Browser CLI - Cross-platform browser automation",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,1317 @@
1
+ /* eslint-disable no-var */
2
+ // __DOM_PICKER_INLINE_START
3
+ /* DOM picker runtime: window.__domPicker
4
+ * Responsible for pointer -> DOM -> highlight -> selection.
5
+ */
6
+ (() => {
7
+ if (typeof window === 'undefined') return;
8
+
9
+ const DEFAULT_TIMEOUT = 25000;
10
+ const HOVER_STYLE = '2px dashed #fbbc05';
11
+ const HIGHLIGHT_CHANNEL = '__camo_dom_picker';
12
+
13
+ const domPickerState = {
14
+ active: false,
15
+ phase: 'idle', // 'idle' | 'hovering' | 'selected' | 'cancelled' | 'timeout'
16
+ lastHover: null,
17
+ selection: null,
18
+ error: null,
19
+ updatedAt: Date.now(),
20
+ };
21
+
22
+ const overlayApi = () => {
23
+ const runtime = window.__camoRuntime;
24
+ if (!runtime || !runtime.highlight || !runtime.highlight.highlightElements) {
25
+ return null;
26
+ }
27
+ return runtime.highlight;
28
+ };
29
+
30
+ const setState = (patch) => {
31
+ Object.assign(domPickerState, patch, { updatedAt: Date.now() });
32
+ };
33
+
34
+ const pickerShield = createPickerShield();
35
+
36
+ function createPickerShield() {
37
+ if (typeof window === 'undefined') return null;
38
+ const SHIELD_CLASS = '__camo_picker_shield__';
39
+ const instances = new Map();
40
+ const observers = new Map();
41
+ const frameLoadListeners = new Map();
42
+ let callbacksRef = null;
43
+
44
+ class ShieldInstance {
45
+ constructor(frameWindow, frameElement, callbacks) {
46
+ this.frameWindow = frameWindow;
47
+ this.frameElement = frameElement || null;
48
+ this.callbacks = callbacks || {};
49
+ this.layer = null;
50
+ this.bindings = null;
51
+ }
52
+
53
+ mount() {
54
+ const doc = this.getDocument();
55
+ if (!doc) return;
56
+ if (this.layer && doc.contains(this.layer)) {
57
+ return;
58
+ }
59
+ this.destroy();
60
+ if (doc.readyState === 'loading') {
61
+ doc.addEventListener('DOMContentLoaded', () => this.createLayer(doc), { once: true });
62
+ } else {
63
+ this.createLayer(doc);
64
+ }
65
+ }
66
+
67
+ destroy() {
68
+ this.unbindEvents();
69
+ if (this.layer && this.layer.parentNode) {
70
+ this.layer.parentNode.removeChild(this.layer);
71
+ }
72
+ this.layer = null;
73
+ }
74
+
75
+ getDocument() {
76
+ try {
77
+ return this.frameWindow.document;
78
+ } catch {
79
+ return null;
80
+ }
81
+ }
82
+
83
+ createLayer(doc) {
84
+ const host = doc.body || doc.documentElement;
85
+ if (!host) return;
86
+ const layer = doc.createElement('div');
87
+ layer.className = SHIELD_CLASS;
88
+ const style = layer.style;
89
+ style.setProperty('position', 'fixed', 'important');
90
+ style.setProperty('inset', '0', 'important');
91
+ style.setProperty('width', '100vw', 'important');
92
+ style.setProperty('height', '100vh', 'important');
93
+ style.setProperty('z-index', '2147483646', 'important');
94
+ style.setProperty('pointer-events', 'auto', 'important');
95
+ style.setProperty('background', 'transparent', 'important');
96
+ style.setProperty('cursor', 'crosshair', 'important');
97
+ style.setProperty('touch-action', 'none', 'important');
98
+ style.setProperty('user-select', 'none', 'important');
99
+ style.setProperty('contain', 'strict', 'important');
100
+ host.appendChild(layer);
101
+ this.layer = layer;
102
+ this.bindEvents();
103
+ }
104
+
105
+ bindEvents() {
106
+ if (!this.layer) return;
107
+ const win = this.frameWindow;
108
+ const pointerMove = (e) => {
109
+ this.consumeEvent(e);
110
+ const target = this.resolveTarget(e);
111
+ this.callbacks.onHover?.({
112
+ target,
113
+ event: e,
114
+ frameWindow: this.frameWindow,
115
+ frameElement: this.frameElement,
116
+ });
117
+ };
118
+ const pointerDown = (e) => {
119
+ this.consumeEvent(e);
120
+ const target = this.resolveTarget(e);
121
+ this.callbacks.onPointerDown?.({
122
+ target,
123
+ event: e,
124
+ frameWindow: this.frameWindow,
125
+ frameElement: this.frameElement,
126
+ });
127
+ };
128
+ const pointerUp = (e) => {
129
+ this.consumeEvent(e);
130
+ const target = this.resolveTarget(e);
131
+ this.callbacks.onPointerUp?.({
132
+ target,
133
+ event: e,
134
+ frameWindow: this.frameWindow,
135
+ frameElement: this.frameElement,
136
+ });
137
+ };
138
+ const click = (e) => {
139
+ this.consumeEvent(e);
140
+ const target = this.resolveTarget(e);
141
+ this.callbacks.onClick?.({
142
+ target,
143
+ event: e,
144
+ frameWindow: this.frameWindow,
145
+ frameElement: this.frameElement,
146
+ });
147
+ };
148
+ const contextMenu = (e) => {
149
+ this.consumeEvent(e);
150
+ };
151
+ win.addEventListener('pointermove', pointerMove, true);
152
+ win.addEventListener('pointerdown', pointerDown, true);
153
+ win.addEventListener('pointerup', pointerUp, true);
154
+ win.addEventListener('pointercancel', pointerUp, true);
155
+ win.addEventListener('click', click, true);
156
+ win.addEventListener('contextmenu', contextMenu, true);
157
+ this.bindings = { pointerMove, pointerDown, pointerUp, click, contextMenu };
158
+ }
159
+
160
+ unbindEvents() {
161
+ if (!this.bindings) return;
162
+ const { pointerMove, pointerDown, pointerUp, click, contextMenu } = this.bindings;
163
+ const win = this.frameWindow;
164
+ win.removeEventListener('pointermove', pointerMove, true);
165
+ win.removeEventListener('pointerdown', pointerDown, true);
166
+ win.removeEventListener('pointerup', pointerUp, true);
167
+ win.removeEventListener('pointercancel', pointerUp, true);
168
+ win.removeEventListener('click', click, true);
169
+ win.removeEventListener('contextmenu', contextMenu, true);
170
+ this.bindings = null;
171
+ }
172
+
173
+ consumeEvent(e) {
174
+ if (!e) return;
175
+ try {
176
+ e.preventDefault();
177
+ e.stopPropagation();
178
+ if (typeof e.stopImmediatePropagation === 'function') {
179
+ e.stopImmediatePropagation();
180
+ }
181
+ } catch {
182
+ /* ignore */
183
+ }
184
+ }
185
+
186
+ resolveTarget(e) {
187
+ if (!e) return null;
188
+ const path = typeof e.composedPath === 'function' ? e.composedPath() : [];
189
+ let fallbackCandidate = null;
190
+ for (const node of path) {
191
+ if (node instanceof Element && !this.isShieldElement(node)) {
192
+ if (!fallbackCandidate) {
193
+ fallbackCandidate = node;
194
+ }
195
+ const tag = node.tagName ? node.tagName.toLowerCase() : '';
196
+ if (tag && tag !== 'html' && tag !== 'body') {
197
+ return node;
198
+ }
199
+ }
200
+ }
201
+ const maybeTarget = e.target instanceof Element ? e.target : null;
202
+ if (maybeTarget && !this.isShieldElement(maybeTarget)) {
203
+ const tag = maybeTarget.tagName ? maybeTarget.tagName.toLowerCase() : '';
204
+ if (tag && tag !== 'html' && tag !== 'body') {
205
+ return maybeTarget;
206
+ }
207
+ if (!fallbackCandidate) {
208
+ fallbackCandidate = maybeTarget;
209
+ }
210
+ }
211
+ const alt = this.elementFromPointSafely(e);
212
+ if (alt && !this.isShieldElement(alt)) {
213
+ return alt;
214
+ }
215
+ return fallbackCandidate;
216
+ }
217
+
218
+ isShieldElement(el) {
219
+ if (!(el instanceof Element)) return false;
220
+ return el.classList.contains(SHIELD_CLASS);
221
+ }
222
+
223
+ elementFromPointSafely(e) {
224
+ const doc = this.getDocument();
225
+ if (!doc || typeof e.clientX !== 'number' || typeof e.clientY !== 'number') return null;
226
+ const originalDisplay = this.layer ? this.layer.style.display : '';
227
+ if (this.layer) {
228
+ this.layer.style.display = 'none';
229
+ }
230
+ let result = null;
231
+ try {
232
+ const candidate = doc.elementFromPoint(e.clientX, e.clientY);
233
+ if (candidate instanceof Element && !this.isShieldElement(candidate)) {
234
+ result = candidate;
235
+ }
236
+ } catch {
237
+ result = null;
238
+ } finally {
239
+ if (this.layer) {
240
+ this.layer.style.display = originalDisplay || '';
241
+ }
242
+ }
243
+ return result;
244
+ }
245
+ }
246
+
247
+ const attachToWindow = (targetWindow, frameElement) => {
248
+ if (!callbacksRef || instances.has(targetWindow)) return;
249
+ const instance = new ShieldInstance(targetWindow, frameElement, callbacksRef);
250
+ instances.set(targetWindow, instance);
251
+ instance.mount();
252
+ observeFrames(targetWindow);
253
+ };
254
+
255
+ const observeFrames = (targetWindow) => {
256
+ const doc = getDocumentSafe(targetWindow);
257
+ if (!doc) return;
258
+ const observer = new MutationObserver(() => scanFrames(targetWindow));
259
+ observer.observe(doc.documentElement || doc, { childList: true, subtree: true });
260
+ observers.set(targetWindow, observer);
261
+ scanFrames(targetWindow);
262
+ };
263
+
264
+ const scanFrames = (targetWindow) => {
265
+ const doc = getDocumentSafe(targetWindow);
266
+ if (!doc) return;
267
+ const frames = Array.from(doc.querySelectorAll('iframe'));
268
+ frames.forEach((frame) => tryAttachFrame(frame));
269
+ };
270
+
271
+ const tryAttachFrame = (frame) => {
272
+ if (!frame || !frame.contentWindow) return;
273
+ const childWindow = frame.contentWindow;
274
+ if (instances.has(childWindow)) {
275
+ instances.get(childWindow)?.mount();
276
+ return;
277
+ }
278
+ try {
279
+ // eslint-disable-next-line no-unused-expressions
280
+ childWindow.document;
281
+ } catch {
282
+ markFrameUnavailable(frame);
283
+ return;
284
+ }
285
+ attachToWindow(childWindow, frame);
286
+ frame.dataset.__camoPickerAttached = 'true';
287
+ const reloadHandler = () => tryAttachFrame(frame);
288
+ frame.addEventListener('load', reloadHandler);
289
+ frameLoadListeners.set(frame, reloadHandler);
290
+ };
291
+
292
+ const markFrameUnavailable = (frame) => {
293
+ frame.dataset.__camoPickerBlocked = 'true';
294
+ if (callbacksRef?.onFrameBlocked) {
295
+ try {
296
+ callbacksRef.onFrameBlocked(frame);
297
+ } catch {
298
+ /* ignore */
299
+ }
300
+ }
301
+ };
302
+
303
+ const detachAll = () => {
304
+ instances.forEach((instance) => instance.destroy());
305
+ instances.clear();
306
+ observers.forEach((observer) => observer.disconnect());
307
+ observers.clear();
308
+ frameLoadListeners.forEach((listener, frame) => {
309
+ frame.removeEventListener('load', listener);
310
+ });
311
+ frameLoadListeners.clear();
312
+ };
313
+
314
+ const getDocumentSafe = (targetWindow) => {
315
+ try {
316
+ return targetWindow.document;
317
+ } catch {
318
+ return null;
319
+ }
320
+ };
321
+
322
+ return {
323
+ attach(cb) {
324
+ detachAll();
325
+ callbacksRef = cb || null;
326
+ if (!callbacksRef) return;
327
+ attachToWindow(window, null);
328
+ },
329
+ detach() {
330
+ detachAll();
331
+ callbacksRef = null;
332
+ },
333
+ };
334
+ }
335
+
336
+ const core = {
337
+ _session: null,
338
+
339
+ _ensureStopped() {
340
+ if (!this._session) return;
341
+ try {
342
+ this._session.stop();
343
+ } catch {}
344
+ this._session = null;
345
+ },
346
+
347
+ startSession(options) {
348
+ const mode = (options && options.mode) || 'hover-select';
349
+ const timeoutMs = Math.min(Math.max(Number(options && options.timeoutMs) || DEFAULT_TIMEOUT, 1000), 60000);
350
+ const rootSelector = (options && (options.rootSelector || options.root_selector)) || null;
351
+
352
+ this._ensureStopped();
353
+
354
+ const session = createSession({ mode, timeoutMs, rootSelector });
355
+ this._session = session;
356
+ session.start();
357
+ return this.getLastState();
358
+ },
359
+
360
+ cancel() {
361
+ if (this._session) {
362
+ this._session.cancel('cancelled-by-host');
363
+ }
364
+ return this.getLastState();
365
+ },
366
+
367
+ getLastState() {
368
+ // return shallow-cloned, read-only-ish snapshot
369
+ return JSON.parse(JSON.stringify(domPickerState));
370
+ },
371
+ };
372
+
373
+ function createSession(config) {
374
+ const mode = config.mode || 'hover-select';
375
+ const timeoutMs = config.timeoutMs || DEFAULT_TIMEOUT;
376
+ const rootSelector = config.rootSelector || null;
377
+
378
+ let active = false;
379
+ let timeoutToken = null;
380
+
381
+ let lastHoverEl = null;
382
+ let shieldActive = false;
383
+
384
+ const listeners = [];
385
+
386
+ const detachShield = () => {
387
+ if (pickerShield && shieldActive) {
388
+ try {
389
+ pickerShield.detach();
390
+ } catch {
391
+ /* ignore */
392
+ }
393
+ shieldActive = false;
394
+ }
395
+ };
396
+
397
+ const addListener = (target, type, handler, options) => {
398
+ target.addEventListener(type, handler, options || true);
399
+ listeners.push(() => {
400
+ try {
401
+ target.removeEventListener(type, handler, options || true);
402
+ } catch {}
403
+ });
404
+ };
405
+
406
+ const clearHighlight = () => {
407
+ const api = overlayApi();
408
+ if (!api || !api.clear) return;
409
+ try {
410
+ api.clear(HIGHLIGHT_CHANNEL);
411
+ } catch {}
412
+ };
413
+
414
+ const highlightEl = (el) => {
415
+ const api = overlayApi();
416
+ if (!api || !api.highlightElements) return;
417
+ if (!el) {
418
+ clearHighlight();
419
+ return;
420
+ }
421
+ try {
422
+ api.highlightElements([el], {
423
+ channel: HIGHLIGHT_CHANNEL,
424
+ style: HOVER_STYLE,
425
+ duration: 0,
426
+ sticky: true,
427
+ maxMatches: 1,
428
+ });
429
+ } catch {}
430
+ };
431
+
432
+ const isRootLike = (el) => {
433
+ if (!el || !el.tagName) return false;
434
+ const tag = el.tagName.toLowerCase();
435
+ if (tag === 'html' || tag === 'body') return true;
436
+ if (el.id && el.id.toLowerCase() === 'app') return true;
437
+ return false;
438
+ };
439
+
440
+ const extractSelector = (el) => {
441
+ if (!el) return null;
442
+ if (el.id) return `#${el.id}`;
443
+ if (el.classList && el.classList.length) {
444
+ return `${el.tagName.toLowerCase()}.${Array.from(el.classList).join('.')}`;
445
+ }
446
+ return el.tagName ? el.tagName.toLowerCase() : null;
447
+ };
448
+
449
+ const extractPath = (el) => {
450
+ if (!el) return null;
451
+ try {
452
+ const runtime = window.__camoRuntime;
453
+ if (!runtime || !runtime.dom || !runtime.dom.buildPathForElement) {
454
+ console.warn('[dom-picker] buildPathForElement missing');
455
+ return null;
456
+ }
457
+ console.log('[dom-picker] extractPath rootSelector:', rootSelector);
458
+ const path = runtime.dom.buildPathForElement(el, rootSelector);
459
+ console.log('[dom-picker] extractPath result:', path);
460
+ return path;
461
+ } catch (err) {
462
+ console.warn('[dom-picker] extractPath error', err);
463
+ return null;
464
+ }
465
+ };
466
+
467
+ const extractRect = (el) => {
468
+ if (!el || !el.getBoundingClientRect) return null;
469
+ const rect = el.getBoundingClientRect();
470
+ return {
471
+ x: Math.round(rect.left),
472
+ y: Math.round(rect.top),
473
+ width: Math.round(rect.width),
474
+ height: Math.round(rect.height),
475
+ };
476
+ };
477
+
478
+ const extractText = (el) => {
479
+ if (!el) return '';
480
+ return (el.textContent || '').replace(/\s+/g, ' ').trim().slice(0, 160);
481
+ };
482
+
483
+ const pickElementFromPoint = (x, y, event) => {
484
+ const elements = (typeof document.elementsFromPoint === 'function'
485
+ ? document.elementsFromPoint(x, y)
486
+ : []);
487
+ const stack = Array.isArray(elements) ? elements : [];
488
+
489
+ const rejectSet = new Set();
490
+
491
+ // Exclude overlays created by our highlight layer
492
+ const overlayLayer = document.getElementById('__camo_highlight_layer');
493
+ if (overlayLayer && overlayLayer.contains) {
494
+ stack.forEach((el) => {
495
+ if (overlayLayer.contains(el)) rejectSet.add(el);
496
+ });
497
+ }
498
+
499
+ for (let i = 0; i < stack.length; i += 1) {
500
+ const el = stack[i];
501
+ if (!(el instanceof Element)) continue;
502
+ if (rejectSet.has(el)) continue;
503
+ if (isRootLike(el)) continue;
504
+ return el;
505
+ }
506
+
507
+ // Fallback using elementFromPoint
508
+ const fallback = document.elementFromPoint(x, y);
509
+ if (fallback instanceof Element && !isRootLike(fallback)) {
510
+ if (!rejectSet.has(fallback)) return fallback;
511
+ }
512
+
513
+ // Last resort: event target
514
+ if (event && event.target && event.target instanceof Element && !isRootLike(event.target)) {
515
+ return event.target;
516
+ }
517
+
518
+ return null;
519
+ };
520
+
521
+ const updateHover = (el) => {
522
+ lastHoverEl = el;
523
+ if (!el) {
524
+ setState({ phase: active ? 'hovering' : domPickerState.phase, lastHover: null });
525
+ clearHighlight();
526
+ return;
527
+ }
528
+ const path = extractPath(el);
529
+ const selector = extractSelector(el);
530
+ setState({
531
+ phase: 'hovering',
532
+ lastHover: {
533
+ path: path || null,
534
+ selector: selector || null,
535
+ },
536
+ });
537
+ highlightEl(el);
538
+ };
539
+
540
+ const finalize = (result) => {
541
+ console.log('[dom-picker] finalize called', result);
542
+ if (!active) return;
543
+ active = false;
544
+ if (timeoutToken) {
545
+ clearTimeout(timeoutToken);
546
+ timeoutToken = null;
547
+ }
548
+ detachShield();
549
+ listeners.forEach((fn) => {
550
+ try {
551
+ fn();
552
+ } catch {}
553
+ });
554
+ clearHighlight();
555
+
556
+ if (result && result.type === 'timeout') {
557
+ setState({ phase: 'timeout', error: null, active: false });
558
+ return;
559
+ }
560
+ if (result && result.type === 'cancel') {
561
+ setState({ phase: 'cancelled', error: null, active: false });
562
+ return;
563
+ }
564
+ if (result && result.type === 'error') {
565
+ setState({ phase: 'cancelled', error: result.error || 'unknown-error', active: false });
566
+ return;
567
+ }
568
+ if (result && result.type === 'select' && result.element) {
569
+ const el = result.element;
570
+ console.log('[dom-picker] finalize select element', el);
571
+ const path = extractPath(el);
572
+ console.log('[dom-picker] finalize path', path);
573
+ const selector = extractSelector(el);
574
+ const rect = extractRect(el);
575
+ const text = extractText(el);
576
+ const tag = el.tagName ? el.tagName.toLowerCase() : '';
577
+ const id = el.id || null;
578
+ const classes = Array.from(el.classList || []);
579
+
580
+ setState({
581
+ phase: 'selected',
582
+ active: false,
583
+ selection: {
584
+ path: path || '',
585
+ selector: selector || '',
586
+ rect: rect || { x: 0, y: 0, width: 0, height: 0 },
587
+ text,
588
+ tag,
589
+ id,
590
+ classes,
591
+ },
592
+ });
593
+ return;
594
+ }
595
+
596
+ // default: cancelled
597
+ setState({ phase: 'cancelled', active: false });
598
+ };
599
+
600
+ const onPointerMove = (event, forcedTarget) => {
601
+ if (!active) return;
602
+ if (forcedTarget) {
603
+ updateHover(forcedTarget);
604
+ return;
605
+ }
606
+ const x = event?.clientX;
607
+ const y = event?.clientY;
608
+ if (typeof x === 'number' && typeof y === 'number') {
609
+ const el = pickElementFromPoint(x, y, event);
610
+ updateHover(el);
611
+ } else {
612
+ updateHover(null);
613
+ }
614
+ };
615
+
616
+ const commitElement = (el, event) => {
617
+ const target = el || lastHoverEl;
618
+ if (!target && event) {
619
+ const fallback = pickElementFromPoint(event.clientX, event.clientY, event);
620
+ if (fallback) {
621
+ finalize({ type: 'select', element: fallback });
622
+ return;
623
+ }
624
+ }
625
+ if (!target) {
626
+ finalize({ type: 'error', error: 'no-element' });
627
+ return;
628
+ }
629
+ finalize({ type: 'select', element: target });
630
+ };
631
+
632
+ const commitFromEvent = (event, forcedTarget) => {
633
+ if (!event && !forcedTarget) {
634
+ commitElement(null, null);
635
+ return;
636
+ }
637
+ if (forcedTarget) {
638
+ commitElement(forcedTarget, event || null);
639
+ return;
640
+ }
641
+ const x = event?.clientX;
642
+ const y = event?.clientY;
643
+ const el = typeof x === 'number' && typeof y === 'number' ? pickElementFromPoint(x, y, event) : null;
644
+ commitElement(el, event || null);
645
+ };
646
+
647
+ const onPointerDown = (event, forcedTarget) => {
648
+ if (!active) return;
649
+ if (event.button !== undefined && event.button !== 0) return;
650
+ event.preventDefault();
651
+ event.stopPropagation();
652
+ commitFromEvent(event, forcedTarget || null);
653
+ };
654
+
655
+ const onMouseDown = (event, forcedTarget) => {
656
+ if (!active) return;
657
+ if (event.button !== 0) return; // left button only
658
+ event.preventDefault();
659
+ event.stopPropagation();
660
+ commitFromEvent(event, forcedTarget || null);
661
+ };
662
+
663
+ const onKeyDown = (event) => {
664
+ if (!active) return;
665
+ if (event.key === 'Escape') {
666
+ event.preventDefault();
667
+ event.stopPropagation();
668
+ finalize({ type: 'cancel' });
669
+ }
670
+ };
671
+
672
+ const onWindowBlur = () => {
673
+ if (!active) return;
674
+ finalize({ type: 'cancel' });
675
+ };
676
+
677
+ const onWindowMouseOut = (event) => {
678
+ if (!active) return;
679
+ const next = event.relatedTarget || event.toElement;
680
+ if (!next || next === window || next === document) {
681
+ updateHover(null);
682
+ }
683
+ };
684
+
685
+ const onScroll = () => {
686
+ if (!active) return;
687
+ if (lastHoverEl) highlightEl(lastHoverEl);
688
+ };
689
+
690
+ return {
691
+ start() {
692
+ active = true;
693
+ setState({
694
+ active: true,
695
+ phase: 'hovering',
696
+ lastHover: null,
697
+ selection: null,
698
+ error: null,
699
+ });
700
+
701
+ if (pickerShield) {
702
+ let attached = false;
703
+ try {
704
+ pickerShield.attach({
705
+ onHover: ({ target, event }) => {
706
+ onPointerMove(event || null, target || null);
707
+ },
708
+ onPointerDown: ({ target, event }) => {
709
+ onPointerDown(event || { button: 0 }, target || null);
710
+ },
711
+ onPointerUp: () => {
712
+ /* no-op */
713
+ },
714
+ onClick: ({ target, event }) => {
715
+ if (event) {
716
+ try {
717
+ event.preventDefault();
718
+ event.stopPropagation();
719
+ } catch {
720
+ /* ignore */
721
+ }
722
+ }
723
+ commitFromEvent(event || null, target || null);
724
+ },
725
+ onFrameBlocked: (frame) => {
726
+ try {
727
+ // eslint-disable-next-line no-console
728
+ console.warn('[dom-picker] frame blocked for shield', frame?.src || frame?.id || 'unknown');
729
+ } catch {
730
+ /* ignore */
731
+ }
732
+ },
733
+ });
734
+ attached = true;
735
+ } catch (err) {
736
+ // eslint-disable-next-line no-console
737
+ console.warn('[dom-picker] pickerShield attach failed', err);
738
+ }
739
+ shieldActive = attached;
740
+ if (!attached) {
741
+ addListener(document, 'pointermove', onPointerMove, true);
742
+ addListener(document, 'pointerdown', onPointerDown, true);
743
+ addListener(document, 'mousedown', onMouseDown, true);
744
+ }
745
+ } else {
746
+ addListener(document, 'pointermove', onPointerMove, true);
747
+ addListener(document, 'pointerdown', onPointerDown, true);
748
+ addListener(document, 'mousedown', onMouseDown, true);
749
+ }
750
+ addListener(document, 'keydown', onKeyDown, true);
751
+ addListener(window, 'blur', onWindowBlur, true);
752
+ addListener(window, 'scroll', onScroll, true);
753
+ addListener(window, 'mouseout', onWindowMouseOut, true);
754
+
755
+ timeoutToken = window.setTimeout(() => {
756
+ finalize({ type: 'timeout' });
757
+ }, timeoutMs);
758
+ },
759
+ cancel(reason) {
760
+ if (!active) return;
761
+ finalize({ type: 'cancel', reason: reason || 'cancelled' });
762
+ },
763
+ stop() {
764
+ if (!active) return;
765
+ finalize({ type: 'cancel', reason: 'stopped' });
766
+ },
767
+ };
768
+ }
769
+
770
+ const api = {
771
+ startSession: (options) => core.startSession(options || {}),
772
+ cancel: () => core.cancel(),
773
+ getLastState: () => core.getLastState(),
774
+ };
775
+
776
+ Object.defineProperty(window, '__domPicker', {
777
+ value: api,
778
+ configurable: true,
779
+ enumerable: false,
780
+ writable: false,
781
+ });
782
+ })();
783
+
784
+ // __DOM_PICKER_INLINE_END
785
+ (function attachDomPickerLoopback() {
786
+ if (typeof window === 'undefined') return;
787
+
788
+ function delay(ms) {
789
+ return new Promise((resolve) => setTimeout(resolve, ms));
790
+ }
791
+
792
+ function findElementCenter(selector) {
793
+ const el = typeof selector === 'string' ? document.querySelector(selector) : selector;
794
+ if (!el) return null;
795
+ const rect = el.getBoundingClientRect();
796
+ if (!rect || !Number.isFinite(rect.left)) return null;
797
+ const vw = Math.max(1, window.innerWidth || document.documentElement.clientWidth || 1);
798
+ const vh = Math.max(1, window.innerHeight || document.documentElement.clientHeight || 1);
799
+ const inset = 6;
800
+ const clamp = (v, min, max) => Math.min(Math.max(v, min), max);
801
+ const safeLeft = Math.max(rect.left + inset, inset);
802
+ const safeRight = Math.min(rect.right - inset, vw - inset);
803
+ const safeTop = Math.max(rect.top + inset, inset);
804
+ const safeBottom = Math.min(rect.bottom - inset, vh - inset);
805
+ const cx = safeRight >= safeLeft ? (safeLeft + safeRight) / 2 : rect.left + rect.width / 2;
806
+ const cy = safeBottom >= safeTop ? (safeTop + safeBottom) / 2 : rect.top + rect.height / 2;
807
+ return {
808
+ x: Math.round(clamp(cx, inset, vw - inset)),
809
+ y: Math.round(clamp(cy, inset, vh - inset)),
810
+ rect,
811
+ element: el,
812
+ };
813
+ }
814
+
815
+ async function hoverLoopCheck(selector, options = {}) {
816
+ const picker = window.__domPicker;
817
+ if (!picker || typeof picker.startSession !== 'function') {
818
+ return { error: 'domPicker unavailable' };
819
+ }
820
+ const center = findElementCenter(selector);
821
+ if (!center) return { error: 'element_not_found', selector };
822
+ const runtime = window.__camoRuntime;
823
+ const buildPath = runtime?.dom?.buildPathForElement;
824
+ const targetPath = buildPath && center.element instanceof Element ? buildPath(center.element, null) : null;
825
+ const fromPoint = document.elementFromPoint(center.x, center.y);
826
+ const fromPointPath = buildPath && fromPoint instanceof Element ? buildPath(fromPoint, null) : null;
827
+ // NOTE: Real mouse move should be triggered by Playwright (page.mouse.move).
828
+ // Here we only start the session and wait for it to pick up the hover.
829
+ const before = picker.getLastState?.();
830
+ if (!before?.phase || before.phase === 'idle') {
831
+ picker.startSession?.({ timeoutMs: options.timeoutMs || 8000 });
832
+ await delay(16);
833
+ }
834
+ await delay(options.settleMs || 32);
835
+ const after = picker.getLastState?.();
836
+ const hoveredPath = after?.selection?.path || after?.hovered?.path || after?.selected?.path || after?.path || null;
837
+ const overlayRect = after?.selection?.rect || after?.hovered?.rect || after?.selected?.rect || after?.rect || null;
838
+ const matches = Boolean(targetPath && hoveredPath && hoveredPath === targetPath && overlayRect);
839
+ return {
840
+ selector,
841
+ point: { x: center.x, y: center.y },
842
+ targetRect: center.rect,
843
+ hoveredPath,
844
+ targetPath,
845
+ fromPointPath,
846
+ overlayRect,
847
+ stateBefore: before,
848
+ stateAfter: after,
849
+ matches,
850
+ };
851
+ }
852
+
853
+ // Expose loopback helper as part of domPicker runtime for system use.
854
+ function ensureDomPickerRuntime() {
855
+ if (!window.__domPicker) return;
856
+ const picker = window.__domPicker;
857
+ picker.hoverLoopCheck = hoverLoopCheck;
858
+ picker.findElementCenter = findElementCenter;
859
+ }
860
+
861
+ if (document.readyState === 'loading') {
862
+ document.addEventListener('DOMContentLoaded', ensureDomPickerRuntime, { once: true });
863
+ } else {
864
+ ensureDomPickerRuntime();
865
+ }
866
+ })();
867
+
868
+ (() => {
869
+ if (typeof window === 'undefined') {
870
+ return;
871
+ }
872
+ if (window.__camoRuntime && window.__camoRuntime.ready) {
873
+ return;
874
+ }
875
+
876
+ window.__camoRuntimeBootCount = (window.__camoRuntimeBootCount || 0) + 1;
877
+
878
+ const VERSION = '0.1.0';
879
+ const DEFAULT_STYLE = null;
880
+ const registry = new Map();
881
+
882
+ function dispatchBridgeEvent(type, data = {}) {
883
+ try {
884
+ if (typeof window.camo_dispatch === 'function') {
885
+ window.camo_dispatch({ ts: Date.now(), type, data });
886
+ }
887
+ window.dispatchEvent(new CustomEvent(`camo:${type}`, { detail: data }));
888
+ } catch {
889
+ /* ignore bridge errors */
890
+ }
891
+ }
892
+
893
+ let handshakeNotified = false;
894
+ function notifyHandshakeStatus(status) {
895
+ if (handshakeNotified) return;
896
+ handshakeNotified = true;
897
+ dispatchBridgeEvent('handshake.status', {
898
+ status,
899
+ href: window.location.href,
900
+ hostname: window.location.hostname,
901
+ runtimeVersion: VERSION,
902
+ bootCount: window.__camoRuntimeBootCount || 1,
903
+ });
904
+ }
905
+
906
+ const domUtils = {
907
+ resolveRoot(selector) {
908
+ if (selector) {
909
+ try {
910
+ const explicit = document.querySelector(selector);
911
+ if (explicit) return explicit;
912
+ } catch {
913
+ /* ignore invalid selector */
914
+ }
915
+ }
916
+ return document.body || document.documentElement;
917
+ },
918
+ resolveByPath(path, selector) {
919
+ if (!path || typeof path !== 'string') return null;
920
+ const parts = path.split('/').filter(Boolean);
921
+ if (!parts.length || parts[0] !== 'root') return null;
922
+ let current = domUtils.resolveRoot(selector);
923
+ for (let i = 1; i < parts.length; i += 1) {
924
+ if (!current) break;
925
+ const idx = Number(parts[i]);
926
+ if (Number.isNaN(idx)) return null;
927
+ const children = current.children || [];
928
+ current = children[idx] || null;
929
+ }
930
+ return current;
931
+ },
932
+ buildPathForElement(el, selector) {
933
+ if (!el || !(el instanceof Element)) return null;
934
+ const root = domUtils.resolveRoot(selector);
935
+ const indices = [];
936
+ let cursor = el;
937
+ let guard = 0;
938
+ while (cursor && guard < 200) {
939
+ if (cursor === root) {
940
+ indices.push('root');
941
+ break;
942
+ }
943
+ const parent = cursor.parentElement;
944
+ if (!parent) break;
945
+ const idx = Array.prototype.indexOf.call(parent.children || [], cursor);
946
+ indices.push(String(idx));
947
+ cursor = parent;
948
+ guard += 1;
949
+ }
950
+ if (!indices.length) return null;
951
+ if (indices[indices.length - 1] !== 'root') {
952
+ indices.push('root');
953
+ }
954
+ return indices.reverse().join('/');
955
+ },
956
+ snapshotNode(el, options = {}) {
957
+ if (!el || !(el instanceof Element)) return null;
958
+ const path = domUtils.buildPathForElement(el, options.rootSelector);
959
+ const childLimit = Number(options.maxChildren || 20);
960
+ const depthLimit = Number(options.maxDepth || 3);
961
+ return domUtils.collectNode(el, {
962
+ path: path || 'root',
963
+ depth: 0,
964
+ depthLimit,
965
+ childLimit,
966
+ forcePaths: options.forcePaths || []
967
+ });
968
+ },
969
+ collectNode(el, ctx) {
970
+ if (!el || !(el instanceof Element)) return null;
971
+ const node = {
972
+ path: ctx.path,
973
+ tag: el.tagName ? el.tagName.toLowerCase() : 'node',
974
+ id: el.id || null,
975
+ classes: Array.from(el.classList || []),
976
+ textSnippet: domUtils.extractText(el),
977
+ text: domUtils.extractText(el),
978
+ childCount: el.children ? el.children.length : 0,
979
+ children: [],
980
+ };
981
+
982
+ // Check if current path needs to be expanded (depthLimit override)
983
+ let shouldExpand = ctx.depth < ctx.depthLimit;
984
+ if (!shouldExpand && ctx.forcePaths && ctx.forcePaths.length > 0) {
985
+ for (const fp of ctx.forcePaths) {
986
+ if (fp.startsWith(ctx.path + '/')) {
987
+ shouldExpand = true;
988
+ break;
989
+ }
990
+ }
991
+ }
992
+
993
+ if (shouldExpand && el.children && el.children.length) {
994
+ const maxChildren = Math.max(1, ctx.childLimit);
995
+ const totalChildren = el.children.length;
996
+ const defaultCount = Math.min(totalChildren, maxChildren);
997
+ const indices = new Set();
998
+
999
+ for (let i = 0; i < defaultCount; i += 1) {
1000
+ indices.add(i);
1001
+ }
1002
+
1003
+ if (ctx.forcePaths && ctx.forcePaths.length > 0) {
1004
+ const prefix = `${ctx.path}/`;
1005
+ for (const fp of ctx.forcePaths) {
1006
+ if (!fp.startsWith(prefix)) continue;
1007
+ const rest = fp.slice(prefix.length);
1008
+ const next = rest.split('/')[0];
1009
+ const idx = Number(next);
1010
+ if (!Number.isNaN(idx) && idx >= 0 && idx < totalChildren) {
1011
+ indices.add(idx);
1012
+ }
1013
+ }
1014
+ }
1015
+
1016
+ const ordered = Array.from(indices).sort((a, b) => a - b);
1017
+ for (const i of ordered) {
1018
+ const child = el.children[i];
1019
+ const childPath = `${ctx.path}/${i}`;
1020
+ const result = domUtils.collectNode(child, {
1021
+ path: childPath,
1022
+ depth: ctx.depth + 1,
1023
+ depthLimit: ctx.depthLimit,
1024
+ childLimit: ctx.childLimit,
1025
+ forcePaths: ctx.forcePaths
1026
+ });
1027
+ if (result) {
1028
+ node.children.push(result);
1029
+ }
1030
+ }
1031
+ }
1032
+ return node;
1033
+ },
1034
+ extractText(el) {
1035
+ if (!el) return '';
1036
+ return (el.textContent || '').replace(/\s+/g, ' ').trim().slice(0, 160);
1037
+ },
1038
+ };
1039
+
1040
+ function ensureOverlayLayer() {
1041
+ let layer = document.getElementById('__camo_highlight_layer');
1042
+ if (layer && layer.parentElement === document.body) return layer;
1043
+ if (!layer) {
1044
+ layer = document.createElement('div');
1045
+ layer.id = '__camo_highlight_layer';
1046
+ }
1047
+ Object.assign(layer.style, {
1048
+ position: 'fixed',
1049
+ left: '0',
1050
+ top: '0',
1051
+ width: '100%',
1052
+ height: '100%',
1053
+ pointerEvents: 'none',
1054
+ zIndex: '2147483646',
1055
+ });
1056
+ if (!layer.parentElement) {
1057
+ document.body.appendChild(layer);
1058
+ }
1059
+ return layer;
1060
+ }
1061
+
1062
+ function createOverlay(rect, style) {
1063
+ const layer = ensureOverlayLayer();
1064
+ const el = document.createElement('div');
1065
+ el.className = '__camo_highlight_box';
1066
+ Object.assign(el.style, {
1067
+ position: 'absolute',
1068
+ boxSizing: 'border-box',
1069
+ left: `${Math.round(rect.x)}px`,
1070
+ top: `${Math.round(rect.y)}px`,
1071
+ width: `${Math.max(0, Math.round(rect.width))}px`,
1072
+ height: `${Math.max(0, Math.round(rect.height))}px`,
1073
+ pointerEvents: 'none',
1074
+ ...style,
1075
+ });
1076
+ layer.appendChild(el);
1077
+ return el;
1078
+ }
1079
+
1080
+ function removeOverlay(overlay) {
1081
+ if (overlay && overlay.parentElement) {
1082
+ overlay.parentElement.removeChild(overlay);
1083
+ }
1084
+ }
1085
+
1086
+ function clearChannel(channel) {
1087
+ const key = channel || 'default';
1088
+ const entry = registry.get(key);
1089
+ if (entry) {
1090
+ if (Array.isArray(entry.items)) {
1091
+ entry.items.forEach((item) => {
1092
+ try { removeOverlay(item.overlay); } catch {}
1093
+ });
1094
+ } else if (Array.isArray(entry.overlays)) {
1095
+ entry.overlays.forEach((ov) => {
1096
+ try { removeOverlay(ov); } catch {}
1097
+ });
1098
+ }
1099
+ }
1100
+ registry.delete(key);
1101
+ }
1102
+
1103
+ let scrollListenerInitialized = false;
1104
+
1105
+ function updateAllOverlays() {
1106
+ registry.forEach((entry) => {
1107
+ if (entry && Array.isArray(entry.items)) {
1108
+ entry.items.forEach(({ overlay, element }) => {
1109
+ if (!element || !element.isConnected || !overlay) return;
1110
+ const rect = element.getBoundingClientRect();
1111
+ if (rect.width > 0 && rect.height > 0) {
1112
+ Object.assign(overlay.style, {
1113
+ left: `${Math.round(rect.x)}px`,
1114
+ top: `${Math.round(rect.y)}px`,
1115
+ width: `${Math.round(rect.width)}px`,
1116
+ height: `${Math.round(rect.height)}px`,
1117
+ display: 'block'
1118
+ });
1119
+ } else {
1120
+ overlay.style.display = 'none';
1121
+ }
1122
+ });
1123
+ }
1124
+ });
1125
+ }
1126
+
1127
+ function setupScrollListener() {
1128
+ if (scrollListenerInitialized) return;
1129
+ scrollListenerInitialized = true;
1130
+
1131
+ let ticking = false;
1132
+ const handler = () => {
1133
+ if (!ticking) {
1134
+ window.requestAnimationFrame(() => {
1135
+ updateAllOverlays();
1136
+ ticking = false;
1137
+ });
1138
+ ticking = true;
1139
+ }
1140
+ };
1141
+
1142
+ window.addEventListener('scroll', handler, { capture: true, passive: true });
1143
+ window.addEventListener('resize', handler, { passive: true });
1144
+ }
1145
+
1146
+
1147
+ function highlightNodes(nodes, options = {}) {
1148
+ const channel = options.channel || 'default';
1149
+ const style = options.style || '2px solid rgba(255, 193, 7, 0.9)';
1150
+ const borderStyle = typeof style === 'string' ? { border: style, borderRadius: '4px' } : style;
1151
+
1152
+ const prev = registry.get(channel);
1153
+ if (prev) {
1154
+ if (Array.isArray(prev.items)) {
1155
+ prev.items.forEach((item) => {
1156
+ try { removeOverlay(item.overlay); } catch {}
1157
+ });
1158
+ } else if (Array.isArray(prev.overlays)) {
1159
+ prev.overlays.forEach((ov) => {
1160
+ try { removeOverlay(ov); } catch {}
1161
+ });
1162
+ }
1163
+ }
1164
+
1165
+ const items = [];
1166
+ const overlays = [];
1167
+ const list = Array.isArray(nodes) ? nodes : [];
1168
+ list.forEach((node) => {
1169
+ if (!(node instanceof Element)) return;
1170
+ const rect = node.getBoundingClientRect();
1171
+ if (!rect || !rect.width || !rect.height) return;
1172
+ const overlay = createOverlay(rect, borderStyle);
1173
+ items.push({ overlay, element: node });
1174
+ overlays.push(overlay);
1175
+ });
1176
+
1177
+ registry.set(channel, {
1178
+ items,
1179
+ overlays,
1180
+ sticky: Boolean(options.sticky || options.hold),
1181
+ cleanup: () => {
1182
+ items.forEach((item) => {
1183
+ try {
1184
+ removeOverlay(item.overlay);
1185
+ } catch {}
1186
+ });
1187
+ },
1188
+ });
1189
+
1190
+ setupScrollListener();
1191
+
1192
+ if (!options.sticky && !options.hold) {
1193
+ const duration = Number(options.duration || 0);
1194
+ if (duration > 0) {
1195
+ setTimeout(() => {
1196
+ clearChannel(channel);
1197
+ }, duration);
1198
+ }
1199
+ }
1200
+
1201
+ return { selector: options.selector || null, count: items.length, channel };
1202
+ }
1203
+
1204
+ function highlightSelector(selector, options = {}) {
1205
+ const rootSelector = options.rootSelector || null;
1206
+ const root = domUtils.resolveRoot(rootSelector);
1207
+ if (!root || !selector) {
1208
+ clearChannel(options.channel);
1209
+ return { selector: selector || null, count: 0, channel: options.channel || 'default' };
1210
+ }
1211
+ let nodes = [];
1212
+ try {
1213
+ const scope = rootSelector ? root : document;
1214
+ if (scope === root && typeof root.matches === 'function' && root.matches(selector)) {
1215
+ nodes.push(root);
1216
+ }
1217
+ nodes = nodes.concat(Array.from(scope.querySelectorAll(selector)));
1218
+ } catch {
1219
+ nodes = [];
1220
+ }
1221
+ return highlightNodes(nodes, { ...options, selector });
1222
+ }
1223
+
1224
+ function getDomBranch(path, options = {}) {
1225
+ const root = domUtils.resolveRoot(options.rootSelector);
1226
+ if (!root) {
1227
+ return {
1228
+ node: null,
1229
+ error: 'root-not-found',
1230
+ };
1231
+ }
1232
+ let target = root;
1233
+ if (path && path !== 'root') {
1234
+ target = domUtils.resolveByPath(path, options.rootSelector) || root;
1235
+ }
1236
+ // Extract forcePaths if provided
1237
+ const forcePaths = Array.isArray(options.forcePaths) ? options.forcePaths : [];
1238
+
1239
+ const node = domUtils.snapshotNode(target, { ...options, forcePaths });
1240
+ return {
1241
+ node,
1242
+ path: node?.path || 'root',
1243
+ capturedAt: Date.now(),
1244
+ };
1245
+ }
1246
+
1247
+ function getNodeDetails(path, options = {}) {
1248
+ const el = domUtils.resolveByPath(path, options.rootSelector);
1249
+ if (!el) {
1250
+ return { path, exists: false };
1251
+ }
1252
+ const rect = el.getBoundingClientRect();
1253
+ return {
1254
+ path,
1255
+ exists: true,
1256
+ tag: el.tagName ? el.tagName.toLowerCase() : 'node',
1257
+ id: el.id || null,
1258
+ classes: Array.from(el.classList || []),
1259
+ text: domUtils.extractText(el),
1260
+ boundingRect: {
1261
+ x: rect.left,
1262
+ y: rect.top,
1263
+ width: rect.width,
1264
+ height: rect.height,
1265
+ },
1266
+ };
1267
+ }
1268
+
1269
+ function bootRuntime() {
1270
+ // dom-picker bootstrap: ensure __domPicker is loaded if bundled separately
1271
+ try {
1272
+ // noop; domPicker.runtime.js attaches itself to window when included by loader
1273
+ } catch { /* ignore */ }
1274
+
1275
+ const runtime = {
1276
+ version: VERSION,
1277
+ ready: true,
1278
+ highlight: {
1279
+ highlightSelector,
1280
+ highlightElements: highlightNodes,
1281
+ clear: clearChannel,
1282
+ },
1283
+ dom: {
1284
+ getBranch: getDomBranch,
1285
+ getNodeDetails,
1286
+ buildPathForElement: domUtils.buildPathForElement,
1287
+ resolveByPath: domUtils.resolveByPath,
1288
+ },
1289
+ getDomBranch,
1290
+ ping() {
1291
+ return { ts: Date.now(), href: window.location.href };
1292
+ },
1293
+ get domPicker() {
1294
+ return window.__domPicker || null;
1295
+ },
1296
+ };
1297
+ Object.defineProperty(window, '__camoRuntime', {
1298
+ value: runtime,
1299
+ configurable: true,
1300
+ enumerable: false,
1301
+ writable: false,
1302
+ });
1303
+ notifyHandshakeStatus('ready');
1304
+ }
1305
+
1306
+ if (document.readyState === 'loading') {
1307
+ document.addEventListener(
1308
+ 'DOMContentLoaded',
1309
+ () => {
1310
+ bootRuntime();
1311
+ },
1312
+ { once: true },
1313
+ );
1314
+ } else {
1315
+ bootRuntime();
1316
+ }
1317
+ })();
@@ -8,6 +8,7 @@ function resolveRuntime() {
8
8
  const base = path.dirname(fileURLToPath(import.meta.url));
9
9
  const candidates = [
10
10
  path.join(process.cwd(), 'modules/camo-backend/src/internal/page-runtime/runtime.js'),
11
+ path.join(process.cwd(), 'src/services/browser-service/internal/page-runtime/runtime.js'),
11
12
  path.join(base, 'page-runtime/runtime.js'),
12
13
  ];
13
14
  for (const candidate of candidates) {
@@ -27,4 +28,4 @@ export async function injectRuntimeBundle({ page }) {
27
28
  await page.addInitScript({ path: runtimePath });
28
29
  await page.addScriptTag({ path: runtimePath });
29
30
  }
30
- //# sourceMappingURL=runtimeInjector.js.map
31
+ //# sourceMappingURL=runtimeInjector.js.map