86 lines
2.5 KiB
TypeScript
86 lines
2.5 KiB
TypeScript
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`,
|
|
}));
|
|
};
|
|
}
|