certctl-cli 1.0.5 → 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 ADDED
@@ -0,0 +1,767 @@
1
+ # Certctl Programmatic API 使用指南
2
+
3
+ 如果你在 Node.js 项目中需要通过代码调用证书申请功能,有以下两种方式:
4
+
5
+ ---
6
+
7
+ ## 方式一:通过子进程调用 CLI(推荐,当前可用)
8
+
9
+ 这是目前最稳定的方式,直接调用 `certctl` 命令。
10
+
11
+ ### 安装
12
+
13
+ ```bash
14
+ npm install certctl-cli
15
+ ```
16
+
17
+ ### 基础用法
18
+
19
+ ```javascript
20
+ const { spawn } = require('child_process');
21
+ const path = require('path');
22
+
23
+ // 获取 certctl 二进制路径
24
+ function getCertctlPath() {
25
+ return path.join(__dirname, 'node_modules', '.bin', 'certctl');
26
+ }
27
+
28
+ /**
29
+ * 申请证书
30
+ * @param {Object} options - 配置选项
31
+ * @param {string} options.domain - 域名 (必填)
32
+ * @param {string} options.email - 邮箱 (必填)
33
+ * @param {string} options.output - 输出目录 (可选,默认 ./certs)
34
+ * @param {string} options.dns - DNS 提供商 (可选: aliyun/tencentcloud)
35
+ * @param {Object} options.credentials - DNS 凭证 (可选)
36
+ */
37
+ function applyCertificate(options) {
38
+ return new Promise((resolve, reject) => {
39
+ const args = [
40
+ 'apply',
41
+ '-d', options.domain,
42
+ '-e', options.email,
43
+ '-o', options.output || './certs'
44
+ ];
45
+
46
+ // 自动 DNS 验证
47
+ if (options.dns) {
48
+ args.push('--dns', options.dns);
49
+
50
+ if (options.dns === 'aliyun' && options.credentials) {
51
+ args.push(
52
+ '--ali-key', options.credentials.accessKeyId,
53
+ '--ali-secret', options.credentials.accessKeySecret
54
+ );
55
+ }
56
+
57
+ if (options.dns === 'tencentcloud' && options.credentials) {
58
+ args.push(
59
+ '--tencent-id', options.credentials.secretId,
60
+ '--tencent-secret', options.credentials.secretKey
61
+ );
62
+ }
63
+ }
64
+
65
+ const certctl = spawn(getCertctlPath(), args, {
66
+ stdio: ['inherit', 'pipe', 'pipe']
67
+ });
68
+
69
+ let stdout = '';
70
+ let stderr = '';
71
+
72
+ certctl.stdout.on('data', (data) => {
73
+ stdout += data.toString();
74
+ console.log(data.toString()); // 实时输出日志
75
+ });
76
+
77
+ certctl.stderr.on('data', (data) => {
78
+ stderr += data.toString();
79
+ });
80
+
81
+ certctl.on('close', (code) => {
82
+ if (code === 0) {
83
+ resolve({
84
+ success: true,
85
+ domain: options.domain,
86
+ output: options.output || './certs',
87
+ stdout
88
+ });
89
+ } else {
90
+ reject(new Error(`证书申请失败: ${stderr || stdout}`));
91
+ }
92
+ });
93
+ });
94
+ }
95
+
96
+ // 使用示例
97
+ async function main() {
98
+ try {
99
+ // 示例 1: 阿里云自动验证
100
+ const result = await applyCertificate({
101
+ domain: 'example.com',
102
+ email: 'admin@example.com',
103
+ output: './my-certs',
104
+ dns: 'aliyun',
105
+ credentials: {
106
+ accessKeyId: process.env.ALICLOUD_ACCESS_KEY,
107
+ accessKeySecret: process.env.ALICLOUD_SECRET_KEY
108
+ }
109
+ });
110
+
111
+ console.log('证书申请成功:', result);
112
+
113
+ // 证书文件位置
114
+ const certPath = `./my-certs/${result.domain}/${result.domain}.pem`;
115
+ const keyPath = `./my-certs/${result.domain}/${result.domain}.key`;
116
+
117
+ console.log('证书路径:', certPath);
118
+ console.log('私钥路径:', keyPath);
119
+
120
+ } catch (error) {
121
+ console.error('申请失败:', error.message);
122
+ }
123
+ }
124
+
125
+ main();
126
+ ```
127
+
128
+ ### 续期证书
129
+
130
+ ```javascript
131
+ const { spawn } = require('child_process');
132
+
133
+ function renewCertificate(domain, outputDir = './certs') {
134
+ return new Promise((resolve, reject) => {
135
+ const certctl = spawn('certctl', [
136
+ 'renew',
137
+ '-d', domain,
138
+ '-o', outputDir
139
+ ], {
140
+ stdio: 'pipe'
141
+ });
142
+
143
+ let output = '';
144
+ certctl.stdout.on('data', (data) => {
145
+ output += data.toString();
146
+ });
147
+
148
+ certctl.on('close', (code) => {
149
+ if (code === 0) {
150
+ resolve({ success: true, output });
151
+ } else {
152
+ reject(new Error(`续期失败: ${output}`));
153
+ }
154
+ });
155
+ });
156
+ }
157
+
158
+ // 使用
159
+ renewCertificate('example.com', './my-certs')
160
+ .then(() => console.log('续期成功'))
161
+ .catch(err => console.error(err));
162
+ ```
163
+
164
+ ### 验证证书
165
+
166
+ ```javascript
167
+ const { spawn } = require('child_process');
168
+
169
+ function verifyCertificate(domain, certPath) {
170
+ return new Promise((resolve, reject) => {
171
+ const args = ['verify', '-d', domain];
172
+ if (certPath) {
173
+ args.push('-p', certPath);
174
+ }
175
+
176
+ const certctl = spawn('certctl', args, {
177
+ stdio: 'pipe'
178
+ });
179
+
180
+ let output = '';
181
+ certctl.stdout.on('data', (data) => {
182
+ output += data.toString();
183
+ });
184
+
185
+ certctl.on('close', (code) => {
186
+ resolve({
187
+ valid: code === 0,
188
+ output
189
+ });
190
+ });
191
+ });
192
+ }
193
+
194
+ // 使用
195
+ verifyCertificate('example.com')
196
+ .then(result => {
197
+ console.log('验证结果:', result.valid ? '通过' : '失败');
198
+ console.log(result.output);
199
+ });
200
+ ```
201
+
202
+ ### 修复证书链
203
+
204
+ ```javascript
205
+ const { spawn } = require('child_process');
206
+
207
+ function fixCertificateChain(domain, certPath) {
208
+ return new Promise((resolve, reject) => {
209
+ const args = ['fix-chain'];
210
+
211
+ if (certPath) {
212
+ args.push('-p', certPath);
213
+ } else if (domain) {
214
+ args.push('-d', domain);
215
+ }
216
+
217
+ const certctl = spawn('certctl', args, {
218
+ stdio: 'pipe'
219
+ });
220
+
221
+ let output = '';
222
+ certctl.stdout.on('data', (data) => {
223
+ output += data.toString();
224
+ });
225
+
226
+ certctl.on('close', (code) => {
227
+ if (code === 0) {
228
+ resolve({ success: true, output });
229
+ } else {
230
+ reject(new Error(`修复失败: ${output}`));
231
+ }
232
+ });
233
+ });
234
+ }
235
+ ```
236
+
237
+ ---
238
+
239
+ ## 方式二:完整 SDK(含自动续期和证书链验证)
240
+
241
+ 我们提供了完整的 SDK 封装,包含自动续期、证书链验证与修复等高级功能。
242
+
243
+ ### 安装
244
+
245
+ ```bash
246
+ npm install certctl-cli
247
+ ```
248
+
249
+ ### 使用 SDK
250
+
251
+ ```javascript
252
+ const { CertctlSDK } = require('certctl-cli/examples/certctl-sdk');
253
+
254
+ // 创建 SDK 实例
255
+ const sdk = new CertctlSDK({
256
+ email: 'admin@example.com',
257
+ outputDir: './certs',
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, // 每天检查
270
+ });
271
+
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,
410
+ dns: {
411
+ provider: 'aliyun',
412
+ accessKeyId: process.env.ALI_KEY,
413
+ accessKeySecret: process.env.ALI_SECRET
414
+ }
415
+ });
416
+
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
+ });
439
+
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
+ });
445
+
446
+ // 启动自动续期服务
447
+ sdk.startAutoRenew(['example.com', 'api.example.com']);
448
+
449
+ app.listen(3000, () => {
450
+ console.log('🚀 证书管理 API 已启动');
451
+ });
452
+ ```
453
+
454
+ ### 示例文件位置
455
+
456
+ 安装后可在 `node_modules/certctl-cli/examples/` 找到:
457
+
458
+ - `certctl-sdk.js` - 完整 SDK 源码
459
+ - `sdk-usage.js` - SDK 使用示例
460
+ - `express-server.js` - Express 集成示例
461
+ - `basic-usage.js` - 基础调用示例
462
+
463
+ ---
464
+
465
+ ## TypeScript 类型定义
466
+
467
+ 如果你使用 TypeScript,以下是类型定义:
468
+
469
+ ```typescript
470
+ // certctl.d.ts
471
+
472
+ export interface ApplyOptions {
473
+ domain: string;
474
+ email: string;
475
+ output?: string;
476
+ dns?: 'aliyun' | 'tencentcloud';
477
+ credentials?: {
478
+ accessKeyId?: string;
479
+ accessKeySecret?: string;
480
+ secretId?: string;
481
+ secretKey?: string;
482
+ };
483
+ staging?: boolean;
484
+ }
485
+
486
+ export interface ApplyResult {
487
+ success: boolean;
488
+ domain: string;
489
+ output: string;
490
+ stdout: string;
491
+ }
492
+
493
+ export interface VerifyResult {
494
+ valid: boolean;
495
+ output: string;
496
+ }
497
+
498
+ export declare function applyCertificate(options: ApplyOptions): Promise<ApplyResult>;
499
+ export declare function renewCertificate(domain: string, outputDir?: string): Promise<{ success: boolean; output: string }>;
500
+ export declare function verifyCertificate(domain: string, certPath?: string): Promise<VerifyResult>;
501
+ export declare function fixCertificateChain(domain?: string, certPath?: string): Promise<{ success: boolean; output: string }>;
502
+ ```
503
+
504
+ ---
505
+
506
+ ## 完整项目示例
507
+
508
+ ```
509
+ my-project/
510
+ ├── src/
511
+ │ ├── certctl-wrapper.js # 封装 certctl 调用
512
+ │ ├── server.js # 你的应用
513
+ │ └── config.js
514
+ ├── certs/ # 证书输出目录
515
+ ├── package.json
516
+ └── .env
517
+ ```
518
+
519
+ ### certctl-wrapper.js
520
+
521
+ ```javascript
522
+ const { spawn } = require('child_process');
523
+ const path = require('path');
524
+ const fs = require('fs');
525
+
526
+ class CertctlWrapper {
527
+ constructor(options = {}) {
528
+ this.email = options.email;
529
+ this.outputDir = options.outputDir || './certs';
530
+ this.binaryPath = options.binaryPath || 'certctl';
531
+ }
532
+
533
+ _spawn(args) {
534
+ return new Promise((resolve, reject) => {
535
+ const proc = spawn(this.binaryPath, args, {
536
+ stdio: ['pipe', 'pipe', 'pipe']
537
+ });
538
+
539
+ let stdout = '';
540
+ let stderr = '';
541
+
542
+ proc.stdout.on('data', (data) => {
543
+ stdout += data.toString();
544
+ });
545
+
546
+ proc.stderr.on('data', (data) => {
547
+ stderr += data.toString();
548
+ });
549
+
550
+ proc.on('close', (code) => {
551
+ if (code === 0) {
552
+ resolve({ stdout, stderr, code });
553
+ } else {
554
+ reject(new Error(`Process exited with code ${code}: ${stderr || stdout}`));
555
+ }
556
+ });
557
+ });
558
+ }
559
+
560
+ async apply(domain, dnsOptions = null) {
561
+ const args = [
562
+ 'apply',
563
+ '-d', domain,
564
+ '-e', this.email,
565
+ '-o', this.outputDir
566
+ ];
567
+
568
+ if (dnsOptions) {
569
+ args.push('--dns', dnsOptions.provider);
570
+
571
+ if (dnsOptions.provider === 'aliyun') {
572
+ args.push(
573
+ '--ali-key', dnsOptions.accessKeyId,
574
+ '--ali-secret', dnsOptions.accessKeySecret
575
+ );
576
+ } else if (dnsOptions.provider === 'tencentcloud') {
577
+ args.push(
578
+ '--tencent-id', dnsOptions.secretId,
579
+ '--tencent-secret', dnsOptions.secretKey
580
+ );
581
+ }
582
+ }
583
+
584
+ const result = await this._spawn(args);
585
+
586
+ return {
587
+ success: true,
588
+ domain,
589
+ paths: {
590
+ cert: path.join(this.outputDir, domain, `${domain}.pem`),
591
+ key: path.join(this.outputDir, domain, `${domain}.key`)
592
+ },
593
+ output: result.stdout
594
+ };
595
+ }
596
+
597
+ async renew(domain) {
598
+ const args = ['renew', '-d', domain, '-o', this.outputDir];
599
+ const result = await this._spawn(args);
600
+ return { success: true, output: result.stdout };
601
+ }
602
+
603
+ async verify(domain) {
604
+ const args = ['verify', '-d', domain];
605
+ try {
606
+ const result = await this._spawn(args);
607
+ return { valid: true, output: result.stdout };
608
+ } catch (err) {
609
+ return { valid: false, output: err.message };
610
+ }
611
+ }
612
+
613
+ async fixChain(domain) {
614
+ const args = ['fix-chain', '-d', domain];
615
+ const result = await this._spawn(args);
616
+ return { success: true, output: result.stdout };
617
+ }
618
+
619
+ getCertificatePaths(domain) {
620
+ return {
621
+ cert: path.join(this.outputDir, domain, `${domain}.pem`),
622
+ key: path.join(this.outputDir, domain, `${domain}.key`)
623
+ };
624
+ }
625
+
626
+ exists(domain) {
627
+ const paths = this.getCertificatePaths(domain);
628
+ return fs.existsSync(paths.cert) && fs.existsSync(paths.key);
629
+ }
630
+ }
631
+
632
+ module.exports = { CertctlWrapper };
633
+ ```
634
+
635
+ ### server.js
636
+
637
+ ```javascript
638
+ const { CertctlWrapper } = require('./certctl-wrapper');
639
+ const express = require('express');
640
+
641
+ const app = express();
642
+ const certClient = new CertctlWrapper({
643
+ email: process.env.ACME_EMAIL,
644
+ outputDir: './certs'
645
+ });
646
+
647
+ // API: 申请证书
648
+ app.post('/api/certs/:domain', async (req, res) => {
649
+ try {
650
+ const { domain } = req.params;
651
+
652
+ // 检查是否已存在
653
+ if (certClient.exists(domain)) {
654
+ return res.json({
655
+ success: true,
656
+ message: '证书已存在',
657
+ paths: certClient.getCertificatePaths(domain)
658
+ });
659
+ }
660
+
661
+ // 申请证书
662
+ const result = await certClient.apply(domain, {
663
+ provider: 'aliyun',
664
+ accessKeyId: process.env.ALI_KEY,
665
+ accessKeySecret: process.env.ALI_SECRET
666
+ });
667
+
668
+ res.json(result);
669
+ } catch (error) {
670
+ res.status(500).json({ success: false, error: error.message });
671
+ }
672
+ });
673
+
674
+ // API: 获取证书信息
675
+ app.get('/api/certs/:domain', async (req, res) => {
676
+ try {
677
+ const { domain } = req.params;
678
+ const verifyResult = await certClient.verify(domain);
679
+
680
+ res.json({
681
+ domain,
682
+ exists: certClient.exists(domain),
683
+ valid: verifyResult.valid,
684
+ paths: certClient.getCertificatePaths(domain)
685
+ });
686
+ } catch (error) {
687
+ res.status(500).json({ error: error.message });
688
+ }
689
+ });
690
+
691
+ app.listen(3000, () => {
692
+ console.log('Server running on http://localhost:3000');
693
+ });
694
+ ```
695
+
696
+ ---
697
+
698
+ ## 常见问题
699
+
700
+ ### Q: 如何在 Docker 中使用?
701
+
702
+ ```dockerfile
703
+ FROM node:18-alpine
704
+
705
+ # 安装 certctl
706
+ RUN npm install -g certctl-cli
707
+
708
+ WORKDIR /app
709
+ COPY package*.json ./
710
+ RUN npm install
711
+
712
+ COPY . .
713
+
714
+ CMD ["node", "server.js"]
715
+ ```
716
+
717
+ ### Q: 如何监控证书到期?
718
+
719
+ ```javascript
720
+ const fs = require('fs');
721
+ const { spawn } = require('child_process');
722
+
723
+ // 定期检查证书有效期
724
+ function checkCertExpiry(domain, certPath) {
725
+ return new Promise((resolve) => {
726
+ const certctl = spawn('certctl', ['verify', '-d', domain, '-p', certPath]);
727
+ let output = '';
728
+
729
+ certctl.stdout.on('data', (data) => {
730
+ output += data.toString();
731
+ });
732
+
733
+ certctl.on('close', () => {
734
+ // 解析输出获取剩余天数
735
+ const match = output.match(/剩余 (\d+) 天/);
736
+ if (match) {
737
+ const days = parseInt(match[1]);
738
+ resolve({ days, needsRenew: days <= 30 });
739
+ } else {
740
+ resolve({ days: 0, needsRenew: true });
741
+ }
742
+ });
743
+ });
744
+ }
745
+
746
+ // 定时任务
747
+ setInterval(async () => {
748
+ const domains = ['example.com', 'api.example.com'];
749
+
750
+ for (const domain of domains) {
751
+ const status = await checkCertExpiry(domain, `./certs/${domain}/${domain}.pem`);
752
+
753
+ if (status.needsRenew) {
754
+ console.log(`证书 ${domain} 即将过期 (${status.days} 天),开始续期...`);
755
+ await renewCertificate(domain);
756
+ }
757
+ }
758
+ }, 24 * 60 * 60 * 1000); // 每天检查一次
759
+ ```
760
+
761
+ ---
762
+
763
+ ## 下一步
764
+
765
+ 1. **当前可用**:使用方式一(子进程调用)
766
+ 2. **未来规划**:如果需要真正的 Node.js SDK,可以开发 `certctl-sdk` 包
767
+ 3. **需求反馈**:如果有特定需求,请提交 Issue