perf: 重构自动加载模块并优化EAB授权处理

refactor(ui): 将分散的auto-*模块整合为统一命名的auto-register模块
perf(plugin-cert): 增强EAB授权功能,支持账号私钥刷新和类型选择
test: 添加EAB授权服务和ACME账号配置的单元测试
docs: 更新AGENTS.md补充ACME/EAB使用注意事项
chore: 统一各package.json中的测试脚本配置
This commit is contained in:
xiaojunnuo
2026-05-10 16:57:12 +08:00
parent 37d03c10f9
commit 4755216505
32 changed files with 911 additions and 105 deletions
+7
View File
@@ -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 等通常视为运行时或构建产物,除非任务明确要求处理它们。
- 注意本地数据和配置里可能包含凭据、证书材料等敏感信息。
- 本仓库代码注释优先使用中文,尤其是解释业务规则、兼容逻辑、协议细节和隐藏风险时;除非文件已有明确英文注释风格或引用外部英文术语,否则不要新增英文说明性注释。
## 插件开发技能
+3 -1
View File
@@ -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"
},
+3 -1
View File
@@ -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",
+3 -1
View File
@@ -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",
+3 -1
View File
@@ -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"
},
+3 -1
View File
@@ -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",
+3 -2
View File
@@ -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",
+3 -1
View File
@@ -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",
+3 -2
View File
@@ -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",
@@ -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",
});
});
});
@@ -123,6 +123,25 @@ export class AccessService extends BaseService<AccessEntity> {
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) {
+3 -1
View File
@@ -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",
+3 -1
View File
@@ -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",
+3 -1
View File
@@ -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",
+3 -2
View File
@@ -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",
@@ -0,0 +1,106 @@
<template>
<div class="refresh-input">
<div class="refresh-input-line">
<a-input class="refresh-input-control" :value="value" :placeholder="placeholder" allow-clear @update:value="emit('update:value', $event)"></a-input>
<fs-button :loading="loading" type="primary" :text="buttonText" :icon="icon" @click="doRefresh"></fs-button>
</div>
<div class="helper" :class="{ error: hasError }">
{{ message }}
</div>
</div>
</template>
<script setup lang="ts">
import { ComponentPropsType, doRequest } from "/@/components/plugins/lib";
import { computed, inject, ref } from "vue";
import { Form } from "ant-design-vue";
import { getInputFromForm } from "./utils";
defineOptions({
name: "RefreshInput",
});
type RefreshInputProps = ComponentPropsType & {
buttonText?: string;
icon?: string;
placeholder?: string;
successMessage?: string;
};
const fromType: any = inject("getFromType");
const getScope: any = inject("get:scope");
const getPluginType: any = inject("get:plugin:type", () => {
return "access";
});
const formItemContext = Form.useInjectFormItemContext();
const props = defineProps<RefreshInputProps>();
const emit = defineEmits<{
"update:value": [value: string];
}>();
const loading = ref(false);
const message = ref("");
const hasError = ref(false);
const action = computed(() => props.action);
const buttonText = computed(() => props.buttonText || "刷新");
const icon = computed(() => props.icon || "ion:refresh-outline");
const placeholder = computed(() => props.placeholder || "");
const successMessage = computed(() => props.successMessage || "刷新成功,请保存配置");
const doRefresh = async () => {
if (loading.value) {
return;
}
if (!action.value) {
hasError.value = true;
message.value = "缺少刷新动作配置";
return;
}
formItemContext.onFieldChange();
const { form } = getScope();
const pluginType = getPluginType();
const { input, record } = getInputFromForm(form, pluginType);
loading.value = true;
message.value = "";
hasError.value = false;
try {
const res = await doRequest(
{
type: pluginType,
typeName: form.type,
action: action.value,
input,
record,
fromType,
},
{
onError(err: any) {
hasError.value = true;
message.value = err.message;
},
showErrorNotify: false,
}
);
emit("update:value", res);
message.value = successMessage.value;
} finally {
loading.value = false;
}
};
</script>
<style lang="less" scoped>
.refresh-input-line {
display: flex;
gap: 8px;
align-items: center;
}
.refresh-input-control {
flex: 1;
}
</style>
@@ -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);
+2 -1
View File
@@ -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",
@@ -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);
@@ -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"),
});
});
});
@@ -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);
}
}
@@ -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();
@@ -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") {
@@ -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();
}
@@ -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);
});
}
@@ -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();
@@ -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();
}
}
@@ -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/);
});
});
@@ -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();
@@ -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账号私钥后重试/);
});
});
@@ -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<any> {
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<acme.Client> {
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);
}
+112 -57
View File
@@ -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: