dsh-api-runner 0.3.0 → 0.3.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.
- package/README.md +35 -4
- package/client/client.js +673 -329
- package/package.json +6 -4
- package/scripts/check-leak.mjs +150 -0
- package/src/db.js +543 -37
- package/src/main.js +77 -13
- package/src/ops.js +46 -8
- package/test/client.mjs +41 -2
- package/test/db.mjs +190 -24
- package/test/plugin.mjs +76 -50
- package/test/security.mjs +200 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-api-runner",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "DSH 插件:按配置调用后端 HTTP 接口、启停/重启本地开发服务、增量读取日志并等待日志特征,让模型能\"改完自己验\"。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -36,10 +36,12 @@
|
|
|
36
36
|
"dsh": ">=0.1.0-rc.6 <0.2.0-0 || >=0.2.0-rc.1 <0.3.0-0"
|
|
37
37
|
},
|
|
38
38
|
"scripts": {
|
|
39
|
-
"test": "node test/smoke.mjs && node test/plugin.mjs && node test/db.mjs",
|
|
40
|
-
"test:all": "node test/smoke.mjs && node test/plugin.mjs && node test/db.mjs && node test/client.mjs && node test/managed.mjs && node test/java.mjs",
|
|
39
|
+
"test": "node test/smoke.mjs && node test/plugin.mjs && node test/db.mjs && node test/security.mjs",
|
|
40
|
+
"test:all": "node test/smoke.mjs && node test/plugin.mjs && node test/db.mjs && node test/security.mjs && node test/client.mjs && node test/managed.mjs && node test/java.mjs",
|
|
41
41
|
"stage": "node scripts/stage.mjs",
|
|
42
|
-
"release": "node scripts/stage.mjs --release"
|
|
42
|
+
"release": "node scripts/stage.mjs --release",
|
|
43
|
+
"check-leak": "node scripts/check-leak.mjs",
|
|
44
|
+
"prepublishOnly": "node scripts/check-leak.mjs"
|
|
43
45
|
},
|
|
44
46
|
"dependencies": {
|
|
45
47
|
"yaml": "^2.8.0"
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* 发布前防泄漏检查(prepublishOnly 自动执行)。
|
|
4
|
+
*
|
|
5
|
+
* 为什么需要它:
|
|
6
|
+
* 仓库里有一份 RELEASE.local.md(本地发版流程),含本机绝对路径、用户名、
|
|
7
|
+
* Git 凭据助手路径。它被 .gitignore 和 npm files 白名单双重排除,
|
|
8
|
+
* 但「双重排除」靠的是配置正确——一次手滑(往 files 里加一行、或 git add -f)
|
|
9
|
+
* 就会泄漏。这里在发布前做一次硬检查,命中就中止发布。
|
|
10
|
+
*
|
|
11
|
+
* 检查两类东西:
|
|
12
|
+
* 1. 会被打进 npm 包的文件里,有没有本机绝对路径 / 用户名
|
|
13
|
+
* 2. 有没有真实凭据(token / 私钥 / 密码字面量)
|
|
14
|
+
*
|
|
15
|
+
* 退出码非 0 时 npm publish 会被中止。
|
|
16
|
+
*/
|
|
17
|
+
import fs from 'node:fs';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
import { fileURLToPath } from 'node:url';
|
|
20
|
+
|
|
21
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
22
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 会被打进包的文件清单。
|
|
26
|
+
* 优先用 package.json 的 files 白名单展开,避免和实际打包内容不一致。
|
|
27
|
+
*/
|
|
28
|
+
function listPackagedFiles() {
|
|
29
|
+
const out = [];
|
|
30
|
+
const always = ['package.json', 'README.md', 'LICENSE'];
|
|
31
|
+
for (const name of always) {
|
|
32
|
+
const p = path.join(root, name);
|
|
33
|
+
if (fs.existsSync(p)) out.push(name);
|
|
34
|
+
}
|
|
35
|
+
const walk = (rel) => {
|
|
36
|
+
const abs = path.join(root, rel);
|
|
37
|
+
if (!fs.existsSync(abs)) return;
|
|
38
|
+
const stat = fs.statSync(abs);
|
|
39
|
+
if (stat.isDirectory()) {
|
|
40
|
+
for (const entry of fs.readdirSync(abs)) walk(path.join(rel, entry));
|
|
41
|
+
} else {
|
|
42
|
+
out.push(rel.replace(/\\/g, '/'));
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
for (const entry of pkg.files || []) {
|
|
46
|
+
// 处理 "locale/*.json" 这类通配
|
|
47
|
+
if (entry.includes('*')) {
|
|
48
|
+
const dir = path.dirname(entry);
|
|
49
|
+
const pattern = path.basename(entry);
|
|
50
|
+
const absDir = path.join(root, dir);
|
|
51
|
+
if (!fs.existsSync(absDir)) continue;
|
|
52
|
+
const re = new RegExp(`^${pattern.replace(/\./g, '\\.').replace(/\*/g, '.*')}$`);
|
|
53
|
+
for (const f of fs.readdirSync(absDir)) {
|
|
54
|
+
if (re.test(f)) out.push(path.join(dir, f).replace(/\\/g, '/'));
|
|
55
|
+
}
|
|
56
|
+
} else {
|
|
57
|
+
walk(entry);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return [...new Set(out)];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** 只检查文本文件;二进制跳过(图片等) */
|
|
64
|
+
const TEXT_EXT = new Set(['.js', '.mjs', '.cjs', '.json', '.yml', '.yaml', '.md', '.ts', '.txt', '.css', '.html']);
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 泄漏规则:命中任意一条就中止。
|
|
68
|
+
* 注意:不把「用户名」本身当规则(作者名会正常出现在 package.json),
|
|
69
|
+
* 只拦「绝对路径」这种能定位到具体机器的信息。
|
|
70
|
+
*/
|
|
71
|
+
const RULES = [
|
|
72
|
+
{ name: 'Windows 用户目录绝对路径', re: /[A-Za-z]:\\+Users\\+[^\\\s"']+/i },
|
|
73
|
+
{ name: 'POSIX 家目录绝对路径', re: /\/home\/[a-z0-9_-]+\//i },
|
|
74
|
+
{ name: 'macOS 用户目录绝对路径', re: /\/Users\/[A-Za-z0-9_-]+\//i },
|
|
75
|
+
{ name: '本机 link: 安装路径', re: /link:[A-Za-z]:[\\/]/i },
|
|
76
|
+
{ name: 'file:// 本机路径', re: /file:\/\/\/[A-Za-z]:/i },
|
|
77
|
+
{ name: 'npm token', re: /npm_[A-Za-z0-9]{30,}/ },
|
|
78
|
+
{ name: 'GitHub token', re: /gh[pousr]_[A-Za-z0-9]{30,}/ },
|
|
79
|
+
{ name: '私钥块', re: /-----BEGIN [A-Z ]*PRIVATE KEY-----/ },
|
|
80
|
+
{ name: 'OpenAI 风格密钥', re: /sk-[A-Za-z0-9]{32,}/ },
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 白名单:这些行是「示例/说明」,允许出现类似路径的文本。
|
|
85
|
+
* 只放行明确无害的写法,不放行真实绝对路径。
|
|
86
|
+
*
|
|
87
|
+
* 原则:示例里用的应该是通用占位(`you`、`<用户名>`、`path/to`),
|
|
88
|
+
* 一旦出现真实用户名(比如本机的 MZ),规则就会命中——这才是要拦的。
|
|
89
|
+
*/
|
|
90
|
+
const ALLOW_LINE = [
|
|
91
|
+
/你的项目根目录/, /绝对路径\/到/, /<绝对路径>/, /link:\/绝对路径/,
|
|
92
|
+
/link:[A-Za-z]:\/(?:path|绝对)/i, // README 里的 link:D:/path/to/...
|
|
93
|
+
/\/Users\/(?:you|your|用户名|username)\//i, // 占位用户名
|
|
94
|
+
/D:\\+data\\/, /D:\\+dsh-api-runner-data/,
|
|
95
|
+
/D:\\?data/, /example/i, /示例/,
|
|
96
|
+
];
|
|
97
|
+
|
|
98
|
+
const files = listPackagedFiles();
|
|
99
|
+
const problems = [];
|
|
100
|
+
const scanned = [];
|
|
101
|
+
|
|
102
|
+
for (const rel of files) {
|
|
103
|
+
const ext = path.extname(rel).toLowerCase();
|
|
104
|
+
if (!TEXT_EXT.has(ext)) continue;
|
|
105
|
+
const abs = path.join(root, rel);
|
|
106
|
+
let text;
|
|
107
|
+
try {
|
|
108
|
+
text = fs.readFileSync(abs, 'utf8');
|
|
109
|
+
} catch {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
scanned.push(rel);
|
|
113
|
+
const lines = text.split('\n');
|
|
114
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
115
|
+
const line = lines[i];
|
|
116
|
+
if (ALLOW_LINE.some((re) => re.test(line))) continue;
|
|
117
|
+
for (const rule of RULES) {
|
|
118
|
+
if (rule.re.test(line)) {
|
|
119
|
+
problems.push({
|
|
120
|
+
file: rel,
|
|
121
|
+
line: i + 1,
|
|
122
|
+
rule: rule.name,
|
|
123
|
+
snippet: line.trim().slice(0, 120),
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// 额外确认:敏感文件绝不能被 files 白名单覆盖
|
|
131
|
+
const FORBIDDEN = ['RELEASE.local.md'];
|
|
132
|
+
for (const name of FORBIDDEN) {
|
|
133
|
+
if (files.some((f) => f.endsWith(name) || f === name)) {
|
|
134
|
+
problems.push({ file: name, line: 0, rule: '敏感文件被打进包', snippet: name });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
console.log(`[check-leak] 扫描 ${scanned.length} 个文本文件(共打包 ${files.length} 个)`);
|
|
139
|
+
|
|
140
|
+
if (problems.length > 0) {
|
|
141
|
+
console.error('\n✗ 发现疑似泄漏,已中止发布:\n');
|
|
142
|
+
for (const p of problems) {
|
|
143
|
+
console.error(` ${p.file}${p.line ? `:${p.line}` : ''} [${p.rule}]`);
|
|
144
|
+
if (p.snippet) console.error(` ${p.snippet}`);
|
|
145
|
+
}
|
|
146
|
+
console.error('\n如果确认这些是无害的示例文本,把它们加进 scripts/check-leak.mjs 的 ALLOW_LINE。');
|
|
147
|
+
process.exit(1);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
console.log('✓ 未发现本地路径或凭据泄漏');
|