90 lines
3.1 KiB
JavaScript
Raw Normal View History

/**
* @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();
});
};