@thinkingos/vsl-sdk 0.4.4 → 1.1.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/index.mjs CHANGED
@@ -1,3 +1,594 @@
1
+ // src/utils/idGenerator.ts
2
+ var TYPE_ABBREVIATIONS = {
3
+ button: "btn",
4
+ input: "inp",
5
+ link: "link",
6
+ container: "cont",
7
+ image: "img",
8
+ select: "sel",
9
+ textarea: "txt",
10
+ heading: "h",
11
+ text: "txt",
12
+ file_input: "file",
13
+ modal: "modal",
14
+ tab: "tab",
15
+ dropdown_toggle: "dropdown",
16
+ footer: "footer",
17
+ scrollable_container: "scroll",
18
+ toolbar: "toolbar",
19
+ list: "list",
20
+ grid: "grid",
21
+ form_field: "form",
22
+ tab_bar: "tabs",
23
+ layout: "layout",
24
+ nav: "nav",
25
+ header: "header",
26
+ main: "main",
27
+ icon: "icon",
28
+ chart: "chart",
29
+ custom_widget: "widget",
30
+ unknown: "el"
31
+ };
32
+ var IdGenerator = class {
33
+ typeCounters = /* @__PURE__ */ new Map();
34
+ /**
35
+ * Generate ID for an element.
36
+ *
37
+ * @param elementId - Existing DOM id attribute (if present)
38
+ * @param vslType - VSL type of the element (for fallback abbreviation)
39
+ * @returns Generated ID string
40
+ */
41
+ generate(elementId2, vslType) {
42
+ if (elementId2 && elementId2.trim().length > 0) {
43
+ return elementId2;
44
+ }
45
+ const shortType = vslType ? TYPE_ABBREVIATIONS[vslType] || "el" : "el";
46
+ const count = (this.typeCounters.get(shortType) || 0) + 1;
47
+ this.typeCounters.set(shortType, count);
48
+ return `${shortType}_${count}`;
49
+ }
50
+ /** Reset counters (for testing or new snapshot). */
51
+ reset() {
52
+ this.typeCounters.clear();
53
+ }
54
+ /** Get current counter value for a type (for debugging). */
55
+ getCounter(type) {
56
+ if (type) {
57
+ return this.typeCounters.get(type) || 0;
58
+ }
59
+ let total = 0;
60
+ for (const count of this.typeCounters.values()) {
61
+ total += count;
62
+ }
63
+ return total;
64
+ }
65
+ };
66
+ function createIdGenerator() {
67
+ return new IdGenerator();
68
+ }
69
+
70
+ // src/security/promptInjectionFilter.ts
71
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2, mkdirSync as mkdirSync2, appendFileSync } from "fs";
72
+ import { join as join2 } from "path";
73
+ import { homedir as homedir2 } from "os";
74
+
75
+ // src/security/remotePatternsLoader.ts
76
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
77
+ import { join, dirname } from "path";
78
+ import { homedir } from "os";
79
+ var DEFAULT_REMOTE_URL = "https://raw.githubusercontent.com/MaxZhadobin/visual-scene-language/vsl_mcp/patterns/latest.json";
80
+ var DEFAULT_CACHE_PATH = join(homedir(), ".vsl", "cache", "remote-patterns.json");
81
+ var DEFAULT_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
82
+ var DEFAULT_TIMEOUT_MS = 5e3;
83
+ var RemotePatternsLoader = class {
84
+ remoteUrl;
85
+ cachePath;
86
+ cacheTtlMs;
87
+ enabled;
88
+ timeoutMs;
89
+ constructor(options = {}) {
90
+ this.remoteUrl = options.remoteUrl ?? DEFAULT_REMOTE_URL;
91
+ this.cachePath = options.cachePath ?? DEFAULT_CACHE_PATH;
92
+ this.cacheTtlMs = options.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS;
93
+ this.enabled = options.enabled ?? true;
94
+ this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
95
+ }
96
+ /**
97
+ * Загружает remote patterns с fallback на кэш.
98
+ *
99
+ * Логика:
100
+ * 1. Если enabled === false — возвращаем empty
101
+ * 2. Проверяем кэш — если валиден (TTL не истёк), используем кэш
102
+ * 3. Пытаемся загрузить с remote URL
103
+ * 4. Если загрузка успешна — обновляем кэш, возвращаем remote patterns
104
+ * 5. Если загрузка не удалась — используем кэш (fallback)
105
+ * 6. Если кэша нет — возвращаем empty
106
+ */
107
+ async load() {
108
+ if (!this.enabled) {
109
+ return { patterns: [], source: "none", fallbackUsed: false };
110
+ }
111
+ const cachedData = this.readCache();
112
+ const cacheIsValid = this.isCacheValid(cachedData);
113
+ if (cacheIsValid) {
114
+ return {
115
+ patterns: cachedData.patterns,
116
+ source: "cache",
117
+ fallbackUsed: false
118
+ };
119
+ }
120
+ try {
121
+ const patterns = await this.fetchRemotePatterns();
122
+ this.writeCache(patterns);
123
+ return {
124
+ patterns,
125
+ source: "remote",
126
+ fallbackUsed: false
127
+ };
128
+ } catch (error) {
129
+ const errorMessage = error instanceof Error ? error.message : String(error);
130
+ if (cachedData) {
131
+ return {
132
+ patterns: cachedData.patterns,
133
+ source: "cache",
134
+ fallbackUsed: true,
135
+ error: errorMessage
136
+ };
137
+ }
138
+ return {
139
+ patterns: [],
140
+ source: "none",
141
+ fallbackUsed: true,
142
+ error: errorMessage
143
+ };
144
+ }
145
+ }
146
+ /**
147
+ * Загружает паттерны с remote URL.
148
+ */
149
+ async fetchRemotePatterns() {
150
+ const controller = new AbortController();
151
+ const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);
152
+ try {
153
+ const response = await fetch(this.remoteUrl, {
154
+ signal: controller.signal,
155
+ headers: {
156
+ "Accept": "application/json",
157
+ "User-Agent": "VSL-Patterns-Loader/1.0"
158
+ }
159
+ });
160
+ if (!response.ok) {
161
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
162
+ }
163
+ const data = await response.json();
164
+ let patternsArray;
165
+ if (Array.isArray(data)) {
166
+ patternsArray = data;
167
+ } else if (typeof data === "object" && data !== null && Array.isArray(data.patterns)) {
168
+ patternsArray = data.patterns;
169
+ } else {
170
+ throw new Error("Invalid patterns format: expected array or { patterns: [] }");
171
+ }
172
+ const patterns = [];
173
+ for (const item of patternsArray) {
174
+ if (this.isValidPattern(item)) {
175
+ patterns.push(item);
176
+ }
177
+ }
178
+ return patterns;
179
+ } finally {
180
+ clearTimeout(timeoutId);
181
+ }
182
+ }
183
+ /**
184
+ * Проверяет валидность паттерна.
185
+ */
186
+ isValidPattern(item) {
187
+ if (typeof item !== "object" || item === null) return false;
188
+ const obj = item;
189
+ return typeof obj.id === "string" && typeof obj.pattern === "string" && typeof obj.type === "string" && (obj.type === "regex" || obj.type === "ml") && typeof obj.severity === "string" && ["low", "medium", "high", "critical"].includes(obj.severity) && typeof obj.action === "string" && ["strip", "log", "block"].includes(obj.action) && typeof obj.description === "string";
190
+ }
191
+ /**
192
+ * Читает кэш из файла.
193
+ */
194
+ readCache() {
195
+ if (!existsSync(this.cachePath)) {
196
+ return null;
197
+ }
198
+ try {
199
+ const content = readFileSync(this.cachePath, "utf-8");
200
+ const data = JSON.parse(content);
201
+ if (!data.metadata || !Array.isArray(data.patterns)) {
202
+ return null;
203
+ }
204
+ return data;
205
+ } catch {
206
+ return null;
207
+ }
208
+ }
209
+ /**
210
+ * Записывает паттерны в кэш.
211
+ */
212
+ writeCache(patterns) {
213
+ const cacheData = {
214
+ metadata: {
215
+ lastFetched: (/* @__PURE__ */ new Date()).toISOString(),
216
+ sourceUrl: this.remoteUrl,
217
+ patternCount: patterns.length
218
+ },
219
+ patterns
220
+ };
221
+ try {
222
+ const cacheDir = dirname(this.cachePath);
223
+ if (!existsSync(cacheDir)) {
224
+ mkdirSync(cacheDir, { recursive: true });
225
+ }
226
+ writeFileSync(this.cachePath, JSON.stringify(cacheData, null, 2), "utf-8");
227
+ } catch (error) {
228
+ console.error(`[RemotePatternsLoader] Failed to write cache: ${error}`);
229
+ }
230
+ }
231
+ /**
232
+ * Проверяет валидность кэша (TTL не истёк).
233
+ */
234
+ isCacheValid(cachedData) {
235
+ if (!cachedData) return false;
236
+ try {
237
+ const lastFetched = new Date(cachedData.metadata.lastFetched).getTime();
238
+ const now = Date.now();
239
+ return now - lastFetched < this.cacheTtlMs;
240
+ } catch {
241
+ return false;
242
+ }
243
+ }
244
+ /**
245
+ * Очищает кэш (для тестирования).
246
+ */
247
+ clearCache() {
248
+ try {
249
+ if (existsSync(this.cachePath)) {
250
+ writeFileSync(this.cachePath, "", "utf-8");
251
+ }
252
+ } catch (error) {
253
+ console.error(`[RemotePatternsLoader] Failed to clear cache: ${error}`);
254
+ }
255
+ }
256
+ /**
257
+ * Получает информацию о кэше (для диагностики).
258
+ */
259
+ getCacheInfo() {
260
+ const cachedData = this.readCache();
261
+ if (!cachedData) {
262
+ return { exists: false };
263
+ }
264
+ return {
265
+ exists: true,
266
+ lastFetched: cachedData.metadata.lastFetched,
267
+ patternCount: cachedData.metadata.patternCount
268
+ };
269
+ }
270
+ /**
271
+ * Получает URL для загрузки remote patterns.
272
+ */
273
+ getRemoteUrl() {
274
+ return this.remoteUrl;
275
+ }
276
+ /**
277
+ * Устанавливает URL для загрузки remote patterns.
278
+ */
279
+ setRemoteUrl(url) {
280
+ this.remoteUrl = url;
281
+ }
282
+ };
283
+
284
+ // src/security/promptInjectionFilter.ts
285
+ var DEFAULT_LOG_DIR = join2(homedir2(), ".vsl", "logs");
286
+ var DEFAULT_LOG_PATH = join2(DEFAULT_LOG_DIR, "security-audit.log");
287
+ var DEFAULT_BUNDLED_PATTERNS_PATH = join2(__dirname, "..", "..", "patterns", "patterns_v1.json");
288
+ var DEFAULT_REMOTE_PATTERNS_PATH = join2(homedir2(), ".vsl", "cache", "remote-patterns.json");
289
+ var DEFAULT_CONFIDENCE_THRESHOLD = 0.7;
290
+ var DEFAULT_MAX_LOG_ENTRIES = 1e4;
291
+ var PromptInjectionFilter = class {
292
+ enabled;
293
+ confidenceThreshold;
294
+ domainWhitelist;
295
+ patterns = [];
296
+ logPath;
297
+ maxLogEntries;
298
+ logEntries = [];
299
+ remoteLoader = null;
300
+ constructor(options = {}) {
301
+ this.enabled = options.enabled ?? true;
302
+ this.confidenceThreshold = options.confidenceThreshold ?? DEFAULT_CONFIDENCE_THRESHOLD;
303
+ this.domainWhitelist = new Set(options.domainWhitelist ?? []);
304
+ this.logPath = options.logPath ?? DEFAULT_LOG_PATH;
305
+ this.maxLogEntries = options.maxLogEntries ?? DEFAULT_MAX_LOG_ENTRIES;
306
+ if (options.remoteLoader?.enabled !== false) {
307
+ this.remoteLoader = new RemotePatternsLoader({
308
+ remoteUrl: options.remoteLoader?.remoteUrl,
309
+ cachePath: options.remoteLoader?.cachePath ?? options.remotePatternsPath,
310
+ cacheTtlMs: options.remoteLoader?.cacheTtlMs,
311
+ enabled: options.remoteLoader?.enabled ?? true,
312
+ timeoutMs: options.remoteLoader?.timeoutMs
313
+ });
314
+ }
315
+ this.loadPatterns(options);
316
+ this.loadExistingLogs();
317
+ }
318
+ /**
319
+ * Загружает паттерны из всех источников (bundled, remote, custom).
320
+ */
321
+ loadPatterns(options) {
322
+ const allPatterns = [];
323
+ const bundledPath = options.bundledPatternsPath ?? DEFAULT_BUNDLED_PATTERNS_PATH;
324
+ if (existsSync2(bundledPath)) {
325
+ try {
326
+ const content = readFileSync2(bundledPath, "utf-8");
327
+ const bundled = JSON.parse(content);
328
+ allPatterns.push(...bundled);
329
+ } catch (error) {
330
+ console.error(`[PromptInjectionFilter] Failed to load bundled patterns: ${error}`);
331
+ }
332
+ }
333
+ const remotePath = options.remotePatternsPath ?? DEFAULT_REMOTE_PATTERNS_PATH;
334
+ if (existsSync2(remotePath)) {
335
+ try {
336
+ const content = readFileSync2(remotePath, "utf-8");
337
+ const remote = JSON.parse(content);
338
+ allPatterns.push(...remote);
339
+ } catch (error) {
340
+ console.error(`[PromptInjectionFilter] Failed to load remote patterns: ${error}`);
341
+ }
342
+ }
343
+ if (options.customPatterns && options.customPatterns.length > 0) {
344
+ allPatterns.push(...options.customPatterns);
345
+ }
346
+ this.patterns = allPatterns.filter((p) => p.type === "regex").map((p) => ({
347
+ ...p,
348
+ regex: new RegExp(p.pattern, "gi"),
349
+ // case-insensitive, global
350
+ confidenceThreshold: p.confidence ?? this.confidenceThreshold
351
+ }));
352
+ }
353
+ /**
354
+ * Асинхронно загружает remote patterns через RemotePatternsLoader.
355
+ * Вызывается опционально для обновления паттернов с CDN/GitHub.
356
+ *
357
+ * @returns Результат загрузки remote patterns
358
+ */
359
+ async loadRemotePatterns() {
360
+ if (!this.remoteLoader) {
361
+ return { patterns: 0, source: "none", fallbackUsed: false };
362
+ }
363
+ const result = await this.remoteLoader.load();
364
+ if (result.patterns.length > 0) {
365
+ const remoteCompiled = result.patterns.filter((p) => p.type === "regex").map((p) => ({
366
+ ...p,
367
+ regex: new RegExp(p.pattern, "gi"),
368
+ confidenceThreshold: p.confidence ?? this.confidenceThreshold
369
+ }));
370
+ this.patterns.push(...remoteCompiled);
371
+ }
372
+ return {
373
+ patterns: result.patterns.length,
374
+ source: result.source,
375
+ fallbackUsed: result.fallbackUsed,
376
+ error: result.error
377
+ };
378
+ }
379
+ /**
380
+ * Получает информацию о кэше remote patterns.
381
+ */
382
+ getRemoteCacheInfo() {
383
+ if (!this.remoteLoader) {
384
+ return { exists: false };
385
+ }
386
+ return this.remoteLoader.getCacheInfo();
387
+ }
388
+ /**
389
+ * Очищает кэш remote patterns.
390
+ */
391
+ clearRemoteCache() {
392
+ if (this.remoteLoader) {
393
+ this.remoteLoader.clearCache();
394
+ }
395
+ }
396
+ /**
397
+ * Загружает существующие логи из файла.
398
+ */
399
+ loadExistingLogs() {
400
+ if (!existsSync2(this.logPath)) {
401
+ return;
402
+ }
403
+ try {
404
+ const content = readFileSync2(this.logPath, "utf-8");
405
+ const lines = content.trim().split("\n").filter((line) => line.length > 0);
406
+ this.logEntries = lines.map((line) => JSON.parse(line));
407
+ } catch (error) {
408
+ console.error(`[PromptInjectionFilter] Failed to load existing logs: ${error}`);
409
+ this.logEntries = [];
410
+ }
411
+ }
412
+ /**
413
+ * Проверяет, находится ли URL в whitelist.
414
+ */
415
+ isWhitelisted(url) {
416
+ if (!url) return false;
417
+ try {
418
+ const hostname = new URL(url).hostname;
419
+ return this.domainWhitelist.has(hostname);
420
+ } catch {
421
+ return false;
422
+ }
423
+ }
424
+ /**
425
+ * Сканирует текст на наличие инъекций.
426
+ *
427
+ * @param text — текст для сканирования
428
+ * @param url — URL страницы (для whitelist и логирования)
429
+ * @returns Результат сканирования с очищенным текстом и списком обнаружений
430
+ */
431
+ scan(text, url) {
432
+ const startTime = performance.now();
433
+ if (!this.enabled || this.isWhitelisted(url)) {
434
+ return {
435
+ cleanText: text,
436
+ detections: [],
437
+ blocked: false,
438
+ scanTimeMs: performance.now() - startTime
439
+ };
440
+ }
441
+ const detections = [];
442
+ let cleanText = text;
443
+ let blocked = false;
444
+ for (const pattern of this.patterns) {
445
+ pattern.regex.lastIndex = 0;
446
+ let match;
447
+ while ((match = pattern.regex.exec(text)) !== null) {
448
+ const matchedText = match[0];
449
+ const index = match.index;
450
+ const contextStart = Math.max(0, index - 50);
451
+ const contextEnd = Math.min(text.length, index + matchedText.length + 50);
452
+ const context = text.slice(contextStart, contextEnd);
453
+ const detection = {
454
+ patternId: pattern.id,
455
+ description: pattern.description,
456
+ severity: pattern.severity,
457
+ action: pattern.action,
458
+ matchedText,
459
+ index,
460
+ context
461
+ };
462
+ detections.push(detection);
463
+ this.logDetection(detection, url);
464
+ if (pattern.action === "block") {
465
+ blocked = true;
466
+ cleanText = "";
467
+ break;
468
+ } else if (pattern.action === "strip") {
469
+ cleanText = cleanText.replace(matchedText, "");
470
+ }
471
+ if (match.index === pattern.regex.lastIndex) {
472
+ pattern.regex.lastIndex++;
473
+ }
474
+ }
475
+ }
476
+ if (detections.some((d) => d.action === "strip")) {
477
+ cleanText = cleanText.replace(/\s+/g, " ").trim();
478
+ }
479
+ return {
480
+ cleanText,
481
+ detections,
482
+ blocked,
483
+ scanTimeMs: performance.now() - startTime
484
+ };
485
+ }
486
+ /**
487
+ * Логирует обнаружение в security audit trail.
488
+ */
489
+ logDetection(detection, url) {
490
+ const entry = {
491
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
492
+ url,
493
+ patternId: detection.patternId,
494
+ description: detection.description,
495
+ severity: detection.severity,
496
+ action: detection.action,
497
+ matchedText: detection.matchedText,
498
+ context: detection.context
499
+ };
500
+ this.logEntries.push(entry);
501
+ if (this.logEntries.length > this.maxLogEntries) {
502
+ this.logEntries = this.logEntries.slice(-this.maxLogEntries);
503
+ }
504
+ this.writeLogEntry(entry);
505
+ }
506
+ /**
507
+ * Записывает одну запись в лог-файл.
508
+ */
509
+ writeLogEntry(entry) {
510
+ try {
511
+ const logDir = join2(this.logPath, "..");
512
+ if (!existsSync2(logDir)) {
513
+ mkdirSync2(logDir, { recursive: true });
514
+ }
515
+ appendFileSync(this.logPath, JSON.stringify(entry) + "\n", "utf-8");
516
+ } catch (error) {
517
+ console.error(`[PromptInjectionFilter] Failed to write log entry: ${error}`);
518
+ }
519
+ }
520
+ /**
521
+ * Получает все логи (для аудита).
522
+ */
523
+ getLogs() {
524
+ return [...this.logEntries];
525
+ }
526
+ /**
527
+ * Очищает логи (для тестирования).
528
+ */
529
+ clearLogs() {
530
+ this.logEntries = [];
531
+ try {
532
+ if (existsSync2(this.logPath)) {
533
+ writeFileSync2(this.logPath, "", "utf-8");
534
+ }
535
+ } catch (error) {
536
+ console.error(`[PromptInjectionFilter] Failed to clear logs: ${error}`);
537
+ }
538
+ }
539
+ /**
540
+ * Добавляет custom pattern динамически.
541
+ */
542
+ addCustomPattern(pattern) {
543
+ if (pattern.type !== "regex") {
544
+ console.warn(`[PromptInjectionFilter] Only regex patterns are supported, skipping: ${pattern.id}`);
545
+ return;
546
+ }
547
+ const compiled = {
548
+ ...pattern,
549
+ regex: new RegExp(pattern.pattern, "gi"),
550
+ confidenceThreshold: pattern.confidence ?? this.confidenceThreshold
551
+ };
552
+ this.patterns.push(compiled);
553
+ }
554
+ /**
555
+ * Удаляет custom pattern по ID.
556
+ */
557
+ removePattern(patternId) {
558
+ const initialLength = this.patterns.length;
559
+ this.patterns = this.patterns.filter((p) => p.id !== patternId);
560
+ return this.patterns.length < initialLength;
561
+ }
562
+ /**
563
+ * Получает количество загруженных паттернов.
564
+ */
565
+ getPatternCount() {
566
+ return this.patterns.length;
567
+ }
568
+ /**
569
+ * Включает/выключает фильтр.
570
+ */
571
+ setEnabled(enabled) {
572
+ this.enabled = enabled;
573
+ }
574
+ /**
575
+ * Проверяет, включён ли фильтр.
576
+ */
577
+ isEnabled() {
578
+ return this.enabled;
579
+ }
580
+ };
581
+ function createFilter(options) {
582
+ return new PromptInjectionFilter(options);
583
+ }
584
+ var globalFilter = null;
585
+ function getGlobalFilter() {
586
+ if (!globalFilter) {
587
+ globalFilter = new PromptInjectionFilter();
588
+ }
589
+ return globalFilter;
590
+ }
591
+
1
592
  // src/capture/domExtractor.ts
2
593
  var NON_RENDERABLE_TAGS = /* @__PURE__ */ new Set([
3
594
  "script",
@@ -29,6 +620,16 @@ function ownText(el) {
29
620
  }
30
621
  return raw.replace(/\s+/g, " ").trim();
31
622
  }
623
+ function filteredOwnText(el, url) {
624
+ const raw = ownText(el);
625
+ if (!raw) return raw;
626
+ const filter = getGlobalFilter();
627
+ if (!filter.isEnabled()) {
628
+ return raw;
629
+ }
630
+ const result = filter.scan(raw, url);
631
+ return result.cleanText;
632
+ }
32
633
  function extractAttributes(el) {
33
634
  const attributes = {};
34
635
  for (const attr of Array.from(el.attributes)) {
@@ -73,7 +674,7 @@ function toRect(rect) {
73
674
  function hasZeroBox(rect) {
74
675
  return rect.width <= 0 || rect.height <= 0;
75
676
  }
76
- function collectVisibleChildren(parent, parentPath) {
677
+ function collectVisibleChildren(parent, parentPath, url) {
77
678
  const result = [];
78
679
  Array.from(parent.children).forEach((child, index) => {
79
680
  const path = [...parentPath, index];
@@ -83,17 +684,17 @@ function collectVisibleChildren(parent, parentPath) {
83
684
  }
84
685
  const rect = toRect(child.getBoundingClientRect());
85
686
  if (isVisibilityHidden(child) || hasZeroBox(rect)) {
86
- result.push(...collectVisibleChildren(child, path));
687
+ result.push(...collectVisibleChildren(child, path, url));
87
688
  return;
88
689
  }
89
690
  result.push({
90
691
  tag,
91
692
  indexPath: path,
92
693
  rect,
93
- text: ownText(child) || null,
694
+ text: filteredOwnText(child, url) || null,
94
695
  attributes: extractAttributes(child),
95
696
  css: captureCss(child),
96
- children: collectVisibleChildren(child, path)
697
+ children: collectVisibleChildren(child, path, url)
97
698
  });
98
699
  });
99
700
  return result;
@@ -105,8 +706,8 @@ function defaultRoot() {
105
706
  }
106
707
  return body;
107
708
  }
108
- function extractDomTree(root = defaultRoot()) {
109
- return collectVisibleChildren(root, []);
709
+ function extractDomTree(root = defaultRoot(), url) {
710
+ return collectVisibleChildren(root, [], url);
110
711
  }
111
712
 
112
713
  // src/segmentation/level1.ts
@@ -131,7 +732,12 @@ var ARIA_ROLE_TYPE_MAP = {
131
732
  button: "button",
132
733
  dialog: "modal",
133
734
  tab: "tab",
134
- tabpanel: "container"
735
+ tabpanel: "container",
736
+ combobox: "input",
737
+ listbox: "select",
738
+ searchbox: "input",
739
+ spinbutton: "input",
740
+ slider: "input"
135
741
  };
136
742
  function resolveAriaRoleType(role) {
137
743
  if (!role) return null;
@@ -314,7 +920,7 @@ var INTERACTIVE_ATTRS = [
314
920
  ];
315
921
  function relative(value, dimension) {
316
922
  if (dimension <= 0) return 0;
317
- return Math.round(value / dimension * 1e4) / 1e4;
923
+ return Math.round(value / dimension * 100) / 100;
318
924
  }
319
925
  function defaultActions(t, st, attributes) {
320
926
  if (st === "disabled") return void 0;
@@ -382,10 +988,10 @@ function extractVisualStyles(css) {
382
988
  }
383
989
  return hasContent ? style : void 0;
384
990
  }
385
- function toVslObject(el, viewport, includedChildren, textBlocks, counter) {
991
+ function toVslObject(el, viewport, includedChildren, textBlocks, counter, idGenerator) {
386
992
  const t = el.t ?? "container";
387
993
  const object = {
388
- id: `${el.tag}_${el.indexPath.join("_")}`,
994
+ id: idGenerator.generate(el.attributes["id"], t),
389
995
  t,
390
996
  p: [relative(el.rect.x, viewport.width), relative(el.rect.y, viewport.height)],
391
997
  s: [Math.round(el.rect.width), Math.round(el.rect.height)]
@@ -419,12 +1025,12 @@ function toVslObject(el, viewport, includedChildren, textBlocks, counter) {
419
1025
  }
420
1026
  return object;
421
1027
  }
422
- function convert(el, viewport, textBlocks, counter) {
423
- const includedChildren = el.ch.map((child) => convert(child, viewport, textBlocks, counter)).filter((obj) => obj !== null);
424
- if (el.t !== null) return toVslObject(el, viewport, includedChildren, textBlocks, counter);
1028
+ function convert(el, viewport, textBlocks, counter, idGenerator) {
1029
+ const includedChildren = el.ch.map((child) => convert(child, viewport, textBlocks, counter, idGenerator)).filter((obj) => obj !== null);
1030
+ if (el.t !== null) return toVslObject(el, viewport, includedChildren, textBlocks, counter, idGenerator);
425
1031
  const score = baseSemanticScore(el) + (includedChildren.length > 0 ? 1 : 0);
426
1032
  if (score >= 3 || includedChildren.length > 0) {
427
- return toVslObject(el, viewport, includedChildren, textBlocks, counter);
1033
+ return toVslObject(el, viewport, includedChildren, textBlocks, counter, idGenerator);
428
1034
  }
429
1035
  return null;
430
1036
  }
@@ -473,7 +1079,8 @@ function buildVslDocument(elements, options = {}) {
473
1079
  };
474
1080
  const textBlocks = /* @__PURE__ */ new Map();
475
1081
  const counter = { value: 0 };
476
- const objects = elements.map((el) => convert(el, viewport, textBlocks, counter)).filter((obj) => obj !== null);
1082
+ const idGenerator = new IdGenerator();
1083
+ const objects = elements.map((el) => convert(el, viewport, textBlocks, counter, idGenerator)).filter((obj) => obj !== null);
477
1084
  const vslDocument = {
478
1085
  vsl_version: VSL_VERSION,
479
1086
  canvas: buildCanvas(options, viewport),
@@ -1932,10 +2539,14 @@ export {
1932
2539
  ActionExecutionError,
1933
2540
  AlibabaAdapter,
1934
2541
  AnthropicAdapter,
2542
+ IdGenerator,
1935
2543
  LEVEL1_TAG_MAP,
1936
2544
  LlmError,
1937
2545
  LlmValidationError,
1938
2546
  OpenAIAdapter,
2547
+ PromptInjectionFilter,
2548
+ RemotePatternsLoader,
2549
+ TYPE_ABBREVIATIONS,
1939
2550
  VALID_ACTIONS,
1940
2551
  VSL_SDK_VERSION,
1941
2552
  VSL_VERSION,
@@ -1948,9 +2559,12 @@ export {
1948
2559
  computeContentHash,
1949
2560
  computeCoordHash,
1950
2561
  createCacheStore,
2562
+ createFilter,
2563
+ createIdGenerator,
1951
2564
  diffVslDocuments,
1952
2565
  executeAction,
1953
2566
  extractDomTree,
2567
+ getGlobalFilter,
1954
2568
  isAriaHidden,
1955
2569
  isVslDiff,
1956
2570
  ownText,