machine-bridge-mcp 0.6.2 → 0.8.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.
@@ -0,0 +1,177 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { existsSync, lstatSync, readFileSync, readdirSync } from "node:fs";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
7
+ const selfPath = "scripts/privacy-check.mjs";
8
+ const candidates = collectCandidateFiles(root);
9
+ const denylist = loadDenylist(path.join(root, ".privacy-denylist"));
10
+ const findings = [];
11
+
12
+ for (const relativePath of candidates) {
13
+ if (relativePath === ".privacy-denylist") continue;
14
+ scanDenylistPath(relativePath, denylist, findings);
15
+ const fullPath = path.join(root, relativePath);
16
+ let info;
17
+ try { info = lstatSync(fullPath); } catch { continue; }
18
+ if (info.isSymbolicLink()) {
19
+ findings.push({ path: relativePath, line: 1, rule: "symbolic link in publication surface" });
20
+ continue;
21
+ }
22
+ if (!info.isFile()) continue;
23
+ let buffer;
24
+ try { buffer = readFileSync(fullPath); } catch { continue; }
25
+ if (buffer.length > 5 * 1024 * 1024) {
26
+ findings.push({ path: relativePath, line: 1, rule: "file exceeds privacy scanner size limit and requires manual review" });
27
+ continue;
28
+ }
29
+ if (buffer.includes(0)) {
30
+ findings.push({ path: relativePath, line: 1, rule: "binary file in publication surface requires manual review" });
31
+ continue;
32
+ }
33
+ let text;
34
+ try { text = new TextDecoder("utf-8", { fatal: true }).decode(buffer); } catch {
35
+ findings.push({ path: relativePath, line: 1, rule: "non-UTF-8 file in publication surface requires manual review" });
36
+ continue;
37
+ }
38
+ if (relativePath !== selfPath) scanBuiltIn(relativePath, text, findings);
39
+ scanDenylist(relativePath, text, denylist, findings);
40
+ }
41
+
42
+ if (findings.length) {
43
+ for (const finding of findings.slice(0, 100)) {
44
+ process.stderr.write(`${redactReportPath(finding.path, denylist)}:${finding.line}: ${finding.rule}\n`);
45
+ }
46
+ if (findings.length > 100) process.stderr.write(`... ${findings.length - 100} additional findings omitted\n`);
47
+ process.stderr.write("Privacy check failed. Replace private identifiers with synthetic examples or review the local denylist.\n");
48
+ process.exit(1);
49
+ }
50
+
51
+ process.stderr.write(`privacy check ok (${candidates.length} tracked/unignored files; ${denylist.length} local denylist entries)\n`);
52
+
53
+ function collectCandidateFiles(directory) {
54
+ try {
55
+ return execFileSync("git", ["-C", directory, "ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
56
+ encoding: "buffer",
57
+ maxBuffer: 32 * 1024 * 1024,
58
+ stdio: ["ignore", "pipe", "ignore"],
59
+ }).toString("utf8").split("\0").filter(Boolean).sort();
60
+ } catch {
61
+ const excluded = new Set([".git", ".wrangler", "node_modules"]);
62
+ const files = [];
63
+ const stack = [""];
64
+ while (stack.length) {
65
+ const relative = stack.pop();
66
+ const absolute = path.join(directory, relative);
67
+ let entries;
68
+ try { entries = readdirSync(absolute, { withFileTypes: true }); } catch { continue; }
69
+ for (const entry of entries) {
70
+ if (excluded.has(entry.name) || entry.name === ".privacy-denylist") continue;
71
+ const child = relative ? path.join(relative, entry.name) : entry.name;
72
+ if (entry.isDirectory()) stack.push(child);
73
+ else if (entry.isFile() || entry.isSymbolicLink()) files.push(child.split(path.sep).join("/"));
74
+ }
75
+ }
76
+ return files.sort();
77
+ }
78
+ }
79
+
80
+ function redactReportPath(relativePath, entries) {
81
+ let shown = String(relativePath);
82
+ for (const entry of entries) {
83
+ const expression = new RegExp(escapeRegExp(entry), "gi");
84
+ shown = shown.replace(expression, "<private>");
85
+ }
86
+ return shown;
87
+ }
88
+
89
+ function escapeRegExp(value) {
90
+ return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
91
+ }
92
+
93
+ function scanBuiltIn(relativePath, text, out) {
94
+ const rules = [
95
+ ["private key material", /-----BEGIN\s+(?:OPENSSH|RSA|EC|DSA)\s+PRIVATE\s+KEY-----/g],
96
+ ["AWS access key", /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/g],
97
+ ["GitHub access token", /\bgh[pousr]_[A-Za-z0-9_]{30,}\b/g],
98
+ ["API secret token", /\bsk-(?:proj-)?[A-Za-z0-9_-]{20,}\b/g],
99
+ ["absolute macOS/Linux home path", /(?:^|[\s"'=(:,])\/(?:Users|home)\/([^/\s"'<>]+)\//gm],
100
+ ["absolute Windows home path", /(?:^|[\s"'=(:,])\b[A-Za-z]:\\Users\\([^\\\s"'<>]+)\\/gm],
101
+ ];
102
+ for (const [rule, expression] of rules) {
103
+ for (const match of text.matchAll(expression)) {
104
+ if (rule.includes("home path") && isSyntheticUser(String(match[1] || ""))) continue;
105
+ out.push({ path: relativePath, line: lineNumber(text, match.index || 0), rule });
106
+ }
107
+ }
108
+ const sshLike = /\b[A-Za-z_][A-Za-z0-9._-]*@([A-Za-z0-9.-]+)\b/g;
109
+ for (const match of text.matchAll(sshLike)) {
110
+ if (isReservedExampleHost(match[1]) || !hasNearbySshContext(text, match.index || 0)) continue;
111
+ out.push({ path: relativePath, line: lineNumber(text, match.index || 0), rule: "non-example SSH user@host identifier" });
112
+ }
113
+ }
114
+
115
+ function scanDenylistPath(relativePath, entries, out) {
116
+ const lower = relativePath.toLocaleLowerCase("en-US");
117
+ for (const entry of entries) {
118
+ if (lower.includes(entry)) out.push({ path: relativePath, line: 1, rule: "local private-identifier denylist match in file path" });
119
+ }
120
+ }
121
+
122
+ function scanDenylist(relativePath, text, entries, out) {
123
+ const lower = text.toLocaleLowerCase("en-US");
124
+ for (const entry of entries) {
125
+ let offset = 0;
126
+ while ((offset = lower.indexOf(entry, offset)) !== -1) {
127
+ out.push({ path: relativePath, line: lineNumber(text, offset), rule: "local private-identifier denylist match" });
128
+ offset += Math.max(1, entry.length);
129
+ }
130
+ }
131
+ }
132
+
133
+ function loadDenylist(file) {
134
+ if (!existsSync(file)) return [];
135
+ let text;
136
+ try { text = readFileSync(file, "utf8"); } catch { return []; }
137
+ return [...new Set(text.split(/\r?\n/)
138
+ .map((line) => line.trim())
139
+ .filter((line) => line && !line.startsWith("#") && line.length >= 3)
140
+ .map((line) => line.toLocaleLowerCase("en-US")))];
141
+ }
142
+
143
+ function isSyntheticUser(value) {
144
+ return /^(?:user|username|example|test|name|home|runner|workspace|tmp|private|path|package)$/i.test(value)
145
+ || /^<[^>]+>$/.test(value);
146
+ }
147
+
148
+ function hasNearbySshContext(text, offset) {
149
+ const lineStart = Math.max(0, text.lastIndexOf("\n", offset - 1));
150
+ let start = lineStart;
151
+ for (let count = 0; count < 4 && start > 0; count += 1) start = Math.max(0, text.lastIndexOf("\n", start - 1));
152
+ let end = offset;
153
+ for (let count = 0; count < 4 && end < text.length; count += 1) {
154
+ const next = text.indexOf("\n", end + 1);
155
+ end = next === -1 ? text.length : next;
156
+ }
157
+ return /\b(?:ssh|scp|sftp)\b/i.test(text.slice(start, end));
158
+ }
159
+
160
+ function isReservedExampleHost(host) {
161
+ const value = String(host || "").toLowerCase().replace(/\.$/, "");
162
+ return value === "localhost"
163
+ || value === "127.0.0.1"
164
+ || value === "::1"
165
+ || value === "example"
166
+ || value.endsWith(".example")
167
+ || value.endsWith(".example.com")
168
+ || value.endsWith(".example.net")
169
+ || value.endsWith(".example.org")
170
+ || value.endsWith(".invalid");
171
+ }
172
+
173
+ function lineNumber(text, offset) {
174
+ let line = 1;
175
+ for (let index = 0; index < offset; index += 1) if (text.charCodeAt(index) === 10) line += 1;
176
+ return line;
177
+ }
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { execFileSync } from "node:child_process";
4
+ import { readFileSync } from "node:fs";
5
+
6
+ const VERSION_TAG = /^v(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/;
7
+
8
+ const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
9
+ const current = parseVersion(String(pkg.version || ""));
10
+ if (!current) fail("package.json contains an invalid version");
11
+
12
+ const tags = git(["tag", "--merged", "HEAD", "--sort=-version:refname", "--list", "v[0-9]*"])
13
+ .split("\n")
14
+ .map((value) => value.trim())
15
+ .filter((value) => VERSION_TAG.test(value));
16
+ if (!tags.length) {
17
+ process.stderr.write("release impact check skipped: no reachable version tag\n");
18
+ process.exit(0);
19
+ }
20
+
21
+ const latestTag = tags[0];
22
+ const latest = parseVersion(latestTag.slice(1));
23
+ const changed = new Set([
24
+ ...lines(git(["diff", "--name-only", latestTag, "--"])),
25
+ ...lines(git(["ls-files", "--others", "--exclude-standard"])),
26
+ ]);
27
+ const relevant = [...changed].sort();
28
+
29
+ if (!relevant.length) {
30
+ process.stderr.write(`release impact check ok: no release-relevant changes since ${latestTag}\n`);
31
+ process.exit(0);
32
+ }
33
+ if (compareVersions(current, latest) <= 0) {
34
+ fail(`release-relevant changes exist since ${latestTag}, but package.json is still ${pkg.version}; bump the npm version and add a CHANGELOG section before merging`);
35
+ }
36
+
37
+ const changelog = readFileSync(new URL("../CHANGELOG.md", import.meta.url), "utf8");
38
+ const heading = new RegExp(`^## ${escapeRegExp(pkg.version)}(?:\\s+-[^\\n]*)?$`, "m");
39
+ if (!heading.test(changelog)) fail(`CHANGELOG.md has no section for ${pkg.version}`);
40
+
41
+ process.stderr.write(`release impact check ok: ${relevant.length} release-relevant file(s) since ${latestTag}; next npm version ${pkg.version}\n`);
42
+
43
+ function git(args) {
44
+ try {
45
+ return execFileSync("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
46
+ } catch (error) {
47
+ const detail = String(error?.stderr || error?.message || error).trim();
48
+ fail(`git ${args.join(" ")} failed${detail ? `: ${detail}` : ""}`);
49
+ }
50
+ }
51
+
52
+ function lines(value) {
53
+ return value ? value.split("\n").map((item) => item.trim()).filter(Boolean) : [];
54
+ }
55
+
56
+ function parseVersion(value) {
57
+ const match = String(value).match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/);
58
+ return match ? { major: Number(match[1]), minor: Number(match[2]), patch: Number(match[3]), prerelease: match[4] || "" } : null;
59
+ }
60
+
61
+ function compareVersions(left, right) {
62
+ for (const key of ["major", "minor", "patch"]) {
63
+ if (left[key] !== right[key]) return left[key] - right[key];
64
+ }
65
+ if (left.prerelease === right.prerelease) return 0;
66
+ if (!left.prerelease) return 1;
67
+ if (!right.prerelease) return -1;
68
+ return left.prerelease.localeCompare(right.prerelease);
69
+ }
70
+
71
+ function escapeRegExp(value) {
72
+ return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
73
+ }
74
+
75
+ function fail(message) {
76
+ process.stderr.write(`release impact check failed: ${message}\n`);
77
+ process.exit(1);
78
+ }