sela-core 0.1.0-alpha.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 (267) hide show
  1. package/README.md +103 -0
  2. package/bin/sela.js +3 -0
  3. package/dist/cli/ErrorHandler.d.ts +10 -0
  4. package/dist/cli/ErrorHandler.d.ts.map +1 -0
  5. package/dist/cli/ErrorHandler.js +70 -0
  6. package/dist/cli/commands/bulk.d.ts +3 -0
  7. package/dist/cli/commands/bulk.d.ts.map +1 -0
  8. package/dist/cli/commands/bulk.js +158 -0
  9. package/dist/cli/commands/find.d.ts +3 -0
  10. package/dist/cli/commands/find.d.ts.map +1 -0
  11. package/dist/cli/commands/find.js +51 -0
  12. package/dist/cli/commands/init.d.ts +3 -0
  13. package/dist/cli/commands/init.d.ts.map +1 -0
  14. package/dist/cli/commands/init.js +166 -0
  15. package/dist/cli/commands/list.d.ts +3 -0
  16. package/dist/cli/commands/list.d.ts.map +1 -0
  17. package/dist/cli/commands/list.js +56 -0
  18. package/dist/cli/commands/merge.d.ts +13 -0
  19. package/dist/cli/commands/merge.d.ts.map +1 -0
  20. package/dist/cli/commands/merge.js +119 -0
  21. package/dist/cli/commands/refactor.d.ts +3 -0
  22. package/dist/cli/commands/refactor.d.ts.map +1 -0
  23. package/dist/cli/commands/refactor.js +30 -0
  24. package/dist/cli/commands/showReport.d.ts +3 -0
  25. package/dist/cli/commands/showReport.d.ts.map +1 -0
  26. package/dist/cli/commands/showReport.js +82 -0
  27. package/dist/cli/commands/status.d.ts +3 -0
  28. package/dist/cli/commands/status.d.ts.map +1 -0
  29. package/dist/cli/commands/status.js +51 -0
  30. package/dist/cli/commands/sync.d.ts +3 -0
  31. package/dist/cli/commands/sync.d.ts.map +1 -0
  32. package/dist/cli/commands/sync.js +131 -0
  33. package/dist/cli/index.d.ts +2 -0
  34. package/dist/cli/index.d.ts.map +1 -0
  35. package/dist/cli/index.js +94 -0
  36. package/dist/cli/ui/DnaTable.d.ts +3 -0
  37. package/dist/cli/ui/DnaTable.d.ts.map +1 -0
  38. package/dist/cli/ui/DnaTable.js +77 -0
  39. package/dist/cli/ui/LocatorPicker.d.ts +8 -0
  40. package/dist/cli/ui/LocatorPicker.d.ts.map +1 -0
  41. package/dist/cli/ui/LocatorPicker.js +33 -0
  42. package/dist/cli/ui/ProgressReporter.d.ts +11 -0
  43. package/dist/cli/ui/ProgressReporter.d.ts.map +1 -0
  44. package/dist/cli/ui/ProgressReporter.js +33 -0
  45. package/dist/cli/ui/RefactorWizard.d.ts +26 -0
  46. package/dist/cli/ui/RefactorWizard.d.ts.map +1 -0
  47. package/dist/cli/ui/RefactorWizard.js +295 -0
  48. package/dist/config/ConfigLoader.d.ts +16 -0
  49. package/dist/config/ConfigLoader.d.ts.map +1 -0
  50. package/dist/config/ConfigLoader.js +185 -0
  51. package/dist/config/DryRunGuard.d.ts +14 -0
  52. package/dist/config/DryRunGuard.d.ts.map +1 -0
  53. package/dist/config/DryRunGuard.js +80 -0
  54. package/dist/config/PathScopeGuard.d.ts +42 -0
  55. package/dist/config/PathScopeGuard.d.ts.map +1 -0
  56. package/dist/config/PathScopeGuard.js +192 -0
  57. package/dist/config/SelaConfig.d.ts +178 -0
  58. package/dist/config/SelaConfig.d.ts.map +1 -0
  59. package/dist/config/SelaConfig.js +141 -0
  60. package/dist/config/defineConfig.d.ts +3 -0
  61. package/dist/config/defineConfig.d.ts.map +1 -0
  62. package/dist/config/defineConfig.js +9 -0
  63. package/dist/engine/FixwrightEngine.d.ts +24 -0
  64. package/dist/engine/FixwrightEngine.d.ts.map +1 -0
  65. package/dist/engine/FixwrightEngine.js +403 -0
  66. package/dist/engine/HealingRegistry.d.ts +40 -0
  67. package/dist/engine/HealingRegistry.d.ts.map +1 -0
  68. package/dist/engine/HealingRegistry.js +99 -0
  69. package/dist/engine/SelaEngine.d.ts +57 -0
  70. package/dist/engine/SelaEngine.d.ts.map +1 -0
  71. package/dist/engine/SelaEngine.js +1146 -0
  72. package/dist/engine/singleton.d.ts +3 -0
  73. package/dist/engine/singleton.d.ts.map +1 -0
  74. package/dist/engine/singleton.js +5 -0
  75. package/dist/errors/SelaError.d.ts +146 -0
  76. package/dist/errors/SelaError.d.ts.map +1 -0
  77. package/dist/errors/SelaError.js +168 -0
  78. package/dist/fixtures/expectProxy.d.ts +12 -0
  79. package/dist/fixtures/expectProxy.d.ts.map +1 -0
  80. package/dist/fixtures/expectProxy.js +246 -0
  81. package/dist/fixtures/index.d.ts +30 -0
  82. package/dist/fixtures/index.d.ts.map +1 -0
  83. package/dist/fixtures/index.js +765 -0
  84. package/dist/fixtures/moduleExpect.d.ts +2 -0
  85. package/dist/fixtures/moduleExpect.d.ts.map +1 -0
  86. package/dist/fixtures/moduleExpect.js +54 -0
  87. package/dist/fixtures/proxyTag.d.ts +12 -0
  88. package/dist/fixtures/proxyTag.d.ts.map +1 -0
  89. package/dist/fixtures/proxyTag.js +45 -0
  90. package/dist/index.d.ts +9 -0
  91. package/dist/index.d.ts.map +1 -0
  92. package/dist/index.js +31 -0
  93. package/dist/reporter/SelaReporter.d.ts +165 -0
  94. package/dist/reporter/SelaReporter.d.ts.map +1 -0
  95. package/dist/reporter/SelaReporter.js +517 -0
  96. package/dist/services/ASTSourceUpdater.d.ts +143 -0
  97. package/dist/services/ASTSourceUpdater.d.ts.map +1 -0
  98. package/dist/services/ASTSourceUpdater.js +3514 -0
  99. package/dist/services/AnchorResolver.d.ts +157 -0
  100. package/dist/services/AnchorResolver.d.ts.map +1 -0
  101. package/dist/services/AnchorResolver.js +289 -0
  102. package/dist/services/ArgumentTypeAnalyzer.d.ts +26 -0
  103. package/dist/services/ArgumentTypeAnalyzer.d.ts.map +1 -0
  104. package/dist/services/ArgumentTypeAnalyzer.js +92 -0
  105. package/dist/services/BlastRadiusAnalyzer.d.ts +15 -0
  106. package/dist/services/BlastRadiusAnalyzer.d.ts.map +1 -0
  107. package/dist/services/BlastRadiusAnalyzer.js +107 -0
  108. package/dist/services/ChainValidator.d.ts +76 -0
  109. package/dist/services/ChainValidator.d.ts.map +1 -0
  110. package/dist/services/ChainValidator.js +570 -0
  111. package/dist/services/CrossFileHealer.d.ts +6 -0
  112. package/dist/services/CrossFileHealer.d.ts.map +1 -0
  113. package/dist/services/CrossFileHealer.js +135 -0
  114. package/dist/services/DecisionEngine.d.ts +51 -0
  115. package/dist/services/DecisionEngine.d.ts.map +1 -0
  116. package/dist/services/DecisionEngine.js +260 -0
  117. package/dist/services/DefinitionTracer.d.ts +41 -0
  118. package/dist/services/DefinitionTracer.d.ts.map +1 -0
  119. package/dist/services/DefinitionTracer.js +366 -0
  120. package/dist/services/DnaEditorService.d.ts +31 -0
  121. package/dist/services/DnaEditorService.d.ts.map +1 -0
  122. package/dist/services/DnaEditorService.js +217 -0
  123. package/dist/services/DnaIndexService.d.ts +24 -0
  124. package/dist/services/DnaIndexService.d.ts.map +1 -0
  125. package/dist/services/DnaIndexService.js +131 -0
  126. package/dist/services/HealBudget.d.ts +32 -0
  127. package/dist/services/HealBudget.d.ts.map +1 -0
  128. package/dist/services/HealBudget.js +85 -0
  129. package/dist/services/HealReportService.d.ts +161 -0
  130. package/dist/services/HealReportService.d.ts.map +1 -0
  131. package/dist/services/HealReportService.js +1039 -0
  132. package/dist/services/HealingAdvisory.d.ts +30 -0
  133. package/dist/services/HealingAdvisory.d.ts.map +1 -0
  134. package/dist/services/HealingAdvisory.js +51 -0
  135. package/dist/services/HealingCacheService.d.ts +135 -0
  136. package/dist/services/HealingCacheService.d.ts.map +1 -0
  137. package/dist/services/HealingCacheService.js +404 -0
  138. package/dist/services/HealthReportService.d.ts +10 -0
  139. package/dist/services/HealthReportService.d.ts.map +1 -0
  140. package/dist/services/HealthReportService.js +84 -0
  141. package/dist/services/InitializerUpdater.d.ts +16 -0
  142. package/dist/services/InitializerUpdater.d.ts.map +1 -0
  143. package/dist/services/InitializerUpdater.js +73 -0
  144. package/dist/services/IntentAuditor.d.ts +56 -0
  145. package/dist/services/IntentAuditor.d.ts.map +1 -0
  146. package/dist/services/IntentAuditor.js +326 -0
  147. package/dist/services/InteractiveReview.d.ts +15 -0
  148. package/dist/services/InteractiveReview.d.ts.map +1 -0
  149. package/dist/services/InteractiveReview.js +128 -0
  150. package/dist/services/LLMService.d.ts +121 -0
  151. package/dist/services/LLMService.d.ts.map +1 -0
  152. package/dist/services/LLMService.js +475 -0
  153. package/dist/services/MutationApplier.d.ts +69 -0
  154. package/dist/services/MutationApplier.d.ts.map +1 -0
  155. package/dist/services/MutationApplier.js +360 -0
  156. package/dist/services/PRAutomationService.d.ts +50 -0
  157. package/dist/services/PRAutomationService.d.ts.map +1 -0
  158. package/dist/services/PRAutomationService.js +760 -0
  159. package/dist/services/PendingPromptLedger.d.ts +51 -0
  160. package/dist/services/PendingPromptLedger.d.ts.map +1 -0
  161. package/dist/services/PendingPromptLedger.js +205 -0
  162. package/dist/services/ReportGenerator.d.ts +156 -0
  163. package/dist/services/ReportGenerator.d.ts.map +1 -0
  164. package/dist/services/ReportGenerator.js +278 -0
  165. package/dist/services/ReportMergeService.d.ts +95 -0
  166. package/dist/services/ReportMergeService.d.ts.map +1 -0
  167. package/dist/services/ReportMergeService.js +0 -0
  168. package/dist/services/SafetyGuard.d.ts +112 -0
  169. package/dist/services/SafetyGuard.d.ts.map +1 -0
  170. package/dist/services/SafetyGuard.js +87 -0
  171. package/dist/services/Sanitizer.d.ts +28 -0
  172. package/dist/services/Sanitizer.d.ts.map +1 -0
  173. package/dist/services/Sanitizer.js +142 -0
  174. package/dist/services/SanitizingAnthropic.d.ts +19 -0
  175. package/dist/services/SanitizingAnthropic.d.ts.map +1 -0
  176. package/dist/services/SanitizingAnthropic.js +84 -0
  177. package/dist/services/SelectorSanitizer.d.ts +52 -0
  178. package/dist/services/SelectorSanitizer.d.ts.map +1 -0
  179. package/dist/services/SelectorSanitizer.js +318 -0
  180. package/dist/services/SnapshotService.d.ts +11 -0
  181. package/dist/services/SnapshotService.d.ts.map +1 -0
  182. package/dist/services/SnapshotService.js +352 -0
  183. package/dist/services/SourceLinkService.d.ts +26 -0
  184. package/dist/services/SourceLinkService.d.ts.map +1 -0
  185. package/dist/services/SourceLinkService.js +165 -0
  186. package/dist/services/SourceUpdater.d.ts +45 -0
  187. package/dist/services/SourceUpdater.d.ts.map +1 -0
  188. package/dist/services/SourceUpdater.js +1091 -0
  189. package/dist/services/TemplateDiffService.d.ts +25 -0
  190. package/dist/services/TemplateDiffService.d.ts.map +1 -0
  191. package/dist/services/TemplateDiffService.js +334 -0
  192. package/dist/services/TraceBackEngine.d.ts +67 -0
  193. package/dist/services/TraceBackEngine.d.ts.map +1 -0
  194. package/dist/services/TraceBackEngine.js +672 -0
  195. package/dist/services/WorkspaceSnapshotService.d.ts +71 -0
  196. package/dist/services/WorkspaceSnapshotService.d.ts.map +1 -0
  197. package/dist/services/WorkspaceSnapshotService.js +200 -0
  198. package/dist/services/safety/PipelineContext.d.ts +63 -0
  199. package/dist/services/safety/PipelineContext.d.ts.map +1 -0
  200. package/dist/services/safety/PipelineContext.js +34 -0
  201. package/dist/services/safety/roleClassifier.d.ts +4 -0
  202. package/dist/services/safety/roleClassifier.d.ts.map +1 -0
  203. package/dist/services/safety/roleClassifier.js +233 -0
  204. package/dist/services/safety/rules/ConfidenceTierRule.d.ts +3 -0
  205. package/dist/services/safety/rules/ConfidenceTierRule.d.ts.map +1 -0
  206. package/dist/services/safety/rules/ConfidenceTierRule.js +32 -0
  207. package/dist/services/safety/rules/DangerousSinkRule.d.ts +10 -0
  208. package/dist/services/safety/rules/DangerousSinkRule.d.ts.map +1 -0
  209. package/dist/services/safety/rules/DangerousSinkRule.js +53 -0
  210. package/dist/services/safety/rules/FunctionalRoleRule.d.ts +3 -0
  211. package/dist/services/safety/rules/FunctionalRoleRule.d.ts.map +1 -0
  212. package/dist/services/safety/rules/FunctionalRoleRule.js +64 -0
  213. package/dist/services/safety/rules/HardConfidenceRule.d.ts +3 -0
  214. package/dist/services/safety/rules/HardConfidenceRule.d.ts.map +1 -0
  215. package/dist/services/safety/rules/HardConfidenceRule.js +22 -0
  216. package/dist/services/safety/rules/IntentAuditorRule.d.ts +3 -0
  217. package/dist/services/safety/rules/IntentAuditorRule.d.ts.map +1 -0
  218. package/dist/services/safety/rules/IntentAuditorRule.js +97 -0
  219. package/dist/services/safety/rules/SemanticOppositeRule.d.ts +3 -0
  220. package/dist/services/safety/rules/SemanticOppositeRule.d.ts.map +1 -0
  221. package/dist/services/safety/rules/SemanticOppositeRule.js +30 -0
  222. package/dist/services/safety/rules/StructuralOnlyRule.d.ts +3 -0
  223. package/dist/services/safety/rules/StructuralOnlyRule.d.ts.map +1 -0
  224. package/dist/services/safety/rules/StructuralOnlyRule.js +38 -0
  225. package/dist/services/safety/rules/VisibilityRule.d.ts +3 -0
  226. package/dist/services/safety/rules/VisibilityRule.d.ts.map +1 -0
  227. package/dist/services/safety/rules/VisibilityRule.js +53 -0
  228. package/dist/services/safety/rules/ZeroTrustRule.d.ts +3 -0
  229. package/dist/services/safety/rules/ZeroTrustRule.d.ts.map +1 -0
  230. package/dist/services/safety/rules/ZeroTrustRule.js +37 -0
  231. package/dist/services/safety/rules/index.d.ts +3 -0
  232. package/dist/services/safety/rules/index.d.ts.map +1 -0
  233. package/dist/services/safety/rules/index.js +34 -0
  234. package/dist/services/types.d.ts +59 -0
  235. package/dist/services/types.d.ts.map +1 -0
  236. package/dist/services/types.js +2 -0
  237. package/dist/storage/SnapshotManager.d.ts +5 -0
  238. package/dist/storage/SnapshotManager.d.ts.map +1 -0
  239. package/dist/storage/SnapshotManager.js +32 -0
  240. package/dist/types/index.d.ts +95 -0
  241. package/dist/types/index.d.ts.map +1 -0
  242. package/dist/types/index.js +7 -0
  243. package/dist/utils/DOMUtils.d.ts +49 -0
  244. package/dist/utils/DOMUtils.d.ts.map +1 -0
  245. package/dist/utils/DOMUtils.js +466 -0
  246. package/dist/utils/FileLock.d.ts +44 -0
  247. package/dist/utils/FileLock.d.ts.map +1 -0
  248. package/dist/utils/FileLock.js +189 -0
  249. package/dist/utils/IsolatedDiff.d.ts +45 -0
  250. package/dist/utils/IsolatedDiff.d.ts.map +1 -0
  251. package/dist/utils/IsolatedDiff.js +382 -0
  252. package/dist/utils/StackUtils.d.ts +11 -0
  253. package/dist/utils/StackUtils.d.ts.map +1 -0
  254. package/dist/utils/StackUtils.js +120 -0
  255. package/dist/utils/SummaryTable.d.ts +17 -0
  256. package/dist/utils/SummaryTable.d.ts.map +1 -0
  257. package/dist/utils/SummaryTable.js +37 -0
  258. package/dist/utils/atomicWrite.d.ts +9 -0
  259. package/dist/utils/atomicWrite.d.ts.map +1 -0
  260. package/dist/utils/atomicWrite.js +83 -0
  261. package/dist/utils/logger.d.ts +59 -0
  262. package/dist/utils/logger.d.ts.map +1 -0
  263. package/dist/utils/logger.js +123 -0
  264. package/dist/vendor/enquirer.d.ts +33 -0
  265. package/dist/vendor/enquirer.d.ts.map +1 -0
  266. package/dist/vendor/enquirer.js +11 -0
  267. package/package.json +71 -0
@@ -0,0 +1,142 @@
1
+ "use strict";
2
+ // src/services/Sanitizer.ts
3
+ //
4
+ // PII / secrets masking (T3). Pure, dependency-free string transforms — no
5
+ // SDK, no config, no I/O — so it is trivially unit-testable in isolation.
6
+ //
7
+ // DESIGN: mask high-precision patterns + sensitive input VALUES only, never
8
+ // blanket structural text. The healer locates elements by their anchors
9
+ // (tag/role/id/class/data-testid/labels/headings/button text); masking those
10
+ // would destroy the very signal the LLM needs. Credit cards, emails, tokens,
11
+ // etc. essentially never serve as a meaningful element-locating anchor, so
12
+ // masking them keeps false-positives near zero.
13
+ //
14
+ // Placeholders are DETERMINISTIC so the local healing cache key (computed on
15
+ // pre-sanitization content) and LLM retry behavior stay reproducible.
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.REDACTIONS = void 0;
18
+ exports.sanitizeText = sanitizeText;
19
+ exports.compileCustomPatterns = compileCustomPatterns;
20
+ const logger_1 = require("../utils/logger");
21
+ exports.REDACTIONS = {
22
+ EMAIL: "[EMAIL_REDACTED]",
23
+ CREDIT_CARD: "[CC_REDACTED]",
24
+ CVV: "[CVV_REDACTED]",
25
+ PHONE: "[PHONE_REDACTED]",
26
+ PASSWORD: "[PASSWORD_REDACTED]",
27
+ BEARER: "[BEARER_REDACTED]",
28
+ TOKEN: "[TOKEN_REDACTED]",
29
+ GENERIC: "[REDACTED]",
30
+ };
31
+ // ── Luhn check — keeps credit-card masking precise (rejects random digit
32
+ // runs like timestamps / ids that merely look card-shaped). ───────────
33
+ function luhnValid(digits) {
34
+ let sum = 0;
35
+ let alt = false;
36
+ for (let i = digits.length - 1; i >= 0; i--) {
37
+ let d = digits.charCodeAt(i) - 48;
38
+ if (d < 0 || d > 9)
39
+ return false;
40
+ if (alt) {
41
+ d *= 2;
42
+ if (d > 9)
43
+ d -= 9;
44
+ }
45
+ sum += d;
46
+ alt = !alt;
47
+ }
48
+ return sum % 10 === 0;
49
+ }
50
+ // ── Pattern table. ORDER MATTERS: most specific first so a broad pattern
51
+ // (phone) never swallows a more specific one (credit card). ───────────
52
+ // Bearer auth header value.
53
+ const BEARER_RE = /\bBearer\s+[A-Za-z0-9._~+/-]+=*/gi;
54
+ // JSON Web Token (header.payload.signature).
55
+ const JWT_RE = /\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g;
56
+ // Common API key prefixes (OpenAI sk-, Anthropic sk-ant-, GitHub ghp_/gho_…).
57
+ const APIKEY_RE = /\b(?:sk-(?:ant-)?[A-Za-z0-9_-]{16,}|gh[pousr]_[A-Za-z0-9]{20,})\b/g;
58
+ // Email address.
59
+ const EMAIL_RE = /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g;
60
+ // Credit-card-shaped run of 13–19 digits, separators only BETWEEN digits
61
+ // (never leading/trailing, so adjacent whitespace is preserved).
62
+ const CC_RE = /\b\d(?:[ -]?\d){12,18}\b/g;
63
+ // Labeled CVV/CVC (avoids masking every 3–4 digit number).
64
+ const CVV_RE = /\b(cvv2?|cvc2?|cid)\b(\s*[:=]?\s*)(\d{3,4})\b/gi;
65
+ // Phone number: optional +cc, optional area code, then 2+ separated groups.
66
+ // No leading \b (a "+" prefix has no word boundary). Applied LAST.
67
+ const PHONE_RE = /(?:\+\d{1,3}[\s.-]?)?(?:\(\d{1,4}\)[\s.-]?)?\d{2,4}(?:[\s.-]\d{2,4}){1,3}/g;
68
+ /** Strip a leading "+" then non-digits, returning just the digit run. */
69
+ function digitsOnly(s) {
70
+ return s.replace(/\D/g, "");
71
+ }
72
+ /**
73
+ * Mask PII/secrets in a single string. Pure — returns a new string; the input
74
+ * is never mutated. Safe to call on already-sanitized text (idempotent on the
75
+ * placeholders, which contain no maskable patterns).
76
+ */
77
+ function sanitizeText(input, opts = {}) {
78
+ if (!input)
79
+ return input;
80
+ let out = input;
81
+ // 1. Input VALUES by type (structural) — run first so a password field
82
+ // holding an email-looking value becomes [PASSWORD_REDACTED], not
83
+ // [EMAIL_REDACTED]. Default on.
84
+ if (opts.redactInputValues !== false) {
85
+ out = maskInputValues(out);
86
+ }
87
+ // 2. Secrets / tokens (most specific).
88
+ out = out.replace(BEARER_RE, exports.REDACTIONS.BEARER);
89
+ out = out.replace(JWT_RE, exports.REDACTIONS.TOKEN);
90
+ out = out.replace(APIKEY_RE, exports.REDACTIONS.TOKEN);
91
+ // 3. Email.
92
+ out = out.replace(EMAIL_RE, exports.REDACTIONS.EMAIL);
93
+ // 4. Credit cards (Luhn-validated to avoid false positives).
94
+ out = out.replace(CC_RE, (m) => {
95
+ const d = digitsOnly(m);
96
+ return d.length >= 13 && d.length <= 19 && luhnValid(d)
97
+ ? exports.REDACTIONS.CREDIT_CARD
98
+ : m;
99
+ });
100
+ // 5. Labeled CVV — keep the label, mask the digits.
101
+ out = out.replace(CVV_RE, (_m, label, sep) => `${label}${sep}${exports.REDACTIONS.CVV}`);
102
+ // 6. Phone numbers (broadest among digit patterns — last).
103
+ out = out.replace(PHONE_RE, (m) => {
104
+ return digitsOnly(m).length >= 7 ? exports.REDACTIONS.PHONE : m;
105
+ });
106
+ // 7. Caller-supplied custom patterns.
107
+ for (const re of opts.customPatterns ?? []) {
108
+ out = out.replace(re, exports.REDACTIONS.GENERIC);
109
+ }
110
+ return out;
111
+ }
112
+ // ── <input> value masking by type ───────────────────────────────────────
113
+ const INPUT_TAG_RE = /<input\b[^>]*>/gi;
114
+ const TYPE_ATTR_RE = /\btype\s*=\s*["']?(password|tel|email)["']?/i;
115
+ const VALUE_ATTR_RE = /(\bvalue\s*=\s*)(["'])(.*?)\2/i;
116
+ const TYPE_TO_REDACTION = {
117
+ password: exports.REDACTIONS.PASSWORD,
118
+ tel: exports.REDACTIONS.PHONE,
119
+ email: exports.REDACTIONS.EMAIL,
120
+ };
121
+ function maskInputValues(html) {
122
+ return html.replace(INPUT_TAG_RE, (tag) => {
123
+ const typeMatch = TYPE_ATTR_RE.exec(tag);
124
+ if (!typeMatch)
125
+ return tag;
126
+ const redaction = TYPE_TO_REDACTION[typeMatch[1].toLowerCase()];
127
+ return tag.replace(VALUE_ATTR_RE, (_m, prefix, quote) => `${prefix}${quote}${redaction}${quote}`);
128
+ });
129
+ }
130
+ /** Compile customPatterns (regex source strings) → RegExp[]; skip invalid. */
131
+ function compileCustomPatterns(sources) {
132
+ const out = [];
133
+ for (const src of sources) {
134
+ try {
135
+ out.push(new RegExp(src, "g"));
136
+ }
137
+ catch (e) {
138
+ logger_1.logger.warn(`Skipping invalid customPattern ${JSON.stringify(src)}: ${e.message}`);
139
+ }
140
+ }
141
+ return out;
142
+ }
@@ -0,0 +1,19 @@
1
+ import Anthropic from "@anthropic-ai/sdk";
2
+ import type { ResolvedSanitization } from "../config/SelaConfig";
3
+ export interface CreateSanitizingAnthropicOptions {
4
+ /** API key for the underlying client (ignored when `inner` is supplied). */
5
+ apiKey?: string;
6
+ /** Pre-built client to wrap (tests / advanced callers). */
7
+ inner?: Anthropic;
8
+ /**
9
+ * Sanitization config. Defaults to `ConfigLoader.getInstance().sanitization`.
10
+ * When `enabled` is false the bare client is returned unwrapped.
11
+ */
12
+ sanitization?: ResolvedSanitization;
13
+ }
14
+ /**
15
+ * Returns an Anthropic client whose `messages.create` sanitizes outbound text.
16
+ * When sanitization is disabled, returns the underlying client unchanged.
17
+ */
18
+ export declare function createSanitizingAnthropic(opts?: CreateSanitizingAnthropicOptions): Anthropic;
19
+ //# sourceMappingURL=SanitizingAnthropic.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SanitizingAnthropic.d.ts","sourceRoot":"","sources":["../../src/services/SanitizingAnthropic.ts"],"names":[],"mappings":"AAYA,OAAO,SAAS,MAAM,mBAAmB,CAAC;AAE1C,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAGjE,MAAM,WAAW,gCAAgC;IAC/C,4EAA4E;IAC5E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,2DAA2D;IAC3D,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB;;;OAGG;IACH,YAAY,CAAC,EAAE,oBAAoB,CAAC;CACrC;AAmCD;;;GAGG;AACH,wBAAgB,yBAAyB,CACvC,IAAI,GAAE,gCAAqC,GAC1C,SAAS,CA+BX"}
@@ -0,0 +1,84 @@
1
+ "use strict";
2
+ // src/services/SanitizingAnthropic.ts
3
+ //
4
+ // The single PII/secrets chokepoint (T3). Wraps an Anthropic client so EVERY
5
+ // outbound `messages.create` has its `system` prompt and all message text
6
+ // scrubbed before the HTTPS request — regardless of which service made the
7
+ // call. This guarantees 100% coverage structurally instead of relying on each
8
+ // call site (LLMService, IntentAuditor, future callers) remembering to mask.
9
+ //
10
+ // Both LLMService and IntentAuditor construct their client here. Tests that
11
+ // inject a mock client via `opts.anthropic` bypass this factory entirely, so
12
+ // they are unaffected; the wrapper itself is unit-tested in isolation.
13
+ var __importDefault = (this && this.__importDefault) || function (mod) {
14
+ return (mod && mod.__esModule) ? mod : { "default": mod };
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.createSanitizingAnthropic = createSanitizingAnthropic;
18
+ const sdk_1 = __importDefault(require("@anthropic-ai/sdk"));
19
+ const ConfigLoader_1 = require("../config/ConfigLoader");
20
+ const Sanitizer_1 = require("./Sanitizer");
21
+ function readSanitization() {
22
+ try {
23
+ return ConfigLoader_1.ConfigLoader.getInstance().sanitization;
24
+ }
25
+ catch {
26
+ // Fail SECURE: if config can't load, default to masking on.
27
+ return { enabled: true, redactInputValues: true, customPatterns: [] };
28
+ }
29
+ }
30
+ /**
31
+ * Scrub a single `messages[].content` entry. Content may be a plain string or
32
+ * an array of content blocks; only text blocks are touched. Non-text blocks
33
+ * (images, tool_use, …) pass through untouched.
34
+ */
35
+ function scrubContent(content, scrub) {
36
+ if (typeof content === "string")
37
+ return scrub(content);
38
+ if (Array.isArray(content)) {
39
+ return content.map((block) => {
40
+ if (block && block.type === "text" && typeof block.text === "string") {
41
+ return { ...block, text: scrub(block.text) };
42
+ }
43
+ return block;
44
+ });
45
+ }
46
+ return content;
47
+ }
48
+ /** Scrub the `system` field (string or array of text blocks). */
49
+ function scrubSystem(system, scrub) {
50
+ if (system === undefined)
51
+ return system;
52
+ return scrubContent(system, scrub);
53
+ }
54
+ /**
55
+ * Returns an Anthropic client whose `messages.create` sanitizes outbound text.
56
+ * When sanitization is disabled, returns the underlying client unchanged.
57
+ */
58
+ function createSanitizingAnthropic(opts = {}) {
59
+ const client = opts.inner ?? new sdk_1.default({ apiKey: opts.apiKey });
60
+ const cfg = opts.sanitization ?? readSanitization();
61
+ if (!cfg.enabled)
62
+ return client;
63
+ const customPatterns = (0, Sanitizer_1.compileCustomPatterns)(cfg.customPatterns ?? []);
64
+ const scrub = (s) => (0, Sanitizer_1.sanitizeText)(s, {
65
+ redactInputValues: cfg.redactInputValues,
66
+ customPatterns,
67
+ });
68
+ const messages = client.messages;
69
+ const original = messages.create.bind(messages);
70
+ messages.create = (params, requestOptions) => {
71
+ const scrubbed = {
72
+ ...params,
73
+ system: scrubSystem(params?.system, scrub),
74
+ messages: Array.isArray(params?.messages)
75
+ ? params.messages.map((m) => ({
76
+ ...m,
77
+ content: scrubContent(m?.content, scrub),
78
+ }))
79
+ : params?.messages,
80
+ };
81
+ return original(scrubbed, requestOptions);
82
+ };
83
+ return client;
84
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * True when `value` is (or contains, in a `>>` chain) a Playwright internal
3
+ * engine selector. These start with the `internal:` engine prefix.
4
+ */
5
+ export declare function isInternalEngineSelector(value: string): boolean;
6
+ interface MethodDescriptor {
7
+ method: string;
8
+ primaryArg: string;
9
+ options?: Record<string, string | boolean | number>;
10
+ }
11
+ /**
12
+ * Parse a SINGLE internal selector segment into a method descriptor, or null
13
+ * when it is not a recognised internal form (so callers REFUSE rather than
14
+ * guess). Mirrors the engine→public mapping used by ASTSourceUpdater.
15
+ */
16
+ export declare function parseInternalSelector(selector: string): MethodDescriptor | null;
17
+ export declare function buildMethodCall(receiver: string, desc: MethodDescriptor): string;
18
+ /**
19
+ * Translate an internal selector string into an idiomatic Playwright method
20
+ * expression rooted at `receiver`. Handles `>>` chains by chaining each
21
+ * segment. Returns null when ANY segment is an unrecognised internal form
22
+ * (frame `internal:control=…` plumbing, unknown engines) — the caller must
23
+ * then refuse rather than emit something fragile.
24
+ */
25
+ export declare function internalToMethodExpression(selector: string, receiver?: string): string | null;
26
+ /**
27
+ * Convert an internal engine selector into a VALID PUBLIC `.locator()` string,
28
+ * preserving `>>` chaining. Returns null when ANY internal segment has no
29
+ * public string engine (so the caller must method-shift or refuse instead of
30
+ * stuffing an unusable string into a constant).
31
+ *
32
+ * Example: `internal:role=button[name="Update System"i]`
33
+ * → `role=button[name="Update System"]`
34
+ */
35
+ export declare function internalToPublicSelectorString(selector: string): string | null;
36
+ export interface SanitizeResult {
37
+ /** The (possibly rewritten) expression text. */
38
+ text: string;
39
+ /** True when the original contained an internal selector that was rewritten. */
40
+ sanitized: boolean;
41
+ /** False when an internal selector remained that could NOT be translated. */
42
+ safe: boolean;
43
+ }
44
+ /**
45
+ * Rewrite any `recv.locator("internal:…")` / `recv.frameLocator("internal:…")`
46
+ * inside `expr` to its idiomatic method form. Returns `safe:false` when an
47
+ * internal selector remains that cannot be translated, so the caller refuses
48
+ * to write it.
49
+ */
50
+ export declare function sanitizeLocatorExpression(expr: string): SanitizeResult;
51
+ export {};
52
+ //# sourceMappingURL=SelectorSanitizer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SelectorSanitizer.d.ts","sourceRoot":"","sources":["../../src/services/SelectorSanitizer.ts"],"names":[],"mappings":"AAoCA;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAK/D;AAMD,UAAU,gBAAgB;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,GAAG,MAAM,CAAC,CAAC;CACrD;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,gBAAgB,GAAG,IAAI,CA6D/E;AAWD,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,gBAAgB,GAAG,MAAM,CAyBhF;AAMD;;;;;;GAMG;AACH,wBAAgB,0BAA0B,CACxC,QAAQ,EAAE,MAAM,EAChB,QAAQ,SAAS,GAChB,MAAM,GAAG,IAAI,CAmBf;AAgDD;;;;;;;;GAQG;AACH,wBAAgB,8BAA8B,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAkB9E;AAMD,MAAM,WAAW,cAAc;IAC7B,gDAAgD;IAChD,IAAI,EAAE,MAAM,CAAC;IACb,gFAAgF;IAChF,SAAS,EAAE,OAAO,CAAC;IACnB,6EAA6E;IAC7E,IAAI,EAAE,OAAO,CAAC;CACf;AAID;;;;;GAKG;AACH,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc,CAgEtE"}
@@ -0,0 +1,318 @@
1
+ "use strict";
2
+ // src/services/SelectorSanitizer.ts
3
+ //
4
+ // THE ABSOLUTE RULE (Ticket #2 hardening):
5
+ // Playwright's `internal:` engine selectors (e.g.
6
+ // `internal:role=button[name="Update System"i]`) are a PRIVATE runtime
7
+ // serialization. They must NEVER be written verbatim into user source — not
8
+ // as a `.locator()` string argument, and not into a string-literal constant
9
+ // consumed by `.locator()`. Although the engine happens to accept them at
10
+ // runtime, they are undocumented, version-fragile, and leak internals into
11
+ // the developer's clean code (bad DX, the exact bug this module prevents).
12
+ //
13
+ // This module is the single source of truth for:
14
+ // 1. DETECTING an internal engine selector (`isInternalEngineSelector`).
15
+ // 2. TRANSLATING it to the idiomatic public method form
16
+ // (`internalToMethodExpression`) — `getByRole(...)`, `getByText(...)`, …
17
+ // 3. SANITIZING a full locator expression (`sanitizeLocatorExpression`):
18
+ // rewriting `recv.locator("internal:…")` → `recv.getByRole(…)`, and
19
+ // reporting whether anything internal remains (unconvertible ⇒ unsafe).
20
+ //
21
+ // Pure, dependency-light (ts-morph only for expression rewriting). Every
22
+ // write-head (InitializerUpdater, MutationApplier, SourceUpdater regex gate)
23
+ // consults this so the invariant holds across all heal paths.
24
+ Object.defineProperty(exports, "__esModule", { value: true });
25
+ exports.isInternalEngineSelector = isInternalEngineSelector;
26
+ exports.parseInternalSelector = parseInternalSelector;
27
+ exports.buildMethodCall = buildMethodCall;
28
+ exports.internalToMethodExpression = internalToMethodExpression;
29
+ exports.internalToPublicSelectorString = internalToPublicSelectorString;
30
+ exports.sanitizeLocatorExpression = sanitizeLocatorExpression;
31
+ const ts_morph_1 = require("ts-morph");
32
+ // ═══════════════════════════════════════════════════════════════════
33
+ // DETECTION
34
+ // ═══════════════════════════════════════════════════════════════════
35
+ /**
36
+ * True when `value` is (or contains, in a `>>` chain) a Playwright internal
37
+ * engine selector. These start with the `internal:` engine prefix.
38
+ */
39
+ function isInternalEngineSelector(value) {
40
+ if (!value)
41
+ return false;
42
+ return value
43
+ .split(">>")
44
+ .some((seg) => seg.trim().toLowerCase().startsWith("internal:"));
45
+ }
46
+ /**
47
+ * Parse a SINGLE internal selector segment into a method descriptor, or null
48
+ * when it is not a recognised internal form (so callers REFUSE rather than
49
+ * guess). Mirrors the engine→public mapping used by ASTSourceUpdater.
50
+ */
51
+ function parseInternalSelector(selector) {
52
+ // Match against the raw (trimmed) selector. The case-insensitivity `i` flag
53
+ // is significant per-pattern (trailing for text/label, inside `[…]` for
54
+ // role), so it must NOT be stripped globally up front.
55
+ const s = selector.trim();
56
+ // internal:role=button[name="X"i][level=2]
57
+ const roleMatch = s.match(/^internal:role=([a-zA-Z]+)(\[.*\])?$/);
58
+ if (roleMatch) {
59
+ const desc = {
60
+ method: "getByRole",
61
+ primaryArg: roleMatch[1],
62
+ };
63
+ const attrs = roleMatch[2] ?? "";
64
+ const nameMatch = attrs.match(/name="([^"]*)"(i)?/);
65
+ const options = {};
66
+ if (nameMatch) {
67
+ options.name = nameMatch[1];
68
+ if (!nameMatch[2])
69
+ options.exact = true; // no `i` flag → exact match
70
+ }
71
+ const levelMatch = attrs.match(/level=(\d+)/);
72
+ if (levelMatch)
73
+ options.level = Number(levelMatch[1]);
74
+ if (Object.keys(options).length)
75
+ desc.options = options;
76
+ return desc;
77
+ }
78
+ // internal:label="X"i | internal:label=X
79
+ const labelQuoted = s.match(/^internal:label="([^"]*)"(i)?$/);
80
+ if (labelQuoted) {
81
+ return labelQuoted[2]
82
+ ? { method: "getByLabel", primaryArg: labelQuoted[1] }
83
+ : { method: "getByLabel", primaryArg: labelQuoted[1], options: { exact: true } };
84
+ }
85
+ const labelBare = s.match(/^internal:label=([^[]+)$/);
86
+ if (labelBare) {
87
+ return { method: "getByLabel", primaryArg: labelBare[1].trim() };
88
+ }
89
+ // internal:text="X"i
90
+ const textMatch = s.match(/^internal:text="([^"]*)"(i)?$/);
91
+ if (textMatch) {
92
+ return textMatch[2]
93
+ ? { method: "getByText", primaryArg: textMatch[1] }
94
+ : { method: "getByText", primaryArg: textMatch[1], options: { exact: true } };
95
+ }
96
+ // internal:attr=[placeholder="X"] / [alt="X"] / [title="X"]
97
+ const placeholder = s.match(/^internal:attr=\[placeholder="([^"]*)"\]$/);
98
+ if (placeholder)
99
+ return { method: "getByPlaceholder", primaryArg: placeholder[1] };
100
+ const alt = s.match(/^internal:attr=\[alt="([^"]*)"\]$/);
101
+ if (alt)
102
+ return { method: "getByAltText", primaryArg: alt[1] };
103
+ const title = s.match(/^internal:attr=\[title="([^"]*)"\]$/);
104
+ if (title)
105
+ return { method: "getByTitle", primaryArg: title[1] };
106
+ // internal:testid=X | internal:attr=[data-testid="X"]
107
+ const testId = s.match(/^internal:testid=(.+)$/) ||
108
+ s.match(/^internal:attr=\[data-testid="([^"]*)"\]$/);
109
+ if (testId)
110
+ return { method: "getByTestId", primaryArg: testId[1].trim() };
111
+ return null; // unrecognised internal form → caller must refuse
112
+ }
113
+ // ═══════════════════════════════════════════════════════════════════
114
+ // BUILD (method descriptor → idiomatic call text)
115
+ // ═══════════════════════════════════════════════════════════════════
116
+ function q(s) {
117
+ // Prefer double quotes, escaping any embedded ones.
118
+ return `"${s.replace(/"/g, '\\"')}"`;
119
+ }
120
+ function buildMethodCall(receiver, desc) {
121
+ switch (desc.method) {
122
+ case "getByRole": {
123
+ const opts = [];
124
+ if (desc.options?.name !== undefined)
125
+ opts.push(`name: ${q(String(desc.options.name))}`);
126
+ if (desc.options?.exact === true)
127
+ opts.push(`exact: true`);
128
+ if (desc.options?.level !== undefined)
129
+ opts.push(`level: ${desc.options.level}`);
130
+ const optStr = opts.length ? `, { ${opts.join(", ")} }` : "";
131
+ return `${receiver}.getByRole(${q(desc.primaryArg)}${optStr})`;
132
+ }
133
+ case "getByText":
134
+ case "getByLabel": {
135
+ const exact = desc.options?.exact === true ? `, { exact: true }` : "";
136
+ return `${receiver}.${desc.method}(${q(desc.primaryArg)}${exact})`;
137
+ }
138
+ case "getByPlaceholder":
139
+ case "getByAltText":
140
+ case "getByTitle":
141
+ case "getByTestId":
142
+ return `${receiver}.${desc.method}(${q(desc.primaryArg)})`;
143
+ default:
144
+ return `${receiver}.locator(${q(desc.primaryArg)})`;
145
+ }
146
+ }
147
+ // ═══════════════════════════════════════════════════════════════════
148
+ // TRANSLATE (full internal selector string → method expression)
149
+ // ═══════════════════════════════════════════════════════════════════
150
+ /**
151
+ * Translate an internal selector string into an idiomatic Playwright method
152
+ * expression rooted at `receiver`. Handles `>>` chains by chaining each
153
+ * segment. Returns null when ANY segment is an unrecognised internal form
154
+ * (frame `internal:control=…` plumbing, unknown engines) — the caller must
155
+ * then refuse rather than emit something fragile.
156
+ */
157
+ function internalToMethodExpression(selector, receiver = "page") {
158
+ const segments = selector
159
+ .split(">>")
160
+ .map((s) => s.trim())
161
+ .filter(Boolean);
162
+ if (segments.length === 0)
163
+ return null;
164
+ let expr = receiver;
165
+ for (const seg of segments) {
166
+ if (seg.toLowerCase().startsWith("internal:")) {
167
+ const desc = parseInternalSelector(seg);
168
+ if (!desc)
169
+ return null; // unconvertible internal form
170
+ expr = buildMethodCall(expr, desc);
171
+ }
172
+ else {
173
+ // Plain CSS / text segment — stays a public .locator() string.
174
+ expr = `${expr}.locator(${q(seg)})`;
175
+ }
176
+ }
177
+ return expr;
178
+ }
179
+ // ═══════════════════════════════════════════════════════════════════
180
+ // TRANSLATE (internal selector → valid PUBLIC .locator() string)
181
+ // ═══════════════════════════════════════════════════════════════════
182
+ function escAttr(s) {
183
+ return s.replace(/"/g, '\\"');
184
+ }
185
+ /**
186
+ * Convert a single internal segment to a valid PUBLIC `.locator()` selector
187
+ * string (the documented selector engines: `role=`, `text=`, css attribute).
188
+ * Returns null for forms that have NO public string engine (label is an
189
+ * associative relationship, not an attribute — only `getByLabel(...)` works).
190
+ */
191
+ function segmentToPublicSelector(seg) {
192
+ const desc = parseInternalSelector(seg);
193
+ if (!desc)
194
+ return null;
195
+ switch (desc.method) {
196
+ case "getByRole": {
197
+ const attrs = [];
198
+ if (desc.options?.name !== undefined)
199
+ attrs.push(`name="${escAttr(String(desc.options.name))}"`);
200
+ if (desc.options?.level !== undefined)
201
+ attrs.push(`level=${desc.options.level}`);
202
+ const attrStr = attrs.length ? `[${attrs.join("][")}]` : "";
203
+ return `role=${desc.primaryArg}${attrStr}`;
204
+ }
205
+ case "getByText":
206
+ // Quoted text engine = exact-ish; unquoted = substring (i-flag default).
207
+ return desc.options?.exact
208
+ ? `text="${escAttr(desc.primaryArg)}"`
209
+ : `text=${desc.primaryArg}`;
210
+ case "getByPlaceholder":
211
+ return `[placeholder="${escAttr(desc.primaryArg)}"]`;
212
+ case "getByAltText":
213
+ return `[alt="${escAttr(desc.primaryArg)}"]`;
214
+ case "getByTitle":
215
+ return `[title="${escAttr(desc.primaryArg)}"]`;
216
+ case "getByTestId":
217
+ return `[data-testid="${escAttr(desc.primaryArg)}"]`;
218
+ case "getByLabel":
219
+ default:
220
+ return null; // no public string-engine equivalent
221
+ }
222
+ }
223
+ /**
224
+ * Convert an internal engine selector into a VALID PUBLIC `.locator()` string,
225
+ * preserving `>>` chaining. Returns null when ANY internal segment has no
226
+ * public string engine (so the caller must method-shift or refuse instead of
227
+ * stuffing an unusable string into a constant).
228
+ *
229
+ * Example: `internal:role=button[name="Update System"i]`
230
+ * → `role=button[name="Update System"]`
231
+ */
232
+ function internalToPublicSelectorString(selector) {
233
+ const segments = selector
234
+ .split(">>")
235
+ .map((s) => s.trim())
236
+ .filter(Boolean);
237
+ if (segments.length === 0)
238
+ return null;
239
+ const out = [];
240
+ for (const seg of segments) {
241
+ if (seg.toLowerCase().startsWith("internal:")) {
242
+ const pub = segmentToPublicSelector(seg);
243
+ if (!pub)
244
+ return null;
245
+ out.push(pub);
246
+ }
247
+ else {
248
+ out.push(seg); // already a public css/text segment
249
+ }
250
+ }
251
+ return out.join(" >> ");
252
+ }
253
+ let probeCounter = 0;
254
+ /**
255
+ * Rewrite any `recv.locator("internal:…")` / `recv.frameLocator("internal:…")`
256
+ * inside `expr` to its idiomatic method form. Returns `safe:false` when an
257
+ * internal selector remains that cannot be translated, so the caller refuses
258
+ * to write it.
259
+ */
260
+ function sanitizeLocatorExpression(expr) {
261
+ // Fast path: nothing internal anywhere.
262
+ if (!expr.includes("internal:")) {
263
+ return { text: expr, sanitized: false, safe: true };
264
+ }
265
+ const project = new ts_morph_1.Project({
266
+ useInMemoryFileSystem: true,
267
+ compilerOptions: { allowJs: true, noEmit: true, skipLibCheck: true, noResolve: true },
268
+ });
269
+ let current = expr;
270
+ let sanitized = false;
271
+ // Iteratively rewrite the first internal locator call until none remain.
272
+ // replaceWithText invalidates nodes, so we re-parse each pass.
273
+ for (let pass = 0; pass < 32; pass++) {
274
+ let sf;
275
+ try {
276
+ sf = project.createSourceFile(`__san${probeCounter++}.ts`, `const __x = ${current};`, {
277
+ overwrite: true,
278
+ });
279
+ }
280
+ catch {
281
+ return { text: expr, sanitized, safe: false };
282
+ }
283
+ const call = sf
284
+ .getDescendantsOfKind(ts_morph_1.SyntaxKind.CallExpression)
285
+ .find((c) => {
286
+ const callee = c.getExpression();
287
+ if (!ts_morph_1.Node.isPropertyAccessExpression(callee))
288
+ return false;
289
+ const m = callee.getName();
290
+ if (m !== "locator" && m !== "frameLocator")
291
+ return false;
292
+ const arg = c.getArguments()[0];
293
+ if (!arg)
294
+ return false;
295
+ const lit = ts_morph_1.Node.isStringLiteral(arg) || ts_morph_1.Node.isNoSubstitutionTemplateLiteral(arg)
296
+ ? arg.getLiteralValue()
297
+ : null;
298
+ return lit !== null && isInternalEngineSelector(lit);
299
+ });
300
+ if (!call)
301
+ break; // no more internal locator calls
302
+ const callee = call.getExpression();
303
+ const receiver = callee.getExpression().getText();
304
+ const arg = call.getArguments()[0];
305
+ const internalSel = arg.getLiteralValue();
306
+ const methodExpr = internalToMethodExpression(internalSel, receiver);
307
+ if (!methodExpr) {
308
+ // Unconvertible — leave as-is and report unsafe.
309
+ return { text: current, sanitized, safe: false };
310
+ }
311
+ call.replaceWithText(methodExpr);
312
+ current = sf.getVariableDeclaration("__x").getInitializer().getText();
313
+ sanitized = true;
314
+ }
315
+ // Final integrity check: nothing internal must survive.
316
+ const safe = !isInternalEngineSelector(current) && !current.includes("internal:");
317
+ return { text: current, sanitized, safe };
318
+ }
@@ -0,0 +1,11 @@
1
+ import { Page } from "@playwright/test";
2
+ import { ElementSnapshot, ElementSnapshotV2 } from "../types";
3
+ export declare class SnapshotService {
4
+ private baseDir;
5
+ constructor();
6
+ save(key: string, data: ElementSnapshot): Promise<void>;
7
+ load(key: string): Promise<ElementSnapshotV2 | null>;
8
+ captureElement(page: Page, selector: string, framePath?: string[]): Promise<ElementSnapshotV2 | null>;
9
+ private migrateToV2;
10
+ }
11
+ //# sourceMappingURL=SnapshotService.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SnapshotService.d.ts","sourceRoot":"","sources":["../../src/services/SnapshotService.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AACxC,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAkB,MAAM,UAAU,CAAC;AAI9E,qBAAa,eAAe;IAC1B,OAAO,CAAC,OAAO,CAAS;;IASlB,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IAcvD,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC;IAsBpD,cAAc,CAClB,IAAI,EAAE,IAAI,EACV,QAAQ,EAAE,MAAM,EAChB,SAAS,GAAE,MAAM,EAAO,GACvB,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC;IAoRpC,OAAO,CAAC,WAAW;CAsBpB"}