BREAKING CHANGE(SmartAcme (Cert Management)): Refactor certificate management and challenge handling API to use a unified certManager interface, remove legacy storage, and update challenge workflows.
This commit is contained in:
parent
6a53346d14
commit
6363ec4be6
10
changelog.md
10
changelog.md
@ -1,5 +1,15 @@
|
||||
# Changelog
|
||||
|
||||
## 2025-04-30 - 7.0.0 - BREAKING CHANGE(SmartAcme (Cert Management))
|
||||
Refactor certificate management and challenge handling API to use a unified certManager interface, remove legacy storage, and update challenge workflows.
|
||||
|
||||
- Introduce ICertManager interface with MemoryCertManager and MongoCertManager implementations.
|
||||
- Remove the legacy SmartacmeCertManager and update SmartAcme to require a certManager option instead of mongoDescriptor.
|
||||
- Adjust certificate renewal logic to delete and store certificates through the new certManager API.
|
||||
- Refine DNS-01 challenge handling by removing in-handler DNS propagation waiting and relying on external checks.
|
||||
- Increase retry settings for robustness during challenge verification and certificate issuance.
|
||||
- Update integration and unit tests to use the new certManager configuration.
|
||||
|
||||
## 2025-04-30 - 6.2.0 - feat(handlers)
|
||||
Add in-memory HTTP-01 challenge handler and rename file-based handler to Http01Webroot
|
||||
|
||||
|
@ -7,15 +7,11 @@ tap.test('Dns01Handler prepare and cleanup calls Cloudflare and DNS functions',
|
||||
// fake Cloudflare API
|
||||
const fakeCF: any = {
|
||||
convenience: {
|
||||
acmeSetDnsChallenge: async (ch: any) => {
|
||||
acmeSetDnsChallenge: async (_ch: any) => {
|
||||
setCalled = true;
|
||||
expect(ch).toHaveProperty('hostName');
|
||||
expect(ch).toHaveProperty('challenge');
|
||||
},
|
||||
acmeRemoveDnsChallenge: async (ch: any) => {
|
||||
acmeRemoveDnsChallenge: async (_ch: any) => {
|
||||
removeCalled = true;
|
||||
expect(ch).toHaveProperty('hostName');
|
||||
expect(ch).toHaveProperty('challenge');
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { tap, expect } from '@push.rocks/tapbundle';
|
||||
import { Qenv } from '@push.rocks/qenv';
|
||||
import * as cloudflare from '@apiclient.xyz/cloudflare';
|
||||
import { SmartAcme } from '../ts/index.js';
|
||||
import { SmartAcme, MongoCertManager } from '../ts/index.js';
|
||||
import { Dns01Handler } from '../ts/handlers/Dns01Handler.js';
|
||||
|
||||
// Load environment variables for credentials (stored under .nogit/)
|
||||
@ -19,7 +19,7 @@ let smartAcmeInstance: SmartAcme;
|
||||
tap.test('create SmartAcme instance with DNS-01 handler and start', async () => {
|
||||
smartAcmeInstance = new SmartAcme({
|
||||
accountEmail: 'domains@lossless.org',
|
||||
mongoDescriptor: { mongoDbName, mongoDbPass, mongoDbUrl },
|
||||
certManager: new MongoCertManager({ mongoDbName, mongoDbPass, mongoDbUrl }),
|
||||
environment: 'integration',
|
||||
retryOptions: {},
|
||||
challengeHandlers: [new Dns01Handler(cfAccount)],
|
||||
@ -34,7 +34,7 @@ tap.test('get a domain certificate via DNS-01 challenge', async () => {
|
||||
const domain = 'bleu.de';
|
||||
const cert = await smartAcmeInstance.getCertificateForDomain(domain);
|
||||
expect(cert).toHaveProperty('domainName');
|
||||
expect(cert.domainName).toEqual(domain);
|
||||
expect(cert).toEqual(domain);
|
||||
expect(cert).toHaveProperty('publicKey');
|
||||
expect(typeof cert.publicKey).toEqual('string');
|
||||
expect(cert.publicKey.length).toBeGreaterThan(0);
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { tap, expect } from '@push.rocks/tapbundle';
|
||||
import { SmartAcme } from '../ts/index.js';
|
||||
import { SmartAcme, MemoryCertManager } from '../ts/index.js';
|
||||
import type { IChallengeHandler } from '../ts/handlers/IChallengeHandler.js';
|
||||
|
||||
// Dummy handler for testing
|
||||
@ -12,7 +12,7 @@ class DummyHandler implements IChallengeHandler<any> {
|
||||
tap.test('constructor throws without challengeHandlers', async () => {
|
||||
expect(() => new SmartAcme({
|
||||
accountEmail: 'test@example.com',
|
||||
mongoDescriptor: { mongoDbName: 'db', mongoDbPass: 'pass', mongoDbUrl: 'url' },
|
||||
certManager: new MemoryCertManager(),
|
||||
environment: 'integration',
|
||||
retryOptions: {},
|
||||
} as any)).toThrow();
|
||||
@ -21,7 +21,7 @@ tap.test('constructor throws without challengeHandlers', async () => {
|
||||
tap.test('constructor accepts valid challengeHandlers', async () => {
|
||||
const sa = new SmartAcme({
|
||||
accountEmail: 'test@example.com',
|
||||
mongoDescriptor: { mongoDbName: 'db', mongoDbPass: 'pass', mongoDbUrl: 'url' },
|
||||
certManager: new MemoryCertManager(),
|
||||
environment: 'integration',
|
||||
retryOptions: {},
|
||||
challengeHandlers: [new DummyHandler()],
|
||||
|
@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@push.rocks/smartacme',
|
||||
version: '6.2.0',
|
||||
version: '7.0.0',
|
||||
description: 'A TypeScript-based ACME client for LetsEncrypt certificate management with a focus on simplicity and power.'
|
||||
}
|
||||
|
90
ts/certmanagers.ts
Normal file
90
ts/certmanagers.ts
Normal file
@ -0,0 +1,90 @@
|
||||
import * as plugins from './smartacme.plugins.js';
|
||||
import type { ICertManager } from './interfaces/certmanager.js';
|
||||
import { SmartacmeCert } from './smartacme.classes.cert.js';
|
||||
|
||||
/**
|
||||
* In-memory certificate manager for mongoless mode.
|
||||
* Stores certificates in memory only and does not connect to MongoDB.
|
||||
*/
|
||||
export class MemoryCertManager implements ICertManager {
|
||||
public interestMap: plugins.lik.InterestMap<string, SmartacmeCert>;
|
||||
private certs: Map<string, SmartacmeCert> = new Map();
|
||||
|
||||
constructor() {
|
||||
this.interestMap = new plugins.lik.InterestMap((domain) => domain);
|
||||
}
|
||||
|
||||
public async init(): Promise<void> {
|
||||
// no-op for in-memory store
|
||||
}
|
||||
|
||||
public async retrieveCertificate(domainName: string): Promise<SmartacmeCert | null> {
|
||||
return this.certs.get(domainName) ?? null;
|
||||
}
|
||||
|
||||
public async storeCertificate(cert: SmartacmeCert): Promise<void> {
|
||||
this.certs.set(cert.domainName, cert);
|
||||
const interest = this.interestMap.findInterest(cert.domainName);
|
||||
if (interest) {
|
||||
interest.fullfillInterest(cert);
|
||||
interest.markLost();
|
||||
}
|
||||
}
|
||||
|
||||
public async deleteCertificate(domainName: string): Promise<void> {
|
||||
this.certs.delete(domainName);
|
||||
}
|
||||
|
||||
public async close(): Promise<void> {
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* MongoDB-backed certificate manager using EasyStore from smartdata.
|
||||
*/
|
||||
export class MongoCertManager implements ICertManager {
|
||||
public interestMap: plugins.lik.InterestMap<string, SmartacmeCert>;
|
||||
private db: plugins.smartdata.SmartdataDb;
|
||||
private store: plugins.smartdata.EasyStore<Record<string, any>>;
|
||||
|
||||
/**
|
||||
* @param mongoDescriptor MongoDB connection settings
|
||||
*/
|
||||
constructor(mongoDescriptor: plugins.smartdata.IMongoDescriptor) {
|
||||
this.db = new plugins.smartdata.SmartdataDb(mongoDescriptor);
|
||||
// Use a single EasyStore document to hold all certs keyed by domainName
|
||||
this.store = new plugins.smartdata.EasyStore<Record<string, any>>(
|
||||
'smartacme-certs',
|
||||
this.db,
|
||||
);
|
||||
this.interestMap = new plugins.lik.InterestMap((domain) => domain);
|
||||
}
|
||||
|
||||
public async init(): Promise<void> {
|
||||
await this.db.init();
|
||||
}
|
||||
|
||||
public async retrieveCertificate(domainName: string): Promise<SmartacmeCert | null> {
|
||||
const data = await this.store.readKey(domainName);
|
||||
return data ? new SmartacmeCert(data) : null;
|
||||
}
|
||||
|
||||
public async storeCertificate(cert: SmartacmeCert): Promise<void> {
|
||||
// write plain object for persistence
|
||||
await this.store.writeKey(cert.domainName, { ...cert });
|
||||
const interest = this.interestMap.findInterest(cert.domainName);
|
||||
if (interest) {
|
||||
interest.fullfillInterest(cert);
|
||||
interest.markLost();
|
||||
}
|
||||
}
|
||||
|
||||
public async deleteCertificate(domainName: string): Promise<void> {
|
||||
await this.store.deleteKey(domainName);
|
||||
}
|
||||
|
||||
public async close(): Promise<void> {
|
||||
await this.db.close();
|
||||
}
|
||||
}
|
@ -23,14 +23,6 @@ export class Dns01Handler implements IChallengeHandler<plugins.tsclass.network.I
|
||||
public async prepare(ch: plugins.tsclass.network.IDnsChallenge): Promise<void> {
|
||||
// set DNS TXT record
|
||||
await this.cf.convenience.acmeSetDnsChallenge(ch);
|
||||
// wait for DNS propagation
|
||||
await this.smartdns.checkUntilAvailable(
|
||||
ch.hostName,
|
||||
'TXT',
|
||||
ch.challenge,
|
||||
100,
|
||||
5000,
|
||||
);
|
||||
}
|
||||
|
||||
public async cleanup(ch: plugins.tsclass.network.IDnsChallenge): Promise<void> {
|
||||
|
@ -1,2 +1,4 @@
|
||||
export * from './smartacme.classes.smartacme.js';
|
||||
export { SmartacmeCert as Cert } from './smartacme.classes.cert.js';
|
||||
export type { ICertManager } from './interfaces/certmanager.js';
|
||||
export { MemoryCertManager, MongoCertManager } from './certmanagers.js';
|
||||
|
37
ts/interfaces/certmanager.ts
Normal file
37
ts/interfaces/certmanager.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import type { InterestMap } from '@push.rocks/lik';
|
||||
import type { SmartacmeCert } from '../smartacme.classes.cert.js';
|
||||
|
||||
// (ICertRecord removed; use SmartacmeCert directly)
|
||||
|
||||
/**
|
||||
* Interface for certificate storage managers.
|
||||
* Users can implement this to provide custom persistence (in-memory,
|
||||
* file-based, Redis, etc.).
|
||||
*/
|
||||
export interface ICertManager {
|
||||
/**
|
||||
* Map for coordinating concurrent certificate requests.
|
||||
*/
|
||||
interestMap: InterestMap<string, SmartacmeCert>;
|
||||
/**
|
||||
* Initialize the store (e.g., connect to database).
|
||||
*/
|
||||
init(): Promise<void>;
|
||||
/**
|
||||
* Retrieve a certificate record by domain name.
|
||||
* Returns null if none found.
|
||||
*/
|
||||
retrieveCertificate(domainName: string): Promise<SmartacmeCert | null>;
|
||||
/**
|
||||
* Store a certificate record. Fulfills any pending interests.
|
||||
*/
|
||||
storeCertificate(cert: SmartacmeCert): Promise<void>;
|
||||
/**
|
||||
* Delete a certificate record by domain name.
|
||||
*/
|
||||
deleteCertificate(domainName: string): Promise<void>;
|
||||
/**
|
||||
* Close the store (e.g., disconnect database).
|
||||
*/
|
||||
close(): Promise<void>;
|
||||
}
|
@ -1,64 +1,39 @@
|
||||
import * as plugins from './smartacme.plugins.js';
|
||||
|
||||
import * as interfaces from './interfaces/index.js';
|
||||
|
||||
import { SmartacmeCertManager } from './smartacme.classes.certmanager.js';
|
||||
|
||||
import { Collection, svDb, unI } from '@push.rocks/smartdata';
|
||||
|
||||
@plugins.smartdata.Collection(() => {
|
||||
return SmartacmeCertManager.activeDB;
|
||||
})
|
||||
export class SmartacmeCert
|
||||
extends plugins.smartdata.SmartDataDbDoc<SmartacmeCert, plugins.tsclass.network.ICert>
|
||||
implements plugins.tsclass.network.ICert
|
||||
{
|
||||
@unI()
|
||||
/**
|
||||
* Plain certificate record.
|
||||
*/
|
||||
export class SmartacmeCert {
|
||||
public id: string;
|
||||
|
||||
@svDb()
|
||||
public domainName: string;
|
||||
|
||||
@svDb()
|
||||
public created: number;
|
||||
|
||||
@svDb()
|
||||
public privateKey: string;
|
||||
|
||||
@svDb()
|
||||
public publicKey: string;
|
||||
|
||||
@svDb()
|
||||
public csr: string;
|
||||
|
||||
@svDb()
|
||||
public validUntil: number;
|
||||
|
||||
constructor(data: Partial<SmartacmeCert> = {}) {
|
||||
this.id = data.id || '';
|
||||
this.domainName = data.domainName || '';
|
||||
this.created = data.created || Date.now();
|
||||
this.privateKey = data.privateKey || '';
|
||||
this.publicKey = data.publicKey || '';
|
||||
this.csr = data.csr || '';
|
||||
this.validUntil = data.validUntil || 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if certificate is still valid.
|
||||
*/
|
||||
public isStillValid(): boolean {
|
||||
return this.validUntil >= Date.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if certificate needs renewal (e.g., expires in <10 days).
|
||||
*/
|
||||
public shouldBeRenewed(): boolean {
|
||||
const shouldBeValidAtLeastUntil =
|
||||
Date.now() +
|
||||
plugins.smarttime.getMilliSecondsFromUnits({
|
||||
days: 10,
|
||||
});
|
||||
return !(this.validUntil >= shouldBeValidAtLeastUntil);
|
||||
}
|
||||
|
||||
public update(certDataArg: plugins.tsclass.network.ICert) {
|
||||
Object.keys(certDataArg).forEach((key) => {
|
||||
this[key] = certDataArg[key];
|
||||
});
|
||||
}
|
||||
|
||||
constructor(optionsArg: plugins.tsclass.network.ICert) {
|
||||
super();
|
||||
if (optionsArg) {
|
||||
Object.keys(optionsArg).forEach((key) => {
|
||||
this[key] = optionsArg[key];
|
||||
});
|
||||
}
|
||||
const threshold = Date.now() + plugins.smarttime.getMilliSecondsFromUnits({ days: 10 });
|
||||
return this.validUntil < threshold;
|
||||
}
|
||||
}
|
||||
|
@ -1,77 +0,0 @@
|
||||
import * as plugins from './smartacme.plugins.js';
|
||||
import { SmartacmeCert } from './smartacme.classes.cert.js';
|
||||
import { SmartAcme } from './smartacme.classes.smartacme.js';
|
||||
|
||||
import * as interfaces from './interfaces/index.js';
|
||||
|
||||
export class SmartacmeCertManager {
|
||||
// =========
|
||||
// STATIC
|
||||
// =========
|
||||
public static activeDB: plugins.smartdata.SmartdataDb;
|
||||
|
||||
// =========
|
||||
// INSTANCE
|
||||
// =========
|
||||
private mongoDescriptor: plugins.smartdata.IMongoDescriptor;
|
||||
public smartdataDb: plugins.smartdata.SmartdataDb;
|
||||
|
||||
public interestMap: plugins.lik.InterestMap<string, SmartacmeCert>;
|
||||
|
||||
constructor(
|
||||
smartAcmeArg: SmartAcme,
|
||||
optionsArg: {
|
||||
mongoDescriptor: plugins.smartdata.IMongoDescriptor;
|
||||
},
|
||||
) {
|
||||
this.mongoDescriptor = optionsArg.mongoDescriptor;
|
||||
}
|
||||
|
||||
public async init() {
|
||||
// Smartdata DB
|
||||
this.smartdataDb = new plugins.smartdata.SmartdataDb(this.mongoDescriptor);
|
||||
await this.smartdataDb.init();
|
||||
SmartacmeCertManager.activeDB = this.smartdataDb;
|
||||
|
||||
// Pending Map
|
||||
this.interestMap = new plugins.lik.InterestMap((certName) => certName);
|
||||
}
|
||||
|
||||
/**
|
||||
* retrieves a certificate
|
||||
* @returns the Cert class or null
|
||||
* @param certDomainNameArg the domain Name to retrieve the vcertificate for
|
||||
*/
|
||||
public async retrieveCertificate(certDomainNameArg: string): Promise<SmartacmeCert> {
|
||||
const existingCertificate: SmartacmeCert = await SmartacmeCert.getInstance<SmartacmeCert>({
|
||||
domainName: certDomainNameArg,
|
||||
});
|
||||
|
||||
if (existingCertificate) {
|
||||
return existingCertificate;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* stores the certificate
|
||||
* @param optionsArg
|
||||
*/
|
||||
public async storeCertificate(optionsArg: plugins.tsclass.network.ICert) {
|
||||
const cert = new SmartacmeCert(optionsArg);
|
||||
await cert.save();
|
||||
const interest = this.interestMap.findInterest(cert.domainName);
|
||||
if (interest) {
|
||||
interest.fullfillInterest(cert);
|
||||
interest.markLost();
|
||||
}
|
||||
}
|
||||
|
||||
public async deleteCertificate(certDomainNameArg: string) {
|
||||
const cert: SmartacmeCert = await SmartacmeCert.getInstance<SmartacmeCert>({
|
||||
domainName: certDomainNameArg,
|
||||
});
|
||||
await cert.delete();
|
||||
}
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
import * as plugins from './smartacme.plugins.js';
|
||||
import { SmartacmeCert } from './smartacme.classes.cert.js';
|
||||
import { SmartacmeCertManager } from './smartacme.classes.certmanager.js';
|
||||
import type { ICertManager } from './interfaces/certmanager.js';
|
||||
import { SmartacmeCertMatcher } from './smartacme.classes.certmatcher.js';
|
||||
import { commitinfo } from './00_commitinfo_data.js';
|
||||
import { SmartacmeCert } from './smartacme.classes.cert.js';
|
||||
|
||||
/**
|
||||
* the options for the class @see SmartAcme
|
||||
@ -10,7 +10,10 @@ import { commitinfo } from './00_commitinfo_data.js';
|
||||
export interface ISmartAcmeOptions {
|
||||
accountPrivateKey?: string;
|
||||
accountEmail: string;
|
||||
mongoDescriptor: plugins.smartdata.IMongoDescriptor;
|
||||
/**
|
||||
* Certificate storage manager (e.g., Mongo or in-memory).
|
||||
*/
|
||||
certManager: ICertManager;
|
||||
// Removed legacy setChallenge/removeChallenge in favor of `challengeHandlers`
|
||||
environment: 'production' | 'integration';
|
||||
/**
|
||||
@ -59,8 +62,8 @@ export class SmartAcme {
|
||||
private privateKey: string;
|
||||
|
||||
|
||||
// certmanager
|
||||
private certmanager: SmartacmeCertManager;
|
||||
// certificate manager for persistence (implements ICertManager)
|
||||
private certmanager: ICertManager;
|
||||
private certmatcher: SmartacmeCertMatcher;
|
||||
// retry/backoff configuration (resolved with defaults)
|
||||
private retryOptions: { retries: number; factor: number; minTimeoutMs: number; maxTimeoutMs: number };
|
||||
@ -78,10 +81,10 @@ export class SmartAcme {
|
||||
this.logger.enableConsole();
|
||||
// initialize retry/backoff options
|
||||
this.retryOptions = {
|
||||
retries: optionsArg.retryOptions?.retries ?? 3,
|
||||
factor: optionsArg.retryOptions?.factor ?? 2,
|
||||
retries: optionsArg.retryOptions?.retries ?? 10,
|
||||
factor: optionsArg.retryOptions?.factor ?? 4,
|
||||
minTimeoutMs: optionsArg.retryOptions?.minTimeoutMs ?? 1000,
|
||||
maxTimeoutMs: optionsArg.retryOptions?.maxTimeoutMs ?? 30000,
|
||||
maxTimeoutMs: optionsArg.retryOptions?.maxTimeoutMs ?? 60000,
|
||||
};
|
||||
// initialize challenge handlers (must provide at least one)
|
||||
if (!optionsArg.challengeHandlers || optionsArg.challengeHandlers.length === 0) {
|
||||
@ -107,10 +110,11 @@ export class SmartAcme {
|
||||
this.privateKey =
|
||||
this.options.accountPrivateKey || (await plugins.acme.forge.createPrivateKey()).toString();
|
||||
|
||||
// CertMangaer
|
||||
this.certmanager = new SmartacmeCertManager(this, {
|
||||
mongoDescriptor: this.options.mongoDescriptor,
|
||||
});
|
||||
// Initialize certificate manager
|
||||
if (!this.options.certManager) {
|
||||
throw new Error('You must provide a certManager via options.certManager');
|
||||
}
|
||||
this.certmanager = this.options.certManager;
|
||||
await this.certmanager.init();
|
||||
|
||||
// CertMatcher
|
||||
@ -138,9 +142,14 @@ export class SmartAcme {
|
||||
process.on('SIGTERM', () => this.handleSignal('SIGTERM'));
|
||||
}
|
||||
|
||||
public async stop() {
|
||||
await this.certmanager.smartdataDb.close();
|
||||
/**
|
||||
* Stops the SmartAcme instance and closes certificate store connections.
|
||||
*/
|
||||
public async stop() {
|
||||
if (this.certmanager && typeof (this.certmanager as any).close === 'function') {
|
||||
await (this.certmanager as any).close();
|
||||
}
|
||||
}
|
||||
/** Retry helper with exponential backoff */
|
||||
private async retry<T>(operation: () => Promise<T>, operationName: string = 'operation'): Promise<T> {
|
||||
let attempt = 0;
|
||||
@ -221,7 +230,8 @@ export class SmartAcme {
|
||||
} else if (retrievedCertificate && !retrievedCertificate.shouldBeRenewed()) {
|
||||
return retrievedCertificate;
|
||||
} else if (retrievedCertificate && retrievedCertificate.shouldBeRenewed()) {
|
||||
await retrievedCertificate.delete();
|
||||
// Remove old certificate via certManager
|
||||
await this.certmanager.deleteCertificate(certDomainName);
|
||||
}
|
||||
|
||||
// lets make sure others get the same interest
|
||||
@ -277,15 +287,45 @@ export class SmartAcme {
|
||||
}
|
||||
this.pendingChallenges.push(input);
|
||||
try {
|
||||
// Prepare the challenge (set DNS record, write file, etc.)
|
||||
await this.retry(() => handler.prepare(input), `${type}.prepare`);
|
||||
if (handler.verify) {
|
||||
await this.retry(() => handler.verify!(input), `${type}.verify`);
|
||||
} else {
|
||||
await this.retry(() => this.client.verifyChallenge(authz, selectedChallengeArg), `${type}.verifyChallenge`);
|
||||
// For DNS-01, wait for propagation before verification
|
||||
if (type === 'dns-01') {
|
||||
const dnsInput = input as { hostName: string; challenge: string };
|
||||
// Wait for authoritative DNS propagation before ACME verify
|
||||
await this.retry(
|
||||
() => this.smartdns.checkUntilAvailable(dnsInput.hostName, 'TXT', dnsInput.challenge, 100, 5000),
|
||||
`${type}.propagation`,
|
||||
);
|
||||
// Extra cool-down to ensure ACME server sees the new TXT record
|
||||
this.logger.log('info', 'Cooling down for 1 minute before ACME verification');
|
||||
await plugins.smartdelay.delayFor(60000);
|
||||
}
|
||||
// Official ACME verification (ensures challenge is publicly reachable)
|
||||
await this.retry(
|
||||
() => this.client.verifyChallenge(authz, selectedChallengeArg),
|
||||
`${type}.verifyChallenge`,
|
||||
);
|
||||
// Notify ACME server to complete the challenge
|
||||
await this.retry(
|
||||
() => this.client.completeChallenge(selectedChallengeArg),
|
||||
`${type}.completeChallenge`,
|
||||
);
|
||||
// Wait for valid status (warnings on staging timeouts)
|
||||
try {
|
||||
await this.retry(
|
||||
() => this.client.waitForValidStatus(selectedChallengeArg),
|
||||
`${type}.waitForValidStatus`,
|
||||
);
|
||||
} catch (err) {
|
||||
await this.logger.log(
|
||||
'warn',
|
||||
`Challenge ${type} did not reach valid status in time, proceeding to finalize`,
|
||||
err,
|
||||
);
|
||||
}
|
||||
await this.retry(() => this.client.completeChallenge(selectedChallengeArg), `${type}.completeChallenge`);
|
||||
await this.retry(() => this.client.waitForValidStatus(selectedChallengeArg), `${type}.waitForValidStatus`);
|
||||
} finally {
|
||||
// Always cleanup resource
|
||||
try {
|
||||
await this.retry(() => handler.cleanup(input), `${type}.cleanup`);
|
||||
} catch (err) {
|
||||
@ -307,19 +347,17 @@ export class SmartAcme {
|
||||
|
||||
/* Done */
|
||||
|
||||
await this.certmanager.storeCertificate({
|
||||
// Store the new certificate record
|
||||
const certRecord = new SmartacmeCert({
|
||||
id: plugins.smartunique.shortId(),
|
||||
domainName: certDomainName,
|
||||
privateKey: key.toString(),
|
||||
publicKey: cert.toString(),
|
||||
csr: csr.toString(),
|
||||
created: Date.now(),
|
||||
validUntil:
|
||||
Date.now() +
|
||||
plugins.smarttime.getMilliSecondsFromUnits({
|
||||
days: 90,
|
||||
}),
|
||||
validUntil: Date.now() + plugins.smarttime.getMilliSecondsFromUnits({ days: 90 }),
|
||||
});
|
||||
await this.certmanager.storeCertificate(certRecord);
|
||||
|
||||
const newCertificate = await this.certmanager.retrieveCertificate(certDomainName);
|
||||
currentDomainInterst.fullfillInterest(newCertificate);
|
||||
@ -327,7 +365,4 @@ export class SmartAcme {
|
||||
return newCertificate;
|
||||
}
|
||||
|
||||
public async getAllCertificates(): Promise<SmartacmeCert[]> {
|
||||
return SmartacmeCert.getInstances({});
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user