@oxvo/ai-live-assist 7.5.0 → 7.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -34,7 +34,7 @@ export declare const normalizeAiGuideOpenInput: (input?: AiGuideOpenInput) => Re
34
34
  export declare const resolveAiGuideProtocolVersion: (bootstrap: Pick<BootstrapConfig, "experienceMode" | "minimumProtocolVersion" | "protocol">) => 1 | 2;
35
35
  export default class AiLiveAssist {
36
36
  private readonly options;
37
- readonly version = "7.5.0";
37
+ readonly version = "7.5.1";
38
38
  private readonly host;
39
39
  private readonly guideAdmission;
40
40
  private readonly standaloneAdmissionPending;
package/cjs/context.js CHANGED
@@ -78,6 +78,7 @@ const navigationTargetBinding = (target) => canonicalJson({
78
78
  inputType: target.inputType ?? null,
79
79
  autocomplete: target.autocomplete ?? null,
80
80
  semanticRegion: target.semanticRegion ?? null,
81
+ ...(target.navigationMenu === true ? { navigationMenu: true } : {}),
81
82
  destinationPath: target.destinationPath ?? null,
82
83
  sensitivity: target.sensitivity,
83
84
  protectedRegion: target.protectedRegion ?? null,
@@ -122,6 +123,7 @@ const actionTargetFingerprintBinding = (target) => canonicalJson({
122
123
  inputType: target.inputType ?? null,
123
124
  autocomplete: target.autocomplete ?? null,
124
125
  semanticRegion: target.semanticRegion ?? null,
126
+ ...(target.navigationMenu === true ? { navigationMenu: true } : {}),
125
127
  destinationPath: target.destinationPath ?? null,
126
128
  sensitivity: target.sensitivity,
127
129
  protectedRegion: target.protectedRegion ?? null,
@@ -145,47 +147,91 @@ const actionTargetFingerprint = async (target, subtle = globalThis.crypto?.subtl
145
147
  };
146
148
  exports.actionTargetFingerprint = actionTargetFingerprint;
147
149
  const isHTMLElement = (value) => value instanceof HTMLElement;
150
+ // Clipping is checked in document geometry, not against the viewport: a
151
+ // rendered footer link remains discoverable before it is scrolled into view.
152
+ // No hidden descendant text is inspected to discover a navigation menu.
153
+ const clippedRect = (element) => {
154
+ const rect = element.getBoundingClientRect();
155
+ let { left, top, right, bottom } = rect;
156
+ let depth = 0;
157
+ for (let parent = element.parentElement; parent; parent = parent.parentElement) {
158
+ if (++depth > 64)
159
+ return null;
160
+ const style = window.getComputedStyle(parent);
161
+ if (Number.parseFloat(style.opacity) === 0 || style.visibility === 'hidden' || style.display === 'none' || style.contentVisibility === 'hidden')
162
+ return null;
163
+ if (parent === document.body || parent === document.documentElement)
164
+ continue;
165
+ const bounds = parent.getBoundingClientRect();
166
+ // Overflow scrolling can reveal an offscreen descendant. A collapsed or
167
+ // clipped disclosure cannot be revealed by scrolling the document to it.
168
+ if (['hidden', 'clip'].includes(style.overflowX)) {
169
+ left = Math.max(left, bounds.left);
170
+ right = Math.min(right, bounds.right);
171
+ }
172
+ if (['hidden', 'clip'].includes(style.overflowY)) {
173
+ top = Math.max(top, bounds.top);
174
+ bottom = Math.min(bottom, bounds.bottom);
175
+ }
176
+ if (right <= left || bottom <= top)
177
+ return null;
178
+ }
179
+ return right > left && bottom > top ? { left, top, right, bottom } : null;
180
+ };
148
181
  const isRendered = (element) => {
149
- if (!isHTMLElement(element) || !element.isConnected || element.closest(PRIVATE_SELECTOR)) {
182
+ if (!isHTMLElement(element) || !element.isConnected || element.closest(PRIVATE_SELECTOR))
150
183
  return false;
151
- }
152
184
  const style = window.getComputedStyle(element);
153
- const opacity = Number.parseFloat(style.opacity);
154
- if (style.display === 'none' ||
155
- style.visibility === 'hidden' ||
156
- (Number.isFinite(opacity) && opacity === 0) ||
157
- style.pointerEvents === 'none') {
185
+ if (style.display === 'none' || style.visibility === 'hidden' ||
186
+ Number.parseFloat(style.opacity) === 0 || style.pointerEvents === 'none')
158
187
  return false;
159
- }
160
- const rect = element.getBoundingClientRect();
161
- return rect.width > 0 && rect.height > 0;
188
+ return clippedRect(element) !== null;
162
189
  };
163
- const isVisible = (element) => {
190
+ const visibleRect = (element) => {
164
191
  if (!isRendered(element))
165
- return false;
166
- const rect = element.getBoundingClientRect();
167
- return (rect.bottom >= 0 &&
168
- rect.right >= 0 &&
169
- rect.top <= window.innerHeight &&
170
- rect.left <= window.innerWidth);
192
+ return null;
193
+ const rect = clippedRect(element);
194
+ if (!rect)
195
+ return null;
196
+ const left = Math.max(0, rect.left), top = Math.max(0, rect.top);
197
+ const right = Math.min(window.innerWidth, rect.right), bottom = Math.min(window.innerHeight, rect.bottom);
198
+ return right > left && bottom > top ? { left, top, right, bottom } : null;
171
199
  };
200
+ const isVisible = (element) => visibleRect(element) !== null;
172
201
  const isCovered = (element, trustedGuideRoot = null) => {
173
- const rect = element.getBoundingClientRect();
174
- const x = Math.max(0, Math.min(window.innerWidth - 1, rect.left + rect.width / 2));
175
- const y = Math.max(0, Math.min(window.innerHeight - 1, rect.top + rect.height / 2));
176
- let top;
177
- if (trustedGuideRoot?.isConnected &&
178
- typeof document.elementsFromPoint === 'function') {
179
- top =
180
- document
181
- .elementsFromPoint(x, y)
182
- .find((candidate) => candidate !== trustedGuideRoot) ??
183
- document.elementFromPoint(x, y);
184
- }
185
- else {
186
- top = document.elementFromPoint(x, y);
187
- }
188
- return Boolean(top && top !== element && !element.contains(top) && !top.contains(element));
202
+ const rect = visibleRect(element);
203
+ if (!rect)
204
+ return true;
205
+ // Five bounded hit tests permit an exposed part of a link without treating
206
+ // an ancestor, empty hit, or a covering sibling as an interactable target.
207
+ const insetX = Math.min(1, (rect.right - rect.left) / 4);
208
+ const insetY = Math.min(1, (rect.bottom - rect.top) / 4);
209
+ const points = [
210
+ [(rect.left + rect.right) / 2, (rect.top + rect.bottom) / 2],
211
+ [rect.left + insetX, rect.top + insetY], [rect.right - insetX, rect.top + insetY],
212
+ [rect.left + insetX, rect.bottom - insetY], [rect.right - insetX, rect.bottom - insetY],
213
+ ];
214
+ return !points.some(([x, y]) => {
215
+ const top = trustedGuideRoot?.isConnected && typeof document.elementsFromPoint === 'function'
216
+ ? document.elementsFromPoint(x, y).find(candidate => candidate !== trustedGuideRoot) ?? null
217
+ : document.elementFromPoint(x, y);
218
+ return top !== null && (top === element || element.contains(top));
219
+ });
220
+ };
221
+ const isNavigationMenu = (element, role) => {
222
+ if (role !== 'button' || literalBooleanAttribute(element, 'aria-expanded') === undefined ||
223
+ element.hasAttribute('aria-pressed') || element.hasAttribute('aria-checked') ||
224
+ element instanceof HTMLButtonElement && element.form !== null && element.type !== 'button')
225
+ return false;
226
+ if (element.closest('nav,[role="navigation"]'))
227
+ return true;
228
+ // aria-controls supplies identity only. Do not capture hidden menu labels,
229
+ // links, values or instructions before native expansion and recapture.
230
+ const controls = (element.getAttribute('aria-controls') ?? '').trim().split(/\s+/).slice(0, 4);
231
+ return controls.some(control => {
232
+ const menu = document.getElementById(control);
233
+ return menu !== null && menu.matches('nav,[role="navigation"],[role="menu"]');
234
+ });
189
235
  };
190
236
  const safePageUrl = () => `${window.location.origin}${window.location.pathname}`;
191
237
  const linkBrowsingTarget = (link) => (link.getAttribute('target') ?? document.querySelector('base[target]')?.getAttribute('target') ?? '').toLowerCase();
@@ -197,11 +243,13 @@ const sameNavigationDestination = (actual, expected) => {
197
243
  return left.origin === right.origin && left.pathname === right.pathname && left.search === right.search;
198
244
  };
199
245
  const safeDestinationPath = (element) => {
200
- if (!(element instanceof HTMLAnchorElement))
246
+ // An empty href is a control, not authority to navigate to the current URL.
247
+ if (!(element instanceof HTMLAnchorElement) || !element.getAttribute('href')?.trim())
201
248
  return null;
202
249
  try {
203
250
  const destination = new URL(element.href, window.location.href);
204
251
  if (destination.origin !== window.location.origin ||
252
+ destination.username !== '' || destination.password !== '' ||
205
253
  destination.search ||
206
254
  destination.hash ||
207
255
  !destination.pathname.startsWith('/') ||
@@ -1061,6 +1109,7 @@ class PageContextCollector {
1061
1109
  }
1062
1110
  : {}),
1063
1111
  ...(semanticRegion ? { semanticRegion } : {}),
1112
+ ...(this.statefulButtonActionProof && isNavigationMenu(element, role) ? { navigationMenu: true } : {}),
1064
1113
  ...(destinationPath ? { destinationPath } : {}),
1065
1114
  sensitivity,
1066
1115
  protectedRegion: sensitivity !== 'none' || regionAssessment.matchedRuleDigests.length > 0,
@@ -1172,6 +1221,7 @@ class PageContextCollector {
1172
1221
  pageId: this.pageId,
1173
1222
  revision,
1174
1223
  url: safePageUrl(),
1224
+ locationHasQuery: window.location.search !== '',
1175
1225
  title: normalize(document.title, 512),
1176
1226
  locale: normalize(document.documentElement.lang || navigator.language || 'en', 16),
1177
1227
  viewport: {
@@ -1,5 +1,5 @@
1
1
  import { type LiveDuplexMediaOptions, type LiveDuplexMediaDependencies } from './liveDuplexMedia.js';
2
- export declare const LIVE_CONTROL_CLIENT_VERSION = "7.5.0";
2
+ export declare const LIVE_CONTROL_CLIENT_VERSION = "7.5.1";
3
3
  export declare const LIVE_CONTROL_CLIENT_CAPABILITIES: readonly ["live_duplex_v1", "live_local_wake_v1", "live_fragment_captions_v1"];
4
4
  declare const phases: readonly ["voice_off", "local_listening", "wake_buffering", "starting", "active", "tentative_idle", "closing", "uncertain", "muted", "suspended", "budget_blocked", "ended"];
5
5
  export type LiveControlState = {
package/cjs/types.d.ts CHANGED
@@ -331,6 +331,7 @@ export type SanitizedTarget = {
331
331
  inputType?: string;
332
332
  autocomplete?: string;
333
333
  semanticRegion?: string;
334
+ navigationMenu?: true;
334
335
  destinationPath?: string;
335
336
  sensitivity: Sensitivity;
336
337
  protectedRegion?: boolean;
@@ -356,6 +357,7 @@ export type PageContextSnapshot = {
356
357
  pageId: string;
357
358
  revision: number;
358
359
  url: string;
360
+ locationHasQuery?: boolean;
359
361
  title: string;
360
362
  locale: string;
361
363
  viewport: {
package/cjs/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const VERSION = "7.5.0";
1
+ export declare const VERSION = "7.5.1";
package/cjs/version.js CHANGED
@@ -1,4 +1,4 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.VERSION = void 0;
4
- exports.VERSION = '7.5.0';
4
+ exports.VERSION = '7.5.1';
@@ -34,7 +34,7 @@ export declare const normalizeAiGuideOpenInput: (input?: AiGuideOpenInput) => Re
34
34
  export declare const resolveAiGuideProtocolVersion: (bootstrap: Pick<BootstrapConfig, "experienceMode" | "minimumProtocolVersion" | "protocol">) => 1 | 2;
35
35
  export default class AiLiveAssist {
36
36
  private readonly options;
37
- readonly version = "7.5.0";
37
+ readonly version = "7.5.1";
38
38
  private readonly host;
39
39
  private readonly guideAdmission;
40
40
  private readonly standaloneAdmissionPending;
package/lib/context.js CHANGED
@@ -75,6 +75,7 @@ const navigationTargetBinding = (target) => canonicalJson({
75
75
  inputType: target.inputType ?? null,
76
76
  autocomplete: target.autocomplete ?? null,
77
77
  semanticRegion: target.semanticRegion ?? null,
78
+ ...(target.navigationMenu === true ? { navigationMenu: true } : {}),
78
79
  destinationPath: target.destinationPath ?? null,
79
80
  sensitivity: target.sensitivity,
80
81
  protectedRegion: target.protectedRegion ?? null,
@@ -119,6 +120,7 @@ const actionTargetFingerprintBinding = (target) => canonicalJson({
119
120
  inputType: target.inputType ?? null,
120
121
  autocomplete: target.autocomplete ?? null,
121
122
  semanticRegion: target.semanticRegion ?? null,
123
+ ...(target.navigationMenu === true ? { navigationMenu: true } : {}),
122
124
  destinationPath: target.destinationPath ?? null,
123
125
  sensitivity: target.sensitivity,
124
126
  protectedRegion: target.protectedRegion ?? null,
@@ -141,47 +143,91 @@ export const actionTargetFingerprint = async (target, subtle = globalThis.crypto
141
143
  }
142
144
  };
143
145
  const isHTMLElement = (value) => value instanceof HTMLElement;
146
+ // Clipping is checked in document geometry, not against the viewport: a
147
+ // rendered footer link remains discoverable before it is scrolled into view.
148
+ // No hidden descendant text is inspected to discover a navigation menu.
149
+ const clippedRect = (element) => {
150
+ const rect = element.getBoundingClientRect();
151
+ let { left, top, right, bottom } = rect;
152
+ let depth = 0;
153
+ for (let parent = element.parentElement; parent; parent = parent.parentElement) {
154
+ if (++depth > 64)
155
+ return null;
156
+ const style = window.getComputedStyle(parent);
157
+ if (Number.parseFloat(style.opacity) === 0 || style.visibility === 'hidden' || style.display === 'none' || style.contentVisibility === 'hidden')
158
+ return null;
159
+ if (parent === document.body || parent === document.documentElement)
160
+ continue;
161
+ const bounds = parent.getBoundingClientRect();
162
+ // Overflow scrolling can reveal an offscreen descendant. A collapsed or
163
+ // clipped disclosure cannot be revealed by scrolling the document to it.
164
+ if (['hidden', 'clip'].includes(style.overflowX)) {
165
+ left = Math.max(left, bounds.left);
166
+ right = Math.min(right, bounds.right);
167
+ }
168
+ if (['hidden', 'clip'].includes(style.overflowY)) {
169
+ top = Math.max(top, bounds.top);
170
+ bottom = Math.min(bottom, bounds.bottom);
171
+ }
172
+ if (right <= left || bottom <= top)
173
+ return null;
174
+ }
175
+ return right > left && bottom > top ? { left, top, right, bottom } : null;
176
+ };
144
177
  const isRendered = (element) => {
145
- if (!isHTMLElement(element) || !element.isConnected || element.closest(PRIVATE_SELECTOR)) {
178
+ if (!isHTMLElement(element) || !element.isConnected || element.closest(PRIVATE_SELECTOR))
146
179
  return false;
147
- }
148
180
  const style = window.getComputedStyle(element);
149
- const opacity = Number.parseFloat(style.opacity);
150
- if (style.display === 'none' ||
151
- style.visibility === 'hidden' ||
152
- (Number.isFinite(opacity) && opacity === 0) ||
153
- style.pointerEvents === 'none') {
181
+ if (style.display === 'none' || style.visibility === 'hidden' ||
182
+ Number.parseFloat(style.opacity) === 0 || style.pointerEvents === 'none')
154
183
  return false;
155
- }
156
- const rect = element.getBoundingClientRect();
157
- return rect.width > 0 && rect.height > 0;
184
+ return clippedRect(element) !== null;
158
185
  };
159
- const isVisible = (element) => {
186
+ const visibleRect = (element) => {
160
187
  if (!isRendered(element))
161
- return false;
162
- const rect = element.getBoundingClientRect();
163
- return (rect.bottom >= 0 &&
164
- rect.right >= 0 &&
165
- rect.top <= window.innerHeight &&
166
- rect.left <= window.innerWidth);
188
+ return null;
189
+ const rect = clippedRect(element);
190
+ if (!rect)
191
+ return null;
192
+ const left = Math.max(0, rect.left), top = Math.max(0, rect.top);
193
+ const right = Math.min(window.innerWidth, rect.right), bottom = Math.min(window.innerHeight, rect.bottom);
194
+ return right > left && bottom > top ? { left, top, right, bottom } : null;
167
195
  };
196
+ const isVisible = (element) => visibleRect(element) !== null;
168
197
  const isCovered = (element, trustedGuideRoot = null) => {
169
- const rect = element.getBoundingClientRect();
170
- const x = Math.max(0, Math.min(window.innerWidth - 1, rect.left + rect.width / 2));
171
- const y = Math.max(0, Math.min(window.innerHeight - 1, rect.top + rect.height / 2));
172
- let top;
173
- if (trustedGuideRoot?.isConnected &&
174
- typeof document.elementsFromPoint === 'function') {
175
- top =
176
- document
177
- .elementsFromPoint(x, y)
178
- .find((candidate) => candidate !== trustedGuideRoot) ??
179
- document.elementFromPoint(x, y);
180
- }
181
- else {
182
- top = document.elementFromPoint(x, y);
183
- }
184
- return Boolean(top && top !== element && !element.contains(top) && !top.contains(element));
198
+ const rect = visibleRect(element);
199
+ if (!rect)
200
+ return true;
201
+ // Five bounded hit tests permit an exposed part of a link without treating
202
+ // an ancestor, empty hit, or a covering sibling as an interactable target.
203
+ const insetX = Math.min(1, (rect.right - rect.left) / 4);
204
+ const insetY = Math.min(1, (rect.bottom - rect.top) / 4);
205
+ const points = [
206
+ [(rect.left + rect.right) / 2, (rect.top + rect.bottom) / 2],
207
+ [rect.left + insetX, rect.top + insetY], [rect.right - insetX, rect.top + insetY],
208
+ [rect.left + insetX, rect.bottom - insetY], [rect.right - insetX, rect.bottom - insetY],
209
+ ];
210
+ return !points.some(([x, y]) => {
211
+ const top = trustedGuideRoot?.isConnected && typeof document.elementsFromPoint === 'function'
212
+ ? document.elementsFromPoint(x, y).find(candidate => candidate !== trustedGuideRoot) ?? null
213
+ : document.elementFromPoint(x, y);
214
+ return top !== null && (top === element || element.contains(top));
215
+ });
216
+ };
217
+ const isNavigationMenu = (element, role) => {
218
+ if (role !== 'button' || literalBooleanAttribute(element, 'aria-expanded') === undefined ||
219
+ element.hasAttribute('aria-pressed') || element.hasAttribute('aria-checked') ||
220
+ element instanceof HTMLButtonElement && element.form !== null && element.type !== 'button')
221
+ return false;
222
+ if (element.closest('nav,[role="navigation"]'))
223
+ return true;
224
+ // aria-controls supplies identity only. Do not capture hidden menu labels,
225
+ // links, values or instructions before native expansion and recapture.
226
+ const controls = (element.getAttribute('aria-controls') ?? '').trim().split(/\s+/).slice(0, 4);
227
+ return controls.some(control => {
228
+ const menu = document.getElementById(control);
229
+ return menu !== null && menu.matches('nav,[role="navigation"],[role="menu"]');
230
+ });
185
231
  };
186
232
  const safePageUrl = () => `${window.location.origin}${window.location.pathname}`;
187
233
  const linkBrowsingTarget = (link) => (link.getAttribute('target') ?? document.querySelector('base[target]')?.getAttribute('target') ?? '').toLowerCase();
@@ -193,11 +239,13 @@ const sameNavigationDestination = (actual, expected) => {
193
239
  return left.origin === right.origin && left.pathname === right.pathname && left.search === right.search;
194
240
  };
195
241
  const safeDestinationPath = (element) => {
196
- if (!(element instanceof HTMLAnchorElement))
242
+ // An empty href is a control, not authority to navigate to the current URL.
243
+ if (!(element instanceof HTMLAnchorElement) || !element.getAttribute('href')?.trim())
197
244
  return null;
198
245
  try {
199
246
  const destination = new URL(element.href, window.location.href);
200
247
  if (destination.origin !== window.location.origin ||
248
+ destination.username !== '' || destination.password !== '' ||
201
249
  destination.search ||
202
250
  destination.hash ||
203
251
  !destination.pathname.startsWith('/') ||
@@ -1057,6 +1105,7 @@ export class PageContextCollector {
1057
1105
  }
1058
1106
  : {}),
1059
1107
  ...(semanticRegion ? { semanticRegion } : {}),
1108
+ ...(this.statefulButtonActionProof && isNavigationMenu(element, role) ? { navigationMenu: true } : {}),
1060
1109
  ...(destinationPath ? { destinationPath } : {}),
1061
1110
  sensitivity,
1062
1111
  protectedRegion: sensitivity !== 'none' || regionAssessment.matchedRuleDigests.length > 0,
@@ -1168,6 +1217,7 @@ export class PageContextCollector {
1168
1217
  pageId: this.pageId,
1169
1218
  revision,
1170
1219
  url: safePageUrl(),
1220
+ locationHasQuery: window.location.search !== '',
1171
1221
  title: normalize(document.title, 512),
1172
1222
  locale: normalize(document.documentElement.lang || navigator.language || 'en', 16),
1173
1223
  viewport: {
@@ -1,5 +1,5 @@
1
1
  import { type LiveDuplexMediaOptions, type LiveDuplexMediaDependencies } from './liveDuplexMedia.js';
2
- export declare const LIVE_CONTROL_CLIENT_VERSION = "7.5.0";
2
+ export declare const LIVE_CONTROL_CLIENT_VERSION = "7.5.1";
3
3
  export declare const LIVE_CONTROL_CLIENT_CAPABILITIES: readonly ["live_duplex_v1", "live_local_wake_v1", "live_fragment_captions_v1"];
4
4
  declare const phases: readonly ["voice_off", "local_listening", "wake_buffering", "starting", "active", "tentative_idle", "closing", "uncertain", "muted", "suspended", "budget_blocked", "ended"];
5
5
  export type LiveControlState = {
package/lib/types.d.ts CHANGED
@@ -331,6 +331,7 @@ export type SanitizedTarget = {
331
331
  inputType?: string;
332
332
  autocomplete?: string;
333
333
  semanticRegion?: string;
334
+ navigationMenu?: true;
334
335
  destinationPath?: string;
335
336
  sensitivity: Sensitivity;
336
337
  protectedRegion?: boolean;
@@ -356,6 +357,7 @@ export type PageContextSnapshot = {
356
357
  pageId: string;
357
358
  revision: number;
358
359
  url: string;
360
+ locationHasQuery?: boolean;
359
361
  title: string;
360
362
  locale: string;
361
363
  viewport: {
package/lib/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const VERSION = "7.5.0";
1
+ export declare const VERSION = "7.5.1";
package/lib/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = '7.5.0';
1
+ export const VERSION = '7.5.1';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@oxvo/ai-live-assist",
3
3
  "description": "Secure AI Guide browser plugin for OXVO Sessions.",
4
- "version": "7.5.0",
4
+ "version": "7.5.1",
5
5
  "keywords": [
6
6
  "ai-live-assist",
7
7
  "webrtc",