@matterfact/embed 0.2.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/react.cjs ADDED
@@ -0,0 +1,1157 @@
1
+ "use client";
2
+ "use strict";
3
+ "use client";
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __esm = (fn, res) => function __init() {
9
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
10
+ };
11
+ var __export = (target, all) => {
12
+ for (var name in all)
13
+ __defProp(target, name, { get: all[name], enumerable: true });
14
+ };
15
+ var __copyProps = (to, from, except, desc) => {
16
+ if (from && typeof from === "object" || typeof from === "function") {
17
+ for (let key of __getOwnPropNames(from))
18
+ if (!__hasOwnProp.call(to, key) && key !== except)
19
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
20
+ }
21
+ return to;
22
+ };
23
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
24
+
25
+ // src/dom/a11y.ts
26
+ function createStyleCache() {
27
+ const cache = /* @__PURE__ */ new WeakMap();
28
+ return ((el, pseudo) => {
29
+ if (pseudo) return window.getComputedStyle(el, pseudo);
30
+ let style = cache.get(el);
31
+ if (!style) {
32
+ style = window.getComputedStyle(el);
33
+ cache.set(el, style);
34
+ }
35
+ return style;
36
+ });
37
+ }
38
+ function hasLayout() {
39
+ return document.documentElement.getClientRects().length > 0;
40
+ }
41
+ function isVisible(el, style, layout) {
42
+ const display = style.display;
43
+ if (display === "none") return false;
44
+ const visibility = style.visibility;
45
+ if (visibility === "hidden" || visibility === "collapse") return false;
46
+ if (Number(style.opacity) === 0) return false;
47
+ if (layout && display !== "contents") {
48
+ const box = el.getBoundingClientRect();
49
+ if (box.width <= 0 || box.height <= 0) return false;
50
+ if (el.getClientRects().length === 0) return false;
51
+ if (box.right < -2e3 || box.bottom < -2e3) return false;
52
+ }
53
+ return true;
54
+ }
55
+ function receivesPointerEvents(style) {
56
+ return style.pointerEvents !== "none";
57
+ }
58
+ function isEditable(el) {
59
+ const v = el.getAttribute("contenteditable");
60
+ return v !== null && v !== "false";
61
+ }
62
+ function isInteractive(el, role, style, parentStyle) {
63
+ const tag = el.tagName;
64
+ if (tag === "A" && el.hasAttribute("href")) return true;
65
+ if (INTERACTIVE_TAGS.has(tag)) return true;
66
+ if (role !== null && INTERACTIVE_ROLES.has(role)) return true;
67
+ const tabindex = el.getAttribute("tabindex");
68
+ if (tabindex !== null && Number(tabindex) >= 0) return true;
69
+ if (isEditable(el)) return true;
70
+ if (style.cursor === "pointer" && parentStyle?.cursor !== "pointer")
71
+ return true;
72
+ return false;
73
+ }
74
+ function isOpaque(el) {
75
+ return OPAQUE_TAGS.has(el.tagName) || isEditable(el);
76
+ }
77
+ function isFilled(el, role) {
78
+ const tag = el.tagName;
79
+ if (tag === "INPUT" || tag === "TEXTAREA") {
80
+ if (!VALUE_ROLES.has(role)) return false;
81
+ return (el.value ?? "").length > 0;
82
+ }
83
+ if (isEditable(el)) return (el.textContent ?? "").trim().length > 0;
84
+ return false;
85
+ }
86
+ function ariaFlag(el, attr) {
87
+ const v = el.getAttribute(attr);
88
+ if (v === "true") return "true";
89
+ if (v === "mixed") return "mixed";
90
+ return null;
91
+ }
92
+ function states(el, role) {
93
+ const out = [];
94
+ const tag = el.tagName;
95
+ if (tag === "INPUT" && (role === "checkbox" || role === "radio")) {
96
+ if (el.checked) out.push("[checked]");
97
+ } else {
98
+ const checked = ariaFlag(el, "aria-checked");
99
+ if (checked === "mixed") out.push("[checked=mixed]");
100
+ else if (checked === "true") out.push("[checked]");
101
+ }
102
+ if ((0, import_dom_accessibility_api.isDisabled)(el)) out.push("[disabled]");
103
+ const expanded = el.getAttribute("aria-expanded");
104
+ if (expanded === "true" || expanded === "false") {
105
+ if (expanded === "true") out.push("[expanded]");
106
+ } else if (tag === "SUMMARY") {
107
+ const details = el.parentElement;
108
+ if (details?.tagName === "DETAILS" && details.open) {
109
+ out.push("[expanded]");
110
+ }
111
+ }
112
+ if (role === "heading") {
113
+ const aria = el.getAttribute("aria-level");
114
+ const level = aria ? Number(aria) : Number(tag[1]);
115
+ if (Number.isFinite(level) && level > 0) out.push(`[level=${level}]`);
116
+ }
117
+ const pressed = ariaFlag(el, "aria-pressed");
118
+ if (pressed === "mixed") out.push("[pressed=mixed]");
119
+ else if (pressed === "true") out.push("[pressed]");
120
+ if (el.getAttribute("aria-selected") === "true") out.push("[selected]");
121
+ if (isFilled(el, role)) out.push("[filled]");
122
+ return out.join(" ");
123
+ }
124
+ function roleOf(el) {
125
+ if (isEditable(el)) return "textbox";
126
+ return (0, import_dom_accessibility_api.getRole)(el);
127
+ }
128
+ function nameIsUntrusted(el) {
129
+ const labelledby = el.getAttribute("aria-labelledby");
130
+ if (labelledby) {
131
+ for (const id of labelledby.split(/\s+/)) {
132
+ if (!id) continue;
133
+ const ref = el.ownerDocument.getElementById(id);
134
+ if (!ref) continue;
135
+ if (ref.matches(UNTRUSTED_CONTENT) || ref.querySelector(UNTRUSTED_CONTENT)) {
136
+ return true;
137
+ }
138
+ }
139
+ return false;
140
+ }
141
+ if (el.hasAttribute("aria-label")) return false;
142
+ return el.querySelector(UNTRUSTED_CONTENT) !== null;
143
+ }
144
+ function nameOf(el, styleOf) {
145
+ const name = (0, import_dom_accessibility_api.computeAccessibleName)(el, {
146
+ getComputedStyle: styleOf,
147
+ // jsdom logs a console error if you claim pseudo-element support it doesn't have.
148
+ computedStyleSupportsPseudoElements: false
149
+ });
150
+ if (!name) return "";
151
+ if (nameIsUntrusted(el)) {
152
+ return el.getAttribute("aria-label") || "";
153
+ }
154
+ return name;
155
+ }
156
+ var import_dom_accessibility_api, INTERACTIVE_ROLES, INTERACTIVE_TAGS, NAME_FROM_CONTENT, VALUE_ROLES, OPAQUE_TAGS, UNTRUSTED_CONTENT;
157
+ var init_a11y = __esm({
158
+ "src/dom/a11y.ts"() {
159
+ "use strict";
160
+ import_dom_accessibility_api = require("dom-accessibility-api");
161
+ INTERACTIVE_ROLES = /* @__PURE__ */ new Set([
162
+ "button",
163
+ "checkbox",
164
+ "combobox",
165
+ "link",
166
+ "listbox",
167
+ "menuitem",
168
+ "menuitemcheckbox",
169
+ "menuitemradio",
170
+ "option",
171
+ "radio",
172
+ "searchbox",
173
+ "slider",
174
+ "spinbutton",
175
+ "switch",
176
+ "tab",
177
+ "textbox",
178
+ "treeitem"
179
+ ]);
180
+ INTERACTIVE_TAGS = /* @__PURE__ */ new Set([
181
+ "BUTTON",
182
+ "INPUT",
183
+ "SELECT",
184
+ "TEXTAREA",
185
+ "SUMMARY"
186
+ ]);
187
+ NAME_FROM_CONTENT = /* @__PURE__ */ new Set([
188
+ "button",
189
+ "cell",
190
+ "checkbox",
191
+ "columnheader",
192
+ "gridcell",
193
+ "heading",
194
+ "link",
195
+ "menuitem",
196
+ "menuitemcheckbox",
197
+ "menuitemradio",
198
+ "option",
199
+ "radio",
200
+ "row",
201
+ "rowheader",
202
+ "switch",
203
+ "tab",
204
+ "tooltip",
205
+ "treeitem"
206
+ ]);
207
+ VALUE_ROLES = /* @__PURE__ */ new Set(["textbox", "searchbox", "spinbutton"]);
208
+ OPAQUE_TAGS = /* @__PURE__ */ new Set(["INPUT", "TEXTAREA"]);
209
+ UNTRUSTED_CONTENT = 'input,textarea,select,[contenteditable=""],[contenteditable="true"],[aria-valuetext],[data-mf-private]';
210
+ }
211
+ });
212
+
213
+ // src/dom/refs.ts
214
+ function beginSnapshot() {
215
+ generation++;
216
+ registry.clear();
217
+ }
218
+ function refFor(el, role, name) {
219
+ const key = `${role} ${name}`;
220
+ let entry = entries.get(el);
221
+ if (!entry || entry.key !== key) {
222
+ entry = { id: `e${++counter}`, key, gen: generation };
223
+ entries.set(el, entry);
224
+ } else {
225
+ entry.gen = generation;
226
+ }
227
+ registry.set(entry.id, el);
228
+ return entry.id;
229
+ }
230
+ function resolveRef(ref) {
231
+ const el = registry.get(ref);
232
+ if (!el) return null;
233
+ if (!el.isConnected) {
234
+ registry.delete(ref);
235
+ return null;
236
+ }
237
+ const entry = entries.get(el);
238
+ if (!entry || entry.id !== ref || entry.gen !== generation) return null;
239
+ return el;
240
+ }
241
+ var counter, generation, entries, registry;
242
+ var init_refs = __esm({
243
+ "src/dom/refs.ts"() {
244
+ "use strict";
245
+ counter = 0;
246
+ generation = 0;
247
+ entries = /* @__PURE__ */ new WeakMap();
248
+ registry = /* @__PURE__ */ new Map();
249
+ }
250
+ });
251
+
252
+ // src/dom/snapshot.ts
253
+ var snapshot_exports = {};
254
+ __export(snapshot_exports, {
255
+ resolveRef: () => resolveRef,
256
+ snapshot: () => snapshot,
257
+ snapshotRegion: () => snapshotRegion
258
+ });
259
+ function withinBudget(w) {
260
+ if (w.nodes >= MAX_NODES || w.lines >= MAX_LINES) {
261
+ w.truncated = true;
262
+ return false;
263
+ }
264
+ return true;
265
+ }
266
+ function isOurs(el) {
267
+ return el.id === "matterfact-embed" || el.hasAttribute("data-mf-embed");
268
+ }
269
+ function isHidden(el, style, layout) {
270
+ if (el.getAttribute("aria-hidden") === "true") return true;
271
+ if (el.hasAttribute("hidden") || el.hasAttribute("inert")) return true;
272
+ return !isVisible(el, style, layout);
273
+ }
274
+ function childrenOf(el) {
275
+ if (el.shadowRoot) return el.shadowRoot.childNodes;
276
+ if (el.tagName === "SLOT") {
277
+ const assigned = el.assignedNodes({ flatten: true });
278
+ if (assigned.length) return assigned;
279
+ }
280
+ return el.childNodes;
281
+ }
282
+ function visitChildren(el, parent, style, w) {
283
+ const children = childrenOf(el);
284
+ for (let i = 0; i < children.length; i++) {
285
+ const child = children[i];
286
+ if (child) visitNode(child, parent, style, w);
287
+ }
288
+ }
289
+ function visitText(node, parent, w) {
290
+ const text = (node.nodeValue ?? "").replace(/\s+/g, " ").trim();
291
+ if (!text) return;
292
+ if (NAME_FROM_CONTENT.has(parent.role)) return;
293
+ if (!withinBudget(w)) return;
294
+ w.lines++;
295
+ parent.children.push(text);
296
+ }
297
+ function optionNode(opt) {
298
+ const disabled = opt.matches(":disabled") || (0, import_dom_accessibility_api2.isDisabled)(opt);
299
+ return {
300
+ role: "option",
301
+ name: (opt.label || opt.textContent || "").trim(),
302
+ props: (disabled ? " [disabled]" : "") + (opt.selected ? " [selected]" : ""),
303
+ children: []
304
+ };
305
+ }
306
+ function emitSelectOptions(select, parent, w) {
307
+ const options = select.options;
308
+ const shown = Math.min(options.length, MAX_OPTIONS);
309
+ for (let i = 0; i < shown; i++) {
310
+ if (!withinBudget(w)) return;
311
+ const opt = options[i];
312
+ if (!opt || isPrivate(opt)) continue;
313
+ w.lines++;
314
+ parent.children.push(optionNode(opt));
315
+ }
316
+ let extraSelected = 0;
317
+ const selected = select.selectedOptions;
318
+ for (let i = 0; i < selected.length; i++) {
319
+ const opt = selected[i];
320
+ if (opt && opt.index >= shown && !isPrivate(opt)) {
321
+ if (!withinBudget(w)) return;
322
+ w.lines++;
323
+ parent.children.push(optionNode(opt));
324
+ extraSelected++;
325
+ }
326
+ }
327
+ const remaining = options.length - shown - extraSelected;
328
+ if (remaining > 0 && withinBudget(w)) {
329
+ w.lines++;
330
+ parent.children.push(`\u2026 (+${remaining} more options)`);
331
+ }
332
+ }
333
+ function emitDatalistOptions(input, parent, w) {
334
+ const list = input.list;
335
+ if (!list) return;
336
+ const options = list.options;
337
+ const shown = Math.min(options.length, MAX_OPTIONS);
338
+ for (let i = 0; i < shown; i++) {
339
+ if (!withinBudget(w)) return;
340
+ const opt = options[i];
341
+ if (!opt || isPrivate(opt)) continue;
342
+ w.lines++;
343
+ parent.children.push(optionNode(opt));
344
+ }
345
+ const remaining = options.length - shown;
346
+ if (remaining > 0 && withinBudget(w)) {
347
+ w.lines++;
348
+ parent.children.push(`\u2026 (+${remaining} more options)`);
349
+ }
350
+ }
351
+ function visitElement(el, parent, parentStyle, w) {
352
+ if (!withinBudget(w)) return;
353
+ if (SKIP_TAGS.has(el.tagName) || isOurs(el)) return;
354
+ if (isPrivate(el)) return;
355
+ w.nodes++;
356
+ const style = w.styleOf(el);
357
+ if (isHidden(el, style, w.layout)) return;
358
+ const role = roleOf(el);
359
+ const transparent = role === null || TRANSPARENT.has(role);
360
+ const interactive = isInteractive(el, role, style, parentStyle);
361
+ if (transparent && !interactive) {
362
+ visitChildren(el, parent, style, w);
363
+ return;
364
+ }
365
+ const emittedRole = role === null || TRANSPARENT.has(role) ? "generic" : role;
366
+ const name = nameOf(el, w.styleOf);
367
+ let ref = "";
368
+ if (interactive && receivesPointerEvents(style)) {
369
+ const r = refFor(el, emittedRole, name);
370
+ ref = ` [ref=${r}]`;
371
+ w.onRef?.(r, el);
372
+ }
373
+ const state = states(el, emittedRole);
374
+ const node = {
375
+ role: emittedRole,
376
+ name,
377
+ props: (state ? ` ${state}` : "") + ref,
378
+ children: []
379
+ };
380
+ w.lines++;
381
+ parent.children.push(node);
382
+ if (el.tagName === "SELECT") {
383
+ emitSelectOptions(el, node, w);
384
+ return;
385
+ }
386
+ if (el.tagName === "INPUT" && el.list) {
387
+ emitDatalistOptions(el, node, w);
388
+ }
389
+ if (isOpaque(el)) return;
390
+ visitChildren(el, node, style, w);
391
+ }
392
+ function visitNode(node, parent, parentStyle, w) {
393
+ if (node.nodeType === 3) {
394
+ visitText(node, parent, w);
395
+ } else if (node.nodeType === 1) {
396
+ visitElement(node, parent, parentStyle, w);
397
+ }
398
+ }
399
+ function sanitize(text, max) {
400
+ const collapsed = text.replace(/\s+/g, " ").trim();
401
+ const clean = redact(collapsed);
402
+ return clean.length > max ? `${clean.slice(0, max - 1)}\u2026` : clean;
403
+ }
404
+ function quote(text) {
405
+ return `"${text.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
406
+ }
407
+ function render(children, depth, out) {
408
+ const pad = " ".repeat(depth);
409
+ for (const child of children) {
410
+ if (typeof child === "string") {
411
+ out.push(`${pad}- text: ${quote(sanitize(child, MAX_TEXT))}`);
412
+ continue;
413
+ }
414
+ const name = child.name ? ` ${quote(sanitize(child.name, MAX_NAME))}` : "";
415
+ const head = `${pad}- ${child.role}${name}${child.props}`;
416
+ if (child.children.length) {
417
+ out.push(`${head}:`);
418
+ render(child.children, depth + 1, out);
419
+ } else {
420
+ out.push(head);
421
+ }
422
+ }
423
+ }
424
+ function walk(root, onRef) {
425
+ const styleOf = createStyleCache();
426
+ const w = {
427
+ styleOf,
428
+ layout: hasLayout(),
429
+ nodes: 0,
430
+ lines: 0,
431
+ truncated: false,
432
+ onRef
433
+ };
434
+ if ((0, import_dom_accessibility_api2.isInaccessible)(root, { getComputedStyle: styleOf })) {
435
+ return { yaml: "", truncated: false };
436
+ }
437
+ const tree = { role: "", name: "", props: "", children: [] };
438
+ visitChildren(root, tree, styleOf(root), w);
439
+ const lines = [];
440
+ render(tree.children, 0, lines);
441
+ if (w.truncated && lines.length) {
442
+ lines[lines.length - 1] = '- text: "\u2026 (snapshot truncated \u2014 more of the page is below)"';
443
+ }
444
+ return { yaml: lines.join("\n"), truncated: w.truncated };
445
+ }
446
+ function snapshot() {
447
+ const root = document.body;
448
+ if (!root) return { yaml: "", truncated: false, visibleRefs: [] };
449
+ beginSnapshot();
450
+ const visibleRefs = [];
451
+ const vh = window.innerHeight || 0;
452
+ const vw = window.innerWidth || 0;
453
+ const result = walk(root, (ref, el) => {
454
+ const r = el.getBoundingClientRect();
455
+ if (r.bottom > 0 && r.top < vh && r.right > 0 && r.left < vw) {
456
+ visibleRefs.push(ref);
457
+ }
458
+ });
459
+ return { ...result, visibleRefs };
460
+ }
461
+ function snapshotRegion(root) {
462
+ if (!root.isConnected) return { yaml: "", truncated: false };
463
+ return walk(root);
464
+ }
465
+ var import_dom_accessibility_api2, MAX_NODES, MAX_LINES, MAX_NAME, MAX_TEXT, MAX_OPTIONS, TRANSPARENT, SKIP_TAGS;
466
+ var init_snapshot = __esm({
467
+ "src/dom/snapshot.ts"() {
468
+ "use strict";
469
+ import_dom_accessibility_api2 = require("dom-accessibility-api");
470
+ init_context();
471
+ init_a11y();
472
+ init_refs();
473
+ init_refs();
474
+ MAX_NODES = 4e3;
475
+ MAX_LINES = 800;
476
+ MAX_NAME = 120;
477
+ MAX_TEXT = 120;
478
+ MAX_OPTIONS = 25;
479
+ TRANSPARENT = /* @__PURE__ */ new Set(["generic", "presentation", "none"]);
480
+ SKIP_TAGS = /* @__PURE__ */ new Set([
481
+ "SCRIPT",
482
+ "STYLE",
483
+ "NOSCRIPT",
484
+ "TEMPLATE",
485
+ "SVG",
486
+ "HEAD",
487
+ "LINK",
488
+ "META",
489
+ "BASE"
490
+ ]);
491
+ }
492
+ });
493
+
494
+ // src/context.ts
495
+ var context_exports = {};
496
+ __export(context_exports, {
497
+ beginAuth: () => beginAuth,
498
+ callTool: () => callTool,
499
+ isPrivate: () => isPrivate,
500
+ readPageContext: () => readPageContext,
501
+ redact: () => redact,
502
+ sendRegion: () => sendRegion,
503
+ sendSnapshot: () => sendSnapshot,
504
+ start: () => start
505
+ });
506
+ function redact(text) {
507
+ let out = text;
508
+ for (const re of PII) out = out.replace(re, "[redacted]");
509
+ return out;
510
+ }
511
+ function isPrivate(el) {
512
+ if (el.closest("[data-mf-private]")) return true;
513
+ const tag = el.tagName;
514
+ if (tag === "INPUT") {
515
+ const t = el.type;
516
+ if (t === "password" || t === "hidden") return true;
517
+ }
518
+ return false;
519
+ }
520
+ function embeddedMatterfactEntities() {
521
+ if (!widgetOrigin) return [];
522
+ const out = [];
523
+ for (const frame of Array.from(document.querySelectorAll("iframe"))) {
524
+ let u;
525
+ try {
526
+ u = new URL(frame.getAttribute("src") || "", location.href);
527
+ } catch {
528
+ continue;
529
+ }
530
+ if (u.origin !== widgetOrigin) continue;
531
+ const m = u.pathname.match(/^\/artifacts\/(.+?)\/?$/);
532
+ if (!m) continue;
533
+ const id = decodeURIComponent(m[1]);
534
+ if (!id || out.some((e) => e.id === id)) continue;
535
+ out.push({
536
+ kind: "artifact",
537
+ id,
538
+ label: redact(frame.getAttribute("title") || "") || id
539
+ });
540
+ }
541
+ return out;
542
+ }
543
+ function opaqueFrameCount() {
544
+ return Array.from(document.querySelectorAll("iframe")).filter((f) => {
545
+ try {
546
+ const u = new URL(f.getAttribute("src") || "", location.href);
547
+ return u.origin !== location.origin && u.origin !== widgetOrigin;
548
+ } catch {
549
+ return false;
550
+ }
551
+ }).length;
552
+ }
553
+ function readPageContext() {
554
+ const declared = window.matterfact?.context ?? {};
555
+ const found = embeddedMatterfactEntities();
556
+ const opaque = opaqueFrameCount();
557
+ return {
558
+ // Path only. A query string is where session ids, tokens and email addresses
559
+ // live; it is not ours to take.
560
+ url: location.origin + location.pathname,
561
+ path: location.pathname,
562
+ title: redact(document.title),
563
+ locale: document.documentElement.lang || void 0,
564
+ ...declared,
565
+ // After the spread: what we FOUND is additive to what the host DECLARED, never a
566
+ // replacement. A host that declares its own entities still gets the artifacts we
567
+ // spotted, and vice versa.
568
+ entities: [...declared.entities ?? [], ...found],
569
+ data: {
570
+ ...declared.data ?? {},
571
+ ...opaque ? { opaque_frames: opaque } : {}
572
+ }
573
+ };
574
+ }
575
+ function publishContext() {
576
+ send?.({ type: "host.context", context: readPageContext() });
577
+ }
578
+ function pushActivity(e) {
579
+ activity.push({ ...e, seq: ++activitySeq, ts: Date.now() });
580
+ if (activity.length > MAX_ACTIVITY) activity.shift();
581
+ send?.({ type: "host.activity", events: [activity[activity.length - 1]] });
582
+ }
583
+ function controlLabel(el) {
584
+ const aria = el.getAttribute("aria-label");
585
+ if (aria) return aria;
586
+ const id = el.id;
587
+ if (id) {
588
+ const forLabel = document.querySelector(`label[for="${CSS.escape(id)}"]`);
589
+ const t = forLabel?.textContent?.trim();
590
+ if (t) return t;
591
+ }
592
+ const wrapping = el.closest("label")?.textContent?.trim();
593
+ if (wrapping) return wrapping;
594
+ const placeholder = el.getAttribute("placeholder");
595
+ if (placeholder) return placeholder;
596
+ return "";
597
+ }
598
+ function describe(el) {
599
+ const role = el.getAttribute("role") || el.tagName.toLowerCase();
600
+ const label = el.getAttribute("aria-label") || (FORM_CONTROLS.has(el.tagName) ? controlLabel(el) : el.innerText?.trim().slice(0, 60)) || el.getAttribute("title") || "";
601
+ return label ? `${role} "${redact(label)}"` : role;
602
+ }
603
+ function onClick(ev) {
604
+ const target = ev.target;
605
+ if (!target || !(target instanceof Element)) return;
606
+ const label = target.closest("label");
607
+ const labelled = label ? label.getAttribute("for") && document.getElementById(label.getAttribute("for")) || label.querySelector("input,select,textarea") : null;
608
+ const actionable = labelled ?? target.closest(ACTIONABLE);
609
+ if (!actionable || isPrivate(actionable)) return;
610
+ pushActivity({ type: "click", summary: `clicked ${describe(actionable)}` });
611
+ }
612
+ function onSubmit(ev) {
613
+ const el = ev.target;
614
+ if (!el || isPrivate(el)) return;
615
+ pushActivity({ type: "submit", summary: `submitted ${describe(el)}` });
616
+ }
617
+ function onChange(ev) {
618
+ const el = ev.target;
619
+ if (!el || !(el instanceof Element) || isPrivate(el)) return;
620
+ const tag = el.tagName;
621
+ const type = el.type;
622
+ if (tag === "INPUT" && (type === "password" || type === "hidden")) return;
623
+ let summary = `changed ${describe(el)}`;
624
+ if (tag === "SELECT") {
625
+ const opt = el.selectedOptions[0]?.text;
626
+ if (opt) summary = `${describe(el)} \u2192 "${redact(opt)}"`;
627
+ } else if (type === "checkbox" || type === "radio") {
628
+ summary = `${el.checked ? "checked" : "unchecked"} ${describe(el)}`;
629
+ }
630
+ pushActivity({ type: "input", summary });
631
+ }
632
+ function watchNavigation() {
633
+ const fire = () => {
634
+ const url = location.pathname;
635
+ if (url === lastUrl) return;
636
+ lastUrl = url;
637
+ pushActivity({ type: "nav", summary: `navigated to ${url}` });
638
+ publishContext();
639
+ };
640
+ for (const name of ["pushState", "replaceState"]) {
641
+ const orig = history[name];
642
+ history[name] = function(...args) {
643
+ const r = orig.apply(this, args);
644
+ fire();
645
+ return r;
646
+ };
647
+ }
648
+ window.addEventListener("popstate", fire);
649
+ }
650
+ async function readFocus() {
651
+ const focus = {};
652
+ const sel = window.getSelection?.();
653
+ if (sel && !sel.isCollapsed) {
654
+ const anchor = sel.anchorNode?.parentElement ?? null;
655
+ if (!anchor || !anchor.closest("[data-mf-private]")) {
656
+ const text = sel.toString().trim().slice(0, 500);
657
+ if (text) focus.selection = redact(text);
658
+ }
659
+ }
660
+ const active = document.activeElement;
661
+ if (active && active !== document.body && !isPrivate(active) && !active.closest("[data-mf-private]")) {
662
+ const { snapshot: snapshot2 } = await Promise.resolve().then(() => (init_snapshot(), snapshot_exports));
663
+ void snapshot2;
664
+ focus.focused = {
665
+ label: redact(describeControl(active)),
666
+ role: active.getAttribute("role") || active.tagName.toLowerCase()
667
+ };
668
+ }
669
+ const doc = document.documentElement;
670
+ const scrollable = doc.scrollHeight - doc.clientHeight;
671
+ focus.scroll = scrollable > 0 ? Math.round(doc.scrollTop / scrollable * 100) / 100 : 0;
672
+ return focus;
673
+ }
674
+ function describeControl(el) {
675
+ return describe(el);
676
+ }
677
+ function scheduleFocus() {
678
+ if (focusTimer) return;
679
+ focusTimer = setTimeout(async () => {
680
+ focusTimer = null;
681
+ lastFocus = await readFocus();
682
+ send?.({ type: "host.focus", focus: lastFocus });
683
+ }, 250);
684
+ }
685
+ async function sendSnapshot(emit) {
686
+ const { snapshot: snapshot2 } = await Promise.resolve().then(() => (init_snapshot(), snapshot_exports));
687
+ const { yaml, truncated, visibleRefs } = snapshot2();
688
+ emit({
689
+ type: "host.snapshot",
690
+ snapshot: { yaml: redact(yaml), seq: ++snapshotSeq, truncated }
691
+ });
692
+ lastFocus = { ...lastFocus, visibleRefs };
693
+ emit({ type: "host.focus", focus: lastFocus });
694
+ }
695
+ async function sendRegion(ref, emit) {
696
+ const { snapshotRegion: snapshotRegion2, resolveRef: resolveRef2 } = await Promise.resolve().then(() => (init_snapshot(), snapshot_exports));
697
+ const el = resolveRef2(ref);
698
+ if (!el) {
699
+ emit({
700
+ type: "host.region",
701
+ ref,
702
+ yaml: "(this element is no longer on the page)"
703
+ });
704
+ return;
705
+ }
706
+ const { yaml } = snapshotRegion2(el);
707
+ emit({ type: "host.region", ref, yaml: redact(yaml) });
708
+ }
709
+ async function callTool(call, emit) {
710
+ emit({
711
+ type: "host.toolResult",
712
+ callId: call.callId,
713
+ ok: false,
714
+ error: "Host tools are read-only in this version; no action was taken."
715
+ });
716
+ }
717
+ function beginAuth(config, emit) {
718
+ const w = window.open(
719
+ `${config.origin}/embed/authorize?k=${encodeURIComponent(
720
+ config.publishableKey
721
+ )}&o=${encodeURIComponent(location.origin)}`,
722
+ "mf_auth",
723
+ "width=460,height=640"
724
+ );
725
+ if (!w) return;
726
+ const onMsg = (event) => {
727
+ if (event.origin !== config.origin) return;
728
+ const d = event.data;
729
+ if (d?.type !== "mf:embed:auth") return;
730
+ window.removeEventListener("message", onMsg);
731
+ emit({ type: "host.auth", token: d.token, expiresAt: d.expiresAt });
732
+ };
733
+ window.addEventListener("message", onMsg);
734
+ }
735
+ function start(emit, origin) {
736
+ send = emit;
737
+ widgetOrigin = origin || "";
738
+ lastUrl = location.pathname;
739
+ publishContext();
740
+ watchNavigation();
741
+ document.addEventListener("click", onClick, { capture: true, passive: true });
742
+ document.addEventListener("submit", onSubmit, {
743
+ capture: true,
744
+ passive: true
745
+ });
746
+ document.addEventListener("change", onChange, {
747
+ capture: true,
748
+ passive: true
749
+ });
750
+ document.addEventListener("selectionchange", scheduleFocus, {
751
+ passive: true
752
+ });
753
+ document.addEventListener("focusin", scheduleFocus, {
754
+ capture: true,
755
+ passive: true
756
+ });
757
+ window.addEventListener("scroll", scheduleFocus, {
758
+ capture: true,
759
+ passive: true
760
+ });
761
+ const theme = matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
762
+ emit({ type: "host.theme", mode: theme });
763
+ }
764
+ var widgetOrigin, MAX_ACTIVITY, send, activitySeq, activity, lastUrl, PII, FORM_CONTROLS, ACTIONABLE, lastFocus, focusTimer, snapshotSeq;
765
+ var init_context = __esm({
766
+ "src/context.ts"() {
767
+ "use strict";
768
+ widgetOrigin = "";
769
+ MAX_ACTIVITY = 40;
770
+ send = null;
771
+ activitySeq = 0;
772
+ activity = [];
773
+ lastUrl = "";
774
+ PII = [
775
+ /\b[\w.+-]+@[\w-]+\.[\w.]{2,}\b/g,
776
+ // email
777
+ /\b(?:\d[ -]?){13,19}\b/g,
778
+ // card-ish
779
+ /\b\d{3}[- ]?\d{2}[- ]?\d{4}\b/g,
780
+ // ssn — dashed, spaced, OR bare 9 digits
781
+ /\b\d{9,}\b/g,
782
+ // long bare digit runs: account / MRN / routing numbers
783
+ /(?:\+?\d{1,2}[\s.-]?)?\(?\d{3}\)?[\s.-]\d{3}[\s.-]\d{4}\b/g,
784
+ // us phone
785
+ /\b(?:sk|pk|rk|ak)_(?:live|test)_[A-Za-z0-9]{8,}\b/g,
786
+ // stripe-style api keys
787
+ /\beyJ[\w-]+\.[\w-]+\.[\w-]+\b/g
788
+ // jwt
789
+ ];
790
+ FORM_CONTROLS = /* @__PURE__ */ new Set(["INPUT", "SELECT", "TEXTAREA"]);
791
+ ACTIONABLE = 'a[href],button,input,select,textarea,summary,[role="button"],[role="link"],[role="menuitem"],[role="tab"],[role="option"],[role="checkbox"],[role="switch"],[role="radio"],[onclick],[tabindex]';
792
+ lastFocus = {};
793
+ focusTimer = null;
794
+ snapshotSeq = 0;
795
+ }
796
+ });
797
+
798
+ // src/react.tsx
799
+ var react_exports = {};
800
+ __export(react_exports, {
801
+ MatterfactAgent: () => MatterfactAgent
802
+ });
803
+ module.exports = __toCommonJS(react_exports);
804
+ var import_react = require("react");
805
+
806
+ // src/protocol.ts
807
+ var PROTOCOL_VERSION = 1;
808
+ var CHANNEL = "mf-embed";
809
+ function envelope(payload, id) {
810
+ return {
811
+ channel: CHANNEL,
812
+ protocol: PROTOCOL_VERSION,
813
+ ...id ? { id } : {},
814
+ payload
815
+ };
816
+ }
817
+ function isEnvelope(data) {
818
+ return typeof data === "object" && data !== null && data.channel === CHANNEL;
819
+ }
820
+
821
+ // src/loader.ts
822
+ function devRequested() {
823
+ try {
824
+ return new URLSearchParams(location.search).get("mfdev") === "1";
825
+ } catch {
826
+ return false;
827
+ }
828
+ }
829
+ var POS_KEY = "mf.embed.pos";
830
+ var EmbedHost = class {
831
+ constructor(config) {
832
+ this.config = config;
833
+ this.iframe = null;
834
+ this.shadow = null;
835
+ /** Buffered until the widget says it's listening — postMessage before load is dropped silently. */
836
+ this.queue = [];
837
+ this.ready = false;
838
+ this.open = false;
839
+ this.expanded = false;
840
+ /** Null until the user drags; then it pins the corner offset and survives reloads. */
841
+ this.pos = null;
842
+ /** Where the launcher sat when the current drag began; deltas are applied to this. */
843
+ this.dragBase = null;
844
+ /** Loaded on first open. Holds everything that touches the customer's DOM. */
845
+ this.context = null;
846
+ /** The host element; kept so `destroy()` can remove it (React lifecycle). */
847
+ this.hostEl = null;
848
+ /**
849
+ * Every message is checked twice, on every single message — not once at setup.
850
+ *
851
+ * `channel` is a namespace, not a boundary. The origin and source checks are the
852
+ * boundary: any frame on the page can postMessage us, and a page with an ad iframe
853
+ * on it has plenty of frames.
854
+ */
855
+ this.onMessage = (event) => {
856
+ if (event.origin !== this.config.origin) return;
857
+ if (event.source !== this.iframe?.contentWindow) return;
858
+ if (!isEnvelope(event.data)) return;
859
+ const msg = event.data.payload;
860
+ switch (msg.type) {
861
+ case "widget.ready":
862
+ this.ready = true;
863
+ this.send({
864
+ type: "host.ready",
865
+ protocol: PROTOCOL_VERSION,
866
+ origin: location.origin
867
+ });
868
+ this.flush();
869
+ if (this.inline) void this.loadContext();
870
+ break;
871
+ case "widget.setOpen":
872
+ if (this.inline) break;
873
+ this.open = msg.open;
874
+ this.iframe?.setAttribute("data-open", String(msg.open));
875
+ if (!msg.open) {
876
+ this.expanded = false;
877
+ this.iframe?.setAttribute("data-expanded", "false");
878
+ }
879
+ if (msg.open) void this.loadContext();
880
+ break;
881
+ case "widget.setExpanded":
882
+ if (this.inline) break;
883
+ this.expanded = msg.expanded;
884
+ this.iframe?.setAttribute("data-expanded", String(msg.expanded));
885
+ if (this.iframe) this.iframe.style.height = "";
886
+ break;
887
+ case "widget.resize":
888
+ if (this.inline) break;
889
+ if (this.iframe && this.open && !this.expanded) {
890
+ this.iframe.style.height = `${Math.min(msg.height, window.innerHeight - 40)}px`;
891
+ }
892
+ break;
893
+ case "widget.requestSnapshot":
894
+ void this.loadContext().then((m) => m.sendSnapshot(this.send));
895
+ break;
896
+ case "widget.readRegion":
897
+ void this.loadContext().then((m) => m.sendRegion(msg.ref, this.send));
898
+ break;
899
+ case "widget.callTool":
900
+ void this.loadContext().then((m) => m.callTool(msg.call, this.send));
901
+ break;
902
+ // Everything below is LAUNCHER chrome: there is no launcher inline (the widget
903
+ // doesn't draw one), and moving or hiding the host's own panel from inside it
904
+ // would be us redecorating their app. An older cached loader could still be told
905
+ // any of these by a newer widget, so they're guarded rather than assumed absent.
906
+ case "widget.dragStart":
907
+ if (this.inline) break;
908
+ this.dragBase = this.cornerOffset();
909
+ break;
910
+ case "widget.dragMove":
911
+ if (this.inline) break;
912
+ this.dragTo(msg.dx, msg.dy);
913
+ break;
914
+ case "widget.dragEnd":
915
+ if (this.inline) break;
916
+ this.dragBase = null;
917
+ this.writePos(this.pos);
918
+ break;
919
+ case "widget.setMenu":
920
+ if (this.inline) break;
921
+ if (this.iframe && !this.open) {
922
+ this.iframe.style.height = msg.open ? `${64 + msg.height}px` : "";
923
+ }
924
+ break;
925
+ case "widget.resetPos":
926
+ if (this.inline) break;
927
+ this.resetPos();
928
+ break;
929
+ case "widget.hide":
930
+ if (this.inline) break;
931
+ if (this.hostEl) this.hostEl.style.display = "none";
932
+ break;
933
+ case "widget.needsAuth":
934
+ void this.provideAuth();
935
+ break;
936
+ }
937
+ };
938
+ this.send = (msg) => {
939
+ if (!this.ready) {
940
+ this.queue.push(msg);
941
+ return;
942
+ }
943
+ this.iframe?.contentWindow?.postMessage(envelope(msg), this.config.origin);
944
+ };
945
+ this.inline = !!config.container;
946
+ }
947
+ mount() {
948
+ const host = document.createElement("div");
949
+ this.hostEl = host;
950
+ host.id = "matterfact-embed";
951
+ if (this.inline) {
952
+ host.style.cssText = [
953
+ "all: initial",
954
+ "position: relative",
955
+ "display: block",
956
+ "width: 100%",
957
+ "height: 100%",
958
+ "contain: layout style"
959
+ ].join(";");
960
+ this.config.container.appendChild(host);
961
+ } else {
962
+ this.pos = this.readPos();
963
+ host.style.cssText = [
964
+ "all: initial",
965
+ "position: fixed",
966
+ `right: ${this.pos ? this.pos.right : 20}px`,
967
+ `bottom: ${this.pos ? this.pos.bottom : 20}px`,
968
+ // Below the max so a host that genuinely needs to cover us (a modal, a cookie
969
+ // banner they are legally obliged to show) still can.
970
+ "z-index: 2147483000",
971
+ "contain: layout style"
972
+ ].join(";");
973
+ document.body.appendChild(host);
974
+ }
975
+ this.shadow = host.attachShadow({ mode: "closed" });
976
+ const style = document.createElement("style");
977
+ style.textContent = ":host{all:initial}iframe{border:0;display:block;color-scheme:light dark;}" + (this.inline ? "iframe{width:100%;height:100%}" : 'iframe{border-radius:12px;box-shadow:0 8px 40px rgba(0,0,0,.16);width:400px;height:64px;transition:height .18s ease,width .18s ease}iframe[data-open="true"]{width:420px;height:640px}iframe[data-open="true"][data-expanded="true"]{width:min(600px,calc(100vw - 40px));height:calc(100vh - 40px)}');
978
+ this.shadow.appendChild(style);
979
+ const iframe = document.createElement("iframe");
980
+ iframe.title = "matterfact assistant";
981
+ iframe.setAttribute(
982
+ "sandbox",
983
+ "allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox"
984
+ );
985
+ iframe.setAttribute("allow", "microphone");
986
+ iframe.src = `${this.config.origin}/embed/chat?k=${encodeURIComponent(
987
+ this.config.publishableKey
988
+ )}&o=${encodeURIComponent(location.origin)}` + (this.config.surface ? `&s=${encodeURIComponent(this.config.surface)}` : "") + (devRequested() ? "&dev=1" : "") + (this.inline ? "&inline=1" : "");
989
+ this.iframe = iframe;
990
+ this.shadow.appendChild(iframe);
991
+ window.addEventListener("message", this.onMessage);
992
+ }
993
+ /**
994
+ * Own a drag for its lifetime.
995
+ *
996
+ * The widget reports the press and then goes quiet: a cross-origin iframe only gets
997
+ * pointer events while the pointer is over it, and a drag leaves that box immediately.
998
+ * So we lay a transparent layer over the whole viewport and track the gesture in the
999
+ * host document, where it can't be lost. The layer also stops the pointer landing on
1000
+ * the customer's own UI mid-drag (text selection, hover states, stray clicks).
1001
+ *
1002
+ * We are the dumb half on purpose — see `widget.dragMove` in the protocol for why the
1003
+ * widget has to own the gesture. All we do is take a delta and place the element.
1004
+ */
1005
+ cornerOffset() {
1006
+ const rect = this.hostEl.getBoundingClientRect();
1007
+ return {
1008
+ right: window.innerWidth - rect.right,
1009
+ bottom: window.innerHeight - rect.bottom
1010
+ };
1011
+ }
1012
+ dragTo(dx, dy) {
1013
+ if (!this.dragBase || !this.hostEl) return;
1014
+ const rect = this.hostEl.getBoundingClientRect();
1015
+ const maxRight = Math.max(0, window.innerWidth - rect.width);
1016
+ const maxBottom = Math.max(0, window.innerHeight - rect.height);
1017
+ const right = Math.min(Math.max(0, this.dragBase.right - dx), maxRight);
1018
+ const bottom = Math.min(Math.max(0, this.dragBase.bottom - dy), maxBottom);
1019
+ this.pos = { right, bottom };
1020
+ this.hostEl.style.right = `${right}px`;
1021
+ this.hostEl.style.bottom = `${bottom}px`;
1022
+ }
1023
+ /** Reset to the default corner (a menu action — the drag is otherwise sticky). */
1024
+ resetPos() {
1025
+ this.pos = null;
1026
+ this.writePos(null);
1027
+ if (this.hostEl) {
1028
+ this.hostEl.style.right = "20px";
1029
+ this.hostEl.style.bottom = "20px";
1030
+ }
1031
+ }
1032
+ readPos() {
1033
+ try {
1034
+ const raw = localStorage.getItem(POS_KEY);
1035
+ if (!raw) return null;
1036
+ const p = JSON.parse(raw);
1037
+ return typeof p?.right === "number" && typeof p?.bottom === "number" ? { right: p.right, bottom: p.bottom } : null;
1038
+ } catch {
1039
+ return null;
1040
+ }
1041
+ }
1042
+ writePos(pos) {
1043
+ try {
1044
+ if (pos) localStorage.setItem(POS_KEY, JSON.stringify(pos));
1045
+ else localStorage.removeItem(POS_KEY);
1046
+ } catch {
1047
+ }
1048
+ }
1049
+ /**
1050
+ * Answer `widget.needsAuth`. If a trusted first-party host has an auth-token provider,
1051
+ * call it and hand the token straight to the widget via `host.auth` — no popup. The
1052
+ * provider comes from EITHER the programmatic config (the React `<MatterfactAgent
1053
+ * getAuthToken>` prop) OR a global the host page sets for the `<script>` loader:
1054
+ *
1055
+ * window.matterfact = { getEmbedAuthToken: () => getIdToken(user) };
1056
+ *
1057
+ * Read fresh at call time (not at readConfig), so a global set after the loader booted
1058
+ * — e.g. once the host's auth is ready — is still picked up. Otherwise fall back to the
1059
+ * hosted-login popup. `expiresAt: 0`: the widget doesn't cache it, it exchanges the
1060
+ * token for a rotating embed session anyway.
1061
+ */
1062
+ async provideAuth() {
1063
+ const provider = this.config.authTokenProvider ?? globalThis.matterfact?.getEmbedAuthToken;
1064
+ if (provider) {
1065
+ try {
1066
+ const token = await provider();
1067
+ if (token) {
1068
+ this.send({ type: "host.auth", token, expiresAt: 0 });
1069
+ return;
1070
+ }
1071
+ } catch {
1072
+ }
1073
+ }
1074
+ void this.loadContext().then((m) => m.beginAuth(this.config, this.send));
1075
+ }
1076
+ loadContext() {
1077
+ this.context ?? (this.context = Promise.resolve().then(() => (init_context(), context_exports)).then((m) => {
1078
+ m.start(this.send, this.config.origin);
1079
+ return m;
1080
+ }));
1081
+ return this.context;
1082
+ }
1083
+ flush() {
1084
+ const pending = this.queue;
1085
+ this.queue = [];
1086
+ for (const m of pending) this.send(m);
1087
+ }
1088
+ /** Tear down: stop listening and remove the host element. For the React wrapper's
1089
+ * unmount — the vanilla `<script>` loader lives for the page's lifetime and never
1090
+ * calls this. */
1091
+ destroy() {
1092
+ window.removeEventListener("message", this.onMessage);
1093
+ this.hostEl?.remove();
1094
+ this.hostEl = null;
1095
+ this.iframe = null;
1096
+ this.shadow = null;
1097
+ this.ready = false;
1098
+ }
1099
+ };
1100
+ function mount(config) {
1101
+ const host = new EmbedHost(config);
1102
+ host.mount();
1103
+ return host;
1104
+ }
1105
+
1106
+ // src/react.tsx
1107
+ var import_jsx_runtime = require("react/jsx-runtime");
1108
+ var DEFAULT_ORIGIN = "https://app.matterfact.com";
1109
+ function MatterfactAgent({
1110
+ publishableKey,
1111
+ widgetOrigin: widgetOrigin2,
1112
+ surface = "",
1113
+ theme = "auto",
1114
+ getAuthToken,
1115
+ inline = false,
1116
+ className,
1117
+ style
1118
+ }) {
1119
+ const authRef = (0, import_react.useRef)(getAuthToken);
1120
+ authRef.current = getAuthToken;
1121
+ const slot = (0, import_react.useRef)(null);
1122
+ (0, import_react.useEffect)(() => {
1123
+ if (typeof window === "undefined") return;
1124
+ if (inline && !slot.current) return;
1125
+ const config = {
1126
+ publishableKey,
1127
+ origin: widgetOrigin2 || DEFAULT_ORIGIN,
1128
+ theme,
1129
+ surface,
1130
+ // Always present; a null return (no getAuthToken supplied) makes the core fall
1131
+ // back to the popup/inline sign-in, so this is safe either way.
1132
+ authTokenProvider: async () => await authRef.current?.() ?? null,
1133
+ container: inline ? slot.current : null
1134
+ };
1135
+ let host = null;
1136
+ try {
1137
+ host = mount(config);
1138
+ } catch (e) {
1139
+ console.error("[matterfact] failed to mount the embed widget", e);
1140
+ }
1141
+ return () => host?.destroy();
1142
+ }, [publishableKey, widgetOrigin2, surface, theme, inline]);
1143
+ if (!inline) return null;
1144
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1145
+ "div",
1146
+ {
1147
+ ref: slot,
1148
+ className,
1149
+ style: { width: "100%", height: "100%", ...style }
1150
+ }
1151
+ );
1152
+ }
1153
+ // Annotate the CommonJS export names for ESM import in node:
1154
+ 0 && (module.exports = {
1155
+ MatterfactAgent
1156
+ });
1157
+ //# sourceMappingURL=react.cjs.map