@guidelytech/guide-runtime 0.16.0 → 0.17.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.
package/dist/capture.js CHANGED
@@ -1,12 +1,38 @@
1
+ import {
2
+ createScope
3
+ } from "./chunk-63M4VHQG.js";
4
+ import {
5
+ CAPTURE_INTERACTIVE_SELECTOR,
6
+ REDACT_SELECTOR,
7
+ actionTarget,
8
+ canCaptureScreen,
9
+ captureLabel,
10
+ captureRole,
11
+ classifyNumber,
12
+ collectRedactions,
13
+ describeCapturedElement,
14
+ encodeScreenshot,
15
+ fieldValue,
16
+ findSensitiveText,
17
+ grabFrame,
18
+ isMaskedElement,
19
+ isSensitiveField,
20
+ isTextEntry,
21
+ labelControl,
22
+ looksLikeSecretValue,
23
+ normalizeRect,
24
+ redactCanvas,
25
+ requestTabCapture,
26
+ screenshotDataUrl,
27
+ selectedOptionText,
28
+ toFrameRect
29
+ } from "./chunk-THGKUOHM.js";
1
30
  import {
2
31
  composedTarget,
3
32
  describeElement,
4
- isGuidelyElement,
5
- normalizeText
6
- } from "./chunk-YU6SKJK4.js";
7
- import {
8
- createScope
9
- } from "./chunk-KQGSHCNW.js";
33
+ isGuidelyElement
34
+ } from "./chunk-EPR4LWA2.js";
35
+ import "./chunk-V6TY7KAL.js";
10
36
 
11
37
  // src/capture-styles.ts
12
38
  var CAPTURE_STYLES = `
@@ -30,420 +56,6 @@ var CAPTURE_STYLES = `
30
56
  .g-cap[data-captured] .g-cap-dot,.g-cap[data-captured] .g-cap-dot::after{background:#22c55e}
31
57
  `;
32
58
 
33
- // src/capture-dom.ts
34
- var CAPTURE_INTERACTIVE_SELECTOR = "a,button,input,select,textarea,label,summary,[role=button],[role=link],[role=tab],[role=menuitem],[role=menuitemcheckbox],[role=menuitemradio],[role=option],[role=checkbox],[role=radio],[role=switch],[role=combobox],[contenteditable]:not([contenteditable=false])";
35
- var REDACT_SELECTOR = "[data-guidely-redact]";
36
- var TEXT_INPUT_TYPES = /* @__PURE__ */ new Set(["", "text", "email", "search", "tel", "url", "number", "password", "date", "datetime-local", "month", "week", "time"]);
37
- function closestSafe(el, selector) {
38
- if (!selector) return null;
39
- try {
40
- return el.closest(selector);
41
- } catch {
42
- return null;
43
- }
44
- }
45
- function inputType(el) {
46
- return el.tagName === "INPUT" ? (el.getAttribute("type") || "").toLowerCase() : "";
47
- }
48
- function isEditableHost(el) {
49
- const v = el.getAttribute("contenteditable");
50
- return v != null && v.toLowerCase() !== "false";
51
- }
52
- function isTextEntry(el) {
53
- if (el.tagName === "TEXTAREA") return true;
54
- if (el.tagName === "INPUT") return TEXT_INPUT_TYPES.has(inputType(el));
55
- return isEditableHost(el) || !!closestSafe(el, "[contenteditable]:not([contenteditable=false])");
56
- }
57
- function actionTarget(target) {
58
- const hit = closestSafe(target, CAPTURE_INTERACTIVE_SELECTOR) ?? target;
59
- if (!isEditableHost(hit) && isTextEntry(hit) && hit.tagName !== "INPUT" && hit.tagName !== "TEXTAREA") return closestSafe(hit, "[contenteditable]:not([contenteditable=false])") ?? hit;
60
- return hit;
61
- }
62
- function labelControl(el) {
63
- if (el.tagName !== "LABEL") return null;
64
- const label = el;
65
- if (label.control) return label.control;
66
- const forId = label.getAttribute("for");
67
- if (forId) return label.ownerDocument.getElementById(forId);
68
- return label.querySelector("input,select,textarea");
69
- }
70
- function isMaskedElement(el, maskSelector) {
71
- return !!closestSafe(el, REDACT_SELECTOR) || !!closestSafe(el, maskSelector);
72
- }
73
- var SENSITIVE_NAME = /pass|card|cvv|cvc|ssn|secret|token|otp/i;
74
- var SENSITIVE_AUTOCOMPLETE = /cc-|one-time-code|current-password|new-password/i;
75
- function isSensitiveField(el, maskSelector = "") {
76
- if (inputType(el) === "password") return true;
77
- if (SENSITIVE_AUTOCOMPLETE.test(el.getAttribute("autocomplete") || "")) return true;
78
- if (/^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName) && (SENSITIVE_NAME.test(el.getAttribute("name") || "") || SENSITIVE_NAME.test(el.id || ""))) return true;
79
- return isMaskedElement(el, maskSelector);
80
- }
81
- var EMAIL = /[A-Z0-9._%+-]+@[A-Z0-9-]+(?:\.[A-Z0-9-]+)*\.[A-Z]{2,}/gi;
82
- var NUMBER_RUN = /(?:\+\d{1,3}[ .-]?)?(?:\(\d{2,4}\)[ .-]?)?\d+(?:[ .-]\d+)*/g;
83
- function classifyNumber(run) {
84
- const groups = run.match(/\d+/g) ?? [];
85
- const lens = groups.map((g) => g.length);
86
- const digits = lens.reduce((a, b) => a + b, 0);
87
- const special = /[+(.]/.test(run);
88
- if (lens.length === 3 && lens[0] === 3 && lens[1] === 2 && lens[2] === 4 && /^\d{3}[ -]\d{2}[ -]\d{4}$/.test(run)) return "ssn";
89
- if (digits >= 13 && digits <= 19 && !special && lens.every((l) => l >= 4)) return "card";
90
- if (digits >= 10 && digits <= 13 && (/[+(]/.test(run) || lens.length >= 3 && lens[lens.length - 1] === 4 && lens.every((l) => l >= 2) && !run.includes("."))) return "phone";
91
- return null;
92
- }
93
- function findSensitiveText(text) {
94
- const out = [];
95
- if (text.length < 7) return out;
96
- if (text.includes("@")) {
97
- EMAIL.lastIndex = 0;
98
- for (let m = EMAIL.exec(text); m && out.length < 200; m = EMAIL.exec(text)) out.push([m.index, m.index + m[0].length]);
99
- }
100
- if (/\d{3}/.test(text)) {
101
- NUMBER_RUN.lastIndex = 0;
102
- for (let m = NUMBER_RUN.exec(text); m && out.length < 200; m = NUMBER_RUN.exec(text)) {
103
- if (m[0].length >= 9 && classifyNumber(m[0])) out.push([m.index, m.index + m[0].length]);
104
- }
105
- }
106
- return out;
107
- }
108
- function looksLikeSecretValue(value) {
109
- NUMBER_RUN.lastIndex = 0;
110
- for (let m = NUMBER_RUN.exec(value); m; m = NUMBER_RUN.exec(value)) {
111
- const kind = classifyNumber(m[0]);
112
- if (kind === "card" || kind === "ssn") return true;
113
- }
114
- return false;
115
- }
116
- var ROLE_MAP = {
117
- button: "button",
118
- link: "link",
119
- tab: "tab",
120
- menuitem: "menuitem",
121
- menuitemcheckbox: "menuitem",
122
- menuitemradio: "menuitem",
123
- checkbox: "checkbox",
124
- radio: "radio",
125
- switch: "switch",
126
- option: "option",
127
- combobox: "dropdown",
128
- listbox: "dropdown",
129
- textbox: "field",
130
- searchbox: "field",
131
- spinbutton: "field"
132
- };
133
- function captureRole(el) {
134
- const explicit = (el.getAttribute("role") || "").trim().split(/\s+/)[0].toLowerCase();
135
- if (ROLE_MAP[explicit]) return ROLE_MAP[explicit];
136
- const tag = el.tagName.toLowerCase();
137
- if (tag === "a") return el.hasAttribute("href") ? "link" : "element";
138
- if (tag === "button" || tag === "summary") return "button";
139
- if (tag === "select") return "dropdown";
140
- if (tag === "option") return "option";
141
- if (tag === "textarea") return "field";
142
- if (tag === "input") {
143
- const t = inputType(el);
144
- if (t === "checkbox") return "checkbox";
145
- if (t === "radio") return "radio";
146
- if (t === "button" || t === "submit" || t === "reset" || t === "image") return "button";
147
- return "field";
148
- }
149
- if (isEditableHost(el)) return "field";
150
- return "element";
151
- }
152
- var clip = (s, max = 80) => s.length > max ? `${s.slice(0, max - 1).trimEnd()}\u2026` : s;
153
- function textOf(el) {
154
- return normalizeText(el.innerText ?? el.textContent);
155
- }
156
- function labelledBy(el) {
157
- const ids = (el.getAttribute("aria-labelledby") || "").split(/\s+/).filter(Boolean);
158
- return normalizeText(ids.map((id) => el.ownerDocument.getElementById(id)).filter((x) => !!x).map(textOf).join(" "));
159
- }
160
- function associatedLabel(el) {
161
- const labels = el.labels;
162
- if (labels && labels.length) return normalizeText(Array.from(labels).map(textOf).join(" "));
163
- if (el.id) {
164
- try {
165
- const l = el.ownerDocument.querySelector(`label[for="${el.id.replace(/["\\]/g, "\\$&")}"]`);
166
- if (l) return textOf(l);
167
- } catch {
168
- }
169
- }
170
- const wrap = el.parentElement?.closest("label");
171
- return wrap ? textOf(wrap) : "";
172
- }
173
- function captureLabel(el) {
174
- const attr = (name) => normalizeText(el.getAttribute(name));
175
- const tag = el.tagName;
176
- const field = tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT" || isEditableHost(el);
177
- const candidates = [
178
- () => attr("aria-label"),
179
- () => labelledBy(el),
180
- () => /^(INPUT|TEXTAREA|SELECT|METER|PROGRESS|OUTPUT)$/.test(tag) ? associatedLabel(el) : "",
181
- () => tag === "INPUT" && /^(button|submit|reset)$/.test(inputType(el)) ? normalizeText(el.value) : "",
182
- () => field ? "" : textOf(el),
183
- () => attr("placeholder"),
184
- () => attr("alt"),
185
- () => normalizeText(el.querySelector?.("img[alt]")?.getAttribute("alt")),
186
- () => attr("title"),
187
- () => normalizeText(el.querySelector?.("svg title")?.textContent),
188
- () => attr("name")
189
- ];
190
- for (const c of candidates) {
191
- const v = c();
192
- if (v) return clip(v);
193
- }
194
- return "";
195
- }
196
- function describeCapturedElement(el, maskSelector = "") {
197
- return { label: isMaskedElement(el, maskSelector) ? "" : captureLabel(el), role: captureRole(el), tag: el.tagName.toLowerCase() };
198
- }
199
- function fieldValue(el) {
200
- const raw = el.tagName === "INPUT" || el.tagName === "TEXTAREA" ? el.value : textOf(el);
201
- return (raw || "").trim().slice(0, 200);
202
- }
203
- function selectedOptionText(el) {
204
- return normalizeText(Array.from(el.selectedOptions ?? []).map((o) => o.text || o.value).join(", ")).slice(0, 200);
205
- }
206
- var clamp01 = (n) => Math.min(1, Math.max(0, n));
207
- var round = (n) => Math.round(n * 1e4) / 1e4;
208
- function normalizeRect(rect, viewport, pad = 4) {
209
- if (!viewport.width || !viewport.height || rect.width <= 0 || rect.height <= 0) return null;
210
- const x0 = clamp01((rect.x - pad) / viewport.width);
211
- const y0 = clamp01((rect.y - pad) / viewport.height);
212
- const x1 = clamp01((rect.x + rect.width + pad) / viewport.width);
213
- const y1 = clamp01((rect.y + rect.height + pad) / viewport.height);
214
- if (x1 <= x0 || y1 <= y0) return null;
215
- return { x: round(x0), y: round(y0), width: round(x1 - x0), height: round(y1 - y0) };
216
- }
217
- function toFrameRect(rect, scaleX, scaleY, frame, pad = 0) {
218
- const x0 = Math.max(0, Math.floor((rect.x - pad) * scaleX));
219
- const y0 = Math.max(0, Math.floor((rect.y - pad) * scaleY));
220
- const x1 = Math.min(frame.width, Math.ceil((rect.x + rect.width + pad) * scaleX));
221
- const y1 = Math.min(frame.height, Math.ceil((rect.y + rect.height + pad) * scaleY));
222
- return x1 > x0 && y1 > y0 ? { x: x0, y: y0, width: x1 - x0, height: y1 - y0 } : null;
223
- }
224
- function intersectsViewport(r, vw, vh) {
225
- return r.width > 0 && r.height > 0 && r.x < vw && r.y < vh && r.x + r.width > 0 && r.y + r.height > 0;
226
- }
227
- var SKIP_TEXT_PARENTS = /^(SCRIPT|STYLE|NOSCRIPT|TEMPLATE|TEXTAREA|OPTION)$/;
228
- function collectRedactions(doc, options = {}) {
229
- const win = doc.defaultView;
230
- const vw = win?.innerWidth ?? 0;
231
- const vh = win?.innerHeight ?? 0;
232
- const out = [];
233
- const push = (r) => {
234
- if (intersectsViewport(r, vw, vh)) out.push({ x: r.x, y: r.y, width: r.width, height: r.height });
235
- };
236
- const root = doc.body;
237
- if (!root) return out;
238
- const mask = options.maskSelector || "";
239
- const blocks = /* @__PURE__ */ new Set();
240
- try {
241
- root.querySelectorAll(mask ? `${REDACT_SELECTOR},${mask}` : REDACT_SELECTOR).forEach((el) => blocks.add(el));
242
- } catch {
243
- root.querySelectorAll(REDACT_SELECTOR).forEach((el) => blocks.add(el));
244
- }
245
- root.querySelectorAll("input,textarea,select,[contenteditable]:not([contenteditable=false])").forEach((el) => {
246
- if (blocks.has(el)) return;
247
- const t = inputType(el);
248
- if (t === "hidden" || t === "checkbox" || t === "radio" || t === "button" || t === "submit" || t === "reset") return;
249
- if (isSensitiveField(el, mask)) blocks.add(el);
250
- else if ((el.tagName === "INPUT" || el.tagName === "TEXTAREA") && findSensitiveText(el.value || "").length) blocks.add(el);
251
- });
252
- blocks.forEach((el) => push(el.getBoundingClientRect()));
253
- const max = options.maxTextNodes ?? 5e3;
254
- const walker = doc.createTreeWalker(
255
- root,
256
- 4
257
- /* NodeFilter.SHOW_TEXT */
258
- );
259
- let range = null;
260
- let seen = 0;
261
- for (let node = walker.nextNode(); node && seen < max; node = walker.nextNode()) {
262
- const text = node.nodeValue || "";
263
- if (text.length < 7) continue;
264
- seen++;
265
- const parent = node.parentElement;
266
- if (!parent || SKIP_TEXT_PARENTS.test(parent.tagName)) continue;
267
- const hits = findSensitiveText(text);
268
- if (!hits.length) continue;
269
- if (Array.from(blocks).some((b) => b.contains(parent))) continue;
270
- range ?? (range = doc.createRange());
271
- for (const [start, end] of hits) {
272
- try {
273
- range.setStart(node, start);
274
- range.setEnd(node, end);
275
- Array.from(range.getClientRects()).forEach(push);
276
- } catch {
277
- }
278
- }
279
- }
280
- return out;
281
- }
282
-
283
- // src/capture-screen.ts
284
- function canCaptureScreen() {
285
- return typeof navigator !== "undefined" && !!navigator.mediaDevices && typeof navigator.mediaDevices.getDisplayMedia === "function";
286
- }
287
- async function requestTabCapture(container, doc = document) {
288
- if (!canCaptureScreen()) return { ok: false, reason: "unsupported" };
289
- let stream;
290
- try {
291
- stream = await navigator.mediaDevices.getDisplayMedia({
292
- video: { displaySurface: "browser", frameRate: 5 },
293
- audio: false,
294
- preferCurrentTab: true,
295
- selfBrowserSurface: "include",
296
- surfaceSwitching: "exclude"
297
- });
298
- } catch (err) {
299
- return { ok: false, reason: err?.name === "NotAllowedError" ? "denied" : "error" };
300
- }
301
- const track = stream.getVideoTracks()[0];
302
- if (!track) {
303
- stream.getTracks().forEach((t) => t.stop());
304
- return { ok: false, reason: "error" };
305
- }
306
- const surface = track.getSettings?.()?.displaySurface;
307
- if (surface && surface !== "browser") {
308
- stream.getTracks().forEach((t) => t.stop());
309
- return { ok: false, reason: "wrong-surface" };
310
- }
311
- const video = doc.createElement("video");
312
- video.muted = true;
313
- video.playsInline = true;
314
- video.setAttribute("aria-hidden", "true");
315
- video.style.cssText = "position:fixed;left:0;top:0;width:2px;height:2px;opacity:0;pointer-events:none";
316
- try {
317
- video.srcObject = stream;
318
- container.appendChild(video);
319
- } catch {
320
- stream.getTracks().forEach((t) => t.stop());
321
- return { ok: false, reason: "error" };
322
- }
323
- try {
324
- await video.play();
325
- if (!video.videoWidth) await new Promise((resolve) => {
326
- const done = () => resolve();
327
- video.addEventListener("loadeddata", done, { once: true });
328
- setTimeout(done, 1500);
329
- });
330
- } catch {
331
- }
332
- let ended = false;
333
- const listeners = [];
334
- const fv = video;
335
- const rvfc = typeof fv.requestVideoFrameCallback === "function";
336
- let frameAt = 0;
337
- let waiters = [];
338
- const onFrame = (now) => {
339
- frameAt = now;
340
- waiters.splice(0).forEach((w) => w());
341
- if (!ended) fv.requestVideoFrameCallback(onFrame);
342
- };
343
- if (rvfc) fv.requestVideoFrameCallback(onFrame);
344
- const end = () => {
345
- if (ended) return;
346
- ended = true;
347
- stream.getTracks().forEach((t) => t.stop());
348
- video.srcObject = null;
349
- video.remove();
350
- listeners.splice(0).forEach((cb) => cb());
351
- };
352
- track.addEventListener("ended", end);
353
- return {
354
- ok: true,
355
- screen: {
356
- live: () => !ended && track.readyState === "live",
357
- onEnded: (cb) => ended ? cb() : listeners.push(cb),
358
- grab: () => ended ? null : grabFrame(video, doc),
359
- freshSince: (changedAt) => rvfc ? frameAt > changedAt : performance.now() - changedAt > 250,
360
- nextFrame: (timeoutMs = 500) => new Promise((resolve) => {
361
- const t = setTimeout(done, rvfc ? timeoutMs : 250);
362
- function done() {
363
- clearTimeout(t);
364
- waiters = waiters.filter((w) => w !== done);
365
- resolve();
366
- }
367
- if (rvfc) waiters.push(done);
368
- }),
369
- stop: () => {
370
- listeners.length = 0;
371
- end();
372
- }
373
- }
374
- };
375
- }
376
- function grabFrame(video, doc = document) {
377
- const w = video.videoWidth;
378
- const h = video.videoHeight;
379
- if (!w || !h) return null;
380
- const canvas = doc.createElement("canvas");
381
- canvas.width = w;
382
- canvas.height = h;
383
- const ctx = canvas.getContext("2d");
384
- if (!ctx) return null;
385
- ctx.drawImage(video, 0, 0, w, h);
386
- return canvas;
387
- }
388
- function redactCanvas(canvas, rects, viewport) {
389
- const ctx = canvas.getContext("2d");
390
- if (!ctx || !rects.length || !viewport.width || !viewport.height) return 0;
391
- const sx = canvas.width / viewport.width;
392
- const sy = canvas.height / viewport.height;
393
- const tmp = canvas.ownerDocument.createElement("canvas");
394
- const tctx = tmp.getContext("2d");
395
- if (!tctx) return 0;
396
- let count = 0;
397
- for (const rect of rects) {
398
- const r = toFrameRect(rect, sx, sy, canvas, 4);
399
- if (!r) continue;
400
- const block = Math.max(16 * sy, Math.min(r.height, 48 * sy));
401
- tmp.width = Math.max(1, Math.round(r.width / block));
402
- tmp.height = Math.max(1, Math.round(r.height / block));
403
- tctx.imageSmoothingEnabled = true;
404
- tctx.drawImage(canvas, r.x, r.y, r.width, r.height, 0, 0, tmp.width, tmp.height);
405
- ctx.imageSmoothingEnabled = true;
406
- ctx.drawImage(tmp, 0, 0, tmp.width, tmp.height, r.x, r.y, r.width, r.height);
407
- ctx.fillStyle = "rgba(120,120,120,.28)";
408
- ctx.fillRect(r.x, r.y, r.width, r.height);
409
- count++;
410
- }
411
- return count;
412
- }
413
- function scaled(canvas, maxWidth) {
414
- if (canvas.width <= maxWidth) return canvas;
415
- const out = canvas.ownerDocument.createElement("canvas");
416
- out.width = maxWidth;
417
- out.height = Math.round(canvas.height * maxWidth / canvas.width);
418
- const ctx = out.getContext("2d");
419
- if (!ctx) return canvas;
420
- ctx.imageSmoothingQuality = "high";
421
- ctx.drawImage(canvas, 0, 0, out.width, out.height);
422
- return out;
423
- }
424
- var toBlob = (canvas, type, quality) => new Promise((resolve) => {
425
- try {
426
- canvas.toBlob((b) => resolve(b), type, quality);
427
- } catch {
428
- resolve(null);
429
- }
430
- });
431
- async function encodeScreenshot(canvas, options = {}) {
432
- const src = scaled(canvas, options.maxWidth ?? 2560);
433
- const maxBytes = options.maxBytes ?? 8 * 1024 * 1024;
434
- let blob = await toBlob(src, "image/webp", 0.85);
435
- if (!blob || blob.type !== "image/webp") blob = await toBlob(src, "image/jpeg", 0.85);
436
- if (blob && blob.size > maxBytes) blob = await toBlob(src, "image/jpeg", 0.6);
437
- return blob && blob.size <= maxBytes ? blob : null;
438
- }
439
- function screenshotDataUrl(canvas) {
440
- try {
441
- return scaled(canvas, 1600).toDataURL("image/jpeg", 0.7);
442
- } catch {
443
- return null;
444
- }
445
- }
446
-
447
59
  // src/capture.ts
448
60
  var DOUBLE_CLICK_MS = 400;
449
61
  var NAV_AFTER_ACTION_MS = 1500;