dsh-plugin-manager-companion 0.1.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/LICENSE +21 -0
- package/README.en.md +144 -0
- package/README.md +142 -0
- package/cordis.patch.yml +9 -0
- package/dist/about.d.ts +77 -0
- package/dist/about.js +179 -0
- package/dist/cli.d.ts +226 -0
- package/dist/cli.js +856 -0
- package/dist/client/AboutPage.d.ts +75 -0
- package/dist/client/ConsolePage.d.ts +79 -0
- package/dist/client/KindsPage.d.ts +21 -0
- package/dist/client/MarketplacePage.d.ts +36 -0
- package/dist/client/OfficialSlots.d.ts +35 -0
- package/dist/client/UpgradeRow.d.ts +108 -0
- package/dist/client/index.d.ts +26 -0
- package/dist/client/locales.d.ts +475 -0
- package/dist/client/pmSelect.d.ts +38 -0
- package/dist/client/shared.d.ts +928 -0
- package/dist/client/upgradeView.d.ts +278 -0
- package/dist/client/wire.d.ts +401 -0
- package/dist/client.js +9194 -0
- package/dist/diagnostics.d.ts +332 -0
- package/dist/diagnostics.js +2631 -0
- package/dist/envManager.d.ts +1047 -0
- package/dist/envManager.js +3214 -0
- package/dist/fix.d.ts +60 -0
- package/dist/fix.js +168 -0
- package/dist/guard.d.ts +133 -0
- package/dist/guard.js +232 -0
- package/dist/index.d.ts +121 -0
- package/dist/index.js +1150 -0
- package/dist/installSession.d.ts +111 -0
- package/dist/installSession.js +150 -0
- package/dist/kinds.d.ts +464 -0
- package/dist/kinds.js +1029 -0
- package/dist/marketView.d.ts +261 -0
- package/dist/marketView.js +406 -0
- package/dist/marketplace.d.ts +248 -0
- package/dist/marketplace.js +500 -0
- package/dist/match.d.ts +67 -0
- package/dist/match.js +203 -0
- package/dist/net.d.ts +108 -0
- package/dist/net.js +163 -0
- package/dist/official.d.ts +145 -0
- package/dist/official.js +205 -0
- package/dist/paths.d.ts +108 -0
- package/dist/paths.js +236 -0
- package/dist/presets.d.ts +299 -0
- package/dist/presets.js +578 -0
- package/dist/qualityGate.d.ts +66 -0
- package/dist/qualityGate.js +247 -0
- package/dist/rank.d.ts +88 -0
- package/dist/rank.js +164 -0
- package/dist/registry.d.ts +295 -0
- package/dist/registry.js +686 -0
- package/dist/rest.d.ts +122 -0
- package/dist/rest.js +219 -0
- package/dist/scan.d.ts +134 -0
- package/dist/scan.js +396 -0
- package/dist/settings.d.ts +447 -0
- package/dist/settings.js +263 -0
- package/dist/tags.d.ts +119 -0
- package/dist/tags.js +166 -0
- package/dist/tools.d.ts +131 -0
- package/dist/tools.js +377 -0
- package/dist/types.d.ts +651 -0
- package/dist/types.js +13 -0
- package/dist/upgrade.d.ts +428 -0
- package/dist/upgrade.js +1100 -0
- package/dist/upgradeView.d.ts +313 -0
- package/dist/upgradeView.js +273 -0
- package/docs/images/readme/01-console-health.png +0 -0
- package/docs/images/readme/02-console-envs.png +0 -0
- package/docs/images/readme/03-marketplace.png +0 -0
- package/docs/images/readme/04-official-plugin-page.png +0 -0
- package/package.json +104 -0
package/dist/scan.js
ADDED
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 安装期环境变量需求扫描与子进程环境过滤(给 git 源插件用)。
|
|
3
|
+
*
|
|
4
|
+
* 归属:B 类·参考算法后重写(纯决策 + 有界只读扫描;旧 src/scan.ts 仅作意图参考,未复制代码)。
|
|
5
|
+
* 旧实现参考:dsh-web-plugin-manager/src/scan.ts(91 行,2 层/40 文件/8 变量上限、
|
|
6
|
+
* TOKEN|KEY|SECRET|PASSWORD|PASS|CREDENTIAL 六族敏感键、\b 词边界的坑)。
|
|
7
|
+
* 官方复用:无(官方没有安装期 env 扫描;官方 plugin-manager 用 scrubbedParentEnv()
|
|
8
|
+
* 清空 service 侧子进程环境,我们借鉴其"默认最小"的取向,但实现自有)。
|
|
9
|
+
* 前提检查:仍然成立——git 源插件的 pnpm 解析与生命周期脚本会继承子进程 env,
|
|
10
|
+
* 全量透传等于把宿主凭据交给未审核的第三方代码。旧实现的已知限制(只覆盖常见
|
|
11
|
+
* 敏感键形态)在这里被显式扩大:见 SENSITIVE_FAMILIES / SENSITIVE_PREFIXES;
|
|
12
|
+
* 未被覆盖的形态仍然放行,这一事实写在 buildFilteredEnv 的文档里,不假装完整。
|
|
13
|
+
*
|
|
14
|
+
* 三条安全不变量(写在代码里,不靠调用方自觉):
|
|
15
|
+
* 1. answers 只按**扫描白名单**注入——PATH/HOME/NODE_OPTIONS 这类键永远进不来
|
|
16
|
+
* (旧仓库审计点:任意键注入可劫持子进程执行)。
|
|
17
|
+
* 2. 子进程环境默认剔除敏感键形态;扫描出的键由调用方显式给定值时按值注入
|
|
18
|
+
* (用户显式提供即同意),未提供的宿主同名残留值仍被剔除。
|
|
19
|
+
* 3. 过滤是"剔除",不是"清空":宿主仍需要 PATH/HOME 才能跑 pnpm。这与官方
|
|
20
|
+
* service 侧的全清策略不同是有意的:CLI 的生命周期脚本依赖用户工具链
|
|
21
|
+
* (nvm 下的 node/pnpm 解析),清空会让安装直接失败。
|
|
22
|
+
*/
|
|
23
|
+
import { readdir, readFile, stat } from 'node:fs/promises';
|
|
24
|
+
import { join, relative, sep } from 'node:path';
|
|
25
|
+
/** 默认上限。旧仓库实测:2 层/40 文件足够覆盖 README + package.json + .env.example。 */
|
|
26
|
+
export const DEFAULT_SCAN_LIMITS = {
|
|
27
|
+
maxDepth: 2,
|
|
28
|
+
maxFiles: 40,
|
|
29
|
+
maxVariables: 8,
|
|
30
|
+
maxFileBytes: 512 * 1024,
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* 敏感键判定族。
|
|
34
|
+
*
|
|
35
|
+
* 与旧实现的差别:旧实现只有六族(TOKEN/KEY/SECRET/PASSWORD/PASS/CREDENTIAL),
|
|
36
|
+
* 且用 \b 词边界——GITHUB_TOKEN 里的 TOKEN 前是下划线(属于 \w),\b 根本不成立;
|
|
37
|
+
* 旧注释记录了这一点并改用字母数字感知边界。这里保留字母数字感知边界,把族扩到
|
|
38
|
+
* 结构词(ACCESS_KEY/SECRET_KEY/AUTH/AUTHORIZATION/BEARER/PRIVATE_KEY)并补上
|
|
39
|
+
* 云厂商与常见平台的固定前缀(AWS_/GH_/GITHUB_/OPENAI_/ANTHROPIC_/DEEPSEEK_ 等)。
|
|
40
|
+
*
|
|
41
|
+
* **不是全集的诚实声明**:任何未被这些形态覆盖的宿主变量(例如自定义
|
|
42
|
+
* SESSION_COOKIE、内部 _TICKET 命名)不会被剔除,会随子进程进入第三方脚本的
|
|
43
|
+
* 生命周期钩子。彻底消除这条路径只能清空环境或引入随宿主版本演进的
|
|
44
|
+
* allowlist——前者打断用户工具链解析,后者不在本模块职责内,因此这里如实声明。
|
|
45
|
+
*/
|
|
46
|
+
const SENSITIVE_FAMILIES = [
|
|
47
|
+
'TOKEN', 'KEY', 'SECRET', 'PASSWORD', 'PASS', 'CREDENTIAL', 'CREDENTIALS',
|
|
48
|
+
'ACCESS_KEY', 'SECRET_KEY', 'AUTH', 'AUTHORIZATION', 'BEARER', 'PRIVATE_KEY',
|
|
49
|
+
];
|
|
50
|
+
/** 云厂商与常见平台的键前缀(大小写不敏感)。 */
|
|
51
|
+
const SENSITIVE_PREFIXES = [
|
|
52
|
+
'AWS_', 'GH_', 'GITHUB_', 'GITLAB_', 'OPENAI_', 'ANTHROPIC_', 'DEEPSEEK_',
|
|
53
|
+
'AZURE_', 'GOOGLE_', 'GCP_', 'SLACK_', 'STRIPE_', 'NPM_', 'NODE_AUTH_',
|
|
54
|
+
'DOCKER_', 'SSH_', 'CI_JOB_',
|
|
55
|
+
];
|
|
56
|
+
/**
|
|
57
|
+
* 敏感环境变量名判定。
|
|
58
|
+
*
|
|
59
|
+
* 边界用字母数字感知(不能用 \b:下划线属于 \w,GITHUB_TOKEN 中 TOKEN 前无边界)。
|
|
60
|
+
* 结果:GITHUB_TOKEN / OPENAI_API_KEY / DB_PASSWORD / XXX_PASS 命中,
|
|
61
|
+
* KEYBOARD_LAYOUT(KEY 后接 B)不误伤,AWS_SECRET_ACCESS_KEY 命中。
|
|
62
|
+
*
|
|
63
|
+
* @param name - 变量名;非字符串输入按字符串处理(调用方可能传任意 JSON)。
|
|
64
|
+
* @returns 是否属于敏感形态。
|
|
65
|
+
*/
|
|
66
|
+
export function isSensitiveEnvKey(name) {
|
|
67
|
+
const text = String(name ?? '');
|
|
68
|
+
if (text.length === 0)
|
|
69
|
+
return false;
|
|
70
|
+
const upper = text.toUpperCase();
|
|
71
|
+
for (const prefix of SENSITIVE_PREFIXES) {
|
|
72
|
+
if (upper.startsWith(prefix))
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
for (const family of SENSITIVE_FAMILIES) {
|
|
76
|
+
// 字母数字感知边界:(?<![A-Za-z0-9]) 与 (?![A-Za-z0-9])
|
|
77
|
+
const pattern = new RegExp('(?<![A-Za-z0-9])' + family + '(?![A-Za-z0-9])', 'i');
|
|
78
|
+
if (pattern.test(text))
|
|
79
|
+
return true;
|
|
80
|
+
// 后缀形态:AWS_SECRET_ACCESS_KEY 中 ACCESS_KEY 紧跟在 '_' 之后(\w,边界不成立),
|
|
81
|
+
// MY_APP_TOKEN 这类以家族名结尾的命名同样属于凭据形态。
|
|
82
|
+
if (upper.endsWith('_' + family))
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
// 驼峰拼接形态:apiKey / accessToken / clientSecret。前一个字符必须是小写字母
|
|
86
|
+
// (camelCase 的拼接点),这样 monkey 这类普通词不会被误判(key 是小写 k)。
|
|
87
|
+
if (/[a-z](?:Key|Token|Secret|Password|Credential|Pass)$/.test(text))
|
|
88
|
+
return true;
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* 候选变量名的文本形态。
|
|
93
|
+
*
|
|
94
|
+
* 只匹配**敏感形态**:安装/构建阶段通常只有凭据类变量需要用户提供,普通配置项
|
|
95
|
+
* (PORT、NODE_ENV)有默认值,扫出来只会打扰用户。
|
|
96
|
+
*/
|
|
97
|
+
const ENV_NAME_PATTERNS = [
|
|
98
|
+
// SCREAMING_SNAKE:OPENAI_API_KEY / GITHUB_TOKEN / DB_PASSWORD / XXX_PASS
|
|
99
|
+
/\b[A-Z][A-Z0-9_]{1,}(?:API_KEY|_KEY|_TOKEN|_SECRET|_PASSWORD|_PASS|_CREDENTIALS?|_ACCESS_KEY)\b/g,
|
|
100
|
+
// 驼峰:apiKey / accessToken / clientSecret / dbPassword
|
|
101
|
+
/\b[a-z][A-Za-z0-9]*(?:ApiKey|AccessKey|Token|Secret|Password|Credential)\b/g,
|
|
102
|
+
// process.env.X / env.X 引用形态
|
|
103
|
+
/(?:process\.env|\benv)\.([A-Z][A-Z0-9_]{2,})\b/g,
|
|
104
|
+
];
|
|
105
|
+
/** 读取目录项,失败返回空数组(权限/不存在都不应中断扫描)。 */
|
|
106
|
+
async function readEntries(dir) {
|
|
107
|
+
try {
|
|
108
|
+
return await readdir(dir, { withFileTypes: true });
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return [];
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/** 候选文件名判定:只读"人写的"文件,跳过构建产物与锁文件。 */
|
|
115
|
+
function isCandidateFile(name) {
|
|
116
|
+
const lower = name.toLowerCase();
|
|
117
|
+
if (/^readme/i.test(name))
|
|
118
|
+
return true;
|
|
119
|
+
if (lower.startsWith('install') || lower.startsWith('setup') || lower.startsWith('bootstrap'))
|
|
120
|
+
return true;
|
|
121
|
+
if (lower.endsWith('.env') || lower.endsWith('.env.example') || lower.endsWith('.env.sample'))
|
|
122
|
+
return true;
|
|
123
|
+
if (lower === 'package.json')
|
|
124
|
+
return true;
|
|
125
|
+
if (lower.endsWith('.yml') || lower.endsWith('.yaml'))
|
|
126
|
+
return true;
|
|
127
|
+
if (lower.endsWith('.md') || lower.endsWith('.mdx'))
|
|
128
|
+
return true;
|
|
129
|
+
if (lower.endsWith('.sh') || lower.endsWith('.ps1'))
|
|
130
|
+
return true;
|
|
131
|
+
if (lower.endsWith('.json') && lower !== 'pnpm-lock.json' && lower !== 'package-lock.json')
|
|
132
|
+
return true;
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
/** 默认跳过的目录:版本库、依赖树、构建产物。 */
|
|
136
|
+
function isSkippedDir(name) {
|
|
137
|
+
if (name.startsWith('.'))
|
|
138
|
+
return true;
|
|
139
|
+
return name === 'node_modules' || name === 'dist' || name === 'build' || name === 'out' || name === 'coverage';
|
|
140
|
+
}
|
|
141
|
+
/** 从一个文本里抽取候选变量名(按出现顺序去重,值为发现形态)。 */
|
|
142
|
+
function namesInText(text, includePlain) {
|
|
143
|
+
const found = new Map();
|
|
144
|
+
for (const pattern of ENV_NAME_PATTERNS) {
|
|
145
|
+
// 全局正则的 lastIndex 在多次调用间会残留;每次重置,避免漏匹配。
|
|
146
|
+
pattern.lastIndex = 0;
|
|
147
|
+
for (const match of text.matchAll(pattern)) {
|
|
148
|
+
// 第三种形态带捕获组(process.env.X);取组 1 存在时的值,否则整体。
|
|
149
|
+
const raw = match[1] ?? match[0];
|
|
150
|
+
const name = raw.trim();
|
|
151
|
+
if (name.length < 3 || name.length > 128)
|
|
152
|
+
continue;
|
|
153
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name))
|
|
154
|
+
continue;
|
|
155
|
+
if (!includePlain && !isSensitiveEnvKey(name))
|
|
156
|
+
continue;
|
|
157
|
+
if (!found.has(name))
|
|
158
|
+
found.set(name, 'text');
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return found;
|
|
162
|
+
}
|
|
163
|
+
/** 从 package.json 里抽取可能消费的环境变量(scripts / config / env / dsh / build)。 */
|
|
164
|
+
function namesInManifest(text) {
|
|
165
|
+
const found = new Map();
|
|
166
|
+
let manifest;
|
|
167
|
+
try {
|
|
168
|
+
manifest = JSON.parse(text);
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
return found;
|
|
172
|
+
}
|
|
173
|
+
if (manifest === null || typeof manifest !== 'object')
|
|
174
|
+
return found;
|
|
175
|
+
const record = manifest;
|
|
176
|
+
const visit = (value, via, depth) => {
|
|
177
|
+
if (depth > 4)
|
|
178
|
+
return;
|
|
179
|
+
if (typeof value === 'string') {
|
|
180
|
+
for (const [name] of namesInText(value, true)) {
|
|
181
|
+
if (!found.has(name))
|
|
182
|
+
found.set(name, via);
|
|
183
|
+
}
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
if (Array.isArray(value)) {
|
|
187
|
+
for (const item of value)
|
|
188
|
+
visit(item, via, depth + 1);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
if (value !== null && typeof value === 'object') {
|
|
192
|
+
for (const [key, item] of Object.entries(value)) {
|
|
193
|
+
// 键本身就是变量名("env": { "OPENAI_API_KEY": "..." } 这种写法)。
|
|
194
|
+
if (/^[A-Z][A-Z0-9_]{2,}$/.test(key) && isSensitiveEnvKey(key) && !found.has(key))
|
|
195
|
+
found.set(key, via);
|
|
196
|
+
visit(item, via, depth + 1);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
for (const field of ['scripts', 'config', 'env', 'dsh', 'build']) {
|
|
201
|
+
if (record[field] !== undefined)
|
|
202
|
+
visit(record[field], 'package.json#' + field, 0);
|
|
203
|
+
}
|
|
204
|
+
return found;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* 扫描仓库目录,给出安装/构建阶段可能需要的环境变量名。
|
|
208
|
+
*
|
|
209
|
+
* 有界:层数、文件数、变量数、单文件大小全部设上限;触顶时 truncated 为 true
|
|
210
|
+
* 并在 truncatedReasons 里说明是哪个维度触顶——漏报一个必需变量会让用户在安装
|
|
211
|
+
* 中途才失败,所以超限必须如实标注而不是静默截断。
|
|
212
|
+
*
|
|
213
|
+
* 只读取,不执行:仓库里的 install.sh 永远不会被本模块或调用方自动执行。
|
|
214
|
+
*
|
|
215
|
+
* @param repoDir - 已克隆/已就绪的仓库根目录。
|
|
216
|
+
* @param options - 上限覆盖与是否收录非敏感形态。
|
|
217
|
+
* @returns 扫描报告。
|
|
218
|
+
*/
|
|
219
|
+
export async function scanRequirements(repoDir, options = {}) {
|
|
220
|
+
const limits = { ...DEFAULT_SCAN_LIMITS, ...options.limits };
|
|
221
|
+
const includePlain = options.includePlain === true;
|
|
222
|
+
const requirements = new Map();
|
|
223
|
+
const truncatedReasons = [];
|
|
224
|
+
const candidates = [];
|
|
225
|
+
let filesRead = 0;
|
|
226
|
+
let depthReached = 0;
|
|
227
|
+
let oversized = 0;
|
|
228
|
+
let depthHit = false;
|
|
229
|
+
let variablesHit = false;
|
|
230
|
+
const walk = async (dir, depth) => {
|
|
231
|
+
if (candidates.length >= limits.maxFiles)
|
|
232
|
+
return;
|
|
233
|
+
if (depth > limits.maxDepth) {
|
|
234
|
+
depthHit = true;
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
depthReached = Math.max(depthReached, depth);
|
|
238
|
+
const entries = await readEntries(dir);
|
|
239
|
+
for (const entry of entries) {
|
|
240
|
+
if (candidates.length >= limits.maxFiles)
|
|
241
|
+
return;
|
|
242
|
+
const path = join(dir, entry.name);
|
|
243
|
+
if (entry.isDirectory()) {
|
|
244
|
+
if (isSkippedDir(entry.name))
|
|
245
|
+
continue;
|
|
246
|
+
await walk(path, depth + 1);
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
if (!entry.isFile() || !isCandidateFile(entry.name))
|
|
250
|
+
continue;
|
|
251
|
+
candidates.push({ path, rel: relative(repoDir, path).split(sep).join('/') });
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
await walk(repoDir, 0);
|
|
255
|
+
if (depthHit)
|
|
256
|
+
truncatedReasons.push('directory depth exceeded ' + String(limits.maxDepth));
|
|
257
|
+
if (candidates.length >= limits.maxFiles)
|
|
258
|
+
truncatedReasons.push('file budget reached (' + String(limits.maxFiles) + ')');
|
|
259
|
+
for (const candidate of candidates.slice(0, limits.maxFiles)) {
|
|
260
|
+
let size;
|
|
261
|
+
try {
|
|
262
|
+
size = (await stat(candidate.path)).size;
|
|
263
|
+
}
|
|
264
|
+
catch {
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
if (size > limits.maxFileBytes) {
|
|
268
|
+
oversized += 1;
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
let text;
|
|
272
|
+
try {
|
|
273
|
+
text = await readFile(candidate.path, 'utf8');
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
// 二进制或不可读:跳过是唯一安全选择(不计入 oversized,那是"太大"的账)。
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
filesRead += 1;
|
|
280
|
+
const found = /(^|\/)package\.json$/i.test(candidate.rel)
|
|
281
|
+
? namesInManifest(text)
|
|
282
|
+
: namesInText(text, includePlain);
|
|
283
|
+
for (const [name, via] of found) {
|
|
284
|
+
if (requirements.has(name))
|
|
285
|
+
continue;
|
|
286
|
+
if (requirements.size >= limits.maxVariables) {
|
|
287
|
+
variablesHit = true;
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
requirements.set(name, { file: candidate.rel, via, sensitive: isSensitiveEnvKey(name) });
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
if (variablesHit)
|
|
294
|
+
truncatedReasons.push('variable budget reached (' + String(limits.maxVariables) + ')');
|
|
295
|
+
return {
|
|
296
|
+
requirements: [...requirements.keys()],
|
|
297
|
+
sources: [...requirements.values()],
|
|
298
|
+
truncated: truncatedReasons.length > 0,
|
|
299
|
+
truncatedReasons,
|
|
300
|
+
filesRead,
|
|
301
|
+
depthReached,
|
|
302
|
+
oversized,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
/** 取某个变量名在扫描报告里的来源。 */
|
|
306
|
+
export function sourceOf(report, name) {
|
|
307
|
+
const index = report.requirements.indexOf(name);
|
|
308
|
+
return index === -1 ? undefined : report.sources[index];
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* 过滤子进程环境:剔除敏感键。
|
|
312
|
+
*
|
|
313
|
+
* 取值为 undefined 的键(Node 允许)保持 undefined,其余原样保留——这里是过滤器
|
|
314
|
+
* 而不是构造器,宿主工具链(PATH/HOME/NVM_DIR)必须继续可用,否则 nvm 用户的
|
|
315
|
+
* node/pnpm 解析会失败(旧仓库 issue 记录)。
|
|
316
|
+
*
|
|
317
|
+
* @param source - 基础环境;默认 process.env。
|
|
318
|
+
* @returns 剔除敏感键后的新对象(不改动传入对象)。
|
|
319
|
+
*/
|
|
320
|
+
export function buildFilteredEnv(source = process.env) {
|
|
321
|
+
const env = {};
|
|
322
|
+
for (const [key, value] of Object.entries(source)) {
|
|
323
|
+
if (isSensitiveEnvKey(key))
|
|
324
|
+
continue;
|
|
325
|
+
env[key] = value;
|
|
326
|
+
}
|
|
327
|
+
return env;
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* 构造注入给子进程的环境:先剔敏感键,再按白名单注入用户提供的值。
|
|
331
|
+
*
|
|
332
|
+
* 白名单来自扫描(session 或 CLI 本次扫描),因此 PATH/HOME/NODE_OPTIONS/
|
|
333
|
+
* __proto__ 这类键即使出现在 answers 里也不会被注入。
|
|
334
|
+
*
|
|
335
|
+
* 显式提供即同意:用户主动为某个**敏感**键提供值时该值会被注入——这个值只存在于
|
|
336
|
+
* 用户自己的机器上、由用户自己录入,安装链路没有"静默读取宿主凭据"的部分。
|
|
337
|
+
* 未提供时,宿主同名变量仍然被剔除。
|
|
338
|
+
*
|
|
339
|
+
* @param source - 基础环境(通常是 process.env)。
|
|
340
|
+
* @param answers - 用户提供的键值;键不在白名单内或值不是非空字符串即忽略。
|
|
341
|
+
* @param allowlist - 允许注入的键(扫描结果)。
|
|
342
|
+
* @returns 供子进程使用的环境对象。
|
|
343
|
+
*/
|
|
344
|
+
export function buildFilteredEnvWithAnswers(source, answers, allowlist) {
|
|
345
|
+
const env = buildFilteredEnv(source);
|
|
346
|
+
if (answers === undefined)
|
|
347
|
+
return env;
|
|
348
|
+
const allowed = new Set(allowlist);
|
|
349
|
+
for (const [key, value] of Object.entries(answers)) {
|
|
350
|
+
if (!allowed.has(key))
|
|
351
|
+
continue;
|
|
352
|
+
if (typeof value !== 'string' || value.length === 0)
|
|
353
|
+
continue;
|
|
354
|
+
env[key] = value;
|
|
355
|
+
}
|
|
356
|
+
return env;
|
|
357
|
+
}
|
|
358
|
+
/** 给用户看的缺失变量清单(CLI 输出用;含每个变量的来源,便于用户自己核实)。 */
|
|
359
|
+
export function formatMissingRequirements(report, provided) {
|
|
360
|
+
const missing = [];
|
|
361
|
+
for (const [index, name] of report.requirements.entries()) {
|
|
362
|
+
const value = provided[name];
|
|
363
|
+
if (typeof value === 'string' && value.length > 0)
|
|
364
|
+
continue;
|
|
365
|
+
const source = report.sources[index];
|
|
366
|
+
missing.push(source === undefined ? name : name + ' (found in ' + source.file + ')');
|
|
367
|
+
}
|
|
368
|
+
return missing;
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* 是否需要为某次安装做 env 扫描。
|
|
372
|
+
*
|
|
373
|
+
* 只对 git 源扫描:本地路径是用户自己的目录(想读环境变量早就读了),npm 包在
|
|
374
|
+
* 安装前没有可扫的仓库内容。git+ / github: / git@ / .git 结尾 / git URL 都算。
|
|
375
|
+
*
|
|
376
|
+
* @param spec - 安装源。
|
|
377
|
+
* @returns 是否为 git 源。
|
|
378
|
+
*/
|
|
379
|
+
export function isGitSource(spec) {
|
|
380
|
+
const text = spec.trim();
|
|
381
|
+
if (text === '')
|
|
382
|
+
return false;
|
|
383
|
+
if (/^git\+/i.test(text))
|
|
384
|
+
return true;
|
|
385
|
+
if (/^github:/i.test(text))
|
|
386
|
+
return true;
|
|
387
|
+
if (/^git@/i.test(text))
|
|
388
|
+
return true;
|
|
389
|
+
if (/^https?:\/\/[^/]*\/(?:[^/]+\/)*[^/]+\.git(?:#|$)/i.test(text))
|
|
390
|
+
return true;
|
|
391
|
+
if (/\.git(?:#|$)/i.test(text))
|
|
392
|
+
return true;
|
|
393
|
+
if (/^ssh:\/\//i.test(text))
|
|
394
|
+
return true;
|
|
395
|
+
return false;
|
|
396
|
+
}
|