@reticlehq/browser 3.7.0 → 3.8.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.
Files changed (104) hide show
  1. package/dist/actions/action-report.d.ts +10 -0
  2. package/dist/actions/action-report.js +59 -14
  3. package/dist/actions/actions-dom.d.ts +4 -0
  4. package/dist/actions/actions-dom.js +20 -4
  5. package/dist/actions/actions.d.ts +1 -0
  6. package/dist/actions/actions.js +94 -46
  7. package/dist/actions/appeared-text.js +2 -2
  8. package/dist/actions/click-target.d.ts +1 -0
  9. package/dist/actions/click-target.js +14 -0
  10. package/dist/actions/danger-context.d.ts +2 -1
  11. package/dist/actions/danger-context.js +43 -17
  12. package/dist/actions/scroll.d.ts +2 -0
  13. package/dist/actions/scroll.js +10 -2
  14. package/dist/activate.d.ts +14 -0
  15. package/dist/activate.js +43 -0
  16. package/dist/busy.d.ts +3 -0
  17. package/dist/busy.js +42 -0
  18. package/dist/commands/commands.js +45 -17
  19. package/dist/connect-options.d.ts +12 -1
  20. package/dist/connect-options.js +18 -1
  21. package/dist/dom/a11y.d.ts +0 -2
  22. package/dist/dom/a11y.js +103 -363
  23. package/dist/dom/chart.d.ts +3 -2
  24. package/dist/dom/chart.js +3 -2
  25. package/dist/dom/dom-ignore.d.ts +6 -0
  26. package/dist/dom/dom-ignore.js +60 -5
  27. package/dist/dom/layout-audit.d.ts +15 -0
  28. package/dist/dom/layout-audit.js +208 -0
  29. package/dist/dom/paint-context.d.ts +15 -0
  30. package/dist/dom/paint-context.js +78 -0
  31. package/dist/dom/query-finders.d.ts +6 -0
  32. package/dist/dom/query-finders.js +216 -0
  33. package/dist/dom/query.d.ts +1 -1
  34. package/dist/dom/query.js +154 -227
  35. package/dist/dom/shadow-registry.d.ts +1 -0
  36. package/dist/dom/shadow-registry.js +27 -0
  37. package/dist/dom/snapshot.d.ts +5 -0
  38. package/dist/dom/snapshot.js +87 -35
  39. package/dist/dom/stable-selector.d.ts +1 -0
  40. package/dist/dom/stable-selector.js +19 -0
  41. package/dist/dom/text-match.d.ts +12 -0
  42. package/dist/dom/text-match.js +115 -0
  43. package/dist/dom/theme.d.ts +1 -0
  44. package/dist/dom/theme.js +21 -9
  45. package/dist/dom/virtualized.js +5 -1
  46. package/dist/dom/visibility.d.ts +7 -0
  47. package/dist/dom/visibility.js +322 -0
  48. package/dist/index.d.ts +4 -2
  49. package/dist/index.js +7 -1
  50. package/dist/observers/blind-spots.js +3 -2
  51. package/dist/observers/canvas.d.ts +18 -0
  52. package/dist/observers/canvas.js +76 -0
  53. package/dist/observers/dom.js +68 -19
  54. package/dist/observers/field.d.ts +11 -0
  55. package/dist/observers/field.js +117 -13
  56. package/dist/observers/install-all.js +4 -0
  57. package/dist/observers/media.d.ts +2 -0
  58. package/dist/observers/media.js +32 -0
  59. package/dist/observers/net-detail/network-body.d.ts +2 -0
  60. package/dist/observers/net-detail/network-body.js +36 -9
  61. package/dist/observers/net-detail/network-event-stream.d.ts +5 -0
  62. package/dist/observers/net-detail/network-event-stream.js +55 -0
  63. package/dist/observers/net-detail/network-frames.d.ts +2 -0
  64. package/dist/observers/net-detail/network-frames.js +39 -0
  65. package/dist/observers/net-detail/network-response-body.d.ts +3 -0
  66. package/dist/observers/net-detail/network-response-body.js +97 -0
  67. package/dist/observers/network.js +111 -101
  68. package/dist/observers/route.d.ts +2 -0
  69. package/dist/observers/route.js +33 -4
  70. package/dist/presenter/chrome/presenter-report-copy.d.ts +0 -5
  71. package/dist/presenter/chrome/presenter-report-copy.js +0 -14
  72. package/dist/presenter/hud-telemetry.js +1 -1
  73. package/dist/presenter/presenter-chat-views.d.ts +3 -4
  74. package/dist/presenter/presenter-chat-views.js +47 -20
  75. package/dist/presenter/presenter-config.d.ts +1 -0
  76. package/dist/presenter/presenter-config.js +1 -0
  77. package/dist/presenter/presenter-frame.d.ts +3 -2
  78. package/dist/presenter/presenter-frame.js +24 -15
  79. package/dist/presenter/presenter-harness-row.d.ts +4 -15
  80. package/dist/presenter/presenter-harness-row.js +11 -50
  81. package/dist/presenter/presenter-report-styles.js +0 -17
  82. package/dist/presenter/presenter-report.d.ts +1 -2
  83. package/dist/presenter/presenter-report.js +4 -54
  84. package/dist/presenter/presenter-run-state.js +2 -1
  85. package/dist/presenter/presenter-settings.d.ts +2 -3
  86. package/dist/presenter/presenter-settings.js +14 -17
  87. package/dist/presenter/presenter-shell.d.ts +6 -4
  88. package/dist/presenter/presenter-shell.js +37 -7
  89. package/dist/presenter/presenter.js +32 -11
  90. package/dist/registry/store-adapters.d.ts +5 -2
  91. package/dist/registry/store-adapters.js +40 -10
  92. package/dist/reticle-inject.js +700 -645
  93. package/dist/reticle.d.ts +4 -2
  94. package/dist/reticle.js +36 -6
  95. package/dist/security/page-location.d.ts +2 -0
  96. package/dist/security/page-location.js +7 -0
  97. package/dist/security/serialization.js +1 -1
  98. package/dist/storage-keys.d.ts +1 -0
  99. package/dist/storage-keys.js +1 -0
  100. package/dist/timers/native/native-timers.d.ts +1 -0
  101. package/dist/timers/native/native-timers.js +5 -0
  102. package/dist/transport/transport.d.ts +1 -0
  103. package/dist/transport/transport.js +18 -0
  104. package/package.json +6 -2
@@ -1,13 +1,34 @@
1
1
  import { readTestId } from '../dom/addressing/testid-attr.js';
2
- import { EventType } from '@reticlehq/core';
2
+ import { EventType, FieldChangeField, FieldChangeKind } from '@reticlehq/core';
3
3
  import { getAccessibleName, isSensitiveField } from '../dom/a11y.js';
4
4
  const MAX_VALUE_LEN = 160;
5
5
  const BURST_MS = 150;
6
+ const CHECKBOX_TYPE = 'checkbox';
7
+ const RADIO_TYPE = 'radio';
8
+ let settledEmit;
6
9
  function isField(target) {
7
10
  return (target instanceof HTMLInputElement ||
8
11
  target instanceof HTMLTextAreaElement ||
9
12
  target instanceof HTMLSelectElement);
10
13
  }
14
+ function isCheckable(element) {
15
+ return (element instanceof HTMLInputElement &&
16
+ (CHECKBOX_TYPE === element.type || RADIO_TYPE === element.type));
17
+ }
18
+ function readingOf(element) {
19
+ if (isCheckable(element))
20
+ return { value: element.value, checked: element.checked };
21
+ return { value: element.value };
22
+ }
23
+ function readingMoved(before, after) {
24
+ if (before.checked !== undefined || after.checked !== undefined) {
25
+ return before.checked !== after.checked;
26
+ }
27
+ return before.value !== after.value;
28
+ }
29
+ function clip(value) {
30
+ return value.length > MAX_VALUE_LEN ? `${value.slice(0, MAX_VALUE_LEN)}…` : value;
31
+ }
11
32
  function fieldName(element) {
12
33
  const testid = readTestId(element);
13
34
  if (null !== testid && testid.length > 0)
@@ -20,21 +41,102 @@ function fieldName(element) {
20
41
  const id = element.getAttribute('id');
21
42
  return null !== id && id.length > 0 ? id : element.tagName.toLowerCase();
22
43
  }
44
+ function fieldPayload(element, kind, after, before) {
45
+ const secret = isSensitiveField(element);
46
+ const payload = {
47
+ [FieldChangeField.FIELD]: fieldName(element),
48
+ [FieldChangeField.KIND]: kind,
49
+ [FieldChangeField.LENGTH]: after.value.length,
50
+ };
51
+ if (after.checked !== undefined)
52
+ payload[FieldChangeField.CHECKED] = after.checked;
53
+ if (before?.checked !== undefined)
54
+ payload[FieldChangeField.PREVIOUS_CHECKED] = before.checked;
55
+ if (secret) {
56
+ payload[FieldChangeField.REDACTED] = true;
57
+ return payload;
58
+ }
59
+ payload[FieldChangeField.VALUE] = clip(after.value);
60
+ if (before !== undefined && before.checked === undefined) {
61
+ payload[FieldChangeField.PREVIOUS] = clip(before.value);
62
+ }
63
+ return payload;
64
+ }
65
+ function settledProperty(el) {
66
+ return isCheckable(el) ? 'checked' : 'value';
67
+ }
68
+ function inheritedDescriptor(el, prop) {
69
+ for (let proto = Object.getPrototypeOf(el); proto !== null;) {
70
+ const found = Object.getOwnPropertyDescriptor(proto, prop);
71
+ if (found !== undefined)
72
+ return found;
73
+ proto = Object.getPrototypeOf(proto);
74
+ }
75
+ return undefined;
76
+ }
77
+ function watchWrites(el, prop) {
78
+ const own = Object.getOwnPropertyDescriptor(el, prop);
79
+ const target = own ?? inheritedDescriptor(el, prop);
80
+ const get = target === undefined ? undefined : Reflect.get(target, 'get');
81
+ const set = target === undefined ? undefined : Reflect.get(target, 'set');
82
+ let written = false;
83
+ const usable = 'function' === typeof get && 'function' === typeof set;
84
+ if (!usable || false === own?.configurable) {
85
+ return { stop: () => { }, written: () => written };
86
+ }
87
+ Object.defineProperty(el, prop, {
88
+ configurable: true,
89
+ enumerable: target?.enumerable ?? false,
90
+ get() {
91
+ return Reflect.apply(get, this, []);
92
+ },
93
+ set(next) {
94
+ written = true;
95
+ Reflect.apply(set, this, [next]);
96
+ },
97
+ });
98
+ let stopped = false;
99
+ return {
100
+ stop: () => {
101
+ if (stopped)
102
+ return;
103
+ stopped = true;
104
+ if (own === undefined)
105
+ Reflect.deleteProperty(el, prop);
106
+ else
107
+ Object.defineProperty(el, prop, own);
108
+ },
109
+ written: () => written,
110
+ };
111
+ }
112
+ export function watchActedField(el) {
113
+ if (!isField(el) || el instanceof HTMLSelectElement)
114
+ return undefined;
115
+ return { before: readingOf(el), ...watchWrites(el, settledProperty(el)) };
116
+ }
117
+ export function publishSettledField(el, acted) {
118
+ const emit = settledEmit;
119
+ if (acted === undefined)
120
+ return;
121
+ acted.stop();
122
+ if (emit === undefined || !isField(el) || !el.isConnected || !acted.written())
123
+ return;
124
+ const before = acted.before;
125
+ if (isSensitiveField(el) && before.checked === undefined)
126
+ return;
127
+ const after = readingOf(el);
128
+ if (!readingMoved(before, after))
129
+ return;
130
+ emit(EventType.FIELD_CHANGE, fieldPayload(el, FieldChangeKind.SETTLED, after, before));
131
+ }
23
132
  export function installField(emit) {
24
133
  const ac = new AbortController();
25
134
  const { signal } = ac;
135
+ const previousEmit = settledEmit;
136
+ settledEmit = emit;
26
137
  const pending = new Map();
27
138
  const report = (element, kind) => {
28
- const value = element.value;
29
- const secret = isSensitiveField(element);
30
- emit(EventType.FIELD_CHANGE, {
31
- field: fieldName(element),
32
- kind,
33
- length: value.length,
34
- ...(secret
35
- ? { redacted: true }
36
- : { value: value.length > MAX_VALUE_LEN ? `${value.slice(0, MAX_VALUE_LEN)}…` : value }),
37
- });
139
+ emit(EventType.FIELD_CHANGE, fieldPayload(element, kind, readingOf(element)));
38
140
  };
39
141
  const clear = (element) => {
40
142
  const timer = pending.get(element);
@@ -49,7 +151,7 @@ export function installField(emit) {
49
151
  clear(element);
50
152
  pending.set(element, setTimeout(() => {
51
153
  pending.delete(element);
52
- report(element, 'input');
154
+ report(element, FieldChangeKind.INPUT);
53
155
  }, BURST_MS));
54
156
  };
55
157
  const onChange = (event) => {
@@ -57,7 +159,7 @@ export function installField(emit) {
57
159
  if (!isField(element))
58
160
  return;
59
161
  clear(element);
60
- report(element, 'change');
162
+ report(element, FieldChangeKind.CHANGE);
61
163
  };
62
164
  document.addEventListener('input', onInput, { signal, capture: true });
63
165
  document.addEventListener('change', onChange, { signal, capture: true });
@@ -65,6 +167,8 @@ export function installField(emit) {
65
167
  for (const timer of pending.values())
66
168
  clearTimeout(timer);
67
169
  pending.clear();
170
+ if (settledEmit === emit)
171
+ settledEmit = previousEmit;
68
172
  ac.abort();
69
173
  };
70
174
  }
@@ -17,6 +17,8 @@ import { installDialogs } from './dialogs.js';
17
17
  import { installAnimation } from './animation.js';
18
18
  import { installScroll } from './scroll.js';
19
19
  import { installHealth } from './health.js';
20
+ import { installMedia } from './media.js';
21
+ import { installCanvas } from './canvas.js';
20
22
  import { reportSdkFailure, SdkSite } from './sdk-failure.js';
21
23
  function guard(emit, site, install) {
22
24
  try {
@@ -65,5 +67,7 @@ export function installAllObservers(emit, options) {
65
67
  guard(emit, SdkSite.DOM_OBSERVER, () => installBlindSpots(emit)),
66
68
  guard(emit, SdkSite.HEALTH_OBSERVER, () => installHealth(emit)),
67
69
  guard(emit, SdkSite.DOM_OBSERVER, () => installContextOpen(emit)),
70
+ guard(emit, SdkSite.DOM_OBSERVER, () => installMedia(emit)),
71
+ guard(emit, SdkSite.DOM_OBSERVER, () => installCanvas(emit)),
68
72
  ];
69
73
  }
@@ -0,0 +1,2 @@
1
+ import { type Emit, type Teardown } from './types.js';
2
+ export declare function installMedia(emit: Emit): Teardown;
@@ -0,0 +1,32 @@
1
+ import { EventType, MediaEvent } from '@reticlehq/core';
2
+ import { refs } from '../dom/addressing/refs.js';
3
+ import { observeSafely } from './types.js';
4
+ const TAGS = { VIDEO: 'video', AUDIO: 'audio' };
5
+ export function installMedia(emit) {
6
+ const ac = new AbortController();
7
+ const onMedia = (event) => {
8
+ observeSafely(() => {
9
+ const el = event.target;
10
+ if (!(el instanceof HTMLMediaElement)) {
11
+ return;
12
+ }
13
+ const tag = TAGS[el.tagName];
14
+ if (tag === undefined) {
15
+ return;
16
+ }
17
+ const duration = Number.isFinite(el.duration) ? el.duration : undefined;
18
+ emit(EventType.MEDIA_STATE, {
19
+ event: event.type,
20
+ tag,
21
+ currentTime: el.currentTime,
22
+ ...(duration === undefined ? {} : { duration }),
23
+ readyState: el.readyState,
24
+ paused: el.paused,
25
+ }, refs.refFor(el));
26
+ });
27
+ };
28
+ for (const type of Object.values(MediaEvent)) {
29
+ document.addEventListener(type, onMedia, { capture: true, signal: ac.signal });
30
+ }
31
+ return () => ac.abort();
32
+ }
@@ -1,5 +1,7 @@
1
+ import { ResponseBodyReason } from '@reticlehq/core';
1
2
  export declare function isStreamingBody(contentType: string | null, contentLength: string | null): boolean;
2
3
  export declare function withBodyDeadline(read: Promise<string>): Promise<string | undefined>;
4
+ export declare function skippedBodyReason(contentType: string | null): ResponseBodyReason | undefined;
3
5
  export declare function isCapturableType(contentType: string | null): boolean;
4
6
  export declare function setNetworkBodyMaxChars(chars: number): void;
5
7
  export declare function networkBodyMaxChars(): number;
@@ -1,12 +1,14 @@
1
- import { REDACTED_VALUE } from '@reticlehq/core';
1
+ import { REDACTED_VALUE, ResponseBodyReason } from '@reticlehq/core';
2
2
  import { isSensitiveKey, safeStringify, scrubKnownSecrets } from '../../security/serialization.js';
3
3
  import { nativeClearTimeout, nativeSetTimeout } from '../../timers/native/native-timers.js';
4
+ import { isEventStream, redactEventStream } from './network-event-stream.js';
4
5
  const CAPTURABLE_CONTENT = /application\/json|text\/|application\/xml|x-www-form-urlencoded|graphql/i;
5
6
  const STREAMING_CONTENT = /event-stream|x-ndjson|application\/stream/i;
6
7
  const ENDLESS_BY_DESIGN = /event-stream/i;
7
8
  export function isStreamingBody(contentType, contentLength) {
8
- if (contentType !== null && ENDLESS_BY_DESIGN.test(contentType))
9
+ if (contentType !== null && ENDLESS_BY_DESIGN.test(contentType)) {
9
10
  return false;
11
+ }
10
12
  return null === contentLength;
11
13
  }
12
14
  const BODY_READ_TIMEOUT_MS = 500;
@@ -21,15 +23,26 @@ export async function withBodyDeadline(read) {
21
23
  ]);
22
24
  }
23
25
  finally {
24
- if (timer !== undefined)
26
+ if (timer !== undefined) {
25
27
  nativeClearTimeout(timer);
28
+ }
26
29
  }
27
30
  }
31
+ export function skippedBodyReason(contentType) {
32
+ if (null === contentType) {
33
+ return undefined;
34
+ }
35
+ return STREAMING_CONTENT.test(contentType)
36
+ ? ResponseBodyReason.STREAMING
37
+ : ResponseBodyReason.NOT_TEXT;
38
+ }
28
39
  export function isCapturableType(contentType) {
29
- if (null === contentType)
40
+ if (null === contentType) {
30
41
  return false;
31
- if (STREAMING_CONTENT.test(contentType))
42
+ }
43
+ if (STREAMING_CONTENT.test(contentType)) {
32
44
  return false;
45
+ }
33
46
  return CAPTURABLE_CONTENT.test(contentType);
34
47
  }
35
48
  const DEFAULT_BODY_CHARS = 8192;
@@ -37,8 +50,9 @@ const MAX_CONFIGURABLE_BODY_CHARS = 262144;
37
50
  const MIN_CONFIGURABLE_BODY_CHARS = 256;
38
51
  let bodyMaxChars = DEFAULT_BODY_CHARS;
39
52
  export function setNetworkBodyMaxChars(chars) {
40
- if (!Number.isFinite(chars))
53
+ if (!Number.isFinite(chars)) {
41
54
  return;
55
+ }
42
56
  bodyMaxChars = Math.min(MAX_CONFIGURABLE_BODY_CHARS, Math.max(MIN_CONFIGURABLE_BODY_CHARS, Math.floor(chars)));
43
57
  }
44
58
  export function networkBodyMaxChars() {
@@ -53,7 +67,13 @@ const AUTH_SCHEME_TOKEN = /\b(Bearer|Basic)\s+(?=[A-Za-z0-9._~+/=-]{16,})(?=[A-Z
53
67
  function redactText(text) {
54
68
  return (text
55
69
  .replace(AUTH_SCHEME_TOKEN, (_m, scheme) => `${scheme} ${REDACTED_VALUE}`)
56
- .replace(/([A-Za-z0-9_.-]+)(\s*[=:]\s*"?)([^&\s,;"}]+)/g, (match, key, sep) => isSensitiveKey(key) ? `${key}${sep}${REDACTED_VALUE}` : match));
70
+ .replace(/([A-Za-z0-9_.-]+)("?\s*[=:]\s*)("(?:\\.|[^"\\])*"?|[^&\s,;"}]+)/g, (match, key, sep, value) => {
71
+ if (!isSensitiveKey(key)) {
72
+ return match;
73
+ }
74
+ const redacted = value.startsWith('"') ? `"${REDACTED_VALUE}"` : REDACTED_VALUE;
75
+ return `${key}${sep}${redacted}`;
76
+ }));
57
77
  }
58
78
  const ERROR_BODY_CHARS = 4096;
59
79
  export function shouldCaptureErrorBody(status, enabled) {
@@ -61,8 +81,9 @@ export function shouldCaptureErrorBody(status, enabled) {
61
81
  }
62
82
  export function projectErrorBody(rawText, contentType) {
63
83
  const projected = projectBody(rawText, contentType);
64
- if (projected.body.length <= ERROR_BODY_CHARS)
84
+ if (projected.body.length <= ERROR_BODY_CHARS) {
65
85
  return projected;
86
+ }
66
87
  return { body: projected.body.slice(0, ERROR_BODY_CHARS), truncated: true };
67
88
  }
68
89
  export function projectBody(rawText, contentType) {
@@ -70,6 +91,7 @@ export function projectBody(rawText, contentType) {
70
91
  const scanChars = scanWidth(!isJson);
71
92
  const oversized = rawText.length > scanChars;
72
93
  const text = oversized ? rawText.slice(0, scanChars) : rawText;
94
+ let incomplete = false;
73
95
  let out;
74
96
  if (isJson) {
75
97
  try {
@@ -79,10 +101,15 @@ export function projectBody(rawText, contentType) {
79
101
  out = redactText(text);
80
102
  }
81
103
  }
104
+ else if (isEventStream(contentType)) {
105
+ const projected = redactEventStream(text);
106
+ out = redactText(projected.body);
107
+ incomplete = projected.truncated;
108
+ }
82
109
  else {
83
110
  out = redactText(text);
84
111
  }
85
112
  out = scrubKnownSecrets(out);
86
- const truncated = oversized || out.length > bodyMaxChars;
113
+ const truncated = incomplete || oversized || out.length > bodyMaxChars;
87
114
  return { body: out.length > bodyMaxChars ? out.slice(0, bodyMaxChars) : out, truncated };
88
115
  }
@@ -0,0 +1,5 @@
1
+ export declare function isEventStream(contentType: string | null): boolean;
2
+ export declare function redactEventStream(text: string): {
3
+ body: string;
4
+ truncated: boolean;
5
+ };
@@ -0,0 +1,55 @@
1
+ import { REDACTED_VALUE } from '@reticlehq/core';
2
+ import { sanitizeWithReport } from '../../security/serialization.js';
3
+ const EVENT_STREAM_CONTENT = /^text\/event-stream(?:\s*;|\s*$)/i;
4
+ const DATA_FIELD = 'data:';
5
+ const DATA_NAME = 'data';
6
+ const JSON_CONTAINER = /^[{[]/;
7
+ const TEXT_MARKER = /^\[[A-Z_]+\]$/;
8
+ export function isEventStream(contentType) {
9
+ return contentType !== null && EVENT_STREAM_CONTENT.test(contentType);
10
+ }
11
+ function isData(line) {
12
+ return DATA_NAME === line || line.startsWith(DATA_FIELD);
13
+ }
14
+ export function redactEventStream(text) {
15
+ let truncated = false;
16
+ const records = text.replace(/\r\n?/g, '\n').split('\n\n');
17
+ const body = records
18
+ .map((record) => {
19
+ const lines = record.split('\n');
20
+ const data = lines
21
+ .filter(isData)
22
+ .map((line) => line.slice(DATA_FIELD.length).replace(/^ /, ''))
23
+ .join('\n');
24
+ if (!JSON_CONTAINER.test(data.trimStart()) || TEXT_MARKER.test(data.trim())) {
25
+ return record;
26
+ }
27
+ let projected;
28
+ try {
29
+ const sanitized = sanitizeWithReport(JSON.parse(data));
30
+ projected = JSON.stringify(sanitized.value);
31
+ if (sanitized.truncation !== undefined) {
32
+ truncated = true;
33
+ }
34
+ }
35
+ catch {
36
+ projected = REDACTED_VALUE;
37
+ truncated = true;
38
+ }
39
+ let first = true;
40
+ return lines
41
+ .flatMap((line) => {
42
+ if (!isData(line)) {
43
+ return [line];
44
+ }
45
+ if (!first) {
46
+ return [];
47
+ }
48
+ first = false;
49
+ return [`${DATA_FIELD} ${projected}`];
50
+ })
51
+ .join('\n');
52
+ })
53
+ .join('\n\n');
54
+ return { body, truncated };
55
+ }
@@ -0,0 +1,2 @@
1
+ export declare function frameFields(data: unknown, captureBodies: boolean): Record<string, unknown>;
2
+ export declare function isBridgeSocket(url: string): boolean;
@@ -0,0 +1,39 @@
1
+ import { RETICLE_WS_PATH } from '@reticlehq/core';
2
+ import { projectBody } from './network-body.js';
3
+ function binaryFrameBytes(data) {
4
+ if (data instanceof ArrayBuffer) {
5
+ return data.byteLength;
6
+ }
7
+ if (typeof Blob !== 'undefined' && data instanceof Blob) {
8
+ return data.size;
9
+ }
10
+ if (ArrayBuffer.isView(data)) {
11
+ return data.byteLength;
12
+ }
13
+ return undefined;
14
+ }
15
+ export function frameFields(data, captureBodies) {
16
+ if (typeof data !== 'string') {
17
+ const bytes = binaryFrameBytes(data);
18
+ return bytes === undefined
19
+ ? { frameType: typeof data }
20
+ : { frameType: 'binary', frameBytes: bytes };
21
+ }
22
+ const out = { frameBytes: data.length };
23
+ if (captureBodies) {
24
+ const { body, truncated } = projectBody(data, 'application/json');
25
+ out['frame'] = body;
26
+ if (truncated) {
27
+ out['frameTruncated'] = true;
28
+ }
29
+ }
30
+ return out;
31
+ }
32
+ export function isBridgeSocket(url) {
33
+ try {
34
+ return new URL(url, location.href).pathname === RETICLE_WS_PATH;
35
+ }
36
+ catch {
37
+ return url.includes(RETICLE_WS_PATH);
38
+ }
39
+ }
@@ -0,0 +1,3 @@
1
+ import { isEventStream } from './network-event-stream.js';
2
+ export { isEventStream };
3
+ export declare function captureResponseBody(response: Response, contentType: string | null, captureBodies: boolean, signal: AbortSignal): Promise<Record<string, unknown>>;
@@ -0,0 +1,97 @@
1
+ import { ResponseBodyReason } from '@reticlehq/core';
2
+ import { nativeClearTimeout, nativeSetTimeout } from '../../timers/native/native-timers.js';
3
+ import { networkBodyMaxChars, projectBody, projectErrorBody, withBodyDeadline, } from './network-body.js';
4
+ import { isEventStream } from './network-event-stream.js';
5
+ export { isEventStream };
6
+ const EVENT_STREAM_READ_MS = 10_000;
7
+ const UTF8_MAX_BYTES_PER_CHAR = 4;
8
+ const REDACTION_LOOKAHEAD = 2;
9
+ async function readEventStream(clone, signal) {
10
+ if (signal.aborted || typeof clone.body?.getReader !== 'function') {
11
+ return undefined;
12
+ }
13
+ const reader = clone.body.getReader();
14
+ const decoder = new TextDecoder();
15
+ const limit = networkBodyMaxChars() * REDACTION_LOOKAHEAD;
16
+ let text = '';
17
+ let complete = false;
18
+ let stopped = false;
19
+ let stop = () => undefined;
20
+ const interrupted = new Promise((resolve) => {
21
+ stop = () => {
22
+ stopped = true;
23
+ resolve();
24
+ };
25
+ });
26
+ const timer = nativeSetTimeout(stop, EVENT_STREAM_READ_MS);
27
+ signal.addEventListener('abort', stop, { once: true });
28
+ try {
29
+ await Promise.race([
30
+ (async () => {
31
+ while (!stopped && !signal.aborted) {
32
+ const next = await reader.read();
33
+ if (stopped || signal.aborted) {
34
+ return;
35
+ }
36
+ if (next.done) {
37
+ text += decoder.decode();
38
+ complete = true;
39
+ return;
40
+ }
41
+ const remaining = limit - text.length;
42
+ const bytes = next.value.subarray(0, remaining * UTF8_MAX_BYTES_PER_CHAR);
43
+ const decoded = decoder.decode(bytes, { stream: true });
44
+ text += decoded.slice(0, remaining);
45
+ if (text.length >= limit || bytes.byteLength < next.value.byteLength) {
46
+ return;
47
+ }
48
+ }
49
+ })(),
50
+ interrupted,
51
+ ]);
52
+ }
53
+ catch {
54
+ }
55
+ finally {
56
+ nativeClearTimeout(timer);
57
+ signal.removeEventListener('abort', stop);
58
+ if (!complete) {
59
+ void reader.cancel().catch(() => undefined);
60
+ }
61
+ reader.releaseLock();
62
+ }
63
+ return !complete && 0 === text.length ? undefined : { text, complete };
64
+ }
65
+ export async function captureResponseBody(response, contentType, captureBodies, signal) {
66
+ const unreadable = captureBodies ? { responseBodyReason: ResponseBodyReason.UNREADABLE } : {};
67
+ if (isEventStream(contentType) && (!captureBodies || signal.aborted)) {
68
+ return unreadable;
69
+ }
70
+ try {
71
+ const clone = response.clone();
72
+ let text;
73
+ let incomplete = false;
74
+ if (isEventStream(contentType)) {
75
+ const stream = await readEventStream(clone, signal);
76
+ text = stream?.text;
77
+ incomplete = false === stream?.complete;
78
+ }
79
+ else {
80
+ text = await withBodyDeadline(clone.text());
81
+ }
82
+ if (undefined === text) {
83
+ return unreadable;
84
+ }
85
+ const projected = captureBodies
86
+ ? projectBody(text, contentType)
87
+ : projectErrorBody(text, contentType);
88
+ return {
89
+ responseBody: projected.body,
90
+ ...(incomplete || projected.truncated ? { responseBodyTruncated: true } : {}),
91
+ ...(captureBodies ? {} : { responseBodyReason: ResponseBodyReason.ERROR_STATUS }),
92
+ };
93
+ }
94
+ catch {
95
+ return unreadable;
96
+ }
97
+ }