90 lines
3.1 KiB
JavaScript
90 lines
3.1 KiB
JavaScript
/**
|
|
* @capability secrets.fetch
|
|
* @description 从国内域名机本地 vault 拉密钥 (走 127.0.0.1 loopback). 不走环境变量, 不走 GitHub Secrets.
|
|
* @signature secrets.fetch({key, vault_url?}) → Promise<{value}>
|
|
* @sovereign TCS-0002∞
|
|
* @copyright 国作登字-2026-A-00037559
|
|
*
|
|
* 注: 这个能力**只在域名机本地 (ZY-SVR-CN01) 跑神笔马良时**可用. 它会走
|
|
* vault 的 /internal/fetch/:key 端点, 该端点只接受 127.0.0.1 来源, 因此:
|
|
*
|
|
* - 域名机本地神笔马良 → 通
|
|
* - 海外机 / 开发者本地 → 拒绝 (vault 那边返 403)
|
|
*
|
|
* 这是 baton-005 C 节的设计: 神笔马良不持有任何长期密钥, 现取现用.
|
|
*/
|
|
'use strict';
|
|
|
|
module.exports = async function secretsFetch({ key, vault_url } = {}) {
|
|
if (!key || typeof key !== 'string') {
|
|
throw new Error('secrets.fetch: key 必填 (例: ZY_AUTODL_PASS)');
|
|
}
|
|
if (!/^[A-Z][A-Z0-9_]{1,63}$/.test(key)) {
|
|
throw new Error('secrets.fetch: key 格式不对 (大写字母+数字+下划线, 字母开头, 2-64 位)');
|
|
}
|
|
|
|
const base = vault_url || process.env.VAULT_INTERNAL_URL || 'http://127.0.0.1:8080';
|
|
// 严守"只本机"边界 — 即使调用方乱传, 这里也强制只允许 loopback host
|
|
let urlObj;
|
|
try {
|
|
urlObj = new URL(base);
|
|
} catch (e) {
|
|
throw new Error('secrets.fetch: vault_url 不是合法 URL');
|
|
}
|
|
if (
|
|
urlObj.hostname !== '127.0.0.1' &&
|
|
urlObj.hostname !== 'localhost' &&
|
|
urlObj.hostname !== '::1' &&
|
|
urlObj.hostname !== '::ffff:127.0.0.1'
|
|
) {
|
|
throw new Error(
|
|
'secrets.fetch: vault 只允许走 loopback (127.0.0.1 / localhost / ::1), 当前=' +
|
|
urlObj.hostname +
|
|
'. 这是 cc-004 强制隔离, 不可绕过.'
|
|
);
|
|
}
|
|
|
|
const http = require('http');
|
|
return new Promise((resolve, reject) => {
|
|
const req = http.request(
|
|
{
|
|
method: 'GET',
|
|
hostname: urlObj.hostname,
|
|
port: urlObj.port || 80,
|
|
path: '/internal/fetch/' + encodeURIComponent(key),
|
|
timeout: 5000,
|
|
},
|
|
(res) => {
|
|
const chunks = [];
|
|
res.on('data', (c) => chunks.push(c));
|
|
res.on('end', () => {
|
|
const text = Buffer.concat(chunks).toString('utf8');
|
|
let body;
|
|
try {
|
|
body = JSON.parse(text);
|
|
} catch (e) {
|
|
return reject(new Error('secrets.fetch: vault 响应不是 JSON: ' + text.slice(0, 120)));
|
|
}
|
|
if (res.statusCode === 200 && body && body.ok) {
|
|
return resolve({ key, value: body.value });
|
|
}
|
|
return reject(
|
|
new Error(
|
|
'secrets.fetch: vault 拒绝 (status=' +
|
|
res.statusCode +
|
|
', code=' +
|
|
(body && body.code) +
|
|
', message=' +
|
|
(body && body.message) +
|
|
')'
|
|
)
|
|
);
|
|
});
|
|
}
|
|
);
|
|
req.on('error', (e) => reject(new Error('secrets.fetch: 连不上 vault (' + e.message + ')')));
|
|
req.on('timeout', () => req.destroy(new Error('secrets.fetch: vault 响应超时 (5s)')));
|
|
req.end();
|
|
});
|
|
};
|