import * as http from 'node:http'; import type { IAcmeServerOptions } from './server.interfaces.js'; import { NonceManager } from './server.classes.nonce.js'; import { JwsVerifier } from './server.classes.jws.verifier.js'; import { MemoryAccountStore } from './server.classes.account.store.js'; import { MemoryOrderStore } from './server.classes.order.store.js'; import { AcmeServerCA } from './server.classes.ca.js'; import { ChallengeVerifier } from './server.classes.challenge.verifier.js'; import { AcmeRouter } from './server.classes.router.js'; import { createDirectoryHandler } from './server.handlers.directory.js'; import { createNonceHeadHandler, createNonceGetHandler } from './server.handlers.nonce.js'; import { createAccountHandler } from './server.handlers.account.js'; import { createNewOrderHandler, createOrderPollHandler } from './server.handlers.order.js'; import { createAuthzHandler } from './server.handlers.authz.js'; import { createChallengeHandler } from './server.handlers.challenge.js'; import { createFinalizeHandler } from './server.handlers.finalize.js'; import { createCertHandler } from './server.handlers.cert.js'; /** * ACME Directory Server — a self-contained RFC 8555 Certificate Authority. * * Usage: * ```ts * const server = new AcmeServer({ port: 14000 }); * await server.start(); * console.log(server.getDirectoryUrl()); // http://localhost:14000/directory * ``` */ export class AcmeServer { private options: Required> & IAcmeServerOptions; private httpServer: http.Server | null = null; private ca: AcmeServerCA; private baseUrl: string; constructor(options: IAcmeServerOptions = {}) { this.options = { port: options.port ?? 14000, hostname: options.hostname ?? '0.0.0.0', ...options, }; this.baseUrl = options.baseUrl ?? `http://localhost:${this.options.port}`; this.ca = new AcmeServerCA(options.caOptions); } async start(): Promise { // Initialize CA await this.ca.init(); // Create stores const accountStore = new MemoryAccountStore(); const orderStore = new MemoryOrderStore(); // Create managers const nonceManager = new NonceManager(); const jwsVerifier = new JwsVerifier(nonceManager, accountStore); const challengeVerifier = new ChallengeVerifier(this.options.challengeVerification ?? true); // Create router and register routes const router = new AcmeRouter(nonceManager); // Directory router.addRoute('GET', '/directory', createDirectoryHandler(this.baseUrl)); // Nonce router.addRoute('HEAD', '/new-nonce', createNonceHeadHandler()); router.addRoute('GET', '/new-nonce', createNonceGetHandler()); // Account router.addRoute('POST', '/new-account', createAccountHandler(this.baseUrl, jwsVerifier, accountStore)); // Order router.addRoute('POST', '/new-order', createNewOrderHandler(this.baseUrl, jwsVerifier, orderStore)); router.addRoute('POST', '/order/:id', createOrderPollHandler(this.baseUrl, jwsVerifier, orderStore)); // Authorization router.addRoute('POST', '/authz/:id', createAuthzHandler(this.baseUrl, jwsVerifier, orderStore)); // Challenge router.addRoute('POST', '/challenge/:id', createChallengeHandler( this.baseUrl, jwsVerifier, orderStore, accountStore, challengeVerifier, )); // Finalize router.addRoute('POST', '/finalize/:id', createFinalizeHandler(this.baseUrl, jwsVerifier, orderStore, this.ca)); // Certificate router.addRoute('POST', '/cert/:id', createCertHandler(this.baseUrl, jwsVerifier, orderStore)); // Start HTTP server this.httpServer = http.createServer((req, res) => { router.handle(req, res).catch((err) => { if (!res.headersSent) { res.writeHead(500, { 'Content-Type': 'application/problem+json' }); res.end(JSON.stringify({ type: 'urn:ietf:params:acme:error:serverInternal', detail: err instanceof Error ? err.message : 'Unknown error', status: 500, })); } }); }); await new Promise((resolve) => { this.httpServer!.listen(this.options.port, this.options.hostname, () => resolve()); }); } async stop(): Promise { if (this.httpServer) { await new Promise((resolve, reject) => { this.httpServer!.close((err) => (err ? reject(err) : resolve())); }); this.httpServer = null; } } getDirectoryUrl(): string { return `${this.baseUrl}/directory`; } getCaCertPem(): string { return this.ca.getCaCertPem(); } }