Compare commits

...
Author SHA1 Message Date
ecc-tools[bot]andGitHub e0e0bb51ce feat: add certd ECC bundle (.claude/homunculus/instincts/inherited/certd-instincts.yaml) 2026-07-30 02:08:23 +00:00
ecc-tools[bot]andGitHub 1dc5a19c45 feat: add certd ECC bundle (.codex/agents/docs-researcher.toml) 2026-07-30 02:08:22 +00:00
ecc-tools[bot]andGitHub 04aa9041e8 feat: add certd ECC bundle (.codex/agents/reviewer.toml) 2026-07-30 02:08:21 +00:00
ecc-tools[bot]andGitHub 0d86c84b28 feat: add certd ECC bundle (.codex/agents/explorer.toml) 2026-07-30 02:08:20 +00:00
ecc-tools[bot]andGitHub 6605669113 feat: add certd ECC bundle (.codex/AGENTS.md) 2026-07-30 02:08:19 +00:00
ecc-tools[bot]andGitHub 4b8747b5da feat: add certd ECC bundle (.codex/config.toml) 2026-07-30 02:08:18 +00:00
ecc-tools[bot]andGitHub 2ba4132c50 feat: add certd ECC bundle (.claude/identity.json) 2026-07-30 02:08:17 +00:00
ecc-tools[bot]andGitHub 49dc2796cd feat: add certd ECC bundle (.agents/skills/certd/agents/openai.yaml) 2026-07-30 02:08:15 +00:00
ecc-tools[bot]andGitHub 1e07d69932 feat: add certd ECC bundle (.agents/skills/certd/SKILL.md) 2026-07-30 02:08:14 +00:00
ecc-tools[bot]andGitHub aabf73a736 feat: add certd ECC bundle (.claude/skills/certd/SKILL.md) 2026-07-30 02:08:13 +00:00
ecc-tools[bot]andGitHub c9be2293ab feat: add certd ECC bundle (.claude/ecc-tools.json) 2026-07-30 02:08:12 +00:00
xiaojunnuo 4662e45e58 build: release 2026-07-19 01:28:05 +08:00
xiaojunnuo 1fefbdc9ab build: publish 2026-07-19 01:14:35 +08:00
xiaojunnuo 1cb2a57c55 build: trigger build image 2026-07-19 01:14:24 +08:00
xiaojunnuo 246ee83015 v1.42.6 2026-07-19 01:13:42 +08:00
xiaojunnuo 335ddfc7a5 build: prepare to build 2026-07-19 01:12:01 +08:00
xiaojunnuo 5b500830a1 fix: 修复正常批量删除流水线报权限不足的bug 2026-07-19 01:05:03 +08:00
xiaojunnuo 5f53b81c75 perf: 优化动态加载依赖镜像地址,多次重试 2026-07-18 21:07:56 +08:00
xiaojunnuo c8c269f612 chore: 1 2026-07-17 15:44:23 +08:00
xiaojunnuo 743617dbda build: release 2026-07-15 23:54:59 +08:00
xiaojunnuo 89806b828a build: publish 2026-07-15 23:35:15 +08:00
xiaojunnuo 2e530bfdb0 build: trigger build image 2026-07-15 23:35:05 +08:00
xiaojunnuo 83495b3213 v1.42.5 2026-07-15 23:34:16 +08:00
xiaojunnuo 5108416904 build: prepare to build 2026-07-15 23:31:35 +08:00
xiaojunnuo 5589da1822 chore: 修复单元测试 2026-07-15 23:30:49 +08:00
xiaojunnuo 21e5aed3f3 chore: 修复单元测试的问题 2026-07-15 23:25:28 +08:00
xiaojunnuo 4a88f795e1 chore: vke kubeconfig 有效期校验 2026-07-15 23:10:58 +08:00
xiaojunnuo 604fa5be63 perf: 优化vke keubconfig获取方式,改成先查询,如果没有再创建临时config 2026-07-15 23:01:32 +08:00
xiaojunnuo 7ed1be994f fix: 修复dingtalk通知格式没有换行的bug 2026-07-15 23:00:42 +08:00
xiaojunnuo 6cc74a1c0a chore: lint 2026-07-15 01:25:55 +08:00
xiaojunnuo 167b303fae fix: 修复上传到cos报runtimeDepsService未初始化的问题 2026-07-15 01:09:57 +08:00
xiaojunnuo b91c9e4ea6 perf: 给SQLITE_IOERR_WRITE增加友好报错提示,将certd:latest镜像改为certd:slim 2026-07-14 21:32:32 +08:00
xiaojunnuo 584b7b6f45 build: release 2026-07-12 03:41:59 +08:00
xiaojunnuo 2312c444ef build: release 2026-07-12 03:36:09 +08:00
xiaojunnuo 0373d019f8 Merge branch 'v2-dev' of https://github.com/certd/certd into v2-dev 2026-07-12 03:11:04 +08:00
xiaojunnuo fe09e75b80 refactor(nginx-proxy-manager): 替换直接动态导入为运行时依赖导入
将插件中直接使用的动态导入改为通过runtimeDepsService统一处理,同时重构导入方式为runtimeImport变量,移除冗余的form-data和otplib直接导入,统一导入逻辑
2026-07-12 03:10:52 +08:00
xiaojunnuo 432c9c6cc1 build: publish 2026-07-12 02:48:34 +08:00
xiaojunnuo 5fc1d55879 build: trigger build image 2026-07-12 02:48:22 +08:00
xiaojunnuo 268cd6cc9c v1.42.4 2026-07-12 02:47:26 +08:00
xiaojunnuo 20ffe31638 build: prepare to build 2026-07-12 02:45:04 +08:00
xiaojunnuo 02d30067ea chore: 1 2026-07-12 02:44:16 +08:00
xiaojunnuo 0ce7808e08 build: prepare to build 2026-07-12 02:39:50 +08:00
xiaojunnuo d609ee2b7a chore(base-service): 完善batchDelete方法的返回类型并添加返回值
为batchDelete方法添加明确的Promise<number>返回类型,同时返回实际删除的id数量
2026-07-12 02:38:49 +08:00
xiaojunnuo 083df61fcc build: prepare to build 2026-07-12 02:33:34 +08:00
xiaojunnuo a524988be6 chore: 1 2026-07-12 02:32:49 +08:00
xiaojunnuo 8517a0b564 refactor(certd-server): 移除非运行时依赖到动态导入
将alipay-sdk、openid-client、otplib、wechatpay-node-v3改为动态导入,从dependencies移到devDependencies?不,是改为通过importRuntime动态加载,移除顶层静态引入,优化启动时依赖加载,减少初始包体积和启动耗时
2026-07-12 00:04:06 +08:00
xiaojunnuo ec69b8f11b fix(pipeline): 重构运行时依赖加载逻辑,修复火山引擎DNS解析报runtimeDepsService未初始化的bug 2026-07-11 23:40:13 +08:00
xiaojunnuo edda1b57f3 chore: 火山vod 上传后等待3秒 2026-07-09 19:09:08 +08:00
xiaojunnuo 5a7766992d chore: 修复update domain传参问题 2026-07-09 16:42:48 +08:00
xiaojunnuo 02dabe11db fix: 修复火山引擎查不到自定义源站域名的问题 2026-07-09 16:39:08 +08:00
xiaojunnuo 58024128d8 Merge branch 'v2-dev' of https://github.com/certd/certd into v2-dev 2026-07-09 14:47:33 +08:00
xiaojunnuo e44bf9d773 fix: 修复火山引擎自定义源站域名查询不到的问题 2026-07-09 14:47:05 +08:00
xiaojunnuo 1114223bda build: release 2026-07-09 00:00:53 +08:00
xiaojunnuo 55022e5fa6 build: publish 2026-07-08 23:48:41 +08:00
xiaojunnuo ae732fb298 build: trigger build image 2026-07-08 23:48:29 +08:00
xiaojunnuo a6ef6996c3 v1.42.3 2026-07-08 23:47:40 +08:00
xiaojunnuo 97cf5e127b build: prepare to build 2026-07-08 23:45:42 +08:00
xiaojunnuo 690df7373c chore: 旧版数据兼容性的ai 提示 2026-07-08 12:04:07 +08:00
xiaojunnuo 9cc01e5da1 Merge branch 'v2-dev' of https://github.com/certd/certd into v2-dev 2026-07-08 11:51:35 +08:00
xiaojunnuo 0a068a2746 fix(volcengine-alb): 修复火山引擎ALB 默认证书部署类型会部署到扩展证书的问题 2026-07-08 11:51:26 +08:00
xiaojunnuo 85a239ff8c build: release 2026-07-07 21:20:19 +08:00
xiaojunnuo a545a28dfb Merge branch 'v2' of https://github.com/certd/certd into v2 2026-07-07 21:18:51 +08:00
xiaojunnuo ac876a980c build: release 2026-07-07 21:15:16 +08:00
xiaojunnuo 4b555e8a56 build: publish 2026-07-07 20:04:20 +08:00
xiaojunnuo 1a8d14dc44 build: trigger build image 2026-07-07 20:04:08 +08:00
xiaojunnuo 83263a72d2 v1.42.2 2026-07-07 19:59:41 +08:00
xiaojunnuo 76f3ba8691 build: prepare to build 2026-07-07 19:56:46 +08:00
xiaojunnuo 072edd7aff fix: 修复cname用阿里云校验时报找不到runtimeDepsService的错误 2026-07-07 19:51:23 +08:00
c0be4d702c fix(certd-server): 修复 AWS 中国区 CloudFront 证书部署问题 (#764) @yw-mao
- 修复证书链拆分,保留 END CERTIFICATE 结尾标记,避免 MalformedCertificate
- 更新 CloudFront 时构造干净的 IAM ViewerCertificate,强制 sni-only,
  解决 InvalidViewerCertificate 及中国区不支持 vip 的报错
- 部署完成后清理被替换掉的旧 IAM 证书,避免续期堆积
- IAM/CloudFront 权限不足时抛出可读中文提示,便于运维排查

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-07 16:20:07 +08:00
xiaojunnuo c3d6db3f1e fix: 修复AsiaIsp CDN证书重复情况下部署失败的问题 2026-07-07 14:15:09 +08:00
xiaojunnuo e05ec53eb5 chore: 1 2026-07-07 10:21:01 +08:00
xiaojunnuo 8d9dad9c82 fix: 修复企业模式下弹出邮箱绑定提醒的问题 2026-07-07 09:43:10 +08:00
xiaojunnuo 0071bcb0e4 fix: 修复火山视频点播源站选择不到自定义源站的bug 2026-07-07 09:42:41 +08:00
xiaojunnuo dbdc1ccd1b build: release 2026-07-07 00:35:25 +08:00
xiaojunnuo 2a606fdb1d build: publish 2026-07-07 00:22:06 +08:00
xiaojunnuo 3b86f30bcf build: trigger build image 2026-07-07 00:21:54 +08:00
xiaojunnuo 7808d028e9 v1.42.1 2026-07-07 00:20:58 +08:00
xiaojunnuo 04bcf73b8b build: prepare to build 2026-07-07 00:18:40 +08:00
xiaojunnuo 1f8179d519 build: prepare to build 2026-07-07 00:14:50 +08:00
xiaojunnuo 3ffd8946f8 Merge branch 'v2-dev' of https://github.com/certd/certd into v2-dev 2026-07-07 00:12:46 +08:00
xiaojunnuo c78898e4c1 perf(certd-server): 使用 jks-go转换jks证书,大幅精简镜像大小 2026-07-07 00:12:37 +08:00
xiaojunnuo d3e4677ea4 perf(pipeline): 将默认历史保留条数从30调整为100
统一修改所有新建流水线场景下的默认keepHistoryCount配置,提升历史记录留存数量
2026-07-06 19:56:54 +08:00
xiaojunnuo d176f9cc0e fix: 修复多域名无法使用passkey登录的bug 2026-07-06 14:34:05 +08:00
xiaojunnuo a65366bbe1 fix: 修复企业模式下登录报projectId不能为空的问题 2026-07-06 13:52:56 +08:00
xiaojunnuo 20cfe74b17 build: release 2026-07-05 21:41:50 +08:00
xiaojunnuo b74db81304 fix(login): 修复输入法 composing 状态下回车触发提交的问题
优化登录页面的回车提交逻辑,避免中文输入法输入过程中按回车误触发表单提交
2026-07-05 21:37:09 +08:00
xiaojunnuo a8adbda04a refactor(runtime-deps): 调整依赖相关类为单例并修复相关逻辑
1. 将NpmRegistryResolver和RuntimeDepsService从请求作用域改为单例作用域
2. 为RuntimeDepsService的安装缓存添加node_modules存在性校验
3. 优化锁文件删除逻辑,处理Windows下文件句柄未立即释放的问题
4. 跳过并修复了清理运行时依赖目录的测试用例
2026-07-05 20:30:00 +08:00
xiaojunnuo 3e80d30ca6 chore: 调整dockerfile 2026-07-05 19:51:08 +08:00
xiaojunnuo 2eb54d50a5 chore(certd-server): adjust dependency installation and docker build steps
调整了依赖包的安装位置,将cross-env和mwtsc移到devDependencies,同时修改Dockerfile中的安装命令:先安装完整依赖构建,再清理非生产依赖,并在最终镜像中仅安装生产依赖
2026-07-05 19:45:49 +08:00
xiaojunnuo 6995308c17 build(certd-server): 调整cross-env依赖的安装位置
将cross-env从devDependencies移动到dependencies中,统一管理运行时依赖
2026-07-05 19:36:41 +08:00
xiaojunnuo 0738d120ae build: publish 2026-07-05 19:31:22 +08:00
xiaojunnuo bad6879589 build: trigger build image 2026-07-05 19:31:10 +08:00
xiaojunnuo b46948c0ba v1.42.0 2026-07-05 19:23:49 +08:00
xiaojunnuo 3024720fc2 build: prepare to build 2026-07-05 19:21:52 +08:00
xiaojunnuo cf854c9278 chore(access-selector): add auto select first item function
1. 为access-selector组件添加defaultSelect属性支持自动选中第一个匹配项
2. 补全组件的typescript语法支持,新增onMounted等依赖导入
3. 在证书申请插件中启用默认选中功能
2026-07-05 01:40:08 +08:00
xiaojunnuo 608cc2a81f perf: 优化用户体验,首次访问时弹出邮箱账号绑定用以初始化账号 2026-07-05 01:14:48 +08:00
xiaojunnuo 396670dc8f chore(locales,plugin): 更新多语言文案并优化插件依赖提示
1. 修复中文文案中"sourcee"拼写错误为"来源"
2. 将插件列表启用禁用列的提示文案替换为更简洁的"点击启用/禁用"
3. 完善插件依赖和第三方依赖的帮助提示,添加格式示例说明
2026-07-04 22:31:46 +08:00
xiaojunnuo 79f65868ca perf(passkey): passkey支持多域名rpid 2026-07-04 21:56:35 +08:00
xiaojunnuo 56e5524a0f perf(cert-plugin): 调整更新天数自动减半逻辑,仅7天ip证书生效,其他情况下不减半 2026-07-04 17:01:02 +08:00
xiaojunnuo 1ae185d0bc fix(aliyun): 修复阿里云CDN/DCDN根据证书自动匹配不到证书的bug 2026-07-03 00:21:50 +08:00
xiaojunnuo 82276b53a8 perf: 阿里云ESA证书部署支持SaaS模式 2026-07-03 00:21:10 +08:00
xiaojunnuo d5882f16be fix: 修复telegram - 符号转义问题 2026-07-02 22:54:24 +08:00
160 changed files with 4536 additions and 3052 deletions
+133
View File
@@ -0,0 +1,133 @@
```markdown
# certd Development Patterns
> Auto-generated skill from repository analysis
## Overview
This skill teaches the core development patterns and workflows for the `certd` TypeScript codebase. It covers coding conventions, file organization, commit patterns, and detailed step-by-step instructions for extending user preferences—a common feature workflow. The repository is structured for modularity, with clear separation between UI and backend logic, and emphasizes maintainable, convention-driven development.
## Coding Conventions
### File Naming
- **CamelCase** is used for file names.
- Example: `userPreferences.ts`, `preferencesDrawer.vue`
### Import Style
- **Absolute imports** are preferred.
- Example:
```typescript
import { getUserPreferences } from 'packages/ui/certd-client/src/vben/layouts/widgets/preferences/api';
```
### Export Style
- **Named exports** are used throughout the codebase.
- Example:
```typescript
export function getUserPreferences() { ... }
export const PREFERENCE_KEYS = [ ... ];
```
### Commit Patterns
- **Conventional commits** are used, with the `feat` prefix for new features.
- Example:
```
feat: add account sync to preferences
```
## Workflows
### Extend Preferences Feature
**Trigger:** When someone wants to add or enhance a user preference feature (e.g., import/export, sync to account).
**Command:** `/extend-preferences`
Follow these steps to extend user preferences functionality:
1. **Update localization files**
Add or modify strings in:
- `packages/ui/certd-client/src/locales/langs/en-US/preferences.ts`
- `packages/ui/certd-client/src/locales/langs/zh-CN/preferences.ts`
```typescript
// en-US/preferences.ts
export default {
sync: "Sync Preferences",
import: "Import Preferences",
// ...
};
```
2. **Modify or add Vue components for preferences UI**
Update or create components such as:
- `preferences-drawer.vue`
- `account-sync.ts`
```vue
<!-- preferences-drawer.vue -->
<template>
<div>
<button @click="syncPreferences">{{ $t('preferences.sync') }}</button>
</div>
</template>
```
3. **Update or add supporting icon definitions**
Edit:
- `packages/ui/certd-client/src/vben/icons/lucide.ts`
```typescript
export const SyncIcon = { /* icon definition */ };
```
4. **Implement or update store logic for settings**
Update:
- `packages/ui/certd-client/src/store/settings/index.tsx`
```typescript
export function syncPreferencesToAccount() { ... }
```
5. **Add or update backend API/controller for user preferences**
Edit or add:
- `packages/ui/certd-client/src/vben/layouts/widgets/preferences/api.ts`
- `packages/ui/certd-server/src/controller/user/mine/user-preferences.ts`
```typescript
// user-preferences.ts
export async function updateUserPreferences(req, res) { ... }
```
6. **Write or update backend tests for new preference logic**
Add or update:
- `packages/ui/certd-server/src/controller/user/mine/user-preferences.test.ts`
```typescript
test('should sync preferences', async () => { ... });
```
7. **Update backend models if necessary**
Edit:
- `packages/ui/certd-server/src/modules/mine/service/models.ts`
```typescript
export interface UserPreferences { ... }
```
## Testing Patterns
- **Test files** follow the `*.test.*` naming convention.
- Example: `user-preferences.test.ts`
- **Testing framework** is not explicitly detected, but tests are written in TypeScript and likely use a standard Node.js testing library (e.g., Jest or Mocha).
- **Test Example:**
```typescript
test('should update preferences', async () => {
// Arrange
// Act
// Assert
});
```
## Commands
| Command | Purpose |
|----------------------|--------------------------------------------------------------|
| /extend-preferences | Guide to extend or enhance user preference functionality |
```
+6
View File
@@ -0,0 +1,6 @@
interface:
display_name: "Certd"
short_description: "Repo-specific patterns and workflows for certd"
default_prompt: "Use the certd repo skill to follow existing architecture, testing, and workflow conventions."
policy:
allow_implicit_invocation: true
+227
View File
@@ -0,0 +1,227 @@
{
"version": "1.3",
"schemaVersion": "1.0",
"generatedBy": "ecc-tools",
"generatedAt": "2026-07-30T02:07:44.046Z",
"repo": "https://github.com/certd/certd",
"referenceSetReadiness": {
"score": 0,
"present": 0,
"total": 7,
"items": [
{
"id": "deep-analyzer-corpus",
"label": "Deep analyzer corpus",
"status": "missing",
"evidence": [],
"recommendation": "Add analyzer fixture, golden, benchmark, or reference-set files that can catch analyzer regressions."
},
{
"id": "rag-evaluator",
"label": "RAG/evaluator comparison",
"status": "missing",
"evidence": [],
"recommendation": "Add retrieval or evaluator reference-set comparison fixtures with expected ranking behavior."
},
{
"id": "pr-salvage",
"label": "PR salvage/review corpus",
"status": "missing",
"evidence": [],
"recommendation": "Add stale-PR, review-thread, reopen-flow, or salvage reference cases for queue cleanup automation."
},
{
"id": "discussion-triage",
"label": "Discussion triage corpus",
"status": "missing",
"evidence": [],
"recommendation": "Add public discussion triage fixtures, golden cases, or reference sets for informational, answered, and no-response classifications."
},
{
"id": "harness-compatibility",
"label": "Harness compatibility",
"status": "missing",
"evidence": [],
"recommendation": "Add cross-harness, adapter-compliance, or harness-audit evidence for Claude, Codex, OpenCode, Zed, dmux, and agent surfaces."
},
{
"id": "security-evidence",
"label": "Security evidence",
"status": "missing",
"evidence": [],
"recommendation": "Attach security evidence such as SBOMs, SARIF, audit reports, or AgentShield evidence packs."
},
{
"id": "ci-failure-mode",
"label": "CI failure-mode evidence",
"status": "missing",
"evidence": [],
"recommendation": "Add captured CI failure logs, dry-run fixtures, or troubleshooting docs for common workflow failure modes."
}
]
},
"profiles": {
"requested": "core",
"recommended": "core",
"effective": "core",
"requestedAlias": "core",
"recommendedAlias": "core",
"effectiveAlias": "core"
},
"requestedProfile": "core",
"profile": "core",
"recommendedProfile": "core",
"effectiveProfile": "core",
"tier": "free",
"requestedComponents": [
"repo-baseline"
],
"selectedComponents": [
"repo-baseline"
],
"requestedAddComponents": [],
"requestedRemoveComponents": [],
"blockedRemovalComponents": [],
"tierFilteredComponents": [],
"requestedRootPackages": [
"runtime-core"
],
"selectedRootPackages": [
"runtime-core"
],
"requestedPackages": [
"runtime-core"
],
"requestedAddPackages": [],
"requestedRemovePackages": [],
"selectedPackages": [
"runtime-core"
],
"packages": [
"runtime-core"
],
"blockedRemovalPackages": [],
"tierFilteredRootPackages": [],
"tierFilteredPackages": [],
"conflictingPackages": [],
"dependencyGraph": {
"runtime-core": []
},
"resolutionOrder": [
"runtime-core"
],
"requestedModules": [
"runtime-core"
],
"selectedModules": [
"runtime-core"
],
"modules": [
"runtime-core"
],
"managedFiles": [
".claude/skills/certd/SKILL.md",
".agents/skills/certd/SKILL.md",
".agents/skills/certd/agents/openai.yaml",
".claude/identity.json",
".codex/config.toml",
".codex/AGENTS.md",
".codex/agents/explorer.toml",
".codex/agents/reviewer.toml",
".codex/agents/docs-researcher.toml",
".claude/homunculus/instincts/inherited/certd-instincts.yaml"
],
"packageFiles": {
"runtime-core": [
".claude/skills/certd/SKILL.md",
".agents/skills/certd/SKILL.md",
".agents/skills/certd/agents/openai.yaml",
".claude/identity.json",
".codex/config.toml",
".codex/AGENTS.md",
".codex/agents/explorer.toml",
".codex/agents/reviewer.toml",
".codex/agents/docs-researcher.toml",
".claude/homunculus/instincts/inherited/certd-instincts.yaml"
]
},
"moduleFiles": {
"runtime-core": [
".claude/skills/certd/SKILL.md",
".agents/skills/certd/SKILL.md",
".agents/skills/certd/agents/openai.yaml",
".claude/identity.json",
".codex/config.toml",
".codex/AGENTS.md",
".codex/agents/explorer.toml",
".codex/agents/reviewer.toml",
".codex/agents/docs-researcher.toml",
".claude/homunculus/instincts/inherited/certd-instincts.yaml"
]
},
"files": [
{
"moduleId": "runtime-core",
"path": ".claude/skills/certd/SKILL.md",
"description": "Repository-specific Claude Code skill generated from git history."
},
{
"moduleId": "runtime-core",
"path": ".agents/skills/certd/SKILL.md",
"description": "Codex-facing copy of the generated repository skill."
},
{
"moduleId": "runtime-core",
"path": ".agents/skills/certd/agents/openai.yaml",
"description": "Codex skill metadata so the repo skill appears cleanly in the skill interface."
},
{
"moduleId": "runtime-core",
"path": ".claude/identity.json",
"description": "Suggested identity.json baseline derived from repository conventions."
},
{
"moduleId": "runtime-core",
"path": ".codex/config.toml",
"description": "Repo-local Codex MCP and multi-agent baseline aligned with ECC defaults."
},
{
"moduleId": "runtime-core",
"path": ".codex/AGENTS.md",
"description": "Codex usage guide that points at the generated repo skill and workflow bundle."
},
{
"moduleId": "runtime-core",
"path": ".codex/agents/explorer.toml",
"description": "Read-only explorer role config for Codex multi-agent work."
},
{
"moduleId": "runtime-core",
"path": ".codex/agents/reviewer.toml",
"description": "Read-only reviewer role config focused on correctness and security."
},
{
"moduleId": "runtime-core",
"path": ".codex/agents/docs-researcher.toml",
"description": "Read-only docs researcher role config for API verification."
},
{
"moduleId": "runtime-core",
"path": ".claude/homunculus/instincts/inherited/certd-instincts.yaml",
"description": "Continuous-learning instincts derived from repository patterns."
}
],
"workflows": [],
"adapters": {
"claudeCode": {
"skillPath": ".claude/skills/certd/SKILL.md",
"identityPath": ".claude/identity.json",
"commandPaths": []
},
"codex": {
"configPath": ".codex/config.toml",
"agentsGuidePath": ".codex/AGENTS.md",
"skillPath": ".agents/skills/certd/SKILL.md"
}
}
}
@@ -0,0 +1,486 @@
# Instincts generated from https://github.com/certd/certd
# Generated: 2026-07-30T02:08:05.402Z
# Version: 2.0
# NOTE: This file supplements (does not replace) any existing curated instincts.
# High-confidence manually curated instincts should be preserved alongside these.
---
id: certd-commit-conventional
trigger: "when writing a commit message"
confidence: 0.85
domain: git
source: repo-analysis
source_repo: https://github.com/certd/certd
---
# Certd Commit Conventional
## Action
Use conventional commit format with prefixes: feat
## Evidence
- 2 commits analyzed
- Detected conventional commit pattern
- Examples: feat: 偏好设置支持从剪切板导入, feat: 偏好设置支持保存到账号并在登录后自动同步
---
id: certd-commit-length
trigger: "when writing a commit message"
confidence: 0.6
domain: git
source: repo-analysis
source_repo: https://github.com/certd/certd
---
# Certd Commit Length
## Action
Keep commit messages concise (~22 characters)
## Evidence
- Average commit message length: 22 chars
- Based on 2 commits
---
id: certd-naming-files
trigger: "when creating a new file"
confidence: 0.8
domain: code-style
source: repo-analysis
source_repo: https://github.com/certd/certd
---
# Certd Naming Files
## Action
Use camelCase naming convention
## Evidence
- Analyzed file naming patterns in repository
- Dominant pattern: camelCase
---
id: certd-export-style
trigger: "when exporting from a module"
confidence: 0.7
domain: code-style
source: repo-analysis
source_repo: https://github.com/certd/certd
---
# Certd Export Style
## Action
Prefer named exports
## Evidence
- Export pattern analysis
- Dominant style: named
---
id: certd-arch-feature-based
trigger: "when adding a new feature"
confidence: 0.85
domain: architecture
source: repo-analysis
source_repo: https://github.com/certd/certd
---
# Certd Arch Feature Based
## Action
Create a new folder in src/features/ with all related code colocated
## Evidence
- Feature-based module organization detected
- Structure: src/features/[feature-name]/
---
id: certd-workflow-extend-preferences-feature
trigger: "when doing extend preferences feature"
confidence: 0.6
domain: workflow
source: repo-analysis
source_repo: https://github.com/certd/certd
---
# Certd Workflow Extend Preferences Feature
## Action
Follow the extend-preferences-feature workflow:
1. Update localization files for new preference-related strings
2. Modify or add Vue components for preferences UI
3. Update or add supporting icon definitions
4. Implement or update store logic for settings
5. Add or update backend API/controller for user preferences
6. Write or update backend tests for new preference logic
7. Update backend models if necessary
## Evidence
- Workflow detected from commit patterns
- Frequency: ~2x per month
- Files: packages/ui/certd-client/src/locales/langs/en-US/preferences.ts, packages/ui/certd-client/src/locales/langs/zh-CN/preferences.ts, packages/ui/certd-client/src/vben/layouts/widgets/preferences/preferences-drawer.vue
---
id: certd-instinct-file-naming
trigger: "When creating a new file in the codebase"
confidence: 0.9
domain: code-style
source: repo-analysis
source_repo: certd/certd
---
# Certd Instinct File Naming
## Action
Name the file using camelCase
## Evidence
- Pattern in codeStyle.namingConventions.files
---
id: certd-instinct-function-naming
trigger: "When defining a new function"
confidence: 0.9
domain: code-style
source: repo-analysis
source_repo: certd/certd
---
# Certd Instinct Function Naming
## Action
Use camelCase for function names
## Evidence
- Pattern in codeStyle.namingConventions.functions
---
id: certd-instinct-class-naming
trigger: "When defining a new class"
confidence: 0.9
domain: code-style
source: repo-analysis
source_repo: certd/certd
---
# Certd Instinct Class Naming
## Action
Use PascalCase for class names
## Evidence
- Pattern in codeStyle.namingConventions.classes
---
id: certd-instinct-constant-naming
trigger: "When declaring a constant"
confidence: 0.9
domain: code-style
source: repo-analysis
source_repo: certd/certd
---
# Certd Instinct Constant Naming
## Action
Use SCREAMING_SNAKE_CASE for constant names
## Evidence
- Pattern in codeStyle.namingConventions.constants
---
id: certd-instinct-import-style
trigger: "When importing modules"
confidence: 0.8
domain: code-style
source: repo-analysis
source_repo: certd/certd
---
# Certd Instinct Import Style
## Action
Use absolute import paths
## Evidence
- Pattern in codeStyle.importStyle
---
id: certd-instinct-export-style
trigger: "When exporting from a module"
confidence: 0.8
domain: code-style
source: repo-analysis
source_repo: certd/certd
---
# Certd Instinct Export Style
## Action
Use named exports
## Evidence
- Pattern in codeStyle.exportStyle
---
id: certd-instinct-try-catch
trigger: "When handling errors in code"
confidence: 0.8
domain: code-style
source: repo-analysis
source_repo: certd/certd
---
# Certd Instinct Try Catch
## Action
Use try-catch blocks for error handling
## Evidence
- Pattern in errorHandling.style
---
id: certd-instinct-test-location
trigger: "When adding or updating tests"
confidence: 0.7
domain: testing
source: repo-analysis
source_repo: certd/certd
---
# Certd Instinct Test Location
## Action
Place tests in both source and test-specific folders as appropriate (mixed location)
## Evidence
- Pattern in architecture.folderStructure.testLocation
- Seen in files like packages/ui/certd-server/src/controller/user/mine/user-preferences.test.ts
---
id: certd-instinct-backend-test-pattern
trigger: "When adding backend logic for user preferences"
confidence: 0.8
domain: testing
source: repo-analysis
source_repo: certd/certd
---
# Certd Instinct Backend Test Pattern
## Action
Create or update a corresponding .test.ts file in the same directory
## Evidence
- Seen in packages/ui/certd-server/src/controller/user/mine/user-preferences.test.ts
---
id: certd-instinct-conventional-commits
trigger: "When writing a commit message"
confidence: 0.9
domain: git
source: repo-analysis
source_repo: certd/certd
---
# Certd Instinct Conventional Commits
## Action
Use the conventional commit format with a type prefix (e.g., feat: ...)
## Evidence
- Pattern in commits.type
- Examples: feat: 偏好设置支持从剪切板导入
---
id: certd-instinct-commit-length
trigger: "When writing a commit message"
confidence: 0.7
domain: git
source: repo-analysis
source_repo: certd/certd
---
# Certd Instinct Commit Length
## Action
Keep the commit message concise, around 22 characters on average
## Evidence
- Pattern in commits.averageLength
---
id: certd-instinct-extend-preferences-localization
trigger: "When extending or adding a user preference feature"
confidence: 0.9
domain: workflow
source: repo-analysis
source_repo: certd/certd
---
# Certd Instinct Extend Preferences Localization
## Action
Update localization files for new preference-related strings
## Evidence
- Workflow: extend-preferences-feature
- Files: packages/ui/certd-client/src/locales/langs/en-US/preferences.ts, packages/ui/certd-client/src/locales/langs/zh-CN/preferences.ts
---
id: certd-instinct-extend-preferences-ui
trigger: "When extending or adding a user preference feature"
confidence: 0.9
domain: workflow
source: repo-analysis
source_repo: certd/certd
---
# Certd Instinct Extend Preferences Ui
## Action
Modify or add Vue components for the preferences UI
## Evidence
- Workflow: extend-preferences-feature
- Files: packages/ui/certd-client/src/vben/layouts/widgets/preferences/preferences-drawer.vue
---
id: certd-instinct-extend-preferences-icons
trigger: "When extending or adding a user preference feature"
confidence: 0.8
domain: workflow
source: repo-analysis
source_repo: certd/certd
---
# Certd Instinct Extend Preferences Icons
## Action
Update or add supporting icon definitions
## Evidence
- Workflow: extend-preferences-feature
- File: packages/ui/certd-client/src/vben/icons/lucide.ts
---
id: certd-instinct-extend-preferences-store
trigger: "When extending or adding a user preference feature"
confidence: 0.9
domain: workflow
source: repo-analysis
source_repo: certd/certd
---
# Certd Instinct Extend Preferences Store
## Action
Implement or update store logic for settings
## Evidence
- Workflow: extend-preferences-feature
- File: packages/ui/certd-client/src/store/settings/index.tsx
---
id: certd-instinct-extend-preferences-backend-api
trigger: "When extending or adding a user preference feature"
confidence: 0.9
domain: workflow
source: repo-analysis
source_repo: certd/certd
---
# Certd Instinct Extend Preferences Backend Api
## Action
Add or update backend API/controller for user preferences
## Evidence
- Workflow: extend-preferences-feature
- Files: packages/ui/certd-server/src/controller/user/mine/user-preferences.ts, packages/ui/certd-server/src/controller/user/mine/user-settings-controller.ts
---
id: certd-instinct-extend-preferences-backend-test
trigger: "When extending or adding a user preference feature"
confidence: 0.9
domain: workflow
source: repo-analysis
source_repo: certd/certd
---
# Certd Instinct Extend Preferences Backend Test
## Action
Write or update backend tests for new preference logic
## Evidence
- Workflow: extend-preferences-feature
- File: packages/ui/certd-server/src/controller/user/mine/user-preferences.test.ts
---
id: certd-instinct-extend-preferences-models
trigger: "When extending or adding a user preference feature and new data is needed"
confidence: 0.8
domain: workflow
source: repo-analysis
source_repo: certd/certd
---
# Certd Instinct Extend Preferences Models
## Action
Update backend models if necessary
## Evidence
- Workflow: extend-preferences-feature
- File: packages/ui/certd-server/src/modules/mine/service/models.ts
+14
View File
@@ -0,0 +1,14 @@
{
"version": "2.0",
"technicalLevel": "technical",
"preferredStyle": {
"verbosity": "detailed",
"codeComments": true,
"explanations": true
},
"domains": [
"typescript"
],
"suggestedBy": "ecc-tools-repo-analysis",
"createdAt": "2026-07-30T02:08:05.402Z"
}
+133
View File
@@ -0,0 +1,133 @@
```markdown
# certd Development Patterns
> Auto-generated skill from repository analysis
## Overview
This skill teaches the core development patterns and workflows for the `certd` TypeScript codebase. It covers coding conventions, file organization, commit patterns, and detailed step-by-step instructions for extending user preferences—a common feature workflow. The repository is structured for modularity, with clear separation between UI and backend logic, and emphasizes maintainable, convention-driven development.
## Coding Conventions
### File Naming
- **CamelCase** is used for file names.
- Example: `userPreferences.ts`, `preferencesDrawer.vue`
### Import Style
- **Absolute imports** are preferred.
- Example:
```typescript
import { getUserPreferences } from 'packages/ui/certd-client/src/vben/layouts/widgets/preferences/api';
```
### Export Style
- **Named exports** are used throughout the codebase.
- Example:
```typescript
export function getUserPreferences() { ... }
export const PREFERENCE_KEYS = [ ... ];
```
### Commit Patterns
- **Conventional commits** are used, with the `feat` prefix for new features.
- Example:
```
feat: add account sync to preferences
```
## Workflows
### Extend Preferences Feature
**Trigger:** When someone wants to add or enhance a user preference feature (e.g., import/export, sync to account).
**Command:** `/extend-preferences`
Follow these steps to extend user preferences functionality:
1. **Update localization files**
Add or modify strings in:
- `packages/ui/certd-client/src/locales/langs/en-US/preferences.ts`
- `packages/ui/certd-client/src/locales/langs/zh-CN/preferences.ts`
```typescript
// en-US/preferences.ts
export default {
sync: "Sync Preferences",
import: "Import Preferences",
// ...
};
```
2. **Modify or add Vue components for preferences UI**
Update or create components such as:
- `preferences-drawer.vue`
- `account-sync.ts`
```vue
<!-- preferences-drawer.vue -->
<template>
<div>
<button @click="syncPreferences">{{ $t('preferences.sync') }}</button>
</div>
</template>
```
3. **Update or add supporting icon definitions**
Edit:
- `packages/ui/certd-client/src/vben/icons/lucide.ts`
```typescript
export const SyncIcon = { /* icon definition */ };
```
4. **Implement or update store logic for settings**
Update:
- `packages/ui/certd-client/src/store/settings/index.tsx`
```typescript
export function syncPreferencesToAccount() { ... }
```
5. **Add or update backend API/controller for user preferences**
Edit or add:
- `packages/ui/certd-client/src/vben/layouts/widgets/preferences/api.ts`
- `packages/ui/certd-server/src/controller/user/mine/user-preferences.ts`
```typescript
// user-preferences.ts
export async function updateUserPreferences(req, res) { ... }
```
6. **Write or update backend tests for new preference logic**
Add or update:
- `packages/ui/certd-server/src/controller/user/mine/user-preferences.test.ts`
```typescript
test('should sync preferences', async () => { ... });
```
7. **Update backend models if necessary**
Edit:
- `packages/ui/certd-server/src/modules/mine/service/models.ts`
```typescript
export interface UserPreferences { ... }
```
## Testing Patterns
- **Test files** follow the `*.test.*` naming convention.
- Example: `user-preferences.test.ts`
- **Testing framework** is not explicitly detected, but tests are written in TypeScript and likely use a standard Node.js testing library (e.g., Jest or Mocha).
- **Test Example:**
```typescript
test('should update preferences', async () => {
// Arrange
// Act
// Assert
});
```
## Commands
| Command | Purpose |
|----------------------|--------------------------------------------------------------|
| /extend-preferences | Guide to extend or enhance user preference functionality |
```
+26
View File
@@ -0,0 +1,26 @@
# ECC for Codex CLI
This supplements the root `AGENTS.md` with a repo-local ECC baseline.
## Repo Skill
- Repo-generated Codex skill: `.agents/skills/certd/SKILL.md`
- Claude-facing companion skill: `.claude/skills/certd/SKILL.md`
- Keep user-specific credentials and private MCPs in `~/.codex/config.toml`, not in this repo.
## MCP Baseline
Treat `.codex/config.toml` as the default ECC-safe baseline for work in this repository.
The generated baseline enables GitHub, Context7, Exa, Memory, Playwright, and Sequential Thinking.
## Multi-Agent Support
- Explorer: read-only evidence gathering
- Reviewer: correctness, security, and regression review
- Docs researcher: API and release-note verification
## Workflow Files
- No dedicated workflow command files were generated for this repo.
Use these workflow files as reusable task scaffolds when the detected repository workflows recur.
+9
View File
@@ -0,0 +1,9 @@
model = "gpt-5.4"
model_reasoning_effort = "medium"
sandbox_mode = "read-only"
developer_instructions = """
Verify APIs, framework behavior, and release-note claims against primary documentation before changes land.
Cite the exact docs or file paths that support each claim.
Do not invent undocumented behavior.
"""
+9
View File
@@ -0,0 +1,9 @@
model = "gpt-5.4"
model_reasoning_effort = "medium"
sandbox_mode = "read-only"
developer_instructions = """
Stay in exploration mode.
Trace the real execution path, cite files and symbols, and avoid proposing fixes unless the parent agent asks for them.
Prefer targeted search and file reads over broad scans.
"""
+9
View File
@@ -0,0 +1,9 @@
model = "gpt-5.4"
model_reasoning_effort = "high"
sandbox_mode = "read-only"
developer_instructions = """
Review like an owner.
Prioritize correctness, security, behavioral regressions, and missing tests.
Lead with concrete findings and avoid style-only feedback unless it hides a real bug.
"""
+48
View File
@@ -0,0 +1,48 @@
#:schema https://developers.openai.com/codex/config-schema.json
# ECC Tools generated Codex baseline
approval_policy = "on-request"
sandbox_mode = "workspace-write"
web_search = "live"
[mcp_servers.github]
command = "npx"
args = ["-y", "@modelcontextprotocol/server-github"]
[mcp_servers.context7]
command = "npx"
args = ["-y", "@upstash/context7-mcp@latest"]
[mcp_servers.exa]
url = "https://mcp.exa.ai/mcp"
[mcp_servers.memory]
command = "npx"
args = ["-y", "@modelcontextprotocol/server-memory"]
[mcp_servers.playwright]
command = "npx"
args = ["-y", "@playwright/mcp@latest", "--extension"]
[mcp_servers.sequential-thinking]
command = "npx"
args = ["-y", "@modelcontextprotocol/server-sequential-thinking"]
[features]
multi_agent = true
[agents]
max_threads = 6
max_depth = 1
[agents.explorer]
description = "Read-only codebase explorer for gathering evidence before changes are proposed."
config_file = "agents/explorer.toml"
[agents.reviewer]
description = "PR reviewer focused on correctness, security, and missing tests."
config_file = "agents/reviewer.toml"
[agents.docs_researcher]
description = "Documentation specialist that verifies APIs, framework behavior, and release notes."
config_file = "agents/docs-researcher.toml"
+1
View File
@@ -400,6 +400,7 @@ export class OAuthDemoAccess extends BaseAccess {
4. **错误处理**:API 调用失败时应抛出明确的错误信息。
5. **测试方法**:实现 `onTestRequest` 方法,以便用户可以测试授权是否正常。
6. **统一接口调用**:封装统一的 API 请求方法,避免重复编写错误处理逻辑。
7. **旧版数据兼容**: 新增注解的插件参数,必须要考虑旧版数据兼容,比如新增一个deployType参数,有两种值:`default``custom`,需要在使用时判空,走旧版逻辑。
## 开发技巧
+1
View File
@@ -454,3 +454,4 @@ new TencentDnsProvider();
3. **记录结构**:定义适合对应云平台的记录数据结构,至少包含 id 字段用于删除记录。
4. **日志输出**:使用 `this.logger` 输出日志,而不是 `console`,参数文本化,不要传对象,否则会输出`[object Object]}`
5. **错误处理**:API 调用失败时应抛出明确的错误信息。
6. **旧版数据兼容**: 新增插件参数,必须要考虑旧版数据兼容,比如新增一个deployType参数,有两种值:`default``custom`,需要在使用时判空,走旧版逻辑。
+3 -2
View File
@@ -281,12 +281,12 @@ export class DemoTest extends AbstractTaskPlugin {
return {
value: item.siteName,
label: item.siteName,
domain: item.siteName,
domain: item.siteName, //这里必须要包含domain 否则后面分组时候全部分配到未匹配中
};
});
//将站点域名名称根据证书域名进行匹配分组,分成匹配的和不匹配的两组选项,返回给前端,供用户选择
return {
list: optionsUtils.buildGroupOptions(options, this.certDomains),
list: optionsUtils.buildGroupOptions(options, this.certDomains), //分组后的列表
total: siteRes.length,
};
}
@@ -641,6 +641,7 @@ new AliyunOSSDeploy();
4. **日志输出**:使用 `this.logger` 输出日志,而不是 `console`,参数文本化,不要传对象,否则会输出`[object Object]}`
5. **错误处理**:执行过程中的错误应被捕获并记录。
6. **授权获取**:使用 `this.getAccess(accessId)` 获取授权信息。
7. **旧版数据兼容** 新增@TaskInput注解的插件参数,必须要考虑旧版数据兼容,比如新增一个deployType参数,有两种值:`default``custom`,需要在使用时判空,走旧版逻辑。
## 部署逻辑注意事项
+8 -1
View File
@@ -70,6 +70,8 @@ Certd 是可私有化部署的 SSL/TLS 证书自动化管理平台,提供 Web
- 先读本文,再按任务读取具体代码或技能文件。
- PowerShell 读取中文、Markdown、locale、文档类文件时使用 `Get-Content -Raw -Encoding UTF8`;仍乱码时先执行 `[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new()`
- PowerShell 中用 `rg` 搜索含引号、括号、反斜杠的 pattern 时,优先用单引号包裹整个 pattern,例如 `rg 'await import\("tencentcloud-sdk-nodejs' packages/ui/certd-server/src -g '*.ts'`
- 手工编辑或创建文件时优先使用 `apply_patch`。单个文件内有多处不连续改动时,拆成多个独立的 `*** Update File` 块,每块只改一处附近上下文;不要在同一个 update hunk 里强塞多个 `@@`
- 只有真正机械化的大批量替换、格式化或生成任务才考虑脚本/工具。若必须使用临时脚本,应放在临时目录并在同一个受控步骤内完成创建、执行、删除;不要把临时脚本落在仓库里跨多步工具调用执行。
- 不要主动运行 `pnpm install`;缺依赖、TTY、网络导致安装或测试失败时,停止尝试并说明环境问题。
- 优先沿用现有模块、插件、service、页面模式;不要为形式上的复用制造过度抽象。
- 代码可读性优先于短写法。复杂条件、三元表达式、链式调用、内联对象和多层 helper 调用要拆成命名清晰的中间变量或小方法。
@@ -102,7 +104,7 @@ Certd 是可私有化部署的 SSL/TLS 证书自动化管理平台,提供 Web
- 只有需要事务传播时才定义 `ctx`;普通查询、纯函数和简单私有方法继续使用明确参数。
- 需要按事务上下文取 Repository 时,用 `BaseService.getRepo(ctx, EntityClass)`
- 需要“有事务则复用、无事务则开启”时,用 `BaseService.transactionWithCtx(ctx, callback)`
- 拼接可选 `projectId` 查询条件时,用 `BaseService.buildUserProjectQuery(userId, projectId)`;不要直接写 `{ userId, projectId }`
- 拼接可选 `projectId` 查询条件时,**必须**使`BaseService.buildUserProjectQuery(userId, projectId)`,禁止直接写 `{ userId, projectId }`因为 `projectId` 可能为 `null`/`undefined`,直接放入查询会生成错误的 `WHERE projectId = NULL` 条件。
- `ctx` 类型复用 `BaseService` 导出的 `ServiceContext`
- 新增 service 方法避免与 `BaseService` 方法签名冲突,例如不要用 `delete(id)` 覆盖 `delete(ids, where?)`;改用 `deleteById` 等具体名称。
@@ -210,3 +212,8 @@ Certd 是可私有化部署的 SSL/TLS 证书自动化管理平台,提供 Web
- 后端业务数据、接口、实体、权限、迁移:改 `packages/ui/certd-server/src/modules``src/controller`
- 表单、列表、插件配置 UI:改 `packages/ui/certd-client/src/views/certd` 及对应 `src/api`
## 其他注意事项
### 旧版数据兼容
- 新增插件参数时,必须要考虑旧版数据兼容,比如新增一个deployType参数,有两种值:`default``custom`,需要在使用时判空,走旧版逻辑。
+86
View File
@@ -3,6 +3,92 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.42.6](https://github.com/certd/certd/compare/v1.42.5...v1.42.6) (2026-07-18)
### Bug Fixes
* 修复正常批量删除流水线报权限不足的bug ([5b50083](https://github.com/certd/certd/commit/5b500830a122c6c42dab054e57fed509050f94da))
### Performance Improvements
* 优化动态加载依赖镜像地址,多次重试 ([5f53b81](https://github.com/certd/certd/commit/5f53b81c75dd242b4260ac08cae14c6d1a08a883))
## [1.42.5](https://github.com/certd/certd/compare/v1.42.4...v1.42.5) (2026-07-15)
### Bug Fixes
* 修复上传到cos报runtimeDepsService未初始化的问题 ([167b303](https://github.com/certd/certd/commit/167b303faeca02cc11cf97e4be2a3df914852167))
* 修复dingtalk通知格式没有换行的bug ([7ed1be9](https://github.com/certd/certd/commit/7ed1be994f8b4b74cdeb38743060c912c027248b))
### Performance Improvements
* 给SQLITE_IOERR_WRITE增加友好报错提示,将certd:latest镜像改为certd:slim ([b91c9e4](https://github.com/certd/certd/commit/b91c9e4ea671cb359ef164e27864de1d66cba9d3))
* 优化vke keubconfig获取方式,改成先查询,如果没有再创建临时config ([604fa5b](https://github.com/certd/certd/commit/604fa5be634d099d797bfee5c2b0f26ce0ac8461))
## [1.42.4](https://github.com/certd/certd/compare/v1.42.3...v1.42.4) (2026-07-11)
### Bug Fixes
* 修复火山引擎查不到自定义源站域名的问题 ([02dabe1](https://github.com/certd/certd/commit/02dabe11db3e9b13ca4621ce9ddd2b808bfca390))
* 修复火山引擎自定义源站域名查询不到的问题 ([e44bf9d](https://github.com/certd/certd/commit/e44bf9d77375d48ac7fd1582e69fae02dfd248fa))
* **pipeline:** 重构运行时依赖加载逻辑,修复火山引擎DNS解析报runtimeDepsService未初始化的bug ([ec69b8f](https://github.com/certd/certd/commit/ec69b8f11bfd4b20991aef74a72a47182ca79a9d))
## [1.42.3](https://github.com/certd/certd/compare/v1.42.2...v1.42.3) (2026-07-08)
### Bug Fixes
* **volcengine-alb:** 修复火山引擎ALB 默认证书部署类型会部署到扩展证书的问题 ([0a068a2](https://github.com/certd/certd/commit/0a068a274673e9768954e9f7367c267d44f3b530))
## [1.42.2](https://github.com/certd/certd/compare/v1.42.1...v1.42.2) (2026-07-07)
### Bug Fixes
* 修复火山视频点播源站选择不到自定义源站的bug ([0071bcb](https://github.com/certd/certd/commit/0071bcb0e4dd108c86d7ca01820a9f6e6960e440))
* 修复企业模式下弹出邮箱绑定提醒的问题 ([8d9dad9](https://github.com/certd/certd/commit/8d9dad9c82f6f2fd3ab3040068946a33f37145b1))
* 修复AsiaIsp CDN证书重复情况下部署失败的问题 ([c3d6db3](https://github.com/certd/certd/commit/c3d6db3f1ef2f1c897b7989521fe8809dffaded1))
* 修复cname用阿里云校验时报找不到runtimeDepsService的错误 ([072edd7](https://github.com/certd/certd/commit/072edd7affee424ab3411f4d41d338f084d7cac6))
## [1.42.1](https://github.com/certd/certd/compare/v1.42.0...v1.42.1) (2026-07-06)
### Bug Fixes
* 修复多域名无法使用passkey登录的bug ([d176f9c](https://github.com/certd/certd/commit/d176f9cc0ebd051a614bfac74d1616d1945fc9a3))
* 修复企业模式下登录报projectId不能为空的问题 ([a65366b](https://github.com/certd/certd/commit/a65366bbe1aadea8baaffbdadab58a5b631d9417))
* **login:** 修复输入法 composing 状态下回车触发提交的问题 ([b74db81](https://github.com/certd/certd/commit/b74db81304bbe68476bbec5ea4307a2264060e92))
### Performance Improvements
* **certd-server:** 使用 jks-go转换jks证书,大幅精简镜像大小 ([c78898e](https://github.com/certd/certd/commit/c78898e4c10dd1701467d2e42e3f72bd8f2a352f))
* **pipeline:** 将默认历史保留条数从30调整为100 ([d3e4677](https://github.com/certd/certd/commit/d3e4677ea4fac8e7533749d7f4187e410489e536))
# [1.42.0](https://github.com/certd/certd/compare/v1.41.4...v1.42.0) (2026-07-05)
### Bug Fixes
* 修复jdk证书格式的问题 ([260f5ae](https://github.com/certd/certd/commit/260f5ae777b83493b0c578fe30fd00ec0c873226))
* 修复telegram - 符号转义问题 ([d5882f1](https://github.com/certd/certd/commit/d5882f16bedb09baf09ace92049b02872620f5dc))
* **aliyun:** 修复阿里云CDN/DCDN根据证书自动匹配不到证书的bug ([1ae185d](https://github.com/certd/certd/commit/1ae185d0bc356f4678bc38ca0582ce3396f82ebe))
### Features
* 通过插件配置懒加载依赖,动态加载第三方依赖包,精简安装镜像大小 ([01568ca](https://github.com/certd/certd/commit/01568ca1489069046b5a89ebdd4ced2f7f6ddf93))
### Performance Improvements
* 阿里云ESA证书部署支持SaaS模式 ([82276b5](https://github.com/certd/certd/commit/82276b53a8474a18a3d0237050907c994fc748f0))
* 【破坏性更新】 证书压缩包不再生成文件存储,而是实时打包下载,证书申请插件不再输出certZip ([7cff1a9](https://github.com/certd/certd/commit/7cff1a98424120585205889874b3ef4956a30583))
* 火山引擎点播插件支持部署到自定义源站域名 ([095791c](https://github.com/certd/certd/commit/095791cdc2b7c1f4b913b634643afec5e30fe9b0))
* 基础镜像改成node:22-trixie-slim,对网络兼容性更好 ([c66a2bd](https://github.com/certd/certd/commit/c66a2bd77ab6dbb3e3fe2c00562b66287a9429ea))
* 新增橙域网络(asia-isp) CDN证书部署插件 ([b48831e](https://github.com/certd/certd/commit/b48831e60b0059bef7ef9a34ab61c9dd2f684641))
* 优化阿里云API网关增加翻页查询 ([ed58ae3](https://github.com/certd/certd/commit/ed58ae3c5339e4a0238a92acfe7ea6d2f566ea28))
* 优化用户体验,首次访问时弹出邮箱账号绑定用以初始化账号 ([608cc2a](https://github.com/certd/certd/commit/608cc2a81ff0b4872c9fe11ed9c9c0b4b90a12a3))
* 优化ACME账号字段的选择提示 ([bfd3cac](https://github.com/certd/certd/commit/bfd3cacc687fc5cbc3cb2ca3cadbc140de300dc2))
* 支持全自动匹配部署宝塔网站证书 ([4dff48e](https://github.com/certd/certd/commit/4dff48e807c32a7623ec9206cf39c88e88f89f6a))
* **cert-plugin:** 调整更新天数自动减半逻辑,仅7天ip证书生效,其他情况下不减半 ([56e5524](https://github.com/certd/certd/commit/56e5524a0f4af3645d70bc3b3ec750b45ba8de10))
* dns默认ipv4first ([194463b](https://github.com/certd/certd/commit/194463bea9e797315aa7a724f4b2930701570419))
* **passkey:** passkey支持多域名rpid ([79f6586](https://github.com/certd/certd/commit/79f65868ca0f5162bbc2f935ce89abc28011d816))
* **plugin:** 在线插件编辑支持配置第三方依赖和插件依赖 ([635f069](https://github.com/certd/certd/commit/635f069012d4193cfb7cb051c96e28eec1247ca2))
## [1.41.4](https://github.com/certd/certd/compare/v1.41.3...v1.41.4) (2026-06-14)
### Bug Fixes
+7 -5
View File
@@ -107,11 +107,13 @@ https://certd.handfree.work/
**镜像版本:**
| 版本标签 | 基础系统 | 说明 |
| --- | --- | --- | --- |
| `latest` / `[version]` | Alpine Linux | 默认版本,镜像体积小 |
| `slim` / `[version]-slim` | Debian slim | 基于glibcdns解析兼容性好(可能需要配置security_opt -seccomp=unconfined |
| `armv7` / `[version]-armv7` | Alpine Linux | ARMv7 架构专用版本 |
| 标签 | 指定版本 | 基础系统 | 说明 |
| --- | --- | --- | --- |
| `latest` | `[version]` | Alpine Linux | 默认版本,镜像体积小 |
| `slim` | `[version]-slim` | Debian slim | 基于glibcdns解析兼容性好(可能需要配置security_opt -seccomp=unconfined |
| `armv7` | `[version]-armv7` | Alpine Linux | ARMv7 架构专用版本 |
**镜像地址:**
+86
View File
@@ -3,6 +3,92 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.42.6](https://github.com/certd/certd/compare/v1.42.5...v1.42.6) (2026-07-18)
### Bug Fixes
* 修复正常批量删除流水线报权限不足的bug ([5b50083](https://github.com/certd/certd/commit/5b500830a122c6c42dab054e57fed509050f94da))
### Performance Improvements
* 优化动态加载依赖镜像地址,多次重试 ([5f53b81](https://github.com/certd/certd/commit/5f53b81c75dd242b4260ac08cae14c6d1a08a883))
## [1.42.5](https://github.com/certd/certd/compare/v1.42.4...v1.42.5) (2026-07-15)
### Bug Fixes
* 修复上传到cos报runtimeDepsService未初始化的问题 ([167b303](https://github.com/certd/certd/commit/167b303faeca02cc11cf97e4be2a3df914852167))
* 修复dingtalk通知格式没有换行的bug ([7ed1be9](https://github.com/certd/certd/commit/7ed1be994f8b4b74cdeb38743060c912c027248b))
### Performance Improvements
* 给SQLITE_IOERR_WRITE增加友好报错提示,将certd:latest镜像改为certd:slim ([b91c9e4](https://github.com/certd/certd/commit/b91c9e4ea671cb359ef164e27864de1d66cba9d3))
* 优化vke keubconfig获取方式,改成先查询,如果没有再创建临时config ([604fa5b](https://github.com/certd/certd/commit/604fa5be634d099d797bfee5c2b0f26ce0ac8461))
## [1.42.4](https://github.com/certd/certd/compare/v1.42.3...v1.42.4) (2026-07-11)
### Bug Fixes
* 修复火山引擎查不到自定义源站域名的问题 ([02dabe1](https://github.com/certd/certd/commit/02dabe11db3e9b13ca4621ce9ddd2b808bfca390))
* 修复火山引擎自定义源站域名查询不到的问题 ([e44bf9d](https://github.com/certd/certd/commit/e44bf9d77375d48ac7fd1582e69fae02dfd248fa))
* **pipeline:** 重构运行时依赖加载逻辑,修复火山引擎DNS解析报runtimeDepsService未初始化的bug ([ec69b8f](https://github.com/certd/certd/commit/ec69b8f11bfd4b20991aef74a72a47182ca79a9d))
## [1.42.3](https://github.com/certd/certd/compare/v1.42.2...v1.42.3) (2026-07-08)
### Bug Fixes
* **volcengine-alb:** 修复火山引擎ALB 默认证书部署类型会部署到扩展证书的问题 ([0a068a2](https://github.com/certd/certd/commit/0a068a274673e9768954e9f7367c267d44f3b530))
## [1.42.2](https://github.com/certd/certd/compare/v1.42.1...v1.42.2) (2026-07-07)
### Bug Fixes
* 修复火山视频点播源站选择不到自定义源站的bug ([0071bcb](https://github.com/certd/certd/commit/0071bcb0e4dd108c86d7ca01820a9f6e6960e440))
* 修复企业模式下弹出邮箱绑定提醒的问题 ([8d9dad9](https://github.com/certd/certd/commit/8d9dad9c82f6f2fd3ab3040068946a33f37145b1))
* 修复AsiaIsp CDN证书重复情况下部署失败的问题 ([c3d6db3](https://github.com/certd/certd/commit/c3d6db3f1ef2f1c897b7989521fe8809dffaded1))
* 修复cname用阿里云校验时报找不到runtimeDepsService的错误 ([072edd7](https://github.com/certd/certd/commit/072edd7affee424ab3411f4d41d338f084d7cac6))
## [1.42.1](https://github.com/certd/certd/compare/v1.42.0...v1.42.1) (2026-07-06)
### Bug Fixes
* 修复多域名无法使用passkey登录的bug ([d176f9c](https://github.com/certd/certd/commit/d176f9cc0ebd051a614bfac74d1616d1945fc9a3))
* 修复企业模式下登录报projectId不能为空的问题 ([a65366b](https://github.com/certd/certd/commit/a65366bbe1aadea8baaffbdadab58a5b631d9417))
* **login:** 修复输入法 composing 状态下回车触发提交的问题 ([b74db81](https://github.com/certd/certd/commit/b74db81304bbe68476bbec5ea4307a2264060e92))
### Performance Improvements
* **certd-server:** 使用 jks-go转换jks证书,大幅精简镜像大小 ([c78898e](https://github.com/certd/certd/commit/c78898e4c10dd1701467d2e42e3f72bd8f2a352f))
* **pipeline:** 将默认历史保留条数从30调整为100 ([d3e4677](https://github.com/certd/certd/commit/d3e4677ea4fac8e7533749d7f4187e410489e536))
# [1.42.0](https://github.com/certd/certd/compare/v1.41.4...v1.42.0) (2026-07-05)
### Bug Fixes
* 修复jdk证书格式的问题 ([260f5ae](https://github.com/certd/certd/commit/260f5ae777b83493b0c578fe30fd00ec0c873226))
* 修复telegram - 符号转义问题 ([d5882f1](https://github.com/certd/certd/commit/d5882f16bedb09baf09ace92049b02872620f5dc))
* **aliyun:** 修复阿里云CDN/DCDN根据证书自动匹配不到证书的bug ([1ae185d](https://github.com/certd/certd/commit/1ae185d0bc356f4678bc38ca0582ce3396f82ebe))
### Features
* 通过插件配置懒加载依赖,动态加载第三方依赖包,精简安装镜像大小 ([01568ca](https://github.com/certd/certd/commit/01568ca1489069046b5a89ebdd4ced2f7f6ddf93))
### Performance Improvements
* 阿里云ESA证书部署支持SaaS模式 ([82276b5](https://github.com/certd/certd/commit/82276b53a8474a18a3d0237050907c994fc748f0))
* 【破坏性更新】 证书压缩包不再生成文件存储,而是实时打包下载,证书申请插件不再输出certZip ([7cff1a9](https://github.com/certd/certd/commit/7cff1a98424120585205889874b3ef4956a30583))
* 火山引擎点播插件支持部署到自定义源站域名 ([095791c](https://github.com/certd/certd/commit/095791cdc2b7c1f4b913b634643afec5e30fe9b0))
* 基础镜像改成node:22-trixie-slim,对网络兼容性更好 ([c66a2bd](https://github.com/certd/certd/commit/c66a2bd77ab6dbb3e3fe2c00562b66287a9429ea))
* 新增橙域网络(asia-isp) CDN证书部署插件 ([b48831e](https://github.com/certd/certd/commit/b48831e60b0059bef7ef9a34ab61c9dd2f684641))
* 优化阿里云API网关增加翻页查询 ([ed58ae3](https://github.com/certd/certd/commit/ed58ae3c5339e4a0238a92acfe7ea6d2f566ea28))
* 优化用户体验,首次访问时弹出邮箱账号绑定用以初始化账号 ([608cc2a](https://github.com/certd/certd/commit/608cc2a81ff0b4872c9fe11ed9c9c0b4b90a12a3))
* 优化ACME账号字段的选择提示 ([bfd3cac](https://github.com/certd/certd/commit/bfd3cacc687fc5cbc3cb2ca3cadbc140de300dc2))
* 支持全自动匹配部署宝塔网站证书 ([4dff48e](https://github.com/certd/certd/commit/4dff48e807c32a7623ec9206cf39c88e88f89f6a))
* **cert-plugin:** 调整更新天数自动减半逻辑,仅7天ip证书生效,其他情况下不减半 ([56e5524](https://github.com/certd/certd/commit/56e5524a0f4af3645d70bc3b3ec750b45ba8de10))
* dns默认ipv4first ([194463b](https://github.com/certd/certd/commit/194463bea9e797315aa7a724f4b2930701570419))
* **passkey:** passkey支持多域名rpid ([79f6586](https://github.com/certd/certd/commit/79f65868ca0f5162bbc2f935ce89abc28011d816))
* **plugin:** 在线插件编辑支持配置第三方依赖和插件依赖 ([635f069](https://github.com/certd/certd/commit/635f069012d4193cfb7cb051c96e28eec1247ca2))
## [1.41.4](https://github.com/certd/certd/compare/v1.41.3...v1.41.4) (2026-06-14)
### Bug Fixes
+19 -18
View File
@@ -1,5 +1,5 @@
# 任务插件
`133` 款任务插件
`134` 款任务插件
## 1. 证书申请
| 序号 | 名称 | 说明 |
@@ -63,22 +63,23 @@
| 7.| **1Panel-部署面板证书** | 更新1Panel的面板证书 |
| 8.| **1Panel-更新站点证书** | 更新1Panel的站点证书 |
| 9.| **宝塔-删除过期证书** | 删除证书夹中过期证书 |
| 10.| **宝塔-WAF证书部署** | 部署宝塔云WAF/aaWAF |
| 11.| **宝塔-面板证书部署** | 部署宝塔面板本身的ssl证书 |
| 12.| **宝塔win-网站证书部署** | 部署到Windows版宝塔管理的站点的ssl证书 |
| 13.| **宝塔-网站证书部署** | 部署宝塔管理的站点的ssl证书,目前支持宝塔网站站点、docker站点等。本插件也支持aaPanel。 |
| 14.| **K8S-Apply自定义yaml** | apply自定义yaml到k8s |
| 15.| **K8S-Ingress 证书部署** | 部署证书到k8s的Ingress |
| 16.| **K8S-部署证书到Secret** | 部署证书到k8s的secret |
| 17.| **lucky-更新Lucky证书** | |
| 18.| **Plesk-部署Plesk网站证书** | |
| 19.| **Plesk-更新证书** | 不会创建新证书记录,直接更新旧的证书 |
| 20.| **雷池-更新证书(支持控制台和防护应用)** | 更新长亭雷池WAF的证书,支持更新控制台和防护应用的证书 |
| 21.| **群晖-部署证书到群晖面板** | Synology,支持6.x以上版本 |
| 22.| **群晖-刷新OTP登录有效期** | 群晖登录状态可能30天失效,需要在失效之前登录一次,刷新有效期,您可以将其放在“部署到群晖面板”任务之后 |
| 23.| **uniCloud-部署到服务空间** | 部署到服务空间 |
| 24.| **Proxmox-上传证书到Proxmox** | |
| 25.| **威联通-部署证书到威联通** | 部署证书到qnap |
| 10.| **宝塔-全自动部署** | 根据证书域名自动匹配宝塔站点,全自动部署SSL证书。新增加速域名自动感知,自动新增部署 |
| 11.| **宝塔-WAF证书部署** | 部署宝塔云WAF/aaWAF |
| 12.| **宝塔-面板证书部署** | 部署宝塔面板本身的ssl证书 |
| 13.| **宝塔win-网站证书部署** | 部署到Windows版宝塔管理的站点的ssl证书 |
| 14.| **宝塔-网站证书部署** | 部署宝塔管理的站点的ssl证书,目前支持宝塔网站站点、docker站点等。本插件也支持aaPanel。 |
| 15.| **K8S-Apply自定义yaml** | apply自定义yaml到k8s |
| 16.| **K8S-Ingress 证书部署** | 部署证书到k8s的Ingress |
| 17.| **K8S-部署证书到Secret** | 部署证书到k8s的secret |
| 18.| **lucky-更新Lucky证书** | |
| 19.| **Plesk-部署Plesk网站证书** | |
| 20.| **Plesk-更新证书** | 不会创建新证书记录,直接更新旧的证书 |
| 21.| **雷池-更新证书(支持控制台和防护应用)** | 更新长亭雷池WAF的证书,支持更新控制台和防护应用的证书。 |
| 22.| **群晖-部署证书到群晖面板** | Synology,支持6.x以上版本 |
| 23.| **群晖-刷新OTP登录有效期** | 群晖登录状态可能30天失效,需要在失效之前登录一次,刷新有效期,您可以将其放在“部署到群晖面板”任务之后 |
| 24.| **uniCloud-部署到服务空间** | 部署到服务空间 |
| 25.| **Proxmox-上传证书到Proxmox** | |
| 26.| **威联通-部署证书到威联通** | 部署证书到qnap |
## 5. 阿里云
| 序号 | 名称 | 说明 |
@@ -91,7 +92,7 @@
| 6.| **阿里云-部署证书至API网关** | 自动部署域名证书至阿里云API网关(APIGateway |
| 7.| **阿里云-部署证书至CDN** | 自动部署域名证书至阿里云CDN |
| 8.| **阿里云-部署证书至DCDN** | 依赖证书申请前置任务,自动部署域名证书至阿里云DCDN |
| 9.| **阿里云-部署至ESA** | 部署证书到阿里云ESA(边缘安全加速),自动删除过期证书 |
| 9.| **阿里云-部署至ESA** | 部署证书到阿里云ESA(边缘安全加速),支持边缘证书和SaaS证书两种模式 |
| 10.| **阿里云-部署至阿里云FC(3.0)** | 部署证书到阿里云函数计算(FC3.0) |
| 11.| **阿里云-部署至GA** | 部署证书到阿里云GA(全球加速),支持更新默认证书和扩展证书 |
| 12.| **阿里云-部署至直播(Live** | 部署证书到阿里云视频直播(Live)域名 |
+1 -1
View File
@@ -9,5 +9,5 @@
}
},
"npmClient": "pnpm",
"version": "1.41.4"
"version": "1.42.6"
}
+1
View File
@@ -39,6 +39,7 @@
"test:unit": "cross-env NODE_ENV=unittest pnpm -r --workspace-concurrency=1 run test:unit",
"pub": "echo 1",
"dev": "pnpm run -r --parallel compile ",
"lint_all": "pnpm run -r --parallel lint ",
"pub_all": "node ./scripts/pub-all.js",
"release": "time /t >trigger/release.trigger && git add trigger/release.trigger && git commit -m \"build: release\" && git push",
"publish_to_atomgit": "node --experimental-json-modules ./scripts/publish-atomgit.js",
+30
View File
@@ -3,6 +3,36 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.42.6](https://github.com/publishlab/node-acme-client/compare/v1.42.5...v1.42.6) (2026-07-18)
**Note:** Version bump only for package @certd/acme-client
## [1.42.5](https://github.com/publishlab/node-acme-client/compare/v1.42.4...v1.42.5) (2026-07-15)
**Note:** Version bump only for package @certd/acme-client
## [1.42.4](https://github.com/publishlab/node-acme-client/compare/v1.42.3...v1.42.4) (2026-07-11)
**Note:** Version bump only for package @certd/acme-client
## [1.42.3](https://github.com/publishlab/node-acme-client/compare/v1.42.2...v1.42.3) (2026-07-08)
**Note:** Version bump only for package @certd/acme-client
## [1.42.2](https://github.com/publishlab/node-acme-client/compare/v1.42.1...v1.42.2) (2026-07-07)
**Note:** Version bump only for package @certd/acme-client
## [1.42.1](https://github.com/publishlab/node-acme-client/compare/v1.42.0...v1.42.1) (2026-07-06)
**Note:** Version bump only for package @certd/acme-client
# [1.42.0](https://github.com/publishlab/node-acme-client/compare/v1.41.4...v1.42.0) (2026-07-05)
### Performance Improvements
* 新增橙域网络(asia-isp) CDN证书部署插件 ([b48831e](https://github.com/publishlab/node-acme-client/commit/b48831e60b0059bef7ef9a34ab61c9dd2f684641))
## [1.41.4](https://github.com/publishlab/node-acme-client/compare/v1.41.3...v1.41.4) (2026-06-14)
**Note:** Version bump only for package @certd/acme-client
-518
View File
@@ -1,518 +0,0 @@
## Classes
<dl>
<dt><a href="#AcmeClient">AcmeClient</a></dt>
<dd><p>AcmeClient</p>
</dd>
</dl>
## Objects
<dl>
<dt><a href="#Client">Client</a> : <code>object</code></dt>
<dd><p>ACME client</p>
</dd>
</dl>
<a name="AcmeClient"></a>
## AcmeClient
AcmeClient
**Kind**: global class
* [AcmeClient](#AcmeClient)
* [new AcmeClient(opts)](#new_AcmeClient_new)
* [.getTermsOfServiceUrl()](#AcmeClient+getTermsOfServiceUrl) ⇒ <code>Promise.&lt;(string\|null)&gt;</code>
* [.getAccountUrl()](#AcmeClient+getAccountUrl) ⇒ <code>string</code>
* [.createAccount([data])](#AcmeClient+createAccount) ⇒ <code>Promise.&lt;object&gt;</code>
* [.updateAccount([data])](#AcmeClient+updateAccount) ⇒ <code>Promise.&lt;object&gt;</code>
* [.updateAccountKey(newAccountKey, [data])](#AcmeClient+updateAccountKey) ⇒ <code>Promise.&lt;object&gt;</code>
* [.createOrder(data)](#AcmeClient+createOrder) ⇒ <code>Promise.&lt;object&gt;</code>
* [.getOrder(order)](#AcmeClient+getOrder) ⇒ <code>Promise.&lt;object&gt;</code>
* [.finalizeOrder(order, csr)](#AcmeClient+finalizeOrder) ⇒ <code>Promise.&lt;object&gt;</code>
* [.getAuthorizations(order)](#AcmeClient+getAuthorizations) ⇒ <code>Promise.&lt;Array.&lt;object&gt;&gt;</code>
* [.deactivateAuthorization(authz)](#AcmeClient+deactivateAuthorization) ⇒ <code>Promise.&lt;object&gt;</code>
* [.getChallengeKeyAuthorization(challenge)](#AcmeClient+getChallengeKeyAuthorization) ⇒ <code>Promise.&lt;string&gt;</code>
* [.verifyChallenge(authz, challenge)](#AcmeClient+verifyChallenge) ⇒ <code>Promise</code>
* [.completeChallenge(challenge)](#AcmeClient+completeChallenge) ⇒ <code>Promise.&lt;object&gt;</code>
* [.waitForValidStatus(item)](#AcmeClient+waitForValidStatus) ⇒ <code>Promise.&lt;object&gt;</code>
* [.getCertificate(order, [preferredChain])](#AcmeClient+getCertificate) ⇒ <code>Promise.&lt;string&gt;</code>
* [.revokeCertificate(cert, [data])](#AcmeClient+revokeCertificate) ⇒ <code>Promise</code>
* [.auto(opts)](#AcmeClient+auto) ⇒ <code>Promise.&lt;string&gt;</code>
<a name="new_AcmeClient_new"></a>
### new AcmeClient(opts)
| Param | Type | Description |
| --- | --- | --- |
| opts | <code>object</code> | |
| opts.directoryUrl | <code>string</code> | ACME directory URL |
| opts.accountKey | <code>buffer</code> \| <code>string</code> | PEM encoded account private key |
| [opts.accountUrl] | <code>string</code> | Account URL, default: `null` |
| [opts.externalAccountBinding] | <code>object</code> | |
| [opts.externalAccountBinding.kid] | <code>string</code> | External account binding KID |
| [opts.externalAccountBinding.hmacKey] | <code>string</code> | External account binding HMAC key |
| [opts.backoffAttempts] | <code>number</code> | Maximum number of backoff attempts, default: `10` |
| [opts.backoffMin] | <code>number</code> | Minimum backoff attempt delay in milliseconds, default: `5000` |
| [opts.backoffMax] | <code>number</code> | Maximum backoff attempt delay in milliseconds, default: `30000` |
**Example**
Create ACME client instance
```js
const client = new acme.Client({
directoryUrl: acme.directory.letsencrypt.staging,
accountKey: 'Private key goes here',
});
```
**Example**
Create ACME client instance
```js
const client = new acme.Client({
directoryUrl: acme.directory.letsencrypt.staging,
accountKey: 'Private key goes here',
accountUrl: 'Optional account URL goes here',
backoffAttempts: 10,
backoffMin: 5000,
backoffMax: 30000,
});
```
**Example**
Create ACME client with external account binding
```js
const client = new acme.Client({
directoryUrl: 'https://acme-provider.example.com/directory-url',
accountKey: 'Private key goes here',
externalAccountBinding: {
kid: 'YOUR-EAB-KID',
hmacKey: 'YOUR-EAB-HMAC-KEY',
},
});
```
<a name="AcmeClient+getTermsOfServiceUrl"></a>
### acmeClient.getTermsOfServiceUrl() ⇒ <code>Promise.&lt;(string\|null)&gt;</code>
Get Terms of Service URL if available
**Kind**: instance method of [<code>AcmeClient</code>](#AcmeClient)
**Returns**: <code>Promise.&lt;(string\|null)&gt;</code> - ToS URL
**Example**
Get Terms of Service URL
```js
const termsOfService = client.getTermsOfServiceUrl();
if (!termsOfService) {
// CA did not provide Terms of Service
}
```
<a name="AcmeClient+getAccountUrl"></a>
### acmeClient.getAccountUrl() ⇒ <code>string</code>
Get current account URL
**Kind**: instance method of [<code>AcmeClient</code>](#AcmeClient)
**Returns**: <code>string</code> - Account URL
**Throws**:
- <code>Error</code> No account URL found
**Example**
Get current account URL
```js
try {
const accountUrl = client.getAccountUrl();
}
catch (e) {
// No account URL exists, need to create account first
}
```
<a name="AcmeClient+createAccount"></a>
### acmeClient.createAccount([data]) ⇒ <code>Promise.&lt;object&gt;</code>
Create a new account
https://datatracker.ietf.org/doc/html/rfc8555#section-7.3
**Kind**: instance method of [<code>AcmeClient</code>](#AcmeClient)
**Returns**: <code>Promise.&lt;object&gt;</code> - Account
| Param | Type | Description |
| --- | --- | --- |
| [data] | <code>object</code> | Request data |
**Example**
Create a new account
```js
const account = await client.createAccount({
termsOfServiceAgreed: true,
});
```
**Example**
Create a new account with contact info
```js
const account = await client.createAccount({
termsOfServiceAgreed: true,
contact: ['mailto:test@example.com'],
});
```
<a name="AcmeClient+updateAccount"></a>
### acmeClient.updateAccount([data]) ⇒ <code>Promise.&lt;object&gt;</code>
Update existing account
https://datatracker.ietf.org/doc/html/rfc8555#section-7.3.2
**Kind**: instance method of [<code>AcmeClient</code>](#AcmeClient)
**Returns**: <code>Promise.&lt;object&gt;</code> - Account
| Param | Type | Description |
| --- | --- | --- |
| [data] | <code>object</code> | Request data |
**Example**
Update existing account
```js
const account = await client.updateAccount({
contact: ['mailto:foo@example.com'],
});
```
<a name="AcmeClient+updateAccountKey"></a>
### acmeClient.updateAccountKey(newAccountKey, [data]) ⇒ <code>Promise.&lt;object&gt;</code>
Update account private key
https://datatracker.ietf.org/doc/html/rfc8555#section-7.3.5
**Kind**: instance method of [<code>AcmeClient</code>](#AcmeClient)
**Returns**: <code>Promise.&lt;object&gt;</code> - Account
| Param | Type | Description |
| --- | --- | --- |
| newAccountKey | <code>buffer</code> \| <code>string</code> | New PEM encoded private key |
| [data] | <code>object</code> | Additional request data |
**Example**
Update account private key
```js
const newAccountKey = 'New private key goes here';
const result = await client.updateAccountKey(newAccountKey);
```
<a name="AcmeClient+createOrder"></a>
### acmeClient.createOrder(data) ⇒ <code>Promise.&lt;object&gt;</code>
Create a new order
https://datatracker.ietf.org/doc/html/rfc8555#section-7.4
**Kind**: instance method of [<code>AcmeClient</code>](#AcmeClient)
**Returns**: <code>Promise.&lt;object&gt;</code> - Order
| Param | Type | Description |
| --- | --- | --- |
| data | <code>object</code> | Request data |
**Example**
Create a new order
```js
const order = await client.createOrder({
identifiers: [
{ type: 'dns', value: 'example.com' },
{ type: 'dns', value: 'test.example.com' },
],
});
```
<a name="AcmeClient+getOrder"></a>
### acmeClient.getOrder(order) ⇒ <code>Promise.&lt;object&gt;</code>
Refresh order object from CA
https://datatracker.ietf.org/doc/html/rfc8555#section-7.4
**Kind**: instance method of [<code>AcmeClient</code>](#AcmeClient)
**Returns**: <code>Promise.&lt;object&gt;</code> - Order
| Param | Type | Description |
| --- | --- | --- |
| order | <code>object</code> | Order object |
**Example**
```js
const order = { ... }; // Previously created order object
const result = await client.getOrder(order);
```
<a name="AcmeClient+finalizeOrder"></a>
### acmeClient.finalizeOrder(order, csr) ⇒ <code>Promise.&lt;object&gt;</code>
Finalize order
https://datatracker.ietf.org/doc/html/rfc8555#section-7.4
**Kind**: instance method of [<code>AcmeClient</code>](#AcmeClient)
**Returns**: <code>Promise.&lt;object&gt;</code> - Order
| Param | Type | Description |
| --- | --- | --- |
| order | <code>object</code> | Order object |
| csr | <code>buffer</code> \| <code>string</code> | PEM encoded Certificate Signing Request |
**Example**
Finalize order
```js
const order = { ... }; // Previously created order object
const csr = { ... }; // Previously created Certificate Signing Request
const result = await client.finalizeOrder(order, csr);
```
<a name="AcmeClient+getAuthorizations"></a>
### acmeClient.getAuthorizations(order) ⇒ <code>Promise.&lt;Array.&lt;object&gt;&gt;</code>
Get identifier authorizations from order
https://datatracker.ietf.org/doc/html/rfc8555#section-7.5
**Kind**: instance method of [<code>AcmeClient</code>](#AcmeClient)
**Returns**: <code>Promise.&lt;Array.&lt;object&gt;&gt;</code> - Authorizations
| Param | Type | Description |
| --- | --- | --- |
| order | <code>object</code> | Order |
**Example**
Get identifier authorizations
```js
const order = { ... }; // Previously created order object
const authorizations = await client.getAuthorizations(order);
authorizations.forEach((authz) => {
const { challenges } = authz;
});
```
<a name="AcmeClient+deactivateAuthorization"></a>
### acmeClient.deactivateAuthorization(authz) ⇒ <code>Promise.&lt;object&gt;</code>
Deactivate identifier authorization
https://datatracker.ietf.org/doc/html/rfc8555#section-7.5.2
**Kind**: instance method of [<code>AcmeClient</code>](#AcmeClient)
**Returns**: <code>Promise.&lt;object&gt;</code> - Authorization
| Param | Type | Description |
| --- | --- | --- |
| authz | <code>object</code> | Identifier authorization |
**Example**
Deactivate identifier authorization
```js
const authz = { ... }; // Identifier authorization resolved from previously created order
const result = await client.deactivateAuthorization(authz);
```
<a name="AcmeClient+getChallengeKeyAuthorization"></a>
### acmeClient.getChallengeKeyAuthorization(challenge) ⇒ <code>Promise.&lt;string&gt;</code>
Get key authorization for ACME challenge
https://datatracker.ietf.org/doc/html/rfc8555#section-8.1
**Kind**: instance method of [<code>AcmeClient</code>](#AcmeClient)
**Returns**: <code>Promise.&lt;string&gt;</code> - Key authorization
| Param | Type | Description |
| --- | --- | --- |
| challenge | <code>object</code> | Challenge object returned by API |
**Example**
Get challenge key authorization
```js
const challenge = { ... }; // Challenge from previously resolved identifier authorization
const key = await client.getChallengeKeyAuthorization(challenge);
// Write key somewhere to satisfy challenge
```
<a name="AcmeClient+verifyChallenge"></a>
### acmeClient.verifyChallenge(authz, challenge) ⇒ <code>Promise</code>
Verify that ACME challenge is satisfied
**Kind**: instance method of [<code>AcmeClient</code>](#AcmeClient)
| Param | Type | Description |
| --- | --- | --- |
| authz | <code>object</code> | Identifier authorization |
| challenge | <code>object</code> | Authorization challenge |
**Example**
Verify satisfied ACME challenge
```js
const authz = { ... }; // Identifier authorization
const challenge = { ... }; // Satisfied challenge
await client.verifyChallenge(authz, challenge);
```
<a name="AcmeClient+completeChallenge"></a>
### acmeClient.completeChallenge(challenge) ⇒ <code>Promise.&lt;object&gt;</code>
Notify CA that challenge has been completed
https://datatracker.ietf.org/doc/html/rfc8555#section-7.5.1
**Kind**: instance method of [<code>AcmeClient</code>](#AcmeClient)
**Returns**: <code>Promise.&lt;object&gt;</code> - Challenge
| Param | Type | Description |
| --- | --- | --- |
| challenge | <code>object</code> | Challenge object returned by API |
**Example**
Notify CA that challenge has been completed
```js
const challenge = { ... }; // Satisfied challenge
const result = await client.completeChallenge(challenge);
```
<a name="AcmeClient+waitForValidStatus"></a>
### acmeClient.waitForValidStatus(item) ⇒ <code>Promise.&lt;object&gt;</code>
Wait for ACME provider to verify status on a order, authorization or challenge
https://datatracker.ietf.org/doc/html/rfc8555#section-7.5.1
**Kind**: instance method of [<code>AcmeClient</code>](#AcmeClient)
**Returns**: <code>Promise.&lt;object&gt;</code> - Valid order, authorization or challenge
| Param | Type | Description |
| --- | --- | --- |
| item | <code>object</code> | An order, authorization or challenge object |
**Example**
Wait for valid challenge status
```js
const challenge = { ... };
await client.waitForValidStatus(challenge);
```
**Example**
Wait for valid authorization status
```js
const authz = { ... };
await client.waitForValidStatus(authz);
```
**Example**
Wait for valid order status
```js
const order = { ... };
await client.waitForValidStatus(order);
```
<a name="AcmeClient+getCertificate"></a>
### acmeClient.getCertificate(order, [preferredChain]) ⇒ <code>Promise.&lt;string&gt;</code>
Get certificate from ACME order
https://datatracker.ietf.org/doc/html/rfc8555#section-7.4.2
**Kind**: instance method of [<code>AcmeClient</code>](#AcmeClient)
**Returns**: <code>Promise.&lt;string&gt;</code> - Certificate
| Param | Type | Default | Description |
| --- | --- | --- | --- |
| order | <code>object</code> | | Order object |
| [preferredChain] | <code>string</code> | <code>null</code> | Indicate which certificate chain is preferred if a CA offers multiple, by exact issuer common name, default: `null` |
**Example**
Get certificate
```js
const order = { ... }; // Previously created order
const certificate = await client.getCertificate(order);
```
**Example**
Get certificate with preferred chain
```js
const order = { ... }; // Previously created order
const certificate = await client.getCertificate(order, 'DST Root CA X3');
```
<a name="AcmeClient+revokeCertificate"></a>
### acmeClient.revokeCertificate(cert, [data]) ⇒ <code>Promise</code>
Revoke certificate
https://datatracker.ietf.org/doc/html/rfc8555#section-7.6
**Kind**: instance method of [<code>AcmeClient</code>](#AcmeClient)
| Param | Type | Description |
| --- | --- | --- |
| cert | <code>buffer</code> \| <code>string</code> | PEM encoded certificate |
| [data] | <code>object</code> | Additional request data |
**Example**
Revoke certificate
```js
const certificate = { ... }; // Previously created certificate
const result = await client.revokeCertificate(certificate);
```
**Example**
Revoke certificate with reason
```js
const certificate = { ... }; // Previously created certificate
const result = await client.revokeCertificate(certificate, {
reason: 4,
});
```
<a name="AcmeClient+auto"></a>
### acmeClient.auto(opts) ⇒ <code>Promise.&lt;string&gt;</code>
Auto mode
**Kind**: instance method of [<code>AcmeClient</code>](#AcmeClient)
**Returns**: <code>Promise.&lt;string&gt;</code> - Certificate
| Param | Type | Description |
| --- | --- | --- |
| opts | <code>object</code> | |
| opts.csr | <code>buffer</code> \| <code>string</code> | Certificate Signing Request |
| opts.challengeCreateFn | <code>function</code> | Function returning Promise triggered before completing ACME challenge |
| opts.challengeRemoveFn | <code>function</code> | Function returning Promise triggered after completing ACME challenge |
| [opts.email] | <code>string</code> | Account email address |
| [opts.termsOfServiceAgreed] | <code>boolean</code> | Agree to Terms of Service, default: `false` |
| [opts.skipChallengeVerification] | <code>boolean</code> | Skip internal challenge verification before notifying ACME provider, default: `false` |
| [opts.challengePriority] | <code>Array.&lt;string&gt;</code> | Array defining challenge type priority, default: `['http-01', 'dns-01']` |
| [opts.preferredChain] | <code>string</code> | Indicate which certificate chain is preferred if a CA offers multiple, by exact issuer common name, default: `null` |
**Example**
Order a certificate using auto mode
```js
const [certificateKey, certificateRequest] = await acme.crypto.createCsr({
altNames: ['test.example.com'],
});
const certificate = await client.auto({
csr: certificateRequest,
email: 'test@example.com',
termsOfServiceAgreed: true,
challengeCreateFn: async (authz, challenge, keyAuthorization) => {
// Satisfy challenge here
},
challengeRemoveFn: async (authz, challenge, keyAuthorization) => {
// Clean up challenge here
},
});
```
**Example**
Order a certificate using auto mode with preferred chain
```js
const [certificateKey, certificateRequest] = await acme.crypto.createCsr({
altNames: ['test.example.com'],
});
const certificate = await client.auto({
csr: certificateRequest,
email: 'test@example.com',
termsOfServiceAgreed: true,
preferredChain: 'DST Root CA X3',
challengeCreateFn: async () => {},
challengeRemoveFn: async () => {},
});
```
<a name="Client"></a>
## Client : <code>object</code>
ACME client
**Kind**: global namespace
+4 -5
View File
@@ -3,7 +3,7 @@
"description": "Simple and unopinionated ACME client",
"private": false,
"author": "nmorsman",
"version": "1.41.4",
"version": "1.42.6",
"type": "module",
"module": "./dist/index.js",
"main": "./dist/index.js",
@@ -18,7 +18,7 @@
"types"
],
"dependencies": {
"@certd/basic": "^1.41.4",
"@certd/basic": "^1.42.6",
"@peculiar/x509": "^1.11.0",
"asn1js": "^3.0.5",
"axios": "^1.9.0",
@@ -50,10 +50,9 @@
"scripts": {
"before-build": "node -e \"const fs=require('fs');fs.rmSync('dist',{recursive:true,force:true});fs.rmSync('tsconfig.tsbuildinfo',{force:true});\"",
"build": "npm run before-build && tsc -p tsconfig.build.json --skipLibCheck",
"build-docs": "jsdoc2md dist/client.js > docs/client.md && jsdoc2md dist/crypto/index.js > docs/crypto.md && jsdoc2md dist/crypto/forge.js > docs/forge.md",
"lint": "eslint \"src/**/*.ts\" \"types/**/*.ts\"",
"lint-types": "tsd --files \"types/index.test-d.ts\"",
"prepublishOnly": "npm run build && npm run build-docs",
"prepublishOnly": "npm run build",
"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": "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\"",
@@ -76,5 +75,5 @@
"bugs": {
"url": "https://github.com/publishlab/node-acme-client/issues"
},
"gitHead": "bc731e4fb119787930e816a7d57c808b1b5cd66a"
"gitHead": "246ee83015bf5589adc2a5fa3d1388c8d9a2a252"
}
+28
View File
@@ -3,6 +3,34 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.42.6](https://github.com/certd/certd/compare/v1.42.5...v1.42.6) (2026-07-18)
**Note:** Version bump only for package @certd/basic
## [1.42.5](https://github.com/certd/certd/compare/v1.42.4...v1.42.5) (2026-07-15)
**Note:** Version bump only for package @certd/basic
## [1.42.4](https://github.com/certd/certd/compare/v1.42.3...v1.42.4) (2026-07-11)
**Note:** Version bump only for package @certd/basic
## [1.42.3](https://github.com/certd/certd/compare/v1.42.2...v1.42.3) (2026-07-08)
**Note:** Version bump only for package @certd/basic
## [1.42.2](https://github.com/certd/certd/compare/v1.42.1...v1.42.2) (2026-07-07)
**Note:** Version bump only for package @certd/basic
## [1.42.1](https://github.com/certd/certd/compare/v1.42.0...v1.42.1) (2026-07-06)
**Note:** Version bump only for package @certd/basic
# [1.42.0](https://github.com/certd/certd/compare/v1.41.4...v1.42.0) (2026-07-05)
**Note:** Version bump only for package @certd/basic
## [1.41.4](https://github.com/certd/certd/compare/v1.41.3...v1.41.4) (2026-06-14)
### Bug Fixes
+1 -1
View File
@@ -1 +1 @@
21:25
01:12
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/basic",
"private": false,
"version": "1.41.4",
"version": "1.42.6",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
@@ -54,5 +54,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "bc731e4fb119787930e816a7d57c808b1b5cd66a"
"gitHead": "246ee83015bf5589adc2a5fa3d1388c8d9a2a252"
}
+40
View File
@@ -3,6 +3,46 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.42.6](https://github.com/certd/certd/compare/v1.42.5...v1.42.6) (2026-07-18)
### Performance Improvements
* 优化动态加载依赖镜像地址,多次重试 ([5f53b81](https://github.com/certd/certd/commit/5f53b81c75dd242b4260ac08cae14c6d1a08a883))
## [1.42.5](https://github.com/certd/certd/compare/v1.42.4...v1.42.5) (2026-07-15)
### Bug Fixes
* 修复上传到cos报runtimeDepsService未初始化的问题 ([167b303](https://github.com/certd/certd/commit/167b303faeca02cc11cf97e4be2a3df914852167))
### Performance Improvements
* 优化vke keubconfig获取方式,改成先查询,如果没有再创建临时config ([604fa5b](https://github.com/certd/certd/commit/604fa5be634d099d797bfee5c2b0f26ce0ac8461))
## [1.42.4](https://github.com/certd/certd/compare/v1.42.3...v1.42.4) (2026-07-11)
### Bug Fixes
* **pipeline:** 重构运行时依赖加载逻辑,修复火山引擎DNS解析报runtimeDepsService未初始化的bug ([ec69b8f](https://github.com/certd/certd/commit/ec69b8f11bfd4b20991aef74a72a47182ca79a9d))
## [1.42.3](https://github.com/certd/certd/compare/v1.42.2...v1.42.3) (2026-07-08)
**Note:** Version bump only for package @certd/pipeline
## [1.42.2](https://github.com/certd/certd/compare/v1.42.1...v1.42.2) (2026-07-07)
**Note:** Version bump only for package @certd/pipeline
## [1.42.1](https://github.com/certd/certd/compare/v1.42.0...v1.42.1) (2026-07-06)
**Note:** Version bump only for package @certd/pipeline
# [1.42.0](https://github.com/certd/certd/compare/v1.41.4...v1.42.0) (2026-07-05)
### Features
* 通过插件配置懒加载依赖,动态加载第三方依赖包,精简安装镜像大小 ([01568ca](https://github.com/certd/certd/commit/01568ca1489069046b5a89ebdd4ced2f7f6ddf93))
## [1.41.4](https://github.com/certd/certd/compare/v1.41.3...v1.41.4) (2026-06-14)
**Note:** Version bump only for package @certd/pipeline
+4 -4
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/pipeline",
"private": false,
"version": "1.41.4",
"version": "1.42.6",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
@@ -21,8 +21,8 @@
"lint": "eslint --fix"
},
"dependencies": {
"@certd/basic": "^1.41.4",
"@certd/plus-core": "^1.41.4",
"@certd/basic": "^1.42.6",
"@certd/plus-core": "^1.42.6",
"dayjs": "^1.11.7",
"lodash-es": "^4.17.21",
"reflect-metadata": "^0.2.2"
@@ -51,5 +51,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "bc731e4fb119787930e816a7d57c808b1b5cd66a"
"gitHead": "246ee83015bf5589adc2a5fa3d1388c8d9a2a252"
}
+2 -12
View File
@@ -3,7 +3,7 @@ import { FormItemProps } from "../dt/index.js";
import { HttpClient, ILogger, utils } from "@certd/basic";
import * as _ from "lodash-es";
import { PluginRequestHandleReq } from "../plugin/index.js";
import { IRuntimeDepsService, IServiceGetter } from "../service/index.js";
import { IServiceGetter, getRuntimeDepsService } from "../service/index.js";
// export type AccessRequestHandleReqInput<T = any> = {
// id?: number;
@@ -48,23 +48,13 @@ export type AccessContext = {
export abstract class BaseAccess implements IAccess {
ctx!: AccessContext;
runtimeDepsService?: IRuntimeDepsService;
async importRuntime(specifier: string) {
if (!this.runtimeDepsService) {
throw new Error("runtimeDepsService 未初始化");
}
return await this.runtimeDepsService.importRuntime(specifier, this.ctx.logger);
return await getRuntimeDepsService().importRuntime(specifier, this.ctx.logger);
}
async setCtx(ctx: AccessContext) {
this.ctx = ctx;
if (!this.runtimeDepsService && this.ctx.serviceGetter) {
this.runtimeDepsService = await this.ctx.serviceGetter.get("runtimeDepsService");
}
if (this.runtimeDepsService && this.ctx.define?.name) {
await this.runtimeDepsService.ensureRuntimeDependencies({ pluginKeys: `access:${this.ctx.define.name}`, logger: this.ctx.logger });
}
}
async onRequest(req: AccessRequestHandleReq) {
@@ -47,7 +47,7 @@ export function AccessInput(input?: AccessInputDefine): PropertyDecorator {
};
}
export async function newAccess(type: string, input: any, accessService: IAccessService, ctx?: AccessContext) {
export async function newAccess(type: string, input: any, accessService: IAccessService, ctx: AccessContext) {
const register = accessRegistry.get(type);
if (register == null) {
throw new Error(`access ${type} not found`);
+6 -12
View File
@@ -3,7 +3,7 @@ import { Registrable } from "../registry/index.js";
import { FormItemProps, HistoryResult, Pipeline } from "../dt/index.js";
import { HttpClient, ILogger, utils } from "@certd/basic";
import * as _ from "lodash-es";
import { IEmailService, IRuntimeDepsService, IServiceGetter } from "../service/index.js";
import { IEmailService, IServiceGetter, getRuntimeDepsService } from "../service/index.js";
export type NotificationBody = {
userId?: number;
@@ -89,16 +89,16 @@ export abstract class BaseNotification implements INotification {
ctx!: NotificationContext;
http!: HttpClient;
logger!: ILogger;
runtimeDepsService?: IRuntimeDepsService;
async importRuntime(specifier: string) {
if (!this.runtimeDepsService) {
return await import(specifier);
}
return await this.runtimeDepsService.importRuntime(specifier, this.logger);
return await getRuntimeDepsService().importRuntime(specifier, this.logger);
}
async doSend(body: NotificationBody) {
if (body.content) {
const content = body.content?.replace(/\n/g, " \n");
body.content = content;
}
return await this.send(body);
}
abstract send(body: NotificationBody): Promise<void>;
@@ -109,12 +109,6 @@ export abstract class BaseNotification implements INotification {
this.ctx = ctx;
this.http = ctx.http;
this.logger = ctx.logger;
if (!this.runtimeDepsService && this.ctx.serviceGetter) {
this.runtimeDepsService = await this.ctx.serviceGetter.get("runtimeDepsService");
}
if (this.runtimeDepsService && this.ctx.define?.name) {
await this.runtimeDepsService.ensureRuntimeDependencies({ pluginKeys: `notification:${this.ctx.define.name}`, logger: this.logger });
}
}
setDefine = (define: NotificationDefine) => {
this.define = define;
+3 -13
View File
@@ -10,7 +10,7 @@ import { INotificationService } from "../notification/index.js";
import { Registrable } from "../registry/index.js";
import { IPluginConfigService } from "../service/config.js";
import { TaskEmitter } from "../service/emit.js";
import { ICnameProxyService, IEmailService, IRuntimeDepsService, IServiceGetter, IUrlService } from "../service/index.js";
import { ICnameProxyService, IEmailService, IServiceGetter, IUrlService, getRuntimeDepsService } from "../service/index.js";
export type PluginRequestHandleReq<T = any> = {
typeName: string;
@@ -76,7 +76,7 @@ export type ITaskPlugin = {
execute(): Promise<void | string>;
onRequest(req: PluginRequestHandleReq<any>): Promise<any>;
setCtx(ctx: TaskInstanceContext): Promise<void>;
importRuntime?(specifier: string): Promise<any>;
importRuntime(specifier: string): Promise<any>;
[key: string]: any;
};
@@ -150,13 +150,9 @@ export abstract class AbstractTaskPlugin implements ITaskPlugin {
logger!: ILogger;
http!: HttpClient;
accessService!: IAccessService;
runtimeDepsService!: IRuntimeDepsService;
async importRuntime(specifier: string) {
if (!this.runtimeDepsService) {
throw new Error("runtimeDepsService 未初始化");
}
return await this.runtimeDepsService.importRuntime(specifier, this.logger);
return await getRuntimeDepsService().importRuntime(specifier, this.logger);
}
clearLastStatus() {
@@ -178,12 +174,6 @@ export abstract class AbstractTaskPlugin implements ITaskPlugin {
this.logger = ctx.logger;
this.accessService = ctx.accessService;
this.http = ctx.http;
if (!this.runtimeDepsService && this.ctx.serviceGetter) {
this.runtimeDepsService = await this.ctx.serviceGetter.get("runtimeDepsService");
}
if (this.runtimeDepsService && this.ctx.define?.name) {
await this.runtimeDepsService.ensureRuntimeDependencies({ pluginKeys: `plugin:${this.ctx.define.name}`, logger: this.logger });
}
// 将证书加入secret
// @ts-ignore
if (this.cert && this.cert.crt && this.cert.key) {
@@ -2,41 +2,41 @@ import assert from "assert";
import fs from "fs";
import path from "path";
import os from "os";
import { RuntimeDepsService, type RuntimeDependencyPluginDefine } from "./runtime-deps-service.js";
import { accessRegistry, pluginRegistry } from "@certd/pipeline";
import { addonRegistry } from "@certd/lib-server";
import { RuntimeDepsService, NpmRegistryResolver, type RuntimeDependencyPluginDefine } from "./runtime.js";
import { accessRegistry } from "../access/registry.js";
import { pluginRegistry } from "../plugin/registry.js";
describe("RuntimeDepsService", () => {
it("detects conflicting dependency ranges across plugins", () => {
const service = new RuntimeDepsService();
const service = new RuntimeDepsService({}, null);
const merged = service.collectDependencies([
{ name: "a", dependPackages: { foo: "^1.0.0" } },
{ name: "b", dependPackages: { foo: "^1.2.0" } },
]);
assert.deepEqual(merged.dependencies, { foo: "^1.0.0" });
assert.equal(merged.conflicts.length, 0);
});
it("reports incompatible dependency ranges", () => {
const service = new RuntimeDepsService();
const service = new RuntimeDepsService({}, null);
const merged = service.collectDependencies([
{ name: "a", dependPackages: { foo: "^1.0.0" } },
{ name: "b", dependPackages: { foo: "^2.0.0" } },
]);
assert.equal(merged.conflicts.length, 1);
assert.equal(merged.conflicts[0].packageName, "foo");
});
it("builds a runtime package manifest in the target directory", async () => {
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-"));
const service = new RuntimeDepsService();
service.runtimeDepsRootDir = rootDir;
const service = new RuntimeDepsService({ rootDir }, null);
service.registryResolver = {
async resolve() {
return "https://registry.npmmirror.com";
},
async resolveOrdered() {
return ["https://registry.npmmirror.com"];
},
} as any;
service.commandRunner = {
async run(command: string, args: string[]) {
@@ -50,22 +50,22 @@ describe("RuntimeDepsService", () => {
return { stdout: "", stderr: "", code: 0 };
},
} as any;
const plugins: RuntimeDependencyPluginDefine[] = [{ name: "a", dependPackages: { foo: "^1.0.0" } }];
const result = await service.ensureInstalled({ plugins });
assert.equal(result.registryUrl, "https://registry.npmmirror.com");
assert.ok(fs.existsSync(path.join(rootDir, "package.json")));
});
it("installs direct dependency maps without plugin metadata", async () => {
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-direct-"));
const service = new RuntimeDepsService();
service.runtimeDepsRootDir = rootDir;
const service = new RuntimeDepsService({ rootDir }, null);
service.registryResolver = {
async resolve() {
return "";
},
async resolveOrdered() {
return [""];
},
} as any;
service.commandRunner = {
async run(command: string, args: string[]) {
@@ -77,9 +77,7 @@ describe("RuntimeDepsService", () => {
return { stdout: "", stderr: "", code: 0 };
},
} as any;
await service.ensureDependencies({ dependencies: { directPkg: "^1.0.0" } });
const manifest = JSON.parse(fs.readFileSync(path.join(rootDir, "package.json"), "utf8"));
assert.deepEqual(manifest.dependencies, { directPkg: "^1.0.0" });
});
@@ -91,31 +89,26 @@ describe("RuntimeDepsService", () => {
fs.writeFileSync(path.join(rootDir, "package.json"), JSON.stringify({ name: "runtime-root", type: "module" }), "utf8");
fs.writeFileSync(path.join(packageDir, "package.json"), JSON.stringify({ name: "runtime-only", type: "module", main: "index.js" }), "utf8");
fs.writeFileSync(path.join(packageDir, "index.js"), "export const value = 42;\n", "utf8");
const service = new RuntimeDepsService();
service.runtimeDepsRootDir = rootDir;
const service = new RuntimeDepsService({ rootDir }, null);
service.commandRunner = {
async run() {
throw new Error("install should not run");
},
} as any;
const mod = await service.importRuntime("runtime-only");
assert.equal(mod.value, 42);
});
it("installs configured lazy dependency when import target is missing", async () => {
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-lazy-"));
const service = new RuntimeDepsService();
service.runtimeDepsRootDir = rootDir;
service.lazyDependencies = {
"lazy-pkg": "^1.2.3",
};
const service = new RuntimeDepsService({ rootDir, lazyDependencies: { "lazy-pkg": "^1.2.3" } }, null);
service.registryResolver = {
async resolve() {
return "";
},
async resolveOrdered() {
return [""];
},
} as any;
service.commandRunner = {
async run(command: string, args: string[]) {
@@ -130,9 +123,7 @@ describe("RuntimeDepsService", () => {
return { stdout: "", stderr: "", code: 0 };
},
} as any;
const mod = await service.importRuntime("lazy-pkg/sub/entry.js");
const manifest = JSON.parse(fs.readFileSync(path.join(rootDir, "package.json"), "utf8"));
assert.deepEqual(manifest.dependencies, { "lazy-pkg": "^1.2.3" });
assert.equal(mod.value, 7);
@@ -140,15 +131,14 @@ describe("RuntimeDepsService", () => {
it("resolves scoped package names for lazy imports", async () => {
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-scoped-"));
const service = new RuntimeDepsService();
service.runtimeDepsRootDir = rootDir;
service.lazyDependencies = {
"@scope/lazy": "^2.0.0",
};
const service = new RuntimeDepsService({ rootDir, lazyDependencies: { "@scope/lazy": "^2.0.0" } }, null);
service.registryResolver = {
async resolve() {
return "";
},
async resolveOrdered() {
return [""];
},
} as any;
service.commandRunner = {
async run(command: string, args: string[]) {
@@ -163,9 +153,7 @@ describe("RuntimeDepsService", () => {
return { stdout: "", stderr: "", code: 0 };
},
} as any;
const mod = await service.importRuntime("@scope/lazy/dist/index.js");
const manifest = JSON.parse(fs.readFileSync(path.join(rootDir, "package.json"), "utf8"));
assert.deepEqual(manifest.dependencies, { "@scope/lazy": "^2.0.0" });
assert.equal(mod.scoped, true);
@@ -173,59 +161,27 @@ describe("RuntimeDepsService", () => {
it("reports missing lazy dependency configuration", async () => {
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-lazy-missing-"));
const service = new RuntimeDepsService();
service.runtimeDepsRootDir = rootDir;
service.lazyDependencies = {};
const service = new RuntimeDepsService({ rootDir, lazyDependencies: {} }, null);
await assert.rejects(() => service.importRuntime("missing-pkg/sub.js"), /未配置懒加载版本: missing-pkg/);
});
it("falls back to project node_modules when lazy dependency is not configured", async () => {
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-project-fallback-"));
const service = new RuntimeDepsService();
service.runtimeDepsRootDir = rootDir;
service.lazyDependencies = {};
const service = new RuntimeDepsService({ rootDir, lazyDependencies: {} }, null);
const mod = await service.importRuntime("dayjs");
assert.equal(typeof mod.default, "function");
});
it("falls back to project node_modules when lazy install fails", async () => {
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-project-fallback-install-"));
const service = new RuntimeDepsService();
service.runtimeDepsRootDir = rootDir;
service.lazyDependencies = {
dayjs: "^1.11.7",
};
service.registryResolver = {
async resolve() {
return "";
},
} as any;
service.commandRunner = {
async run(command: string, args: string[]) {
assert.equal(command, "pnpm");
if (args.includes("--version")) {
return { stdout: "9.1.0\n", stderr: "", code: 0 };
}
return { stdout: "", stderr: "install failed in test", code: 1 };
},
} as any;
const mod = await service.importRuntime("dayjs");
assert.equal(typeof mod.default, "function");
});
it("keeps previously installed dependencies when installing a later plugin", async () => {
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-merge-"));
const service = new RuntimeDepsService();
service.runtimeDepsRootDir = rootDir;
const service = new RuntimeDepsService({ rootDir }, null);
service.registryResolver = {
async resolve() {
return "";
},
async resolveOrdered() {
return [""];
},
} as any;
service.commandRunner = {
async run(command: string, args: string[]) {
@@ -237,22 +193,17 @@ describe("RuntimeDepsService", () => {
return { stdout: "", stderr: "", code: 0 };
},
} as any;
await service.ensureInstalled({ plugins: [{ name: "a", pluginType: "deploy", dependPackages: { foo: "^1.0.0" } }] });
await service.ensureInstalled({ plugins: [{ name: "b", pluginType: "deploy", dependPackages: { bar: "^2.0.0" } }] });
const manifest = JSON.parse(fs.readFileSync(path.join(rootDir, "package.json"), "utf8"));
assert.deepEqual(manifest.dependencies, {
foo: "^1.0.0",
bar: "^2.0.0",
});
assert.deepEqual(manifest.dependencies, { foo: "^1.0.0", bar: "^2.0.0" });
});
it("includes npm dependencies from dependent plugins", () => {
const service = new RuntimeDepsService();
it("includes npm dependencies from dependent plugins", async () => {
const service = new RuntimeDepsService({}, { accessRegistry, pluginRegistry });
accessRegistry.register("runtimeDepsAccess", {
define: { name: "runtimeDepsAccess", title: "access", dependPackages: { accessOnly: "^1.0.0" } } as any,
target: async () => ({} as any),
target: async () => ({}) as any,
});
try {
const resolved = service.resolvePluginDependencies({
@@ -262,11 +213,7 @@ describe("RuntimeDepsService", () => {
dependPackages: { deployOnly: "^1.0.0" },
});
const merged = service.collectDependencies(resolved);
assert.deepEqual(merged.dependencies, {
deployOnly: "^1.0.0",
accessOnly: "^1.0.0",
});
assert.deepEqual(merged.dependencies, { deployOnly: "^1.0.0", accessOnly: "^1.0.0" });
} finally {
accessRegistry.unRegister("runtimeDepsAccess");
}
@@ -274,12 +221,14 @@ describe("RuntimeDepsService", () => {
it("installs dependencies by registered plugin key", async () => {
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-key-"));
const service = new RuntimeDepsService();
service.runtimeDepsRootDir = rootDir;
const service = new RuntimeDepsService({ rootDir }, { pluginRegistry, accessRegistry });
service.registryResolver = {
async resolve() {
return "";
},
async resolveOrdered() {
return [""];
},
} as any;
service.commandRunner = {
async run(command: string, args: string[]) {
@@ -293,11 +242,11 @@ describe("RuntimeDepsService", () => {
} as any;
pluginRegistry.register("runtimeDepsKey", {
define: { name: "runtimeDepsKey", title: "key", dependPackages: { keyed: "^1.0.0" } } as any,
target: async () => ({} as any),
target: async () => ({}) as any,
});
try {
service.setRegistries({ pluginRegistry, accessRegistry });
await service.ensureRuntimeDependencies({ pluginKeys: "plugin:runtimeDepsKey" });
const manifest = JSON.parse(fs.readFileSync(path.join(rootDir, "package.json"), "utf8"));
assert.deepEqual(manifest.dependencies, { keyed: "^1.0.0" });
} finally {
@@ -305,58 +254,16 @@ describe("RuntimeDepsService", () => {
}
});
it("installs dependencies from multiple plugin keys including addon subtype keys", async () => {
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-keys-"));
const service = new RuntimeDepsService();
service.runtimeDepsRootDir = rootDir;
service.registryResolver = {
async resolve() {
return "";
},
} as any;
service.commandRunner = {
async run(command: string, args: string[]) {
assert.equal(command, "pnpm");
if (args.includes("--version")) {
return { stdout: "9.1.0\n", stderr: "", code: 0 };
}
fs.mkdirSync(path.join(rootDir, "node_modules"), { recursive: true });
return { stdout: "", stderr: "", code: 0 };
},
} as any;
accessRegistry.register("runtimeDepsArrayAccess", {
define: { name: "runtimeDepsArrayAccess", title: "access", dependPackages: { accessPkg: "^1.0.0" } } as any,
target: async () => ({} as any),
});
addonRegistry.register("captcha:runtimeDepsArrayAddon", {
define: { addonType: "captcha", name: "runtimeDepsArrayAddon", title: "addon", dependPackages: { addonPkg: "^2.0.0" } } as any,
target: async () => ({} as any),
});
try {
await service.ensureRuntimeDependencies({ pluginKeys: ["access:runtimeDepsArrayAccess", "addon:captcha:runtimeDepsArrayAddon"] });
const manifest = JSON.parse(fs.readFileSync(path.join(rootDir, "package.json"), "utf8"));
assert.deepEqual(manifest.dependencies, {
accessPkg: "^1.0.0",
addonPkg: "^2.0.0",
});
} finally {
accessRegistry.unRegister("runtimeDepsArrayAccess");
addonRegistry.unRegister("captcha:runtimeDepsArrayAddon");
}
});
it("reports missing dependent plugins", () => {
const service = new RuntimeDepsService();
const service = new RuntimeDepsService({}, { accessRegistry, pluginRegistry });
assert.throws(() => service.resolvePluginDependencies({ name: "deploy", pluginType: "deploy", dependPlugins: { "access:access": "*" } }), /插件依赖缺失/);
});
it("reports incompatible dependent plugin versions", () => {
const service = new RuntimeDepsService();
const service = new RuntimeDepsService({}, { accessRegistry, pluginRegistry });
accessRegistry.register("runtimeDepsVersionedAccess", {
define: { name: "runtimeDepsVersionedAccess", title: "access", version: "1.4.0", dependPackages: { accessOnly: "^1.0.0" } } as any,
target: async () => ({} as any),
target: async () => ({}) as any,
});
try {
assert.throws(
@@ -374,73 +281,10 @@ describe("RuntimeDepsService", () => {
});
it("reports bare dependent plugin names as invalid format", () => {
const service = new RuntimeDepsService();
const service = new RuntimeDepsService({}, null);
assert.throws(() => service.resolvePluginDependencies({ name: "deploy", pluginType: "deploy", dependPlugins: { runtimeDepsBareName: "*" } }), /插件依赖格式错误/);
});
it("records runtime install environment state", async () => {
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-state-"));
const service = new RuntimeDepsService();
service.runtimeDepsRootDir = rootDir;
service.registryResolver = {
async resolve() {
return "";
},
} as any;
service.commandRunner = {
async run(command: string, args: string[]) {
assert.equal(command, "pnpm");
if (args.includes("--version")) {
return { stdout: "9.1.0\n", stderr: "", code: 0 };
}
assert.equal(args[0], "install");
return { stdout: "", stderr: "", code: 0 };
},
} as any;
await service.ensureInstalled({ plugins: [{ name: "a", dependPackages: { foo: "^1.0.0" } }] });
const state = JSON.parse(fs.readFileSync(path.join(rootDir, "install-state.json"), "utf8"));
assert.equal(state.nodeVersion, process.version);
assert.equal(state.pnpmVersion, "9.1.0");
assert.equal(state.lastError, undefined);
});
it("serializes installs with a file lock", async () => {
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-lock-"));
const serviceA = new RuntimeDepsService();
const serviceB = new RuntimeDepsService();
for (const service of [serviceA, serviceB]) {
service.runtimeDepsRootDir = rootDir;
service.registryResolver = {
async resolve() {
return "";
},
} as any;
}
let installCount = 0;
const commandRunner = {
async run(command: string, args: string[]) {
assert.equal(command, "pnpm");
if (args.includes("--version")) {
return { stdout: "9.1.0\n", stderr: "", code: 0 };
}
assert.equal(args[0], "install");
installCount++;
await new Promise(resolve => setTimeout(resolve, 50));
fs.mkdirSync(path.join(rootDir, "node_modules"), { recursive: true });
return { stdout: "", stderr: "", code: 0 };
},
};
serviceA.commandRunner = commandRunner as any;
serviceB.commandRunner = commandRunner as any;
await Promise.all([serviceA.ensureInstalled({ plugins: [{ name: "a", dependPackages: { foo: "^1.0.0" } }] }), serviceB.ensureInstalled({ plugins: [{ name: "a", dependPackages: { foo: "^1.0.0" } }] })]);
assert.equal(installCount, 1);
});
it("does not pass node debugger options to pnpm child process", async () => {
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-env-"));
const oldNodeOptions = process.env.NODE_OPTIONS;
@@ -448,12 +292,14 @@ describe("RuntimeDepsService", () => {
process.env.NODE_OPTIONS = "--inspect=127.0.0.1:9229 --max-old-space-size=4096";
process.env.VSCODE_INSPECTOR_OPTIONS = '{"inspectorIpc":"test"}';
try {
const service = new RuntimeDepsService();
service.runtimeDepsRootDir = rootDir;
const service = new RuntimeDepsService({ rootDir }, null);
service.registryResolver = {
async resolve() {
return "";
},
async resolveOrdered() {
return [""];
},
} as any;
service.commandRunner = {
async run(command: string, args: string[], options: { env?: NodeJS.ProcessEnv }) {
@@ -467,7 +313,6 @@ describe("RuntimeDepsService", () => {
return { stdout: "", stderr: "", code: 0 };
},
} as any;
await service.ensureInstalled({ plugins: [{ name: "a", dependPackages: { foo: "^1.0.0" } }] });
} finally {
if (oldNodeOptions == null) {
@@ -483,26 +328,94 @@ describe("RuntimeDepsService", () => {
}
});
it("clears runtime dependency directory", async () => {
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-clear-"));
const runtimeRootDir = path.join(rootDir, ".runtime-deps");
fs.mkdirSync(path.join(runtimeRootDir, "node_modules", "foo"), { recursive: true });
fs.writeFileSync(path.join(runtimeRootDir, "package.json"), "{}", "utf8");
const service = new RuntimeDepsService();
service.runtimeDepsRootDir = runtimeRootDir;
service.installTimeoutMs = 1000;
await service.clearRuntimeDeps();
assert.equal(fs.existsSync(runtimeRootDir), true);
assert.equal(fs.readdirSync(runtimeRootDir).length, 0);
});
it("rejects clearing unexpected runtime dependency path", async () => {
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-clear-invalid-"));
const service = new RuntimeDepsService();
service.runtimeDepsRootDir = rootDir;
const service = new RuntimeDepsService({ rootDir }, null);
await assert.rejects(() => service.clearRuntimeDeps(), /动态依赖目录配置异常/);
});
});
describe("NpmRegistryResolver", () => {
it("returns the fastest successful registry via resolve()", async () => {
const resolver = new NpmRegistryResolver({
mode: "auto",
candidates: ["https://slow.example.com", "https://fast.example.com"],
probeTimeoutMs: 100,
cacheTtlMs: 1000,
});
resolver.probe = async (registryUrl: string) => ({
registryUrl,
ok: true,
elapsedMs: registryUrl.includes("fast") ? 10 : 50,
});
const result = await resolver.resolve();
assert.equal(result, "https://fast.example.com");
});
it("uses fixed registry without probing", async () => {
const resolver = new NpmRegistryResolver({
mode: "fixed",
fixedUrl: "https://registry.example.com",
probeTimeoutMs: 100,
cacheTtlMs: 1000,
});
const result = await resolver.resolve();
assert.equal(result, "https://registry.example.com");
});
it("returns ordered list via resolveOrdered (fastest first)", async () => {
const resolver = new NpmRegistryResolver({
mode: "auto",
candidates: ["https://slow.example.com", "https://fast.example.com"],
probeTimeoutMs: 100,
cacheTtlMs: 1000,
});
resolver.probe = async (registryUrl: string) => ({
registryUrl,
ok: true,
elapsedMs: registryUrl.includes("fast") ? 10 : 50,
});
const result = await resolver.resolveOrdered();
assert.deepEqual(result, ["https://fast.example.com", "https://slow.example.com"]);
});
it("includes failed registries at the end of resolveOrdered", async () => {
const resolver = new NpmRegistryResolver({
mode: "auto",
candidates: ["https://good.example.com", "https://bad.example.com"],
probeTimeoutMs: 100,
cacheTtlMs: 1000,
});
resolver.probe = async (registryUrl: string) => {
if (registryUrl.includes("bad")) {
return { registryUrl, ok: false, elapsedMs: 200 };
}
return { registryUrl, ok: true, elapsedMs: 30 };
};
const result = await resolver.resolveOrdered();
assert.deepEqual(result, ["https://good.example.com", "https://bad.example.com"]);
});
it("returns empty ordered list when no candidates", async () => {
const resolver = new NpmRegistryResolver({ mode: "auto", candidates: [] });
const result = await resolver.resolveOrdered();
assert.deepEqual(result, []);
const single = await resolver.resolve();
assert.equal(single, "");
});
it("re-validates cached URL on resolveOrdered call", async () => {
let probeCount = 0;
const resolver = new NpmRegistryResolver({
mode: "auto",
candidates: ["https://mirror.example.com"],
cacheTtlMs: 60000,
});
resolver.probe = async (registryUrl: string) => {
probeCount++;
return { registryUrl, ok: true, elapsedMs: 10 };
};
const first = await resolver.resolveOrdered();
assert.deepEqual(first, ["https://mirror.example.com"]);
assert.equal(probeCount, 1);
const second = await resolver.resolveOrdered();
assert.deepEqual(second, ["https://mirror.example.com"]);
assert.equal(probeCount, 2);
});
});
+814 -14
View File
@@ -1,27 +1,827 @@
/**
*
*/
export type ImportRuntime = (specifier: string, logger?: ILogger) => Promise<any>;
/**
*
*/
import fs from "fs";
import path from "path";
import { spawn } from "child_process";
import crypto from "crypto";
import { createRequire } from "module";
import { pathToFileURL } from "url";
import { logger as defaultLogger } from "@certd/basic";
import type { Registry } from "../registry/registry.js";
export type ILogger = {
info: (message: string) => void;
warn?: (message: string) => void;
error?: (message: string, ...args: any[]) => void;
};
/**
*
*/
export type ImportRuntime = (specifier: string, logger?: ILogger) => Promise<any>;
export type EnsureRuntimeDepsOptions = {
pluginKeys: string | string[];
logger?: ILogger;
};
/**
*
*/
export interface IRuntimeDepsService {
ensureRuntimeDependencies(options: EnsureRuntimeDepsOptions): Promise<any>;
importRuntime: ImportRuntime;
}
export type RuntimeDependencyPluginDefine = {
name: string;
key?: string;
title?: string;
version?: string;
pluginType?: string;
addonType?: string;
dependPlugins?: Record<string, string>;
dependPackages?: Record<string, string>;
};
type RegisteredDefineLike = RuntimeDependencyPluginDefine & {
key?: string;
pluginType?: string;
addonType?: string;
dependPlugins?: Record<string, string>;
dependPackages?: Record<string, string>;
};
type DependencyConflict = {
packageName: string;
ranges: Array<{ pluginName: string; range: string }>;
};
type CollectDependenciesResult = {
dependencies: Record<string, string>;
conflicts: DependencyConflict[];
};
type InstallResult = {
registryUrl: string;
packageJsonPath: string;
};
type RuntimeImportResolveResult = {
resolved: string;
packageName: string;
};
type CommandRunnerResult = {
stdout: string;
stderr: string;
code: number;
};
type CommandRunner = {
run(command: string, args: string[], options: { cwd: string; timeoutMs: number; env?: NodeJS.ProcessEnv }): Promise<CommandRunnerResult>;
};
export type NpmRegistryResolverConfig = {
mode?: "auto" | "fixed" | "system";
fixedUrl?: string;
candidates?: string[];
probeTimeoutMs?: number;
cacheTtlMs?: number;
};
export type RegistryProbeResult = {
registryUrl: string;
ok: boolean;
elapsedMs: number;
};
export class NpmRegistryResolver {
config: NpmRegistryResolverConfig;
private cache?: { orderedUrls: string[]; expiresAt: number };
constructor(config?: NpmRegistryResolverConfig) {
this.config = config || {};
}
async resolve(): Promise<string> {
const config = this.config;
if (config?.mode === "fixed" && config.fixedUrl) {
return config.fixedUrl;
}
if (config?.mode === "system") {
return "";
}
const cached = this.cache;
if (cached && cached.expiresAt > Date.now()) {
const fastUrl = cached.orderedUrls[0];
if (fastUrl) {
const probeResult = await this.probe(fastUrl);
if (probeResult.ok) {
return cached.orderedUrls[0] || "";
}
}
this.cache = undefined;
}
const candidates = (config?.candidates || []).filter(Boolean);
if (candidates.length === 0) {
return "";
}
const orderedUrls = await this.internalProbeAll(candidates);
this.cache = { orderedUrls, expiresAt: Date.now() + (config?.cacheTtlMs ?? 300_000) };
return orderedUrls[0] || "";
}
async resolveOrdered(): Promise<string[]> {
const config = this.config;
if (config?.mode === "fixed" && config.fixedUrl) {
return [config.fixedUrl];
}
if (config?.mode === "system") {
return [];
}
const cached = this.cache;
if (cached && cached.expiresAt > Date.now()) {
const fastUrl = cached.orderedUrls[0];
if (fastUrl) {
const probeResult = await this.probe(fastUrl);
if (probeResult.ok) {
return cached.orderedUrls;
}
}
this.cache = undefined;
}
const candidates = (config?.candidates || []).filter(Boolean);
if (candidates.length === 0) {
return [];
}
const orderedUrls = await this.internalProbeAll(candidates);
this.cache = { orderedUrls, expiresAt: Date.now() + (config?.cacheTtlMs ?? 300_000) };
return orderedUrls;
}
private async internalProbeAll(candidates: string[]): Promise<string[]> {
const probes = await Promise.allSettled(candidates.map(registryUrl => this.probe(registryUrl)));
const okList: RegistryProbeResult[] = [];
const failList: RegistryProbeResult[] = [];
for (const item of probes) {
const result = item.status === "fulfilled" ? item.value : null;
if (result && result.ok) {
okList.push(result);
} else if (result) {
failList.push(result);
}
}
okList.sort((a, b) => a.elapsedMs - b.elapsedMs);
failList.sort((a, b) => a.elapsedMs - b.elapsedMs);
return [...okList.map(r => r.registryUrl), ...failList.map(r => r.registryUrl)];
}
async probe(registryUrl: string): Promise<RegistryProbeResult> {
const timeoutMs = this.config?.probeTimeoutMs || 3000;
const started = Date.now();
try {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
const res = await fetch(`${registryUrl.replace(/\/$/, "")}/-/ping`, { signal: controller.signal });
return { registryUrl, ok: res.ok, elapsedMs: Date.now() - started };
} finally {
clearTimeout(timer);
}
} catch {
return { registryUrl, ok: false, elapsedMs: Date.now() - started };
}
}
}
export type RuntimeDepsConfig = {
rootDir?: string;
autoInstall?: boolean;
enabled?: boolean;
installTimeoutMs?: number;
pnpmCommand?: string;
lazyDependencies?: Record<string, string>;
registry?: NpmRegistryResolverConfig;
};
function normalizeRange(range: string) {
return range.trim().replace(/^\^/, "").replace(/^~?/, "");
}
function areRangesCompatible(a: string, b: string) {
if (!a || !b) {
return true;
}
if (a === "*" || b === "*") {
return true;
}
const left = normalizeRange(a).split(".");
const right = normalizeRange(b).split(".");
return left[0] === right[0];
}
const PROCESS_LOCKS = new Map<string, Promise<unknown>>();
class DefaultCommandRunner implements CommandRunner {
async run(command: string, args: string[], options: { cwd: string; timeoutMs: number; env?: NodeJS.ProcessEnv }): Promise<CommandRunnerResult> {
return await new Promise<CommandRunnerResult>(resolve => {
let stdout = "";
let stderr = "";
let settled = false;
const child = spawn(command, args, { cwd: options.cwd, env: options.env, windowsHide: true, shell: process.platform === "win32" });
const timer = setTimeout(() => {
if (settled) {
return;
}
settled = true;
child.kill("SIGTERM");
resolve({ stdout, stderr: stderr || `command timeout after ${options.timeoutMs}ms`, code: 1 });
}, options.timeoutMs);
child.stdout?.on("data", chunk => {
stdout += chunk.toString();
});
child.stderr?.on("data", chunk => {
stderr += chunk.toString();
});
child.on("error", error => {
if (settled) {
return;
}
settled = true;
clearTimeout(timer);
resolve({ stdout, stderr: error.message, code: 1 });
});
child.on("close", code => {
if (settled) {
return;
}
settled = true;
clearTimeout(timer);
resolve({ stdout, stderr, code: code || 0 });
});
});
}
}
export class RuntimeDepsService {
runtimeDepsRootDir: string;
autoInstall: boolean;
enabled: boolean;
installTimeoutMs: number;
pnpmCommand: string;
lazyDependencies: Record<string, string>;
registryResolver!: NpmRegistryResolver;
commandRunner: CommandRunner = new DefaultCommandRunner();
pluginLazyDependencies: Record<string, string> = {};
private installPromises = new Map<string, Promise<InstallResult>>();
private registriesMap: Record<string, { registry: Registry<any>; pluginType: string; addonType?: string }> | null = null;
constructor(config: RuntimeDepsConfig, registries: any) {
this.runtimeDepsRootDir = config?.rootDir ?? "./data/.runtime-deps";
this.autoInstall = config?.autoInstall ?? true;
this.enabled = config?.enabled ?? true;
this.installTimeoutMs = config?.installTimeoutMs ?? 120000;
this.pnpmCommand = config?.pnpmCommand ?? "";
this.lazyDependencies = config?.lazyDependencies ?? {};
this.registryResolver = new NpmRegistryResolver(config?.registry);
if (registries) {
this.setRegistries(registries);
}
}
setRegistries(registries: { pluginRegistry?: Registry<any>; accessRegistry?: Registry<any>; notificationRegistry?: Registry<any>; dnsProviderRegistry?: Registry<any>; addonRegistry?: Registry<any> }) {
const map: Record<string, { registry: Registry<any>; pluginType: string; addonType?: string }> = {};
if (registries.pluginRegistry) {
map["plugin"] = { registry: registries.pluginRegistry, pluginType: "plugin" };
}
if (registries.accessRegistry) {
map["access"] = { registry: registries.accessRegistry, pluginType: "access" };
}
if (registries.notificationRegistry) {
map["notification"] = { registry: registries.notificationRegistry, pluginType: "notification" };
}
if (registries.dnsProviderRegistry) {
map["dnsProvider"] = { registry: registries.dnsProviderRegistry, pluginType: "dnsProvider" };
}
if (registries.addonRegistry) {
map["addon"] = { registry: registries.addonRegistry, pluginType: "addon", addonType: "" };
}
this.registriesMap = map;
}
collectDependencies(plugins: RuntimeDependencyPluginDefine[]): CollectDependenciesResult {
const merged: Record<string, string> = {};
const seen: Record<string, Array<{ pluginName: string; range: string }>> = {};
for (const plugin of plugins) {
const deps = plugin.dependPackages || {};
for (const [packageName, range] of Object.entries(deps)) {
seen[packageName] ||= [];
seen[packageName].push({ pluginName: plugin.name, range });
}
}
const conflicts: DependencyConflict[] = [];
for (const [packageName, ranges] of Object.entries(seen)) {
const first = ranges[0]?.range;
if (!first) {
continue;
}
const conflict = ranges.some(item => !areRangesCompatible(first, item.range));
if (conflict) {
conflicts.push({ packageName, ranges });
continue;
}
merged[packageName] = first;
}
return { dependencies: merged, conflicts };
}
async ensureInstalled(options: { plugins: RuntimeDependencyPluginDefine[]; logger?: ILogger }): Promise<InstallResult> {
const { plugins, logger: log } = options;
const { dependencies, conflicts } = this.resolveDependenciesFromPlugins(plugins);
if (conflicts.length > 0) {
const conflict = conflicts[0];
throw new Error(`动态依赖版本冲突: ${conflict.packageName} => ${conflict.ranges.map(item => `${item.pluginName}:${item.range}`).join(", ")}`);
}
return await this.ensureDependencies({ dependencies, logger: log });
}
async ensureDependencies(options: { dependencies: Record<string, string>; logger?: ILogger }): Promise<InstallResult> {
const { dependencies, logger: log } = options;
if (!this.enabled) {
return { registryUrl: "", packageJsonPath: path.join(this.getRuntimeDepsRootDir(), "package.json") };
}
if (!this.autoInstall) {
return { registryUrl: "", packageJsonPath: path.join(this.getRuntimeDepsRootDir(), "package.json") };
}
const dependenciesHash = this.createDependenciesHash(dependencies);
let installPromise = this.installPromises.get(dependenciesHash);
if (installPromise) {
const nodeModulesPath = path.join(this.getRuntimeDepsRootDir(), "node_modules");
if (!fs.existsSync(nodeModulesPath)) {
this.installPromises.delete(dependenciesHash);
installPromise = undefined;
}
}
if (!installPromise) {
installPromise = this.doEnsureInstalled({ dependencies, logger: log }).catch(error => {
this.installPromises.delete(dependenciesHash);
throw error;
});
this.installPromises.set(dependenciesHash, installPromise);
}
return await installPromise;
}
resolveDependenciesFromPlugins(plugins: RuntimeDependencyPluginDefine[]): CollectDependenciesResult {
const expandedPlugins = plugins.flatMap(plugin => this.resolvePluginDependencies(plugin));
return this.collectDependencies(expandedPlugins);
}
async ensureRuntimeDependencies(options: { pluginKeys: string | string[]; logger?: ILogger }): Promise<InstallResult> {
const { pluginKeys, logger: log } = options;
const keys = Array.isArray(pluginKeys) ? pluginKeys : [pluginKeys];
const pluginDefines = keys.map(pluginKey => this.getDefineByPluginKey(pluginKey));
if (pluginDefines.every(pluginDefine => !pluginDefine.dependPackages && !pluginDefine.dependPlugins)) {
return { registryUrl: "", packageJsonPath: path.join(this.getRuntimeDepsRootDir(), "package.json") };
}
const expandedPluginDefines = pluginDefines.flatMap(pluginDefine => this.resolvePluginDependencies(pluginDefine));
return await this.ensureInstalled({ plugins: expandedPluginDefines, logger: log });
}
async importRuntime(specifier: string, logger: ILogger = defaultLogger) {
if (this.isNativeImportSpecifier(specifier)) {
return await import(specifier);
}
const resolved = await this.resolveImportSpecifier(specifier, logger);
return await import(pathToFileURL(resolved).href);
}
private async resolveImportSpecifier(specifier: string, logger: ILogger = defaultLogger) {
try {
return this.resolveRuntimeSpecifier(specifier).resolved;
} catch (runtimeError: any) {
if (!this.isModuleNotFoundError(runtimeError)) {
throw runtimeError;
}
return await this.resolveMissingRuntimeSpecifier(specifier, runtimeError, logger);
}
}
private async resolveMissingRuntimeSpecifier(specifier: string, runtimeError: any, logger?: ILogger) {
const packageName = this.parsePackageName(specifier);
const mergedDeps = this.getMergedLazyDependencies();
const lazyRange = mergedDeps[packageName];
if (!lazyRange) {
try {
return this.resolveProjectSpecifier(specifier, runtimeError).resolved;
} catch {
throw new Error(`动态依赖未安装且未配置懒加载版本: ${packageName}`);
}
}
try {
await this.ensureLazyDependency(packageName, logger);
return this.resolveRuntimeSpecifier(specifier).resolved;
} catch (lazyError: any) {
logger?.error?.(`动态依赖安装失败: ${lazyError.message}`);
return this.resolveProjectSpecifier(specifier, lazyError).resolved;
}
}
private isNativeImportSpecifier(specifier: string) {
return specifier.startsWith(".") || specifier.startsWith("/") || specifier.startsWith("file:") || specifier.startsWith("node:");
}
private resolveRuntimeSpecifier(specifier: string): RuntimeImportResolveResult {
const packageName = this.parsePackageName(specifier);
const packageJsonPath = path.join(this.getRuntimeDepsRootDir(), "package.json");
const require = createRequire(packageJsonPath);
const resolved = require.resolve(specifier);
return { packageName, resolved };
}
private resolveProjectSpecifier(specifier: string, cause?: any): RuntimeImportResolveResult {
try {
const packageName = this.parsePackageName(specifier);
const packageJsonPath = path.resolve("package.json");
const require = createRequire(packageJsonPath);
const resolved = require.resolve(specifier);
return { packageName, resolved };
} catch (projectError: any) {
if (cause) {
projectError.cause = cause;
}
throw projectError;
}
}
private parsePackageName(specifier: string) {
if (!specifier || specifier.trim() !== specifier) {
throw new Error(`动态依赖导入路径无效: ${specifier}`);
}
const parts = specifier.split("/");
if (specifier.startsWith("@")) {
if (parts.length < 2 || !parts[0] || !parts[1]) {
throw new Error(`动态依赖导入路径无效: ${specifier}`);
}
return `${parts[0]}/${parts[1]}`;
}
if (!parts[0]) {
throw new Error(`动态依赖导入路径无效: ${specifier}`);
}
return parts[0];
}
private async ensureLazyDependency(packageName: string, logger?: ILogger) {
const range = this.lazyDependencies?.[packageName];
if (!range) {
throw new Error(`动态依赖未安装且未配置懒加载版本: ${packageName}`);
}
await this.ensureDependencies({ dependencies: { [packageName]: range }, logger });
}
private isModuleNotFoundError(error: any) {
return error?.code === "MODULE_NOT_FOUND" || error?.code === "ERR_MODULE_NOT_FOUND";
}
resolvePluginDependencies(current: RuntimeDependencyPluginDefine): RuntimeDependencyPluginDefine[] {
const resolved: RuntimeDependencyPluginDefine[] = [];
const visited = new Set<string>();
const visit = (item: RuntimeDependencyPluginDefine) => {
const key = this.buildPluginDependencyKey(item);
if (visited.has(key)) {
return;
}
visited.add(key);
resolved.push(item);
for (const [dependencyName, expectedRange] of Object.entries(item.dependPlugins || {})) {
const dependency = this.getDefineByPluginKey(dependencyName, item);
if (!isPluginVersionCompatible(dependency, expectedRange)) {
throw new Error(`插件依赖版本冲突: ${item.name} 依赖 ${dependencyName}@${expectedRange},当前版本为 ${dependency.version || "未声明"}`);
}
visit(dependency);
}
};
visit(current);
return resolved;
}
private buildPluginDependencyKey(plugin: RuntimeDependencyPluginDefine) {
if (plugin.pluginType === "addon" && plugin.addonType) {
return `addon:${plugin.addonType}:${plugin.name}`;
}
const pluginType = plugin.pluginType === "deploy" ? "plugin" : plugin.pluginType || "unknown";
return `${pluginType}:${plugin.name}`;
}
private getDefineByPluginKey(pluginKey: string, owner?: RuntimeDependencyPluginDefine): RuntimeDependencyPluginDefine {
const parts = pluginKey.split(":");
let pluginType: string, name: string, subtype: string | undefined;
if (parts.length === 2) {
[pluginType, name] = parts;
} else if (parts.length === 3) {
[pluginType, subtype, name] = parts;
} else {
const ownerName = owner?.name || pluginKey;
throw new Error(`插件依赖格式错误: ${ownerName} 依赖 ${pluginKey}`);
}
if (!this.registriesMap) {
throw new Error("注册表未设置,请先调用 setRegistries");
}
const target = this.registriesMap[pluginType];
if (!target) {
const ownerName = owner?.name || pluginKey;
throw new Error(`插件依赖格式错误: ${ownerName} 依赖 ${pluginKey},未知插件类型 ${pluginType}`);
}
// addon 类型的 key 需要包含 subtype
const registryKey = pluginType === "addon" && subtype ? `${subtype}:${name}` : name;
const define = target.registry.getDefine(registryKey) as RegisteredDefineLike;
if (!define) {
throw new Error(`插件依赖缺失: ${owner?.name || pluginKey} 依赖 ${pluginKey},但该插件未注册或已禁用`);
}
return { ...define, key: pluginKey, pluginType: target.pluginType, addonType: target.addonType };
}
private async doEnsureInstalled(options: { dependencies: Record<string, string>; logger?: ILogger }): Promise<InstallResult> {
let { dependencies } = options;
const log = options.logger || defaultLogger;
return await this.withInstallLock(async () => {
const rootDir = this.getRuntimeDepsRootDir();
const packageJsonPath = path.join(rootDir, "package.json");
const lockPath = path.join(rootDir, "pnpm-lock.yaml");
log.info(`第三方依赖安装: ${JSON.stringify(dependencies)}`);
dependencies = this.mergeInstalledDependencies(this.readManifestDependencies(packageJsonPath), dependencies);
const dependenciesHash = this.createDependenciesHash(dependencies);
const statePath = path.join(rootDir, "install-state.json");
const currentState = this.readInstallState(statePath);
if (currentState?.dependenciesHash === dependenciesHash && fs.existsSync(path.join(rootDir, "node_modules"))) {
log.info("第三方依赖已安装");
return { registryUrl: currentState.registryUrl || "", packageJsonPath };
}
const manifest = { name: "certd-runtime-deps", private: true, type: "module", dependencies };
fs.writeFileSync(packageJsonPath, JSON.stringify(manifest, null, 2), "utf8");
const registryUrl = await this.registryResolver.resolve();
const env = this.buildChildEnv(registryUrl);
const command = this.getPnpmCommand();
const pnpmVersion = await this.getPnpmVersion(command, env);
const allRegistryUrls = await this.registryResolver.resolveOrdered();
const urlsToTry = allRegistryUrls.length > 0 ? allRegistryUrls : [""];
let lastError: string | undefined;
for (const tryUrl of urlsToTry) {
const args = ["install", "--prod", "--ignore-scripts", "--ignore-workspace", "--no-frozen-lockfile", "--reporter=append-only"];
if (tryUrl) {
args.push(`--registry=${tryUrl}`);
}
const tryEnv = tryUrl ? this.buildChildEnv(tryUrl) : env;
log.info(`开始安装第三方依赖: ${Object.keys(dependencies).join(", ")}${tryUrl ? `,镜像: ${tryUrl}` : ""}`);
const result = await this.commandRunner.run(command, args, { cwd: rootDir, timeoutMs: this.installTimeoutMs, env: tryEnv });
if (result.code === 0) {
this.writeInstallState(statePath, { installedAt: new Date().toISOString(), registryUrl: tryUrl, dependenciesHash, nodeVersion: process.version, pnpmVersion, lockFileExists: fs.existsSync(lockPath) });
log.info("第三方依赖安装完成");
return { registryUrl: tryUrl, packageJsonPath };
}
lastError = result.stderr || result.stdout || "unknown error";
log.warn?.(`镜像 ${tryUrl || "默认"} 安装失败${urlsToTry.length > 1 ? ",尝试下一个镜像..." : ""}`);
}
this.writeInstallState(statePath, {
...currentState,
installedAt: currentState?.installedAt,
failedAt: new Date().toISOString(),
registryUrl: urlsToTry[0],
dependenciesHash,
nodeVersion: process.version,
pnpmVersion,
lockFileExists: fs.existsSync(lockPath),
lastError,
});
throw new Error(`动态依赖安装失败: ${lastError}`);
});
}
private async withInstallLock<T>(run: () => Promise<T>): Promise<T> {
const rootDir = this.getRuntimeDepsRootDir();
fs.mkdirSync(rootDir, { recursive: true });
const lockFile = path.join(rootDir, ".install.lock");
const previous = PROCESS_LOCKS.get(lockFile);
if (previous) {
await previous.catch(() => undefined);
}
let releaseProcessLock!: () => void;
const current = new Promise<void>(resolve => {
releaseProcessLock = resolve;
});
PROCESS_LOCKS.set(lockFile, current);
let fd: number | undefined;
try {
fd = await this.acquireFileLock(lockFile);
return await run();
} finally {
if (fd != null) {
fs.closeSync(fd);
try {
fs.rmSync(lockFile, { force: true });
} catch {
try {
fs.rmSync(lockFile, { force: true });
} catch {}
}
}
releaseProcessLock();
if (PROCESS_LOCKS.get(lockFile) === current) {
PROCESS_LOCKS.delete(lockFile);
}
}
}
private async acquireFileLock(lockFile: string) {
const deadline = Date.now() + this.installTimeoutMs;
while (true) {
try {
const fd = fs.openSync(lockFile, "wx");
fs.writeFileSync(fd, JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString() }), "utf8");
return fd;
} catch (error: any) {
if (error?.code !== "EEXIST") {
throw error;
}
if (Date.now() > deadline) {
throw new Error(`动态依赖安装锁等待超时: ${lockFile}`);
}
await this.waitForExternalLock(lockFile, deadline);
}
}
}
private async waitForExternalLock(lockFile: string, deadline: number) {
while (fs.existsSync(lockFile)) {
if (Date.now() > deadline) {
throw new Error(`动态依赖安装锁等待超时: ${lockFile}`);
}
await new Promise(resolve => setTimeout(resolve, 300));
}
}
async clearRuntimeDeps() {
const rootDir = this.getRuntimeDepsRootDir();
const normalizedRootDir = path.normalize(rootDir);
if (!normalizedRootDir.endsWith(path.normalize(".runtime-deps"))) {
throw new Error(`动态依赖目录配置异常,拒绝清理: ${rootDir}`);
}
await this.withInstallLock(async () => {
if (fs.existsSync(rootDir)) {
const entries = fs.readdirSync(rootDir);
for (const entry of entries) {
if (entry === ".install.lock") {
continue;
}
fs.rmSync(path.join(rootDir, entry), { recursive: true, force: true });
}
}
this.installPromises.clear();
return undefined;
});
}
getMergedLazyDependencies(): Record<string, string> {
return { ...this.lazyDependencies, ...this.pluginLazyDependencies };
}
collectPluginDeps(logger?: ILogger) {
if (!this.registriesMap) {
return;
}
const deps: Record<string, string> = {};
for (const { registry } of Object.values(this.registriesMap)) {
const defineList = registry.getDefineList();
for (const define of defineList) {
const dependPackages = (define as any).dependPackages as Record<string, string> | undefined;
if (!dependPackages) {
continue;
}
for (const [pkgName, range] of Object.entries(dependPackages)) {
const existing = deps[pkgName];
if (existing && !areRangesCompatible(existing, range)) {
(logger || defaultLogger).warn?.(`懒加载依赖版本冲突: ${pkgName} => ${existing} vs ${range},保留已有版本`);
continue;
}
deps[pkgName] = range;
}
}
}
this.pluginLazyDependencies = deps;
(logger || defaultLogger).info(`从插件注册表收集到 ${Object.keys(deps).length} 个懒加载依赖`);
}
refreshPluginDeps(logger?: ILogger) {
this.collectPluginDeps(logger);
}
private readInstallState(statePath: string): any {
if (!fs.existsSync(statePath)) {
return null;
}
try {
return JSON.parse(fs.readFileSync(statePath, "utf8"));
} catch {
return null;
}
}
private writeInstallState(statePath: string, state: any) {
fs.writeFileSync(statePath, JSON.stringify(state, null, 2), "utf8");
}
private readManifestDependencies(packageJsonPath: string): Record<string, string> {
if (!fs.existsSync(packageJsonPath)) {
return {};
}
try {
const manifest = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
return manifest.dependencies || {};
} catch {
return {};
}
}
private mergeInstalledDependencies(installed: Record<string, string>, requested: Record<string, string>) {
const dependencies = { ...installed };
for (const [packageName, range] of Object.entries(requested)) {
const installedRange = dependencies[packageName];
if (installedRange && !areRangesCompatible(installedRange, range)) {
throw new Error(`动态依赖版本冲突: ${packageName} => installed:${installedRange}, requested:${range}`);
}
dependencies[packageName] = installedRange || range;
}
return dependencies;
}
private async getPnpmVersion(command: string, env: NodeJS.ProcessEnv) {
const result = await this.commandRunner.run(command, ["--version"], { cwd: this.getRuntimeDepsRootDir(), timeoutMs: Math.min(this.installTimeoutMs, 10000), env });
if (result.code !== 0) {
return "";
}
return (result.stdout || result.stderr || "").trim();
}
private getPnpmCommand() {
return this.pnpmCommand || "pnpm";
}
private buildChildEnv(registryUrl: string) {
const env = { ...process.env };
for (const key of ["NODE_OPTIONS", "VSCODE_INSPECTOR_OPTIONS", "NODE_INSPECTOR_PORT", "NODE_DEBUG"]) {
if (!env[key]) {
continue;
}
if (key === "NODE_OPTIONS") {
env[key] = this.stripDebugNodeOptions(env[key] as string);
} else {
delete env[key];
}
}
if (registryUrl) {
env.npm_config_registry = registryUrl;
env.pnpm_config_registry = registryUrl;
}
env.CI = env.CI || "true";
env.npm_config_confirm_modules_purge = "false";
env.pnpm_config_confirm_modules_purge = "false";
return env;
}
private stripDebugNodeOptions(value: string) {
return value
.split(/\s+/)
.filter(Boolean)
.filter(item => !/^--inspect(-brk|-port)?(=|$)/.test(item))
.filter(item => !/^--debug(=|$)/.test(item))
.join(" ");
}
getRuntimeDepsRootDir() {
return path.resolve(this.runtimeDepsRootDir);
}
private createDependenciesHash(dependencies: Record<string, string>) {
return crypto.createHash("sha256").update(JSON.stringify(dependencies)).digest("hex");
}
}
function isPluginVersionCompatible(plugin: RuntimeDependencyPluginDefine, expectedRange: string) {
if (!expectedRange || expectedRange === "*") {
return true;
}
if (!plugin.version) {
return false;
}
return areRangesCompatible(expectedRange, plugin.version);
}
let runtimeDepsServiceInstance: RuntimeDepsService | null = null;
export function initRuntimeDepsService(config: RuntimeDepsConfig, registries: any): RuntimeDepsService {
runtimeDepsServiceInstance = new RuntimeDepsService(config, registries);
return runtimeDepsServiceInstance;
}
export function getRuntimeDepsService(): RuntimeDepsService {
if (!runtimeDepsServiceInstance) {
throw new Error("RuntimeDepsService 未初始化");
}
return runtimeDepsServiceInstance!;
}
export async function importRuntime(specifier: string, logger: ILogger = defaultLogger): Promise<any> {
return getRuntimeDepsService().importRuntime(specifier, logger);
}
+28
View File
@@ -3,6 +3,34 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.42.6](https://github.com/certd/certd/compare/v1.42.5...v1.42.6) (2026-07-18)
**Note:** Version bump only for package @certd/lib-huawei
## [1.42.5](https://github.com/certd/certd/compare/v1.42.4...v1.42.5) (2026-07-15)
**Note:** Version bump only for package @certd/lib-huawei
## [1.42.4](https://github.com/certd/certd/compare/v1.42.3...v1.42.4) (2026-07-11)
**Note:** Version bump only for package @certd/lib-huawei
## [1.42.3](https://github.com/certd/certd/compare/v1.42.2...v1.42.3) (2026-07-08)
**Note:** Version bump only for package @certd/lib-huawei
## [1.42.2](https://github.com/certd/certd/compare/v1.42.1...v1.42.2) (2026-07-07)
**Note:** Version bump only for package @certd/lib-huawei
## [1.42.1](https://github.com/certd/certd/compare/v1.42.0...v1.42.1) (2026-07-06)
**Note:** Version bump only for package @certd/lib-huawei
# [1.42.0](https://github.com/certd/certd/compare/v1.41.4...v1.42.0) (2026-07-05)
**Note:** Version bump only for package @certd/lib-huawei
## [1.41.4](https://github.com/certd/certd/compare/v1.41.3...v1.41.4) (2026-06-14)
### Bug Fixes
+3 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/lib-huawei",
"private": false,
"version": "1.41.4",
"version": "1.42.6",
"main": "./dist/bundle.js",
"module": "./dist/bundle.js",
"types": "./dist/d/index.d.ts",
@@ -26,9 +26,10 @@
"@typescript-eslint/eslint-plugin": "^8.26.1",
"@typescript-eslint/parser": "^8.26.1",
"cross-env": "^7.0.3",
"eslint": "^8.57.0",
"esmock": "^2.7.5",
"prettier": "3.3.3",
"tslib": "^2.8.1"
},
"gitHead": "bc731e4fb119787930e816a7d57c808b1b5cd66a"
"gitHead": "246ee83015bf5589adc2a5fa3d1388c8d9a2a252"
}
+28
View File
@@ -3,6 +3,34 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.42.6](https://github.com/certd/certd/compare/v1.42.5...v1.42.6) (2026-07-18)
**Note:** Version bump only for package @certd/lib-iframe
## [1.42.5](https://github.com/certd/certd/compare/v1.42.4...v1.42.5) (2026-07-15)
**Note:** Version bump only for package @certd/lib-iframe
## [1.42.4](https://github.com/certd/certd/compare/v1.42.3...v1.42.4) (2026-07-11)
**Note:** Version bump only for package @certd/lib-iframe
## [1.42.3](https://github.com/certd/certd/compare/v1.42.2...v1.42.3) (2026-07-08)
**Note:** Version bump only for package @certd/lib-iframe
## [1.42.2](https://github.com/certd/certd/compare/v1.42.1...v1.42.2) (2026-07-07)
**Note:** Version bump only for package @certd/lib-iframe
## [1.42.1](https://github.com/certd/certd/compare/v1.42.0...v1.42.1) (2026-07-06)
**Note:** Version bump only for package @certd/lib-iframe
# [1.42.0](https://github.com/certd/certd/compare/v1.41.4...v1.42.0) (2026-07-05)
**Note:** Version bump only for package @certd/lib-iframe
## [1.41.4](https://github.com/certd/certd/compare/v1.41.3...v1.41.4) (2026-06-14)
### Bug Fixes
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/lib-iframe",
"private": false,
"version": "1.41.4",
"version": "1.42.6",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
@@ -37,5 +37,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "bc731e4fb119787930e816a7d57c808b1b5cd66a"
"gitHead": "246ee83015bf5589adc2a5fa3d1388c8d9a2a252"
}
+28
View File
@@ -3,6 +3,34 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.42.6](https://github.com/certd/certd/compare/v1.42.5...v1.42.6) (2026-07-18)
**Note:** Version bump only for package @certd/jdcloud
## [1.42.5](https://github.com/certd/certd/compare/v1.42.4...v1.42.5) (2026-07-15)
**Note:** Version bump only for package @certd/jdcloud
## [1.42.4](https://github.com/certd/certd/compare/v1.42.3...v1.42.4) (2026-07-11)
**Note:** Version bump only for package @certd/jdcloud
## [1.42.3](https://github.com/certd/certd/compare/v1.42.2...v1.42.3) (2026-07-08)
**Note:** Version bump only for package @certd/jdcloud
## [1.42.2](https://github.com/certd/certd/compare/v1.42.1...v1.42.2) (2026-07-07)
**Note:** Version bump only for package @certd/jdcloud
## [1.42.1](https://github.com/certd/certd/compare/v1.42.0...v1.42.1) (2026-07-06)
**Note:** Version bump only for package @certd/jdcloud
# [1.42.0](https://github.com/certd/certd/compare/v1.41.4...v1.42.0) (2026-07-05)
**Note:** Version bump only for package @certd/jdcloud
## [1.41.4](https://github.com/certd/certd/compare/v1.41.3...v1.41.4) (2026-06-14)
### Bug Fixes
+3 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@certd/jdcloud",
"version": "1.41.4",
"version": "1.42.6",
"description": "jdcloud openApi sdk",
"main": "./dist/bundle.js",
"module": "./dist/bundle.js",
@@ -35,6 +35,7 @@
"chai": "^5.1.0",
"config": "^1.30.0",
"cross-env": "^7.0.3",
"eslint": "^8.57.0",
"esmock": "^2.7.5",
"js-yaml": "^3.11.0",
"mocha": "^10.6.0",
@@ -62,5 +63,5 @@
"fetch"
]
},
"gitHead": "bc731e4fb119787930e816a7d57c808b1b5cd66a"
"gitHead": "246ee83015bf5589adc2a5fa3d1388c8d9a2a252"
}
+28
View File
@@ -3,6 +3,34 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.42.6](https://github.com/certd/certd/compare/v1.42.5...v1.42.6) (2026-07-18)
**Note:** Version bump only for package @certd/lib-k8s
## [1.42.5](https://github.com/certd/certd/compare/v1.42.4...v1.42.5) (2026-07-15)
**Note:** Version bump only for package @certd/lib-k8s
## [1.42.4](https://github.com/certd/certd/compare/v1.42.3...v1.42.4) (2026-07-11)
**Note:** Version bump only for package @certd/lib-k8s
## [1.42.3](https://github.com/certd/certd/compare/v1.42.2...v1.42.3) (2026-07-08)
**Note:** Version bump only for package @certd/lib-k8s
## [1.42.2](https://github.com/certd/certd/compare/v1.42.1...v1.42.2) (2026-07-07)
**Note:** Version bump only for package @certd/lib-k8s
## [1.42.1](https://github.com/certd/certd/compare/v1.42.0...v1.42.1) (2026-07-06)
**Note:** Version bump only for package @certd/lib-k8s
# [1.42.0](https://github.com/certd/certd/compare/v1.41.4...v1.42.0) (2026-07-05)
**Note:** Version bump only for package @certd/lib-k8s
## [1.41.4](https://github.com/certd/certd/compare/v1.41.3...v1.41.4) (2026-06-14)
**Note:** Version bump only for package @certd/lib-k8s
+3 -3
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/lib-k8s",
"private": false,
"version": "1.41.4",
"version": "1.42.6",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
@@ -21,7 +21,7 @@
"lint": "eslint --fix"
},
"dependencies": {
"@certd/basic": "^1.41.4",
"@certd/basic": "^1.42.6",
"@kubernetes/client-node": "0.21.0"
},
"devDependencies": {
@@ -38,5 +38,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "bc731e4fb119787930e816a7d57c808b1b5cd66a"
"gitHead": "246ee83015bf5589adc2a5fa3d1388c8d9a2a252"
}
+43
View File
@@ -3,6 +3,49 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.42.6](https://github.com/certd/certd/compare/v1.42.5...v1.42.6) (2026-07-18)
### Bug Fixes
* 修复正常批量删除流水线报权限不足的bug ([5b50083](https://github.com/certd/certd/commit/5b500830a122c6c42dab054e57fed509050f94da))
## [1.42.5](https://github.com/certd/certd/compare/v1.42.4...v1.42.5) (2026-07-15)
### Bug Fixes
* 修复上传到cos报runtimeDepsService未初始化的问题 ([167b303](https://github.com/certd/certd/commit/167b303faeca02cc11cf97e4be2a3df914852167))
## [1.42.4](https://github.com/certd/certd/compare/v1.42.3...v1.42.4) (2026-07-11)
### Bug Fixes
* **pipeline:** 重构运行时依赖加载逻辑,修复火山引擎DNS解析报runtimeDepsService未初始化的bug ([ec69b8f](https://github.com/certd/certd/commit/ec69b8f11bfd4b20991aef74a72a47182ca79a9d))
## [1.42.3](https://github.com/certd/certd/compare/v1.42.2...v1.42.3) (2026-07-08)
**Note:** Version bump only for package @certd/lib-server
## [1.42.2](https://github.com/certd/certd/compare/v1.42.1...v1.42.2) (2026-07-07)
**Note:** Version bump only for package @certd/lib-server
## [1.42.1](https://github.com/certd/certd/compare/v1.42.0...v1.42.1) (2026-07-06)
**Note:** Version bump only for package @certd/lib-server
# [1.42.0](https://github.com/certd/certd/compare/v1.41.4...v1.42.0) (2026-07-05)
### Features
* 通过插件配置懒加载依赖,动态加载第三方依赖包,精简安装镜像大小 ([01568ca](https://github.com/certd/certd/commit/01568ca1489069046b5a89ebdd4ced2f7f6ddf93))
### Performance Improvements
* 【破坏性更新】 证书压缩包不再生成文件存储,而是实时打包下载,证书申请插件不再输出certZip ([7cff1a9](https://github.com/certd/certd/commit/7cff1a98424120585205889874b3ef4956a30583))
* 优化用户体验,首次访问时弹出邮箱账号绑定用以初始化账号 ([608cc2a](https://github.com/certd/certd/commit/608cc2a81ff0b4872c9fe11ed9c9c0b4b90a12a3))
* 支持全自动匹配部署宝塔网站证书 ([4dff48e](https://github.com/certd/certd/commit/4dff48e807c32a7623ec9206cf39c88e88f89f6a))
* dns默认ipv4first ([194463b](https://github.com/certd/certd/commit/194463bea9e797315aa7a724f4b2930701570419))
## [1.41.4](https://github.com/certd/certd/compare/v1.41.3...v1.41.4) (2026-06-14)
**Note:** Version bump only for package @certd/lib-server
+9 -9
View File
@@ -1,6 +1,6 @@
{
"name": "@certd/lib-server",
"version": "1.41.4",
"version": "1.42.6",
"description": "midway with flyway, sql upgrade way ",
"private": false,
"type": "module",
@@ -29,11 +29,11 @@
],
"license": "AGPL",
"dependencies": {
"@certd/acme-client": "^1.41.4",
"@certd/basic": "^1.41.4",
"@certd/pipeline": "^1.41.4",
"@certd/plugin-lib": "^1.41.4",
"@certd/plus-core": "^1.41.4",
"@certd/acme-client": "^1.42.6",
"@certd/basic": "^1.42.6",
"@certd/pipeline": "^1.42.6",
"@certd/plugin-lib": "^1.42.6",
"@certd/plus-core": "^1.42.6",
"@midwayjs/cache": "3.14.0",
"@midwayjs/core": "3.20.11",
"@midwayjs/i18n": "3.20.13",
@@ -49,8 +49,6 @@
"typeorm": "^0.3.20"
},
"devDependencies": {
"mwts": "^1.3.0",
"mwtsc": "^1.4.0",
"@types/chai": "^4.3.12",
"@types/mocha": "^10.0.6",
"@types/node": "^18",
@@ -62,6 +60,8 @@
"eslint-plugin-prettier": "^5.1.3",
"esmock": "^2.7.5",
"mocha": "^10.6.0",
"mwts": "^1.3.0",
"mwtsc": "^1.4.0",
"prettier": "3.3.3",
"rimraf": "^5.0.5",
"ts-node": "^10.9.2",
@@ -69,5 +69,5 @@
"typeorm": "^0.3.20",
"typescript": "^5.4.2"
},
"gitHead": "bc731e4fb119787930e816a7d57c808b1b5cd66a"
"gitHead": "246ee83015bf5589adc2a5fa3d1388c8d9a2a252"
}
@@ -1,9 +1,8 @@
import { ApplicationContext, Inject } from '@midwayjs/core';
import type {IMidwayContainer} from '@midwayjs/core';
import * as koa from '@midwayjs/koa';
import { Constants } from './constants.js';
import { isEnterprise } from './mode.js';
import { ApplicationContext, Inject } from "@midwayjs/core";
import type { IMidwayContainer } from "@midwayjs/core";
import * as koa from "@midwayjs/koa";
import { Constants } from "./constants.js";
import { isEnterprise } from "./mode.js";
export abstract class BaseController {
@Inject()
@@ -41,7 +40,7 @@ export abstract class BaseController {
getUserId() {
const userId = this.ctx.user?.id;
if (userId == null) {
throw new Error('Token已过期');
throw new Error("Token已过期");
}
return userId;
}
@@ -49,7 +48,7 @@ export abstract class BaseController {
getLoginUser() {
const user = this.ctx.user;
if (user == null) {
throw new Error('Token已过期');
throw new Error("Token已过期");
}
return user;
}
@@ -61,73 +60,71 @@ export abstract class BaseController {
}
}
async getProjectId(permission:string) {
async getProjectId(permission: string) {
if (!isEnterprise()) {
return undefined
return undefined;
}
let projectIdStr = this.ctx.headers["project-id"] as string;
if (!projectIdStr){
if (!projectIdStr) {
projectIdStr = this.ctx.request.query["projectId"] as string;
}
if (!projectIdStr) {
//这里必须抛异常,否则可能会有权限问题
throw new Error("projectId 不能为空")
throw new Error("projectId 不能为空");
}
const userId = this.getUserId()
const projectId = parseInt(projectIdStr)
await this.checkProjectPermission(userId, projectId,permission)
const userId = this.getUserId();
const projectId = parseInt(projectIdStr);
await this.checkProjectPermission(userId, projectId, permission);
return projectId;
}
async getProjectUserId(permission:string){
let userId = this.getUserId()
const projectId = await this.getProjectId(permission)
if(projectId){
userId = -1 // 企业管理模式下,用户id固定-1
async getProjectUserId(permission: string) {
let userId = this.getUserId();
const projectId = await this.getProjectId(permission);
if (projectId) {
userId = -1; // 企业管理模式下,用户id固定-1
}
return {
projectId,userId
}
projectId,
userId,
};
}
async getProjectUserIdRead(){
return await this.getProjectUserId("read")
async getProjectUserIdRead() {
return await this.getProjectUserId("read");
}
async getProjectUserIdWrite(){
return await this.getProjectUserId("write")
async getProjectUserIdWrite() {
return await this.getProjectUserId("write");
}
async getProjectUserIdAdmin(){
return await this.getProjectUserId("admin")
async getProjectUserIdAdmin() {
return await this.getProjectUserId("admin");
}
async checkProjectPermission(userId: number, projectId: number,permission:string) {
const projectService:any = await this.applicationContext.getAsync("projectService");
await projectService.checkPermission({userId,projectId,permission})
async checkProjectPermission(userId: number, projectId: number, permission: string) {
const projectService: any = await this.applicationContext.getAsync("projectService");
await projectService.checkPermission({ userId, projectId, permission });
}
/**
*
*
* @param service
* @param id
* @param id
*/
async checkOwner(service:any,id:number,permission:string,allowAdmin:boolean = false){
let { projectId,userId } = await this.getProjectUserId(permission)
const authService:any = await this.applicationContext.getAsync("authService");
async checkOwner(service: any, id: number, permission: string, allowAdmin: boolean = false) {
const { projectId, userId } = await this.getProjectUserId(permission);
const authService: any = await this.applicationContext.getAsync("authService");
if (projectId) {
await authService.checkProjectId(service, id, projectId);
}else{
if(userId === Constants.systemUserId){
} else {
if (userId === Constants.systemUserId) {
//系统级别,不检查权限
}else{
if(allowAdmin){
} else {
if (allowAdmin) {
await authService.checkUserIdButAllowAdmin(this.ctx, service, id);
}else{
await authService.checkUserId( service, id, userId);
} else {
await authService.checkUserId(service, id, userId);
}
}
}
return {projectId,userId}
return { projectId, userId };
}
}
@@ -56,7 +56,7 @@ export abstract class BaseService<T> {
return dataSource.getRepository(entity);
}
protected buildUserProjectQuery(userId: number, projectId?: number) {
public buildUserProjectQuery(userId: number, projectId?: number) {
const query: { userId: number; projectId?: number; [key: string]: any } = {
userId,
};
@@ -253,7 +253,6 @@ export abstract class BaseService<T> {
if (!Array.isArray(ids)) {
ids = [ids];
}
ids = this.filterIds(ids);
const res = await this.getRepository().find({
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
@@ -266,7 +265,7 @@ export abstract class BaseService<T> {
},
});
if (!res || res.length === ids.length) {
return;
return ids;
}
throw new PermissionException("权限不足");
}
@@ -279,7 +278,13 @@ export abstract class BaseService<T> {
return item != null && item != "";
});
}
async batchDelete(ids: number[], userId: number, projectId?: number) {
async batchDelete(ids: number[], userId: number, projectId?: number): Promise<number> {
if (!ids || ids.length === 0) {
throw new ValidateException("ids不能为空");
}
if (!Array.isArray(ids)) {
ids = [ids];
}
ids = this.filterIds(ids);
if (userId != null) {
const userProjectQuery = this.buildUserProjectQuery(userId, projectId);
@@ -295,6 +300,7 @@ export abstract class BaseService<T> {
}
await this.delete(ids);
return ids.length;
}
async findOne(options: FindOneOptions<T>) {
@@ -1,33 +1,33 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
/**
*/
@Entity('sys_settings')
@Entity("sys_settings")
export class SysSettingsEntity {
@PrimaryGeneratedColumn()
id: number;
@Column({ comment: 'key', length: 100 })
@Column({ comment: "key", length: 100 })
key: string;
@Column({ comment: '名称', length: 100 })
@Column({ comment: "名称", length: 100 })
title: string;
@Column({ name: 'setting', comment: '设置', length: 1024, nullable: true })
@Column({ name: "setting", comment: "设置", length: 1024, nullable: true })
setting: string;
// public 公开读,私有写, private 私有读,私有写
@Column({ name: 'access', comment: '访问权限' })
@Column({ name: "access", comment: "访问权限" })
access: string;
@Column({
name: 'create_time',
comment: '创建时间',
default: () => 'CURRENT_TIMESTAMP',
name: "create_time",
comment: "创建时间",
default: () => "CURRENT_TIMESTAMP",
})
createTime: Date;
@Column({
name: 'update_time',
comment: '修改时间',
default: () => 'CURRENT_TIMESTAMP',
name: "update_time",
comment: "修改时间",
default: () => "CURRENT_TIMESTAMP",
})
updateTime: Date;
}
@@ -1,19 +1,19 @@
import { cloneDeep } from 'lodash-es';
import { cloneDeep } from "lodash-es";
export class BaseSettings {
static __key__: string;
static __title__: string;
static __access__ = 'private';
static __access__ = "private";
static getCacheKey() {
return 'settings.' + this.__key__;
return "settings." + this.__key__;
}
}
export class SysPublicSettings extends BaseSettings {
static __key__ = 'sys.public';
static __title__ = '系统公共设置';
static __access__ = 'public';
static __key__ = "sys.public";
static __title__ = "系统公共设置";
static __access__ = "public";
registerEnabled = false;
userValidTimeEnabled?: boolean = false;
@@ -34,19 +34,15 @@ export class SysPublicSettings extends BaseSettings {
aiChatEnabled = true;
homePageEnabled = true;
//验证码是否开启
captchaEnabled = false;
//验证码类型
captchaType?: string;
captchaAddonId?: number;
//流水线是否启用有效期
pipelineValidTimeEnabled?: boolean = false;
//证书域名添加到监控
certDomainAddToMonitorEnabled?: boolean = false;
@@ -60,12 +56,15 @@ export class SysPublicSettings extends BaseSettings {
// 第三方OAuth配置
oauthEnabled?: boolean = false;
oauthProviders: Record<string, {
type: string;
title: string;
addonId: number;
icon?: string;
}> = {};
oauthProviders: Record<
string,
{
type: string;
title: string;
addonId: number;
icon?: string;
}
> = {};
notice?: string;
@@ -73,40 +72,37 @@ export class SysPublicSettings extends BaseSettings {
}
export class SysPrivateSettings extends BaseSettings {
static __title__ = '系统私有设置';
static __access__ = 'private';
static __key__ = 'sys.private';
static __title__ = "系统私有设置";
static __access__ = "private";
static __key__ = "sys.private";
jwtKey?: string;
encryptSecret?: string;
httpsProxy? = '';
httpProxy? = '';
noProxy? = '';
commonHeaders?: string = '';
httpsProxy? = "";
httpProxy? = "";
noProxy? = "";
commonHeaders?: string = "";
reverseProxies?: Record<string, string> = {};
dnsResultOrder? = '';
dnsResultOrder? = "";
commonCnameEnabled?: boolean = true;
httpRequestTimeout?: number = 30;
pipelineMaxRunningCount?: number;
environmentVars?: string = '';
environmentVars?: string = "";
acmeWalkFromAuthoritative?: boolean = true;
sms?: {
type?: string;
config?: any;
} = {
type: 'aliyun',
config: {},
};
type: "aliyun",
config: {},
};
removeSecret() {
const clone = cloneDeep(this);
@@ -117,9 +113,9 @@ export class SysPrivateSettings extends BaseSettings {
}
export class SysInstallInfo extends BaseSettings {
static __title__ = '系统安装信息';
static __key__ = 'sys.install';
static __access__ = 'private';
static __title__ = "系统安装信息";
static __key__ = "sys.install";
static __access__ = "private";
installTime?: number;
siteId?: string;
bindUserId?: number;
@@ -130,21 +126,20 @@ export class SysInstallInfo extends BaseSettings {
}
export class SysLicenseInfo extends BaseSettings {
static __title__ = '授权许可信息';
static __key__ = 'sys.license';
static __access__ = 'private';
static __title__ = "授权许可信息";
static __key__ = "sys.license";
static __access__ = "private";
license?: string;
}
export type EmailTemplate = {
addonId?: number;
}
};
export class SysEmailConf extends BaseSettings {
static __title__ = '邮箱配置';
static __key__ = 'sys.email';
static __access__ = 'private';
static __title__ = "邮箱配置";
static __key__ = "sys.email";
static __access__ = "private";
host: string;
port: number;
@@ -160,18 +155,18 @@ export class SysEmailConf extends BaseSettings {
sender: string;
usePlus?: boolean;
templates:{
registerCode?: EmailTemplate,
forgotPassword?: EmailTemplate,
pipelineResult?: EmailTemplate,
common?: EmailTemplate,
}
templates: {
registerCode?: EmailTemplate;
forgotPassword?: EmailTemplate;
pipelineResult?: EmailTemplate;
common?: EmailTemplate;
};
}
export class SysSiteInfo extends BaseSettings {
static __title__ = '站点信息';
static __key__ = 'sys.site';
static __access__ = 'public';
static __title__ = "站点信息";
static __key__ = "sys.site";
static __access__ = "public";
title?: string;
slogan?: string;
logo?: string;
@@ -179,9 +174,9 @@ export class SysSiteInfo extends BaseSettings {
}
export class SysSecretBackup extends BaseSettings {
static __title__ = '密钥信息备份';
static __key__ = 'sys.secret.backup';
static __access__ = 'private';
static __title__ = "密钥信息备份";
static __key__ = "sys.secret.backup";
static __access__ = "private";
siteId?: string;
encryptSecret?: string;
}
@@ -190,9 +185,9 @@ export class SysSecretBackup extends BaseSettings {
*
*/
export class SysSecret extends BaseSettings {
static __title__ = '密钥信息';
static __key__ = 'sys.secret';
static __access__ = 'private';
static __title__ = "密钥信息";
static __key__ = "sys.secret";
static __access__ = "private";
siteId?: string;
encryptSecret?: string;
}
@@ -215,9 +210,9 @@ export type MenuItem = {
children?: MenuItem[];
};
export class SysHeaderMenus extends BaseSettings {
static __title__ = '顶部菜单';
static __key__ = 'sys.header.menus';
static __access__ = 'public';
static __title__ = "顶部菜单";
static __key__ = "sys.header.menus";
static __access__ = "public";
menus: MenuItem[];
}
@@ -228,9 +223,9 @@ export type PaymentItem = {
};
export class SysPaymentSetting extends BaseSettings {
static __title__ = '支付设置';
static __key__ = 'sys.payment';
static __access__ = 'private';
static __title__ = "支付设置";
static __key__ = "sys.payment";
static __access__ = "private";
yizhifu?: PaymentItem = { enabled: false };
@@ -240,9 +235,9 @@ export class SysPaymentSetting extends BaseSettings {
}
export class SysSuiteSetting extends BaseSettings {
static __title__ = '套餐设置';
static __key__ = 'sys.suite';
static __access__ = 'private';
static __title__ = "套餐设置";
static __key__ = "sys.suite";
static __access__ = "private";
enabled: boolean = false;
@@ -257,26 +252,25 @@ export class SysSuiteSetting extends BaseSettings {
}
export class SysAutoFixSetting extends BaseSettings {
static __title__ = '自动修复记录';
static __key__ = 'sys.auto.fix';
static __access__ = 'private';
static __title__ = "自动修复记录";
static __key__ = "sys.auto.fix";
static __access__ = "private";
fixed: Record<string, boolean> = {};
}
export type SiteHidden = {
enabled: boolean;
openPath?: string;
//md5 hash 两次后保存
openPassword?: string;
autoHiddenTimes?: number;
hiddenOpenApi?: boolean
hiddenOpenApi?: boolean;
};
export class SysSafeSetting extends BaseSettings {
static __title__ = '站点安全设置';
static __key__ = 'sys.safe';
static __access__ = 'private';
static __title__ = "站点安全设置";
static __key__ = "sys.safe";
static __access__ = "private";
// 站点隐藏
hidden: SiteHidden = {
@@ -1,29 +1,20 @@
import { IAccessService, IRuntimeDepsService } from "@certd/pipeline";
export type AccessRuntimeDepsService = IRuntimeDepsService;
import { IAccessService } from "@certd/pipeline";
export class AccessGetter implements IAccessService {
userId: number;
projectId?: number;
runtimeDepsService?: AccessRuntimeDepsService;
getter: <T>(id: any, userId?: number, projectId?: number, ignorePermission?: boolean, runtimeDepsService?: AccessRuntimeDepsService) => Promise<T>;
constructor(
userId: number,
projectId: number,
getter: (id: any, userId: number, projectId?: number, ignorePermission?: boolean, runtimeDepsService?: AccessRuntimeDepsService) => Promise<any>,
runtimeDepsService?: AccessRuntimeDepsService
) {
getter: <T>(id: any, userId?: number, projectId?: number, ignorePermission?: boolean) => Promise<T>;
constructor(userId: number, projectId: number, getter: (id: any, userId: number, projectId?: number, ignorePermission?: boolean) => Promise<any>) {
this.userId = userId;
this.projectId = projectId;
this.getter = getter;
this.runtimeDepsService = runtimeDepsService;
}
async getById<T = any>(id: any) {
return await this.getter<T>(id, this.userId, this.projectId, false, this.runtimeDepsService);
return await this.getter<T>(id, this.userId, this.projectId, false);
}
async getCommonById<T = any>(id: any) {
return await this.getter<T>(id, 0, null, false, this.runtimeDepsService);
return await this.getter<T>(id, 0, null, false);
}
}
@@ -1,8 +1,8 @@
import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
import { ApplicationContext, Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
import type { IMidwayContainer } from "@midwayjs/core";
import { InjectEntityModel } from "@midwayjs/typeorm";
import { In, Repository } from "typeorm";
import { AccessGetter, BaseService, PageReq, PermissionException, ValidateException } from "../../../index.js";
import type { AccessRuntimeDepsService } from "./access-getter.js";
import { AccessEntity } from "../entity/access.js";
import { AccessDefine, accessRegistry, newAccess } from "@certd/pipeline";
import { EncryptService } from "./encrypt-service.js";
@@ -20,6 +20,9 @@ export class AccessService extends BaseService<AccessEntity> {
@Inject()
encryptService: EncryptService;
@ApplicationContext()
applicationContext: IMidwayContainer;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
getRepository() {
@@ -161,7 +164,7 @@ export class AccessService extends BaseService<AccessEntity> {
};
}
async getAccessById(id: any, checkUserId: boolean, userId?: number, projectId?: number, runtimeDepsService?: AccessRuntimeDepsService): Promise<any> {
async getAccessById(id: any, checkUserId: boolean, userId?: number, projectId?: number): Promise<any> {
const entity = await this.info(id);
if (entity == null) {
throw new Error(`该授权配置不存在,请确认是否已被删除:id=${id}`);
@@ -184,20 +187,23 @@ export class AccessService extends BaseService<AccessEntity> {
id: entity.id,
...setting,
};
const taskServiceBuilder: any = await this.applicationContext.getAsync("taskServiceBuilder");
const serviceGetter = taskServiceBuilder.create({ userId: userId || 0, projectId });
const getAccessById = this.getById.bind(this);
const accessGetter = new AccessGetter(userId, projectId, getAccessById, runtimeDepsService);
const accessGetter = new AccessGetter(userId, projectId, getAccessById);
const accessContext = {
logger,
http,
utils,
accessService: accessGetter,
serviceGetter,
} as any;
const access = await newAccess(entity.type, input, accessGetter, accessContext);
return access;
}
async getById(id: any, userId: number, projectId?: number, _ignorePermission?: boolean, runtimeDepsService?: AccessRuntimeDepsService): Promise<any> {
return await this.getAccessById(id, true, userId, projectId, runtimeDepsService);
async getById(id: any, userId: number, projectId?: number, _ignorePermission?: boolean): Promise<any> {
return await this.getAccessById(id, true, userId, projectId);
}
decryptAccessEntity(entity: AccessEntity): any {
@@ -1,15 +1,6 @@
import { HttpClient, ILogger, utils } from "@certd/basic";
import {upperFirst} from "lodash-es";
import {
accessRegistry,
FormItemProps,
IAccessService,
IRuntimeDepsService,
IServiceGetter,
PluginRequestHandleReq,
Registrable
} from "@certd/pipeline";
import { upperFirst } from "lodash-es";
import { accessRegistry, FormItemProps, IAccessService, IServiceGetter, PluginRequestHandleReq, Registrable, getRuntimeDepsService } from "@certd/pipeline";
export type AddonRequestHandleReqInput<T = any> = {
id?: number;
@@ -19,7 +10,7 @@ export type AddonRequestHandleReqInput<T = any> = {
export type AddonRequestHandleReq<T = any> = {
addonType: string;
} &PluginRequestHandleReq<AddonRequestHandleReqInput<T>>;
} & PluginRequestHandleReq<AddonRequestHandleReqInput<T>>;
export type AddonInputDefine = FormItemProps & {
title: string;
@@ -48,8 +39,6 @@ export type AddonInstanceConfig = {
};
};
export interface IAddon {
ctx: AddonContext;
[key: string]: any;
@@ -67,13 +56,9 @@ export abstract class BaseAddon implements IAddon {
ctx!: AddonContext;
http!: HttpClient;
logger!: ILogger;
runtimeDepsService?: IRuntimeDepsService;
async importRuntime(specifier: string) {
if (!this.runtimeDepsService) {
return await import(specifier);
}
return await this.runtimeDepsService.importRuntime(specifier, this.logger);
return await getRuntimeDepsService().importRuntime(specifier, this.logger);
}
title!: string;
@@ -85,7 +70,7 @@ export abstract class BaseAddon implements IAddon {
if (accessId == null) {
throw new Error("您还没有配置授权");
}
const accessService = await this.ctx.serviceGetter.get<IAccessService>("accessService")
const accessService = await this.ctx.serviceGetter.get<IAccessService>("accessService");
let res: any = null;
if (isCommon) {
res = await accessService.getCommonById(accessId);
@@ -118,18 +103,12 @@ export abstract class BaseAddon implements IAddon {
this.ctx = ctx;
this.http = ctx.http;
this.logger = ctx.logger;
if (!this.runtimeDepsService && this.ctx.serviceGetter) {
this.runtimeDepsService = await this.ctx.serviceGetter.get("runtimeDepsService");
}
if (this.runtimeDepsService && this.define?.addonType && this.define?.name) {
await this.runtimeDepsService.ensureRuntimeDependencies({ pluginKeys: `addon:${this.define.addonType}:${this.define.name}`, logger: this.logger });
}
}
setDefine = (define:AddonDefine) => {
setDefine = (define: AddonDefine) => {
this.define = define;
};
async onRequest(req:AddonRequestHandleReq) {
async onRequest(req: AddonRequestHandleReq) {
if (!req.action) {
throw new Error("action is required");
}
@@ -147,10 +126,8 @@ export abstract class BaseAddon implements IAddon {
}
throw new Error(`action ${req.action} not found`);
}
}
export interface IAddonGetter {
getById<T = any>(id: any): Promise<T>;
getCommonById<T = any>(id: any): Promise<T>;
@@ -3,6 +3,36 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.42.6](https://github.com/certd/certd/compare/v1.42.5...v1.42.6) (2026-07-18)
**Note:** Version bump only for package @certd/midway-flyway-js
## [1.42.5](https://github.com/certd/certd/compare/v1.42.4...v1.42.5) (2026-07-15)
### Performance Improvements
* 给SQLITE_IOERR_WRITE增加友好报错提示,将certd:latest镜像改为certd:slim ([b91c9e4](https://github.com/certd/certd/commit/b91c9e4ea671cb359ef164e27864de1d66cba9d3))
## [1.42.4](https://github.com/certd/certd/compare/v1.42.3...v1.42.4) (2026-07-11)
**Note:** Version bump only for package @certd/midway-flyway-js
## [1.42.3](https://github.com/certd/certd/compare/v1.42.2...v1.42.3) (2026-07-08)
**Note:** Version bump only for package @certd/midway-flyway-js
## [1.42.2](https://github.com/certd/certd/compare/v1.42.1...v1.42.2) (2026-07-07)
**Note:** Version bump only for package @certd/midway-flyway-js
## [1.42.1](https://github.com/certd/certd/compare/v1.42.0...v1.42.1) (2026-07-06)
**Note:** Version bump only for package @certd/midway-flyway-js
# [1.42.0](https://github.com/certd/certd/compare/v1.41.4...v1.42.0) (2026-07-05)
**Note:** Version bump only for package @certd/midway-flyway-js
## [1.41.4](https://github.com/certd/certd/compare/v1.41.3...v1.41.4) (2026-06-14)
### Bug Fixes
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@certd/midway-flyway-js",
"version": "1.41.4",
"version": "1.42.6",
"description": "midway with flyway, sql upgrade way ",
"private": false,
"type": "module",
@@ -52,5 +52,5 @@
"typeorm": "^0.3.20",
"typescript": "^5.4.2"
},
"gitHead": "bc731e4fb119787930e816a7d57c808b1b5cd66a"
"gitHead": "246ee83015bf5589adc2a5fa3d1388c8d9a2a252"
}
+37 -32
View File
@@ -1,8 +1,8 @@
import * as path from 'path';
import * as fs from 'fs';
import { QueryRunner, Table } from 'typeorm';
import { FlywayHistory } from './entity.js';
import * as crypto from 'crypto';
import * as path from "path";
import * as fs from "fs";
import { QueryRunner, Table } from "typeorm";
import { FlywayHistory } from "./entity.js";
import * as crypto from "crypto";
/**
*
@@ -32,10 +32,10 @@ const DefaultLogger = {
},
};
let customLogger:any = null;
export function setFlywayLogger (logger: any) {
let customLogger: any = null;
export function setFlywayLogger(logger: any) {
customLogger = logger;
};
}
export class Flyway {
scriptDir;
@@ -45,8 +45,8 @@ export class Flyway {
connection;
logger;
constructor(opts: any) {
this.scriptDir = opts.scriptDir ?? 'db/migration';
this.flywayTableName = opts.flywayTableName ?? 'flyway_history';
this.scriptDir = opts.scriptDir ?? "db/migration";
this.flywayTableName = opts.flywayTableName ?? "flyway_history";
this.baseline = opts.baseline ?? false;
this.allowHashNotMatch = opts.allowHashNotMatch ?? false;
this.logger = customLogger || opts.logger || DefaultLogger;
@@ -54,9 +54,9 @@ export class Flyway {
}
async run(ignores?: (RegExp | string)[]) {
this.logger.info('[ midfly ] start-------------');
this.logger.info("[ midfly ] start-------------");
if (!fs.existsSync(this.scriptDir)) {
this.logger.info('[ midfly ] scriptDir<' + this.scriptDir + '> not found');
this.logger.info("[ midfly ] scriptDir<" + this.scriptDir + "> not found");
return;
}
@@ -77,7 +77,7 @@ export class Flyway {
continue;
}
if (!file.isBaseline) {
this.logger.info('need exec script file: ', file.script);
this.logger.info("need exec script file: ", file.script);
//执行sql文件
if (/\.sql$/.test(file.script)) {
await this.execSql(filepath, queryRunner);
@@ -87,7 +87,7 @@ export class Flyway {
// await this.execJsOrTs(filepath, t);
// }
} else {
this.logger.info('baseline script file: ', file.script);
this.logger.info("baseline script file: ", file.script);
}
await this.storeSqlExecLog(file.script, filepath, true, queryRunner);
await queryRunner.commitTransaction();
@@ -95,10 +95,15 @@ export class Flyway {
this.logger.error(err);
await this.storeSqlExecLog(file.script, filepath, false, queryRunner);
await queryRunner.rollbackTransaction();
if (err.code === "SQLITE_IOERR_WRITE") {
this.logger.warn("SQLite数据库写入失败,可能您的操作系统版本太低,请将「certd:latest」镜像改为「certd:slim」即可。(如需指定版本可以修改成「certd:[version]-slim」)", file.script);
}
throw err;
}
}
this.logger.info('[ midfly ] end-------------');
this.logger.info("[ midfly ] end-------------");
}
private async storeSqlExecLog(filename: string, filepath: string, success: boolean, queryRunner: QueryRunner) {
@@ -160,17 +165,17 @@ export class Flyway {
name: this.flywayTableName,
columns: [
{
name: 'id',
name: "id",
type: this.connection.driver.normalizeType({
type: this.connection.driver.mappedDataTypes.migrationId,
}),
isGenerated: true,
generationStrategy: 'increment',
generationStrategy: "increment",
isPrimary: true,
isNullable: false,
},
{
name: 'timestamp',
name: "timestamp",
type: this.connection.driver.normalizeType({
type: this.connection.driver.mappedDataTypes.migrationTimestamp,
}),
@@ -178,23 +183,23 @@ export class Flyway {
isNullable: false,
},
{
name: 'name',
name: "name",
type: this.connection.driver.normalizeType({
type: this.connection.driver.mappedDataTypes.migrationName,
}),
isNullable: false,
},
{
name: 'hash',
name: "hash",
type: this.connection.driver.normalizeType({
type: this.connection.driver.mappedDataTypes.migrationName,
}),
isNullable: true,
},
{
name: 'success',
name: "success",
type: this.connection.driver.normalizeType({
type: 'boolean',
type: "boolean",
}),
isNullable: true,
},
@@ -210,7 +215,7 @@ export class Flyway {
}
let ret = false;
for (const ignore of ignores) {
if (typeof ignore === 'string' && file === ignore) {
if (typeof ignore === "string" && file === ignore) {
ret = true;
break;
}
@@ -233,20 +238,20 @@ export class Flyway {
if (history.hash !== hash && this.allowHashNotMatch === false) {
throw new Error(file + `hash conflict ,old: ${history.hash} != new: ${hash}`);
}
this.logger.info('[ midfly ] script<' + file + '> already executed');
this.logger.info("[ midfly ] script<" + file + "> already executed");
return true;
}
this.logger.info('[ midfly ] script<' + file + '> not yet execute');
this.logger.info("[ midfly ] script<" + file + "> not yet execute");
return false;
}
private async getFileHash(filepath: string) {
const content = fs.readFileSync(filepath).toString();
return crypto.createHash('md5').update(content.toString()).digest('hex');
return crypto.createHash("md5").update(content.toString()).digest("hex");
}
private async execSql(filepath: string, queryRunner: QueryRunner) {
this.logger.info('[ midfly ] exec ', filepath);
this.logger.info("[ midfly ] exec ", filepath);
const content = fs.readFileSync(filepath).toString().trim();
const arr = this.splitSql2Array(content);
for (const s of arr) {
@@ -255,11 +260,11 @@ export class Flyway {
}
private async execOnePart(sql: string, queryRunner: QueryRunner) {
this.logger.debug('exec sql index: ', sql);
this.logger.debug("exec sql index: ", sql);
try {
await queryRunner.query(sql);
} catch (err: any) {
this.logger.error('exec sql error ', err.message, err);
this.logger.error("exec sql error ", err.message, err);
throw err;
}
}
@@ -275,11 +280,11 @@ export class Flyway {
const temp = String(str).trim();
if (temp === 'null') {
if (temp === "null") {
return [];
}
const semicolon = ';';
const semicolon = ";";
const deepChars = ['"', "'"];
const splits = [];
@@ -289,7 +294,7 @@ export class Flyway {
if (deepChars.indexOf(charAt) >= 0) {
//如果是深度char
if (i !== 0 && temp.charAt(i - 1) === '\\') {
if (i !== 0 && temp.charAt(i - 1) === "\\") {
//如果前一个是转义字符,忽略它
} else {
//说明需要进出深度了
+28
View File
@@ -3,6 +3,34 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.42.6](https://github.com/certd/certd/compare/v1.42.5...v1.42.6) (2026-07-18)
**Note:** Version bump only for package @certd/plugin-cert
## [1.42.5](https://github.com/certd/certd/compare/v1.42.4...v1.42.5) (2026-07-15)
**Note:** Version bump only for package @certd/plugin-cert
## [1.42.4](https://github.com/certd/certd/compare/v1.42.3...v1.42.4) (2026-07-11)
**Note:** Version bump only for package @certd/plugin-cert
## [1.42.3](https://github.com/certd/certd/compare/v1.42.2...v1.42.3) (2026-07-08)
**Note:** Version bump only for package @certd/plugin-cert
## [1.42.2](https://github.com/certd/certd/compare/v1.42.1...v1.42.2) (2026-07-07)
**Note:** Version bump only for package @certd/plugin-cert
## [1.42.1](https://github.com/certd/certd/compare/v1.42.0...v1.42.1) (2026-07-06)
**Note:** Version bump only for package @certd/plugin-cert
# [1.42.0](https://github.com/certd/certd/compare/v1.41.4...v1.42.0) (2026-07-05)
**Note:** Version bump only for package @certd/plugin-cert
## [1.41.4](https://github.com/certd/certd/compare/v1.41.3...v1.41.4) (2026-06-14)
**Note:** Version bump only for package @certd/plugin-cert
+3 -3
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/plugin-cert",
"private": false,
"version": "1.41.4",
"version": "1.42.6",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -20,7 +20,7 @@
"lint": "eslint --fix"
},
"dependencies": {
"@certd/plugin-lib": "^1.41.4"
"@certd/plugin-lib": "^1.42.6"
},
"devDependencies": {
"@types/chai": "^4.3.12",
@@ -38,5 +38,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "bc731e4fb119787930e816a7d57c808b1b5cd66a"
"gitHead": "246ee83015bf5589adc2a5fa3d1388c8d9a2a252"
}
+45
View File
@@ -3,6 +3,51 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.42.6](https://github.com/certd/certd/compare/v1.42.5...v1.42.6) (2026-07-18)
**Note:** Version bump only for package @certd/plugin-lib
## [1.42.5](https://github.com/certd/certd/compare/v1.42.4...v1.42.5) (2026-07-15)
### Bug Fixes
* 修复上传到cos报runtimeDepsService未初始化的问题 ([167b303](https://github.com/certd/certd/commit/167b303faeca02cc11cf97e4be2a3df914852167))
## [1.42.4](https://github.com/certd/certd/compare/v1.42.3...v1.42.4) (2026-07-11)
### Bug Fixes
* **pipeline:** 重构运行时依赖加载逻辑,修复火山引擎DNS解析报runtimeDepsService未初始化的bug ([ec69b8f](https://github.com/certd/certd/commit/ec69b8f11bfd4b20991aef74a72a47182ca79a9d))
## [1.42.3](https://github.com/certd/certd/compare/v1.42.2...v1.42.3) (2026-07-08)
**Note:** Version bump only for package @certd/plugin-lib
## [1.42.2](https://github.com/certd/certd/compare/v1.42.1...v1.42.2) (2026-07-07)
**Note:** Version bump only for package @certd/plugin-lib
## [1.42.1](https://github.com/certd/certd/compare/v1.42.0...v1.42.1) (2026-07-06)
### Performance Improvements
* **certd-server:** 使用 jks-go转换jks证书,大幅精简镜像大小 ([c78898e](https://github.com/certd/certd/commit/c78898e4c10dd1701467d2e42e3f72bd8f2a352f))
# [1.42.0](https://github.com/certd/certd/compare/v1.41.4...v1.42.0) (2026-07-05)
### Bug Fixes
* 修复jdk证书格式的问题 ([260f5ae](https://github.com/certd/certd/commit/260f5ae777b83493b0c578fe30fd00ec0c873226))
### Features
* 通过插件配置懒加载依赖,动态加载第三方依赖包,精简安装镜像大小 ([01568ca](https://github.com/certd/certd/commit/01568ca1489069046b5a89ebdd4ced2f7f6ddf93))
### Performance Improvements
* 【破坏性更新】 证书压缩包不再生成文件存储,而是实时打包下载,证书申请插件不再输出certZip ([7cff1a9](https://github.com/certd/certd/commit/7cff1a98424120585205889874b3ef4956a30583))
* 新增橙域网络(asia-isp) CDN证书部署插件 ([b48831e](https://github.com/certd/certd/commit/b48831e60b0059bef7ef9a34ab61c9dd2f684641))
## [1.41.4](https://github.com/certd/certd/compare/v1.41.3...v1.41.4) (2026-06-14)
**Note:** Version bump only for package @certd/plugin-lib
+6 -6
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/plugin-lib",
"private": false,
"version": "1.41.4",
"version": "1.42.6",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -17,9 +17,9 @@
"lint": "eslint --fix"
},
"dependencies": {
"@certd/acme-client": "^1.41.4",
"@certd/basic": "^1.41.4",
"@certd/pipeline": "^1.41.4",
"@certd/acme-client": "^1.42.6",
"@certd/basic": "^1.42.6",
"@certd/pipeline": "^1.42.6",
"dayjs": "^1.11.7",
"jszip": "^3.10.1",
"lodash-es": "^4.17.21",
@@ -27,7 +27,6 @@
"punycode.js": "^2.3.1"
},
"devDependencies": {
"rimraf": "^5.0.5",
"@types/chai": "^4.3.12",
"@types/mocha": "^10.0.6",
"@typescript-eslint/eslint-plugin": "^8.26.1",
@@ -41,9 +40,10 @@
"mocha": "^10.6.0",
"node-forge": "^1.3.1",
"prettier": "3.3.3",
"rimraf": "^5.0.5",
"ts-node": "^10.9.2",
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "bc731e4fb119787930e816a7d57c808b1b5cd66a"
"gitHead": "246ee83015bf5589adc2a5fa3d1388c8d9a2a252"
}
+88 -13
View File
@@ -1,4 +1,4 @@
import { ILogger, sp } from "@certd/basic";
import { ILogger, sp, http } from "@certd/basic";
import type { CertInfo } from "./cert-reader.js";
import { CertReader, CertReaderHandleContext } from "./cert-reader.js";
import path from "path";
@@ -52,6 +52,81 @@ export class CertConverter {
});
}
async getJksGoPath(): Promise<string> {
const osType = process.platform === "win32" ? "windows" : "linux";
const jksGoDir = path.resolve("./tools/jks-go");
const JKS_GO_VERSION = process.env.JKS_GO_VERSION || "1.0.0";
const versionFile = path.join(jksGoDir, "version");
const finalPath = path.join(jksGoDir, osType === "windows" ? "jks-go.exe" : "jks-go");
let needDownload = false;
if (!fs.existsSync(finalPath)) {
needDownload = true;
} else if (!fs.existsSync(versionFile)) {
needDownload = true;
} else {
const currentVersion = fs.readFileSync(versionFile, "utf-8").trim();
if (currentVersion !== JKS_GO_VERSION) {
this.logger.info(`jks-go版本不匹配,当前版本:${currentVersion},期望版本:${JKS_GO_VERSION},准备重新下载`);
needDownload = true;
}
}
if (!needDownload) {
return finalPath;
}
if (!fs.existsSync(jksGoDir)) {
fs.mkdirSync(jksGoDir, { recursive: true });
}
const arch = process.arch;
let platformArch = "amd64";
if (arch === "arm64") {
platformArch = "arm64";
} else if (arch === "arm") {
platformArch = "arm_armv7";
}
let jksGoFileName = `jks-go_${osType}_${platformArch}`;
if (osType === "windows") {
jksGoFileName += ".exe";
}
const jksGoFilePath = path.join(jksGoDir, jksGoFileName);
this.logger.info(`jks-go文件不存在或版本不匹配,准备下载:${jksGoFileName}`);
const downloadUrl = `https://atomgit.com/certd/jks-go/releases/download/v${JKS_GO_VERSION}/${jksGoFileName}`;
// https://atomgit.com/certd/jks-go/releases/download/v1.0.2/jks-go_linux_amd64
const response = await http.request({
url: downloadUrl,
method: "GET",
responseType: "arraybuffer",
logRes: false,
logParams: false,
logData: false,
});
const buffer = Buffer.from(response);
fs.writeFileSync(jksGoFilePath, buffer);
this.logger.info("下载jks-go成功");
if (fs.existsSync(finalPath)) {
fs.unlinkSync(finalPath);
}
fs.copyFileSync(jksGoFilePath, finalPath);
if (osType === "linux") {
await sp.spawn({
cmd: `chmod +x ${finalPath}`,
});
}
fs.writeFileSync(versionFile, JKS_GO_VERSION, "utf-8");
this.logger.info(`jks-go版本已更新为:${JKS_GO_VERSION}`);
return finalPath;
}
private async convertPfx(opts: CertReaderHandleContext, pfxPassword: string, pfxArgs: string) {
const { tmpCrtPath, tmpKeyPath } = opts;
@@ -118,22 +193,22 @@ export class CertConverter {
const jksPassword = pfxPassword || "123456";
try {
const randomStr = Math.floor(Math.random() * 1000000) + "";
const { tmpOnePath } = opts;
const p12Path = path.join(os.tmpdir(), "/certd/tmp/", randomStr + `_cert.p12`);
const { tmpCrtPath, tmpKeyPath } = opts;
let passwordArg = "-passout pass:";
if (jksPassword) {
passwordArg = `-password pass:${jksPassword}`;
}
await this.exec(`openssl pkcs12 -export -in ${tmpCrtPath} -inkey ${tmpKeyPath} -out ${p12Path} -name certd ${passwordArg}`);
const jksPath = path.join(os.tmpdir(), "/certd/tmp/", randomStr + `_cert.jks`);
const dir = path.dirname(jksPath);
const bundlePath = path.join(os.tmpdir(), "/certd/tmp/", randomStr + `_bundle.pem`);
const dir = path.dirname(bundlePath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
await this.exec(`keytool -importkeystore -srckeystore ${p12Path} -srcstoretype PKCS12 -srcstorepass "${jksPassword}" -destkeystore ${jksPath} -deststoretype JKS -deststorepass "${jksPassword}" `);
fs.unlinkSync(p12Path);
const crtContent = fs.readFileSync(tmpOnePath);
fs.writeFileSync(bundlePath, crtContent);
const jksPath = path.join(os.tmpdir(), "/certd/tmp/", randomStr + `_cert.jks`);
const jksGoPath = await this.getJksGoPath();
await this.exec(`${jksGoPath} -importkeystore -srckeystore ${bundlePath} -srcstoretype PEM -destkeystore ${jksPath} -deststorepass "${jksPassword}"`);
fs.unlinkSync(bundlePath);
const fileBuffer = fs.readFileSync(jksPath);
const certBase64 = fileBuffer.toString("base64");
@@ -1,5 +1,5 @@
import { HttpClient, ILogger } from "@certd/basic";
import { IAccessService, IRuntimeDepsService, PageRes, PageSearch } from "@certd/pipeline";
import { IAccessService, PageRes, PageSearch, getRuntimeDepsService } from "@certd/pipeline";
import punycode from "punycode.js";
import { CreateRecordOptions, DnsProviderContext, DnsProviderDefine, DnsResolveRecord, DomainRecord, IDnsProvider, RemoveRecordOptions } from "./api.js";
import { dnsProviderRegistry } from "./registry.js";
@@ -7,13 +7,9 @@ export abstract class AbstractDnsProvider<T = any> implements IDnsProvider<T> {
ctx!: DnsProviderContext;
http!: HttpClient;
logger!: ILogger;
runtimeDepsService?: IRuntimeDepsService;
async importRuntime(specifier: string) {
if (!this.runtimeDepsService) {
throw new Error("runtimeDepsService 未初始化");
}
return await this.runtimeDepsService.importRuntime(specifier, this.logger);
return await getRuntimeDepsService().importRuntime(specifier, this.logger);
}
usePunyCode(): boolean {
@@ -42,12 +38,6 @@ export abstract class AbstractDnsProvider<T = any> implements IDnsProvider<T> {
this.ctx = ctx;
this.logger = ctx.logger;
this.http = ctx.http;
if (!this.runtimeDepsService && this.ctx.serviceGetter) {
this.runtimeDepsService = await this.ctx.serviceGetter.get("runtimeDepsService");
}
if (this.runtimeDepsService && this.ctx.define?.name) {
await this.runtimeDepsService.ensureRuntimeDependencies({ pluginKeys: `dnsProvider:${this.ctx.define.name}`, logger: this.logger });
}
}
async parseDomain(fullDomain: string) {
+30 -8
View File
@@ -18,11 +18,14 @@ COPY . /workspace/
RUN npm install -g pnpm@10.33.4
RUN cp /workspace/certd-client/dist/* /workspace/certd-server/public/ -rf
RUN cd /workspace/certd-server && pnpm install --production && npm run build-on-docker
RUN cd /workspace/certd-server && pnpm install && npm run build-on-docker
RUN rm -rf /workspace/certd-server/node_modules
ARG base_type=alpine
# ------------------------------------------------------------------
# 构建生产环境镜像
# ------------------------------------------------------------------
FROM base-${TARGETARCH}${TARGETVARIANT:+-}${TARGETVARIANT}-${base_type}
EXPOSE 7001
EXPOSE 7002
@@ -35,21 +38,16 @@ RUN if [ -f /etc/debian_version ]; then \
apt-get update \
&& apt-get install -y --no-install-recommends \
ca-certificates \
gnupg \
wget \
openssl \
netcat-openbsd \
iputils-ping \
dnsutils \
iproute2 \
&& wget -O - https://packages.adoptium.net/artifactory/api/gpg/key/public | gpg --dearmor | tee /usr/share/keyrings/adoptium.gpg > /dev/null \
&& echo "deb [signed-by=/usr/share/keyrings/adoptium.gpg] https://packages.adoptium.net/artifactory/deb bookworm main" | tee /etc/apt/sources.list.d/adoptium.list \
&& apt-get update \
&& apt-get install -y --no-install-recommends temurin-8-jre \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*; \
elif [ -f /etc/alpine-release ]; then \
apk add --no-cache openssl openjdk8-jre; \
apk add --no-cache openssl wget ca-certificates; \
else \
echo "Unsupported base image"; exit 1; \
fi
@@ -60,6 +58,9 @@ ENV TERM=xterm
ENV LEGO_VERSION=4.30.1
ENV LEGO_DOWNLOAD_DIR=/app/tools/lego
ENV JKS_GO_VERSION=1.0.3
ENV JKS_GO_DOWNLOAD_DIR=/app/tools/jks-go
ENV ALIYUN_CLIENT_CONNECT_TIMEOUT=10000
ENV ALIYUN_CLIENT_READ_TIMEOUT=20000
@@ -76,6 +77,26 @@ RUN ARCH=$(uname -m) && \
echo "Unsupported architecture: $ARCH"; \
fi
RUN mkdir -p $JKS_GO_DOWNLOAD_DIR
# 根据架构下载jks-go
RUN ARCH=$(uname -m) && \
if [ "$ARCH" = "x86_64" ]; then \
wget -O $JKS_GO_DOWNLOAD_DIR/jks-go_linux_amd64 https://github.com/certd/jks-go/releases/download/v${JKS_GO_VERSION}/jks-go_linux_amd64 && \
chmod +x $JKS_GO_DOWNLOAD_DIR/jks-go_linux_amd64 && \
ln -s $JKS_GO_DOWNLOAD_DIR/jks-go_linux_amd64 /usr/local/bin/jks-go; \
elif [ "$ARCH" = "aarch64" ]; then \
wget -O $JKS_GO_DOWNLOAD_DIR/jks-go_linux_arm64 https://github.com/certd/jks-go/releases/download/v${JKS_GO_VERSION}/jks-go_linux_arm64 && \
chmod +x $JKS_GO_DOWNLOAD_DIR/jks-go_linux_arm64 && \
ln -s $JKS_GO_DOWNLOAD_DIR/jks-go_linux_arm64 /usr/local/bin/jks-go; \
elif [ "$ARCH" = "armv7l" ]; then \
wget -O $JKS_GO_DOWNLOAD_DIR/jks-go_linux_arm_armv7 https://github.com/certd/jks-go/releases/download/v${JKS_GO_VERSION}/jks-go_linux_arm_armv7 && \
chmod +x $JKS_GO_DOWNLOAD_DIR/jks-go_linux_arm_armv7 && \
ln -s $JKS_GO_DOWNLOAD_DIR/jks-go_linux_arm_armv7 /usr/local/bin/jks-go; \
else \
echo "Unsupported architecture: $ARCH"; \
fi
ENV TZ=Asia/Shanghai
ENV NODE_ENV=production
ENV MIDWAY_SERVER_ENV=production
@@ -84,5 +105,6 @@ RUN npm install -g pnpm@10.33.4
COPY --from=builder /workspace/certd-server/ /app/
RUN pnpm install --production
COPY ./patch/ssh2/*.js /app/node_modules/.pnpm/node_modules/ssh2/lib/protocol/
CMD ["node", "--optimize-for-size", "./bootstrap.js"]
+43
View File
@@ -3,6 +3,49 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.42.6](https://github.com/certd/certd/compare/v1.42.5...v1.42.6) (2026-07-18)
**Note:** Version bump only for package @certd/ui-client
## [1.42.5](https://github.com/certd/certd/compare/v1.42.4...v1.42.5) (2026-07-15)
### Bug Fixes
* 修复上传到cos报runtimeDepsService未初始化的问题 ([167b303](https://github.com/certd/certd/commit/167b303faeca02cc11cf97e4be2a3df914852167))
## [1.42.4](https://github.com/certd/certd/compare/v1.42.3...v1.42.4) (2026-07-11)
**Note:** Version bump only for package @certd/ui-client
## [1.42.3](https://github.com/certd/certd/compare/v1.42.2...v1.42.3) (2026-07-08)
**Note:** Version bump only for package @certd/ui-client
## [1.42.2](https://github.com/certd/certd/compare/v1.42.1...v1.42.2) (2026-07-07)
### Bug Fixes
* 修复企业模式下弹出邮箱绑定提醒的问题 ([8d9dad9](https://github.com/certd/certd/commit/8d9dad9c82f6f2fd3ab3040068946a33f37145b1))
## [1.42.1](https://github.com/certd/certd/compare/v1.42.0...v1.42.1) (2026-07-06)
### Bug Fixes
* **login:** 修复输入法 composing 状态下回车触发提交的问题 ([b74db81](https://github.com/certd/certd/commit/b74db81304bbe68476bbec5ea4307a2264060e92))
### Performance Improvements
* **pipeline:** 将默认历史保留条数从30调整为100 ([d3e4677](https://github.com/certd/certd/commit/d3e4677ea4fac8e7533749d7f4187e410489e536))
# [1.42.0](https://github.com/certd/certd/compare/v1.41.4...v1.42.0) (2026-07-05)
### Performance Improvements
* 【破坏性更新】 证书压缩包不再生成文件存储,而是实时打包下载,证书申请插件不再输出certZip ([7cff1a9](https://github.com/certd/certd/commit/7cff1a98424120585205889874b3ef4956a30583))
* 优化用户体验,首次访问时弹出邮箱账号绑定用以初始化账号 ([608cc2a](https://github.com/certd/certd/commit/608cc2a81ff0b4872c9fe11ed9c9c0b4b90a12a3))
* **passkey:** passkey支持多域名rpid ([79f6586](https://github.com/certd/certd/commit/79f65868ca0f5162bbc2f935ce89abc28011d816))
* **plugin:** 在线插件编辑支持配置第三方依赖和插件依赖 ([635f069](https://github.com/certd/certd/commit/635f069012d4193cfb7cb051c96e28eec1247ca2))
## [1.41.4](https://github.com/certd/certd/compare/v1.41.3...v1.41.4) (2026-06-14)
### Bug Fixes
+3 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@certd/ui-client",
"version": "1.41.4",
"version": "1.42.6",
"private": true,
"scripts": {
"dev": "vite --open",
@@ -59,7 +59,6 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"core-js": "^3.36.0",
"cos-js-sdk-v5": "^1.7.0",
"cron-parser": "^4.9.0",
"cropperjs": "^1.6.1",
"cssnano": "^7.0.6",
@@ -105,8 +104,8 @@
"zod-defaults": "^0.1.3"
},
"devDependencies": {
"@certd/lib-iframe": "^1.41.4",
"@certd/pipeline": "^1.41.4",
"@certd/lib-iframe": "^1.42.6",
"@certd/pipeline": "^1.42.6",
"@rollup/plugin-commonjs": "^25.0.7",
"@rollup/plugin-node-resolve": "^15.2.3",
"@types/chai": "^4.3.12",
@@ -26,9 +26,10 @@ export default {
store: "Store",
version: "Version",
pluginDependencies: "Plugin Dependencies",
pluginDependenciesHelper: "Format: [author/]pluginName[:version]. Required plugins must be installed first",
pluginDependenciesHelper:
"Format: pluginType:pluginName, use * for version\nSupported: plugin:name, access:name, notification:name, dnsProvider:name, addon:subtype:name\nExample: access:AliyunAccess, plugin:DeployToAliyunCDN",
thirdPartyDependencies: "Third-party Dependencies",
thirdPartyDependenciesHelper: "Format: npmPackageName: version. Auto-installed at runtime",
thirdPartyDependenciesHelper: "Format: npmPackageName: version. Auto-installed at runtime\nExample: aliyun-sdk: ^1.0.0",
editableRunStrategy: "Editable Run Strategy",
editable: "Editable",
notEditable: "Not Editable",
@@ -70,7 +70,7 @@ export default {
confirmToggleStatus: "确定要{action}吗?",
batchDelete: "批量删除",
sourcee: "来源",
clickToToggle: "点击切换启用/禁用",
clickToToggle: "点击启用/禁用",
nickName: "昵称",
avatar: "头像",
expires: "过期",
@@ -26,9 +26,9 @@ export default {
store: "市场",
version: "版本",
pluginDependencies: "插件依赖",
pluginDependenciesHelper: "格式: [作者/]插件名[:版本],需先安装依赖插件",
pluginDependenciesHelper: "格式: 插件类型:插件名,版本号填 *\n支持: plugin:name、access:name、notification:name、dnsProvider:name、addon:subtype:name\n示例: access:AliyunAccess, plugin:DeployToAliyunCDN",
thirdPartyDependencies: "第三方依赖",
thirdPartyDependenciesHelper: "格式: npm包名: 版本号,运行时自动安装",
thirdPartyDependenciesHelper: "格式: npm包名: 版本号,运行时自动安装\n示例: aliyun-sdk: ^1.0.0",
editableRunStrategy: "可编辑运行策略",
editable: "可编辑",
notEditable: "不可编辑",
@@ -31,6 +31,13 @@ export function useFormDialog() {
crudOptions: {
columns: req.columns,
form: {
labelCol: {
// @ts-ignore
span: null,
style: {
width: "100px",
},
},
initialForm: req.initialForm,
wrapper: warpper,
async afterSubmit() {},
@@ -44,7 +51,7 @@ export function useFormDialog() {
};
}
const { crudOptions } = createCrudOptions();
await openCrudFormDialog({ crudOptions });
return await openCrudFormDialog({ crudOptions });
}
return {
openFormDialog,
@@ -73,6 +73,14 @@ async function handleSubmit() {
}
}
function handleKeydownEnter(e: KeyboardEvent) {
if (e.isComposing) {
return;
}
e.preventDefault();
handleSubmit();
}
function handleGo(path: string) {
router.push(path);
}
@@ -89,7 +97,7 @@ defineExpose({
</script>
<template>
<div @keydown.enter.prevent="handleSubmit">
<div @keydown.enter="handleKeydownEnter">
<slot name="title">
<Title>
<slot name="title">
@@ -16,8 +16,8 @@
</div>
</template>
<script>
import { defineComponent, reactive, ref, watch, inject } from "vue";
<script lang="ts">
import { defineComponent, reactive, ref, watch, inject, onMounted } from "vue";
import CertAccessModal from "./access/index.vue";
import { createAccessApi } from "../api";
import { message } from "ant-design-vue";
@@ -55,6 +55,10 @@ export default defineComponent({
type: Boolean,
default: false,
},
defaultSelect: {
type: Boolean,
default: false,
},
},
emits: ["update:modelValue", "change", "selectedChange"],
setup(props, ctx) {
@@ -158,12 +162,43 @@ export default defineComponent({
},
});
async function selectFirst(clearCurrent = false) {
if (!clearCurrent && props.modelValue) {
return;
}
const searchForm = projectStore.getSearchForm();
const query: any = {
query: {
type: props.type,
...searchForm,
},
page: { page: 1, pageSize: 1 },
sort: { prop: "id", order: "ascending" },
};
if (props.subtype) {
query.query.subtype = props.subtype;
}
const res = await api.GetList(query);
const records = res?.records || [];
if (records.length > 0) {
await emitValue(records[0].id);
}
}
onMounted(async () => {
if (!props.defaultSelect) {
return;
}
await selectFirst();
});
return {
clear,
target,
selectedId,
providerDefine,
chooseForm,
selectFirst,
};
},
});
@@ -18,6 +18,10 @@ defineProps<{
showButton: boolean;
}>();
const emit = defineEmits<{
(e: "close"): void;
}>();
let passwordFormRef = ref();
type OpenOptions = {
@@ -68,8 +72,8 @@ const passwordFormOptions: CrudOptions = {
},
async afterSubmit() {
const formData = passwordFormRef.value?.getFormData?.();
const message = formData?.init ? t("authentication.initPasswordSuccessMessage") : t("authentication.successMessage");
notification.success({ message });
const msg = formData?.init ? t("authentication.initPasswordSuccessMessage") : t("authentication.successMessage");
notification.success({ message: msg });
},
},
columns: {
@@ -84,6 +88,7 @@ const passwordFormOptions: CrudOptions = {
title: t("authentication.oldPassword"),
type: "password",
form: {
//@ts-ignore
show: compute(({ form }) => form.init !== true),
rules: [{ required: true, message: t("authentication.oldPasswordRequired") }],
},
@@ -118,16 +123,18 @@ const passwordFormOptions: CrudOptions = {
async function open(opts: OpenOptions = {}) {
const formOptions = buildFormOptions(passwordFormOptions);
formOptions.newInstance = true; //
formOptions.newInstance = true;
if (opts.init) {
formOptions.wrapper.title = t("authentication.initPasswordTitle");
}
formOptions.wrapper.onClosed = () => {
emit("close");
};
passwordFormRef.value = await openDialog(formOptions);
passwordFormRef.value.setFormData({
init: opts.init === true,
password: opts.password || "",
});
console.log(passwordFormRef.value);
}
const scope = ref({
@@ -107,6 +107,10 @@
<div class="passkey-info">
<div class="passkey-name">{{ passkey.deviceName }}</div>
<div class="passkey-meta flex items-center">
<span class="meta-item flex items-center">
<fs-icon icon="ion:globe-outline" class="meta-icon" />
{{ passkey.rpId || "-" }}
</span>
<span class="meta-item flex items-center">
<fs-icon icon="ion:calendar-outline" class="meta-icon" />
{{ formatDate(passkey.registeredAt) }}
@@ -454,6 +458,8 @@ onMounted(async () => {
}
.card-header {
background: linear-gradient(145deg, #1e1e1e, #252525);
.header-bg-gradient {
background: rgba(255, 255, 255, 0.04);
opacity: 1;
@@ -472,6 +478,7 @@ onMounted(async () => {
.detail-tag {
background: #3b3b3b;
border-color: rgba(255, 255, 255, 0.12);
color: #e5e5e5;
.tag-icon {
@@ -480,6 +487,23 @@ onMounted(async () => {
}
}
.card-title {
border-bottom-color: rgba(255, 255, 255, 0.1);
}
.binding-icon {
background: linear-gradient(135deg, rgba(102, 126, 234, 0.22) 0%, rgba(160, 120, 234, 0.22) 100%);
}
.passkey-icon {
background: linear-gradient(135deg, rgba(17, 153, 142, 0.22) 0%, rgba(56, 239, 125, 0.22) 100%);
}
.binding-icon .icon,
.passkey-icon .icon {
color: rgba(255, 255, 255, 0.7);
}
.bindings-list {
.binding-item {
background: #2d2d2d;
@@ -207,7 +207,7 @@ export function useCertUpload() {
const { id } = await api.Save({
title: pipeline.title,
content: JSON.stringify(pipeline),
keepHistoryCount: 30,
keepHistoryCount: 100,
type: "cert_upload",
groupId: form.groupId,
});
@@ -605,7 +605,7 @@ export function useCertPipelineCreator({ formWrapperRef }: { formWrapperRef: Ref
const { id } = await api.Save({
title: pipeline.title,
content: JSON.stringify(pipeline),
keepHistoryCount: 30,
keepHistoryCount: 100,
type: "cert",
groupId,
addToMonitorEnabled: form.addToMonitorEnabled,
@@ -160,7 +160,7 @@ export async function createPipelineByTemplate(opts: { templateId: number; title
return await templateApi.CreatePipelineByTemplate({
title,
content: JSON.stringify(pipeline),
keepHistoryCount: keepHistoryCount ?? 30,
keepHistoryCount: keepHistoryCount ?? 100,
groupId,
templateId,
});
@@ -2,22 +2,116 @@
<fs-page class="home—index bg-neutral-100 dark:bg-black">
<!-- <page-content />-->
<dashboard-user />
<change-password-button ref="changePasswordButtonRef" :show-button="false"></change-password-button>
<change-password-button ref="changePasswordButtonRef" :show-button="false" @close="checkAndSetupAccount"></change-password-button>
</fs-page>
</template>
<script lang="ts" setup>
<script lang="tsx" setup>
import DashboardUser from "./dashboard/index.vue";
import { useUserStore } from "/@/store/user";
import ChangePasswordButton from "/@/views/certd/mine/change-password-button.vue";
import { onMounted, ref } from "vue";
import { Modal } from "ant-design-vue";
import { Modal, notification } from "ant-design-vue";
import { useI18n } from "/src/locales";
import { request } from "/@/api/service";
import { useFormDialog } from "/@/use/use-dialog";
import { useSettingStore } from "/@/store/settings/index.jsx";
const { t } = useI18n();
const { openFormDialog } = useFormDialog();
const userStore = useUserStore();
const settingStore = useSettingStore();
const changePasswordButtonRef = ref();
const emailFormWrapperRef = ref<any>();
const validateEmailConfirm = async (_rule: any, value: string) => {
if (!value) {
return;
}
const formData = emailFormWrapperRef.value?.getFormData?.();
if (formData && value !== formData.email) {
throw new Error("两次输入的邮箱地址不一致");
}
};
async function checkAndSetupAccount() {
if (settingStore.isEnterprise) {
return;
}
try {
const userInfo = userStore.getUserInfo as any;
if (!userInfo.needInitAccount) {
return;
}
if (userInfo.email) {
await request({
url: "/mine/accountInit",
method: "post",
});
return;
}
emailFormWrapperRef.value = await openFormDialog({
title: "绑定邮箱",
wrapper: {
width: 560,
},
initialForm: { email: "", emailConfirm: "" },
async onSubmit(form: any) {
await request({
url: "/mine/accountInit",
method: "post",
data: { email: form.email },
});
notification.success({
message: "邮箱绑定成功",
});
},
body: () => {
return <a-alert class="mb-4" message="为保证用户体验,请先绑定邮箱,初始化您的账号" type="success" show-icon></a-alert>;
},
columns: {
email: {
title: "邮箱",
type: "text",
form: {
col: { span: 24 },
component: {
placeholder: "请输入邮箱地址",
},
helper: "请输入您的邮箱",
rules: [
{ required: true, message: "请输入邮箱地址" },
{ type: "email", message: "请输入有效的邮箱地址" },
],
},
},
emailConfirm: {
title: "确认邮箱",
type: "text",
form: {
col: { span: 24 },
component: {
placeholder: "请再次输入邮箱地址",
},
helper: "请再次输入邮箱,以确认邮箱地址无误",
rules: [
{ required: true, message: "请再次输入邮箱地址" },
{ type: "email", message: "请输入有效的邮箱地址" },
{ validator: validateEmailConfirm, trigger: "blur" },
],
},
},
},
});
} catch (e) {
console.error("AcmeAccount setup failed:", e);
}
}
onMounted(() => {
if (userStore.getUserInfo.isWeak === true) {
Modal.info({
@@ -30,6 +124,9 @@ onMounted(() => {
},
okText: t("authentication.changeNow"),
});
} else {
//
checkAndSetupAccount();
}
});
</script>
@@ -450,7 +450,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
},
},
disabled: {
title: t("certd.enableDisable"),
title: t("certd.clickToToggle"),
type: "dict-switch",
dict: dict({
data: [
+1
View File
@@ -1,2 +1,3 @@
LEGO_VERSION=4.30.1
JKS_GO_VERSION=1.0.3
certd_plugin_loadmode=dev
-26
View File
@@ -1,26 +0,0 @@
{
"parser": "@typescript-eslint/parser",
"plugins": [
"@typescript-eslint"
],
"ignorePatterns": ["dist"],
"extends": [
"plugin:@typescript-eslint/recommended",
"plugin:prettier/recommended",
"prettier"
],
"env": {
"mocha": true
},
"rules": {
"@typescript-eslint/no-var-requires": "off",
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/ban-ts-ignore": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/no-this-alias": "off",
// 允许any
"@typescript-eslint/no-unsafe-anyassignment": "off"
}
}
+1
View File
@@ -20,6 +20,7 @@ run/
.env.pgpl.yaml
tools/lego/*
tools/jks-go
!tools/lego/readme.md
test.mjs
isolate-*.log
+76
View File
@@ -3,6 +3,82 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.42.6](https://github.com/certd/certd/compare/v1.42.5...v1.42.6) (2026-07-18)
### Bug Fixes
* 修复正常批量删除流水线报权限不足的bug ([5b50083](https://github.com/certd/certd/commit/5b500830a122c6c42dab054e57fed509050f94da))
## [1.42.5](https://github.com/certd/certd/compare/v1.42.4...v1.42.5) (2026-07-15)
### Bug Fixes
* 修复上传到cos报runtimeDepsService未初始化的问题 ([167b303](https://github.com/certd/certd/commit/167b303faeca02cc11cf97e4be2a3df914852167))
* 修复dingtalk通知格式没有换行的bug ([7ed1be9](https://github.com/certd/certd/commit/7ed1be994f8b4b74cdeb38743060c912c027248b))
### Performance Improvements
* 优化vke keubconfig获取方式,改成先查询,如果没有再创建临时config ([604fa5b](https://github.com/certd/certd/commit/604fa5be634d099d797bfee5c2b0f26ce0ac8461))
## [1.42.4](https://github.com/certd/certd/compare/v1.42.3...v1.42.4) (2026-07-11)
### Bug Fixes
* 修复火山引擎查不到自定义源站域名的问题 ([02dabe1](https://github.com/certd/certd/commit/02dabe11db3e9b13ca4621ce9ddd2b808bfca390))
* 修复火山引擎自定义源站域名查询不到的问题 ([e44bf9d](https://github.com/certd/certd/commit/e44bf9d77375d48ac7fd1582e69fae02dfd248fa))
* **pipeline:** 重构运行时依赖加载逻辑,修复火山引擎DNS解析报runtimeDepsService未初始化的bug ([ec69b8f](https://github.com/certd/certd/commit/ec69b8f11bfd4b20991aef74a72a47182ca79a9d))
## [1.42.3](https://github.com/certd/certd/compare/v1.42.2...v1.42.3) (2026-07-08)
### Bug Fixes
* **volcengine-alb:** 修复火山引擎ALB 默认证书部署类型会部署到扩展证书的问题 ([0a068a2](https://github.com/certd/certd/commit/0a068a274673e9768954e9f7367c267d44f3b530))
## [1.42.2](https://github.com/certd/certd/compare/v1.42.1...v1.42.2) (2026-07-07)
### Bug Fixes
* 修复火山视频点播源站选择不到自定义源站的bug ([0071bcb](https://github.com/certd/certd/commit/0071bcb0e4dd108c86d7ca01820a9f6e6960e440))
* 修复企业模式下弹出邮箱绑定提醒的问题 ([8d9dad9](https://github.com/certd/certd/commit/8d9dad9c82f6f2fd3ab3040068946a33f37145b1))
* 修复AsiaIsp CDN证书重复情况下部署失败的问题 ([c3d6db3](https://github.com/certd/certd/commit/c3d6db3f1ef2f1c897b7989521fe8809dffaded1))
* 修复cname用阿里云校验时报找不到runtimeDepsService的错误 ([072edd7](https://github.com/certd/certd/commit/072edd7affee424ab3411f4d41d338f084d7cac6))
## [1.42.1](https://github.com/certd/certd/compare/v1.42.0...v1.42.1) (2026-07-06)
### Bug Fixes
* 修复多域名无法使用passkey登录的bug ([d176f9c](https://github.com/certd/certd/commit/d176f9cc0ebd051a614bfac74d1616d1945fc9a3))
* 修复企业模式下登录报projectId不能为空的问题 ([a65366b](https://github.com/certd/certd/commit/a65366bbe1aadea8baaffbdadab58a5b631d9417))
### Performance Improvements
* **certd-server:** 使用 jks-go转换jks证书,大幅精简镜像大小 ([c78898e](https://github.com/certd/certd/commit/c78898e4c10dd1701467d2e42e3f72bd8f2a352f))
* **pipeline:** 将默认历史保留条数从30调整为100 ([d3e4677](https://github.com/certd/certd/commit/d3e4677ea4fac8e7533749d7f4187e410489e536))
# [1.42.0](https://github.com/certd/certd/compare/v1.41.4...v1.42.0) (2026-07-05)
### Bug Fixes
* 修复telegram - 符号转义问题 ([d5882f1](https://github.com/certd/certd/commit/d5882f16bedb09baf09ace92049b02872620f5dc))
* **aliyun:** 修复阿里云CDN/DCDN根据证书自动匹配不到证书的bug ([1ae185d](https://github.com/certd/certd/commit/1ae185d0bc356f4678bc38ca0582ce3396f82ebe))
### Features
* 通过插件配置懒加载依赖,动态加载第三方依赖包,精简安装镜像大小 ([01568ca](https://github.com/certd/certd/commit/01568ca1489069046b5a89ebdd4ced2f7f6ddf93))
### Performance Improvements
* 阿里云ESA证书部署支持SaaS模式 ([82276b5](https://github.com/certd/certd/commit/82276b53a8474a18a3d0237050907c994fc748f0))
* 【破坏性更新】 证书压缩包不再生成文件存储,而是实时打包下载,证书申请插件不再输出certZip ([7cff1a9](https://github.com/certd/certd/commit/7cff1a98424120585205889874b3ef4956a30583))
* 火山引擎点播插件支持部署到自定义源站域名 ([095791c](https://github.com/certd/certd/commit/095791cdc2b7c1f4b913b634643afec5e30fe9b0))
* 新增橙域网络(asia-isp) CDN证书部署插件 ([b48831e](https://github.com/certd/certd/commit/b48831e60b0059bef7ef9a34ab61c9dd2f684641))
* 优化阿里云API网关增加翻页查询 ([ed58ae3](https://github.com/certd/certd/commit/ed58ae3c5339e4a0238a92acfe7ea6d2f566ea28))
* 优化用户体验,首次访问时弹出邮箱账号绑定用以初始化账号 ([608cc2a](https://github.com/certd/certd/commit/608cc2a81ff0b4872c9fe11ed9c9c0b4b90a12a3))
* 优化ACME账号字段的选择提示 ([bfd3cac](https://github.com/certd/certd/commit/bfd3cacc687fc5cbc3cb2ca3cadbc140de300dc2))
* 支持全自动匹配部署宝塔网站证书 ([4dff48e](https://github.com/certd/certd/commit/4dff48e807c32a7623ec9206cf39c88e88f89f6a))
* **cert-plugin:** 调整更新天数自动减半逻辑,仅7天ip证书生效,其他情况下不减半 ([56e5524](https://github.com/certd/certd/commit/56e5524a0f4af3645d70bc3b3ec750b45ba8de10))
* **passkey:** passkey支持多域名rpid ([79f6586](https://github.com/certd/certd/commit/79f65868ca0f5162bbc2f935ce89abc28011d816))
## [1.41.4](https://github.com/certd/certd/compare/v1.41.3...v1.41.4) (2026-06-14)
### Bug Fixes
@@ -0,0 +1,6 @@
ALTER TABLE `sys_passkey` ADD COLUMN `rp_id` varchar(256) NULL;
DROP INDEX `index_passkey_passkey_id` ON `sys_passkey`;
ALTER TABLE `sys_passkey` ADD UNIQUE INDEX `index_passkey_passkey_id` (`passkey_id`);
@@ -0,0 +1,6 @@
ALTER TABLE "sys_passkey" ADD COLUMN "rp_id" varchar(256) NULL;
DROP INDEX "index_passkey_passkey_id";
CREATE UNIQUE INDEX "index_passkey_passkey_id" ON "sys_passkey" ("passkey_id");
@@ -0,0 +1,6 @@
ALTER TABLE "sys_passkey" ADD COLUMN "rp_id" varchar(256) NULL;
DROP INDEX "index_passkey_passkey_id";
CREATE UNIQUE INDEX "index_passkey_passkey_id" ON "sys_passkey" ("passkey_id");
@@ -74,7 +74,7 @@ input:
credentials链接,然后点击编辑按钮,查看Secret key和HMAC key
litessl:[litesslEAB页面](https://freessl.cn/automation/eab-manager),然后点击新增EAB
required: false
required: true
encrypt: true
mergeScript: |2-
@@ -92,7 +92,7 @@ input:
title: EAB HMAC Key
component:
placeholder: 需要EAB的颁发机构生成账号时填写
required: false
required: true
encrypt: true
mergeScript: |2-
@@ -6,7 +6,7 @@ name: AliyunDeployCertToESA
title: 阿里云-部署至ESA
icon: svg:icon-aliyun
group: aliyun
desc: 部署证书到阿里云ESA(边缘安全加速),自动删除过期证书
desc: 部署证书到阿里云ESA(边缘安全加速),支持边缘证书和SaaS证书两种模式
needPlus: false
input:
cert:
@@ -70,6 +70,20 @@ input:
type: aliyun
required: true
order: 0
deployMode:
title: 部署模式
value: edge
component:
name: a-radio-group
vModel: value
options:
- label: 边缘证书
value: edge
- label: SaaS证书
value: saas
helper: 边缘证书:将证书部署到站点的边缘节点;SaaS证书:将证书部署到站点的SaaS域名
required: true
order: 0
siteIds:
title: 站点
component:
@@ -99,6 +113,39 @@ input:
helper: 请选择要部署证书的站点
order: 0
saasDomainIds:
title: SaaS域名
component:
name: remote-select
vModel: value
mode: tags
type: plugin
action: onGetCustomHostnameList
search: false
pager: false
single: false
watches:
- certDomains
- accessId
- siteIds
- accessId
- regionId
required: false
mergeScript: |2-
return {
show: ctx.compute(({form})=>{
return form.deployMode === 'saas'
}),
component:{
form: ctx.compute(({form})=>{
return form
})
},
}
helper: 请选择要部署证书的SaaS域名(SaaS证书模式下必选)
order: 0
certLimit:
title: 免费证书数量限制
value: 2
@@ -0,0 +1,52 @@
showRunStrategy: false
default:
strategy:
runStrategy: 1
name: BaotaAutoDeploySiteCert
title: 宝塔-全自动部署
icon: svg:icon-bt
group: panel
desc: 根据证书域名自动匹配宝塔站点,全自动部署SSL证书。新增加速域名自动感知,自动新增部署
runStrategy: 0
needPlus: true
input:
cert:
title: 域名证书
helper: 请选择前置任务输出的域名证书
component:
name: output-selector
from:
- ':cert:'
required: true
order: 0
certDomains:
title: 当前证书域名
component:
name: cert-domains-getter
mergeScript: |2-
return {
component:{
inputKey: ctx.compute(({form})=>{
return form.cert
}),
}
}
template: false
required: false
order: 0
accessId:
title: 宝塔授权
helper: 将自动查找证书匹配的站点,检查证书即将过期的站点并更新
component:
name: access-selector
type: baota
required: true
order: 0
output:
deployedList:
title: 已部署过的站点
pluginType: deploy
type: builtIn
scriptFilePath: /plugins/plugin-plus/baota/plugins/plugin-deploy-automatch.js
@@ -314,6 +314,7 @@ input:
component:
name: access-selector
type: acmeAccount
defaultSelect: true
required: false
helper: |-
直接本地生成,无需外部注册
@@ -96,24 +96,11 @@ input:
label: 点播加速域名
- value: image
label: 封面加速域名
- value: third
label: 自定义源站
value: play
required: true
order: 0
sourceStationType:
title: 源站类型
helper: 选择源站类型
component:
name: a-select
vModel: value
options:
- value: 1
label: 点播源站
- value: 2
label: 自定义源站
value: 1
helper: 注意:封面加速域名不支持自定义源站
required: false
order: 0
domainList:
title: 域名
component:
@@ -132,7 +119,6 @@ input:
- accessId
- spaceName
- domainType
- sourceStationType
required: true
mergeScript: |2-
+25 -24
View File
@@ -1,6 +1,6 @@
{
"name": "@certd/ui-server",
"version": "1.41.4",
"version": "1.42.6",
"description": "fast-server base midway",
"private": true,
"type": "module",
@@ -25,6 +25,7 @@
"format": "prettier --write src",
"lint": "mwts fix",
"ci": "pnpm run cov",
"lint2": "cross-env NODE_ENV=production mwtsc -p tsconfig.build.json",
"build-only": "cross-env NODE_ENV=production mwtsc -p tsconfig.build.json --cleanOutDir --skipLibCheck",
"build": "pnpm run build-only && pnpm run export-metadata",
"export-metadata": "node export-plugin-yaml.js",
@@ -41,20 +42,20 @@
"lint1": "eslint --fix"
},
"dependencies": {
"@certd/acme-client": "^1.41.4",
"@certd/basic": "^1.41.4",
"@certd/commercial-core": "^1.41.4",
"@certd/acme-client": "^1.42.6",
"@certd/basic": "^1.42.6",
"@certd/commercial-core": "^1.42.6",
"@certd/cv4pve-api-javascript": "^8.4.2",
"@certd/jdcloud": "^1.41.4",
"@certd/lib-huawei": "^1.41.4",
"@certd/lib-k8s": "^1.41.4",
"@certd/lib-server": "^1.41.4",
"@certd/midway-flyway-js": "^1.41.4",
"@certd/pipeline": "^1.41.4",
"@certd/plugin-cert": "^1.41.4",
"@certd/plugin-lib": "^1.41.4",
"@certd/plugin-plus": "^1.41.4",
"@certd/plus-core": "^1.41.4",
"@certd/jdcloud": "^1.42.6",
"@certd/lib-huawei": "^1.42.6",
"@certd/lib-k8s": "^1.42.6",
"@certd/lib-server": "^1.42.6",
"@certd/midway-flyway-js": "^1.42.6",
"@certd/pipeline": "^1.42.6",
"@certd/plugin-cert": "^1.42.6",
"@certd/plugin-lib": "^1.42.6",
"@certd/plugin-plus": "^1.42.6",
"@certd/plus-core": "^1.42.6",
"@koa/cors": "^5.0.0",
"@midwayjs/bootstrap": "3.20.11",
"@midwayjs/cache": "3.14.0",
@@ -71,7 +72,6 @@
"@peculiar/x509": "^1.11.0",
"@simplewebauthn/browser": "^13.2.2",
"@simplewebauthn/server": "^13.2.3",
"alipay-sdk": "^4.13.0",
"axios": "^1.9.0",
"bcryptjs": "^2.4.3",
"better-sqlite3": "^11.1.2",
@@ -96,8 +96,6 @@
"nanoid": "^5.0.7",
"node-forge": "^1.3.1",
"nodemailer": "^6.9.16",
"openid-client": "^6.8.1",
"otplib": "^12.0.1",
"pg": "^8.12.0",
"psl": "^1.15.0",
"punycode.js": "^2.3.1",
@@ -113,13 +111,10 @@
"svg-captcha": "^1.4.0",
"typeorm": "^0.3.20",
"uuid": "^10.0.0",
"wechatpay-node-v3": "^2.2.1",
"whoiser": "2.0.0-beta.10",
"xml2js": "^0.6.2",
"mwtsc": "^1.15.1"
"xml2js": "^0.6.2"
},
"devDependencies": {
"mwts": "^1.3.0",
"@midwayjs/mock": "3.20.11",
"@types/ali-oss": "^6.16.11",
"@types/cache-manager": "^4.0.6",
@@ -131,8 +126,11 @@
"@types/nodemailer": "^6.4.8",
"c8": "^10.1.2",
"cross-env": "^7.0.3",
"eslint": "^7.32.0",
"esmock": "^2.7.5",
"mocha": "^10.6.0",
"mwts": "^1.3.0",
"mwtsc": "^1.15.1",
"prettier": "3.3.3",
"rimraf": "^5.0.5",
"ts-node": "^10.9.2",
@@ -140,7 +138,7 @@
"typescript": "^5.4.2",
"why-is-node-running": "^3.2.2"
},
"lazyDependencies": {
"lazyDependencies": {
"@alicloud/fc20230330": "^4.1.7",
"@alicloud/tea-typescript": "^1.8.0",
"@alicloud/openapi-client": "^0.4.12",
@@ -170,7 +168,11 @@
"@google-cloud/publicca": "^1.3.0",
"basic-ftp": "^5.0.5",
"esdk-obs-nodejs": "^3.25.6",
"qiniu": "^7.12.0"
"qiniu": "^7.12.0",
"alipay-sdk": "^4.13.0",
"wechatpay-node-v3": "^2.2.1",
"openid-client": "^6.8.1",
"otplib": "^12.0.1"
},
"engines": {
"node": ">=20.0.0"
@@ -182,7 +184,6 @@
"pnpm": {
"neverBuiltDependencies": []
},
"author": "anonymous",
"license": "MIT"
}
@@ -1,3 +1,4 @@
import { getRuntimeDepsService } from "@certd/pipeline";
import { ALL, Body, Controller, Inject, Post, Provide, Query, RequestIP } from "@midwayjs/core";
import { addonRegistry, AddonService, CrudController, SysPrivateSettings, SysPublicSettings, SysSafeSetting, SysSettingsEntity, SysSettingsService } from "@certd/lib-server";
import { cloneDeep, merge } from "lodash-es";
@@ -7,7 +8,6 @@ import { getEmailSettings } from "../../../modules/sys/settings/fix.js";
import { http, logger, utils } from "@certd/basic";
import { CodeService } from "../../../modules/basic/service/code-service.js";
import { SmsServiceFactory } from "../../../modules/basic/sms/factory.js";
import { RuntimeDepsService } from "../../../modules/runtime-deps/runtime-deps-service.js";
/**
*/
@@ -24,9 +24,6 @@ export class SysSettingsController extends CrudController<SysSettingsService> {
codeService: CodeService;
@Inject()
addonService: AddonService;
@Inject()
runtimeDepsService: RuntimeDepsService;
getService() {
return this.service;
}
@@ -222,7 +219,7 @@ export class SysSettingsController extends CrudController<SysSettingsService> {
@Post("/clearRuntimeDeps", { description: "sys:settings:edit" })
async clearRuntimeDeps() {
await this.runtimeDepsService.clearRuntimeDeps();
await getRuntimeDepsService().clearRuntimeDeps();
return this.ok(true);
}
}
@@ -1,10 +1,15 @@
import { BaseController, Constants, SysSettingsService } from "@certd/lib-server";
import { AccessGetter, AccessService, BaseController, Constants, isEnterprise, SysSettingsService } from "@certd/lib-server";
import { ALL, Body, Controller, Inject, Post, Provide } from "@midwayjs/core";
import { PasskeyService } from "../../../modules/login/service/passkey-service.js";
import { RoleService } from "../../../modules/sys/authority/service/role-service.js";
import { UserService } from "../../../modules/sys/authority/service/user-service.js";
import { NotificationService } from "../../../modules/pipeline/service/notification-service.js";
import { newAccess } from "@certd/pipeline";
import { http, logger, utils } from "@certd/basic";
import { ApiTags } from "@midwayjs/swagger";
import { CodeService } from "../../../modules/basic/service/code-service.js";
import { EmailService } from "../../../modules/basic/service/email-service.js";
import { TaskServiceBuilder } from "../../../modules/pipeline/service/getter/task-service-getter.js";
/**
*/
@@ -27,6 +32,18 @@ export class MineController extends BaseController {
@Inject()
sysSettingsService: SysSettingsService;
@Inject()
accessService: AccessService;
@Inject()
notificationService: NotificationService;
@Inject()
emailService: EmailService;
@Inject()
taskServiceBuilder: TaskServiceBuilder;
@Post("/info", { description: Constants.per.authOnly, summary: "查询用户信息" })
public async info() {
const userId = this.getUserId();
@@ -41,6 +58,18 @@ export class MineController extends BaseController {
delete user.password;
//@ts-ignore
user.needInitPassword = needInitPassword;
//@ts-ignore
user.needInitAccount = false;
if (!isEnterprise()) {
const existingAccess = await this.accessService.findOne({
where: { type: "acmeAccount", subtype: "letsencrypt", userId },
});
if (!existingAccess) {
//@ts-ignore
user.needInitAccount = true;
}
}
return this.ok(user);
}
@@ -122,4 +151,59 @@ export class MineController extends BaseController {
});
return this.ok({});
}
@Post("/accountInit", { description: Constants.per.authOnly, summary: "初始化Let's Encrypt ACME账号和邮件通知" })
public async accountInit(@Body("email") email?: string) {
const userId = this.getUserId();
let userEmail = email;
let user: any = null;
if (!userEmail) {
user = await this.userService.info(userId);
userEmail = user.email;
}
if (!userEmail) {
return this.ok({ needEmail: true });
}
if (email) {
if (!user) {
user = await this.userService.info(userId);
}
if (!user.email) {
await this.userService.updateEmail(userId, { email: userEmail });
}
}
await this.emailService.add(userId, userEmail);
await this.notificationService.getOrCreateDefault(userEmail, userId);
const getAccessById = this.accessService.getById.bind(this.accessService);
const accessGetter = new AccessGetter(userId, undefined, getAccessById);
const serviceGetter = this.taskServiceBuilder.create({ userId });
const accessContext = {
http,
logger,
utils,
accessService: accessGetter,
serviceGetter,
define: undefined,
} as any;
const access = await newAccess("acmeAccount", { caType: "letsencrypt", email: userEmail }, accessGetter, accessContext);
const accountJson = await access.onGenerateAccount();
await this.accessService.add({
type: "acmeAccount",
name: "Let's Encrypt",
userId,
projectId: undefined,
setting: JSON.stringify({
caType: "letsencrypt",
email: userEmail,
account: accountJson,
}),
});
return this.ok({ success: true });
}
}
@@ -69,7 +69,7 @@ export class MinePasskeyController extends BaseController {
public async getPasskeys() {
const userId = this.getUserId();
const passkeys = await this.passkeyService.find({
select: ["id", "deviceName", "registeredAt", "transports", "passkeyId", "updateTime"],
select: ["id", "deviceName", "registeredAt", "transports", "passkeyId", "rpId", "updateTime"],
where: { userId },
order: { registeredAt: "DESC" },
});
@@ -8,7 +8,6 @@ import { TaskServiceBuilder } from "../../../modules/pipeline/service/getter/tas
import { cloneDeep } from "lodash-es";
import { ApiTags } from "@midwayjs/swagger";
import { AuthService } from "../../../modules/sys/authority/service/auth-service.js";
import { RuntimeDepsService } from "../../../modules/runtime-deps/runtime-deps-service.js";
@Provide()
@Controller("/api/pi/handle")
@@ -29,9 +28,6 @@ export class HandleController extends BaseController {
@Inject()
notificationService: NotificationService;
@Inject()
runtimeDepsService: RuntimeDepsService;
@Post("/access", { description: Constants.per.authOnly, summary: "处理授权请求" })
async accessRequest(@Body(ALL) body: AccessRequestHandleReq) {
let { projectId, userId } = await this.getProjectUserIdRead();
@@ -64,12 +60,14 @@ export class HandleController extends BaseController {
}
}
const getAccessById = this.accessService.getById.bind(this.accessService);
const accessGetter = new AccessGetter(userId, projectId, getAccessById, this.runtimeDepsService);
const accessGetter = new AccessGetter(userId, projectId, getAccessById);
const serviceGetter = this.taskServiceBuilder.create({ userId, projectId });
const accessContext = {
http,
logger,
utils,
accessService: accessGetter,
serviceGetter,
define: undefined,
} as any;
const access = await newAccess(body.typeName, inputAccess, accessGetter, accessContext);
@@ -1,11 +1,11 @@
import { Autoload, Init, Inject, Scope, ScopeEnum } from "@midwayjs/core";
import { Autoload, Init, Inject, Scope, ScopeEnum } from "@midwayjs/core";
import { AutoCron } from "./auto-cron.js";
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 "./fix/auto-fix.js";
import { AutoPrint } from "./auto-print.js";
import { AutoFix } from "./fix/auto-fix.js";
@Autoload()
@Scope(ScopeEnum.Request, { allowDowngrade: true })
@@ -1,7 +1,11 @@
import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
import { Config, 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";
import { getRuntimeDepsService, initRuntimeDepsService } from "@certd/pipeline";
import { pluginRegistry, accessRegistry, notificationRegistry } from "@certd/pipeline";
import { dnsProviderRegistry } from "@certd/plugin-lib";
import { addonRegistry } from "@certd/lib-server";
@Provide()
@Scope(ScopeEnum.Request, { allowDowngrade: true })
@@ -9,6 +13,9 @@ export class AutoLoadPlugins {
@Inject()
pluginService: PluginService;
@Config("runtimeDeps")
runtimeDepsConfig: any;
async init() {
logger.info(`加载插件开始,加载模式:${process.env.certd_plugin_loadmode}`);
if (process.env.certd_plugin_loadmode === "metadata") {
@@ -30,5 +37,17 @@ export class AutoLoadPlugins {
await registerPaymentProviders();
logger.info(`加载插件完成,加载模式:${process.env.certd_plugin_loadmode}`);
//初始化第三方依赖服务
initRuntimeDepsService(this.runtimeDepsConfig, {
pluginRegistry,
accessRegistry,
notificationRegistry,
dnsProviderRegistry,
addonRegistry,
});
// 收集插件 dependPackages 并安装
const service = getRuntimeDepsService();
service.refreshPluginDeps();
}
}
@@ -1,4 +1,4 @@
import { cache, isDev, randomNumber, simpleNanoId } from "@certd/basic";
import { cache, isDev, randomNumber, simpleNanoId } from "@certd/basic";
import { AccessService, AccessSysGetter, CodeErrorException, SysSettingsService } from "@certd/lib-server";
import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
import { ISmsService } from "../sms/api.js";
@@ -6,7 +6,6 @@ import { SmsServiceFactory } from "../sms/factory.js";
import { CaptchaService } from "./captcha-service.js";
import { EmailService } from "./email-service.js";
import { CaptchaRequest } from "../../../plugins/plugin-captcha/api.js";
import { RuntimeDepsService } from "../../runtime-deps/runtime-deps-service.js";
// {data: '<svg.../svg>', text: 'abcd'}
/**
@@ -25,9 +24,6 @@ export class CodeService {
@Inject()
captchaService: CaptchaService;
@Inject()
runtimeDepsService: RuntimeDepsService;
async checkCaptcha(body: any, req: CaptchaRequest) {
return await this.captchaService.doValidate({ form: body, req });
}
@@ -60,7 +56,6 @@ export class CodeService {
await sender.setCtx({
accessService: accessGetter,
config: smsConfig,
runtimeDepsService: this.runtimeDepsService,
});
const smsCode = randomNumber(verificationCodeLength);
await sender.sendSmsCode({
@@ -1,9 +1,8 @@
import { FormItemProps, IAccessService } from "@certd/pipeline";
import type { RuntimeDepsService } from "../../runtime-deps/runtime-deps-service.js";
import { FormItemProps, IAccessService } from "@certd/pipeline";
export interface ISmsService {
sendSmsCode(opts: { mobile: string; code: string; phoneCode: string }): Promise<void>;
setCtx(ctx: { accessService: IAccessService; config: { [key: string]: any }; runtimeDepsService?: RuntimeDepsService }): Promise<void>;
setCtx(ctx: { accessService: IAccessService; config: { [key: string]: any } }): Promise<void>;
}
export type PluginInputs<T = any> = {
@@ -13,5 +12,4 @@ export type PluginInputs<T = any> = {
export type SmsPluginCtx<T = any> = {
accessService: IAccessService;
config: T;
runtimeDepsService?: RuntimeDepsService;
};
@@ -1,5 +1,6 @@
import { TencentAccess } from "../../../plugins/plugin-lib/tencent/access.js";
import { ISmsService, PluginInputs, SmsPluginCtx } from "./api.js";
import { TencentAccess } from "../../../plugins/plugin-lib/tencent/access.js";
import { importRuntime } from "@certd/pipeline";
import { ISmsService, PluginInputs } from "./api.js";
export type TencentSmsConfig = {
accessId: string;
@@ -66,26 +67,16 @@ export class TencentSmsService implements ISmsService {
};
}
ctx: SmsPluginCtx<TencentSmsConfig>;
ctx: { accessService: any; config: TencentSmsConfig };
async setCtx(ctx: any) {
this.ctx = ctx;
if (this.ctx.runtimeDepsService) {
await this.ctx.runtimeDepsService.ensureDependencies({
dependencies: {
"tencentcloud-sdk-nodejs": "^4.1.112",
},
});
}
}
async getClient() {
if (!this.ctx.runtimeDepsService) {
throw new Error("动态依赖服务未初始化,无法加载腾讯云短信SDK");
}
const sdk = await this.ctx.runtimeDepsService.importRuntime("tencentcloud-sdk-nodejs/tencentcloud/services/sms/v20210111/index.js");
const sdk = await importRuntime("tencentcloud-sdk-nodejs/tencentcloud/services/sms/v20210111/index.js");
const client = sdk.v20210111.Client;
const access = await this.ctx.accessService.getById<TencentAccess>(this.ctx.config.accessId);
const access: TencentAccess = await this.ctx.accessService.getById(this.ctx.config.accessId);
// const region = this.region;
const clientConfig = {
@@ -11,7 +11,7 @@ export class PasskeyEntity {
@Column({ name: "device_name", comment: "设备名称" })
deviceName: string;
@Column({ name: "passkey_id", comment: "passkey_id" })
@Column({ name: "passkey_id", comment: "passkey_id", unique: true })
passkeyId: string;
@Column({ name: "public_key", comment: "公钥", type: "text" })
@@ -23,6 +23,9 @@ export class PasskeyEntity {
@Column({ name: "transports", comment: "传输方式", type: "text", nullable: true })
transports: string;
@Column({ name: "rp_id", comment: "注册时的rpId,域名可能会变", nullable: true })
rpId: string;
@Column({ name: "registered_at", comment: "注册时间" })
registeredAt: number;

Some files were not shown because too many files have changed in this diff Show More