import type * as http from 'node:http'; import * as crypto from 'node:crypto'; import type { JwsVerifier } from './server.classes.jws.verifier.js'; import { AcmeServerError } from './server.classes.jws.verifier.js'; import type { IServerOrderStore, IServerAccountStore } from './server.interfaces.js'; import type { IAcmeIdentifier } from '../ts/acme/acme.interfaces.js'; /** * POST /new-order — Create a new ACME order. */ export function createNewOrderHandler( baseUrl: string, jwsVerifier: JwsVerifier, orderStore: IServerOrderStore, ) { return async ( req: http.IncomingMessage, res: http.ServerResponse, _params: Record, body: any, ): Promise => { const requestUrl = `${baseUrl}/new-order`; const verified = await jwsVerifier.verify(body, requestUrl); if (!verified.kid) { throw new AcmeServerError(400, 'urn:ietf:params:acme:error:malformed', 'newOrder requires kid'); } const { payload } = verified; const identifiers: IAcmeIdentifier[] = payload?.identifiers; if (!identifiers || !Array.isArray(identifiers) || identifiers.length === 0) { throw new AcmeServerError( 400, 'urn:ietf:params:acme:error:malformed', 'Order must include at least one identifier', ); } const orderId = crypto.randomBytes(16).toString('hex'); const expires = new Date(); expires.setDate(expires.getDate() + 7); // Create authorizations and challenges for each identifier const authorizationIds: string[] = []; for (const identifier of identifiers) { const authzId = crypto.randomBytes(16).toString('hex'); const isWildcard = identifier.value.startsWith('*.'); const domain = isWildcard ? identifier.value.slice(2) : identifier.value; // Create challenges for this authorization const challengeIds: string[] = []; // HTTP-01 challenge (not for wildcards) if (!isWildcard) { const http01Id = crypto.randomBytes(16).toString('hex'); const http01Token = crypto.randomBytes(32).toString('base64url'); await orderStore.createChallenge({ id: http01Id, authorizationId: authzId, type: 'http-01', token: http01Token, status: 'pending', }); challengeIds.push(http01Id); } // DNS-01 challenge (always) const dns01Id = crypto.randomBytes(16).toString('hex'); const dns01Token = crypto.randomBytes(32).toString('base64url'); await orderStore.createChallenge({ id: dns01Id, authorizationId: authzId, type: 'dns-01', token: dns01Token, status: 'pending', }); challengeIds.push(dns01Id); await orderStore.createAuthorization({ id: authzId, orderId, identifier: { type: 'dns', value: domain }, status: 'pending', expires: expires.toISOString(), challengeIds, wildcard: isWildcard || undefined, }); authorizationIds.push(authzId); } const order = await orderStore.createOrder({ id: orderId, accountUrl: verified.kid, status: 'pending', identifiers, authorizationIds, expires: expires.toISOString(), finalize: `${baseUrl}/finalize/${orderId}`, }); const responseBody = { status: order.status, expires: order.expires, identifiers: order.identifiers, authorizations: order.authorizationIds.map((id) => `${baseUrl}/authz/${id}`), finalize: order.finalize, }; res.writeHead(201, { 'Content-Type': 'application/json', 'Location': `${baseUrl}/order/${orderId}`, }); res.end(JSON.stringify(responseBody)); }; } /** * POST /order/:id — Poll order status (POST-as-GET). */ export function createOrderPollHandler( baseUrl: string, jwsVerifier: JwsVerifier, orderStore: IServerOrderStore, ) { return async ( req: http.IncomingMessage, res: http.ServerResponse, params: Record, body: any, ): Promise => { const orderId = params.id; const requestUrl = `${baseUrl}/order/${orderId}`; await jwsVerifier.verify(body, requestUrl); const order = await orderStore.getOrder(orderId); if (!order) { throw new AcmeServerError(404, 'urn:ietf:params:acme:error:malformed', 'Order not found'); } // Check if all authorizations are valid → transition to ready if (order.status === 'pending') { let allValid = true; for (const authzId of order.authorizationIds) { const authz = await orderStore.getAuthorization(authzId); if (!authz || authz.status !== 'valid') { allValid = false; break; } } if (allValid) { await orderStore.updateOrder(orderId, { status: 'ready' }); order.status = 'ready'; } } const responseBody: Record = { status: order.status, expires: order.expires, identifiers: order.identifiers, authorizations: order.authorizationIds.map((id) => `${baseUrl}/authz/${id}`), finalize: order.finalize, }; if (order.certificate) { responseBody.certificate = order.certificate; } res.writeHead(200, { 'Content-Type': 'application/json', 'Location': requestUrl, }); res.end(JSON.stringify(responseBody)); }; }