@reticlehq/browser 3.9.0 → 3.10.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 (56) hide show
  1. package/dist/actions/action-report.d.ts +14 -0
  2. package/dist/actions/action-report.js +95 -22
  3. package/dist/actions/actions-dom.d.ts +16 -6
  4. package/dist/actions/actions-dom.js +94 -46
  5. package/dist/actions/actions-press.d.ts +10 -0
  6. package/dist/actions/actions-press.js +146 -2
  7. package/dist/actions/actions.js +52 -22
  8. package/dist/actions/insert-text.d.ts +3 -0
  9. package/dist/actions/insert-text.js +90 -0
  10. package/dist/actions/scroll.d.ts +1 -1
  11. package/dist/actions/scroll.js +25 -1
  12. package/dist/actions/value-input.d.ts +2 -0
  13. package/dist/actions/value-input.js +30 -9
  14. package/dist/commands/commands.js +82 -6
  15. package/dist/connect-options.d.ts +11 -1
  16. package/dist/connect-options.js +27 -1
  17. package/dist/dom/a11y.js +4 -3
  18. package/dist/dom/chart-geometry.d.ts +7 -0
  19. package/dist/dom/chart-geometry.js +39 -0
  20. package/dist/dom/occlusion.js +21 -0
  21. package/dist/dom/query-finders.d.ts +1 -1
  22. package/dist/dom/query-finders.js +10 -7
  23. package/dist/dom/query.d.ts +1 -0
  24. package/dist/dom/query.js +78 -51
  25. package/dist/dom/scope-roots.d.ts +8 -0
  26. package/dist/dom/scope-roots.js +44 -0
  27. package/dist/dom/snapshot.d.ts +3 -0
  28. package/dist/dom/snapshot.js +9 -2
  29. package/dist/dom/text-match.js +4 -3
  30. package/dist/dom/theme.js +6 -7
  31. package/dist/dom/visibility.d.ts +2 -0
  32. package/dist/dom/visibility.js +40 -8
  33. package/dist/observers/canvas.d.ts +1 -0
  34. package/dist/observers/canvas.js +10 -1
  35. package/dist/observers/console.js +46 -35
  36. package/dist/observers/dom.js +28 -4
  37. package/dist/observers/net-detail/resource-timing.d.ts +12 -0
  38. package/dist/observers/net-detail/resource-timing.js +69 -0
  39. package/dist/observers/network.js +12 -45
  40. package/dist/observers/perf.js +15 -1
  41. package/dist/presenter/chrome/presenter-verbs.js +1 -0
  42. package/dist/presenter/presenter-config.d.ts +1 -0
  43. package/dist/presenter/presenter-config.js +1 -0
  44. package/dist/presenter/presenter-shell.d.ts +1 -0
  45. package/dist/presenter/presenter-shell.js +6 -3
  46. package/dist/presenter/tour/tour.js +4 -0
  47. package/dist/recorder/recorder.js +82 -39
  48. package/dist/reticle-inject.js +597 -597
  49. package/dist/reticle.d.ts +3 -19
  50. package/dist/reticle.js +74 -108
  51. package/dist/security/serialization.js +35 -0
  52. package/dist/storage-keys.js +2 -1
  53. package/dist/timers/native/native-timers.js +21 -4
  54. package/dist/transport/bridge-url.d.ts +8 -0
  55. package/dist/transport/bridge-url.js +52 -0
  56. package/package.json +10 -10
package/dist/reticle.d.ts CHANGED
@@ -1,27 +1,11 @@
1
- import { EventType, RETICLE_URL_PARAM, HudVisibility, type ReticleEvent } from '@reticlehq/core';
1
+ import { EventType, RETICLE_URL_PARAM, type ReticleEvent } from '@reticlehq/core';
2
2
  import { type CapabilitiesInput } from './registry/capabilities.js';
3
3
  import { type ReticleConnectOptions } from './connect-options.js';
4
- export { helloMode, shouldBlockProduction } from './connect-options.js';
4
+ export { helloMode, hudWithoutPanel, reticleParamsFromSearch, shouldBlockProduction, } from './connect-options.js';
5
5
  export type { ReticleConnectOptions };
6
- export declare function withBridgePath(bridgeUrl: string): {
7
- url: string;
8
- adjusted: boolean;
9
- };
10
- export declare function connectionPolicy(pageHostname: string, bridgeUrl: string, allowNonLocalhost: boolean, token: string | undefined, pageProtocol?: string): {
11
- allowed: boolean;
12
- reason?: string;
13
- };
6
+ export { connectionPolicy, withBridgePath } from './transport/bridge-url.js';
14
7
  export declare function resolveSessionLabel(option: string | undefined, gen: () => string): string;
15
8
  export { RETICLE_URL_PARAM };
16
- export declare function reticleParamsFromSearch(search: string): {
17
- session?: string;
18
- projectId?: string;
19
- hud?: HudVisibility;
20
- };
21
- export declare function hudWithoutPanel(hud: HudVisibility | undefined): {
22
- remember?: HudVisibility;
23
- placed: HudVisibility[];
24
- };
25
9
  export declare function resolveConnectIdentity(options: {
26
10
  session?: string;
27
11
  projectId?: string;
package/dist/reticle.js CHANGED
@@ -3,7 +3,7 @@ import { trackBusy } from './busy.js';
3
3
  import { declareRoutes } from './observers/route.js';
4
4
  import { setOutwardPolicy } from './actions/danger-context.js';
5
5
  import { coverageMarkers, notApplicableFor } from './dom/addressing/coverage-markers.js';
6
- import { EventType, RETICLE_DEFAULT_PORT, RETICLE_PROTOCOL_VERSION, RETICLE_URL_PARAM, RETICLE_WS_PATH, bridgeWsUrl, ReticleCommand, MessageKind, SESSION_AUTO, SessionState, TRANSPORT_LIMITS, buildRedactionPolicy, isLoopbackHostname, isLocalPage, setActiveRedactionPolicy, wireRedactionKeys, RETICLE_ROOT_GLOBAL, RETICLE_SDK_VERSION_GLOBAL, CONTRACT_FINGERPRINT, CONTRACT_PARTS, newDocumentId, NO_EDITS_OBSERVED, PresenterMode, CommandActor, HudCorner, HudVisibility, } from '@reticlehq/core';
6
+ import { EventType, RETICLE_DEFAULT_PORT, RETICLE_PROTOCOL_VERSION, RETICLE_URL_PARAM, RETICLE_WS_PATH, bridgeWsUrl, ReticleCommand, MessageKind, SESSION_AUTO, SessionState, buildRedactionPolicy, setActiveRedactionPolicy, wireRedactionKeys, RETICLE_ROOT_GLOBAL, RETICLE_SDK_VERSION_GLOBAL, CONTRACT_FINGERPRINT, CONTRACT_PARTS, newDocumentId, NO_EDITS_OBSERVED, PresenterMode, CommandActor, HudCorner, HudVisibility, } from '@reticlehq/core';
7
7
  import { rememberSessionLabel, tabSessionStore } from './session-continuity.js';
8
8
  import { editEpoch } from './edit-epoch.js';
9
9
  import { createCommandRegistry, RELOAD_CACHE_BUST_PARAM, } from './commands/commands.js';
@@ -24,60 +24,11 @@ import { actionVerb } from './presenter/chrome/presenter-verbs.js';
24
24
  import { str, refLabel, modeForCommand, presentStatus } from './reticle-presenter-helpers.js';
25
25
  import { resetClock } from './timers/clock.js';
26
26
  import { nativeWarn } from './timers/console/native-console.js';
27
- import { installRecorder } from './recorder/recorder.js';
28
27
  import { Annotator } from './review/annotator.js';
29
- import { helloMode, leaseToken, shouldBlockProduction, } from './connect-options.js';
30
- export { helloMode, shouldBlockProduction } from './connect-options.js';
31
- export function withBridgePath(bridgeUrl) {
32
- let parsed;
33
- try {
34
- parsed = new URL(bridgeUrl);
35
- }
36
- catch {
37
- return { url: bridgeUrl, adjusted: false };
38
- }
39
- if ((parsed.protocol !== 'ws:' && parsed.protocol !== 'wss:') ||
40
- (parsed.pathname !== '' && parsed.pathname !== '/')) {
41
- return { url: bridgeUrl, adjusted: false };
42
- }
43
- parsed.pathname = RETICLE_WS_PATH;
44
- return { url: parsed.toString(), adjusted: true };
45
- }
46
- export function connectionPolicy(pageHostname, bridgeUrl, allowNonLocalhost, token, pageProtocol = 'http:') {
47
- let bridge;
48
- try {
49
- bridge = new URL(bridgeUrl);
50
- }
51
- catch {
52
- return { allowed: false, reason: 'invalid Reticle bridge URL' };
53
- }
54
- if (bridge.protocol !== 'ws:' && bridge.protocol !== 'wss:') {
55
- return { allowed: false, reason: 'Reticle bridge URL must use ws:// or wss://' };
56
- }
57
- if ((token?.length ?? 0) > TRANSPORT_LIMITS.MAX_TOKEN_LENGTH) {
58
- return {
59
- allowed: false,
60
- reason: `Reticle pairing token exceeds ${String(TRANSPORT_LIMITS.MAX_TOKEN_LENGTH)} characters`,
61
- };
62
- }
63
- const remoteBridge = !isLoopbackHostname(bridge.hostname);
64
- if (remoteBridge && bridge.protocol !== 'wss:') {
65
- return { allowed: false, reason: 'a non-local Reticle bridge must use wss://' };
66
- }
67
- const remote = !isLocalPage(pageProtocol, pageHostname) || remoteBridge;
68
- if (!remote)
69
- return { allowed: true };
70
- if (!allowNonLocalhost) {
71
- return {
72
- allowed: false,
73
- reason: 'Reticle is disabled outside localhost unless allowNonLocalhost is explicitly enabled',
74
- };
75
- }
76
- if (token === undefined || 0 === token.length) {
77
- return { allowed: false, reason: 'a pairing token is required outside localhost' };
78
- }
79
- return { allowed: true };
80
- }
28
+ import { helloMode, hudWithoutPanel, leaseToken, oneOf, reticleParamsFromSearch, shouldBlockProduction, } from './connect-options.js';
29
+ export { helloMode, hudWithoutPanel, reticleParamsFromSearch, shouldBlockProduction, } from './connect-options.js';
30
+ export { connectionPolicy, withBridgePath } from './transport/bridge-url.js';
31
+ import { connectionPolicy, withBridgePath } from './transport/bridge-url.js';
81
32
  const MAX_PENDING_NARRATIONS = 20;
82
33
  const KILLED_MESSAGE = '[Reticle] killed for this page. Restart your dev server or reload the page to bring it back.';
83
34
  const BRIDGE_LOST_SUMMARY = 'Session ended - lost connection to Reticle (the agent is no longer running).';
@@ -88,33 +39,15 @@ export { RETICLE_URL_PARAM };
88
39
  function stripReloadCacheBustParam() {
89
40
  try {
90
41
  const current = new URL(window.location.href);
91
- if (!current.searchParams.has(RELOAD_CACHE_BUST_PARAM))
42
+ if (!current.searchParams.has(RELOAD_CACHE_BUST_PARAM)) {
92
43
  return;
44
+ }
93
45
  current.searchParams.delete(RELOAD_CACHE_BUST_PARAM);
94
46
  window.history.replaceState(window.history.state, '', current.toString());
95
47
  }
96
48
  catch {
97
49
  }
98
50
  }
99
- export function reticleParamsFromSearch(search) {
100
- const params = new URLSearchParams(search);
101
- const out = {};
102
- const session = params.get(RETICLE_URL_PARAM.SESSION);
103
- const projectId = params.get(RETICLE_URL_PARAM.PROJECT);
104
- const hud = oneOf(HudVisibility, params.get(RETICLE_URL_PARAM.HUD));
105
- if (session !== null && session.length > 0)
106
- out.session = session;
107
- if (projectId !== null && projectId.length > 0)
108
- out.projectId = projectId;
109
- if (hud !== undefined)
110
- out.hud = hud;
111
- return out;
112
- }
113
- export function hudWithoutPanel(hud) {
114
- if (hud === undefined)
115
- return { placed: [] };
116
- return { remember: hud, placed: HudVisibility.SHOWN === hud ? [] : [hud] };
117
- }
118
51
  export function resolveConnectIdentity(options, search) {
119
52
  const url = reticleParamsFromSearch(search);
120
53
  const explicitSession = options.session !== undefined && options.session !== SESSION_AUTO ? options.session : undefined;
@@ -173,8 +106,9 @@ export class Reticle {
173
106
  this.#adoptLateProjectId(options);
174
107
  return;
175
108
  }
176
- if ('undefined' === typeof window || 'undefined' === typeof document)
109
+ if ('undefined' === typeof window || 'undefined' === typeof document) {
177
110
  return;
111
+ }
178
112
  const proc = globalThis.process;
179
113
  const nodeEnv = proc?.env?.['NODE_ENV'];
180
114
  if (shouldBlockProduction(nodeEnv, true === options.allowInProduction)) {
@@ -253,11 +187,19 @@ export class Reticle {
253
187
  this.#overlay.update({ connected: true, events: 0 });
254
188
  }
255
189
  this.#pendingHud = reticleParamsFromSearch(window.location.search).hud;
256
- if (options.present !== false && HudVisibility.REMOVED !== this.#pendingHud)
190
+ if (options.present !== false && HudVisibility.REMOVED !== this.#pendingHud) {
257
191
  this.#fetchAndShowPanel(options);
192
+ }
258
193
  if (true === options.recorder) {
259
- this.#recorder = installRecorder({ emit, now: () => Date.now() });
260
- this.#recorder.mount();
194
+ void import('./recorder/recorder.js')
195
+ .then(({ installRecorder }) => {
196
+ if (!this.#connected || this.#recorder !== undefined) {
197
+ return;
198
+ }
199
+ this.#recorder = installRecorder({ emit, now: () => Date.now() });
200
+ this.#recorder.mount();
201
+ })
202
+ .catch(() => undefined);
261
203
  }
262
204
  if (options.annotate ?? options.present !== false) {
263
205
  this.#annotator = new Annotator({
@@ -273,32 +215,40 @@ export class Reticle {
273
215
  this.#connected = true;
274
216
  }
275
217
  #fetchAndShowPanel(options) {
276
- if (this.#panelRequested)
218
+ if (this.#panelRequested) {
277
219
  return;
220
+ }
278
221
  this.#panelRequested = true;
279
222
  void import('./presenter/presenter.js')
280
223
  .then(({ Presenter }) => import('./presenter/presenter-settings.js').then((settings) => ({ Presenter, settings })))
281
224
  .then(({ Presenter, settings }) => {
282
- if (!this.#connected)
225
+ if (!this.#connected) {
283
226
  return;
227
+ }
284
228
  this.#readPanelSettings = settings.getPresenterSettings;
285
229
  const panelOptions = {};
286
- if (options.pace !== undefined)
230
+ if (options.pace !== undefined) {
287
231
  panelOptions.paceMs = options.pace;
232
+ }
288
233
  if (options.narrationDwellMs !== undefined) {
289
234
  panelOptions.narrationDwellMs = options.narrationDwellMs;
290
235
  }
291
- if (options.border !== undefined)
236
+ if (options.border !== undefined) {
292
237
  panelOptions.border = options.border;
293
- if (options.logMax !== undefined)
238
+ }
239
+ if (options.logMax !== undefined) {
294
240
  panelOptions.logMax = options.logMax;
295
- if (options.endedFadeMs !== undefined)
241
+ }
242
+ if (options.endedFadeMs !== undefined) {
296
243
  panelOptions.endedFadeMs = options.endedFadeMs;
297
- if (options.idleEndMs !== undefined)
244
+ }
245
+ if (options.idleEndMs !== undefined) {
298
246
  panelOptions.idleEndMs = options.idleEndMs;
247
+ }
299
248
  panelOptions.sessionId = this.#session;
300
- if (this.#projectId !== undefined)
249
+ if (this.#projectId !== undefined) {
301
250
  panelOptions.projectId = this.#projectId;
251
+ }
302
252
  panelOptions.onControl = (intent) => this.#emit(EventType.HUMAN_CONTROL, intent.text !== undefined
303
253
  ? { kind: intent.kind, text: intent.text }
304
254
  : { kind: intent.kind });
@@ -311,19 +261,24 @@ export class Reticle {
311
261
  this.#presenter = panel;
312
262
  panel.mount();
313
263
  panel.showSdkVersion(this.#sdkVersion);
314
- for (const buffered of this.#pendingPushes.values())
264
+ for (const buffered of this.#pendingPushes.values()) {
315
265
  panel.handlePush(buffered);
266
+ }
316
267
  this.#pendingPushes.clear();
317
- if (0 < this.#pendingNarrations.length)
268
+ if (0 < this.#pendingNarrations.length) {
318
269
  panel.sessionStart();
319
- for (const early of this.#pendingNarrations.splice(0))
270
+ }
271
+ for (const early of this.#pendingNarrations.splice(0)) {
320
272
  panel.narrate(early.text, early.level, early.actor, early.summary);
321
- if (this.#pendingHud !== undefined)
273
+ }
274
+ if (this.#pendingHud !== undefined) {
322
275
  panel.placeHud(this.#pendingHud, undefined);
276
+ }
323
277
  this.#pendingHud = undefined;
324
278
  void import('./presenter/tour/tour.js').then((tour) => {
325
- if (!this.#connected)
279
+ if (!this.#connected) {
326
280
  return;
281
+ }
327
282
  const shown = tour.mountTour({
328
283
  document,
329
284
  storage: tour.safeLocalStorage(),
@@ -334,21 +289,25 @@ export class Reticle {
334
289
  copy: (text) => navigator.clipboard?.writeText(text).then(() => true, () => false) ?? false,
335
290
  select: (element) => {
336
291
  const selection = window.getSelection();
337
- if (null === selection)
292
+ if (null === selection) {
338
293
  return;
294
+ }
339
295
  const range = document.createRange();
340
296
  range.selectNodeContents(element);
341
297
  selection.removeAllRanges();
342
298
  selection.addRange(range);
343
299
  },
344
300
  });
345
- if (shown !== undefined)
301
+ if (shown !== undefined) {
346
302
  this.#teardowns.push(shown.destroy);
303
+ }
347
304
  });
348
- if (this.#bridgeConnected)
305
+ if (this.#bridgeConnected) {
349
306
  panel.sessionStart();
350
- if (this.#annotator !== undefined)
307
+ }
308
+ if (this.#annotator !== undefined) {
351
309
  panel.bindAnnotator(this.#annotator);
310
+ }
352
311
  })
353
312
  .catch(() => {
354
313
  });
@@ -371,8 +330,9 @@ export class Reticle {
371
330
  this.#emit(EventType.STATE_CHANGE, { name, value });
372
331
  }
373
332
  renderCommit(commits) {
374
- if (commits > 0)
333
+ if (commits > 0) {
375
334
  this.#emit(EventType.RENDER_COMMIT, { commits });
335
+ }
376
336
  }
377
337
  describe(input) {
378
338
  registerCapabilities(input);
@@ -384,8 +344,9 @@ export class Reticle {
384
344
  editEpoch.observe(hot);
385
345
  }
386
346
  disconnect() {
387
- if (!this.#connected)
347
+ if (!this.#connected) {
388
348
  return;
349
+ }
389
350
  runTeardowns(this.#emit, this.#teardowns);
390
351
  this.#teardowns = [];
391
352
  this.#transport?.close();
@@ -417,8 +378,9 @@ export class Reticle {
417
378
  this.#transport?.sendEvent(event);
418
379
  this.#eventCount += 1;
419
380
  this.#overlay?.update({ connected: true, events: this.#eventCount });
420
- if (type === EventType.ROUTE_CHANGE)
381
+ if (type === EventType.ROUTE_CHANGE) {
421
382
  this.#presenter?.refilterFlows();
383
+ }
422
384
  }
423
385
  catch {
424
386
  }
@@ -428,11 +390,13 @@ export class Reticle {
428
390
  this.#redactKeys = wireRedactionKeys(config);
429
391
  }
430
392
  #adoptLateProjectId(options) {
431
- if (this.#projectId !== undefined)
393
+ if (this.#projectId !== undefined) {
432
394
  return;
395
+ }
433
396
  const late = resolveConnectIdentity(options, window.location.search).projectId;
434
- if (late === undefined)
397
+ if (late === undefined) {
435
398
  return;
399
+ }
436
400
  this.#projectId = late;
437
401
  this.#transport?.reannounce();
438
402
  }
@@ -472,13 +436,14 @@ export class Reticle {
472
436
  }
473
437
  if (command.name === ReticleCommand.NARRATE) {
474
438
  if (this.#presenter === undefined && this.#panelRequested) {
475
- if (this.#pendingNarrations.length < MAX_PENDING_NARRATIONS)
439
+ if (this.#pendingNarrations.length < MAX_PENDING_NARRATIONS) {
476
440
  this.#pendingNarrations.push({
477
441
  text: str(command.args['text']),
478
442
  level: str(command.args['level'], 'info'),
479
443
  actor: narrationActor(command.args),
480
444
  summary: true === command.args['summary'],
481
445
  });
446
+ }
482
447
  return { ok: true, result: { shown: false, buffered: true } };
483
448
  }
484
449
  this.#presenter?.sessionStart();
@@ -487,18 +452,21 @@ export class Reticle {
487
452
  }
488
453
  if (command.name === ReticleCommand.SESSION_CONFIG) {
489
454
  const idleEndMs = command.args['idleEndMs'];
490
- if ('number' === typeof idleEndMs)
455
+ if ('number' === typeof idleEndMs) {
491
456
  this.#presenter?.setIdleEndMs(idleEndMs);
457
+ }
492
458
  const hud = oneOf(HudVisibility, command.args['hud']);
493
459
  const corner = oneOf(HudCorner, command.args['corner']);
494
460
  const presenter = this.#presenter;
495
461
  let placed;
496
- if (presenter !== undefined)
462
+ if (presenter !== undefined) {
497
463
  placed = presenter.placeHud(hud, corner);
464
+ }
498
465
  else {
499
466
  const early = hudWithoutPanel(hud);
500
- if (early.remember !== undefined)
467
+ if (early.remember !== undefined) {
501
468
  this.#pendingHud = early.remember;
469
+ }
502
470
  placed = early.placed;
503
471
  }
504
472
  return {
@@ -548,8 +516,9 @@ export class Reticle {
548
516
  }
549
517
  async #presentBefore(command) {
550
518
  const p = this.#presenter;
551
- if (p === undefined)
519
+ if (p === undefined) {
552
520
  return;
521
+ }
553
522
  p.setMode(modeForCommand(command.name));
554
523
  this.#actHandle = undefined;
555
524
  const actor = CommandActor.HARNESS === command.by ? LOG_ACTOR.HARNESS : LOG_ACTOR.AGENT;
@@ -579,6 +548,3 @@ export class Reticle {
579
548
  function narrationActor(args) {
580
549
  return CommandActor.HARNESS === args['actor'] ? LOG_ACTOR.HARNESS : LOG_ACTOR.AGENT;
581
550
  }
582
- function oneOf(values, raw) {
583
- return Object.values(values).includes(raw) ? raw : undefined;
584
- }
@@ -2,6 +2,12 @@ import { REDACTED_VALUE, TRANSPORT_LIMITS, isSensitiveKey, scrubKnownSecrets, }
2
2
  const TRUNCATED_VALUE = '[TRUNCATED]';
3
3
  const UNSERIALIZABLE_VALUE = '[UNSERIALIZABLE]';
4
4
  const OMIT_VALUE = Symbol('omit');
5
+ const PROMISE_SUMMARY = '[Promise]';
6
+ const WINDOW_SUMMARY = '[Window]';
7
+ const WINDOW_TAG = 'Window';
8
+ const PROMISE_TAG = 'Promise';
9
+ const PLAIN_OBJECT_TAG = 'Object';
10
+ const OBJECT_TAG_PATTERN = /^\[object (\w+)\]$/;
5
11
  const MAX_KEY_LENGTH = 256;
6
12
  const MAX_TOTAL_CHARACTERS = Math.floor(TRANSPORT_LIMITS.MAX_MESSAGE_BYTES / 4);
7
13
  const MAX_TOTAL_NODES = TRANSPORT_LIMITS.MAX_COLLECTION_ITEMS * 5;
@@ -18,6 +24,30 @@ function boundedString(value, state, max) {
18
24
  state.remainingCharacters -= truncated.length;
19
25
  return truncated;
20
26
  }
27
+ function opaqueSummary(value) {
28
+ try {
29
+ const tag = OBJECT_TAG_PATTERN.exec(Object.prototype.toString.call(value))?.[1];
30
+ if (WINDOW_TAG === tag) {
31
+ return WINDOW_SUMMARY;
32
+ }
33
+ if (PROMISE_TAG === tag) {
34
+ return PROMISE_SUMMARY;
35
+ }
36
+ if (tag !== undefined && PLAIN_OBJECT_TAG !== tag) {
37
+ const node = value;
38
+ if ('number' === typeof node.nodeType && 'string' === typeof node.nodeName) {
39
+ return `[${tag}]`;
40
+ }
41
+ }
42
+ if ('function' === typeof value.then) {
43
+ return PROMISE_SUMMARY;
44
+ }
45
+ }
46
+ catch {
47
+ return undefined;
48
+ }
49
+ return undefined;
50
+ }
21
51
  function sanitize(value, state, depth, key) {
22
52
  if (key !== undefined && isSensitiveKey(key))
23
53
  return REDACTED_VALUE;
@@ -40,6 +70,11 @@ function sanitize(value, state, depth, key) {
40
70
  if ('undefined' === typeof value || 'function' === typeof value || 'symbol' === typeof value) {
41
71
  return OMIT_VALUE;
42
72
  }
73
+ const opaque = opaqueSummary(value);
74
+ if (opaque !== undefined) {
75
+ state.truncatedValues += 1;
76
+ return boundedString(opaque, state, MAX_KEY_LENGTH);
77
+ }
43
78
  if (value instanceof Date)
44
79
  return Number.isNaN(value.getTime()) ? null : value.toISOString();
45
80
  if (value instanceof Map) {
@@ -1,5 +1,6 @@
1
+ import { RETICLE_SESSION_STORAGE_KEY } from '@reticlehq/core';
1
2
  export const ReticleStorageKey = {
2
- SESSION: '__reticle_session',
3
+ SESSION: RETICLE_SESSION_STORAGE_KEY,
3
4
  REF_BASE: '__reticle_ref_base',
4
5
  PRESENTER_LOG: 'reticle-presenter-log',
5
6
  PRESENTER_LOG_OWNER: 'reticle-presenter-log-owner',
@@ -1,8 +1,25 @@
1
+ import { DETERMINISM_GLOBAL_KEY } from '@reticlehq/core';
1
2
  const g = globalThis;
2
- const realSetTimeout = 'function' === typeof g.setTimeout ? g.setTimeout.bind(g) : null;
3
- const realClearTimeout = 'function' === typeof g.clearTimeout ? g.clearTimeout.bind(g) : null;
4
- const realRaf = 'function' === typeof g.requestAnimationFrame ? g.requestAnimationFrame.bind(g) : null;
5
- const realPerfNow = 'function' === typeof g.performance?.now ? g.performance.now.bind(g.performance) : null;
3
+ function kept(name) {
4
+ const holder = g[Symbol.for(DETERMINISM_GLOBAL_KEY)];
5
+ if ('object' !== typeof holder || null === holder) {
6
+ return undefined;
7
+ }
8
+ const natives = holder.natives;
9
+ if ('object' !== typeof natives || null === natives) {
10
+ return undefined;
11
+ }
12
+ const fn = natives[name];
13
+ return 'function' === typeof fn ? fn : undefined;
14
+ }
15
+ const realSetTimeout = kept('setTimeout') ??
16
+ ('function' === typeof g.setTimeout ? g.setTimeout.bind(g) : null);
17
+ const realClearTimeout = kept('clearTimeout') ??
18
+ ('function' === typeof g.clearTimeout ? g.clearTimeout.bind(g) : null);
19
+ const realRaf = kept('requestAnimationFrame') ??
20
+ ('function' === typeof g.requestAnimationFrame ? g.requestAnimationFrame.bind(g) : null);
21
+ const realPerfNow = kept('performanceNow') ??
22
+ ('function' === typeof g.performance?.now ? g.performance.now.bind(g.performance) : null);
6
23
  export const nativeNow = () => (realPerfNow ? realPerfNow() : 0);
7
24
  export const nativeSetTimeout = (cb, ms = 0) => realSetTimeout ? realSetTimeout(cb, ms) : 0;
8
25
  export const nativeClearTimeout = (id) => {
@@ -0,0 +1,8 @@
1
+ export declare function withBridgePath(bridgeUrl: string): {
2
+ url: string;
3
+ adjusted: boolean;
4
+ };
5
+ export declare function connectionPolicy(pageHostname: string, bridgeUrl: string, allowNonLocalhost: boolean, token: string | undefined, pageProtocol?: string): {
6
+ allowed: boolean;
7
+ reason?: string;
8
+ };
@@ -0,0 +1,52 @@
1
+ import { RETICLE_WS_PATH, TRANSPORT_LIMITS, isLocalPage, isLoopbackHostname, } from '@reticlehq/core';
2
+ export function withBridgePath(bridgeUrl) {
3
+ let parsed;
4
+ try {
5
+ parsed = new URL(bridgeUrl);
6
+ }
7
+ catch {
8
+ return { url: bridgeUrl, adjusted: false };
9
+ }
10
+ if ((parsed.protocol !== 'ws:' && parsed.protocol !== 'wss:') ||
11
+ (parsed.pathname !== '' && parsed.pathname !== '/')) {
12
+ return { url: bridgeUrl, adjusted: false };
13
+ }
14
+ parsed.pathname = RETICLE_WS_PATH;
15
+ return { url: parsed.toString(), adjusted: true };
16
+ }
17
+ export function connectionPolicy(pageHostname, bridgeUrl, allowNonLocalhost, token, pageProtocol = 'http:') {
18
+ let bridge;
19
+ try {
20
+ bridge = new URL(bridgeUrl);
21
+ }
22
+ catch {
23
+ return { allowed: false, reason: 'invalid Reticle bridge URL' };
24
+ }
25
+ if (bridge.protocol !== 'ws:' && bridge.protocol !== 'wss:') {
26
+ return { allowed: false, reason: 'Reticle bridge URL must use ws:// or wss://' };
27
+ }
28
+ if ((token?.length ?? 0) > TRANSPORT_LIMITS.MAX_TOKEN_LENGTH) {
29
+ return {
30
+ allowed: false,
31
+ reason: `Reticle pairing token exceeds ${String(TRANSPORT_LIMITS.MAX_TOKEN_LENGTH)} characters`,
32
+ };
33
+ }
34
+ const remoteBridge = !isLoopbackHostname(bridge.hostname);
35
+ if (remoteBridge && bridge.protocol !== 'wss:') {
36
+ return { allowed: false, reason: 'a non-local Reticle bridge must use wss://' };
37
+ }
38
+ const remote = !isLocalPage(pageProtocol, pageHostname) || remoteBridge;
39
+ if (!remote) {
40
+ return { allowed: true };
41
+ }
42
+ if (!allowNonLocalhost) {
43
+ return {
44
+ allowed: false,
45
+ reason: 'Reticle is disabled outside localhost unless allowNonLocalhost is explicitly enabled',
46
+ };
47
+ }
48
+ if (token === undefined || 0 === token.length) {
49
+ return { allowed: false, reason: 'a pairing token is required outside localhost' };
50
+ }
51
+ return { allowed: true };
52
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reticlehq/browser",
3
- "version": "3.9.0",
3
+ "version": "3.10.0",
4
4
  "description": "Reticle browser SDK — installs observers, builds semantic snapshots, executes actions, talks to the bridge.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -22,23 +22,23 @@
22
22
  }
23
23
  },
24
24
  "dependencies": {
25
- "@reticlehq/core": "3.9.0"
25
+ "@reticlehq/core": "3.10.0"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@iconify-json/heroicons-outline": "^1.2.1",
29
29
  "@iconify-json/heroicons-solid": "^1.2.1",
30
- "@tanstack/query-core": "^5.103.2",
30
+ "@tanstack/query-core": "^5.105.2",
31
31
  "esbuild": "^0.28.2",
32
- "jotai": "^3.0.0",
32
+ "jotai": "^3.0.1",
33
33
  "jsdom": "^30.0.1",
34
- "mobx": "^7.0.5",
35
- "pinia": "^4.0.3",
36
- "react": "^19.3.0",
34
+ "mobx": "^7.1.4",
35
+ "pinia": "^4.1.0",
36
+ "react": "^19.3.1",
37
37
  "recoil": "^0.7.7",
38
- "svelte": "^5.57.1",
38
+ "svelte": "^5.57.3",
39
39
  "valtio": "^2.3.2",
40
- "vue": "^3.5.43",
41
- "xstate": "^5.33.2"
40
+ "vue": "^3.5.44",
41
+ "xstate": "^5.33.3"
42
42
  },
43
43
  "license": "Apache-2.0",
44
44
  "author": "Reticle contributors",