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 编译产物)
This commit is contained in:
1 parent
a3bc10c68c
commit
1427c47dcf
15 files changed
+1387
-52
No files matched your search
@@ -109,6 +109,23 @@ export class AcmeClient {
|
||||
private nonce: string | null = null;
|
||||
private readonly log: AcmeLogger;
|
||||
|
||||
/**
|
||||
* ★ 账户私钥的 CryptoKey 缓存(2026-10-06 加)。
|
||||
*
|
||||
* 原来 signJws() 每次调用都 `importKey('jwk', ...)`。一次签发有 ~11 次 JWS,
|
||||
* 本地实测(.editor-tmp/cpu-bench4.mjs,3000 次迭代):
|
||||
* importKey('jwk') 单次 126 µs;若密钥已就绪,纯 sign 只要 84 µs。
|
||||
* 也就是说每次签发白烧 ≈ 1.4 ms。Workers 免费版 CPU 硬顶 10 ms,
|
||||
* 这 1.4 ms 值得省;就算跑在国内机,少一次密钥解析也没坏处。
|
||||
*
|
||||
* 缓存安全性:账户密钥(this.account.jwk)在实例生命周期内**不变**,
|
||||
* 而一次签发自始至终用同一个实例(见 certissue.ts 的 issueDomain)。
|
||||
*
|
||||
* 存 Promise 而不是 CryptoKey:并发调用时只真正 import 一次
|
||||
* (存 CryptoKey 的话,两个并发请求会各 import 一次,结果一样但白花 CPU)。
|
||||
*/
|
||||
private signingKey: Promise<CryptoKey> | null = null;
|
||||
|
||||
constructor(
|
||||
private readonly directoryUrl: string,
|
||||
private readonly account: { jwk: JsonWebKey; kid: string },
|
||||
@@ -168,14 +185,29 @@ export class AcmeClient {
|
||||
return base;
|
||||
}
|
||||
|
||||
/**
|
||||
* 取(并缓存)账户签名密钥 —— 见 signingKey 字段注释。
|
||||
* 只在第一次调用时真的 importKey,之后复用同一个 CryptoKey。
|
||||
*/
|
||||
private importSigningKey(): Promise<CryptoKey> {
|
||||
if (!this.signingKey) {
|
||||
// 失败时清掉缓存,否则一次网络/参数抖动会被永久缓存成 reject
|
||||
this.signingKey = (crypto.subtle.importKey(
|
||||
'jwk',
|
||||
this.account.jwk,
|
||||
{ name: 'ECDSA', namedCurve: 'P-256' },
|
||||
false,
|
||||
['sign'],
|
||||
) as Promise<CryptoKey>).catch((e) => {
|
||||
this.signingKey = null;
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
return this.signingKey;
|
||||
}
|
||||
|
||||
private async signJws(protectedHeader: Record<string, unknown>, payload: unknown): Promise<string> {
|
||||
const key = await crypto.subtle.importKey(
|
||||
'jwk',
|
||||
this.account.jwk,
|
||||
{ name: 'ECDSA', namedCurve: 'P-256' },
|
||||
false,
|
||||
['sign'],
|
||||
);
|
||||
const key = await this.importSigningKey();
|
||||
const signingInput = `${b64uJson(protectedHeader)}.${b64uJson(payload)}`;
|
||||
const sig = await crypto.subtle.sign(
|
||||
{ name: 'ECDSA', hash: 'SHA-256' },
|
||||
|
||||
@@ -134,6 +134,30 @@ export class DogeCloudDeployer implements Deployer {
|
||||
return d.data as T;
|
||||
}
|
||||
|
||||
/**
|
||||
* 只读探活:列一次 CDN 域名,验证 AK/SK 与连通性。
|
||||
*
|
||||
* ★ 与 OnePanelDeployer.ping 保持**同一签名**(返回对象、不抛错),
|
||||
* 这样调用方(cn-certkeeper 的 /preflight、Worker 的 /ssl/selfcheck)
|
||||
* 能统一处理,不必为每种部署器各写一套判错逻辑。
|
||||
*/
|
||||
async ping(): Promise<{ ok: boolean; error?: string; hint?: string; detail?: string }> {
|
||||
try {
|
||||
const d = await this.call<{ domains?: unknown[] }>('/cdn/domain/list.json', {});
|
||||
const n = Array.isArray(d?.domains) ? d.domains.length : 0;
|
||||
return { ok: true, detail: `${n} 个 CDN 域名` };
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
return {
|
||||
ok: false,
|
||||
error: msg,
|
||||
hint: /签名|signature|auth|TOKEN/i.test(msg)
|
||||
? 'AK/SK 不对或签名算法有变(多吉云 → 个人中心 → API 密钥)'
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async deploy(cert: DeployCert, opts: DeployOptions): Promise<DeployResult> {
|
||||
const log = opts.log || (() => {});
|
||||
const details: string[] = [];
|
||||
@@ -142,7 +166,8 @@ export class DogeCloudDeployer implements Deployer {
|
||||
const certId = await this.uploadOrReuse(cert, opts.dogecloudDomains || [], log);
|
||||
details.push(`证书 #${certId}`);
|
||||
|
||||
// ② 逐个域名绑定(★ 字段名是 cert_id,下划线)
|
||||
// ② 逐个域名绑定(★ 字段名是 `id` —— 实测传 `cert_id` 会被服务端**无视**,
|
||||
// 见本文件顶部「用假 id 999999 做对照实验」那段)
|
||||
const domains = (opts.dogecloudDomains || []).map((s) => s.trim()).filter(Boolean);
|
||||
if (!domains.length) {
|
||||
log('多吉云:没有配置要绑定的域名,只上传不绑定');
|
||||
|
||||
@@ -278,11 +278,107 @@ export class CloudflareDns implements DnsProvider {
|
||||
}
|
||||
}
|
||||
|
||||
// ==================================================================== 远程转发(国内机)
|
||||
|
||||
/**
|
||||
* 把 TXT 增删**委托给国内机上的 cn-dns-helper**(Docker 容器)。
|
||||
*
|
||||
* ★ 为什么需要这么一层:
|
||||
* Worker 侧的 `cloudflare` 凭据是 Workers/KV/D1 专用的,没有 Zone/DNS 权限,
|
||||
* 直接做 DNS-01 会被 CF 回 403。国内机的 certimate 里有一条**有完整 DNS 权限**
|
||||
* 的 token —— 与其让用户去 CF 后台重新签发 token、再往 Worker 塞一份写权限凭据,
|
||||
* 不如把「写 TXT」的执行权留在国内机,Worker 只持有一个**只能改 DNS 的窄权限**
|
||||
* 共享密钥。权限面更小,且复用现有基础设施。
|
||||
*
|
||||
* 契约见 deploy/cn-dns-helper/src/server.mjs。
|
||||
*/
|
||||
export class RemoteDns implements DnsProvider {
|
||||
readonly kind = 'remote';
|
||||
|
||||
constructor(
|
||||
private readonly baseUrl: string,
|
||||
private readonly helperToken: string,
|
||||
) {
|
||||
if (!baseUrl) throw new Error('远程 DNS 凭据缺少 serverUrl');
|
||||
if (!helperToken) throw new Error('远程 DNS 凭据缺少 helperToken');
|
||||
}
|
||||
|
||||
private async call(path: string, init: RequestInit): Promise<any> {
|
||||
const url = this.baseUrl.replace(/\/+$/, '') + path;
|
||||
let resp: Response;
|
||||
try {
|
||||
resp = await fetch(url, {
|
||||
...init,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Auth-Token': this.helperToken,
|
||||
...(init.headers || {}),
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
// 网络层错误单独包装:国内机不可达时,报错要能一眼看出是「通道」问题
|
||||
throw new Error(`连不上 DNS 助手(${url}):${e instanceof Error ? e.message : String(e)}`);
|
||||
}
|
||||
const text = await resp.text();
|
||||
let data: any;
|
||||
try {
|
||||
data = JSON.parse(text);
|
||||
} catch {
|
||||
throw new Error(`DNS 助手返回非 JSON(HTTP ${resp.status}):${text.slice(0, 200)}`);
|
||||
}
|
||||
if (resp.status === 401) throw new Error('DNS 助手鉴权失败(helperToken 不匹配)');
|
||||
if (!resp.ok) throw new Error(`DNS 助手出错(HTTP ${resp.status}):${data?.error || text.slice(0, 200)}`);
|
||||
return data;
|
||||
}
|
||||
|
||||
/** `_acme-challenge.200181.xyz` → zone=`200181.xyz`、记录名保持全名(CF 接受 FQDN) */
|
||||
private split(fqdn: string): { zone: string; name: string } {
|
||||
const name = fqdn.replace(/\.$/, '').toLowerCase();
|
||||
return { zone: this.rootOf(name), name };
|
||||
}
|
||||
|
||||
private rootOf(name: string): string {
|
||||
const parts = name.split('.');
|
||||
if (parts.length <= 2) return name;
|
||||
return parts.slice(-2).join('.');
|
||||
}
|
||||
|
||||
async addTxt(fqdn: string, value: string): Promise<void> {
|
||||
const { zone, name } = this.split(fqdn);
|
||||
await this.call('/dns/txt', { method: 'POST', body: JSON.stringify({ zone, name, value }) });
|
||||
}
|
||||
|
||||
async delTxt(fqdn: string, value: string): Promise<void> {
|
||||
const { zone, name } = this.split(fqdn);
|
||||
await this.call('/dns/del', { method: 'POST', body: JSON.stringify({ zone, name, value }) });
|
||||
}
|
||||
|
||||
async listTxt(fqdn: string): Promise<string[]> {
|
||||
const { zone, name } = this.split(fqdn);
|
||||
const out = await this.call(`/dns/list?zone=${encodeURIComponent(zone)}&name=${encodeURIComponent(name)}`, {
|
||||
method: 'GET',
|
||||
});
|
||||
return (out?.records || []).map((r: any) => String(r.value));
|
||||
}
|
||||
|
||||
/** 只读探活:selfcheck 用,验证国内机通道 + token 是否都好使 */
|
||||
async ping(): Promise<string> {
|
||||
const zone = this.pingZone;
|
||||
const out = await this.call(`/dns/list?zone=${encodeURIComponent(zone)}&name=_acme-selfcheck.${zone}`, {
|
||||
method: 'GET',
|
||||
});
|
||||
return `可达(zone ${zone} 下现有 ${(out?.records || []).length} 条 TXT)`;
|
||||
}
|
||||
|
||||
/** ping 时用哪个 zone 探(构造时按凭据的 zoneName 指定,默认 200181.xyz) */
|
||||
pingZone = '200181.xyz';
|
||||
}
|
||||
|
||||
// ==================================================================== 工厂
|
||||
|
||||
/**
|
||||
* 按凭据记录造一个 DNS 客户端。
|
||||
* ★ 只认识 tencentcloud / cloudflare 两类;其余类型抛错而不是静默返回 null ——
|
||||
* ★ 只认识 tencentcloud / cloudflare / remote 三类;其余类型抛错而不是静默返回 null ——
|
||||
* 静默返回会让「配置错了」表现为「验证一直 pending 到超时」,排查起来很痛苦。
|
||||
*/
|
||||
export function makeDnsProvider(rec: AccessRecord): DnsProvider {
|
||||
@@ -303,7 +399,15 @@ export function makeDnsProvider(rec: AccessRecord): DnsProvider {
|
||||
rec.zoneName ? String(rec.zoneName) : undefined,
|
||||
);
|
||||
}
|
||||
throw new Error(`DNS-01 不支持凭据类型「${t}」(目前只支持 tencentcloud / cloudflare)`);
|
||||
if (t === 'remote') {
|
||||
// 走国内机 cn-dns-helper 转发(用于 CF token 无 DNS 权限的场景)
|
||||
const base = String(rec.serverUrl || '');
|
||||
const ht = String(rec.helperToken || '');
|
||||
const p = new RemoteDns(base, ht);
|
||||
if (rec.zoneName) p.pingZone = String(rec.zoneName);
|
||||
return p;
|
||||
}
|
||||
throw new Error(`DNS-01 不支持凭据类型「${t}」(支持 tencentcloud / cloudflare / remote)`);
|
||||
}
|
||||
|
||||
// ==================================================================== crypto 小工具
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* 证书管家的**域名配置唯一事实源**。
|
||||
*
|
||||
* 为什么单独抽出来(2026-10-06):
|
||||
* 这份配置有两个消费方 ——
|
||||
* ① `seed-ssl-config.mjs`:写进 Cloudflare KV(Worker 侧 UI / 探针 / 自检用)
|
||||
* ② `export-certkeeper-data.mjs`:生成国内机 cn-certkeeper 的数据目录
|
||||
* 原先 CONFIG 内联在 seed 脚本里,而那个脚本**一被 import 就执行**
|
||||
* (读 .env、连 KV 开始写),第 ② 个消费方没法安全复用。
|
||||
* 抽出来之后两边共用一份定义,不会出现「改了 KV 忘了改国内机」。
|
||||
*
|
||||
* ★ 站点名必须与 1Panel 里的 `primaryDomain` 或 `alias` **精确对上**,
|
||||
* 否则部署时会被跳过。下面这些是 2026-10-06 从面板实测出来的。
|
||||
*/
|
||||
|
||||
export const CONFIG = {
|
||||
version: 1,
|
||||
notify: { emails: ['177018615@qq.com'], daysBefore: 30 },
|
||||
domains: [
|
||||
{
|
||||
name: 'usj.cc',
|
||||
san: ['usj.cc', '*.usj.cc'],
|
||||
dns: 'tencent-usj',
|
||||
deploy: ['dogecloud', '1panel'],
|
||||
// ★ 站点名必须与 1Panel 里的 `primaryDomain` 或 `alias` 精确对上,
|
||||
// 否则部署时会被跳过。下面这些是 2026-10-06 从面板实测出来的
|
||||
// (面板上**没有** primaryDomain 为 `usj.cc` 的网站 —— 它只是证书名)。
|
||||
dogecloud_domains: ['usj.cc', 'www.usj.cc', 'artalk.usj.cc'],
|
||||
one_panel_sites: [
|
||||
'artalk.usj.cc', // blog 评论后端
|
||||
'openlist.usj.cc', // 网盘
|
||||
'wifi.usj.cc',
|
||||
'openwrt.usj.cc',
|
||||
'vw.usj.cc', // vaultwarden(alias 才是这个名字)
|
||||
],
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
name: 't-t.live',
|
||||
san: ['t-t.live', '*.t-t.live'],
|
||||
dns: 'tencent-tt',
|
||||
deploy: ['1panel'],
|
||||
one_panel_sites: ['t-t.live', 'www.t-t.live', 'pl.t-t.live', 'pwd.t-t.live', 'certd.t-t.live'],
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
name: '200181.xyz',
|
||||
san: ['200181.xyz', '*.200181.xyz'],
|
||||
dns: 'cloudflare',
|
||||
deploy: ['1panel'],
|
||||
one_panel_sites: ['ssh.200181.xyz'],
|
||||
disabled: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* 把证书管家的配置 + 凭据导出成**国内机 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);
|
||||
@@ -76,46 +76,10 @@ const ACCESS = SEEDS.access; // [{ name, type, note, fields: {...} }]
|
||||
// Cloudflare 的 `imql`(apiTokenForZone)没进来:现有 `cloudflare` 那条已够用。
|
||||
// (seeds 文件里同样只收了国内机那条,与当初迁移时的取舍一致。)
|
||||
|
||||
const CONFIG = {
|
||||
version: 1,
|
||||
notify: { emails: ['177018615@qq.com'], daysBefore: 30 },
|
||||
domains: [
|
||||
{
|
||||
name: 'usj.cc',
|
||||
san: ['usj.cc', '*.usj.cc'],
|
||||
dns: 'tencent-usj',
|
||||
deploy: ['dogecloud', '1panel'],
|
||||
// ★ 站点名必须与 1Panel 里的 `primaryDomain` 或 `alias` 精确对上,
|
||||
// 否则部署时会被跳过。下面这些是 2026-10-06 从面板实测出来的
|
||||
// (面板上**没有** primaryDomain 为 `usj.cc` 的网站 —— 它只是证书名)。
|
||||
dogecloud_domains: ['usj.cc', 'www.usj.cc', 'artalk.usj.cc'],
|
||||
one_panel_sites: [
|
||||
'artalk.usj.cc', // blog 评论后端
|
||||
'openlist.usj.cc', // 网盘
|
||||
'wifi.usj.cc',
|
||||
'openwrt.usj.cc',
|
||||
'vw.usj.cc', // vaultwarden(alias 才是这个名字)
|
||||
],
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
name: 't-t.live',
|
||||
san: ['t-t.live', '*.t-t.live'],
|
||||
dns: 'tencent-tt',
|
||||
deploy: ['1panel'],
|
||||
one_panel_sites: ['t-t.live', 'www.t-t.live', 'pl.t-t.live', 'pwd.t-t.live', 'certd.t-t.live'],
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
name: '200181.xyz',
|
||||
san: ['200181.xyz', '*.200181.xyz'],
|
||||
dns: 'cloudflare',
|
||||
deploy: ['1panel'],
|
||||
one_panel_sites: ['ssh.200181.xyz'],
|
||||
disabled: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
// ★ CONFIG 已抽到 ./certkeeper-config.mjs(唯一事实源,国内机导出脚本也用同一份)
|
||||
// 见那个文件的头注释:原先是内联在这里,但 export-certkeeper-data.mjs
|
||||
// 需要复用却又不能 import 本脚本(本脚本一 import 就会连 KV 开始写)。
|
||||
import { CONFIG } from './certkeeper-config.mjs';
|
||||
|
||||
// ---------------------------------------------------------------- 写入
|
||||
const devVars = readFileSync(join(ROOT, '.dev.vars'), 'utf8');
|
||||
|
||||
Reference in new issue
Block a user