@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.
- package/AGENTS.md +48 -0
- package/ARCHITECTURE.md +590 -0
- package/LICENSE +631 -0
- package/README.md +909 -0
- package/SECURITY.md +196 -0
- package/TROUBLESHOOTING.md +365 -0
- package/dist/anticaptcha/detector.d.ts +49 -0
- package/dist/anticaptcha/detector.js +465 -0
- package/dist/anticaptcha/integration.d.ts +36 -0
- package/dist/anticaptcha/integration.js +123 -0
- package/dist/anticaptcha/solver.d.ts +27 -0
- package/dist/anticaptcha/solver.js +552 -0
- package/dist/audit/logger.d.ts +13 -0
- package/dist/audit/logger.js +177 -0
- package/dist/audit/telemetry.d.ts +36 -0
- package/dist/audit/telemetry.js +162 -0
- package/dist/cdp/bridge.d.ts +16 -0
- package/dist/cdp/bridge.js +826 -0
- package/dist/cdp/confirm.d.ts +26 -0
- package/dist/cdp/confirm.js +79 -0
- package/dist/cdp/connector.d.ts +37 -0
- package/dist/cdp/connector.js +661 -0
- package/dist/cdp/extension.d.ts +36 -0
- package/dist/cdp/extension.js +118 -0
- package/dist/cdp/finder.d.ts +1 -0
- package/dist/cdp/finder.js +2 -0
- package/dist/cdp/helpers.d.ts +11 -0
- package/dist/cdp/helpers.js +195 -0
- package/dist/cdp/label_cache.d.ts +12 -0
- package/dist/cdp/label_cache.js +51 -0
- package/dist/cdp/lock.d.ts +33 -0
- package/dist/cdp/lock.js +206 -0
- package/dist/cdp/omni_broker.d.ts +17 -0
- package/dist/cdp/omni_broker.js +119 -0
- package/dist/cdp/profiles.d.ts +1 -0
- package/dist/cdp/profiles.js +87 -0
- package/dist/cdp/pruner.d.ts +41 -0
- package/dist/cdp/pruner.js +263 -0
- package/dist/cdp/ref_cache.d.ts +101 -0
- package/dist/cdp/ref_cache.js +234 -0
- package/dist/cdp/retry.d.ts +5 -0
- package/dist/cdp/retry.js +39 -0
- package/dist/cdp/retry_enhanced.d.ts +44 -0
- package/dist/cdp/retry_enhanced.js +102 -0
- package/dist/cdp/security.d.ts +50 -0
- package/dist/cdp/security.js +237 -0
- package/dist/cdp/verify.d.ts +39 -0
- package/dist/cdp/verify.js +143 -0
- package/dist/cdp/walker.d.ts +91 -0
- package/dist/cdp/walker.js +896 -0
- package/dist/cdp/walker.test.d.ts +2 -0
- package/dist/cdp/walker.test.js +172 -0
- package/dist/cdp/websocket_transport.d.ts +15 -0
- package/dist/cdp/websocket_transport.js +118 -0
- package/dist/cli/plugin.d.ts +9 -0
- package/dist/cli/plugin.js +1134 -0
- package/dist/cli/updater.d.ts +1 -0
- package/dist/cli/updater.js +72 -0
- package/dist/config/loader.d.ts +120 -0
- package/dist/config/loader.js +704 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1248 -0
- package/dist/intelligence/hints.d.ts +11 -0
- package/dist/intelligence/hints.js +39 -0
- package/dist/intelligence/intent.d.ts +33 -0
- package/dist/intelligence/intent.js +171 -0
- package/dist/intelligence/sessionMemory.d.ts +66 -0
- package/dist/intelligence/sessionMemory.js +284 -0
- package/dist/intelligence/sessionMemory.test.d.ts +2 -0
- package/dist/intelligence/sessionMemory.test.js +62 -0
- package/dist/intelligence/trail.d.ts +64 -0
- package/dist/intelligence/trail.js +233 -0
- package/dist/intelligence/trail.test.d.ts +2 -0
- package/dist/intelligence/trail.test.js +110 -0
- package/dist/intelligence/vision.d.ts +24 -0
- package/dist/intelligence/vision.js +151 -0
- package/dist/lifecycle/mission_manager.d.ts +16 -0
- package/dist/lifecycle/mission_manager.js +44 -0
- package/dist/overlay/bubble.d.ts +16 -0
- package/dist/overlay/bubble.js +37 -0
- package/dist/overlay/ghost.d.ts +35 -0
- package/dist/overlay/ghost.js +508 -0
- package/dist/overlay/injector.d.ts +22 -0
- package/dist/overlay/injector.js +198 -0
- package/dist/plugin/interface.d.ts +134 -0
- package/dist/plugin/interface.js +1 -0
- package/dist/plugin/loader.d.ts +18 -0
- package/dist/plugin/loader.js +155 -0
- package/dist/plugin/registry.d.ts +50 -0
- package/dist/plugin/registry.js +234 -0
- package/dist/security/blacklist.d.ts +24 -0
- package/dist/security/blacklist.js +80 -0
- package/dist/security/filter.d.ts +2 -0
- package/dist/security/filter.js +20 -0
- package/dist/security/guard.d.ts +8 -0
- package/dist/security/guard.js +35 -0
- package/dist/security/ratelimit.d.ts +11 -0
- package/dist/security/ratelimit.js +57 -0
- package/dist/security/secrets.d.ts +75 -0
- package/dist/security/secrets.js +365 -0
- package/dist/sensors/actionability.d.ts +81 -0
- package/dist/sensors/actionability.js +303 -0
- package/dist/sensors/guard.d.ts +25 -0
- package/dist/sensors/guard.js +168 -0
- package/dist/sensors/mutations.d.ts +23 -0
- package/dist/sensors/mutations.js +93 -0
- package/dist/sensors/network.d.ts +67 -0
- package/dist/sensors/network.js +193 -0
- package/dist/sensors/operation_monitor.d.ts +80 -0
- package/dist/sensors/operation_monitor.js +203 -0
- package/dist/sensors/pacing.d.ts +44 -0
- package/dist/sensors/pacing.js +111 -0
- package/dist/sensors/thoughts.d.ts +2 -0
- package/dist/sensors/thoughts.js +59 -0
- package/dist/sensors/wait.d.ts +12 -0
- package/dist/sensors/wait.js +44 -0
- package/dist/server.d.ts +7 -0
- package/dist/server.js +1687 -0
- package/dist/state/agent.d.ts +36 -0
- package/dist/state/agent.js +101 -0
- package/dist/state/cache.d.ts +16 -0
- package/dist/state/cache.js +92 -0
- package/dist/state/cache.test.d.ts +2 -0
- package/dist/state/cache.test.js +76 -0
- package/dist/state/delta.d.ts +71 -0
- package/dist/state/delta.js +234 -0
- package/dist/state/delta.test.d.ts +2 -0
- package/dist/state/delta.test.js +279 -0
- package/dist/state/groups.d.ts +15 -0
- package/dist/state/groups.js +80 -0
- package/dist/state/session.d.ts +30 -0
- package/dist/state/session.js +155 -0
- package/dist/tools/accessibility.d.ts +14 -0
- package/dist/tools/accessibility.js +32 -0
- package/dist/tools/ask.d.ts +14 -0
- package/dist/tools/ask.js +49 -0
- package/dist/tools/automation.d.ts +36 -0
- package/dist/tools/automation.js +218 -0
- package/dist/tools/browser_detect.d.ts +13 -0
- package/dist/tools/browser_detect.js +73 -0
- package/dist/tools/burst.d.ts +35 -0
- package/dist/tools/burst.js +344 -0
- package/dist/tools/canvas.d.ts +25 -0
- package/dist/tools/canvas.js +132 -0
- package/dist/tools/captcha.d.ts +38 -0
- package/dist/tools/captcha.js +209 -0
- package/dist/tools/click.d.ts +36 -0
- package/dist/tools/click.js +561 -0
- package/dist/tools/click_and_wait.d.ts +33 -0
- package/dist/tools/click_and_wait.js +149 -0
- package/dist/tools/clipboard.d.ts +29 -0
- package/dist/tools/clipboard.js +279 -0
- package/dist/tools/console.d.ts +21 -0
- package/dist/tools/console.js +107 -0
- package/dist/tools/cookies.d.ts +67 -0
- package/dist/tools/cookies.js +121 -0
- package/dist/tools/design_extract.d.ts +109 -0
- package/dist/tools/design_extract.js +641 -0
- package/dist/tools/dialog.d.ts +24 -0
- package/dist/tools/dialog.js +109 -0
- package/dist/tools/drag.d.ts +15 -0
- package/dist/tools/drag.js +29 -0
- package/dist/tools/emulation.d.ts +33 -0
- package/dist/tools/emulation.js +69 -0
- package/dist/tools/eval.d.ts +20 -0
- package/dist/tools/eval.js +92 -0
- package/dist/tools/fill.d.ts +41 -0
- package/dist/tools/fill.js +667 -0
- package/dist/tools/find.d.ts +30 -0
- package/dist/tools/find.js +60 -0
- package/dist/tools/geo.d.ts +19 -0
- package/dist/tools/geo.js +29 -0
- package/dist/tools/github/analyze.d.ts +4 -0
- package/dist/tools/github/analyze.js +113 -0
- package/dist/tools/github/cache.d.ts +21 -0
- package/dist/tools/github/cache.js +28 -0
- package/dist/tools/github/index.d.ts +4 -0
- package/dist/tools/github/index.js +4 -0
- package/dist/tools/github/issues.d.ts +9 -0
- package/dist/tools/github/issues.js +88 -0
- package/dist/tools/github/pr.d.ts +5 -0
- package/dist/tools/github/pr.js +88 -0
- package/dist/tools/github/rateLimiter.d.ts +4 -0
- package/dist/tools/github/rateLimiter.js +46 -0
- package/dist/tools/github/read.d.ts +34 -0
- package/dist/tools/github/read.js +74 -0
- package/dist/tools/github/repoContext.d.ts +20 -0
- package/dist/tools/github/repoContext.js +129 -0
- package/dist/tools/highlight.d.ts +20 -0
- package/dist/tools/highlight.js +140 -0
- package/dist/tools/hover.d.ts +19 -0
- package/dist/tools/hover.js +57 -0
- package/dist/tools/inspect.d.ts +17 -0
- package/dist/tools/inspect.js +125 -0
- package/dist/tools/integration.test.d.ts +2 -0
- package/dist/tools/integration.test.js +96 -0
- package/dist/tools/key.d.ts +25 -0
- package/dist/tools/key.js +173 -0
- package/dist/tools/macro_compiler.d.ts +7 -0
- package/dist/tools/macro_compiler.js +82 -0
- package/dist/tools/mouse.d.ts +21 -0
- package/dist/tools/mouse.js +62 -0
- package/dist/tools/navigate.d.ts +16 -0
- package/dist/tools/navigate.js +188 -0
- package/dist/tools/network_intercept.d.ts +17 -0
- package/dist/tools/network_intercept.js +151 -0
- package/dist/tools/office/index.d.ts +9 -0
- package/dist/tools/office/index.js +7 -0
- package/dist/tools/office/list.d.ts +15 -0
- package/dist/tools/office/list.js +41 -0
- package/dist/tools/office/parse.d.ts +20 -0
- package/dist/tools/office/parse.js +172 -0
- package/dist/tools/pdf.d.ts +22 -0
- package/dist/tools/pdf.js +42 -0
- package/dist/tools/performance.d.ts +12 -0
- package/dist/tools/performance.js +47 -0
- package/dist/tools/performance_test.d.ts +70 -0
- package/dist/tools/performance_test.js +128 -0
- package/dist/tools/plan.d.ts +39 -0
- package/dist/tools/plan.js +161 -0
- package/dist/tools/pointer.d.ts +35 -0
- package/dist/tools/pointer.js +522 -0
- package/dist/tools/read.d.ts +30 -0
- package/dist/tools/read.js +488 -0
- package/dist/tools/recording.d.ts +45 -0
- package/dist/tools/recording.js +124 -0
- package/dist/tools/response.d.ts +129 -0
- package/dist/tools/response.js +5 -0
- package/dist/tools/scrape.d.ts +24 -0
- package/dist/tools/scrape.js +140 -0
- package/dist/tools/screenshot.d.ts +40 -0
- package/dist/tools/screenshot.js +79 -0
- package/dist/tools/scroll.d.ts +23 -0
- package/dist/tools/scroll.js +196 -0
- package/dist/tools/select.d.ts +13 -0
- package/dist/tools/select.js +217 -0
- package/dist/tools/snapshot.d.ts +27 -0
- package/dist/tools/snapshot.js +99 -0
- package/dist/tools/storage.d.ts +18 -0
- package/dist/tools/storage.js +29 -0
- package/dist/tools/tabs.d.ts +77 -0
- package/dist/tools/tabs.js +342 -0
- package/dist/tools/thought.d.ts +16 -0
- package/dist/tools/thought.js +79 -0
- package/dist/tools/type.d.ts +65 -0
- package/dist/tools/type.js +1061 -0
- package/dist/tools/type_secret.d.ts +23 -0
- package/dist/tools/type_secret.js +229 -0
- package/dist/tools/upload.d.ts +19 -0
- package/dist/tools/upload.js +103 -0
- package/dist/tools/video_compiler.d.ts +24 -0
- package/dist/tools/video_compiler.js +199 -0
- package/dist/tools/viewport.d.ts +44 -0
- package/dist/tools/viewport.js +230 -0
- package/dist/tools/wait.d.ts +19 -0
- package/dist/tools/wait.js +202 -0
- package/dist/tools/wait_for.d.ts +23 -0
- package/dist/tools/wait_for.js +30 -0
- package/dist/tools/windows.d.ts +22 -0
- package/dist/tools/windows.js +57 -0
- package/dist/ui/index.d.ts +1 -0
- package/dist/ui/index.js +12 -0
- package/docs/DEV_MODE.md +68 -0
- package/docs/DOCUMENTATION.md +532 -0
- package/docs/PLUGINS.md +538 -0
- package/docs/TOOLS.md +104 -0
- package/docs/dev_plugin.md +310 -0
- package/ext-dist/background.js +467 -0
- package/ext-dist/content.js +3089 -0
- package/ext-dist/dashboard.html +583 -0
- package/ext-dist/hud/hud.js +73 -0
- package/ext-dist/hud/index.html +20 -0
- package/ext-dist/manifest.json +77 -0
- package/ext-dist/sidepanel/sidepanel.js +25 -0
- package/ext-dist/sidepanel.html +547 -0
- package/ext-dist/styles.css +218 -0
- package/icon.png +0 -0
- package/mcp.json +796 -0
- package/package.json +113 -0
- package/server.json +23 -0
- 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
|
+
}>;
|