@weavetab/mcp 2.5.0-beta.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.
Files changed (281) hide show
  1. package/AGENTS.md +48 -0
  2. package/ARCHITECTURE.md +590 -0
  3. package/LICENSE +631 -0
  4. package/README.md +909 -0
  5. package/SECURITY.md +196 -0
  6. package/TROUBLESHOOTING.md +365 -0
  7. package/dist/anticaptcha/detector.d.ts +49 -0
  8. package/dist/anticaptcha/detector.js +465 -0
  9. package/dist/anticaptcha/integration.d.ts +36 -0
  10. package/dist/anticaptcha/integration.js +123 -0
  11. package/dist/anticaptcha/solver.d.ts +27 -0
  12. package/dist/anticaptcha/solver.js +552 -0
  13. package/dist/audit/logger.d.ts +13 -0
  14. package/dist/audit/logger.js +177 -0
  15. package/dist/audit/telemetry.d.ts +36 -0
  16. package/dist/audit/telemetry.js +162 -0
  17. package/dist/cdp/bridge.d.ts +16 -0
  18. package/dist/cdp/bridge.js +826 -0
  19. package/dist/cdp/confirm.d.ts +26 -0
  20. package/dist/cdp/confirm.js +79 -0
  21. package/dist/cdp/connector.d.ts +37 -0
  22. package/dist/cdp/connector.js +661 -0
  23. package/dist/cdp/extension.d.ts +36 -0
  24. package/dist/cdp/extension.js +118 -0
  25. package/dist/cdp/finder.d.ts +1 -0
  26. package/dist/cdp/finder.js +2 -0
  27. package/dist/cdp/helpers.d.ts +11 -0
  28. package/dist/cdp/helpers.js +195 -0
  29. package/dist/cdp/label_cache.d.ts +12 -0
  30. package/dist/cdp/label_cache.js +51 -0
  31. package/dist/cdp/lock.d.ts +33 -0
  32. package/dist/cdp/lock.js +206 -0
  33. package/dist/cdp/omni_broker.d.ts +17 -0
  34. package/dist/cdp/omni_broker.js +119 -0
  35. package/dist/cdp/profiles.d.ts +1 -0
  36. package/dist/cdp/profiles.js +87 -0
  37. package/dist/cdp/pruner.d.ts +41 -0
  38. package/dist/cdp/pruner.js +263 -0
  39. package/dist/cdp/ref_cache.d.ts +101 -0
  40. package/dist/cdp/ref_cache.js +234 -0
  41. package/dist/cdp/retry.d.ts +5 -0
  42. package/dist/cdp/retry.js +39 -0
  43. package/dist/cdp/retry_enhanced.d.ts +44 -0
  44. package/dist/cdp/retry_enhanced.js +102 -0
  45. package/dist/cdp/security.d.ts +50 -0
  46. package/dist/cdp/security.js +237 -0
  47. package/dist/cdp/verify.d.ts +39 -0
  48. package/dist/cdp/verify.js +143 -0
  49. package/dist/cdp/walker.d.ts +91 -0
  50. package/dist/cdp/walker.js +896 -0
  51. package/dist/cdp/walker.test.d.ts +2 -0
  52. package/dist/cdp/walker.test.js +172 -0
  53. package/dist/cdp/websocket_transport.d.ts +15 -0
  54. package/dist/cdp/websocket_transport.js +118 -0
  55. package/dist/cli/plugin.d.ts +9 -0
  56. package/dist/cli/plugin.js +1134 -0
  57. package/dist/cli/updater.d.ts +1 -0
  58. package/dist/cli/updater.js +72 -0
  59. package/dist/config/loader.d.ts +120 -0
  60. package/dist/config/loader.js +704 -0
  61. package/dist/index.d.ts +2 -0
  62. package/dist/index.js +1248 -0
  63. package/dist/intelligence/hints.d.ts +11 -0
  64. package/dist/intelligence/hints.js +39 -0
  65. package/dist/intelligence/intent.d.ts +33 -0
  66. package/dist/intelligence/intent.js +171 -0
  67. package/dist/intelligence/sessionMemory.d.ts +66 -0
  68. package/dist/intelligence/sessionMemory.js +284 -0
  69. package/dist/intelligence/sessionMemory.test.d.ts +2 -0
  70. package/dist/intelligence/sessionMemory.test.js +62 -0
  71. package/dist/intelligence/trail.d.ts +64 -0
  72. package/dist/intelligence/trail.js +233 -0
  73. package/dist/intelligence/trail.test.d.ts +2 -0
  74. package/dist/intelligence/trail.test.js +110 -0
  75. package/dist/intelligence/vision.d.ts +24 -0
  76. package/dist/intelligence/vision.js +151 -0
  77. package/dist/lifecycle/mission_manager.d.ts +16 -0
  78. package/dist/lifecycle/mission_manager.js +44 -0
  79. package/dist/overlay/bubble.d.ts +16 -0
  80. package/dist/overlay/bubble.js +37 -0
  81. package/dist/overlay/ghost.d.ts +35 -0
  82. package/dist/overlay/ghost.js +508 -0
  83. package/dist/overlay/injector.d.ts +22 -0
  84. package/dist/overlay/injector.js +198 -0
  85. package/dist/plugin/interface.d.ts +134 -0
  86. package/dist/plugin/interface.js +1 -0
  87. package/dist/plugin/loader.d.ts +18 -0
  88. package/dist/plugin/loader.js +155 -0
  89. package/dist/plugin/registry.d.ts +50 -0
  90. package/dist/plugin/registry.js +234 -0
  91. package/dist/security/blacklist.d.ts +24 -0
  92. package/dist/security/blacklist.js +80 -0
  93. package/dist/security/filter.d.ts +2 -0
  94. package/dist/security/filter.js +20 -0
  95. package/dist/security/guard.d.ts +8 -0
  96. package/dist/security/guard.js +35 -0
  97. package/dist/security/ratelimit.d.ts +11 -0
  98. package/dist/security/ratelimit.js +57 -0
  99. package/dist/security/secrets.d.ts +75 -0
  100. package/dist/security/secrets.js +365 -0
  101. package/dist/sensors/actionability.d.ts +81 -0
  102. package/dist/sensors/actionability.js +303 -0
  103. package/dist/sensors/guard.d.ts +25 -0
  104. package/dist/sensors/guard.js +168 -0
  105. package/dist/sensors/mutations.d.ts +23 -0
  106. package/dist/sensors/mutations.js +93 -0
  107. package/dist/sensors/network.d.ts +67 -0
  108. package/dist/sensors/network.js +193 -0
  109. package/dist/sensors/operation_monitor.d.ts +80 -0
  110. package/dist/sensors/operation_monitor.js +203 -0
  111. package/dist/sensors/pacing.d.ts +44 -0
  112. package/dist/sensors/pacing.js +111 -0
  113. package/dist/sensors/thoughts.d.ts +2 -0
  114. package/dist/sensors/thoughts.js +59 -0
  115. package/dist/sensors/wait.d.ts +12 -0
  116. package/dist/sensors/wait.js +44 -0
  117. package/dist/server.d.ts +7 -0
  118. package/dist/server.js +1687 -0
  119. package/dist/state/agent.d.ts +36 -0
  120. package/dist/state/agent.js +101 -0
  121. package/dist/state/cache.d.ts +16 -0
  122. package/dist/state/cache.js +92 -0
  123. package/dist/state/cache.test.d.ts +2 -0
  124. package/dist/state/cache.test.js +76 -0
  125. package/dist/state/delta.d.ts +71 -0
  126. package/dist/state/delta.js +234 -0
  127. package/dist/state/delta.test.d.ts +2 -0
  128. package/dist/state/delta.test.js +279 -0
  129. package/dist/state/groups.d.ts +15 -0
  130. package/dist/state/groups.js +80 -0
  131. package/dist/state/session.d.ts +30 -0
  132. package/dist/state/session.js +155 -0
  133. package/dist/tools/accessibility.d.ts +14 -0
  134. package/dist/tools/accessibility.js +32 -0
  135. package/dist/tools/ask.d.ts +14 -0
  136. package/dist/tools/ask.js +49 -0
  137. package/dist/tools/automation.d.ts +36 -0
  138. package/dist/tools/automation.js +218 -0
  139. package/dist/tools/browser_detect.d.ts +13 -0
  140. package/dist/tools/browser_detect.js +73 -0
  141. package/dist/tools/burst.d.ts +35 -0
  142. package/dist/tools/burst.js +344 -0
  143. package/dist/tools/canvas.d.ts +25 -0
  144. package/dist/tools/canvas.js +132 -0
  145. package/dist/tools/captcha.d.ts +38 -0
  146. package/dist/tools/captcha.js +209 -0
  147. package/dist/tools/click.d.ts +36 -0
  148. package/dist/tools/click.js +561 -0
  149. package/dist/tools/click_and_wait.d.ts +33 -0
  150. package/dist/tools/click_and_wait.js +149 -0
  151. package/dist/tools/clipboard.d.ts +29 -0
  152. package/dist/tools/clipboard.js +279 -0
  153. package/dist/tools/console.d.ts +21 -0
  154. package/dist/tools/console.js +107 -0
  155. package/dist/tools/cookies.d.ts +67 -0
  156. package/dist/tools/cookies.js +121 -0
  157. package/dist/tools/design_extract.d.ts +109 -0
  158. package/dist/tools/design_extract.js +641 -0
  159. package/dist/tools/dialog.d.ts +24 -0
  160. package/dist/tools/dialog.js +109 -0
  161. package/dist/tools/drag.d.ts +15 -0
  162. package/dist/tools/drag.js +29 -0
  163. package/dist/tools/emulation.d.ts +33 -0
  164. package/dist/tools/emulation.js +69 -0
  165. package/dist/tools/eval.d.ts +20 -0
  166. package/dist/tools/eval.js +92 -0
  167. package/dist/tools/fill.d.ts +41 -0
  168. package/dist/tools/fill.js +667 -0
  169. package/dist/tools/find.d.ts +30 -0
  170. package/dist/tools/find.js +60 -0
  171. package/dist/tools/geo.d.ts +19 -0
  172. package/dist/tools/geo.js +29 -0
  173. package/dist/tools/github/analyze.d.ts +4 -0
  174. package/dist/tools/github/analyze.js +113 -0
  175. package/dist/tools/github/cache.d.ts +21 -0
  176. package/dist/tools/github/cache.js +28 -0
  177. package/dist/tools/github/index.d.ts +4 -0
  178. package/dist/tools/github/index.js +4 -0
  179. package/dist/tools/github/issues.d.ts +9 -0
  180. package/dist/tools/github/issues.js +88 -0
  181. package/dist/tools/github/pr.d.ts +5 -0
  182. package/dist/tools/github/pr.js +88 -0
  183. package/dist/tools/github/rateLimiter.d.ts +4 -0
  184. package/dist/tools/github/rateLimiter.js +46 -0
  185. package/dist/tools/github/read.d.ts +34 -0
  186. package/dist/tools/github/read.js +74 -0
  187. package/dist/tools/github/repoContext.d.ts +20 -0
  188. package/dist/tools/github/repoContext.js +129 -0
  189. package/dist/tools/highlight.d.ts +20 -0
  190. package/dist/tools/highlight.js +140 -0
  191. package/dist/tools/hover.d.ts +19 -0
  192. package/dist/tools/hover.js +57 -0
  193. package/dist/tools/inspect.d.ts +17 -0
  194. package/dist/tools/inspect.js +125 -0
  195. package/dist/tools/integration.test.d.ts +2 -0
  196. package/dist/tools/integration.test.js +96 -0
  197. package/dist/tools/key.d.ts +25 -0
  198. package/dist/tools/key.js +173 -0
  199. package/dist/tools/macro_compiler.d.ts +7 -0
  200. package/dist/tools/macro_compiler.js +82 -0
  201. package/dist/tools/mouse.d.ts +21 -0
  202. package/dist/tools/mouse.js +62 -0
  203. package/dist/tools/navigate.d.ts +16 -0
  204. package/dist/tools/navigate.js +188 -0
  205. package/dist/tools/network_intercept.d.ts +17 -0
  206. package/dist/tools/network_intercept.js +151 -0
  207. package/dist/tools/office/index.d.ts +9 -0
  208. package/dist/tools/office/index.js +7 -0
  209. package/dist/tools/office/list.d.ts +15 -0
  210. package/dist/tools/office/list.js +41 -0
  211. package/dist/tools/office/parse.d.ts +20 -0
  212. package/dist/tools/office/parse.js +172 -0
  213. package/dist/tools/pdf.d.ts +22 -0
  214. package/dist/tools/pdf.js +42 -0
  215. package/dist/tools/performance.d.ts +12 -0
  216. package/dist/tools/performance.js +47 -0
  217. package/dist/tools/performance_test.d.ts +70 -0
  218. package/dist/tools/performance_test.js +128 -0
  219. package/dist/tools/plan.d.ts +39 -0
  220. package/dist/tools/plan.js +161 -0
  221. package/dist/tools/pointer.d.ts +35 -0
  222. package/dist/tools/pointer.js +522 -0
  223. package/dist/tools/read.d.ts +30 -0
  224. package/dist/tools/read.js +488 -0
  225. package/dist/tools/recording.d.ts +45 -0
  226. package/dist/tools/recording.js +124 -0
  227. package/dist/tools/response.d.ts +129 -0
  228. package/dist/tools/response.js +5 -0
  229. package/dist/tools/scrape.d.ts +24 -0
  230. package/dist/tools/scrape.js +140 -0
  231. package/dist/tools/screenshot.d.ts +40 -0
  232. package/dist/tools/screenshot.js +79 -0
  233. package/dist/tools/scroll.d.ts +23 -0
  234. package/dist/tools/scroll.js +196 -0
  235. package/dist/tools/select.d.ts +13 -0
  236. package/dist/tools/select.js +217 -0
  237. package/dist/tools/snapshot.d.ts +27 -0
  238. package/dist/tools/snapshot.js +99 -0
  239. package/dist/tools/storage.d.ts +18 -0
  240. package/dist/tools/storage.js +29 -0
  241. package/dist/tools/tabs.d.ts +77 -0
  242. package/dist/tools/tabs.js +342 -0
  243. package/dist/tools/thought.d.ts +16 -0
  244. package/dist/tools/thought.js +79 -0
  245. package/dist/tools/type.d.ts +65 -0
  246. package/dist/tools/type.js +1061 -0
  247. package/dist/tools/type_secret.d.ts +23 -0
  248. package/dist/tools/type_secret.js +229 -0
  249. package/dist/tools/upload.d.ts +19 -0
  250. package/dist/tools/upload.js +103 -0
  251. package/dist/tools/video_compiler.d.ts +24 -0
  252. package/dist/tools/video_compiler.js +199 -0
  253. package/dist/tools/viewport.d.ts +44 -0
  254. package/dist/tools/viewport.js +230 -0
  255. package/dist/tools/wait.d.ts +19 -0
  256. package/dist/tools/wait.js +202 -0
  257. package/dist/tools/wait_for.d.ts +23 -0
  258. package/dist/tools/wait_for.js +30 -0
  259. package/dist/tools/windows.d.ts +22 -0
  260. package/dist/tools/windows.js +57 -0
  261. package/dist/ui/index.d.ts +1 -0
  262. package/dist/ui/index.js +12 -0
  263. package/docs/DEV_MODE.md +68 -0
  264. package/docs/DOCUMENTATION.md +532 -0
  265. package/docs/PLUGINS.md +538 -0
  266. package/docs/TOOLS.md +104 -0
  267. package/docs/dev_plugin.md +310 -0
  268. package/ext-dist/background.js +467 -0
  269. package/ext-dist/content.js +3089 -0
  270. package/ext-dist/dashboard.html +583 -0
  271. package/ext-dist/hud/hud.js +73 -0
  272. package/ext-dist/hud/index.html +20 -0
  273. package/ext-dist/manifest.json +77 -0
  274. package/ext-dist/sidepanel/sidepanel.js +25 -0
  275. package/ext-dist/sidepanel.html +547 -0
  276. package/ext-dist/styles.css +218 -0
  277. package/icon.png +0 -0
  278. package/mcp.json +796 -0
  279. package/package.json +113 -0
  280. package/server.json +23 -0
  281. package/smithery.yaml +11 -0
@@ -0,0 +1,365 @@
1
+ /**
2
+ * Weavetab — security/secrets.ts
3
+ * Structured secret store with per-key domain scoping and chmod 600 enforcement.
4
+ * Replaces the old EnvSecretProvider (.env-based) with a JSON secret store.
5
+ *
6
+ * Storage locations:
7
+ * - Global default: ~/.weavetab/secrets.json
8
+ *
9
+ * File format:
10
+ * {
11
+ * "version": 1,
12
+ * "secrets": {
13
+ * "MY_KEY": {
14
+ * "value": "s3cr3t",
15
+ * "domains": ["github.com", "gitlab.com"],
16
+ * "createdAt": "2024-01-01T00:00:00Z",
17
+ * "updatedAt": "2024-01-01T00:00:00Z"
18
+ * }
19
+ * }
20
+ * }
21
+ *
22
+ * Security guarantees:
23
+ * - File created with mode 0o600 (owner-read/write only).
24
+ * - On POSIX: startup hard-fails if file exists with wrong permissions.
25
+ * - On Windows: warning only (NTFS ACLs, not POSIX mode bits).
26
+ * - Resolved value lives in-process only; never returned in any tool_result.
27
+ * - `domains: []` means "any domain" — not recommended for sensitive credentials.
28
+ *
29
+ * Weavetab by fy2ne
30
+ */
31
+ import * as fs from "node:fs";
32
+ import * as path from "node:path";
33
+ import * as os from "node:os";
34
+ import * as crypto from "node:crypto";
35
+ // Encryption key derivation for secrets at rest
36
+ const ENCRYPTION_KEY_FILE = path.join(os.homedir(), ".weavetab-system", "secrets-key");
37
+ let encryptionKey = null;
38
+ function getEncryptionKey() {
39
+ if (encryptionKey)
40
+ return encryptionKey;
41
+ try {
42
+ if (fs.existsSync(ENCRYPTION_KEY_FILE)) {
43
+ const keyData = fs.readFileSync(ENCRYPTION_KEY_FILE, "utf-8").trim();
44
+ encryptionKey = Buffer.from(keyData, "hex");
45
+ }
46
+ else {
47
+ // Generate a new encryption key
48
+ encryptionKey = crypto.randomBytes(32); // 256-bit key
49
+ const dir = path.dirname(ENCRYPTION_KEY_FILE);
50
+ if (!fs.existsSync(dir))
51
+ fs.mkdirSync(dir, { recursive: true });
52
+ fs.writeFileSync(ENCRYPTION_KEY_FILE, encryptionKey.toString("hex"), { mode: 0o600 });
53
+ }
54
+ }
55
+ catch {
56
+ // Fallback to a derived key if file operations fail
57
+ const machineId = os.hostname() + os.platform() + os.arch();
58
+ encryptionKey = crypto.createHash("sha256").update(machineId).digest();
59
+ }
60
+ return encryptionKey;
61
+ }
62
+ function encrypt(plaintext) {
63
+ const key = getEncryptionKey();
64
+ const iv = crypto.randomBytes(16);
65
+ const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
66
+ let encrypted = cipher.update(plaintext, "utf8", "hex");
67
+ encrypted += cipher.final("hex");
68
+ const authTag = cipher.getAuthTag();
69
+ // Combine IV + authTag + encrypted data
70
+ return iv.toString("hex") + authTag.toString("hex") + encrypted;
71
+ }
72
+ function decrypt(ciphertext) {
73
+ const key = getEncryptionKey();
74
+ // Extract IV (first 32 hex chars = 16 bytes)
75
+ const iv = Buffer.from(ciphertext.slice(0, 32), "hex");
76
+ // Extract authTag (next 32 hex chars = 16 bytes)
77
+ const authTag = Buffer.from(ciphertext.slice(32, 64), "hex");
78
+ // Extract encrypted data
79
+ const encrypted = ciphertext.slice(64);
80
+ const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv);
81
+ decipher.setAuthTag(authTag);
82
+ let decrypted = decipher.update(encrypted, "hex", "utf8");
83
+ decrypted += decipher.final("utf8");
84
+ return decrypted;
85
+ }
86
+ import { GLOBAL_CONFIG_DIR } from "../config/loader.js";
87
+ // ─── Path resolution ──────────────────────────────────────────────────────────
88
+ export const GLOBAL_SECRETS_PATH = path.join(GLOBAL_CONFIG_DIR, "secrets.json");
89
+ function resolveSecretsPath() {
90
+ return GLOBAL_SECRETS_PATH;
91
+ }
92
+ // ─── Permission guard ─────────────────────────────────────────────────────────
93
+ const IS_WINDOWS = os.platform() === "win32";
94
+ const REQUIRED_MODE = 0o600;
95
+ /**
96
+ * Enforce chmod 600 on the secrets file.
97
+ * Hard-fails on POSIX if the file is accessible by anyone other than the owner.
98
+ * Warns on Windows (NTFS doesn't map cleanly to POSIX mode bits).
99
+ */
100
+ function enforceFilePermissions(filePath) {
101
+ if (!fs.existsSync(filePath))
102
+ return;
103
+ if (IS_WINDOWS) {
104
+ try {
105
+ const { execFileSync } = require('child_process');
106
+ const username = process.env.USERNAME || '';
107
+ // Validate username against a strict allowlist pattern, then pass filePath
108
+ // and the grant argument as separate argv tokens to execFile (no shell
109
+ // involved) so the username can never be injected into a command string.
110
+ if (!/^[a-zA-Z0-9._-]+$/.test(username)) {
111
+ process.stderr.write(`[Weavetab Secrets] ⚠ Refusing to set ACLs: USERNAME contains unsafe characters.\n`);
112
+ return;
113
+ }
114
+ // Pass each argument as a separate element — no shell, no injection possible
115
+ execFileSync('icacls', [filePath, '/inheritance:r', '/grant:r', `${username}:F`], { stdio: 'ignore' });
116
+ }
117
+ catch (e) {
118
+ process.stderr.write(`[Weavetab Secrets] ⚠ Failed to enforce Windows ACLs on "${filePath}".\n`);
119
+ }
120
+ return;
121
+ }
122
+ try {
123
+ const stats = fs.statSync(filePath);
124
+ const mode = stats.mode & 0o777;
125
+ if (mode !== REQUIRED_MODE) {
126
+ throw new Error(`[Weavetab Secrets] ✗ SECURITY ERROR: Secrets file "${filePath}" has unsafe permissions ` +
127
+ `(${mode.toString(8)}, expected 600). ` +
128
+ `Fix with: chmod 600 "${filePath}"`);
129
+ }
130
+ }
131
+ catch (e) {
132
+ if (e.message.includes("SECURITY ERROR"))
133
+ throw e;
134
+ // stat failed for some other reason — skip
135
+ }
136
+ }
137
+ function applyFilePermissions(filePath) {
138
+ if (IS_WINDOWS) {
139
+ enforceFilePermissions(filePath);
140
+ return;
141
+ }
142
+ try {
143
+ fs.chmodSync(filePath, REQUIRED_MODE);
144
+ }
145
+ catch { /* ignore — might be on a FS that doesn't support chmod */ }
146
+ }
147
+ // ─── File I/O ─────────────────────────────────────────────────────────────────
148
+ function readSecretsFile(filePath) {
149
+ enforceFilePermissions(filePath);
150
+ if (!fs.existsSync(filePath)) {
151
+ return { version: 1, secrets: {} };
152
+ }
153
+ try {
154
+ const raw = fs.readFileSync(filePath, "utf-8").trim();
155
+ if (!raw)
156
+ return { version: 1, secrets: {} };
157
+ const parsed = JSON.parse(raw);
158
+ if (typeof parsed !== "object" || parsed.version !== 1 || typeof parsed.secrets !== "object") {
159
+ throw new Error("Malformed secrets file");
160
+ }
161
+ // Decrypt secret values after reading
162
+ for (const key in parsed.secrets) {
163
+ if (parsed.secrets[key].value) {
164
+ try {
165
+ parsed.secrets[key].value = decrypt(parsed.secrets[key].value);
166
+ }
167
+ catch {
168
+ // If decryption fails, assume it's not encrypted (legacy data)
169
+ // and leave it as-is
170
+ }
171
+ }
172
+ }
173
+ return parsed;
174
+ }
175
+ catch (e) {
176
+ throw new Error(`[Weavetab Secrets] ✗ Failed to read secrets file "${filePath}": ${e.message}`);
177
+ }
178
+ }
179
+ function writeSecretsFile(filePath, data) {
180
+ const dir = path.dirname(filePath);
181
+ if (!fs.existsSync(dir)) {
182
+ fs.mkdirSync(dir, { recursive: true });
183
+ }
184
+ // Encrypt secret values before writing
185
+ const encryptedData = {
186
+ version: data.version,
187
+ secrets: {}
188
+ };
189
+ for (const key in data.secrets) {
190
+ encryptedData.secrets[key] = {
191
+ ...data.secrets[key],
192
+ value: encrypt(data.secrets[key].value)
193
+ };
194
+ }
195
+ const tmp = filePath + `.tmp.${process.pid}.${crypto.randomBytes(4).toString("hex")}`;
196
+ try {
197
+ fs.writeFileSync(tmp, JSON.stringify(encryptedData, null, 2), { encoding: "utf-8", mode: REQUIRED_MODE });
198
+ applyFilePermissions(tmp);
199
+ fs.renameSync(tmp, filePath);
200
+ applyFilePermissions(filePath);
201
+ }
202
+ catch (e) {
203
+ try {
204
+ if (fs.existsSync(tmp))
205
+ fs.unlinkSync(tmp);
206
+ }
207
+ catch { /* ignore */ }
208
+ throw e;
209
+ }
210
+ }
211
+ // ─── JsonSecretStore ──────────────────────────────────────────────────────────
212
+ class JsonSecretStoreImpl {
213
+ /**
214
+ * Resolve a secret value by key.
215
+ * Returns undefined if the key doesn't exist.
216
+ * Does NOT check domain — use checkDomain() for that before calling this.
217
+ */
218
+ async getSecret(key) {
219
+ const filePath = resolveSecretsPath();
220
+ const data = readSecretsFile(filePath);
221
+ const entry = data.secrets[key];
222
+ return entry?.value;
223
+ }
224
+ /**
225
+ * Check whether the given hostname is allowed for this key.
226
+ * Returns true if:
227
+ * - The key doesn't exist (will fail at getSecret time anyway)
228
+ * - The key's domains list is empty (wildcard — any domain)
229
+ * - The hostname matches one of the allowed domains (exact or suffix match)
230
+ */
231
+ checkDomain(key, hostname) {
232
+ const filePath = resolveSecretsPath();
233
+ const data = readSecretsFile(filePath);
234
+ const entry = data.secrets[key];
235
+ if (!entry)
236
+ return false; // key not found
237
+ if (entry.domains.length === 0)
238
+ return true; // wildcard
239
+ const h = hostname.toLowerCase();
240
+ return entry.domains.some(d => {
241
+ const allowed = d.toLowerCase();
242
+ return h === allowed || h.endsWith("." + allowed);
243
+ });
244
+ }
245
+ /**
246
+ * Get the allowed domains list for a key (for error messages).
247
+ * Never returns the value.
248
+ */
249
+ getDomains(key) {
250
+ const filePath = resolveSecretsPath();
251
+ const data = readSecretsFile(filePath);
252
+ const entry = data.secrets[key];
253
+ return entry ? entry.domains : null;
254
+ }
255
+ /**
256
+ * Store a secret. Value must be passed in-process — never as a CLI arg.
257
+ * domains: list of allowed hostnames. Empty = any domain (not recommended).
258
+ */
259
+ async setSecret(key, value, domains) {
260
+ if (!key || !key.trim())
261
+ throw new Error("Secret key cannot be empty");
262
+ if (!value)
263
+ throw new Error("Secret value cannot be empty");
264
+ const filePath = resolveSecretsPath();
265
+ const data = readSecretsFile(filePath);
266
+ const now = new Date().toISOString();
267
+ const existing = data.secrets[key];
268
+ data.secrets[key] = {
269
+ value,
270
+ domains: domains.map(d => d.toLowerCase().trim()).filter(Boolean),
271
+ createdAt: existing?.createdAt ?? now,
272
+ updatedAt: now,
273
+ };
274
+ writeSecretsFile(filePath, data);
275
+ }
276
+ /**
277
+ * List all secrets — returns key names, domains, and timestamps ONLY.
278
+ * Never returns values.
279
+ */
280
+ async listSecrets() {
281
+ const filePath = resolveSecretsPath();
282
+ const data = readSecretsFile(filePath);
283
+ return Object.entries(data.secrets).map(([key, entry]) => ({
284
+ key,
285
+ domains: entry.domains,
286
+ updatedAt: entry.updatedAt,
287
+ }));
288
+ }
289
+ /**
290
+ * Remove a secret by key. Returns true if the key existed and was removed.
291
+ */
292
+ async removeSecret(key) {
293
+ const filePath = resolveSecretsPath();
294
+ const data = readSecretsFile(filePath);
295
+ if (!data.secrets[key])
296
+ return false;
297
+ delete data.secrets[key];
298
+ writeSecretsFile(filePath, data);
299
+ return true;
300
+ }
301
+ /** Returns the active secrets file path for display purposes. */
302
+ getSecretsFilePath() {
303
+ return resolveSecretsPath();
304
+ }
305
+ }
306
+ export const secretStore = new JsonSecretStoreImpl();
307
+ // ─── Legacy shim — kept so other callers don't break ─────────────────────────
308
+ // @deprecated — use secretStore directly
309
+ /** @deprecated Use secretStore instead. */
310
+ export class EnvSecretProvider {
311
+ async getSecret(key) {
312
+ process.stderr.write(`[Weavetab Secrets] ⚠ EnvSecretProvider is deprecated. Falling back to JsonSecretStore.\n`);
313
+ return secretStore.getSecret(key);
314
+ }
315
+ }
316
+ // ─── SessionSecretTracker ─────────────────────────────────────────────────────
317
+ // Tracks which DOM backendNodeIds contain secrets so cross-tool redaction
318
+ // (walker, AX tree, delta) can redact them at read time without re-querying
319
+ // the secret store.
320
+ class SessionSecretTrackerImpl {
321
+ /** Maps sessionId (CDP targetId) → Set of backendNodeIds marked as secret. */
322
+ trackedNodes = new Map();
323
+ /** Maps sessionId (CDP targetId) → Set of plaintext secret strings injected. */
324
+ sessionSecrets = new Map();
325
+ markNodeAsSecret(sessionId, backendNodeId) {
326
+ let set = this.trackedNodes.get(sessionId);
327
+ if (!set) {
328
+ set = new Set();
329
+ this.trackedNodes.set(sessionId, set);
330
+ }
331
+ set.add(backendNodeId);
332
+ }
333
+ isNodeSecret(sessionId, backendNodeId) {
334
+ const set = this.trackedNodes.get(sessionId);
335
+ return set ? set.has(backendNodeId) : false;
336
+ }
337
+ registerSecretValue(sessionId, value) {
338
+ if (!value || value.length < 3)
339
+ return; // Don't redact tiny strings
340
+ let set = this.sessionSecrets.get(sessionId);
341
+ if (!set) {
342
+ set = new Set();
343
+ this.sessionSecrets.set(sessionId, set);
344
+ }
345
+ set.add(value);
346
+ }
347
+ redact(sessionId, text) {
348
+ if (!text || typeof text !== 'string')
349
+ return text;
350
+ const set = this.sessionSecrets.get(sessionId);
351
+ if (!set || set.size === 0)
352
+ return text;
353
+ let redacted = text;
354
+ for (const secret of set) {
355
+ // Fast path string replace all instances
356
+ redacted = redacted.split(secret).join('[REDACTED]');
357
+ }
358
+ return redacted;
359
+ }
360
+ clearSession(sessionId) {
361
+ this.trackedNodes.delete(sessionId);
362
+ this.sessionSecrets.delete(sessionId);
363
+ }
364
+ }
365
+ export const SessionSecretTracker = new SessionSecretTrackerImpl();
@@ -0,0 +1,81 @@
1
+ /**
2
+ * src/sensors/actionability.ts
3
+ *
4
+ * Playwright-Strategy Actionability Engine for WeaveTab
5
+ * ─────────────────────────────────────────────────────
6
+ * Implements every correctness principle from playwright-strategy.md:
7
+ *
8
+ * 1. RAF-stability gate — element must not be animating before we touch it
9
+ * 2. Strict hit-target — elementsFromPoint at the computed click point must
10
+ * confirm the target is on top (or the target's child).
11
+ * 3. WeaveTab transparency — WeaveTab's own UI (data-wt-ignore nodes) is excluded
12
+ * from the hit stack so the tool never blocks itself.
13
+ * 4. 4-way scroll rotation — escape sticky headers / overlays by cycling four
14
+ * scrollIntoView alignments before giving up.
15
+ * 5. Per-action backoff — [0, 20, 100, 100, 500] ms retry on transient failures.
16
+ * 6. Rich blocker report — on permanent occlusion, return the blocking element's
17
+ * description so the model can reason about it.
18
+ *
19
+ * Usage:
20
+ * import { verifyActionability } from "../sensors/actionability.js";
21
+ * const result = await verifyActionability(session, selector);
22
+ * if (result.status !== "ACTIONABLE") { ... }
23
+ */
24
+ import CDP from "chrome-remote-interface";
25
+ export type ActionabilityStatus = "ACTIONABLE" | "NOT_FOUND" | "INVISIBLE" | "UNSTABLE" | "OCCLUDED" | "FINGERPRINT_MISMATCH";
26
+ export interface OccluderInfo {
27
+ tag: string;
28
+ id: string;
29
+ className: string;
30
+ ariaLabel: string | null;
31
+ isWeavetabOwned: boolean;
32
+ }
33
+ export interface ActionabilityResult {
34
+ status: ActionabilityStatus;
35
+ /** Computed click center in viewport coordinates. */
36
+ x?: number;
37
+ y?: number;
38
+ /** Populated only when status === "OCCLUDED". */
39
+ occludedBy?: OccluderInfo;
40
+ /** True if the blocker was WeaveTab's own UI and can be ignored. */
41
+ blockerIsWeaveTab?: boolean;
42
+ }
43
+ export interface ActionabilityOptions {
44
+ /**
45
+ * Maximum time (ms) to wait for stability before giving up.
46
+ * Default: 600ms.
47
+ */
48
+ stabilityTimeoutMs?: number;
49
+ /**
50
+ * Optional fingerprint to verify the element hasn't been replaced by
51
+ * a framework re-render. Checked once before anything else.
52
+ */
53
+ fingerprint?: {
54
+ tag?: string;
55
+ role?: string;
56
+ };
57
+ /**
58
+ * Total time budget across all retry attempts.
59
+ * Default: 2000ms.
60
+ */
61
+ totalTimeoutMs?: number;
62
+ }
63
+ /**
64
+ * Full Playwright-strategy actionability verification.
65
+ *
66
+ * Runs up to `BACKOFF.length` attempts with 4-way scroll rotation,
67
+ * RAF stability gating, WeaveTab-transparent hit-target checking,
68
+ * and per-attempt retry backoff.
69
+ *
70
+ * Returns `status: "ACTIONABLE"` with computed (x, y) when safe to dispatch,
71
+ * or a failure status with an optional `occludedBy` description.
72
+ */
73
+ export declare function verifyActionability(session: CDP.Client, selector: string, opts?: ActionabilityOptions): Promise<ActionabilityResult>;
74
+ /**
75
+ * Quick check: is an element at these coords actually blocked by a non-WT overlay?
76
+ * Used for lightweight pre-dispatch validation inside click.ts.
77
+ */
78
+ export declare function quickHitCheck(session: CDP.Client, x: number, y: number, selectorOfTarget: string): Promise<{
79
+ clear: boolean;
80
+ blockerDescription?: string;
81
+ }>;