diff --git a/changelog.md b/changelog.md index 1e84028..6a2a978 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,13 @@ # 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) Add optional wipe methods to certificate managers and update integration tests, plus bump tapbundle dependency diff --git a/package.json b/package.json index 902dcf9..72df2a1 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ "@git.zone/tsrun": "^1.3.3", "@git.zone/tstest": "^1.0.96", "@push.rocks/qenv": "^6.1.0", - "@push.rocks/tapbundle": "^6.0.1", + "@push.rocks/tapbundle": "^6.0.3", "@types/node": "^22.15.3" }, "files": [ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d665271..0b68a9e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -64,8 +64,8 @@ importers: specifier: ^6.1.0 version: 6.1.0 '@push.rocks/tapbundle': - specifier: ^6.0.1 - version: 6.0.1(@aws-sdk/credential-providers@3.797.0)(socks@2.8.4) + specifier: ^6.0.3 + version: 6.0.3(@aws-sdk/credential-providers@3.797.0)(socks@2.8.4) '@types/node': specifier: ^22.15.3 version: 22.15.3 @@ -821,8 +821,8 @@ packages: '@push.rocks/smartexpect@1.6.1': resolution: {integrity: sha512-NFQXEPkGiMNxyvFwKyzDWe3ADYdf8KNvIcV7TGNZZT3uPQtk65te4Q+a1cWErjP/61yE9XdYiQA66QQp+TV9IQ==} - '@push.rocks/smartexpect@2.3.2': - resolution: {integrity: sha512-cKRPl8GTU4j0zwiQsq8+NiAzBv2iJ9laoGkjEpTs37XhkHIN/EympenvMkXWE4/2HDAlyQm1ZwIl4NRzPBzXbA==} + '@push.rocks/smartexpect@2.4.2': + resolution: {integrity: sha512-L+aS1n5rWhf/yOh5R3zPgwycYtDr5FfrDWgasy6ShhN6Zbn/z/AOPbWcF/OpeTmx0XabWB2h5d4xBcCKLl47cQ==} '@push.rocks/smartfeed@1.0.11': resolution: {integrity: sha512-02uhXxQamgfBo3T12FsAdfyElnpoWuDUb08B2AE60DbIaukVx/7Mi17xwobApY1flNSr5StZDt8N8vxPhBhIXw==} @@ -959,8 +959,8 @@ packages: '@push.rocks/tapbundle@5.6.3': resolution: {integrity: sha512-hFzsf59rg1K70i45llj7PCyyCZp7JW19XRR+Q1gge1T0pBN8Wi53aYqP/2qtxdMiNVe2s3ESp6VJZv3sLOMYPQ==} - '@push.rocks/tapbundle@6.0.1': - resolution: {integrity: sha512-GeReOjCSF+X+dnHgG+yxl7Tbc9Hk9HKWMqAGLo/B5g8/u4B+V6C+ZA/Sb6Nks8aQlZLm1wXc2ZwxffoYjUHTig==} + '@push.rocks/tapbundle@6.0.3': + resolution: {integrity: sha512-SuP14V6TPdtd1y1CYTvwTKJdpHa7EzY55NfaaEMxW4oRKvHgJiOiPEiR/IrtL9tSiDMSfrx12waTMgZheYaBug==} '@push.rocks/taskbuffer@3.1.7': resolution: {integrity: sha512-QktGVJPucqQmW/QNGnscf4FAigT1H7JWKFGFdRuDEaOHKFh9qN+PXG3QY7DtZ4jfXdGLxPN4yAufDuPSAJYFnw==} @@ -5830,7 +5830,7 @@ snapshots: '@push.rocks/smartpromise': 4.2.3 fast-deep-equal: 3.1.3 - '@push.rocks/smartexpect@2.3.2': + '@push.rocks/smartexpect@2.4.2': dependencies: '@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartpromise': 4.2.3 @@ -6240,7 +6240,7 @@ snapshots: - supports-color - 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: '@open-wc/testing': 4.0.0 '@push.rocks/consolecolor': 2.0.2 @@ -6248,7 +6248,7 @@ snapshots: '@push.rocks/smartcrypto': 2.0.4 '@push.rocks/smartdelay': 3.0.5 '@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/smartjson': 5.0.20 '@push.rocks/smartmongo': 2.0.12(@aws-sdk/credential-providers@3.797.0)(socks@2.8.4) diff --git a/readme.plan.md b/readme.plan.md index 767554f..a7d3337 100644 --- a/readme.plan.md +++ b/readme.plan.md @@ -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` -and introduce a new diskless (in-memory) HTTP-01 handler for integration with arbitrary HTTP servers -(e.g., Express). +## Goal +- Pull the interest coordination mechanism out of the ICertManager implementations and into the SmartAcme class. -## 1. Rename existing handler to Http01Webroot -- In `ts/handlers/Http01Handler.ts`: - - Rename `Http01HandlerOptions` to `Http01WebrootOptions`. - - Rename class `Http01Handler` to `Http01Webroot`. - - Remove the legacy alias; rename the handler directly. - - In `ts/handlers/index.ts`: - - Export `Http01Webroot` under its new name. - - Remove any `Http01Handler` export. - - Update existing tests (e.g., `test.handlers-http01.ts`) to import `Http01Webroot` instead of `Http01Handler`. +## Steps +1. Remove `interestMap` from `ICertManager` interface (`ts/interfaces/certmanager.ts`) and its import of `InterestMap`. +2. Strip out `interestMap` property, initialization, and usage from `MemoryCertManager` and `MongoCertManager` (`ts/certmanagers/*.ts`). +3. In `Smartacme` class (`ts/smartacme.classes.smartacme.ts`): + - Add a private `interestMap: plugins.lik.InterestMap` property. + - Initialize it in the constructor: `this.interestMap = new plugins.lik.InterestMap((domain) => domain);`. + - Update `getCertificateForDomain()` and any other consumers to reference `this.interestMap` instead of `this.certmanager.interestMap`. +4. Remove any tests or code that reference the old `interestMap` on `ICertManager` (if any). +5. Run CI (`pnpm build` and `pnpm test`) and fix any regressions. -## 2. Add new diskless (in-memory) HTTP-01 handler -- Create `ts/handlers/Http01MemoryHandler.ts`: - - Implement `IChallengeHandler<{ token: string; keyAuthorization: string; webPath: string }>`, storing challenges in a private `Map`. - - `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. \ No newline at end of file +Please review and confirm before we begin the refactor. \ No newline at end of file diff --git a/test/test.smartacme.integration.ts b/test/test.smartacme.integration.ts index aee83d5..dbd3a93 100644 --- a/test/test.smartacme.integration.ts +++ b/test/test.smartacme.integration.ts @@ -30,14 +30,17 @@ tap.test('create SmartAcme instance with DNS-01 handler and start', async () => 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 () => { // Replace 'bleu.de' with your test domain if different const domain = 'bleu.de'; const cert = await smartAcmeInstance.getCertificateForDomain(domain); - console.log(cert); - expect(cert).object.toHaveOwnProperty('domainName'); + expect(cert).toHaveProperty('domainName'); expect(cert.domainName).toEqual(domain); - expect(cert).object.toHaveOwnProperty('publicKey'); + expect(cert).toHaveProperty('publicKey'); expect(typeof cert.publicKey).toEqual('string'); expect(cert.publicKey.length).toBeGreaterThan(0); }); diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index 03bb1be..62cc077 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { 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.' } diff --git a/ts/certmanagers/index.ts b/ts/certmanagers/index.ts new file mode 100644 index 0000000..1cdf8f9 --- /dev/null +++ b/ts/certmanagers/index.ts @@ -0,0 +1,2 @@ +export * from './memory.js'; +export * from './mongo.js'; diff --git a/ts/certmanagers/memory.ts b/ts/certmanagers/memory.ts new file mode 100644 index 0000000..581a416 --- /dev/null +++ b/ts/certmanagers/memory.ts @@ -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; + private certs: Map = new Map(); + + constructor() { + this.interestMap = new plugins.lik.InterestMap((domain) => domain); + } + + public async init(): Promise { + // no-op for in-memory store + } + + public async retrieveCertificate(domainName: string): Promise { + return this.certs.get(domainName) ?? null; + } + + public async storeCertificate(cert: SmartacmeCert): Promise { + 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 { + this.certs.delete(domainName); + } + + public async close(): Promise { + // no-op + } + /** + * Wipe all certificates from the in-memory store (for testing) + */ + public async wipe(): Promise { + this.certs.clear(); + // reset interest map + this.interestMap = new plugins.lik.InterestMap((domain) => domain); + } +} \ No newline at end of file diff --git a/ts/certmanagers.ts b/ts/certmanagers/mongo.ts similarity index 55% rename from ts/certmanagers.ts rename to ts/certmanagers/mongo.ts index 73c1725..14e5c89 100644 --- a/ts/certmanagers.ts +++ b/ts/certmanagers/mongo.ts @@ -1,52 +1,6 @@ -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; - private certs: Map = new Map(); - - constructor() { - this.interestMap = new plugins.lik.InterestMap((domain) => domain); - } - - public async init(): Promise { - // no-op for in-memory store - } - - public async retrieveCertificate(domainName: string): Promise { - return this.certs.get(domainName) ?? null; - } - - public async storeCertificate(cert: SmartacmeCert): Promise { - 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 { - this.certs.delete(domainName); - } - - public async close(): Promise { - // no-op - } - /** - * Wipe all certificates from the in-memory store (for testing) - */ - public async wipe(): Promise { - this.certs.clear(); - // reset interest map - this.interestMap = new plugins.lik.InterestMap((domain) => domain); - } -} +import * as plugins from '../smartacme.plugins.js'; +import type { ICertManager } from '../interfaces/certmanager.js'; +import { SmartacmeCert } from '../smartacme.classes.cert.js'; /** * MongoDB-backed certificate manager using EasyStore from smartdata. @@ -104,4 +58,4 @@ export class MongoCertManager implements ICertManager { // reset interest map this.interestMap = new plugins.lik.InterestMap((domain) => domain); } -} \ No newline at end of file +} diff --git a/ts/index.ts b/ts/index.ts index c5705d2..44e1ba2 100644 --- a/ts/index.ts +++ b/ts/index.ts @@ -1,4 +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'; +export { MemoryCertManager, MongoCertManager } from './certmanagers/index.js'; diff --git a/ts/interfaces/certmanager.ts b/ts/interfaces/certmanager.ts index e036c08..a3a918b 100644 --- a/ts/interfaces/certmanager.ts +++ b/ts/interfaces/certmanager.ts @@ -1,4 +1,3 @@ -import type { InterestMap } from '@push.rocks/lik'; import type { SmartacmeCert } from '../smartacme.classes.cert.js'; // (ICertRecord removed; use SmartacmeCert directly) @@ -9,10 +8,6 @@ import type { SmartacmeCert } from '../smartacme.classes.cert.js'; * file-based, Redis, etc.). */ export interface ICertManager { - /** - * Map for coordinating concurrent certificate requests. - */ - interestMap: InterestMap; /** * 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) */ - wipe?(): Promise; + wipe(): Promise; } \ No newline at end of file diff --git a/ts/smartacme.classes.smartacme.ts b/ts/smartacme.classes.smartacme.ts index f7c4401..0690776 100644 --- a/ts/smartacme.classes.smartacme.ts +++ b/ts/smartacme.classes.smartacme.ts @@ -63,7 +63,7 @@ export class SmartAcme { // certificate manager for persistence (implements ICertManager) - private certmanager: ICertManager; + public certmanager: ICertManager; private certmatcher: SmartacmeCertMatcher; // retry/backoff configuration (resolved with defaults) private retryOptions: { retries: number; factor: number; minTimeoutMs: number; maxTimeoutMs: number }; @@ -116,11 +116,6 @@ export class SmartAcme { } this.certmanager = this.options.certManager; 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 this.certmatcher = new SmartacmeCertMatcher();