feat(core): Refactor SmartAcme core to centralize interest coordination and update dependencies

This commit is contained in:
Philipp Kunz 2025-05-01 09:15:19 +00:00
parent f814038a6a
commit 6fedf0505e
12 changed files with 96 additions and 118 deletions

View File

@ -1,5 +1,13 @@
# Changelog # Changelog
## 2025-05-01 - 7.2.0 - feat(core)
Refactor SmartAcme core to centralize interest coordination and update dependencies
- Moved interest coordination mechanism out of ICertManager implementations and into SmartAcme core
- Updated certificate managers (MemoryCertManager and MongoCertManager) to remove redundant interestMap handling
- Upgraded @push.rocks/tapbundle from 6.0.1 to 6.0.3 in package.json
- Revised readme.plan.md to reflect the new interest coordination approach
## 2025-04-30 - 7.1.0 - feat(certmanagers/integration) ## 2025-04-30 - 7.1.0 - feat(certmanagers/integration)
Add optional wipe methods to certificate managers and update integration tests, plus bump tapbundle dependency Add optional wipe methods to certificate managers and update integration tests, plus bump tapbundle dependency

View File

@ -59,7 +59,7 @@
"@git.zone/tsrun": "^1.3.3", "@git.zone/tsrun": "^1.3.3",
"@git.zone/tstest": "^1.0.96", "@git.zone/tstest": "^1.0.96",
"@push.rocks/qenv": "^6.1.0", "@push.rocks/qenv": "^6.1.0",
"@push.rocks/tapbundle": "^6.0.1", "@push.rocks/tapbundle": "^6.0.3",
"@types/node": "^22.15.3" "@types/node": "^22.15.3"
}, },
"files": [ "files": [

18
pnpm-lock.yaml generated
View File

@ -64,8 +64,8 @@ importers:
specifier: ^6.1.0 specifier: ^6.1.0
version: 6.1.0 version: 6.1.0
'@push.rocks/tapbundle': '@push.rocks/tapbundle':
specifier: ^6.0.1 specifier: ^6.0.3
version: 6.0.1(@aws-sdk/credential-providers@3.797.0)(socks@2.8.4) version: 6.0.3(@aws-sdk/credential-providers@3.797.0)(socks@2.8.4)
'@types/node': '@types/node':
specifier: ^22.15.3 specifier: ^22.15.3
version: 22.15.3 version: 22.15.3
@ -821,8 +821,8 @@ packages:
'@push.rocks/smartexpect@1.6.1': '@push.rocks/smartexpect@1.6.1':
resolution: {integrity: sha512-NFQXEPkGiMNxyvFwKyzDWe3ADYdf8KNvIcV7TGNZZT3uPQtk65te4Q+a1cWErjP/61yE9XdYiQA66QQp+TV9IQ==} resolution: {integrity: sha512-NFQXEPkGiMNxyvFwKyzDWe3ADYdf8KNvIcV7TGNZZT3uPQtk65te4Q+a1cWErjP/61yE9XdYiQA66QQp+TV9IQ==}
'@push.rocks/smartexpect@2.3.2': '@push.rocks/smartexpect@2.4.2':
resolution: {integrity: sha512-cKRPl8GTU4j0zwiQsq8+NiAzBv2iJ9laoGkjEpTs37XhkHIN/EympenvMkXWE4/2HDAlyQm1ZwIl4NRzPBzXbA==} resolution: {integrity: sha512-L+aS1n5rWhf/yOh5R3zPgwycYtDr5FfrDWgasy6ShhN6Zbn/z/AOPbWcF/OpeTmx0XabWB2h5d4xBcCKLl47cQ==}
'@push.rocks/smartfeed@1.0.11': '@push.rocks/smartfeed@1.0.11':
resolution: {integrity: sha512-02uhXxQamgfBo3T12FsAdfyElnpoWuDUb08B2AE60DbIaukVx/7Mi17xwobApY1flNSr5StZDt8N8vxPhBhIXw==} resolution: {integrity: sha512-02uhXxQamgfBo3T12FsAdfyElnpoWuDUb08B2AE60DbIaukVx/7Mi17xwobApY1flNSr5StZDt8N8vxPhBhIXw==}
@ -959,8 +959,8 @@ packages:
'@push.rocks/tapbundle@5.6.3': '@push.rocks/tapbundle@5.6.3':
resolution: {integrity: sha512-hFzsf59rg1K70i45llj7PCyyCZp7JW19XRR+Q1gge1T0pBN8Wi53aYqP/2qtxdMiNVe2s3ESp6VJZv3sLOMYPQ==} resolution: {integrity: sha512-hFzsf59rg1K70i45llj7PCyyCZp7JW19XRR+Q1gge1T0pBN8Wi53aYqP/2qtxdMiNVe2s3ESp6VJZv3sLOMYPQ==}
'@push.rocks/tapbundle@6.0.1': '@push.rocks/tapbundle@6.0.3':
resolution: {integrity: sha512-GeReOjCSF+X+dnHgG+yxl7Tbc9Hk9HKWMqAGLo/B5g8/u4B+V6C+ZA/Sb6Nks8aQlZLm1wXc2ZwxffoYjUHTig==} resolution: {integrity: sha512-SuP14V6TPdtd1y1CYTvwTKJdpHa7EzY55NfaaEMxW4oRKvHgJiOiPEiR/IrtL9tSiDMSfrx12waTMgZheYaBug==}
'@push.rocks/taskbuffer@3.1.7': '@push.rocks/taskbuffer@3.1.7':
resolution: {integrity: sha512-QktGVJPucqQmW/QNGnscf4FAigT1H7JWKFGFdRuDEaOHKFh9qN+PXG3QY7DtZ4jfXdGLxPN4yAufDuPSAJYFnw==} resolution: {integrity: sha512-QktGVJPucqQmW/QNGnscf4FAigT1H7JWKFGFdRuDEaOHKFh9qN+PXG3QY7DtZ4jfXdGLxPN4yAufDuPSAJYFnw==}
@ -5830,7 +5830,7 @@ snapshots:
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
fast-deep-equal: 3.1.3 fast-deep-equal: 3.1.3
'@push.rocks/smartexpect@2.3.2': '@push.rocks/smartexpect@2.4.2':
dependencies: dependencies:
'@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
@ -6240,7 +6240,7 @@ snapshots:
- supports-color - supports-color
- utf-8-validate - utf-8-validate
'@push.rocks/tapbundle@6.0.1(@aws-sdk/credential-providers@3.797.0)(socks@2.8.4)': '@push.rocks/tapbundle@6.0.3(@aws-sdk/credential-providers@3.797.0)(socks@2.8.4)':
dependencies: dependencies:
'@open-wc/testing': 4.0.0 '@open-wc/testing': 4.0.0
'@push.rocks/consolecolor': 2.0.2 '@push.rocks/consolecolor': 2.0.2
@ -6248,7 +6248,7 @@ snapshots:
'@push.rocks/smartcrypto': 2.0.4 '@push.rocks/smartcrypto': 2.0.4
'@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartenv': 5.0.12 '@push.rocks/smartenv': 5.0.12
'@push.rocks/smartexpect': 2.3.2 '@push.rocks/smartexpect': 2.4.2
'@push.rocks/smartfile': 11.2.0 '@push.rocks/smartfile': 11.2.0
'@push.rocks/smartjson': 5.0.20 '@push.rocks/smartjson': 5.0.20
'@push.rocks/smartmongo': 2.0.12(@aws-sdk/credential-providers@3.797.0)(socks@2.8.4) '@push.rocks/smartmongo': 2.0.12(@aws-sdk/credential-providers@3.797.0)(socks@2.8.4)

View File

@ -1,44 +1,16 @@
# Plan: Diskless HTTP-01 Handler and Renaming Existing Handler # Plan: Move interestMap from certmanager to smartacme core
This plan outlines steps to rename the existing filesystem-based HTTP-01 handler to `Http01Webroot` ## Goal
and introduce a new diskless (in-memory) HTTP-01 handler for integration with arbitrary HTTP servers - Pull the interest coordination mechanism out of the ICertManager implementations and into the SmartAcme class.
(e.g., Express).
## 1. Rename existing handler to Http01Webroot ## Steps
- In `ts/handlers/Http01Handler.ts`: 1. Remove `interestMap` from `ICertManager` interface (`ts/interfaces/certmanager.ts`) and its import of `InterestMap`.
- Rename `Http01HandlerOptions` to `Http01WebrootOptions`. 2. Strip out `interestMap` property, initialization, and usage from `MemoryCertManager` and `MongoCertManager` (`ts/certmanagers/*.ts`).
- Rename class `Http01Handler` to `Http01Webroot`. 3. In `Smartacme` class (`ts/smartacme.classes.smartacme.ts`):
- Remove the legacy alias; rename the handler directly. - Add a private `interestMap: plugins.lik.InterestMap<string, SmartacmeCert>` property.
- In `ts/handlers/index.ts`: - Initialize it in the constructor: `this.interestMap = new plugins.lik.InterestMap((domain) => domain);`.
- Export `Http01Webroot` under its new name. - Update `getCertificateForDomain()` and any other consumers to reference `this.interestMap` instead of `this.certmanager.interestMap`.
- Remove any `Http01Handler` export. 4. Remove any tests or code that reference the old `interestMap` on `ICertManager` (if any).
- Update existing tests (e.g., `test.handlers-http01.ts`) to import `Http01Webroot` instead of `Http01Handler`. 5. Run CI (`pnpm build` and `pnpm test`) and fix any regressions.
## 2. Add new diskless (in-memory) HTTP-01 handler Please review and confirm before we begin the refactor.
- Create `ts/handlers/Http01MemoryHandler.ts`:
- Implement `IChallengeHandler<{ token: string; keyAuthorization: string; webPath: string }>`, storing challenges in a private `Map<string, string>`.
- `prepare()`: add token→keyAuthorization mapping.
- `verify()`: no-op.
- `cleanup()`: remove mapping.
- Add `handleRequest(req, res, next?)` method:
- Parse `/.well-known/acme-challenge/:token` from `req.url`.
- If token exists, respond with the key authorization and status 200.
- If missing and `next` provided, call `next()`, otherwise respond 404.
- Export `Http01MemoryHandler` in `ts/handlers/index.ts`.
## 3. Write tests for Http01MemoryHandler
- Create `test/test.handlers-http01-memory.ts`:
- Use `tap` and `expect` to:
1. `prepare()` a challenge.
2. Invoke `handleRequest()` with a fake `req`/`res` to confirm 200 and correct body.
3. `cleanup()` the challenge.
4. Confirm `handleRequest()` now yields 404.
## 4. Update documentation
- Add examples in `readme.md` showing how to use both `Http01Webroot` and the new `Http01MemoryHandler`:
- Sample code for Express integration using `handleRequest`.
## 5. Build and test
- Run `pnpm build` and `pnpm test`, ensuring existing tests are updated for `Http01Webroot` and new tests pass.
Please review and let me know if this plan makes sense before proceeding with implementation.

View File

@ -30,14 +30,17 @@ tap.test('create SmartAcme instance with DNS-01 handler and start', async () =>
expect(smartAcmeInstance).toBeInstanceOf(SmartAcme); expect(smartAcmeInstance).toBeInstanceOf(SmartAcme);
}); });
tap.test('should wipe the certmanager for this test', async () => {
await smartAcmeInstance.certmanager.wipe();
});
tap.test('get a domain certificate via DNS-01 challenge', async () => { tap.test('get a domain certificate via DNS-01 challenge', async () => {
// Replace 'bleu.de' with your test domain if different // Replace 'bleu.de' with your test domain if different
const domain = 'bleu.de'; const domain = 'bleu.de';
const cert = await smartAcmeInstance.getCertificateForDomain(domain); const cert = await smartAcmeInstance.getCertificateForDomain(domain);
console.log(cert); expect(cert).toHaveProperty('domainName');
expect(cert).object.toHaveOwnProperty('domainName');
expect(cert.domainName).toEqual(domain); expect(cert.domainName).toEqual(domain);
expect(cert).object.toHaveOwnProperty('publicKey'); expect(cert).toHaveProperty('publicKey');
expect(typeof cert.publicKey).toEqual('string'); expect(typeof cert.publicKey).toEqual('string');
expect(cert.publicKey.length).toBeGreaterThan(0); expect(cert.publicKey.length).toBeGreaterThan(0);
}); });

View File

@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@push.rocks/smartacme', name: '@push.rocks/smartacme',
version: '7.1.0', version: '7.2.0',
description: 'A TypeScript-based ACME client for LetsEncrypt certificate management with a focus on simplicity and power.' description: 'A TypeScript-based ACME client for LetsEncrypt certificate management with a focus on simplicity and power.'
} }

2
ts/certmanagers/index.ts Normal file
View File

@ -0,0 +1,2 @@
export * from './memory.js';
export * from './mongo.js';

49
ts/certmanagers/memory.ts Normal file
View File

@ -0,0 +1,49 @@
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
}
/**
* Wipe all certificates from the in-memory store (for testing)
*/
public async wipe(): Promise<void> {
this.certs.clear();
// reset interest map
this.interestMap = new plugins.lik.InterestMap((domain) => domain);
}
}

View File

@ -1,52 +1,6 @@
import * as plugins from './smartacme.plugins.js'; import * as plugins from '../smartacme.plugins.js';
import type { ICertManager } from './interfaces/certmanager.js'; import type { ICertManager } from '../interfaces/certmanager.js';
import { SmartacmeCert } from './smartacme.classes.cert.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
}
/**
* Wipe all certificates from the in-memory store (for testing)
*/
public async wipe(): Promise<void> {
this.certs.clear();
// reset interest map
this.interestMap = new plugins.lik.InterestMap((domain) => domain);
}
}
/** /**
* MongoDB-backed certificate manager using EasyStore from smartdata. * MongoDB-backed certificate manager using EasyStore from smartdata.
@ -104,4 +58,4 @@ export class MongoCertManager implements ICertManager {
// reset interest map // reset interest map
this.interestMap = new plugins.lik.InterestMap((domain) => domain); this.interestMap = new plugins.lik.InterestMap((domain) => domain);
} }
} }

View File

@ -1,4 +1,4 @@
export * from './smartacme.classes.smartacme.js'; export * from './smartacme.classes.smartacme.js';
export { SmartacmeCert as Cert } from './smartacme.classes.cert.js'; export { SmartacmeCert as Cert } from './smartacme.classes.cert.js';
export type { ICertManager } from './interfaces/certmanager.js'; export type { ICertManager } from './interfaces/certmanager.js';
export { MemoryCertManager, MongoCertManager } from './certmanagers.js'; export { MemoryCertManager, MongoCertManager } from './certmanagers/index.js';

View File

@ -1,4 +1,3 @@
import type { InterestMap } from '@push.rocks/lik';
import type { SmartacmeCert } from '../smartacme.classes.cert.js'; import type { SmartacmeCert } from '../smartacme.classes.cert.js';
// (ICertRecord removed; use SmartacmeCert directly) // (ICertRecord removed; use SmartacmeCert directly)
@ -9,10 +8,6 @@ import type { SmartacmeCert } from '../smartacme.classes.cert.js';
* file-based, Redis, etc.). * file-based, Redis, etc.).
*/ */
export interface ICertManager { export interface ICertManager {
/**
* Map for coordinating concurrent certificate requests.
*/
interestMap: InterestMap<string, SmartacmeCert>;
/** /**
* Initialize the store (e.g., connect to database). * Initialize the store (e.g., connect to database).
*/ */
@ -37,5 +32,5 @@ export interface ICertManager {
/** /**
* Optional: wipe all stored certificates (e.g., for integration testing) * Optional: wipe all stored certificates (e.g., for integration testing)
*/ */
wipe?(): Promise<void>; wipe(): Promise<void>;
} }

View File

@ -63,7 +63,7 @@ export class SmartAcme {
// certificate manager for persistence (implements ICertManager) // certificate manager for persistence (implements ICertManager)
private certmanager: ICertManager; public certmanager: ICertManager;
private certmatcher: SmartacmeCertMatcher; private certmatcher: SmartacmeCertMatcher;
// retry/backoff configuration (resolved with defaults) // retry/backoff configuration (resolved with defaults)
private retryOptions: { retries: number; factor: number; minTimeoutMs: number; maxTimeoutMs: number }; private retryOptions: { retries: number; factor: number; minTimeoutMs: number; maxTimeoutMs: number };
@ -116,11 +116,6 @@ export class SmartAcme {
} }
this.certmanager = this.options.certManager; this.certmanager = this.options.certManager;
await this.certmanager.init(); await this.certmanager.init();
// For integration environment, clear any existing certificates to avoid stale entries
if (this.options.environment === 'integration' && typeof (this.certmanager as any).wipe === 'function') {
// this.logger.log('warn', 'Wiping existing certificates for integration environment');
// await (this.certmanager as any).wipe();
}
// CertMatcher // CertMatcher
this.certmatcher = new SmartacmeCertMatcher(); this.certmatcher = new SmartacmeCertMatcher();