diff --git a/AGENTS.md b/AGENTS.md index 3fb9c8ca2..9ff15c991 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -142,6 +142,12 @@ Certd 是一个支持私有化部署的 SSL/TLS 证书自动化管理平台。 如果只是某个服务商或部署目标的问题,不要轻易修改共享 pipeline/core 行为,除非确实是可复用的公共能力。 +### ACME / EAB 注意事项 + +- 公共 EAB(尤其是 Google EAB)可能只能创建一次 ACME 账号。要跨用户复用公共 EAB,应保存并复用同一个 ACME account private key;`accountUrl` 如果存到 `userContext` 里,只能视为当前用户缓存,因为 `userContext` 跟用户 id 走。 +- ACME 协议的 `newAccount` 支持 `onlyReturnExisting`。使用同一个 account private key 调用 `newAccount({ onlyReturnExisting: true })` 可以取回已创建账号的 URL,且不会再次消费 EAB。 +- 修改 EAB 的 `kid` 后,应重新生成绑定该 `kid` 的 account private key;否则应阻止继续申请并提示用户刷新账号私钥。 + ## 数据与迁移 后端使用 TypeORM 实体加 SQL 迁移。 @@ -161,6 +167,7 @@ Certd 是一个支持私有化部署的 SSL/TLS 证书自动化管理平台。 - 优先沿用现有模块、插件、服务模式,再考虑新增抽象。 - `packages/ui/certd-server/data/`、`logs/`、生成的 metadata/dist 等通常视为运行时或构建产物,除非任务明确要求处理它们。 - 注意本地数据和配置里可能包含凭据、证书材料等敏感信息。 +- 本仓库代码注释优先使用中文,尤其是解释业务规则、兼容逻辑、协议细节和隐藏风险时;除非文件已有明确英文注释风格或引用外部英文术语,否则不要新增英文说明性注释。 ## 插件开发技能 diff --git a/packages/core/acme-client/package.json b/packages/core/acme-client/package.json index 30345c6e2..132780365 100644 --- a/packages/core/acme-client/package.json +++ b/packages/core/acme-client/package.json @@ -35,10 +35,12 @@ "@typescript-eslint/parser": "^8.26.1", "chai": "^4.4.1", "chai-as-promised": "^7.1.2", + "cross-env": "^7.0.3", "eslint": "^8.57.0", "eslint-config-prettier": "^8.5.0", "eslint-plugin-import": "^2.29.1", "eslint-plugin-prettier": "^4.2.1", + "esmock": "^2.7.5", "jsdoc-to-markdown": "^8.0.1", "mocha": "^10.6.0", "nock": "^13.5.4", @@ -55,7 +57,7 @@ "prepublishOnly": "npm run build && npm run build-docs", "test": "mocha -t 60000 \"test/setup.js\" \"test/**/*.spec.js\"", "before-test:unit": "node -e \"const fs=require('fs');fs.rmSync('dist-test',{recursive:true,force:true});fs.rmSync('tsconfig.test.tsbuildinfo',{force:true});\"", - "test:unit": "npm run before-test:unit && tsc -p tsconfig.test.json --skipLibCheck && mocha -t 60000 \"dist-test/**/*.test.js\"", + "test:unit": "cross-env NODE_ENV=unittest npm run before-test:unit && cross-env NODE_ENV=unittest tsc -p tsconfig.test.json --skipLibCheck && cross-env NODE_ENV=unittest mocha -t 60000 \"dist-test/**/*.test.js\"", "pub": "npm publish", "compile": "tsc --skipLibCheck --watch" }, diff --git a/packages/core/basic/package.json b/packages/core/basic/package.json index c9ac8efa7..a603b1da4 100644 --- a/packages/core/basic/package.json +++ b/packages/core/basic/package.json @@ -13,7 +13,7 @@ "dev-build": "npm run build", "preview": "vite preview", "test": "mocha --loader=ts-node/esm", - "test:unit": "mocha --node-option no-warnings --node-option loader=ts-node/esm \"src/**/*.test.ts\"", + "test:unit": "cross-env NODE_ENV=unittest mocha --node-option no-warnings --node-option loader=ts-node/esm \"src/**/*.test.ts\"", "pub": "npm publish", "compile": "tsc --skipLibCheck --watch" }, @@ -40,9 +40,11 @@ "@typescript-eslint/eslint-plugin": "^8.26.1", "@typescript-eslint/parser": "^8.26.1", "chai": "4.3.10", + "cross-env": "^7.0.3", "eslint": "^8.41.0", "eslint-config-prettier": "^8.8.0", "eslint-plugin-prettier": "^4.2.1", + "esmock": "^2.7.5", "mocha": "^10.2.0", "prettier": "^2.8.8", "rimraf": "^5.0.5", diff --git a/packages/core/pipeline/package.json b/packages/core/pipeline/package.json index a2b7e3274..75ab1fc2e 100644 --- a/packages/core/pipeline/package.json +++ b/packages/core/pipeline/package.json @@ -14,7 +14,7 @@ "build3": "rollup -c", "preview": "vite preview", "test": "mocha --loader=ts-node/esm", - "test:unit": "mocha --no-config --node-option no-warnings --node-option loader=ts-node/esm \"src/**/*.test.ts\"", + "test:unit": "cross-env NODE_ENV=unittest mocha --no-config --node-option no-warnings --node-option loader=ts-node/esm \"src/**/*.test.ts\"", "pub": "npm publish", "compile": "tsc --skipLibCheck --watch" }, @@ -37,9 +37,11 @@ "@typescript-eslint/eslint-plugin": "^8.26.1", "@typescript-eslint/parser": "^8.26.1", "chai": "4.3.10", + "cross-env": "^7.0.3", "eslint": "^8.41.0", "eslint-config-prettier": "^8.8.0", "eslint-plugin-prettier": "^4.2.1", + "esmock": "^2.7.5", "mocha": "^10.2.0", "prettier": "^2.8.8", "rimraf": "^5.0.5", diff --git a/packages/libs/lib-huawei/package.json b/packages/libs/lib-huawei/package.json index f758486af..234828966 100644 --- a/packages/libs/lib-huawei/package.json +++ b/packages/libs/lib-huawei/package.json @@ -11,7 +11,7 @@ "build": "npm run before-build && rollup -c ", "dev-build": "npm run build", "preview": "vite preview", - "test:unit": "echo no unit tests", + "test:unit": "cross-env NODE_ENV=unittest echo no unit tests", "pub": "npm publish" }, "dependencies": { @@ -22,6 +22,8 @@ "devDependencies": { "@typescript-eslint/eslint-plugin": "^8.26.1", "@typescript-eslint/parser": "^8.26.1", + "cross-env": "^7.0.3", + "esmock": "^2.7.5", "prettier": "^2.8.8", "tslib": "^2.8.1" }, diff --git a/packages/libs/lib-iframe/package.json b/packages/libs/lib-iframe/package.json index 27e2c440c..057cc3517 100644 --- a/packages/libs/lib-iframe/package.json +++ b/packages/libs/lib-iframe/package.json @@ -14,7 +14,7 @@ "build3": "rollup -c", "build2": "vue-tsc --noEmit && vite build", "preview": "vite preview", - "test:unit": "echo no unit tests", + "test:unit": "cross-env NODE_ENV=unittest echo no unit tests", "pub": "npm publish" }, "dependencies": { @@ -24,9 +24,11 @@ "@types/chai": "^4.3.3", "@typescript-eslint/eslint-plugin": "^8.26.1", "@typescript-eslint/parser": "^8.26.1", + "cross-env": "^7.0.3", "eslint": "^8.24.0", "eslint-config-prettier": "^8.5.0", "eslint-plugin-prettier": "^4.2.1", + "esmock": "^2.7.5", "prettier": "^2.8.8", "rimraf": "^5.0.5", "tslib": "^2.8.1", diff --git a/packages/libs/lib-jdcloud/package.json b/packages/libs/lib-jdcloud/package.json index d07e27e41..4c247cbc9 100644 --- a/packages/libs/lib-jdcloud/package.json +++ b/packages/libs/lib-jdcloud/package.json @@ -8,7 +8,7 @@ "scripts": { "build": "rollup -c ", "dev-build": "npm run build", - "test:unit": "echo no unit tests", + "test:unit": "cross-env NODE_ENV=unittest echo no unit tests", "pub": "npm publish" }, "author": "", @@ -30,7 +30,8 @@ "@typescript-eslint/parser": "^8.26.1", "chai": "^4.1.2", "config": "^1.30.0", - "cross-env": "^5.1.4", + "cross-env": "^7.0.3", + "esmock": "^2.7.5", "js-yaml": "^3.11.0", "mocha": "^5.0.0", "prettier": "^2.8.8", diff --git a/packages/libs/lib-k8s/package.json b/packages/libs/lib-k8s/package.json index 1cb356bd7..8030f76cc 100644 --- a/packages/libs/lib-k8s/package.json +++ b/packages/libs/lib-k8s/package.json @@ -14,7 +14,7 @@ "build3": "rollup -c", "build2": "vue-tsc --noEmit && vite build", "preview": "vite preview", - "test:unit": "echo no unit tests", + "test:unit": "cross-env NODE_ENV=unittest echo no unit tests", "pub": "npm publish", "compile": "tsc --skipLibCheck --watch" }, @@ -26,9 +26,11 @@ "@types/chai": "^4.3.3", "@typescript-eslint/eslint-plugin": "^8.26.1", "@typescript-eslint/parser": "^8.26.1", + "cross-env": "^7.0.3", "eslint": "^8.24.0", "eslint-config-prettier": "^8.5.0", "eslint-plugin-prettier": "^4.2.1", + "esmock": "^2.7.5", "prettier": "^2.8.8", "rimraf": "^5.0.5", "tslib": "^2.8.1", diff --git a/packages/libs/lib-server/package.json b/packages/libs/lib-server/package.json index fb3709b06..db2a26a96 100644 --- a/packages/libs/lib-server/package.json +++ b/packages/libs/lib-server/package.json @@ -12,7 +12,7 @@ "build": "npm run before-build && tsc --skipLibCheck", "dev-build": "npm run build", "test": "midway-bin test --ts -V", - "test:unit": "mocha --no-config --node-option no-warnings --node-option loader=ts-node/esm \"src/**/*.test.ts\"", + "test:unit": "cross-env NODE_ENV=unittest mocha --no-config --node-option no-warnings --node-option loader=ts-node/esm \"src/**/*.test.ts\"", "test1": "midway-bin test --ts -V -f test/blank.test.ts -t 'hash-check'", "cov": "midway-bin cov --ts", "lint": "mwts check", @@ -44,7 +44,6 @@ "@midwayjs/upload": "3.20.13", "@midwayjs/validate": "3.20.13", "better-sqlite3": "^11.1.2", - "cross-env": "^7.0.3", "dayjs": "^1.11.7", "lodash-es": "^4.17.21", "mwts": "^1.3.0", @@ -57,9 +56,11 @@ "@types/node": "^18", "@typescript-eslint/eslint-plugin": "^8.26.1", "@typescript-eslint/parser": "^8.26.1", + "cross-env": "^7.0.3", "eslint": "^8.24.0", "eslint-config-prettier": "^8.5.0", "eslint-plugin-prettier": "^4.2.1", + "esmock": "^2.7.5", "mocha": "^10.2.0", "prettier": "^2.8.8", "rimraf": "^5.0.5", diff --git a/packages/libs/lib-server/src/user/access/service/access-service.test.ts b/packages/libs/lib-server/src/user/access/service/access-service.test.ts new file mode 100644 index 000000000..4e9758eb4 --- /dev/null +++ b/packages/libs/lib-server/src/user/access/service/access-service.test.ts @@ -0,0 +1,30 @@ +import assert from "assert"; +import { AccessService } from "./access-service.js"; + +describe("AccessService", () => { + it("does not write id into access setting when updating selected fields", async () => { + let updateParam: any; + const service = new AccessService(); + service.info = async () => ({ + id: 12, + type: "eab", + } as any); + service.decryptAccessEntity = () => ({ + kid: "kid-1", + }); + service.update = async (param: any) => { + updateParam = param; + return param; + }; + + await service.updateAccess({ + id: 12, + accountKey: "account-key", + }); + + assert.deepEqual(JSON.parse(updateParam.setting), { + kid: "kid-1", + accountKey: "account-key", + }); + }); +}); diff --git a/packages/libs/lib-server/src/user/access/service/access-service.ts b/packages/libs/lib-server/src/user/access/service/access-service.ts index f2d92ee0f..8bbb81e90 100644 --- a/packages/libs/lib-server/src/user/access/service/access-service.ts +++ b/packages/libs/lib-server/src/user/access/service/access-service.ts @@ -123,6 +123,25 @@ export class AccessService extends BaseService { return await super.update(param); } + async updateAccess(access: any) { + const oldEntity = await this.info(access.id); + if (oldEntity == null) { + throw new ValidateException('该授权配置不存在,请确认是否已被删除'); + } + const setting = this.decryptAccessEntity(oldEntity); + for (const key of Object.keys(access)) { + if (key === 'id') { + continue; + } + setting[key] = access[key]; + } + return await this.update({ + id: access.id, + type: oldEntity.type, + setting: JSON.stringify(setting), + }); + } + async getSimpleInfo(id: number) { const entity = await this.info(id); if (entity == null) { diff --git a/packages/libs/midway-flyway-js/package.json b/packages/libs/midway-flyway-js/package.json index 3867f0cbb..5264337a6 100644 --- a/packages/libs/midway-flyway-js/package.json +++ b/packages/libs/midway-flyway-js/package.json @@ -13,7 +13,7 @@ "dev-build": "npm run build", "test": "midway-bin test --ts -V", "test1": "midway-bin test --ts -V -f test/blank.test.ts -t 'hash-check'", - "test:unit": "echo no unit tests", + "test:unit": "cross-env NODE_ENV=unittest echo no unit tests", "cov": "midway-bin cov --ts", "prepublish": "npm run build", "pub": "npm publish" @@ -36,11 +36,13 @@ "@types/node": "^18", "@typescript-eslint/eslint-plugin": "^8.26.1", "@typescript-eslint/parser": "^8.26.1", + "cross-env": "^7.0.3", "eslint": "^8.24.0", "eslint-config-prettier": "^8.5.0", "eslint-plugin-import": "^2.26.0", "eslint-plugin-node": "^11.1.0", "eslint-plugin-prettier": "^4.2.1", + "esmock": "^2.7.5", "prettier": "^2.8.8", "rimraf": "^5.0.5", "tslib": "^2.8.1", diff --git a/packages/plugins/plugin-cert/package.json b/packages/plugins/plugin-cert/package.json index c3f8310a5..c5d4840aa 100644 --- a/packages/plugins/plugin-cert/package.json +++ b/packages/plugins/plugin-cert/package.json @@ -13,7 +13,7 @@ "build3": "rollup -c", "build2": "vue-tsc --noEmit && vite build", "preview": "vite preview", - "test:unit": "echo no unit tests", + "test:unit": "cross-env NODE_ENV=unittest echo no unit tests", "pub": "npm publish", "compile": "tsc --skipLibCheck --watch" }, @@ -31,9 +31,11 @@ "@typescript-eslint/eslint-plugin": "^8.26.1", "@typescript-eslint/parser": "^8.26.1", "chai": "^4.3.6", + "cross-env": "^7.0.3", "eslint": "^8.24.0", "eslint-config-prettier": "^8.5.0", "eslint-plugin-prettier": "^4.2.1", + "esmock": "^2.7.5", "mocha": "^10.1.0", "prettier": "^2.8.8", "tslib": "^2.8.1", diff --git a/packages/plugins/plugin-lib/package.json b/packages/plugins/plugin-lib/package.json index a55205377..0a863d961 100644 --- a/packages/plugins/plugin-lib/package.json +++ b/packages/plugins/plugin-lib/package.json @@ -13,7 +13,7 @@ "build3": "rollup -c", "build2": "vue-tsc --noEmit && vite build", "preview": "vite preview", - "test:unit": "mocha --no-config --node-option no-warnings --node-option loader=ts-node/esm \"src/**/*.test.ts\"", + "test:unit": "cross-env NODE_ENV=unittest mocha --no-config --node-option no-warnings --node-option loader=ts-node/esm \"src/**/*.test.ts\"", "pub": "npm publish", "compile": "tsc --skipLibCheck --watch" }, @@ -50,9 +50,11 @@ "@typescript-eslint/eslint-plugin": "^8.26.1", "@typescript-eslint/parser": "^8.26.1", "chai": "^4.3.6", + "cross-env": "^7.0.3", "eslint": "^8.24.0", "eslint-config-prettier": "^8.5.0", "eslint-plugin-prettier": "^4.2.1", + "esmock": "^2.7.5", "mocha": "^10.1.0", "prettier": "^2.8.8", "ts-node": "^10.9.2", diff --git a/packages/ui/certd-client/package.json b/packages/ui/certd-client/package.json index c2980ed5f..f2087d8e9 100644 --- a/packages/ui/certd-client/package.json +++ b/packages/ui/certd-client/package.json @@ -12,7 +12,7 @@ "debug:force": "vite --force --mode debug", "build": "cross-env NODE_OPTIONS=--max-old-space-size=40960 vite build ", "dev-build": "echo 1", - "test:unit": "echo no unit tests", + "test:unit": "cross-env NODE_ENV=unittest echo no unit tests", "test:vue": "vitest run", "serve": "vite preview", "preview": "vite preview", @@ -62,7 +62,6 @@ "cos-js-sdk-v5": "^1.7.0", "cron-parser": "^4.9.0", "cropperjs": "^1.6.1", - "cross-env": "^7.0.3", "cssnano": "^7.0.6", "dayjs": "^1.11.7", "defu": "^6.1.4", @@ -127,6 +126,7 @@ "autoprefixer": "^10.4.21", "caller-path": "^4.0.0", "chai": "^5.1.0", + "cross-env": "^7.0.3", "dependency-cruiser": "^16.2.3", "dot": "^1.1.3", "eslint": "8.57.0", @@ -136,6 +136,7 @@ "eslint-plugin-prettier": "^5.1.3", "eslint-plugin-promise": "^6.1.1", "eslint-plugin-vue": "^9.23.0", + "esmock": "^2.7.5", "less": "^4.2.0", "less-loader": "^12.2.0", "postcss": "^8.4.35", diff --git a/packages/ui/certd-client/src/components/plugins/common/refresh-input.vue b/packages/ui/certd-client/src/components/plugins/common/refresh-input.vue new file mode 100644 index 000000000..8139a8c57 --- /dev/null +++ b/packages/ui/certd-client/src/components/plugins/common/refresh-input.vue @@ -0,0 +1,106 @@ + + + + + diff --git a/packages/ui/certd-client/src/components/plugins/index.ts b/packages/ui/certd-client/src/components/plugins/index.ts index 117d66864..393e2d10d 100644 --- a/packages/ui/certd-client/src/components/plugins/index.ts +++ b/packages/ui/certd-client/src/components/plugins/index.ts @@ -12,6 +12,7 @@ import AccessSelector from "/@/views/certd/access/access-selector/index.vue"; import InputPassword from "./common/input-password.vue"; import CertInfoUpdater from "/@/views/certd/pipeline/cert-upload/index.vue"; import ApiTest from "./common/api-test.vue"; +import RefreshInput from "./common/refresh-input.vue"; import ParamsShow from "./common/params-show.vue"; export * from "./cert/index.js"; export default { @@ -23,6 +24,7 @@ export default { app.component("CertInfoUpdater", CertInfoUpdater); app.component("ApiTest", ApiTest); + app.component("RefreshInput", RefreshInput); app.component("SynologyDeviceIdGetter", SynologyIdDeviceGetter); app.component("RemoteAutoComplete", RemoteAutoComplete); diff --git a/packages/ui/certd-server/package.json b/packages/ui/certd-server/package.json index 4984a4d2e..b1fa7a637 100644 --- a/packages/ui/certd-server/package.json +++ b/packages/ui/certd-server/package.json @@ -101,7 +101,6 @@ "cache-manager": "^6.1.0", "cos-nodejs-sdk-v5": "^2.14.6", "cron-parser": "^4.9.0", - "cross-env": "^7.0.3", "crypto-js": "^4.2.0", "dayjs": "^1.11.7", "esdk-obs-nodejs": "^3.25.6", @@ -159,6 +158,8 @@ "@types/node": "^18", "@types/nodemailer": "^6.4.8", "c8": "^10.1.2", + "cross-env": "^7.0.3", + "esmock": "^2.7.5", "mocha": "^10.2.0", "prettier": "^2.8.8", "rimraf": "^5.0.5", diff --git a/packages/ui/certd-server/src/modules/auto/auto-c-register-cron.ts b/packages/ui/certd-server/src/modules/auto/auto-cron.ts similarity index 98% rename from packages/ui/certd-server/src/modules/auto/auto-c-register-cron.ts rename to packages/ui/certd-server/src/modules/auto/auto-cron.ts index f84745a83..879479b7b 100644 --- a/packages/ui/certd-server/src/modules/auto/auto-c-register-cron.ts +++ b/packages/ui/certd-server/src/modules/auto/auto-cron.ts @@ -1,7 +1,7 @@ import { logger } from '@certd/basic'; import { SysSettingsService, SysSiteInfo } from '@certd/lib-server'; import { getPlusInfo, isPlus } from "@certd/plus-core"; -import { Autoload, Config, Init, Inject, Scope, ScopeEnum } from '@midwayjs/core'; +import { Config, Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core'; import dayjs from "dayjs"; import { Between } from "typeorm"; import { DomainService } from '../cert/service/domain-service.js'; @@ -14,9 +14,9 @@ import { PipelineService } from '../pipeline/service/pipeline-service.js'; import { UserService } from "../sys/authority/service/user-service.js"; import { ProjectService } from '../sys/enterprise/service/project-service.js'; -@Autoload() +@Provide() @Scope(ScopeEnum.Request, { allowDowngrade: true }) -export class AutoCRegisterCron { +export class AutoCron { @Inject() pipelineService: PipelineService; @@ -53,7 +53,6 @@ export class AutoCRegisterCron { - @Init() async init() { logger.info('加载定时trigger开始'); await this.pipelineService.onStartup(this.immediateTriggerOnce, this.onlyAdminUser); diff --git a/packages/ui/certd-server/src/modules/auto/auto-fix.test.ts b/packages/ui/certd-server/src/modules/auto/auto-fix.test.ts new file mode 100644 index 000000000..101ee5af0 --- /dev/null +++ b/packages/ui/certd-server/src/modules/auto/auto-fix.test.ts @@ -0,0 +1,182 @@ +import assert from "assert"; +import esmock from "esmock"; +import { AutoFix, buildEabAccountKeyValue, buildLegacyGoogleAccountConfigWhere, parseStorageValue } from "./auto-fix.js"; + +function createAutoFix(options: { pluginConfigService?: any; accessService?: any; storageService?: any }) { + const autoFix = new AutoFix(); + autoFix.pluginConfigService = options.pluginConfigService; + autoFix.accessService = options.accessService; + autoFix.storageService = options.storageService; + return autoFix; +} + +describe("AutoFix", () => { + it("parses legacy storage values", () => { + const config = parseStorageValue( + JSON.stringify({ + value: { + key: "legacy-private-key", + accountUrl: "https://example.com/acct/1", + }, + }) + ); + + assert.equal(config.key, "legacy-private-key"); + }); + + it("builds the EAB account key payload", () => { + const payload = JSON.parse(buildEabAccountKeyValue("kid-1", "private-key")); + + assert.deepEqual(payload, { + kid: "kid-1", + privateKey: "private-key", + }); + }); + + it("builds legacy Google account config query by exact email key only", () => { + assert.deepEqual(buildLegacyGoogleAccountConfigWhere("user@example.com"), { + userId: 1, + scope: "user", + namespace: "1", + key: "acme.config.google.user@example.com", + }); + }); + + it("finds legacy Google account config by exact email key only", async () => { + let findOneWhere: any; + let findCalled = false; + const autoFix = createAutoFix({ + pluginConfigService: null as any, + accessService: null as any, + storageService: { + getRepository() { + return { + async findOne(options: any) { + findOneWhere = options.where; + return { + value: JSON.stringify({ + value: { + privateKey: "legacy-private-key", + }, + }), + }; + }, + async find() { + findCalled = true; + return []; + }, + }; + }, + } as any, + }); + + const config = await autoFix.getLegacyGoogleAccountConfig("user@example.com"); + + assert.equal(config.privateKey, "legacy-private-key"); + assert.deepEqual(findOneWhere, buildLegacyGoogleAccountConfigWhere("user@example.com")); + assert.equal(findCalled, false); + }); + + it("does not query legacy Google account config without email", async () => { + let repositoryCalled = false; + const autoFix = createAutoFix({ + pluginConfigService: null as any, + accessService: null as any, + storageService: { + getRepository() { + repositoryCalled = true; + return {}; + }, + } as any, + }); + + const config = await autoFix.getLegacyGoogleAccountConfig(); + + assert.equal(config, null); + assert.equal(repositoryCalled, false); + }); + + it("skips Google common EAB account key fix outside commercial edition", async () => { + let pluginConfigCalled = false; + const autoFix = createAutoFix({ + pluginConfigService: { + async getPluginConfig() { + pluginConfigCalled = true; + return null; + }, + } as any, + accessService: null as any, + storageService: null as any, + }); + + await autoFix.init(); + + assert.equal(pluginConfigCalled, false); + }); + + it("fixes Google common EAB account key in commercial edition", async () => { + const { AutoFix: MockedAutoFix } = await esmock("./auto-fix.js", { + "@certd/plus-core": { + isComm: () => true, + }, + }); + let getAccessByIdArgs: any[] = []; + let findOneWhere: any; + let updateAccessParam: any; + const autoFix = new MockedAutoFix(); + autoFix.pluginConfigService = { + async getPluginConfig(options: any) { + assert.deepEqual(options, { + name: "CertApply", + type: "builtIn", + }); + return { + sysSetting: { + input: { + googleCommonEabAccessId: 12, + }, + }, + }; + }, + }; + autoFix.accessService = { + async getAccessById(...args: any[]) { + getAccessByIdArgs = args; + return { + kid: "kid-1", + email: "user@example.com", + }; + }, + async updateAccess(param: any) { + updateAccessParam = param; + }, + }; + autoFix.storageService = { + getRepository() { + return { + async findOne(options: any) { + findOneWhere = options.where; + return { + value: JSON.stringify({ + value: { + privateKey: "legacy-private-key", + }, + }), + }; + }, + }; + }, + }; + + await autoFix.fixGoogleCommonEabAccountKey(); + + assert.deepEqual(getAccessByIdArgs, [12, false]); + assert.deepEqual(findOneWhere, buildLegacyGoogleAccountConfigWhere("user@example.com")); + assert.deepEqual(updateAccessParam, { + id: 12, + eabType: "google", + accountKey: buildEabAccountKeyValue("kid-1", "legacy-private-key"), + }); + }); + +}); diff --git a/packages/ui/certd-server/src/modules/auto/auto-fix.ts b/packages/ui/certd-server/src/modules/auto/auto-fix.ts new file mode 100644 index 000000000..50f2975f2 --- /dev/null +++ b/packages/ui/certd-server/src/modules/auto/auto-fix.ts @@ -0,0 +1,107 @@ +import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core"; +import { logger } from "@certd/basic"; +import { AccessService } from "@certd/lib-server"; +import { isComm } from "@certd/plus-core"; +import { PluginConfigService } from "../plugin/service/plugin-config-service.js"; +import { StorageService } from "../pipeline/service/storage-service.js"; + +export function parseStorageValue(value?: string) { + if (!value) { + return null; + } + try { + const parsed = JSON.parse(value); + return parsed?.value || null; + } catch { + return null; + } +} + +export function buildEabAccountKeyValue(kid: string, privateKey: string) { + return JSON.stringify({ + kid, + privateKey, + }); +} + +export function buildLegacyGoogleAccountConfigWhere(email: string) { + return { + userId: 1, + scope: "user", + namespace: "1", + key: `acme.config.google.${email}`, + }; +} + +@Provide() +@Scope(ScopeEnum.Request, { allowDowngrade: true }) +export class AutoFix { + @Inject() + pluginConfigService: PluginConfigService; + + @Inject() + accessService: AccessService; + + @Inject() + storageService: StorageService; + + async init() { + await this.fixGoogleCommonEabAccountKey(); + } + async fixGoogleCommonEabAccountKey() { + if (!isComm()) { + return; + } + try { + const certApplyConfig = await this.pluginConfigService.getPluginConfig({ + name: "CertApply", + type: "builtIn", + }); + const googleCommonEabAccessId = certApplyConfig?.sysSetting?.input?.googleCommonEabAccessId; + if (!googleCommonEabAccessId) { + return; + } + + const eabAccess = await this.accessService.getAccessById(googleCommonEabAccessId, false); + if (eabAccess.accountKey) { + return; + } + if (!eabAccess.kid) { + logger.info("公共Google EAB授权缺少KID,跳过历史ACME账号私钥修复"); + return; + } + + const accountConfig = await this.getLegacyGoogleAccountConfig(eabAccess.email); + const privateKey = accountConfig?.privateKey || accountConfig?.key || accountConfig?.accountKey; + if (!privateKey) { + logger.info("未找到可迁移到公共Google EAB授权的历史ACME账号私钥"); + return; + } + + const accountKey = buildEabAccountKeyValue(eabAccess.kid, privateKey); + await this.accessService.updateAccess({ id: googleCommonEabAccessId, eabType: "google", accountKey }); + logger.info(`已修复公共Google EAB授权的ACME账号私钥,accessId=${googleCommonEabAccessId}`); + } catch (e: any) { + logger.error("修复公共Google EAB授权ACME账号私钥失败", e); + } + } + + async getLegacyGoogleAccountConfig(email?: string) { + if (!email) { + return null; + } + const repository = this.storageService.getRepository(); + const exact = await repository.findOne({ + where: buildLegacyGoogleAccountConfigWhere(email), + }); + const exactValue = this.parseStorageValue(exact?.value); + if (exactValue?.key || exactValue?.privateKey || exactValue?.accountKey) { + return exactValue; + } + return null; + } + + parseStorageValue(value?: string) { + return parseStorageValue(value); + } +} diff --git a/packages/ui/certd-server/src/modules/auto/auto-a-init-site.ts b/packages/ui/certd-server/src/modules/auto/auto-init-site.ts similarity index 95% rename from packages/ui/certd-server/src/modules/auto/auto-a-init-site.ts rename to packages/ui/certd-server/src/modules/auto/auto-init-site.ts index aedeeb8af..171aee397 100644 --- a/packages/ui/certd-server/src/modules/auto/auto-a-init-site.ts +++ b/packages/ui/certd-server/src/modules/auto/auto-init-site.ts @@ -1,14 +1,14 @@ import { logger } from '@certd/basic'; import { PlusService, SysInstallInfo, SysPrivateSettings, SysSettingsService } from '@certd/lib-server'; -import { Autoload, Config, Init, Inject, Scope, ScopeEnum } from '@midwayjs/core'; +import { Config, Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core'; import crypto from 'crypto'; import { nanoid } from 'nanoid'; import { UserService } from '../sys/authority/service/user-service.js'; import { SafeService } from "../sys/settings/safe-service.js"; -@Autoload() +@Provide() @Scope(ScopeEnum.Request, { allowDowngrade: true }) -export class AutoAInitSite { +export class AutoInitSite { @Inject() userService: UserService; @@ -22,7 +22,6 @@ export class AutoAInitSite { @Inject() safeService: SafeService; - @Init() async init() { logger.info('初始化站点开始'); await this.startOptimizeDb(); diff --git a/packages/ui/certd-server/src/modules/auto/auto-b-load-plugins.ts b/packages/ui/certd-server/src/modules/auto/auto-load-plugins.ts similarity index 89% rename from packages/ui/certd-server/src/modules/auto/auto-b-load-plugins.ts rename to packages/ui/certd-server/src/modules/auto/auto-load-plugins.ts index 1dfef42b8..2526c622c 100644 --- a/packages/ui/certd-server/src/modules/auto/auto-b-load-plugins.ts +++ b/packages/ui/certd-server/src/modules/auto/auto-load-plugins.ts @@ -1,16 +1,15 @@ -import { Autoload, Init, Inject, Scope, ScopeEnum } from "@midwayjs/core"; +import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core"; import { logger } from "@certd/basic"; import { PluginService } from "../plugin/service/plugin-service.js"; import { registerPaymentProviders } from "../suite/payments/index.js"; -@Autoload() +@Provide() @Scope(ScopeEnum.Request, { allowDowngrade: true }) -export class AutoBLoadPlugins { +export class AutoLoadPlugins { @Inject() pluginService: PluginService; - @Init() async init() { logger.info(`加载插件开始,加载模式:${process.env.certd_plugin_loadmode}`); if (process.env.certd_plugin_loadmode === "metadata") { diff --git a/packages/ui/certd-server/src/modules/auto/auto-d-mitter-register.ts b/packages/ui/certd-server/src/modules/auto/auto-mitter-register.ts similarity index 78% rename from packages/ui/certd-server/src/modules/auto/auto-d-mitter-register.ts rename to packages/ui/certd-server/src/modules/auto/auto-mitter-register.ts index 438d1703b..1b87f004a 100644 --- a/packages/ui/certd-server/src/modules/auto/auto-d-mitter-register.ts +++ b/packages/ui/certd-server/src/modules/auto/auto-mitter-register.ts @@ -1,14 +1,13 @@ import { logger, utils } from '@certd/basic'; import { UserSuiteService } from '@certd/commercial-core'; -import { Autoload, Init, Inject, Scope, ScopeEnum } from '@midwayjs/core'; +import { Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core'; -@Autoload() +@Provide() @Scope(ScopeEnum.Request, { allowDowngrade: true }) -export class AutoDMitterRegister { +export class AutoMitterRegister { @Inject() userSuiteService: UserSuiteService; - @Init() async init() { await this.registerOnNewUser(); } diff --git a/packages/ui/certd-server/src/modules/auto/auto-e-pipeline-emitter-register.ts b/packages/ui/certd-server/src/modules/auto/auto-pipeline-emitter-register.ts similarity index 76% rename from packages/ui/certd-server/src/modules/auto/auto-e-pipeline-emitter-register.ts rename to packages/ui/certd-server/src/modules/auto/auto-pipeline-emitter-register.ts index eb420fa69..12605c94a 100644 --- a/packages/ui/certd-server/src/modules/auto/auto-e-pipeline-emitter-register.ts +++ b/packages/ui/certd-server/src/modules/auto/auto-pipeline-emitter-register.ts @@ -1,21 +1,21 @@ -import { Autoload, Init, Inject, Scope, ScopeEnum } from "@midwayjs/core"; +import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core"; import { CertInfoService } from "../monitor/index.js"; import { pipelineEmitter } from "@certd/pipeline"; import { CertInfo, EVENT_CERT_APPLY_SUCCESS } from "@certd/plugin-cert"; import { PipelineEvent } from "@certd/pipeline"; -@Autoload() +@Provide() @Scope(ScopeEnum.Request, { allowDowngrade: true }) -export class AutoEPipelineEmitterRegister { +export class AutoPipelineEmitterRegister { @Inject() certInfoService: CertInfoService; - @Init() async init() { await this.onCertApplySuccess(); } + async onCertApplySuccess() { - pipelineEmitter.on(EVENT_CERT_APPLY_SUCCESS, async (event: PipelineEvent<{cert:CertInfo,file:string}>) => { + pipelineEmitter.on(EVENT_CERT_APPLY_SUCCESS, async (event: PipelineEvent<{ cert: CertInfo; file: string }>) => { await this.certInfoService.updateCertByPipelineId(event.pipeline.id, event.event.cert, event.event.file); }); } diff --git a/packages/ui/certd-server/src/modules/auto/auto-z.ts b/packages/ui/certd-server/src/modules/auto/auto-print.ts similarity index 96% rename from packages/ui/certd-server/src/modules/auto/auto-z.ts rename to packages/ui/certd-server/src/modules/auto/auto-print.ts index feefd5acb..79a9cda17 100644 --- a/packages/ui/certd-server/src/modules/auto/auto-z.ts +++ b/packages/ui/certd-server/src/modules/auto/auto-print.ts @@ -1,4 +1,4 @@ -import { App, Autoload, Config, Init, Inject, Scope, ScopeEnum } from '@midwayjs/core'; +import { App, Config, Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core'; import { getPlusInfo, isPlus } from '@certd/plus-core'; import { isDev, logger } from '@certd/basic'; @@ -11,9 +11,9 @@ import { UserService } from '../sys/authority/service/user-service.js'; import { UserSettingsService } from '../mine/service/user-settings-service.js'; import { startProxyServer } from './proxy/server.js'; -@Autoload() +@Provide() @Scope(ScopeEnum.Request, { allowDowngrade: true }) -export class AutoZPrint { +export class AutoPrint { @Inject() sysSettingsService: SysSettingsService; @@ -34,7 +34,6 @@ export class AutoZPrint { @Config('system.resetAdminPasswd') private resetAdminPasswd: boolean; - @Init() async init() { //监听https this.startHttpsServer(); diff --git a/packages/ui/certd-server/src/modules/auto/auto-register.ts b/packages/ui/certd-server/src/modules/auto/auto-register.ts new file mode 100644 index 000000000..6a7fbb120 --- /dev/null +++ b/packages/ui/certd-server/src/modules/auto/auto-register.ts @@ -0,0 +1,44 @@ +import { Autoload, Init, Inject, Scope, ScopeEnum } from "@midwayjs/core"; +import { AutoInitSite } from "./auto-init-site.js"; +import { AutoLoadPlugins } from "./auto-load-plugins.js"; +import { AutoCron } from "./auto-cron.js"; +import { AutoMitterRegister } from "./auto-mitter-register.js"; +import { AutoPipelineEmitterRegister } from "./auto-pipeline-emitter-register.js"; +import { AutoFix } from "./auto-fix.js"; +import { AutoPrint } from "./auto-print.js"; + +@Autoload() +@Scope(ScopeEnum.Request, { allowDowngrade: true }) +export class AutoRegister { + @Inject() + autoInitSite: AutoInitSite; + + @Inject() + autoLoadPlugins: AutoLoadPlugins; + + @Inject() + autoCron: AutoCron; + + @Inject() + autoMitterRegister: AutoMitterRegister; + + @Inject() + autoPipelineEmitterRegister: AutoPipelineEmitterRegister; + + @Inject() + autoPrint: AutoPrint; + + @Inject() + autoFix: AutoFix; + + @Init() + async init() { + await this.autoInitSite.init(); + await this.autoLoadPlugins.init(); + await this.autoCron.init(); + await this.autoMitterRegister.init(); + await this.autoPipelineEmitterRegister.init(); + await this.autoFix.init(); + await this.autoPrint.init(); + } +} diff --git a/packages/ui/certd-server/src/plugins/plugin-cert/access/eab-access.test.ts b/packages/ui/certd-server/src/plugins/plugin-cert/access/eab-access.test.ts new file mode 100644 index 000000000..1bad9ed63 --- /dev/null +++ b/packages/ui/certd-server/src/plugins/plugin-cert/access/eab-access.test.ts @@ -0,0 +1,20 @@ +import assert from "assert"; +import { EabAccess } from "./eab-access.js"; + +describe("EabAccess", () => { + it("generates an account key payload for the current kid", async () => { + const access = new EabAccess(); + access.kid = "kid-1"; + + const payload = JSON.parse(await access.onGenerateAccountKey()); + + assert.equal(payload.kid, "kid-1"); + assert.match(payload.privateKey, /BEGIN (RSA )?PRIVATE KEY/); + }); + + it("requires kid before generating the account key payload", async () => { + const access = new EabAccess(); + + await assert.rejects(() => access.onGenerateAccountKey(), /请先填写KID/); + }); +}); diff --git a/packages/ui/certd-server/src/plugins/plugin-cert/access/eab-access.ts b/packages/ui/certd-server/src/plugins/plugin-cert/access/eab-access.ts index 1162c4df9..63c45f06d 100644 --- a/packages/ui/certd-server/src/plugins/plugin-cert/access/eab-access.ts +++ b/packages/ui/certd-server/src/plugins/plugin-cert/access/eab-access.ts @@ -1,4 +1,5 @@ import { IsAccess, AccessInput, BaseAccess } from "@certd/pipeline"; +import * as acme from "@certd/acme-client"; @IsAccess({ name: "eab", @@ -7,6 +8,23 @@ import { IsAccess, AccessInput, BaseAccess } from "@certd/pipeline"; icon: "ic:outline-lock", }) export class EabAccess extends BaseAccess { + @AccessInput({ + title: "EAB类型", + component: { + name: "a-select", + options: [ + { value: "google", label: "Google(免费)", icon: "flat-color-icons:google" }, + { value: "zerossl", label: "ZeroSSL(免费)", icon: "emojione:digit-zero" }, + { value: "litessl", label: "litessl(免费)", icon: "roentgen:free" }, + { value: "sslcom", label: "SSL.com(仅主域名和www免费)", icon: "la:expeditedssl" }, + ], + }, + helper: "请选择EAB类型", + required: true, + encrypt: false, + }) + eabType = ""; + @AccessInput({ title: "KID", component: { @@ -34,10 +52,35 @@ export class EabAccess extends BaseAccess { placeholder: "绑定一个邮箱", }, rules: [{ type: "email", message: "请输入正确的邮箱" }], - helper: "Google的EAB申请证书,更换邮箱会导致EAB失效,可以在此处绑定一个邮箱避免此问题", + helper: "绑定一个邮箱,避免失效", required: true, }) email = ""; + + @AccessInput({ + title: "ACME账号私钥", + component: { + name: "refresh-input", + action: "GenerateAccountKey", + buttonText: "刷新账号私钥", + successMessage: "账号私钥已刷新,请保存授权配置", + }, + required: true, + helper: "如果修改了KID,请点击刷新重新生成账号私钥", + encrypt: true, + }) + accountKey = ""; + + async onGenerateAccountKey() { + if (!this.kid) { + throw new Error("请先填写KID"); + } + const key = await acme.crypto.createPrivateKey(2048); + return JSON.stringify({ + kid: this.kid, + privateKey: key.toString(), + }); + } } new EabAccess(); diff --git a/packages/ui/certd-server/src/plugins/plugin-cert/plugin/cert-plugin/acme.test.ts b/packages/ui/certd-server/src/plugins/plugin-cert/plugin/cert-plugin/acme.test.ts new file mode 100644 index 000000000..54736f192 --- /dev/null +++ b/packages/ui/certd-server/src/plugins/plugin-cert/plugin/cert-plugin/acme.test.ts @@ -0,0 +1,114 @@ +import assert from "assert"; +import { AcmeService } from "./acme.js"; + +const logger = { + info() {}, + error() {}, + warn() {}, + debug() {}, +}; + +describe("AcmeService account config", () => { + it("keeps legacy email-based account config when EAB has no saved account key", async () => { + const userContext = { + async getObj(key: string) { + if (key === "acme.config.google.user@example.com") { + return { + key: "legacy-email-key", + accountUrl: "https://dv.acme-v02.api.pki.goog/acme/acct/legacy", + }; + } + return null; + }, + async setObj() {}, + }; + const service = new AcmeService({ + userId: 1, + userContext: userContext as any, + logger: logger as any, + sslProvider: "google", + eab: { + id: 12, + kid: "kid-1", + hmacKey: "hmac", + } as any, + domainParser: {} as any, + }); + + const conf = await service.getAccountConfig("user@example.com", { enabled: false, mappings: {} }); + + assert.equal(conf.key, "legacy-email-key"); + assert.equal(conf.accountUrl, "https://dv.acme-v02.api.pki.goog/acme/acct/legacy"); + }); + + it("uses the account key saved on the EAB access before legacy email config", async () => { + const userContext = { + async getObj(key: string) { + if (key === "acme.config.google.access.12") { + return { accountUrl: "https://dv.acme-v02.api.pki.goog/acme/acct/1" }; + } + if (key === "acme.config.google.user@example.com") { + return { key: "legacy-email-key" }; + } + return null; + }, + async setObj() {}, + }; + const service = new AcmeService({ + userId: 1, + userContext: userContext as any, + logger: logger as any, + sslProvider: "google", + eab: { + id: 12, + kid: "kid-1", + hmacKey: "hmac", + accountKey: JSON.stringify({ kid: "kid-1", privateKey: "eab-account-key" }), + } as any, + domainParser: {} as any, + }); + + const conf = await service.getAccountConfig("user@example.com", { enabled: false, mappings: {} }); + + assert.equal(conf.key, "eab-account-key"); + assert.equal(conf.accountUrl, "https://dv.acme-v02.api.pki.goog/acme/acct/1"); + }); + + it("rejects an EAB account key generated for another kid", async () => { + const service = new AcmeService({ + userId: 1, + userContext: {} as any, + logger: logger as any, + sslProvider: "google", + eab: { + id: 12, + kid: "kid-2", + hmacKey: "hmac", + accountKey: JSON.stringify({ kid: "kid-1", privateKey: "eab-account-key" }), + } as any, + domainParser: {} as any, + }); + + assert.throws(() => service.getEabAccountPrivateKey(), /请点击刷新重新生成ACME账号私钥/); + }); + + it("formats expired EAB errors with a Chinese recovery hint", () => { + const service = new AcmeService({ + userId: 1, + userContext: {} as any, + logger: logger as any, + sslProvider: "google", + eab: { + id: 12, + kid: "kid-1", + hmacKey: "hmac", + } as any, + domainParser: {} as any, + }); + + const error = service.formatCreateAccountError(new Error("Unknown external account binding (EAB) key. This may be due to the EAB key expiring")); + + assert.match(error.message, /EAB授权已失效或已过期/); + assert.match(error.message, /请重新获取EAB授权并刷新ACME账号私钥后重试/); + }); +}); diff --git a/packages/ui/certd-server/src/plugins/plugin-cert/plugin/cert-plugin/acme.ts b/packages/ui/certd-server/src/plugins/plugin-cert/plugin/cert-plugin/acme.ts index 9661e2b25..0a157d4b4 100644 --- a/packages/ui/certd-server/src/plugins/plugin-cert/plugin/cert-plugin/acme.ts +++ b/packages/ui/certd-server/src/plugins/plugin-cert/plugin/cert-plugin/acme.ts @@ -49,11 +49,15 @@ export type CertInfo = { }; export type SSLProvider = "letsencrypt" | "google" | "zerossl" | "sslcom" | "letsencrypt_staging"; export type PrivateKeyType = "rsa_1024" | "rsa_2048" | "rsa_3072" | "rsa_4096" | "ec_256" | "ec_384" | "ec_521"; +type AcmeEabOptions = ClientExternalAccountBindingOptions & { + id?: number; + accountKey?: string; +}; type AcmeServiceOptions = { userContext: IContext; logger: ILogger; sslProvider: SSLProvider; - eab?: ClientExternalAccountBindingOptions; + eab?: AcmeEabOptions; skipLocalVerify?: boolean; useMappingProxy?: boolean; reverseProxy?: string; @@ -71,7 +75,7 @@ export class AcmeService { logger: ILogger; sslProvider: SSLProvider; skipLocalVerify = true; - eab?: ClientExternalAccountBindingOptions; + eab?: AcmeEabOptions; constructor(options: AcmeServiceOptions) { this.options = options; this.userContext = options.userContext; @@ -85,7 +89,14 @@ export class AcmeService { } async getAccountConfig(email: string, urlMapping: UrlMapping): Promise { - const conf = (await this.userContext.getObj(this.buildAccountKey(email))) || {}; + let conf = (await this.userContext.getObj(this.buildAccountKey(email))) || {}; + const eabAccountKey = this.getEabAccountPrivateKey(); + if (eabAccountKey) { + conf = { + ...((await this.userContext.getObj(this.buildAccessAccountKey())) || {}), + key: eabAccountKey, + }; + } if (urlMapping && urlMapping.mappings) { for (const key in urlMapping.mappings) { if (Object.prototype.hasOwnProperty.call(urlMapping.mappings, key)) { @@ -104,16 +115,49 @@ export class AcmeService { return `acme.config.${this.sslProvider}.${email}`; } + buildAccessAccountKey() { + return `acme.config.${this.sslProvider}.access.${this.eab.id}`; + } + + getEabAccountPrivateKey() { + if (!this.eab?.accountKey) { + return null; + } + let accountKey; + try { + accountKey = JSON.parse(this.eab.accountKey); + } catch { + return this.eab.accountKey; + } + if (accountKey.kid !== this.eab.kid) { + throw new Error("EAB的KID已变化,请点击刷新重新生成ACME账号私钥"); + } + return accountKey.privateKey; + } + + formatCreateAccountError(e: any) { + const message = e?.message || ""; + if (message.includes("Unknown external account binding (EAB) key")) { + return new Error(`EAB授权已失效或已过期,请重新获取EAB授权并刷新ACME账号私钥后重试。原始错误:${message}`); + } + return e; + } + async saveAccountConfig(email: string, conf: any) { + if (this.getEabAccountPrivateKey()) { + // userContext 跟用户走。公共 EAB 场景下这里仅作为当前用户缓存; + // 其他用户会通过 onlyReturnExisting 用同一个账号私钥取回 accountUrl。 + await this.userContext.setObj(this.buildAccessAccountKey(), { accountUrl: conf.accountUrl }); + return; + } await this.userContext.setObj(this.buildAccountKey(email), conf); } async getAcmeClient(email: string): Promise { - const directoryUrl = acme.getDirectoryUrl({ sslProvider: this.sslProvider, pkType: this.options.privateKeyType }); let targetUrl = directoryUrl.replace("https://", ""); targetUrl = targetUrl.substring(0, targetUrl.indexOf("/")); - + const mappings = { "acme-v02.api.letsencrypt.org": "le.px.certd.handfree.work", "dv.acme-v02.api.pki.goog": "gg.px.certd.handfree.work", @@ -171,7 +215,23 @@ export class AcmeService { contact: [`mailto:${email}`], externalAccountBinding: this.eab, }; - await client.createAccount(accountPayload); + if (this.getEabAccountPrivateKey()) { + try { + // RFC 8555 的 newAccount 支持 onlyReturnExisting。 + // 使用同一个账号私钥时,CA 会返回已存在账号的 URL,不会再次消费 EAB。 + await client.createAccount({ onlyReturnExisting: true }); + conf.accountUrl = client.getAccountUrl(); + await this.saveAccountConfig(email, conf); + return client; + } catch (e: any) { + this.logger.info(`未找到已存在的ACME账号,准备创建新账号:${e.message}`); + } + } + try { + await client.createAccount(accountPayload); + } catch (e: any) { + throw this.formatCreateAccountError(e); + } conf.accountUrl = client.getAccountUrl(); await this.saveAccountConfig(email, conf); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 97bf0d262..2984eb73b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -94,6 +94,9 @@ importers: chai-as-promised: specifier: ^7.1.2 version: 7.1.2(chai@4.5.0) + cross-env: + specifier: ^7.0.3 + version: 7.0.3 eslint: specifier: ^8.57.0 version: 8.57.0 @@ -106,6 +109,9 @@ importers: eslint-plugin-prettier: specifier: ^4.2.1 version: 4.2.1(eslint-config-prettier@8.10.0(eslint@8.57.0))(eslint@8.57.0)(prettier@2.8.8) + esmock: + specifier: ^2.7.5 + version: 2.7.5 jsdoc-to-markdown: specifier: ^8.0.1 version: 8.0.3 @@ -188,6 +194,9 @@ importers: chai: specifier: 4.3.10 version: 4.3.10 + cross-env: + specifier: ^7.0.3 + version: 7.0.3 eslint: specifier: ^8.41.0 version: 8.57.0 @@ -197,6 +206,9 @@ importers: eslint-plugin-prettier: specifier: ^4.2.1 version: 4.2.1(eslint-config-prettier@8.10.0(eslint@8.57.0))(eslint@8.57.0)(prettier@2.8.8) + esmock: + specifier: ^2.7.5 + version: 2.7.5 mocha: specifier: ^10.2.0 version: 10.8.2 @@ -267,6 +279,9 @@ importers: chai: specifier: 4.3.10 version: 4.3.10 + cross-env: + specifier: ^7.0.3 + version: 7.0.3 eslint: specifier: ^8.41.0 version: 8.57.0 @@ -276,6 +291,9 @@ importers: eslint-plugin-prettier: specifier: ^4.2.1 version: 4.2.1(eslint-config-prettier@8.10.0(eslint@8.57.0))(eslint@8.57.0)(prettier@2.8.8) + esmock: + specifier: ^2.7.5 + version: 2.7.5 mocha: specifier: ^10.2.0 version: 10.8.2 @@ -313,6 +331,12 @@ importers: '@typescript-eslint/parser': specifier: ^8.26.1 version: 8.32.1(eslint@8.57.0)(typescript@5.9.3) + cross-env: + specifier: ^7.0.3 + version: 7.0.3 + esmock: + specifier: ^2.7.5 + version: 2.7.5 prettier: specifier: ^2.8.8 version: 2.8.8 @@ -335,6 +359,9 @@ importers: '@typescript-eslint/parser': specifier: ^8.26.1 version: 8.32.1(eslint@8.57.0)(typescript@5.9.3) + cross-env: + specifier: ^7.0.3 + version: 7.0.3 eslint: specifier: ^8.24.0 version: 8.57.0 @@ -344,6 +371,9 @@ importers: eslint-plugin-prettier: specifier: ^4.2.1 version: 4.2.1(eslint-config-prettier@8.10.0(eslint@8.57.0))(eslint@8.57.0)(prettier@2.8.8) + esmock: + specifier: ^2.7.5 + version: 2.7.5 prettier: specifier: ^2.8.8 version: 2.8.8 @@ -403,8 +433,11 @@ importers: specifier: ^1.30.0 version: 1.31.0 cross-env: - specifier: ^5.1.4 - version: 5.2.1 + specifier: ^7.0.3 + version: 7.0.3 + esmock: + specifier: ^2.7.5 + version: 2.7.5 js-yaml: specifier: ^3.11.0 version: 3.14.1 @@ -436,6 +469,9 @@ importers: '@typescript-eslint/parser': specifier: ^8.26.1 version: 8.32.1(eslint@8.57.0)(typescript@5.9.3) + cross-env: + specifier: ^7.0.3 + version: 7.0.3 eslint: specifier: ^8.24.0 version: 8.57.0 @@ -445,6 +481,9 @@ importers: eslint-plugin-prettier: specifier: ^4.2.1 version: 4.2.1(eslint-config-prettier@8.10.0(eslint@8.57.0))(eslint@8.57.0)(prettier@2.8.8) + esmock: + specifier: ^2.7.5 + version: 2.7.5 prettier: specifier: ^2.8.8 version: 2.8.8 @@ -505,9 +544,6 @@ importers: better-sqlite3: specifier: ^11.1.2 version: 11.10.0 - cross-env: - specifier: ^7.0.3 - version: 7.0.3 dayjs: specifier: ^1.11.7 version: 1.11.13 @@ -539,6 +575,9 @@ importers: '@typescript-eslint/parser': specifier: ^8.26.1 version: 8.32.1(eslint@8.57.0)(typescript@5.9.3) + cross-env: + specifier: ^7.0.3 + version: 7.0.3 eslint: specifier: ^8.24.0 version: 8.57.0 @@ -548,6 +587,9 @@ importers: eslint-plugin-prettier: specifier: ^4.2.1 version: 4.2.1(eslint-config-prettier@8.10.0(eslint@8.57.0))(eslint@8.57.0)(prettier@2.8.8) + esmock: + specifier: ^2.7.5 + version: 2.7.5 mocha: specifier: ^10.2.0 version: 10.8.2 @@ -594,6 +636,9 @@ importers: '@typescript-eslint/parser': specifier: ^8.26.1 version: 8.32.1(eslint@8.57.0)(typescript@5.9.3) + cross-env: + specifier: ^7.0.3 + version: 7.0.3 eslint: specifier: ^8.24.0 version: 8.57.0 @@ -609,6 +654,9 @@ importers: eslint-plugin-prettier: specifier: ^4.2.1 version: 4.2.1(eslint-config-prettier@8.10.0(eslint@8.57.0))(eslint@8.57.0)(prettier@2.8.8) + esmock: + specifier: ^2.7.5 + version: 2.7.5 prettier: specifier: ^2.8.8 version: 2.8.8 @@ -661,6 +709,9 @@ importers: chai: specifier: ^4.3.6 version: 4.5.0 + cross-env: + specifier: ^7.0.3 + version: 7.0.3 eslint: specifier: ^8.24.0 version: 8.57.0 @@ -670,6 +721,9 @@ importers: eslint-plugin-prettier: specifier: ^4.2.1 version: 4.2.1(eslint-config-prettier@8.10.0(eslint@8.57.0))(eslint@8.57.0)(prettier@2.8.8) + esmock: + specifier: ^2.7.5 + version: 2.7.5 mocha: specifier: ^10.1.0 version: 10.8.2 @@ -776,6 +830,9 @@ importers: chai: specifier: ^4.3.6 version: 4.5.0 + cross-env: + specifier: ^7.0.3 + version: 7.0.3 eslint: specifier: ^8.24.0 version: 8.57.0 @@ -785,6 +842,9 @@ importers: eslint-plugin-prettier: specifier: ^4.2.1 version: 4.2.1(eslint-config-prettier@8.10.0(eslint@8.57.0))(eslint@8.57.0)(prettier@2.8.8) + esmock: + specifier: ^2.7.5 + version: 2.7.5 mocha: specifier: ^10.1.0 version: 10.8.2 @@ -861,6 +921,9 @@ importers: '@typescript-eslint/parser': specifier: ^8.26.1 version: 8.32.1(eslint@8.57.0)(typescript@5.9.3) + cross-env: + specifier: ^7.0.3 + version: 7.0.3 eslint: specifier: ^8.24.0 version: 8.57.0 @@ -870,6 +933,9 @@ importers: eslint-plugin-prettier: specifier: ^4.2.1 version: 4.2.1(eslint-config-prettier@8.10.0(eslint@8.57.0))(eslint@8.57.0)(prettier@2.8.8) + esmock: + specifier: ^2.7.5 + version: 2.7.5 mocha: specifier: ^10.2.0 version: 10.8.2 @@ -952,6 +1018,9 @@ importers: chai: specifier: 4.3.10 version: 4.3.10 + cross-env: + specifier: ^7.0.3 + version: 7.0.3 eslint: specifier: ^8.41.0 version: 8.57.0 @@ -961,6 +1030,9 @@ importers: eslint-plugin-prettier: specifier: ^4.2.1 version: 4.2.1(eslint-config-prettier@8.10.0(eslint@8.57.0))(eslint@8.57.0)(prettier@2.8.8) + esmock: + specifier: ^2.7.5 + version: 2.7.5 mocha: specifier: ^10.2.0 version: 10.8.2 @@ -1019,6 +1091,9 @@ importers: chai: specifier: 4.3.10 version: 4.3.10 + cross-env: + specifier: ^7.0.3 + version: 7.0.3 eslint: specifier: ^8.41.0 version: 8.57.0 @@ -1028,6 +1103,9 @@ importers: eslint-plugin-prettier: specifier: ^4.2.1 version: 4.2.1(eslint-config-prettier@8.10.0(eslint@8.57.0))(eslint@8.57.0)(prettier@2.8.8) + esmock: + specifier: ^2.7.5 + version: 2.7.5 mocha: specifier: ^10.2.0 version: 10.8.2 @@ -1154,9 +1232,6 @@ importers: cropperjs: specifier: ^1.6.1 version: 1.6.2 - cross-env: - specifier: ^7.0.3 - version: 7.0.3 cssnano: specifier: ^7.0.6 version: 7.0.7(postcss@8.5.6) @@ -1344,6 +1419,9 @@ importers: chai: specifier: ^5.1.0 version: 5.2.0 + cross-env: + specifier: ^7.0.3 + version: 7.0.3 dependency-cruiser: specifier: ^16.2.3 version: 16.10.2 @@ -1371,6 +1449,9 @@ importers: eslint-plugin-vue: specifier: ^9.23.0 version: 9.33.0(eslint@8.57.0) + esmock: + specifier: ^2.7.5 + version: 2.7.5 less: specifier: ^4.2.0 version: 4.3.0 @@ -1632,9 +1713,6 @@ importers: cron-parser: specifier: ^4.9.0 version: 4.9.0 - cross-env: - specifier: ^7.0.3 - version: 7.0.3 crypto-js: specifier: ^4.2.0 version: 4.2.0 @@ -1801,6 +1879,12 @@ importers: c8: specifier: ^10.1.2 version: 10.1.3 + cross-env: + specifier: ^7.0.3 + version: 7.0.3 + esmock: + specifier: ^2.7.5 + version: 2.7.5 mocha: specifier: ^10.2.0 version: 10.8.2 @@ -4384,56 +4468,67 @@ packages: resolution: {integrity: sha512-u72Mzc6jyJwKjJbZZcIYmd9bumJu7KNmHYdue43vT1rXPm2rITwmPWF0mmPzLm9/vJWxIRbao/jrQmxTO0Sm9w==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.50.0': resolution: {integrity: sha512-S4UefYdV0tnynDJV1mdkNawp0E5Qm2MtSs330IyHgaccOFrwqsvgigUD29uT+B/70PDY1eQ3t40+xf6wIvXJyg==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.50.0': resolution: {integrity: sha512-1EhkSvUQXJsIhk4msxP5nNAUWoB4MFDHhtc4gAYvnqoHlaL9V3F37pNHabndawsfy/Tp7BPiy/aSa6XBYbaD1g==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.50.0': resolution: {integrity: sha512-EtBDIZuDtVg75xIPIK1l5vCXNNCIRM0OBPUG+tbApDuJAy9mKago6QxX+tfMzbCI6tXEhMuZuN1+CU8iDW+0UQ==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loongarch64-gnu@4.50.0': resolution: {integrity: sha512-BGYSwJdMP0hT5CCmljuSNx7+k+0upweM2M4YGfFBjnFSZMHOLYR0gEEj/dxyYJ6Zc6AiSeaBY8dWOa11GF/ppQ==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-gnu@4.50.0': resolution: {integrity: sha512-I1gSMzkVe1KzAxKAroCJL30hA4DqSi+wGc5gviD0y3IL/VkvcnAqwBf4RHXHyvH66YVHxpKO8ojrgc4SrWAnLg==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-gnu@4.50.0': resolution: {integrity: sha512-bSbWlY3jZo7molh4tc5dKfeSxkqnf48UsLqYbUhnkdnfgZjgufLS/NTA8PcP/dnvct5CCdNkABJ56CbclMRYCA==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.50.0': resolution: {integrity: sha512-LSXSGumSURzEQLT2e4sFqFOv3LWZsEF8FK7AAv9zHZNDdMnUPYH3t8ZlaeYYZyTXnsob3htwTKeWtBIkPV27iQ==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.50.0': resolution: {integrity: sha512-CxRKyakfDrsLXiCyucVfVWVoaPA4oFSpPpDwlMcDFQvrv3XY6KEzMtMZrA+e/goC8xxp2WSOxHQubP8fPmmjOQ==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.50.0': resolution: {integrity: sha512-8PrJJA7/VU8ToHVEPu14FzuSAqVKyo5gg/J8xUerMbyNkWkO9j2ExBho/68RnJsMGNJq4zH114iAttgm7BZVkA==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.50.0': resolution: {integrity: sha512-SkE6YQp+CzpyOrbw7Oc4MgXFvTw2UIBElvAvLCo230pyxOLmYwRPwZ/L5lBe/VW/qT1ZgND9wJfOsdy0XptRvw==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-openharmony-arm64@4.50.0': resolution: {integrity: sha512-PZkNLPfvXeIOgJWA804zjSFH7fARBBCpCXxgkGDRjjAhRLOR8o0IGS01ykh5GYfod4c2yiiREuDM8iZ+pVsT+Q==} @@ -6807,20 +6902,11 @@ packages: cropperjs@1.6.2: resolution: {integrity: sha512-nhymn9GdnV3CqiEHJVai54TULFAE3VshJTXSqSJKa8yXAKyBKDWdhHarnlIPrshJ0WMFTGuFvG02YjLXfPiuOA==} - cross-env@5.2.1: - resolution: {integrity: sha512-1yHhtcfAd1r4nwQgknowuUNfIT9E8dOMMspC36g45dN+iD1blloi7xp8X/xAIDnjHWyt1uQ8PHk2fkNaym7soQ==} - engines: {node: '>=4.0'} - hasBin: true - cross-env@7.0.3: resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==} engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'} hasBin: true - cross-spawn@6.0.6: - resolution: {integrity: sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==} - engines: {node: '>=4.8'} - cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -7600,6 +7686,10 @@ packages: deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. hasBin: true + esmock@2.7.5: + resolution: {integrity: sha512-jKwL7yYpVOERalCllSnPur59s9M0gV5BKijtmOKclqDMuhqdS7DT/a7cODjz6w1XusE0wAaHBTrK+zgab/ENgw==} + engines: {node: '>=14.16.0'} + esniff@2.0.1: resolution: {integrity: sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==} engines: {node: '>=0.10'} @@ -9822,9 +9912,6 @@ packages: next-tick@1.1.0: resolution: {integrity: sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==} - nice-try@1.0.5: - resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==} - no-case@3.0.4: resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} @@ -10234,10 +10321,6 @@ packages: resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} engines: {node: '>=0.10.0'} - path-key@2.0.1: - resolution: {integrity: sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==} - engines: {node: '>=4'} - path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} @@ -11475,18 +11558,10 @@ packages: shallow-equal@1.2.1: resolution: {integrity: sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA==} - shebang-command@1.2.0: - resolution: {integrity: sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==} - engines: {node: '>=0.10.0'} - shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} - shebang-regex@1.0.0: - resolution: {integrity: sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==} - engines: {node: '>=0.10.0'} - shebang-regex@3.0.0: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} @@ -20418,22 +20493,10 @@ snapshots: cropperjs@1.6.2: {} - cross-env@5.2.1: - dependencies: - cross-spawn: 6.0.6 - cross-env@7.0.3: dependencies: cross-spawn: 7.0.6 - cross-spawn@6.0.6: - dependencies: - nice-try: 1.0.5 - path-key: 2.0.1 - semver: 5.7.2 - shebang-command: 1.2.0 - which: 1.3.1 - cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -21439,6 +21502,8 @@ snapshots: transitivePeerDependencies: - supports-color + esmock@2.7.5: {} + esniff@2.0.1: dependencies: d: 1.0.2 @@ -23862,8 +23927,6 @@ snapshots: next-tick@1.1.0: {} - nice-try@1.0.5: {} - no-case@3.0.4: dependencies: lower-case: 2.0.2 @@ -24331,8 +24394,6 @@ snapshots: path-is-absolute@1.0.1: {} - path-key@2.0.1: {} - path-key@3.1.1: {} path-key@4.0.0: {} @@ -25682,16 +25743,10 @@ snapshots: shallow-equal@1.2.1: {} - shebang-command@1.2.0: - dependencies: - shebang-regex: 1.0.0 - shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 - shebang-regex@1.0.0: {} - shebang-regex@3.0.0: {} shiki@3.4.1: