@oxvo/ai-live-assist 7.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.
Files changed (76) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +17 -0
  3. package/cjs/AiLiveAssist.d.ts +108 -0
  4. package/cjs/AiLiveAssist.js +1774 -0
  5. package/cjs/client.d.ts +53 -0
  6. package/cjs/client.js +193 -0
  7. package/cjs/context.d.ts +58 -0
  8. package/cjs/context.js +979 -0
  9. package/cjs/control.d.ts +31 -0
  10. package/cjs/control.js +190 -0
  11. package/cjs/experienceState.d.ts +18 -0
  12. package/cjs/experienceState.js +82 -0
  13. package/cjs/index.d.ts +13 -0
  14. package/cjs/index.js +32 -0
  15. package/cjs/media.d.ts +34 -0
  16. package/cjs/media.js +207 -0
  17. package/cjs/messages.d.ts +2 -0
  18. package/cjs/messages.js +95 -0
  19. package/cjs/package.json +1 -0
  20. package/cjs/placement.d.ts +52 -0
  21. package/cjs/placement.js +293 -0
  22. package/cjs/presentation.d.ts +41 -0
  23. package/cjs/presentation.js +483 -0
  24. package/cjs/recordingPolicy.d.ts +2 -0
  25. package/cjs/recordingPolicy.js +12 -0
  26. package/cjs/safeSvg.d.ts +1 -0
  27. package/cjs/safeSvg.js +157 -0
  28. package/cjs/tabLock.d.ts +31 -0
  29. package/cjs/tabLock.js +260 -0
  30. package/cjs/types.d.ts +299 -0
  31. package/cjs/types.js +2 -0
  32. package/cjs/ui.d.ts +184 -0
  33. package/cjs/ui.js +2353 -0
  34. package/cjs/version.d.ts +1 -0
  35. package/cjs/version.js +4 -0
  36. package/cjs/visualContext.d.ts +21 -0
  37. package/cjs/visualContext.js +72 -0
  38. package/cjs/voicePresenceUi.d.ts +148 -0
  39. package/cjs/voicePresenceUi.js +2182 -0
  40. package/lib/AiLiveAssist.d.ts +108 -0
  41. package/lib/AiLiveAssist.js +1769 -0
  42. package/lib/client.d.ts +53 -0
  43. package/lib/client.js +187 -0
  44. package/lib/context.d.ts +58 -0
  45. package/lib/context.js +975 -0
  46. package/lib/control.d.ts +31 -0
  47. package/lib/control.js +186 -0
  48. package/lib/experienceState.d.ts +18 -0
  49. package/lib/experienceState.js +78 -0
  50. package/lib/index.d.ts +13 -0
  51. package/lib/index.js +26 -0
  52. package/lib/media.d.ts +34 -0
  53. package/lib/media.js +203 -0
  54. package/lib/messages.d.ts +2 -0
  55. package/lib/messages.js +92 -0
  56. package/lib/placement.d.ts +52 -0
  57. package/lib/placement.js +286 -0
  58. package/lib/presentation.d.ts +41 -0
  59. package/lib/presentation.js +478 -0
  60. package/lib/recordingPolicy.d.ts +2 -0
  61. package/lib/recordingPolicy.js +8 -0
  62. package/lib/safeSvg.d.ts +1 -0
  63. package/lib/safeSvg.js +153 -0
  64. package/lib/tabLock.d.ts +31 -0
  65. package/lib/tabLock.js +256 -0
  66. package/lib/types.d.ts +299 -0
  67. package/lib/types.js +1 -0
  68. package/lib/ui.d.ts +184 -0
  69. package/lib/ui.js +2349 -0
  70. package/lib/version.d.ts +1 -0
  71. package/lib/version.js +1 -0
  72. package/lib/visualContext.d.ts +21 -0
  73. package/lib/visualContext.js +68 -0
  74. package/lib/voicePresenceUi.d.ts +148 -0
  75. package/lib/voicePresenceUi.js +2178 -0
  76. package/package.json +58 -0
package/lib/context.js ADDED
@@ -0,0 +1,975 @@
1
+ import { renderSanitizedLayout } from './visualContext.js';
2
+ const MAX_TARGETS = 256;
3
+ const MAX_TEXT_ITEMS = 256;
4
+ const MAX_TOTAL_TEXT = 24000;
5
+ const MAX_OPTIONS = 100;
6
+ const MAX_SELECTED_TEXT = 2000;
7
+ const SELECTION_RETENTION_MS = 120000;
8
+ const PRIVATE_SELECTOR = [
9
+ '[data-oxvo-ai-live-assist-root]',
10
+ '[data-oxvo-private]',
11
+ '[data-openreplay-hidden]',
12
+ '[data-openreplay-obscured]',
13
+ '[contenteditable]:not([contenteditable="false"])',
14
+ '[aria-hidden="true"]',
15
+ '[inert]',
16
+ '[hidden]',
17
+ ].join(',');
18
+ const SENSITIVE_PATTERNS = [
19
+ ['password', /password|passwd|passcode/i],
20
+ ['otp', /\botp\b|one.?time|verification.?code|security.?code|2fa|mfa/i],
21
+ ['payment', /card.?number|credit.?card|debit.?card|\bcvv\b|\bcvc\b|expiry/i],
22
+ ['banking', /bank|routing|iban|swift|account.?number/i],
23
+ ['recovery', /recovery.?code|backup.?code/i],
24
+ ['authentication_secret', /auth.*secret|private.?key|access.?token/i],
25
+ ['api_secret', /api.?key|client.?secret|secret.?key/i],
26
+ ];
27
+ const id = (prefix) => `${prefix}_${crypto.randomUUID()}`;
28
+ const normalize = (value, maximum) => value
29
+ .replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, '')
30
+ .replace(/\s+/g, ' ')
31
+ .trim()
32
+ .slice(0, maximum);
33
+ const isHTMLElement = (value) => value instanceof HTMLElement;
34
+ const isRendered = (element) => {
35
+ if (!isHTMLElement(element) || !element.isConnected || element.closest(PRIVATE_SELECTOR)) {
36
+ return false;
37
+ }
38
+ const style = window.getComputedStyle(element);
39
+ const opacity = Number.parseFloat(style.opacity);
40
+ if (style.display === 'none' ||
41
+ style.visibility === 'hidden' ||
42
+ (Number.isFinite(opacity) && opacity === 0) ||
43
+ style.pointerEvents === 'none') {
44
+ return false;
45
+ }
46
+ const rect = element.getBoundingClientRect();
47
+ return rect.width > 0 && rect.height > 0;
48
+ };
49
+ const isVisible = (element) => {
50
+ if (!isRendered(element))
51
+ return false;
52
+ const rect = element.getBoundingClientRect();
53
+ return (rect.bottom >= 0 &&
54
+ rect.right >= 0 &&
55
+ rect.top <= window.innerHeight &&
56
+ rect.left <= window.innerWidth);
57
+ };
58
+ const isCovered = (element) => {
59
+ const rect = element.getBoundingClientRect();
60
+ const x = Math.max(0, Math.min(window.innerWidth - 1, rect.left + rect.width / 2));
61
+ const y = Math.max(0, Math.min(window.innerHeight - 1, rect.top + rect.height / 2));
62
+ const top = document.elementFromPoint(x, y);
63
+ return Boolean(top && top !== element && !element.contains(top) && !top.contains(element));
64
+ };
65
+ const safePageUrl = () => `${window.location.origin}${window.location.pathname}`;
66
+ const sensitivityFor = (element, name) => {
67
+ const input = element instanceof HTMLInputElement ? element : null;
68
+ const type = input?.type.toLowerCase() ?? '';
69
+ const autocomplete = input?.autocomplete.toLowerCase() ?? '';
70
+ if (type === 'password' || autocomplete.includes('password'))
71
+ return 'password';
72
+ if (autocomplete.includes('one-time-code'))
73
+ return 'otp';
74
+ if (/^cc-/.test(autocomplete))
75
+ return 'payment';
76
+ const descriptors = [
77
+ name,
78
+ element.getAttribute('name') ?? '',
79
+ element.id,
80
+ element.getAttribute('autocomplete') ?? '',
81
+ element.getAttribute('data-oxvo-sensitivity') ?? '',
82
+ ].join(' ');
83
+ for (const [sensitivity, pattern] of SENSITIVE_PATTERNS) {
84
+ if (pattern.test(descriptors))
85
+ return sensitivity;
86
+ }
87
+ if (element.closest('[data-oxvo-private]'))
88
+ return 'private';
89
+ return 'none';
90
+ };
91
+ const roleFor = (element) => {
92
+ const explicit = normalize(element.getAttribute('role') ?? '', 64);
93
+ if (explicit)
94
+ return explicit;
95
+ const tag = element.tagName.toLowerCase();
96
+ if (tag === 'a')
97
+ return 'link';
98
+ if (tag === 'button')
99
+ return 'button';
100
+ if (tag === 'select')
101
+ return 'combobox';
102
+ if (tag === 'textarea')
103
+ return 'textbox';
104
+ if (tag === 'form')
105
+ return 'form';
106
+ if (tag === 'summary')
107
+ return 'button';
108
+ if (element instanceof HTMLInputElement) {
109
+ if (['button', 'submit', 'reset'].includes(element.type))
110
+ return 'button';
111
+ if (['checkbox', 'radio'].includes(element.type))
112
+ return element.type;
113
+ if (element.type === 'number')
114
+ return 'spinbutton';
115
+ return 'textbox';
116
+ }
117
+ return tag.slice(0, 64);
118
+ };
119
+ const nameFor = (element) => {
120
+ const labelledBy = element.getAttribute('aria-labelledby');
121
+ const labelledText = labelledBy
122
+ ?.split(/\s+/)
123
+ .map((value) => document.getElementById(value)?.textContent ?? '')
124
+ .join(' ');
125
+ const input = element instanceof HTMLInputElement ? element : null;
126
+ const formControl = element instanceof HTMLInputElement ||
127
+ element instanceof HTMLSelectElement ||
128
+ element instanceof HTMLTextAreaElement
129
+ ? element
130
+ : null;
131
+ const associatedLabel = formControl
132
+ ? Array.from(formControl.labels ?? [])
133
+ .flatMap((label) => {
134
+ const walker = document.createTreeWalker(label, NodeFilter.SHOW_TEXT);
135
+ const text = [];
136
+ let node = walker.nextNode();
137
+ while (node) {
138
+ if (!node.parentElement?.closest('input,select,textarea,option,button')) {
139
+ text.push(node.textContent ?? '');
140
+ }
141
+ node = walker.nextNode();
142
+ }
143
+ return text;
144
+ })
145
+ .join(' ')
146
+ : '';
147
+ const explicitName = element.getAttribute('aria-label') ??
148
+ labelledText ??
149
+ element.getAttribute('alt') ??
150
+ element.getAttribute('title') ??
151
+ element.getAttribute('placeholder');
152
+ if (element instanceof HTMLFormElement) {
153
+ return normalize(explicitName ??
154
+ element.getAttribute('data-oxvo-ai-region') ??
155
+ element.getAttribute('name') ??
156
+ 'Form', 120);
157
+ }
158
+ return normalize(explicitName ??
159
+ (associatedLabel || null) ??
160
+ (input && ['button', 'submit', 'reset'].includes(input.type) ? input.value : null) ??
161
+ element.textContent ??
162
+ '', 300);
163
+ };
164
+ const isActionTarget = (element) => element.matches('a[href],button,input,select,textarea,summary,form,[contenteditable="true"],[role="button"],[role="link"],[role="checkbox"],[role="radio"],[role="combobox"],[role="textbox"],[tabindex]');
165
+ const enabled = (element) => {
166
+ if (element.getAttribute('aria-disabled') === 'true')
167
+ return false;
168
+ if (element instanceof HTMLButtonElement ||
169
+ element instanceof HTMLInputElement ||
170
+ element instanceof HTMLSelectElement ||
171
+ element instanceof HTMLTextAreaElement) {
172
+ return !element.disabled;
173
+ }
174
+ return true;
175
+ };
176
+ const consequenceFor = (element, role, name) => {
177
+ const form = element instanceof HTMLFormElement ? element : element.closest('form');
178
+ const explicit = element
179
+ .closest('[data-oxvo-ai-consequence]')
180
+ ?.getAttribute('data-oxvo-ai-consequence');
181
+ const description = normalize([
182
+ role,
183
+ name,
184
+ element.getAttribute('aria-label'),
185
+ element.getAttribute('title'),
186
+ element.getAttribute('name'),
187
+ element.getAttribute('data-action'),
188
+ form?.getAttribute('aria-label'),
189
+ form?.getAttribute('name'),
190
+ form?.getAttribute('data-oxvo-ai-region'),
191
+ ]
192
+ .filter(Boolean)
193
+ .join(' '), 800).toLowerCase();
194
+ // High-impact labels cannot be downgraded by page-provided metadata.
195
+ if (/\b(?:delete|destroy|erase|close account|cancel account|remove (?:account|member|project|workspace)|archive (?:account|project|workspace)|supprimer|eliminar)\b/u.test(description)) {
196
+ return 'destructive';
197
+ }
198
+ if (/\b(?:pay|purchase|buy|place order|checkout|subscribe|upgrade plan|downgrade plan|renew subscription|acheter|comprar|pagar)\b/u.test(description)) {
199
+ return 'financial';
200
+ }
201
+ if (/\b(?:(?:manage|change|edit|update|grant|revoke|assign)\s+(?:member\s+|user\s+|workspace\s+|account\s+)?permissions?|permissions?\s+(?:settings?|access|polic(?:y|ies)|roles?)|member role|workspace owner|account access|api access|security settings?|authentication settings?|invite member)\b/u.test(description)) {
202
+ return 'account';
203
+ }
204
+ if (/\b(?:publish|send (?:message|email|invitation)|share publicly|post comment|register account)\b/u.test(description)) {
205
+ return 'submission';
206
+ }
207
+ if (explicit === 'none' ||
208
+ explicit === 'navigation' ||
209
+ explicit === 'state_change' ||
210
+ explicit === 'submission' ||
211
+ explicit === 'financial' ||
212
+ explicit === 'account' ||
213
+ explicit === 'destructive') {
214
+ return explicit;
215
+ }
216
+ if (element instanceof HTMLFormElement ||
217
+ (element instanceof HTMLInputElement && element.type === 'submit') ||
218
+ (element instanceof HTMLButtonElement && element.type === 'submit')) {
219
+ return 'submission';
220
+ }
221
+ if (element instanceof HTMLAnchorElement)
222
+ return 'navigation';
223
+ return role === 'button' || role === 'checkbox' || role === 'radio' ? 'state_change' : 'none';
224
+ };
225
+ export class PageContextCollector {
226
+ constructor(callbacks, selectorRegionRules = [], policyVersion = 1) {
227
+ this.callbacks = callbacks;
228
+ this.selectorRegionRules = selectorRegionRules;
229
+ this.policyVersion = policyVersion;
230
+ this.pageId = id('page');
231
+ this.revision = 0;
232
+ this.running = false;
233
+ this.mutationObserver = null;
234
+ this.captureTimer = null;
235
+ this.targetIds = new WeakMap();
236
+ this.textIds = new WeakMap();
237
+ this.optionIds = new WeakMap();
238
+ this.targets = new Map();
239
+ this.originalPushState = null;
240
+ this.originalReplaceState = null;
241
+ this.executingActionId = null;
242
+ this.pendingNavigation = null;
243
+ this.pendingCapture = false;
244
+ this.deferredNavigation = null;
245
+ this.recentSelection = null;
246
+ this.scheduleCapture = () => {
247
+ if (!this.running)
248
+ return;
249
+ if (this.executingActionId) {
250
+ this.pendingCapture = true;
251
+ return;
252
+ }
253
+ if (this.captureTimer)
254
+ return;
255
+ this.captureTimer = setTimeout(() => {
256
+ this.captureTimer = null;
257
+ void this.captureAndPublish();
258
+ }, 250);
259
+ };
260
+ this.handleSelectionChange = () => {
261
+ const selection = this.readSafeSelection();
262
+ if (selection)
263
+ this.recentSelection = selection;
264
+ this.scheduleCapture();
265
+ };
266
+ this.handleControlStateChange = () => {
267
+ this.scheduleCapture();
268
+ };
269
+ this.handleNavigation = () => {
270
+ if (!this.running)
271
+ return;
272
+ this.pageId = id('page');
273
+ this.revision += 1;
274
+ this.targets.clear();
275
+ this.recentSelection = null;
276
+ if (this.executingActionId) {
277
+ this.pendingNavigation = {
278
+ pageId: this.pageId,
279
+ revision: this.revision,
280
+ url: safePageUrl(),
281
+ };
282
+ this.pendingCapture = true;
283
+ return;
284
+ }
285
+ void Promise.resolve(this.callbacks.onNavigation(this.pageId, this.revision, safePageUrl())).then(() => this.captureAndPublish());
286
+ };
287
+ }
288
+ get currentRevision() {
289
+ return this.revision;
290
+ }
291
+ get currentPageId() {
292
+ return this.pageId;
293
+ }
294
+ presentationTarget(targetId, revision) {
295
+ const handle = this.targets.get(targetId);
296
+ if (revision !== this.revision ||
297
+ !handle ||
298
+ handle.revision !== revision ||
299
+ !handle.element.isConnected) {
300
+ return null;
301
+ }
302
+ const rect = handle.element.getBoundingClientRect();
303
+ return {
304
+ targetId,
305
+ pageId: this.pageId,
306
+ revision,
307
+ rect,
308
+ safe: handle.target.sensitivity === 'none' &&
309
+ handle.target.protectedRegion === false &&
310
+ handle.target.frameOrigin === 'same_origin' &&
311
+ handle.target.state.visible &&
312
+ handle.target.state.enabled &&
313
+ handle.target.state.covered !== true,
314
+ };
315
+ }
316
+ start() {
317
+ if (this.running)
318
+ return;
319
+ this.running = true;
320
+ this.installNavigationHooks();
321
+ this.mutationObserver = new MutationObserver(() => this.scheduleCapture());
322
+ if (document.body) {
323
+ this.mutationObserver.observe(document.body, {
324
+ subtree: true,
325
+ childList: true,
326
+ characterData: true,
327
+ attributes: true,
328
+ attributeFilter: [
329
+ 'aria-label',
330
+ 'aria-disabled',
331
+ 'aria-expanded',
332
+ 'aria-live',
333
+ 'class',
334
+ 'disabled',
335
+ 'hidden',
336
+ 'inert',
337
+ 'role',
338
+ 'style',
339
+ ],
340
+ });
341
+ }
342
+ window.addEventListener('resize', this.scheduleCapture, { passive: true });
343
+ document.addEventListener('scroll', this.scheduleCapture, {
344
+ capture: true,
345
+ passive: true,
346
+ });
347
+ window.visualViewport?.addEventListener('resize', this.scheduleCapture, {
348
+ passive: true,
349
+ });
350
+ window.visualViewport?.addEventListener('scroll', this.scheduleCapture, {
351
+ passive: true,
352
+ });
353
+ window.addEventListener('popstate', this.handleNavigation);
354
+ document.addEventListener('selectionchange', this.handleSelectionChange);
355
+ document.addEventListener('input', this.handleControlStateChange, true);
356
+ document.addEventListener('change', this.handleControlStateChange, true);
357
+ void this.captureAndPublish();
358
+ }
359
+ stop() {
360
+ this.running = false;
361
+ this.mutationObserver?.disconnect();
362
+ this.mutationObserver = null;
363
+ if (this.captureTimer)
364
+ clearTimeout(this.captureTimer);
365
+ this.captureTimer = null;
366
+ window.removeEventListener('resize', this.scheduleCapture);
367
+ document.removeEventListener('scroll', this.scheduleCapture, true);
368
+ window.visualViewport?.removeEventListener('resize', this.scheduleCapture);
369
+ window.visualViewport?.removeEventListener('scroll', this.scheduleCapture);
370
+ window.removeEventListener('popstate', this.handleNavigation);
371
+ document.removeEventListener('selectionchange', this.handleSelectionChange);
372
+ document.removeEventListener('input', this.handleControlStateChange, true);
373
+ document.removeEventListener('change', this.handleControlStateChange, true);
374
+ this.restoreNavigationHooks();
375
+ this.targets.clear();
376
+ this.executingActionId = null;
377
+ this.pendingNavigation = null;
378
+ this.pendingCapture = false;
379
+ this.deferredNavigation = null;
380
+ this.recentSelection = null;
381
+ this.callbacks.onHighlight(null, 'focus');
382
+ }
383
+ async captureAndPublish() {
384
+ const snapshot = this.capture();
385
+ if (this.running)
386
+ await this.callbacks.onSnapshot(snapshot);
387
+ return snapshot;
388
+ }
389
+ capture() {
390
+ this.revision += 1;
391
+ const revision = this.revision;
392
+ const nextTargets = new Map();
393
+ const targetValues = [];
394
+ const elements = document.querySelectorAll('a[href],button,input,select,textarea,summary,form,[contenteditable="true"],[role],[tabindex]');
395
+ let truncated = false;
396
+ for (const element of elements) {
397
+ if (targetValues.length >= MAX_TARGETS) {
398
+ truncated = true;
399
+ break;
400
+ }
401
+ if (!isActionTarget(element) || !isRendered(element))
402
+ continue;
403
+ const name = nameFor(element);
404
+ if (!name && !['form', 'textbox'].includes(roleFor(element)))
405
+ continue;
406
+ const targetId = this.targetIds.get(element) ?? id('target');
407
+ this.targetIds.set(element, targetId);
408
+ const optionMap = new Map();
409
+ let options;
410
+ if (element instanceof HTMLSelectElement) {
411
+ options = Array.from(element.options)
412
+ .slice(0, MAX_OPTIONS)
413
+ .map((option) => {
414
+ const optionId = this.optionIds.get(option) ?? id('option');
415
+ this.optionIds.set(option, optionId);
416
+ optionMap.set(optionId, option);
417
+ return {
418
+ id: optionId,
419
+ label: normalize(option.label || option.textContent || '', 300),
420
+ disabled: option.disabled,
421
+ };
422
+ });
423
+ }
424
+ const semanticRegion = normalize(element.closest('[data-oxvo-ai-region]')?.getAttribute('data-oxvo-ai-region') ?? '', 128);
425
+ const evaluatedRuleDigests = [];
426
+ const matchedRuleDigests = [];
427
+ for (const rule of this.selectorRegionRules.slice(0, 100)) {
428
+ if (!/^[0-9a-f]{64}$/.test(rule.digest) || rule.selector.length > 256) {
429
+ continue;
430
+ }
431
+ try {
432
+ const matched = element.matches(rule.selector) || Boolean(element.closest(rule.selector));
433
+ evaluatedRuleDigests.push(rule.digest);
434
+ if (matched)
435
+ matchedRuleDigests.push(rule.digest);
436
+ }
437
+ catch {
438
+ // Invalid tenant selectors remain unevaluated so server policy fails closed.
439
+ }
440
+ }
441
+ const role = roleFor(element);
442
+ const sensitivity = sensitivityFor(element, name);
443
+ const visible = isVisible(element);
444
+ const target = {
445
+ targetId,
446
+ revision,
447
+ role,
448
+ name,
449
+ ...(element instanceof HTMLInputElement || element instanceof HTMLButtonElement
450
+ ? { inputType: normalize(element.type, 32) }
451
+ : {}),
452
+ ...(element.getAttribute('autocomplete')
453
+ ? { autocomplete: normalize(element.getAttribute('autocomplete') ?? '', 128) }
454
+ : {}),
455
+ ...(semanticRegion ? { semanticRegion } : {}),
456
+ sensitivity,
457
+ protectedRegion: sensitivity !== 'none' || matchedRuleDigests.length > 0,
458
+ frameOrigin: 'same_origin',
459
+ consequence: consequenceFor(element, role, name),
460
+ regionAssessment: {
461
+ policyVersion: this.policyVersion,
462
+ evaluatedRuleDigests,
463
+ matchedRuleDigests,
464
+ },
465
+ state: {
466
+ visible,
467
+ enabled: enabled(element),
468
+ covered: visible ? isCovered(element) : false,
469
+ ...(element instanceof HTMLInputElement && ['checkbox', 'radio'].includes(element.type)
470
+ ? { checked: element.checked }
471
+ : {}),
472
+ },
473
+ ...(options ? { options } : {}),
474
+ };
475
+ targetValues.push(target);
476
+ nextTargets.set(targetId, { element, revision, options: optionMap, target });
477
+ }
478
+ this.targets = nextTargets;
479
+ const visibleText = [];
480
+ let totalText = 0;
481
+ const walker = document.createTreeWalker(document.body ?? document.documentElement, NodeFilter.SHOW_TEXT);
482
+ let node = walker.nextNode();
483
+ while (node && visibleText.length < MAX_TEXT_ITEMS && totalText < MAX_TOTAL_TEXT) {
484
+ const parent = node.parentElement;
485
+ const text = normalize(node.textContent ?? '', 500);
486
+ if (parent &&
487
+ text &&
488
+ !parent.closest(PRIVATE_SELECTOR) &&
489
+ !parent.closest('script,style,noscript,template,svg') &&
490
+ !parent.closest('input,select,textarea,option') &&
491
+ isVisible(parent) &&
492
+ sensitivityFor(parent, nameFor(parent)) === 'none') {
493
+ const textId = this.textIds.get(node) ?? id('text');
494
+ this.textIds.set(node, textId);
495
+ visibleText.push({ id: textId, text, provenance: 'page' });
496
+ totalText += text.length;
497
+ }
498
+ node = walker.nextNode();
499
+ }
500
+ if (node)
501
+ truncated = true;
502
+ const landmarks = Array.from(document.querySelectorAll('main,nav,header,footer,aside,[role="main"],[role="navigation"]'))
503
+ .filter(isVisible)
504
+ .slice(0, 32)
505
+ .map((element) => ({ role: roleFor(element), label: nameFor(element) }));
506
+ const alerts = Array.from(document.querySelectorAll('[role="alert"],[aria-live]'))
507
+ .filter(isVisible)
508
+ .slice(0, 16)
509
+ .map((element) => normalize(element.textContent ?? '', 500))
510
+ .filter(Boolean);
511
+ const activeSelection = this.readSafeSelection();
512
+ if (activeSelection)
513
+ this.recentSelection = activeSelection;
514
+ if (this.recentSelection &&
515
+ Date.now() - Date.parse(this.recentSelection.capturedAt) > SELECTION_RETENTION_MS) {
516
+ this.recentSelection = null;
517
+ }
518
+ return {
519
+ schemaVersion: 1,
520
+ pageId: this.pageId,
521
+ revision,
522
+ url: safePageUrl(),
523
+ title: normalize(document.title, 512),
524
+ locale: normalize(document.documentElement.lang || navigator.language || 'en', 16),
525
+ viewport: {
526
+ width: Math.max(1, Math.round(window.innerWidth)),
527
+ height: Math.max(1, Math.round(window.innerHeight)),
528
+ scrollY: Math.max(0, Math.round(window.scrollY)),
529
+ },
530
+ landmarks,
531
+ visibleText,
532
+ ...(this.recentSelection ? { selection: this.recentSelection } : {}),
533
+ targets: targetValues,
534
+ alerts,
535
+ truncated,
536
+ };
537
+ }
538
+ captureVisualContext() {
539
+ if (this.revision < 1 || this.targets.size === 0)
540
+ return null;
541
+ const viewportWidth = Math.max(1, Math.round(window.innerWidth));
542
+ const viewportHeight = Math.max(1, Math.round(window.innerHeight));
543
+ const regions = [];
544
+ for (const handle of this.targets.values()) {
545
+ const rect = handle.element.getBoundingClientRect();
546
+ const geometry = this.clippedGeometry(rect, viewportWidth, viewportHeight);
547
+ if (!geometry)
548
+ continue;
549
+ const protectedTarget = handle.target.sensitivity !== 'none' || handle.target.protectedRegion !== false;
550
+ regions.push({
551
+ ...geometry,
552
+ kind: protectedTarget ? 'mask' : 'target',
553
+ role: protectedTarget ? 'protected' : handle.target.role,
554
+ label: protectedTarget ? '' : normalize(handle.target.name, 80),
555
+ });
556
+ if (regions.length >= 256)
557
+ break;
558
+ }
559
+ const protectedElements = document.querySelectorAll(`${PRIVATE_SELECTOR},iframe`);
560
+ for (const element of protectedElements) {
561
+ if (element.matches('[data-oxvo-ai-live-assist-root]'))
562
+ continue;
563
+ const geometry = this.clippedGeometry(element.getBoundingClientRect(), viewportWidth, viewportHeight);
564
+ if (!geometry)
565
+ continue;
566
+ regions.push({ ...geometry, kind: 'mask', role: 'protected', label: '' });
567
+ if (regions.length >= 320)
568
+ break;
569
+ }
570
+ return renderSanitizedLayout({
571
+ pageId: this.pageId,
572
+ revision: this.revision,
573
+ viewport: { width: viewportWidth, height: viewportHeight },
574
+ regions,
575
+ });
576
+ }
577
+ async execute(grant) {
578
+ const handle = this.targets.get(grant.targetId);
579
+ const base = {
580
+ grant: grant.grant,
581
+ actionId: grant.actionId,
582
+ };
583
+ if (Date.parse(grant.expiresAt) <= Date.now() ||
584
+ grant.domRevision !== this.revision ||
585
+ !handle ||
586
+ handle.revision !== this.revision ||
587
+ !handle.element.isConnected) {
588
+ return {
589
+ ...base,
590
+ status: 'stale',
591
+ observed: {
592
+ url: safePageUrl(),
593
+ revision: this.revision,
594
+ targetState: 'detached',
595
+ safeSummary: 'The requested target changed before execution.',
596
+ },
597
+ };
598
+ }
599
+ const element = handle.element;
600
+ const sensitivity = sensitivityFor(element, nameFor(element));
601
+ const canRevealDesiredStateTarget = grant.action === 'click' &&
602
+ grant.desiredChecked !== undefined &&
603
+ element instanceof HTMLInputElement &&
604
+ ['checkbox', 'radio'].includes(element.type);
605
+ if (sensitivity !== 'none' ||
606
+ handle.target.protectedRegion !== false ||
607
+ !isRendered(element) ||
608
+ (grant.action !== 'scroll' && !isVisible(element) && !canRevealDesiredStateTarget) ||
609
+ !enabled(element) ||
610
+ (grant.action !== 'scroll' && isVisible(element) && isCovered(element))) {
611
+ return {
612
+ ...base,
613
+ status: 'blocked',
614
+ observed: {
615
+ url: safePageUrl(),
616
+ revision: this.revision,
617
+ targetState: 'unchanged',
618
+ safeSummary: 'The target is unavailable or protected.',
619
+ },
620
+ };
621
+ }
622
+ if (!isVisible(element) && canRevealDesiredStateTarget) {
623
+ element.scrollIntoView({ behavior: 'auto', block: 'center' });
624
+ await new Promise((resolve) => setTimeout(resolve, 80));
625
+ if (!isRendered(element) || !isVisible(element) || isCovered(element)) {
626
+ return {
627
+ ...base,
628
+ status: 'blocked',
629
+ observed: {
630
+ url: safePageUrl(),
631
+ revision: this.revision,
632
+ targetState: 'unchanged',
633
+ safeSummary: 'The target could not be safely brought into view.',
634
+ },
635
+ };
636
+ }
637
+ }
638
+ const before = this.safeState(element);
639
+ this.executingActionId = grant.actionId;
640
+ this.pendingCapture = false;
641
+ this.deferredNavigation = null;
642
+ this.callbacks.onHighlight(element, 'focus');
643
+ try {
644
+ switch (grant.action) {
645
+ case 'scroll':
646
+ element.scrollIntoView({
647
+ behavior: this.reducedMotion() ? 'auto' : 'smooth',
648
+ block: 'center',
649
+ });
650
+ break;
651
+ case 'highlight':
652
+ break;
653
+ case 'focus':
654
+ if (!(element instanceof HTMLElement))
655
+ throw new Error('Target cannot receive focus.');
656
+ element.focus({ preventScroll: false });
657
+ break;
658
+ case 'click':
659
+ this.click(element, grant.actionId, grant.desiredChecked);
660
+ break;
661
+ case 'type':
662
+ this.type(element, grant.value ?? '');
663
+ break;
664
+ case 'select':
665
+ this.select(element, handle, grant.optionValueId);
666
+ break;
667
+ case 'submit':
668
+ this.submit(element);
669
+ break;
670
+ }
671
+ await new Promise((resolve) => setTimeout(resolve, 80));
672
+ if (!this.actionApplied(grant, element, handle)) {
673
+ throw new Error('The approved value was not applied.');
674
+ }
675
+ const snapshot = this.capture();
676
+ const attached = element.isConnected;
677
+ const after = attached ? this.safeState(element) : '';
678
+ this.callbacks.onHighlight(attached ? element : null, 'success');
679
+ return {
680
+ ...base,
681
+ status: 'executed',
682
+ observed: {
683
+ url: safePageUrl(),
684
+ revision: snapshot.revision,
685
+ targetState: !attached
686
+ ? 'detached'
687
+ : grant.action === 'type' || grant.action === 'select' || before !== after
688
+ ? 'changed'
689
+ : 'unchanged',
690
+ safeSummary: grant.action === 'type'
691
+ ? 'The requested text was applied to the approved field.'
692
+ : grant.action === 'select'
693
+ ? 'The requested option was applied to the approved field.'
694
+ : grant.desiredChecked !== undefined
695
+ ? 'The requested checkbox state was applied.'
696
+ : `${grant.action} executed on the approved ${roleFor(element)} target.`,
697
+ ...(element instanceof HTMLInputElement && ['checkbox', 'radio'].includes(element.type)
698
+ ? { checked: element.checked }
699
+ : {}),
700
+ },
701
+ };
702
+ }
703
+ catch {
704
+ this.callbacks.onHighlight(element, 'failure');
705
+ return {
706
+ ...base,
707
+ status: 'failed',
708
+ observed: {
709
+ url: safePageUrl(),
710
+ revision: this.revision,
711
+ targetState: element.isConnected ? 'unchanged' : 'detached',
712
+ safeSummary: 'The approved page action failed safely.',
713
+ },
714
+ };
715
+ }
716
+ }
717
+ async completeActionExecution(actionId) {
718
+ if (this.executingActionId !== actionId)
719
+ return null;
720
+ this.executingActionId = null;
721
+ const navigation = this.pendingNavigation;
722
+ this.pendingNavigation = null;
723
+ const shouldCapture = this.pendingCapture || Boolean(navigation);
724
+ this.pendingCapture = false;
725
+ if (navigation && this.running) {
726
+ await this.callbacks.onNavigation(navigation.pageId, Math.max(navigation.revision, this.revision), navigation.url);
727
+ }
728
+ if (this.running && (shouldCapture || !navigation)) {
729
+ await this.captureAndPublish();
730
+ }
731
+ const deferred = this.deferredNavigation;
732
+ this.deferredNavigation = null;
733
+ return deferred?.actionId === actionId && window.location.href === deferred.fromUrl
734
+ ? deferred.toUrl
735
+ : null;
736
+ }
737
+ click(element, actionId, desiredChecked) {
738
+ if (!(element instanceof HTMLElement))
739
+ throw new Error('Target is not clickable.');
740
+ if (desiredChecked !== undefined) {
741
+ if (!(element instanceof HTMLInputElement) || !['checkbox', 'radio'].includes(element.type)) {
742
+ throw new Error('Desired checked state requires a checkbox or radio target.');
743
+ }
744
+ if (element.checked === desiredChecked)
745
+ return;
746
+ if (element.type === 'radio' && !desiredChecked) {
747
+ throw new Error('A radio target cannot be cleared by clicking it.');
748
+ }
749
+ }
750
+ if (element instanceof HTMLAnchorElement) {
751
+ const target = new URL(element.href, window.location.href);
752
+ if (target.origin !== window.location.origin) {
753
+ throw new Error('External navigation is blocked.');
754
+ }
755
+ if ((element.target && element.target.toLowerCase() !== '_self') ||
756
+ element.hasAttribute('download')) {
757
+ throw new Error('New-window and download navigation is blocked.');
758
+ }
759
+ const fromUrl = window.location.href;
760
+ const preventDocumentNavigation = (event) => {
761
+ if (event.composedPath().includes(element))
762
+ event.preventDefault();
763
+ };
764
+ window.addEventListener('click', preventDocumentNavigation, { once: true });
765
+ try {
766
+ element.click();
767
+ }
768
+ finally {
769
+ window.removeEventListener('click', preventDocumentNavigation);
770
+ }
771
+ if (window.location.href === fromUrl && target.href !== fromUrl) {
772
+ this.deferredNavigation = {
773
+ actionId,
774
+ fromUrl,
775
+ toUrl: target.href,
776
+ };
777
+ }
778
+ return;
779
+ }
780
+ element.click();
781
+ }
782
+ type(element, value) {
783
+ if (value.length > 2000)
784
+ throw new Error('Value is too long.');
785
+ if (element instanceof HTMLInputElement) {
786
+ if (!['text', 'search', 'email', 'tel', 'url', 'number'].includes(element.type)) {
787
+ throw new Error('Input type is not supported.');
788
+ }
789
+ const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set;
790
+ if (!setter)
791
+ throw new Error('Input value cannot be updated safely.');
792
+ const previousValue = element.value;
793
+ const nextValue = element.type === 'number' ? value.trim() : value;
794
+ if (element.type === 'number' && (!nextValue || !Number.isFinite(Number(nextValue)))) {
795
+ throw new Error('Numeric input requires a finite value.');
796
+ }
797
+ setter.call(element, nextValue);
798
+ if (element.type === 'number' &&
799
+ (element.value !== nextValue ||
800
+ element.validity.badInput ||
801
+ element.validity.rangeUnderflow ||
802
+ element.validity.rangeOverflow ||
803
+ element.validity.stepMismatch)) {
804
+ setter.call(element, previousValue);
805
+ throw new Error('Numeric input value is outside the allowed constraints.');
806
+ }
807
+ }
808
+ else if (element instanceof HTMLTextAreaElement) {
809
+ const setter = Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, 'value')?.set;
810
+ setter?.call(element, value);
811
+ }
812
+ else if (element instanceof HTMLElement && element.isContentEditable) {
813
+ element.textContent = value;
814
+ }
815
+ else {
816
+ throw new Error('Target does not accept text.');
817
+ }
818
+ element.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'insertText', data: value }));
819
+ element.dispatchEvent(new Event('change', { bubbles: true }));
820
+ }
821
+ select(element, handle, optionValueId) {
822
+ if (!(element instanceof HTMLSelectElement) || !optionValueId) {
823
+ throw new Error('Select target is invalid.');
824
+ }
825
+ const option = handle.options.get(optionValueId);
826
+ if (!option || option.disabled || !option.isConnected) {
827
+ throw new Error('Select option is unavailable.');
828
+ }
829
+ element.value = option.value;
830
+ element.dispatchEvent(new Event('input', { bubbles: true }));
831
+ element.dispatchEvent(new Event('change', { bubbles: true }));
832
+ }
833
+ submit(element) {
834
+ const form = element instanceof HTMLFormElement ? element : element.closest('form');
835
+ if (!(form instanceof HTMLFormElement))
836
+ throw new Error('Form target is unavailable.');
837
+ const submitter = element instanceof HTMLButtonElement ||
838
+ (element instanceof HTMLInputElement && element.type === 'submit')
839
+ ? element
840
+ : undefined;
841
+ if (typeof form.requestSubmit !== 'function') {
842
+ throw new Error('Safe form submission is not supported.');
843
+ }
844
+ form.requestSubmit(submitter);
845
+ }
846
+ safeState(element) {
847
+ return JSON.stringify({
848
+ connected: element.isConnected,
849
+ disabled: !enabled(element),
850
+ expanded: element.getAttribute('aria-expanded'),
851
+ checked: element instanceof HTMLInputElement && ['checkbox', 'radio'].includes(element.type)
852
+ ? element.checked
853
+ : undefined,
854
+ selectedIndex: element instanceof HTMLSelectElement ? element.selectedIndex : undefined,
855
+ path: window.location.pathname,
856
+ });
857
+ }
858
+ actionApplied(grant, element, handle) {
859
+ if (grant.desiredChecked !== undefined) {
860
+ return (element instanceof HTMLInputElement &&
861
+ ['checkbox', 'radio'].includes(element.type) &&
862
+ element.checked === grant.desiredChecked);
863
+ }
864
+ if (grant.action === 'type') {
865
+ if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) {
866
+ return element.value === (grant.value ?? '');
867
+ }
868
+ if (element instanceof HTMLElement && element.isContentEditable) {
869
+ return element.textContent === (grant.value ?? '');
870
+ }
871
+ return false;
872
+ }
873
+ if (grant.action === 'select') {
874
+ const option = grant.optionValueId ? handle.options.get(grant.optionValueId) : undefined;
875
+ return (element instanceof HTMLSelectElement && Boolean(option) && element.value === option?.value);
876
+ }
877
+ return true;
878
+ }
879
+ clippedGeometry(rect, viewportWidth, viewportHeight) {
880
+ const left = Math.max(0, Math.min(viewportWidth, rect.left));
881
+ const top = Math.max(0, Math.min(viewportHeight, rect.top));
882
+ const right = Math.max(0, Math.min(viewportWidth, rect.right));
883
+ const bottom = Math.max(0, Math.min(viewportHeight, rect.bottom));
884
+ const width = Math.round(right - left);
885
+ const height = Math.round(bottom - top);
886
+ if (width < 2 || height < 2)
887
+ return null;
888
+ return {
889
+ x: Math.round(left),
890
+ y: Math.round(top),
891
+ width,
892
+ height,
893
+ };
894
+ }
895
+ readSafeSelection() {
896
+ const selection = window.getSelection();
897
+ if (!selection || selection.isCollapsed || selection.rangeCount !== 1)
898
+ return null;
899
+ const range = selection.getRangeAt(0);
900
+ const root = range.commonAncestorContainer;
901
+ const nodes = [];
902
+ if (root.nodeType === Node.TEXT_NODE) {
903
+ nodes.push(root);
904
+ }
905
+ else {
906
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
907
+ let node = walker.nextNode();
908
+ while (node) {
909
+ nodes.push(node);
910
+ node = walker.nextNode();
911
+ }
912
+ }
913
+ const segments = [];
914
+ let total = 0;
915
+ for (const node of nodes) {
916
+ if (total >= MAX_SELECTED_TEXT)
917
+ break;
918
+ const parent = node.parentElement;
919
+ if (!parent ||
920
+ parent.closest(PRIVATE_SELECTOR) ||
921
+ !isVisible(parent) ||
922
+ sensitivityFor(parent, nameFor(parent)) !== 'none') {
923
+ continue;
924
+ }
925
+ try {
926
+ if (!range.intersectsNode(node))
927
+ continue;
928
+ }
929
+ catch {
930
+ continue;
931
+ }
932
+ const start = node === range.startContainer ? range.startOffset : 0;
933
+ const end = node === range.endContainer ? range.endOffset : node.data.length;
934
+ const segment = node.data.slice(start, end);
935
+ if (!segment)
936
+ continue;
937
+ segments.push(segment);
938
+ total += segment.length;
939
+ }
940
+ const text = normalize(segments.join(' '), MAX_SELECTED_TEXT);
941
+ if (!text)
942
+ return null;
943
+ return {
944
+ text,
945
+ source: 'document',
946
+ capturedAt: new Date().toISOString(),
947
+ };
948
+ }
949
+ installNavigationHooks() {
950
+ this.originalPushState = history.pushState;
951
+ this.originalReplaceState = history.replaceState;
952
+ const notify = () => queueMicrotask(this.handleNavigation);
953
+ const push = this.originalPushState;
954
+ const replace = this.originalReplaceState;
955
+ history.pushState = function (...args) {
956
+ push.apply(this, args);
957
+ notify();
958
+ };
959
+ history.replaceState = function (...args) {
960
+ replace.apply(this, args);
961
+ notify();
962
+ };
963
+ }
964
+ restoreNavigationHooks() {
965
+ if (this.originalPushState)
966
+ history.pushState = this.originalPushState;
967
+ if (this.originalReplaceState)
968
+ history.replaceState = this.originalReplaceState;
969
+ this.originalPushState = null;
970
+ this.originalReplaceState = null;
971
+ }
972
+ reducedMotion() {
973
+ return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
974
+ }
975
+ }