certctl-cli 1.0.6 → 1.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/API.md CHANGED
@@ -236,62 +236,229 @@ function fixCertificateChain(domain, certPath) {
236
236
 
237
237
  ---
238
238
 
239
- ## 方式二:完整的 Node.js SDK(需要额外开发)
239
+ ## 方式二:完整 SDK(含自动续期和证书链验证)
240
240
 
241
- 如果你需要更优雅的 API,可以开发一个包装器。以下是一个设计提案:
241
+ 我们提供了完整的 SDK 封装,包含自动续期、证书链验证与修复等高级功能。
242
242
 
243
- ### 安装方式
243
+ ### 安装
244
244
 
245
245
  ```bash
246
- npm install certctl-sdk
246
+ npm install certctl-cli
247
247
  ```
248
248
 
249
- ### 期望的 API 设计
249
+ ### 使用 SDK
250
250
 
251
251
  ```javascript
252
- const { CertctlClient } = require('certctl-sdk');
252
+ const { CertctlSDK } = require('certctl-cli/examples/certctl-sdk');
253
253
 
254
- // 创建客户端
255
- const client = new CertctlClient({
254
+ // 创建 SDK 实例
255
+ const sdk = new CertctlSDK({
256
256
  email: 'admin@example.com',
257
257
  outputDir: './certs',
258
- staging: false // 是否为测试环境
258
+
259
+ // DNS 配置(用于自动续期)
260
+ dns: {
261
+ provider: 'aliyun',
262
+ accessKeyId: process.env.ALI_KEY,
263
+ accessKeySecret: process.env.ALI_SECRET
264
+ },
265
+
266
+ // 自动续期配置
267
+ autoRenew: true, // 开启自动续期
268
+ renewBeforeDays: 30, // 到期前30天续期
269
+ checkInterval: 24 * 60 * 60 * 1000, // 每天检查
259
270
  });
260
271
 
261
- // 申请证书
262
- const cert = await client.apply({
263
- domain: 'example.com',
272
+ // 监听事件
273
+ sdk.on('apply:success', ({ domain, certInfo }) => {
274
+ console.log(`✅ 证书申请成功: ${domain}`);
275
+ });
276
+
277
+ sdk.on('verify:fixing', ({ domain }) => {
278
+ console.log(`🔧 正在自动修复证书链: ${domain}`);
279
+ });
280
+
281
+ sdk.on('autoRenew:trigger', ({ domain, reason }) => {
282
+ console.log(`🔄 自动续期: ${domain} (${reason})`);
283
+ });
284
+
285
+ // 申请证书(自动验证并修复证书链)
286
+ const result = await sdk.apply('example.com', {
287
+ verifyChain: true, // 申请后验证证书链
288
+ autoFix: true, // 链不完整时自动修复
289
+ });
290
+
291
+ // 返回结果
292
+ // {
293
+ // success: true,
294
+ // domain: 'example.com',
295
+ // paths: { cert: '...', key: '...' },
296
+ // chainValid: true, // 证书链是否完整
297
+ // chainLength: 2, // 证书链长度
298
+ // expireDays: 89 // 剩余天数
299
+ // }
300
+ ```
301
+
302
+ ### SDK 配置选项
303
+
304
+ | 选项 | 类型 | 默认值 | 说明 |
305
+ |------|------|--------|------|
306
+ | `email` | string | - | Let's Encrypt 账户邮箱 |
307
+ | `outputDir` | string | './certs' | 证书输出目录 |
308
+ | `autoRenew` | boolean | true | 是否开启自动续期 |
309
+ | `renewBeforeDays` | number | 30 | 到期前多少天续期 |
310
+ | `checkInterval` | number | 86400000 | 检查间隔(毫秒) |
311
+ | `dns` | object | null | DNS 配置用于自动续期 |
312
+
313
+ ### SDK 方法
314
+
315
+ #### `apply(domain, options)` - 申请证书
316
+
317
+ ```javascript
318
+ const result = await sdk.apply('example.com', {
319
+ email: 'admin@example.com', // 可选
320
+ verifyChain: true, // 验证证书链
321
+ autoFix: true, // 自动修复
322
+ dns: { provider: 'aliyun', ... } // 可选
323
+ });
324
+ ```
325
+
326
+ #### `verify(domain, options)` - 验证证书(含自动修复)
327
+
328
+ ```javascript
329
+ const result = await sdk.verify('example.com', {
330
+ autoFix: true // 验证失败时自动修复证书链
331
+ });
332
+
333
+ // 返回结果
334
+ // {
335
+ // valid: true, // 是否通过
336
+ // chainValid: true, // 证书链完整
337
+ // chainLength: 2,
338
+ // expireDays: 89,
339
+ // domainMatch: true,
340
+ // notExpired: true
341
+ // }
342
+ ```
343
+
344
+ #### `renew(domain, options)` - 续期证书
345
+
346
+ ```javascript
347
+ const result = await sdk.renew('example.com', {
348
+ autoFix: true // 续期后自动修复
349
+ });
350
+ ```
351
+
352
+ #### `checkRenewal(domain)` - 检查是否需要续期
353
+
354
+ ```javascript
355
+ const status = await sdk.checkRenewal('example.com');
356
+
357
+ // 返回结果
358
+ // {
359
+ // needRenew: false, // 是否需要续期
360
+ // reason: '...', // 原因
361
+ // expireDays: 89,
362
+ // chainValid: true
363
+ // }
364
+ ```
365
+
366
+ #### `startAutoRenew(domains)` - 启动自动续期服务
367
+
368
+ ```javascript
369
+ // 启动自动续期监控
370
+ sdk.startAutoRenew(['example.com', 'api.example.com']);
371
+
372
+ // 服务会自动:
373
+ // 1. 定期检查证书有效期
374
+ // 2. 证书链不完整时自动修复
375
+ // 3. 即将过期时自动续期
376
+ // 4. 通过事件通知状态变化
377
+ ```
378
+
379
+ #### `stopAutoRenew()` - 停止自动续期
380
+
381
+ ```javascript
382
+ sdk.stopAutoRenew();
383
+ ```
384
+
385
+ ### SDK 事件
386
+
387
+ | 事件名 | 参数 | 说明 |
388
+ |--------|------|------|
389
+ | `apply:success` | `{ domain, certInfo }` | 证书申请成功 |
390
+ | `verify:fixing` | `{ domain }` | 正在修复证书链 |
391
+ | `verify:success` | `{ domain, ... }` | 验证通过 |
392
+ | `autoRenew:trigger` | `{ domain, reason }` | 触发自动续期 |
393
+ | `autoRenew:error` | `{ domain, error }` | 自动续期失败 |
394
+
395
+ ### Express HTTP API + 自动续期完整示例
396
+
397
+ ```javascript
398
+ const express = require('express');
399
+ const { CertctlSDK } = require('certctl-cli/examples/certctl-sdk');
400
+
401
+ const app = express();
402
+ app.use(express.json());
403
+
404
+ // 创建 SDK 实例
405
+ const sdk = new CertctlSDK({
406
+ email: process.env.ACME_EMAIL,
407
+ outputDir: './certs',
408
+ autoRenew: true,
409
+ renewBeforeDays: 30,
264
410
  dns: {
265
411
  provider: 'aliyun',
266
- accessKeyId: 'your-key',
267
- accessKeySecret: 'your-secret'
412
+ accessKeyId: process.env.ALI_KEY,
413
+ accessKeySecret: process.env.ALI_SECRET
268
414
  }
269
415
  });
270
416
 
271
- console.log(cert.paths);
272
- // {
273
- // cert: './certs/example.com/example.com.pem',
274
- // key: './certs/example.com/example.com.key'
275
- // }
417
+ // 监听事件
418
+ sdk.on('apply:success', ({ domain }) => console.log(`[${new Date().toISOString()}] 申请成功: ${domain}`));
419
+ sdk.on('autoRenew:trigger', ({ domain, reason }) => console.log(`[${new Date().toISOString()}] 自动续期: ${domain} - ${reason}`));
420
+
421
+ // API: 申请证书(自动验证并修复证书链)
422
+ app.post('/api/certs/:domain', async (req, res) => {
423
+ try {
424
+ const result = await sdk.apply(req.params.domain, {
425
+ verifyChain: true, // 验证证书链
426
+ autoFix: true // 自动修复
427
+ });
428
+ res.json(result);
429
+ } catch (error) {
430
+ res.status(500).json({ success: false, error: error.message });
431
+ }
432
+ });
433
+
434
+ // API: 验证证书(自动修复)
435
+ app.get('/api/certs/:domain/verify', async (req, res) => {
436
+ const result = await sdk.verify(req.params.domain, { autoFix: true });
437
+ res.json(result);
438
+ });
276
439
 
277
- // 续期
278
- await client.renew('example.com');
440
+ // API: 检查续期状态
441
+ app.get('/api/certs/:domain/renewal-status', async (req, res) => {
442
+ const result = await sdk.checkRenewal(req.params.domain);
443
+ res.json(result);
444
+ });
279
445
 
280
- // 验证
281
- const isValid = await client.verify('example.com');
446
+ // 启动自动续期服务
447
+ sdk.startAutoRenew(['example.com', 'api.example.com']);
282
448
 
283
- // 修复证书链
284
- await client.fixChain('example.com');
449
+ app.listen(3000, () => {
450
+ console.log('🚀 证书管理 API 已启动');
451
+ });
285
452
  ```
286
453
 
287
- ### 是否需要开发 SDK?
454
+ ### 示例文件位置
455
+
456
+ 安装后可在 `node_modules/certctl-cli/examples/` 找到:
288
457
 
289
- | 场景 | 建议 |
290
- |------|------|
291
- | 只是简单调用 | 使用方式一(子进程)即可 |
292
- | 需要深度集成 | 可以开发 `certctl-sdk` |
293
- | 需要事件通知 | 子进程方式可以实时获取输出 |
294
- | 需要类型支持 | 可以添加 TypeScript 定义 |
458
+ - `certctl-sdk.js` - 完整 SDK 源码
459
+ - `sdk-usage.js` - SDK 使用示例
460
+ - `express-server.js` - Express 集成示例
461
+ - `basic-usage.js` - 基础调用示例
295
462
 
296
463
  ---
297
464
 
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -2,49 +2,87 @@
2
2
 
3
3
  这里提供了在 Node.js 项目中使用 certctl 的各种示例。
4
4
 
5
- ## 示例列表
5
+ ## 🚀 推荐:SDK 方式(certctl-sdk.js)
6
6
 
7
- ### 1. basic-usage.js - 基础使用示例
7
+ 最推荐的方式,包含**自动续期**和**证书链自动修复**功能。
8
8
 
9
- 展示如何在 Node.js 代码中调用 certctl 命令。
9
+ ### 快速开始
10
10
 
11
- **功能:**
12
- - 申请证书(支持阿里云自动验证)
13
- - 验证证书
14
- - 修复证书链
15
- - 续期证书
11
+ ```javascript
12
+ const { CertctlSDK } = require('./certctl-sdk');
13
+
14
+ const sdk = new CertctlSDK({
15
+ email: 'admin@example.com',
16
+ autoRenew: true,
17
+ dns: {
18
+ provider: 'aliyun',
19
+ accessKeyId: process.env.ALI_KEY,
20
+ accessKeySecret: process.env.ALI_SECRET
21
+ }
22
+ });
23
+
24
+ // 申请证书(自动验证并修复证书链)
25
+ const result = await sdk.apply('example.com', {
26
+ verifyChain: true,
27
+ autoFix: true
28
+ });
29
+
30
+ // 启动自动续期监控
31
+ sdk.startAutoRenew(['example.com', 'api.example.com']);
32
+ ```
33
+
34
+ ### 2. sdk-usage.js - SDK 完整使用示例
35
+
36
+ 展示 SDK 的所有功能:
37
+ - 证书申请(自动验证链)
38
+ - 自动续期服务
39
+ - 事件监听
40
+ - 批量管理
16
41
 
17
42
  **运行:**
18
43
  ```bash
19
- # 使用手动验证
20
- node basic-usage.js example.com
21
-
22
- # 使用阿里云自动验证(需设置环境变量)
23
- export ALI_KEY=your-access-key
24
- export ALI_SECRET=your-access-secret
44
+ export ALI_KEY=your-key
45
+ export ALI_SECRET=your-secret
25
46
  export ACME_EMAIL=admin@example.com
26
- node basic-usage.js example.com
47
+ node sdk-usage.js example.com
27
48
  ```
28
49
 
29
- ### 2. express-server.js - Express HTTP API
50
+ ### 3. certctl-sdk.js - SDK 源码
30
51
 
31
- 提供一个完整的 HTTP API 服务,通过 REST API 管理证书。
52
+ 完整的 SDK 实现,包含:
53
+ - 自动续期机制
54
+ - 证书链验证与修复
55
+ - 事件通知
56
+ - 完整错误处理
57
+
58
+ 可直接复制到你的项目中使用。
59
+
60
+ ---
61
+
62
+ ## 基础示例
63
+
64
+ ### 4. basic-usage.js - 基础使用示例
65
+
66
+ 展示如何在 Node.js 代码中调用 certctl 命令。
32
67
 
33
68
  **功能:**
34
- - 列出所有证书
35
- - 申请新证书
36
- - 续期证书
69
+ - 申请证书(支持阿里云自动验证)
37
70
  - 验证证书
38
71
  - 修复证书链
39
- - 下载证书文件
72
+ - 续期证书
40
73
 
41
- **安装:**
74
+ **运行:**
42
75
  ```bash
43
- npm install express certctl-cli
76
+ node basic-usage.js example.com
44
77
  ```
45
78
 
79
+ ### 5. express-server.js - Express HTTP API
80
+
81
+ 提供一个完整的 HTTP API 服务,通过 REST API 管理证书。
82
+
46
83
  **运行:**
47
84
  ```bash
85
+ npm install express
48
86
  node express-server.js
49
87
  ```
50
88
 
@@ -57,28 +95,17 @@ curl http://localhost:3000/api/certs
57
95
  # 申请证书(阿里云自动验证)
58
96
  curl -X POST http://localhost:3000/api/certs/example.com \
59
97
  -H "Content-Type: application/json" \
60
- -d '{
61
- "dns": {
62
- "provider": "aliyun",
63
- "accessKeyId": "your-key",
64
- "accessKeySecret": "your-secret"
65
- }
66
- }'
98
+ -d '{"dns":{"provider":"aliyun","accessKeyId":"xxx","accessKeySecret":"xxx"}}'
67
99
 
68
100
  # 验证证书
69
- curl -X POST http://localhost:3000/api/certs/example.com/verify
101
+ curl http://localhost:3000/api/certs/example.com/verify
70
102
 
71
103
  # 续期证书
72
104
  curl -X POST http://localhost:3000/api/certs/example.com/renew
73
-
74
- # 修复证书链
75
- curl -X POST http://localhost:3000/api/certs/example.com/fix-chain
76
-
77
- # 下载证书
78
- curl http://localhost:3000/api/certs/example.com/download?type=cert -o cert.pem
79
- curl http://localhost:3000/api/certs/example.com/download?type=key -o key.pem
80
105
  ```
81
106
 
82
- ## 完整 API 文档
107
+ ---
108
+
109
+ ## 📚 完整 API 文档
83
110
 
84
111
  更多详细信息请参考:[API.md](../API.md)
@@ -0,0 +1,517 @@
1
+ /**
2
+ * Certctl 完整 SDK 封装
3
+ *
4
+ * 特性:
5
+ * - 自动续期(定时检查)
6
+ * - 证书链验证与自动修复
7
+ * - 事件通知
8
+ * - 完整错误处理
9
+ */
10
+
11
+ const { spawn } = require('child_process');
12
+ const path = require('path');
13
+ const fs = require('fs');
14
+ const EventEmitter = require('events');
15
+
16
+ class CertctlSDK extends EventEmitter {
17
+ constructor(options = {}) {
18
+ super();
19
+
20
+ this.config = {
21
+ email: options.email || process.env.ACME_EMAIL,
22
+ outputDir: options.outputDir || './certs',
23
+ binaryPath: options.binaryPath || 'certctl',
24
+ // 自动续期配置
25
+ autoRenew: options.autoRenew !== false, // 默认开启
26
+ renewBeforeDays: options.renewBeforeDays || 30, // 到期前30天续期
27
+ checkInterval: options.checkInterval || 24 * 60 * 60 * 1000, // 每天检查一次
28
+ // DNS 配置(用于自动续期)
29
+ dns: options.dns || null, // { provider, accessKeyId, accessKeySecret }
30
+ };
31
+
32
+ this.timers = new Map(); // 存储定时器
33
+ this.isRunning = false;
34
+ }
35
+
36
+ /**
37
+ * ==========================================
38
+ * 核心命令执行
39
+ * ==========================================
40
+ */
41
+
42
+ _exec(args, options = {}) {
43
+ return new Promise((resolve, reject) => {
44
+ const { captureOutput = true, timeout = 300000 } = options;
45
+
46
+ this.emit('command:start', { command: 'certctl', args });
47
+
48
+ const proc = spawn(this.config.binaryPath, args, {
49
+ stdio: captureOutput ? 'pipe' : 'inherit',
50
+ timeout
51
+ });
52
+
53
+ let stdout = '';
54
+ let stderr = '';
55
+
56
+ if (captureOutput) {
57
+ proc.stdout.on('data', (data) => {
58
+ stdout += data.toString();
59
+ this.emit('command:stdout', data.toString());
60
+ });
61
+
62
+ proc.stderr.on('data', (data) => {
63
+ stderr += data.toString();
64
+ this.emit('command:stderr', data.toString());
65
+ });
66
+ }
67
+
68
+ proc.on('error', (error) => {
69
+ this.emit('command:error', error);
70
+ reject(error);
71
+ });
72
+
73
+ proc.on('close', (code) => {
74
+ const result = { code, stdout, stderr, args };
75
+
76
+ if (code === 0) {
77
+ this.emit('command:success', result);
78
+ resolve(result);
79
+ } else {
80
+ this.emit('command:fail', result);
81
+ reject(new Error(`Command failed with code ${code}: ${stderr || stdout}`));
82
+ }
83
+ });
84
+ });
85
+ }
86
+
87
+ /**
88
+ * ==========================================
89
+ * 证书申请
90
+ * ==========================================
91
+ */
92
+
93
+ /**
94
+ * 申请证书
95
+ * @param {string} domain - 域名
96
+ * @param {Object} options - 选项
97
+ * @param {string} options.email - 邮箱(可选,使用全局配置)
98
+ * @param {Object} options.dns - DNS 配置(可选)
99
+ * @param {boolean} options.verifyChain - 申请后是否验证证书链(默认 true)
100
+ * @param {boolean} options.autoFix - 验证失败时是否自动修复(默认 true)
101
+ */
102
+ async apply(domain, options = {}) {
103
+ const opts = { ...this.config, ...options };
104
+
105
+ this.emit('apply:start', { domain });
106
+
107
+ try {
108
+ // 1. 构建参数
109
+ const args = [
110
+ 'apply',
111
+ '-d', domain,
112
+ '-e', opts.email,
113
+ '-o', opts.outputDir
114
+ ];
115
+
116
+ // 添加 DNS 配置
117
+ if (opts.dns) {
118
+ if (opts.dns.provider === 'aliyun') {
119
+ args.push('--dns', 'aliyun');
120
+ args.push('--ali-key', opts.dns.accessKeyId);
121
+ args.push('--ali-secret', opts.dns.accessKeySecret);
122
+ } else if (opts.dns.provider === 'tencentcloud') {
123
+ args.push('--dns', 'tencentcloud');
124
+ args.push('--tencent-id', opts.dns.secretId);
125
+ args.push('--tencent-secret', opts.dns.secretKey);
126
+ }
127
+ }
128
+
129
+ // 2. 执行申请
130
+ await this._exec(args);
131
+
132
+ // 3. 获取证书信息
133
+ let certInfo = this._getCertPaths(domain);
134
+
135
+ // 4. 验证证书链(可选)
136
+ if (opts.verifyChain !== false) {
137
+ const verifyResult = await this.verify(domain, {
138
+ autoFix: opts.autoFix
139
+ });
140
+ certInfo = { ...certInfo, ...verifyResult };
141
+ }
142
+
143
+ // 5. 启动自动续期监控(如果开启)
144
+ if (this.config.autoRenew) {
145
+ this._startRenewMonitor(domain);
146
+ }
147
+
148
+ this.emit('apply:success', { domain, certInfo });
149
+
150
+ return {
151
+ success: true,
152
+ domain,
153
+ ...certInfo
154
+ };
155
+
156
+ } catch (error) {
157
+ this.emit('apply:error', { domain, error });
158
+ throw error;
159
+ }
160
+ }
161
+
162
+ /**
163
+ * ==========================================
164
+ * 证书验证
165
+ * ==========================================
166
+ */
167
+
168
+ /**
169
+ * 验证证书
170
+ * @param {string} domain - 域名
171
+ * @param {Object} options - 选项
172
+ * @param {boolean} options.autoFix - 验证失败时自动修复(默认 false)
173
+ * @returns {Object} 验证结果
174
+ */
175
+ async verify(domain, options = {}) {
176
+ this.emit('verify:start', { domain });
177
+
178
+ try {
179
+ const args = ['verify', '-d', domain, '-o', this.config.outputDir];
180
+
181
+ // 如果需要自动修复,添加 --fix 参数
182
+ if (options.autoFix) {
183
+ args.push('--fix');
184
+ }
185
+
186
+ const result = await this._exec(args, { captureOutput: true });
187
+
188
+ // 解析验证结果
189
+ const verifyInfo = this._parseVerifyOutput(result.stdout);
190
+
191
+ this.emit('verify:success', { domain, ...verifyInfo });
192
+
193
+ return {
194
+ valid: true,
195
+ domain,
196
+ ...verifyInfo
197
+ };
198
+
199
+ } catch (error) {
200
+ // 验证失败,尝试获取详细信息
201
+ const verifyInfo = this._parseVerifyOutput(error.message);
202
+
203
+ const result = {
204
+ valid: false,
205
+ domain,
206
+ error: error.message,
207
+ ...verifyInfo
208
+ };
209
+
210
+ // 自动修复
211
+ if (options.autoFix && !verifyInfo.chainValid) {
212
+ this.emit('verify:fixing', { domain });
213
+
214
+ try {
215
+ await this.fixChain(domain);
216
+
217
+ // 重新验证
218
+ return this.verify(domain, { autoFix: false });
219
+ } catch (fixError) {
220
+ result.fixError = fixError.message;
221
+ this.emit('verify:fixFailed', { domain, error: fixError });
222
+ }
223
+ }
224
+
225
+ this.emit('verify:fail', result);
226
+ return result;
227
+ }
228
+ }
229
+
230
+ /**
231
+ * 解析验证输出
232
+ */
233
+ _parseVerifyOutput(output) {
234
+ const info = {
235
+ chainValid: false,
236
+ chainLength: 0,
237
+ domainMatch: false,
238
+ notExpired: false,
239
+ expireDays: 0
240
+ };
241
+
242
+ // 解析证书链长度
243
+ const chainMatch = output.match(/证书链长度[::]\s*(\d+)/);
244
+ if (chainMatch) {
245
+ info.chainLength = parseInt(chainMatch[1]);
246
+ info.chainValid = info.chainLength >= 2;
247
+ }
248
+
249
+ // 解析剩余天数
250
+ const daysMatch = output.match(/剩余\s*(\d+)\s*天/);
251
+ if (daysMatch) {
252
+ info.expireDays = parseInt(daysMatch[1]);
253
+ info.notExpired = info.expireDays > 0;
254
+ }
255
+
256
+ // 检查是否通过
257
+ if (output.includes('✅ 证书验证通过') || output.includes('验证通过')) {
258
+ info.chainValid = true;
259
+ info.domainMatch = true;
260
+ info.notExpired = true;
261
+ }
262
+
263
+ return info;
264
+ }
265
+
266
+ /**
267
+ * ==========================================
268
+ * 修复证书链
269
+ * ==========================================
270
+ */
271
+
272
+ /**
273
+ * 修复证书链
274
+ * @param {string} domain - 域名
275
+ */
276
+ async fixChain(domain) {
277
+ this.emit('fixChain:start', { domain });
278
+
279
+ try {
280
+ await this._exec([
281
+ 'fix-chain',
282
+ '-d', domain,
283
+ '-o', this.config.outputDir
284
+ ]);
285
+
286
+ this.emit('fixChain:success', { domain });
287
+
288
+ return {
289
+ success: true,
290
+ domain
291
+ };
292
+
293
+ } catch (error) {
294
+ this.emit('fixChain:error', { domain, error });
295
+ throw error;
296
+ }
297
+ }
298
+
299
+ /**
300
+ * ==========================================
301
+ * 续期功能
302
+ * ==========================================
303
+ */
304
+
305
+ /**
306
+ * 手动续期证书
307
+ * @param {string} domain - 域名
308
+ * @param {Object} options - 选项
309
+ */
310
+ async renew(domain, options = {}) {
311
+ this.emit('renew:start', { domain });
312
+
313
+ try {
314
+ const args = [
315
+ 'renew',
316
+ '-d', domain,
317
+ '-o', this.config.outputDir
318
+ ];
319
+
320
+ // 如果需要 DNS 自动验证,添加参数
321
+ if (options.dns || this.config.dns) {
322
+ const dns = options.dns || this.config.dns;
323
+ if (dns.provider === 'aliyun') {
324
+ args.push('--dns', 'aliyun');
325
+ args.push('--ali-key', dns.accessKeyId);
326
+ args.push('--ali-secret', dns.accessKeySecret);
327
+ }
328
+ }
329
+
330
+ await this._exec(args);
331
+
332
+ // 验证新证书
333
+ const verifyResult = await this.verify(domain, {
334
+ autoFix: options.autoFix !== false
335
+ });
336
+
337
+ this.emit('renew:success', { domain, verifyResult });
338
+
339
+ return {
340
+ success: true,
341
+ domain,
342
+ ...this._getCertPaths(domain),
343
+ ...verifyResult
344
+ };
345
+
346
+ } catch (error) {
347
+ this.emit('renew:error', { domain, error });
348
+ throw error;
349
+ }
350
+ }
351
+
352
+ /**
353
+ * 检查证书是否需要续期
354
+ */
355
+ async checkRenewal(domain) {
356
+ const verifyResult = await this.verify(domain);
357
+
358
+ if (!verifyResult.valid) {
359
+ return {
360
+ needRenew: true,
361
+ reason: '证书无效',
362
+ ...verifyResult
363
+ };
364
+ }
365
+
366
+ if (verifyResult.expireDays <= this.config.renewBeforeDays) {
367
+ return {
368
+ needRenew: true,
369
+ reason: `证书将在 ${verifyResult.expireDays} 天后过期`,
370
+ ...verifyResult
371
+ };
372
+ }
373
+
374
+ return {
375
+ needRenew: false,
376
+ expireDays: verifyResult.expireDays,
377
+ ...verifyResult
378
+ };
379
+ }
380
+
381
+ /**
382
+ * 启动自动续期监控
383
+ */
384
+ startAutoRenew(domains) {
385
+ if (this.isRunning) {
386
+ console.log('自动续期服务已在运行');
387
+ return;
388
+ }
389
+
390
+ this.isRunning = true;
391
+ this.emit('autoRenew:start', { domains });
392
+
393
+ // 立即检查一次
394
+ this._checkAndRenew(domains);
395
+
396
+ // 定时检查
397
+ const timer = setInterval(() => {
398
+ this._checkAndRenew(domains);
399
+ }, this.config.checkInterval);
400
+
401
+ this.timers.set('autoRenew', timer);
402
+
403
+ console.log(`✅ 自动续期服务已启动,检查间隔: ${this.config.checkInterval / 1000 / 60} 分钟`);
404
+ }
405
+
406
+ /**
407
+ * 停止自动续期
408
+ */
409
+ stopAutoRenew() {
410
+ this.isRunning = false;
411
+
412
+ for (const [name, timer] of this.timers) {
413
+ clearInterval(timer);
414
+ console.log(`⏹️ 已停止: ${name}`);
415
+ }
416
+
417
+ this.timers.clear();
418
+ this.emit('autoRenew:stop');
419
+ }
420
+
421
+ /**
422
+ * 检查并续期
423
+ */
424
+ async _checkAndRenew(domains) {
425
+ for (const domain of domains) {
426
+ try {
427
+ const check = await this.checkRenewal(domain);
428
+
429
+ if (check.needRenew) {
430
+ this.emit('autoRenew:trigger', { domain, reason: check.reason });
431
+ console.log(`🔄 [${new Date().toISOString()}] ${domain}: ${check.reason},开始续期...`);
432
+
433
+ await this.renew(domain);
434
+ console.log(`✅ [${new Date().toISOString()}] ${domain}: 续期成功`);
435
+ } else {
436
+ console.log(`✓ [${new Date().toISOString()}] ${domain}: 证书正常,${check.expireDays}天后过期`);
437
+ }
438
+ } catch (error) {
439
+ console.error(`❌ [${new Date().toISOString()}] ${domain}: 检查/续期失败 - ${error.message}`);
440
+ this.emit('autoRenew:error', { domain, error });
441
+ }
442
+ }
443
+ }
444
+
445
+ /**
446
+ * 为单个域名启动续期监控
447
+ */
448
+ _startRenewMonitor(domain) {
449
+ if (!this.config.autoRenew) return;
450
+
451
+ console.log(`🔄 已为 ${domain} 启用自动续期监控`);
452
+
453
+ // 这里只是标记,实际的定时任务由 startAutoRenew 统一管理
454
+ // 或者可以单独为每个域名设置定时器
455
+ }
456
+
457
+ /**
458
+ * ==========================================
459
+ * 工具方法
460
+ * ==========================================
461
+ */
462
+
463
+ /**
464
+ * 获取证书路径
465
+ */
466
+ _getCertPaths(domain) {
467
+ const baseDir = path.join(this.config.outputDir, domain);
468
+ return {
469
+ domain,
470
+ exists: fs.existsSync(path.join(baseDir, `${domain}.pem`)),
471
+ paths: {
472
+ cert: path.join(baseDir, `${domain}.pem`),
473
+ key: path.join(baseDir, `${domain}.key`)
474
+ },
475
+ dir: baseDir
476
+ };
477
+ }
478
+
479
+ /**
480
+ * 列出所有证书
481
+ */
482
+ list() {
483
+ const certs = [];
484
+ const baseDir = this.config.outputDir;
485
+
486
+ if (!fs.existsSync(baseDir)) {
487
+ return certs;
488
+ }
489
+
490
+ const domains = fs.readdirSync(baseDir);
491
+ for (const domain of domains) {
492
+ const domainPath = path.join(baseDir, domain);
493
+ if (fs.statSync(domainPath).isDirectory()) {
494
+ certs.push(this._getCertPaths(domain));
495
+ }
496
+ }
497
+
498
+ return certs;
499
+ }
500
+
501
+ /**
502
+ * 删除证书
503
+ */
504
+ remove(domain) {
505
+ const certInfo = this._getCertPaths(domain);
506
+
507
+ if (certInfo.exists) {
508
+ fs.rmSync(certInfo.dir, { recursive: true, force: true });
509
+ this.emit('remove', { domain });
510
+ return true;
511
+ }
512
+
513
+ return false;
514
+ }
515
+ }
516
+
517
+ module.exports = { CertctlSDK };
@@ -0,0 +1,224 @@
1
+ /**
2
+ * Certctl SDK 完整使用示例
3
+ *
4
+ * 展示如何使用 SDK 实现:
5
+ * - 证书申请(自动验证证书链)
6
+ * - 自动续期
7
+ * - 事件监听
8
+ * - 批量管理
9
+ */
10
+
11
+ const { CertctlSDK } = require('./certctl-sdk');
12
+
13
+ // ==================== 配置 ====================
14
+ const config = {
15
+ email: process.env.ACME_EMAIL || 'admin@example.com',
16
+ outputDir: './my-certs',
17
+
18
+ // DNS 配置(用于自动续期)
19
+ dns: {
20
+ provider: 'aliyun',
21
+ accessKeyId: process.env.ALI_KEY,
22
+ accessKeySecret: process.env.ALI_SECRET
23
+ },
24
+
25
+ // 自动续期配置
26
+ autoRenew: true, // 开启自动续期
27
+ renewBeforeDays: 30, // 到期前30天自动续期
28
+ checkInterval: 60 * 1000, // 每分钟检查一次(生产环境建议每天)
29
+ };
30
+
31
+ // ==================== 创建 SDK 实例 ====================
32
+ const sdk = new CertctlSDK(config);
33
+
34
+ // ==================== 事件监听 ====================
35
+
36
+ sdk.on('apply:start', ({ domain }) => {
37
+ console.log(`📝 开始申请证书: ${domain}`);
38
+ });
39
+
40
+ sdk.on('apply:success', ({ domain, certInfo }) => {
41
+ console.log(`✅ 证书申请成功: ${domain}`);
42
+ console.log(` 证书路径: ${certInfo.paths.cert}`);
43
+ console.log(` 私钥路径: ${certInfo.paths.key}`);
44
+ console.log(` 链完整: ${certInfo.chainValid ? '✅' : '❌'}`);
45
+ });
46
+
47
+ sdk.on('verify:fixing', ({ domain }) => {
48
+ console.log(`🔧 证书链不完整,正在自动修复: ${domain}`);
49
+ });
50
+
51
+ sdk.on('verify:success', ({ domain, chainValid, expireDays }) => {
52
+ console.log(`✅ 验证通过: ${domain} (链完整: ${chainValid}, ${expireDays}天后过期)`);
53
+ });
54
+
55
+ sdk.on('verify:fail', ({ domain, chainValid, error }) => {
56
+ console.error(`❌ 验证失败: ${domain}`);
57
+ console.error(` 错误: ${error}`);
58
+ });
59
+
60
+ sdk.on('autoRenew:trigger', ({ domain, reason }) => {
61
+ console.log(`🔄 触发自动续期: ${domain} (${reason})`);
62
+ });
63
+
64
+ sdk.on('autoRenew:error', ({ domain, error }) => {
65
+ console.error(`❌ 自动续期失败: ${domain} - ${error.message}`);
66
+ });
67
+
68
+ // ==================== 使用示例 ====================
69
+
70
+ async function examples() {
71
+ const domain = process.argv[2] || 'example.com';
72
+
73
+ console.log('========================================');
74
+ console.log('Certctl SDK 使用示例');
75
+ console.log('========================================\n');
76
+
77
+ // -------------------- 示例 1: 申请证书(自动验证链) --------------------
78
+ console.log('\n📌 示例 1: 申请证书(自动验证证书链)\n');
79
+
80
+ try {
81
+ // 申请证书,自动验证并修复证书链
82
+ const result = await sdk.apply(domain, {
83
+ verifyChain: true, // 申请后验证证书链
84
+ autoFix: true, // 链不完整时自动修复
85
+ });
86
+
87
+ console.log('\n申请结果:', result);
88
+
89
+ } catch (error) {
90
+ console.error('申请失败:', error.message);
91
+ }
92
+
93
+ // -------------------- 示例 2: 手动验证证书 --------------------
94
+ console.log('\n📌 示例 2: 手动验证证书\n');
95
+
96
+ const verifyResult = await sdk.verify(domain, { autoFix: true });
97
+ console.log('验证结果:', {
98
+ 有效: verifyResult.valid,
99
+ 链完整: verifyResult.chainValid,
100
+ 链长度: verifyResult.chainLength,
101
+ 剩余天数: verifyResult.expireDays,
102
+ });
103
+
104
+ // -------------------- 示例 3: 检查是否需要续期 --------------------
105
+ console.log('\n📌 示例 3: 检查续期状态\n');
106
+
107
+ const renewalStatus = await sdk.checkRenewal(domain);
108
+ console.log('续期检查:', {
109
+ 需要续期: renewalStatus.needRenew,
110
+ 原因: renewalStatus.reason,
111
+ 剩余天数: renewalStatus.expireDays,
112
+ });
113
+
114
+ // -------------------- 示例 4: 手动续期 --------------------
115
+ if (renewalStatus.needRenew) {
116
+ console.log('\n📌 示例 4: 手动续期\n');
117
+
118
+ try {
119
+ const renewResult = await sdk.renew(domain, { autoFix: true });
120
+ console.log('续期结果:', renewResult);
121
+ } catch (error) {
122
+ console.error('续期失败:', error.message);
123
+ }
124
+ }
125
+
126
+ // -------------------- 示例 5: 列出所有证书 --------------------
127
+ console.log('\n📌 示例 5: 列出所有证书\n');
128
+
129
+ const allCerts = sdk.list();
130
+ console.log(`共有 ${allCerts.length} 个证书:`);
131
+ for (const cert of allCerts) {
132
+ console.log(` - ${cert.domain}: ${cert.exists ? '✅' : '❌'}`);
133
+ }
134
+
135
+ // -------------------- 示例 6: 启动自动续期服务 --------------------
136
+ console.log('\n📌 示例 6: 启动自动续期服务\n');
137
+
138
+ // 启动自动续期,监控多个域名
139
+ const domainsToMonitor = [domain];
140
+ sdk.startAutoRenew(domainsToMonitor);
141
+
142
+ console.log('自动续期服务已启动,按 Ctrl+C 停止\n');
143
+
144
+ // 保持进程运行
145
+ process.on('SIGINT', () => {
146
+ console.log('\n\n正在停止服务...');
147
+ sdk.stopAutoRenew();
148
+ process.exit(0);
149
+ });
150
+ }
151
+
152
+ // 运行示例
153
+ examples().catch(console.error);
154
+
155
+ /*
156
+ ============================
157
+ 其他使用场景
158
+ ============================
159
+
160
+ // 场景 1: 批量申请多个域名
161
+ async function batchApply() {
162
+ const domains = ['api.example.com', 'app.example.com', 'cdn.example.com'];
163
+
164
+ for (const domain of domains) {
165
+ try {
166
+ await sdk.apply(domain, { verifyChain: true, autoFix: true });
167
+ console.log(`✅ ${domain} 申请成功`);
168
+ } catch (error) {
169
+ console.error(`❌ ${domain} 申请失败: ${error.message}`);
170
+ }
171
+ }
172
+ }
173
+
174
+ // 场景 2: 定期验证所有证书
175
+ async function validateAll() {
176
+ const certs = sdk.list();
177
+
178
+ for (const cert of certs) {
179
+ const result = await sdk.verify(cert.domain, { autoFix: true });
180
+
181
+ if (!result.valid) {
182
+ console.error(`⚠️ ${cert.domain} 证书异常,需要处理`);
183
+ }
184
+ }
185
+ }
186
+
187
+ // 场景 3: 在 Express 中使用
188
+ const express = require('express');
189
+ const app = express();
190
+
191
+ // 申请证书 API
192
+ app.post('/api/certs/:domain', async (req, res) => {
193
+ const { domain } = req.params;
194
+
195
+ try {
196
+ const result = await sdk.apply(domain, {
197
+ verifyChain: true,
198
+ autoFix: true
199
+ });
200
+ res.json({ success: true, data: result });
201
+ } catch (error) {
202
+ res.status(500).json({ success: false, error: error.message });
203
+ }
204
+ });
205
+
206
+ // 验证证书 API
207
+ app.get('/api/certs/:domain/verify', async (req, res) => {
208
+ const { domain } = req.params;
209
+ const result = await sdk.verify(domain, { autoFix: true });
210
+ res.json(result);
211
+ });
212
+
213
+ // 检查续期状态 API
214
+ app.get('/api/certs/:domain/renewal-status', async (req, res) => {
215
+ const { domain } = req.params;
216
+ const result = await sdk.checkRenewal(domain);
217
+ res.json(result);
218
+ });
219
+
220
+ // 启动自动续期服务
221
+ sdk.startAutoRenew(['example.com', 'api.example.com']);
222
+
223
+ app.listen(3000);
224
+ */
package/package.json CHANGED
@@ -1,10 +1,19 @@
1
1
  {
2
2
  "name": "certctl-cli",
3
- "version": "1.0.6",
3
+ "version": "1.0.10",
4
4
  "description": "Lightweight SSL Certificate CLI Tool",
5
5
  "bin": {
6
6
  "certctl": "bin/run.js"
7
7
  },
8
+ "scripts": {
9
+ "build": "cd .. && bash build.sh",
10
+ "version": "node scripts/version.js",
11
+ "postversion": "echo '✅ 版本已更新,运行 npm publish 发布'",
12
+ "release": "node scripts/release.js",
13
+ "release:minor": "node scripts/release.js minor",
14
+ "release:major": "node scripts/release.js major",
15
+ "publish:dry": "npm publish --dry-run"
16
+ },
8
17
  "files": [
9
18
  "bin/",
10
19
  "examples/",
@@ -36,4 +45,4 @@
36
45
  "engines": {
37
46
  "node": ">=14"
38
47
  }
39
- }
48
+ }