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