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 +199 -32
- package/bin/certctl-darwin-amd64 +0 -0
- package/bin/certctl-darwin-arm64 +0 -0
- package/bin/certctl-linux-amd64 +0 -0
- package/bin/certctl-linux-arm64 +0 -0
- package/bin/certctl-windows-amd64.exe +0 -0
- package/examples/README.md +66 -39
- package/examples/certctl-sdk.js +517 -0
- package/examples/sdk-usage.js +224 -0
- package/package.json +11 -2
package/API.md
CHANGED
|
@@ -236,62 +236,229 @@ function fixCertificateChain(domain, certPath) {
|
|
|
236
236
|
|
|
237
237
|
---
|
|
238
238
|
|
|
239
|
-
##
|
|
239
|
+
## 方式二:完整 SDK(含自动续期和证书链验证)
|
|
240
240
|
|
|
241
|
-
|
|
241
|
+
我们提供了完整的 SDK 封装,包含自动续期、证书链验证与修复等高级功能。
|
|
242
242
|
|
|
243
|
-
###
|
|
243
|
+
### 安装
|
|
244
244
|
|
|
245
245
|
```bash
|
|
246
|
-
npm install certctl-
|
|
246
|
+
npm install certctl-cli
|
|
247
247
|
```
|
|
248
248
|
|
|
249
|
-
###
|
|
249
|
+
### 使用 SDK
|
|
250
250
|
|
|
251
251
|
```javascript
|
|
252
|
-
const {
|
|
252
|
+
const { CertctlSDK } = require('certctl-cli/examples/certctl-sdk');
|
|
253
253
|
|
|
254
|
-
//
|
|
255
|
-
const
|
|
254
|
+
// 创建 SDK 实例
|
|
255
|
+
const sdk = new CertctlSDK({
|
|
256
256
|
email: 'admin@example.com',
|
|
257
257
|
outputDir: './certs',
|
|
258
|
-
|
|
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
|
-
|
|
263
|
-
domain
|
|
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:
|
|
267
|
-
accessKeySecret:
|
|
412
|
+
accessKeyId: process.env.ALI_KEY,
|
|
413
|
+
accessKeySecret: process.env.ALI_SECRET
|
|
268
414
|
}
|
|
269
415
|
});
|
|
270
416
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
-
|
|
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
|
-
|
|
446
|
+
// 启动自动续期服务
|
|
447
|
+
sdk.startAutoRenew(['example.com', 'api.example.com']);
|
|
282
448
|
|
|
283
|
-
|
|
284
|
-
|
|
449
|
+
app.listen(3000, () => {
|
|
450
|
+
console.log('🚀 证书管理 API 已启动');
|
|
451
|
+
});
|
|
285
452
|
```
|
|
286
453
|
|
|
287
|
-
###
|
|
454
|
+
### 示例文件位置
|
|
455
|
+
|
|
456
|
+
安装后可在 `node_modules/certctl-cli/examples/` 找到:
|
|
288
457
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
|
package/bin/certctl-darwin-amd64
CHANGED
|
Binary file
|
package/bin/certctl-darwin-arm64
CHANGED
|
Binary file
|
package/bin/certctl-linux-amd64
CHANGED
|
Binary file
|
package/bin/certctl-linux-arm64
CHANGED
|
Binary file
|
|
Binary file
|
package/examples/README.md
CHANGED
|
@@ -2,49 +2,87 @@
|
|
|
2
2
|
|
|
3
3
|
这里提供了在 Node.js 项目中使用 certctl 的各种示例。
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## 🚀 推荐:SDK 方式(certctl-sdk.js)
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
最推荐的方式,包含**自动续期**和**证书链自动修复**功能。
|
|
8
8
|
|
|
9
|
-
|
|
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
|
-
|
|
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
|
|
47
|
+
node sdk-usage.js example.com
|
|
27
48
|
```
|
|
28
49
|
|
|
29
|
-
###
|
|
50
|
+
### 3. certctl-sdk.js - SDK 源码
|
|
30
51
|
|
|
31
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
+
}
|