feat(server): add an embedded ACME directory server and certificate authority with challenge, order, and certificate endpoints
This commit is contained in:
85
ts_server/server.handlers.account.ts
Normal file
85
ts_server/server.handlers.account.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import type * as http from 'node:http';
|
||||
import * as crypto from 'node:crypto';
|
||||
import { AcmeCrypto } from '../ts/acme/acme.classes.crypto.js';
|
||||
import type { JwsVerifier } from './server.classes.jws.verifier.js';
|
||||
import { AcmeServerError } from './server.classes.jws.verifier.js';
|
||||
import type { IServerAccountStore } from './server.interfaces.js';
|
||||
|
||||
/**
|
||||
* POST /new-account — Register or retrieve an ACME account.
|
||||
* Expects JWS with JWK in protected header (not kid).
|
||||
*/
|
||||
export function createAccountHandler(
|
||||
baseUrl: string,
|
||||
jwsVerifier: JwsVerifier,
|
||||
accountStore: IServerAccountStore,
|
||||
) {
|
||||
return async (
|
||||
req: http.IncomingMessage,
|
||||
res: http.ServerResponse,
|
||||
_params: Record<string, string>,
|
||||
body: any,
|
||||
): Promise<void> => {
|
||||
const requestUrl = `${baseUrl}/new-account`;
|
||||
const verified = await jwsVerifier.verify(body, requestUrl);
|
||||
|
||||
// Account creation must use JWK, not kid
|
||||
if (verified.kid) {
|
||||
throw new AcmeServerError(
|
||||
400,
|
||||
'urn:ietf:params:acme:error:malformed',
|
||||
'newAccount requests must use JWK, not kid',
|
||||
);
|
||||
}
|
||||
|
||||
const { payload, jwk, thumbprint } = verified;
|
||||
|
||||
// Check if account already exists
|
||||
const existing = await accountStore.getByThumbprint(thumbprint);
|
||||
if (existing) {
|
||||
// If onlyReturnExisting, or just returning the existing account
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'application/json',
|
||||
'Location': existing.url,
|
||||
});
|
||||
res.end(JSON.stringify({
|
||||
status: existing.status,
|
||||
contact: existing.contact,
|
||||
orders: `${baseUrl}/account/${existing.id}/orders`,
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
// onlyReturnExisting = true but no account found
|
||||
if (payload?.onlyReturnExisting) {
|
||||
throw new AcmeServerError(
|
||||
400,
|
||||
'urn:ietf:params:acme:error:accountDoesNotExist',
|
||||
'Account does not exist',
|
||||
);
|
||||
}
|
||||
|
||||
// Create new account
|
||||
const id = crypto.randomBytes(16).toString('hex');
|
||||
const accountUrl = `${baseUrl}/account/${id}`;
|
||||
const account = await accountStore.create({
|
||||
id,
|
||||
thumbprint,
|
||||
url: accountUrl,
|
||||
jwk,
|
||||
status: 'valid',
|
||||
contact: payload?.contact || [],
|
||||
createdAt: new Date().toISOString(),
|
||||
});
|
||||
|
||||
res.writeHead(201, {
|
||||
'Content-Type': 'application/json',
|
||||
'Location': accountUrl,
|
||||
});
|
||||
res.end(JSON.stringify({
|
||||
status: account.status,
|
||||
contact: account.contact,
|
||||
orders: `${baseUrl}/account/${id}/orders`,
|
||||
}));
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user