Files
blog/blog-admin/tools/export-certkeeper-data.mjs
T
zqlit 1427c47dcf feat(ssl): 证书签发链路搬到国内机 Docker,清理 1Panel 过期证书
架构定案(B+C):CF Worker 免费版 CPU 硬顶 10ms(cron 同),
不再购买 Paid($5/月≈¥36),改为——
  B. Worker 留免费版 + 代码优化(把 CPU 压进预算)
  C. 签发+部署整条链路搬国内机 Docker 容器

代码
- acme.ts: 缓存 signingKey 为 Promise,单次签发 importKey 12→1 次
  (实测 importKey 126µs / sign 84µs;一次签发 3.3ms → 1.4ms)
- deployer.ts: 新增 DogeCloudDeployer.ping();修正 cert_id → id 的注释
- dnsprovider.ts: 新增 RemoteDns(把 DNS-01 写 TXT 委托给国内机 cn-dns-helper)
- tools/certkeeper-config.mjs: 域名配置抽为唯一事实源(两个消费方共用)
- tools/export-certkeeper-data.mjs: 导出国内机数据目录

新增部署单元
- deploy/cn-certkeeper: 签发+部署容器(只绑 127.0.0.1:8019,compose 管理)
  含 FileKV(文件系统版 KVNamespace)、带鉴权 HTTP、每日 4:10 续期
- deploy/cn-dns-helper: DNS-01 写 TXT 助手(只绑 127.0.0.1:8018)

文档
- 函数版证书管家-方案.md 新增第九章:B+C 定案、实测 CPU 数据、
  容器验收记录、1Panel 过期证书清理记录、t-t.live 两套管理冲突
- 标注旧 8.3 节「免费版跑不了签发」为未实测误判

一并纳入:.gitignore 忽略 deploy/cn-certkeeper/lib/(tsc 编译产物)
2026-10-06 18:28:55 +08:00

129 lines
5.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 把证书管家的配置 + 凭据导出成**国内机 cn-certkeeper 可直接用的数据目录**。
*
* 为什么需要它:
* 签发链路搬到了国内机(Workers 免费版 CPU 只有 10ms,签发要 ~4ms 起步,
* 擦着上限跑不划算)。国内机要独立跑完整的「探针 → 签发 → 部署」,
* 就得有一份自己的配置。这里从**同一个事实源**生成,避免两边配置漂移:
* · 凭据:`E:/GitHub/secrets-backup/certkeeper-seeds.json`(明文底账)
* · 域名:`tools/certkeeper-config.mjs`(与 seed-ssl-config.mjs 共用)
*
* ★ 凭据用与 Worker 侧**完全相同**的 sealJson + 同一个 TOKEN_SECRET 加密,
* 所以产物格式与线上 KV 一致 —— 将来要拿 Worker KV 的备份直接替换数据目录
* 也能读。反过来也一样。
*
* 跑法:
* node tools/export-certkeeper-data.mjs --out <目录>
* TOKEN_SECRET 取值顺序:--token-secret 参数 > 环境变量 > .dev.vars
*/
import fs from 'node:fs';
import path from 'node:path';
import { createRequire } from 'node:module';
import { fileURLToPath } from 'node:url';
import { FileKV } from '../../deploy/cn-certkeeper/src/kv-file.mjs';
import { CONFIG } from './certkeeper-config.mjs';
const require = createRequire(import.meta.url);
const HERE = path.dirname(fileURLToPath(import.meta.url));
const ROOT = path.resolve(HERE, '..'); // blog-admin/
const REPO = path.resolve(ROOT, '..'); // 仓库根
const { sealJson } = require(path.join(ROOT, '.selftest-ssl/lib/certvault.js'));
// ---------------------------------------------------------------- 参数
const argv = process.argv.slice(2);
const argOf = (name) => {
const i = argv.indexOf(name);
return i >= 0 ? argv[i + 1] : undefined;
};
const OUT = argOf('--out');
if (!OUT) {
console.error('用法:node tools/export-certkeeper-data.mjs --out <目录> [--token-secret <值>]');
process.exit(1);
}
// TOKEN_SECRET:参数 > 环境变量 > .dev.vars
let secret = argOf('--token-secret') || process.env.TOKEN_SECRET || '';
if (!secret) {
const devVars = path.join(ROOT, '.dev.vars');
if (fs.existsSync(devVars)) {
const m = fs.readFileSync(devVars, 'utf8').match(/^TOKEN_SECRET\s*=\s*"?([^"\r\n]+)"?/m);
if (m) secret = m[1].trim();
}
}
if (!secret) throw new Error('没拿到 TOKEN_SECRET(用 --token-secret / 环境变量 / blog-admin/.dev.vars 任一提供)');
// 编译产物必须先就位
if (!fs.existsSync(path.join(ROOT, '.selftest-ssl/lib/certvault.js'))) {
throw new Error('缺少编译产物:先跑 `npm run selftest:ssl:build`(在 blog-admin 下)');
}
// ---------------------------------------------------------------- seeds
const seedsPath = path.join(REPO, '../secrets-backup/certkeeper-seeds.json');
const resolvedSeeds = fs.existsSync(seedsPath)
? seedsPath
: path.resolve('E:/GitHub/secrets-backup/certkeeper-seeds.json');
if (!fs.existsSync(resolvedSeeds)) throw new Error(`找不到 seeds 文件:${resolvedSeeds}`);
const seeds = JSON.parse(fs.readFileSync(resolvedSeeds, 'utf8'));
if (!Array.isArray(seeds.access)) throw new Error('seeds 里没有 access 数组');
// ---------------------------------------------------------------- 生成
console.log('导出到:', path.resolve(OUT));
console.log('TOKEN_SECRET 长度:', secret.length, '(应与 Worker 侧一致)');
console.log('seeds:', resolvedSeeds);
console.log();
const kv = new FileKV(OUT);
const env = { TOKEN_SECRET: secret };
// ① 域名配置(明文,与 certstore.saveConfig 的写法一致)
await kv.put('certkeeper:config', JSON.stringify(CONFIG));
console.log(`✓ certkeeper:config(${CONFIG.domains.length} 组域名)`);
for (const d of CONFIG.domains) {
console.log(` ${d.name.padEnd(12)} dns=${d.dns.padEnd(14)} deploy=[${d.deploy.join(', ')}]`);
}
console.log();
console.log('凭据(AES-GCM 加密,与 Worker 侧同一把密钥):');
for (const a of seeds.access) {
const rec = { type: a.type, note: a.note, ...(a.fields || {}) };
await kv.put('certkeeper:access:' + a.name, await sealJson(env, rec));
console.log(` ✓ ${a.name.padEnd(16)} ${String(a.type).padEnd(14)} ${Object.keys(a.fields || {}).join(', ')}`);
}
// ② 回读校验:确保写得进去也读得出来(并且能解回明文)
console.log();
console.log('回读校验:');
const back = await kv.get('certkeeper:config');
const parsed = JSON.parse(back);
console.log(` ✓ config 回读:${parsed.domains.length} 组域名`);
const { openJson } = require(path.join(ROOT, '.selftest-ssl/lib/certvault.js'));
let okCount = 0;
for (const a of seeds.access) {
const raw = await kv.get('certkeeper:access:' + a.name);
const opened = await openJson(env, raw);
if (opened.type !== a.type) throw new Error(`凭据 ${a.name} 回读后 type 不一致`);
okCount++;
}
console.log(` ✓ ${okCount} 条凭据全部解回明文(type 一致)`);
// ③ list 语义校验(certstore.listAccess 会用到)
const listed = await kv.list({ prefix: 'certkeeper:access:' });
console.log(` ✓ list 前缀查询命中 ${listed.keys.length} 条`);
console.log();
console.log('完成。数据目录结构:');
const walk = (d, indent = ' ') => {
for (const e of fs.readdirSync(d, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
const full = path.join(d, e.name);
if (e.isDirectory()) {
console.log(indent + e.name + '/');
walk(full, indent + ' ');
} else {
console.log(indent + e.name);
}
}
};
walk(OUT);