@push.rocks/smartacme 9.1.2 → 9.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/dist_ts/00_commitinfo_data.js +2 -2
  2. package/dist_ts/acme/acme.classes.account.js +1 -1
  3. package/dist_ts/acme/acme.classes.challenge.js +1 -1
  4. package/dist_ts/acme/acme.classes.client.js +1 -1
  5. package/dist_ts/acme/acme.classes.crypto.d.ts +1 -1
  6. package/dist_ts/acme/acme.classes.crypto.js +1 -1
  7. package/dist_ts/acme/acme.classes.directory.js +1 -1
  8. package/dist_ts/acme/acme.classes.error.js +1 -1
  9. package/dist_ts/acme/acme.classes.http-client.js +1 -1
  10. package/dist_ts/acme/acme.classes.order.js +1 -1
  11. package/dist_ts/acme/acme.interfaces.js +1 -1
  12. package/dist_ts/acme/index.js +1 -1
  13. package/dist_ts/certmanagers/index.js +1 -1
  14. package/dist_ts/certmanagers/memory.js +1 -1
  15. package/dist_ts/certmanagers/mongo.js +1 -1
  16. package/dist_ts/handlers/Dns01Handler.js +1 -1
  17. package/dist_ts/handlers/Http01Handler.js +1 -1
  18. package/dist_ts/handlers/Http01MemoryHandler.js +1 -1
  19. package/dist_ts/handlers/IChallengeHandler.js +1 -1
  20. package/dist_ts/handlers/index.js +1 -1
  21. package/dist_ts/index.d.ts +2 -0
  22. package/dist_ts/index.js +4 -1
  23. package/dist_ts/interfaces/accountdata.js +1 -1
  24. package/dist_ts/interfaces/certmanager.js +1 -1
  25. package/dist_ts/interfaces/index.js +1 -1
  26. package/dist_ts/plugins.js +1 -1
  27. package/dist_ts/smartacme.classes.cert.js +1 -1
  28. package/dist_ts/smartacme.classes.certmatcher.js +1 -1
  29. package/dist_ts/smartacme.classes.smartacme.js +2 -1
  30. package/dist_ts_server/index.d.ts +4 -0
  31. package/dist_ts_server/index.js +4 -0
  32. package/dist_ts_server/server.classes.account.store.d.ts +12 -0
  33. package/dist_ts_server/server.classes.account.store.js +25 -0
  34. package/dist_ts_server/server.classes.acmeserver.d.ts +22 -0
  35. package/dist_ts_server/server.classes.acmeserver.js +100 -0
  36. package/dist_ts_server/server.classes.ca.d.ts +25 -0
  37. package/dist_ts_server/server.classes.ca.js +109 -0
  38. package/dist_ts_server/server.classes.challenge.verifier.d.ts +16 -0
  39. package/dist_ts_server/server.classes.challenge.verifier.js +56 -0
  40. package/dist_ts_server/server.classes.jws.verifier.d.ts +31 -0
  41. package/dist_ts_server/server.classes.jws.verifier.js +116 -0
  42. package/dist_ts_server/server.classes.nonce.d.ts +12 -0
  43. package/dist_ts_server/server.classes.nonce.js +32 -0
  44. package/dist_ts_server/server.classes.order.store.d.ts +21 -0
  45. package/dist_ts_server/server.classes.order.store.js +57 -0
  46. package/dist_ts_server/server.classes.router.d.ts +17 -0
  47. package/dist_ts_server/server.classes.router.js +99 -0
  48. package/dist_ts_server/server.handlers.account.d.ts +8 -0
  49. package/dist_ts_server/server.handlers.account.js +59 -0
  50. package/dist_ts_server/server.handlers.authz.d.ts +7 -0
  51. package/dist_ts_server/server.handlers.authz.js +41 -0
  52. package/dist_ts_server/server.handlers.cert.d.ts +7 -0
  53. package/dist_ts_server/server.handlers.cert.js +18 -0
  54. package/dist_ts_server/server.handlers.challenge.d.ts +10 -0
  55. package/dist_ts_server/server.handlers.challenge.js +110 -0
  56. package/dist_ts_server/server.handlers.directory.d.ts +5 -0
  57. package/dist_ts_server/server.handlers.directory.js +23 -0
  58. package/dist_ts_server/server.handlers.finalize.d.ts +8 -0
  59. package/dist_ts_server/server.handlers.finalize.js +65 -0
  60. package/dist_ts_server/server.handlers.nonce.d.ts +7 -0
  61. package/dist_ts_server/server.handlers.nonce.js +17 -0
  62. package/dist_ts_server/server.handlers.order.d.ts +11 -0
  63. package/dist_ts_server/server.handlers.order.js +131 -0
  64. package/dist_ts_server/server.interfaces.d.ts +72 -0
  65. package/dist_ts_server/server.interfaces.js +2 -0
  66. package/package.json +11 -11
  67. package/ts/00_commitinfo_data.ts +1 -1
  68. package/ts/acme/acme.classes.crypto.ts +1 -1
  69. package/ts/index.ts +4 -0
  70. package/ts/smartacme.classes.smartacme.ts +1 -0
  71. package/ts_server/index.ts +12 -0
  72. package/ts_server/server.classes.account.store.ts +27 -0
  73. package/ts_server/server.classes.acmeserver.ts +128 -0
  74. package/ts_server/server.classes.ca.ts +142 -0
  75. package/ts_server/server.classes.challenge.verifier.ts +61 -0
  76. package/ts_server/server.classes.jws.verifier.ts +153 -0
  77. package/ts_server/server.classes.nonce.ts +36 -0
  78. package/ts_server/server.classes.order.store.ts +72 -0
  79. package/ts_server/server.classes.router.ts +116 -0
  80. package/ts_server/server.handlers.account.ts +85 -0
  81. package/ts_server/server.handlers.authz.ts +58 -0
  82. package/ts_server/server.handlers.cert.ts +32 -0
  83. package/ts_server/server.handlers.challenge.ts +142 -0
  84. package/ts_server/server.handlers.directory.ts +30 -0
  85. package/ts_server/server.handlers.finalize.ts +93 -0
  86. package/ts_server/server.handlers.nonce.ts +29 -0
  87. package/ts_server/server.handlers.order.ts +177 -0
  88. package/ts_server/server.interfaces.ts +98 -0
@@ -0,0 +1,131 @@
1
+ import * as crypto from 'node:crypto';
2
+ import { AcmeServerError } from './server.classes.jws.verifier.js';
3
+ /**
4
+ * POST /new-order — Create a new ACME order.
5
+ */
6
+ export function createNewOrderHandler(baseUrl, jwsVerifier, orderStore) {
7
+ return async (req, res, _params, body) => {
8
+ const requestUrl = `${baseUrl}/new-order`;
9
+ const verified = await jwsVerifier.verify(body, requestUrl);
10
+ if (!verified.kid) {
11
+ throw new AcmeServerError(400, 'urn:ietf:params:acme:error:malformed', 'newOrder requires kid');
12
+ }
13
+ const { payload } = verified;
14
+ const identifiers = payload?.identifiers;
15
+ if (!identifiers || !Array.isArray(identifiers) || identifiers.length === 0) {
16
+ throw new AcmeServerError(400, 'urn:ietf:params:acme:error:malformed', 'Order must include at least one identifier');
17
+ }
18
+ const orderId = crypto.randomBytes(16).toString('hex');
19
+ const expires = new Date();
20
+ expires.setDate(expires.getDate() + 7);
21
+ // Create authorizations and challenges for each identifier
22
+ const authorizationIds = [];
23
+ for (const identifier of identifiers) {
24
+ const authzId = crypto.randomBytes(16).toString('hex');
25
+ const isWildcard = identifier.value.startsWith('*.');
26
+ const domain = isWildcard ? identifier.value.slice(2) : identifier.value;
27
+ // Create challenges for this authorization
28
+ const challengeIds = [];
29
+ // HTTP-01 challenge (not for wildcards)
30
+ if (!isWildcard) {
31
+ const http01Id = crypto.randomBytes(16).toString('hex');
32
+ const http01Token = crypto.randomBytes(32).toString('base64url');
33
+ await orderStore.createChallenge({
34
+ id: http01Id,
35
+ authorizationId: authzId,
36
+ type: 'http-01',
37
+ token: http01Token,
38
+ status: 'pending',
39
+ });
40
+ challengeIds.push(http01Id);
41
+ }
42
+ // DNS-01 challenge (always)
43
+ const dns01Id = crypto.randomBytes(16).toString('hex');
44
+ const dns01Token = crypto.randomBytes(32).toString('base64url');
45
+ await orderStore.createChallenge({
46
+ id: dns01Id,
47
+ authorizationId: authzId,
48
+ type: 'dns-01',
49
+ token: dns01Token,
50
+ status: 'pending',
51
+ });
52
+ challengeIds.push(dns01Id);
53
+ await orderStore.createAuthorization({
54
+ id: authzId,
55
+ orderId,
56
+ identifier: { type: 'dns', value: domain },
57
+ status: 'pending',
58
+ expires: expires.toISOString(),
59
+ challengeIds,
60
+ wildcard: isWildcard || undefined,
61
+ });
62
+ authorizationIds.push(authzId);
63
+ }
64
+ const order = await orderStore.createOrder({
65
+ id: orderId,
66
+ accountUrl: verified.kid,
67
+ status: 'pending',
68
+ identifiers,
69
+ authorizationIds,
70
+ expires: expires.toISOString(),
71
+ finalize: `${baseUrl}/finalize/${orderId}`,
72
+ });
73
+ const responseBody = {
74
+ status: order.status,
75
+ expires: order.expires,
76
+ identifiers: order.identifiers,
77
+ authorizations: order.authorizationIds.map((id) => `${baseUrl}/authz/${id}`),
78
+ finalize: order.finalize,
79
+ };
80
+ res.writeHead(201, {
81
+ 'Content-Type': 'application/json',
82
+ 'Location': `${baseUrl}/order/${orderId}`,
83
+ });
84
+ res.end(JSON.stringify(responseBody));
85
+ };
86
+ }
87
+ /**
88
+ * POST /order/:id — Poll order status (POST-as-GET).
89
+ */
90
+ export function createOrderPollHandler(baseUrl, jwsVerifier, orderStore) {
91
+ return async (req, res, params, body) => {
92
+ const orderId = params.id;
93
+ const requestUrl = `${baseUrl}/order/${orderId}`;
94
+ await jwsVerifier.verify(body, requestUrl);
95
+ const order = await orderStore.getOrder(orderId);
96
+ if (!order) {
97
+ throw new AcmeServerError(404, 'urn:ietf:params:acme:error:malformed', 'Order not found');
98
+ }
99
+ // Check if all authorizations are valid → transition to ready
100
+ if (order.status === 'pending') {
101
+ let allValid = true;
102
+ for (const authzId of order.authorizationIds) {
103
+ const authz = await orderStore.getAuthorization(authzId);
104
+ if (!authz || authz.status !== 'valid') {
105
+ allValid = false;
106
+ break;
107
+ }
108
+ }
109
+ if (allValid) {
110
+ await orderStore.updateOrder(orderId, { status: 'ready' });
111
+ order.status = 'ready';
112
+ }
113
+ }
114
+ const responseBody = {
115
+ status: order.status,
116
+ expires: order.expires,
117
+ identifiers: order.identifiers,
118
+ authorizations: order.authorizationIds.map((id) => `${baseUrl}/authz/${id}`),
119
+ finalize: order.finalize,
120
+ };
121
+ if (order.certificate) {
122
+ responseBody.certificate = order.certificate;
123
+ }
124
+ res.writeHead(200, {
125
+ 'Content-Type': 'application/json',
126
+ 'Location': requestUrl,
127
+ });
128
+ res.end(JSON.stringify(responseBody));
129
+ };
130
+ }
131
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2VydmVyLmhhbmRsZXJzLm9yZGVyLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vdHNfc2VydmVyL3NlcnZlci5oYW5kbGVycy5vcmRlci50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFDQSxPQUFPLEtBQUssTUFBTSxNQUFNLGFBQWEsQ0FBQztBQUV0QyxPQUFPLEVBQUUsZUFBZSxFQUFFLE1BQU0sa0NBQWtDLENBQUM7QUFJbkU7O0dBRUc7QUFDSCxNQUFNLFVBQVUscUJBQXFCLENBQ25DLE9BQWUsRUFDZixXQUF3QixFQUN4QixVQUE2QjtJQUU3QixPQUFPLEtBQUssRUFDVixHQUF5QixFQUN6QixHQUF3QixFQUN4QixPQUErQixFQUMvQixJQUFTLEVBQ00sRUFBRTtRQUNqQixNQUFNLFVBQVUsR0FBRyxHQUFHLE9BQU8sWUFBWSxDQUFDO1FBQzFDLE1BQU0sUUFBUSxHQUFHLE1BQU0sV0FBVyxDQUFDLE1BQU0sQ0FBQyxJQUFJLEVBQUUsVUFBVSxDQUFDLENBQUM7UUFFNUQsSUFBSSxDQUFDLFFBQVEsQ0FBQyxHQUFHLEVBQUUsQ0FBQztZQUNsQixNQUFNLElBQUksZUFBZSxDQUFDLEdBQUcsRUFBRSxzQ0FBc0MsRUFBRSx1QkFBdUIsQ0FBQyxDQUFDO1FBQ2xHLENBQUM7UUFFRCxNQUFNLEVBQUUsT0FBTyxFQUFFLEdBQUcsUUFBUSxDQUFDO1FBQzdCLE1BQU0sV0FBVyxHQUFzQixPQUFPLEVBQUUsV0FBVyxDQUFDO1FBRTVELElBQUksQ0FBQyxXQUFXLElBQUksQ0FBQyxLQUFLLENBQUMsT0FBTyxDQUFDLFdBQVcsQ0FBQyxJQUFJLFdBQVcsQ0FBQyxNQUFNLEtBQUssQ0FBQyxFQUFFLENBQUM7WUFDNUUsTUFBTSxJQUFJLGVBQWUsQ0FDdkIsR0FBRyxFQUNILHNDQUFzQyxFQUN0Qyw0Q0FBNEMsQ0FDN0MsQ0FBQztRQUNKLENBQUM7UUFFRCxNQUFNLE9BQU8sR0FBRyxNQUFNLENBQUMsV0FBVyxDQUFDLEVBQUUsQ0FBQyxD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
@@ -0,0 +1,72 @@
1
+ import type { IAcmeIdentifier } from '../dist_ts/acme/acme.interfaces.js';
2
+ export interface IAcmeServerOptions {
3
+ port?: number;
4
+ hostname?: string;
5
+ baseUrl?: string;
6
+ /** When false, challenges auto-approve on trigger (useful for testing) */
7
+ challengeVerification?: boolean;
8
+ caOptions?: {
9
+ commonName?: string;
10
+ validityDays?: number;
11
+ certValidityDays?: number;
12
+ };
13
+ }
14
+ export interface IServerAccountStore {
15
+ create(account: IServerAccount): Promise<IServerAccount>;
16
+ getByThumbprint(thumbprint: string): Promise<IServerAccount | null>;
17
+ getByUrl(url: string): Promise<IServerAccount | null>;
18
+ }
19
+ export interface IServerOrderStore {
20
+ createOrder(order: IServerOrder): Promise<IServerOrder>;
21
+ getOrder(id: string): Promise<IServerOrder | null>;
22
+ updateOrder(id: string, updates: Partial<IServerOrder>): Promise<void>;
23
+ createAuthorization(authz: IServerAuthorization): Promise<IServerAuthorization>;
24
+ getAuthorization(id: string): Promise<IServerAuthorization | null>;
25
+ updateAuthorization(id: string, updates: Partial<IServerAuthorization>): Promise<void>;
26
+ createChallenge(challenge: IServerChallenge): Promise<IServerChallenge>;
27
+ getChallenge(id: string): Promise<IServerChallenge | null>;
28
+ updateChallenge(id: string, updates: Partial<IServerChallenge>): Promise<void>;
29
+ storeCertPem(orderId: string, pem: string): Promise<void>;
30
+ getCertPem(orderId: string): Promise<string | null>;
31
+ }
32
+ export interface IServerAccount {
33
+ id: string;
34
+ thumbprint: string;
35
+ url: string;
36
+ jwk: Record<string, string>;
37
+ status: string;
38
+ contact: string[];
39
+ createdAt: string;
40
+ }
41
+ export interface IServerOrder {
42
+ id: string;
43
+ accountUrl: string;
44
+ status: string;
45
+ identifiers: IAcmeIdentifier[];
46
+ authorizationIds: string[];
47
+ expires: string;
48
+ finalize: string;
49
+ certificate?: string;
50
+ }
51
+ export interface IServerAuthorization {
52
+ id: string;
53
+ orderId: string;
54
+ identifier: IAcmeIdentifier;
55
+ status: string;
56
+ expires: string;
57
+ challengeIds: string[];
58
+ wildcard?: boolean;
59
+ }
60
+ export interface IServerChallenge {
61
+ id: string;
62
+ authorizationId: string;
63
+ type: string;
64
+ token: string;
65
+ status: string;
66
+ validated?: string;
67
+ error?: {
68
+ type: string;
69
+ detail: string;
70
+ };
71
+ }
72
+ export type TRouteHandler = (req: import('node:http').IncomingMessage, res: import('node:http').ServerResponse, params: Record<string, string>, body: any) => Promise<void>;
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2VydmVyLmludGVyZmFjZXMuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi90c19zZXJ2ZXIvc2VydmVyLmludGVyZmFjZXMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IiJ9
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@push.rocks/smartacme",
3
- "version": "9.1.2",
3
+ "version": "9.2.0",
4
4
  "private": false,
5
5
  "description": "A TypeScript-based ACME client for LetsEncrypt certificate management with a focus on simplicity and power.",
6
6
  "main": "dist_ts/index.js",
@@ -8,7 +8,7 @@
8
8
  "type": "module",
9
9
  "scripts": {
10
10
  "test": "(tstest test/ --verbose --logfile --timeout 600)",
11
- "build": "(tsbuild)",
11
+ "build": "(tsbuild tsfolders)",
12
12
  "buildDocs": "tsdoc"
13
13
  },
14
14
  "repository": {
@@ -41,28 +41,28 @@
41
41
  "dependencies": {
42
42
  "@apiclient.xyz/cloudflare": "^7.1.0",
43
43
  "@peculiar/x509": "^1.14.3",
44
- "@push.rocks/lik": "^6.2.2",
45
- "@push.rocks/smartdata": "^7.0.15",
44
+ "@push.rocks/lik": "^6.3.1",
45
+ "@push.rocks/smartdata": "^7.1.0",
46
46
  "@push.rocks/smartdelay": "^3.0.5",
47
- "@push.rocks/smartdns": "^7.8.1",
48
- "@push.rocks/smartlog": "^3.1.10",
47
+ "@push.rocks/smartdns": "^7.9.0",
48
+ "@push.rocks/smartlog": "^3.2.1",
49
49
  "@push.rocks/smartnetwork": "^4.4.0",
50
50
  "@push.rocks/smartstring": "^4.1.0",
51
51
  "@push.rocks/smarttime": "^4.2.3",
52
52
  "@push.rocks/smartunique": "^3.0.9",
53
53
  "@push.rocks/taskbuffer": "^6.1.2",
54
- "@tsclass/tsclass": "^9.3.0"
54
+ "@tsclass/tsclass": "^9.5.0"
55
55
  },
56
56
  "devDependencies": {
57
- "@git.zone/tsbuild": "^4.1.2",
57
+ "@git.zone/tsbuild": "^4.3.0",
58
58
  "@git.zone/tsrun": "^2.0.1",
59
- "@git.zone/tstest": "^3.1.8",
59
+ "@git.zone/tstest": "^3.4.0",
60
60
  "@push.rocks/qenv": "^6.1.3",
61
- "@types/node": "^25.2.3"
61
+ "@types/node": "^25.5.0"
62
62
  },
63
63
  "files": [
64
64
  "ts/**/*",
65
- "ts_web/**/*",
65
+ "ts_*/**/*",
66
66
  "dist/**/*",
67
67
  "dist_*/**/*",
68
68
  "dist_ts/**/*",
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@push.rocks/smartacme',
6
- version: '9.1.2',
6
+ version: '9.2.0',
7
7
  description: 'A TypeScript-based ACME client for LetsEncrypt certificate management with a focus on simplicity and power.'
8
8
  }
@@ -175,7 +175,7 @@ export class AcmeCrypto {
175
175
  /**
176
176
  * Determine JWS algorithm from key type
177
177
  */
178
- private static getAlg(keyPem: string): string {
178
+ static getAlg(keyPem: string): string {
179
179
  const keyObj = crypto.createPrivateKey(keyPem);
180
180
  const keyType = keyObj.asymmetricKeyType;
181
181
  if (keyType === 'rsa') return 'RS256';
package/ts/index.ts CHANGED
@@ -10,5 +10,9 @@ export { certmanagers };
10
10
  import * as handlers from './handlers/index.js';
11
11
  export { handlers };
12
12
 
13
+ // server (ACME Directory Server / CA)
14
+ import * as server from '../ts_server/index.js';
15
+ export { server };
16
+
13
17
  // re-export taskbuffer event types for consumers
14
18
  export type { ITaskEvent, ITaskMetadata } from '@push.rocks/taskbuffer';
@@ -430,6 +430,7 @@ export class SmartAcme {
430
430
 
431
431
  if (isWildcardRequest) {
432
432
  identifiers.push({ type: 'dns', value: `*.${certDomainName}` });
433
+ identifiers.push({ type: 'dns', value: certDomainName });
433
434
  } else {
434
435
  identifiers.push({ type: 'dns', value: certDomainName });
435
436
 
@@ -0,0 +1,12 @@
1
+ export { AcmeServer } from './server.classes.acmeserver.js';
2
+ export { AcmeServerError } from './server.classes.jws.verifier.js';
3
+ export { AcmeServerCA } from './server.classes.ca.js';
4
+ export type {
5
+ IAcmeServerOptions,
6
+ IServerAccountStore,
7
+ IServerOrderStore,
8
+ IServerAccount,
9
+ IServerOrder,
10
+ IServerAuthorization,
11
+ IServerChallenge,
12
+ } from './server.interfaces.js';
@@ -0,0 +1,27 @@
1
+ import type { IServerAccountStore, IServerAccount } from './server.interfaces.js';
2
+
3
+ /**
4
+ * In-memory account storage for the ACME server.
5
+ */
6
+ export class MemoryAccountStore implements IServerAccountStore {
7
+ private accounts = new Map<string, IServerAccount>();
8
+ private byThumbprint = new Map<string, string>();
9
+ private byUrl = new Map<string, string>();
10
+
11
+ async create(account: IServerAccount): Promise<IServerAccount> {
12
+ this.accounts.set(account.id, account);
13
+ this.byThumbprint.set(account.thumbprint, account.id);
14
+ this.byUrl.set(account.url, account.id);
15
+ return account;
16
+ }
17
+
18
+ async getByThumbprint(thumbprint: string): Promise<IServerAccount | null> {
19
+ const id = this.byThumbprint.get(thumbprint);
20
+ return id ? this.accounts.get(id) || null : null;
21
+ }
22
+
23
+ async getByUrl(url: string): Promise<IServerAccount | null> {
24
+ const id = this.byUrl.get(url);
25
+ return id ? this.accounts.get(id) || null : null;
26
+ }
27
+ }
@@ -0,0 +1,128 @@
1
+ import * as http from 'node:http';
2
+ import type { IAcmeServerOptions } from './server.interfaces.js';
3
+ import { NonceManager } from './server.classes.nonce.js';
4
+ import { JwsVerifier } from './server.classes.jws.verifier.js';
5
+ import { MemoryAccountStore } from './server.classes.account.store.js';
6
+ import { MemoryOrderStore } from './server.classes.order.store.js';
7
+ import { AcmeServerCA } from './server.classes.ca.js';
8
+ import { ChallengeVerifier } from './server.classes.challenge.verifier.js';
9
+ import { AcmeRouter } from './server.classes.router.js';
10
+ import { createDirectoryHandler } from './server.handlers.directory.js';
11
+ import { createNonceHeadHandler, createNonceGetHandler } from './server.handlers.nonce.js';
12
+ import { createAccountHandler } from './server.handlers.account.js';
13
+ import { createNewOrderHandler, createOrderPollHandler } from './server.handlers.order.js';
14
+ import { createAuthzHandler } from './server.handlers.authz.js';
15
+ import { createChallengeHandler } from './server.handlers.challenge.js';
16
+ import { createFinalizeHandler } from './server.handlers.finalize.js';
17
+ import { createCertHandler } from './server.handlers.cert.js';
18
+
19
+ /**
20
+ * ACME Directory Server — a self-contained RFC 8555 Certificate Authority.
21
+ *
22
+ * Usage:
23
+ * ```ts
24
+ * const server = new AcmeServer({ port: 14000 });
25
+ * await server.start();
26
+ * console.log(server.getDirectoryUrl()); // http://localhost:14000/directory
27
+ * ```
28
+ */
29
+ export class AcmeServer {
30
+ private options: Required<Pick<IAcmeServerOptions, 'port' | 'hostname'>> & IAcmeServerOptions;
31
+ private httpServer: http.Server | null = null;
32
+ private ca: AcmeServerCA;
33
+ private baseUrl: string;
34
+
35
+ constructor(options: IAcmeServerOptions = {}) {
36
+ this.options = {
37
+ port: options.port ?? 14000,
38
+ hostname: options.hostname ?? '0.0.0.0',
39
+ ...options,
40
+ };
41
+ this.baseUrl = options.baseUrl ?? `http://localhost:${this.options.port}`;
42
+ this.ca = new AcmeServerCA(options.caOptions);
43
+ }
44
+
45
+ async start(): Promise<void> {
46
+ // Initialize CA
47
+ await this.ca.init();
48
+
49
+ // Create stores
50
+ const accountStore = new MemoryAccountStore();
51
+ const orderStore = new MemoryOrderStore();
52
+
53
+ // Create managers
54
+ const nonceManager = new NonceManager();
55
+ const jwsVerifier = new JwsVerifier(nonceManager, accountStore);
56
+ const challengeVerifier = new ChallengeVerifier(this.options.challengeVerification ?? true);
57
+
58
+ // Create router and register routes
59
+ const router = new AcmeRouter(nonceManager);
60
+
61
+ // Directory
62
+ router.addRoute('GET', '/directory', createDirectoryHandler(this.baseUrl));
63
+
64
+ // Nonce
65
+ router.addRoute('HEAD', '/new-nonce', createNonceHeadHandler());
66
+ router.addRoute('GET', '/new-nonce', createNonceGetHandler());
67
+
68
+ // Account
69
+ router.addRoute('POST', '/new-account', createAccountHandler(this.baseUrl, jwsVerifier, accountStore));
70
+
71
+ // Order
72
+ router.addRoute('POST', '/new-order', createNewOrderHandler(this.baseUrl, jwsVerifier, orderStore));
73
+ router.addRoute('POST', '/order/:id', createOrderPollHandler(this.baseUrl, jwsVerifier, orderStore));
74
+
75
+ // Authorization
76
+ router.addRoute('POST', '/authz/:id', createAuthzHandler(this.baseUrl, jwsVerifier, orderStore));
77
+
78
+ // Challenge
79
+ router.addRoute('POST', '/challenge/:id', createChallengeHandler(
80
+ this.baseUrl,
81
+ jwsVerifier,
82
+ orderStore,
83
+ accountStore,
84
+ challengeVerifier,
85
+ ));
86
+
87
+ // Finalize
88
+ router.addRoute('POST', '/finalize/:id', createFinalizeHandler(this.baseUrl, jwsVerifier, orderStore, this.ca));
89
+
90
+ // Certificate
91
+ router.addRoute('POST', '/cert/:id', createCertHandler(this.baseUrl, jwsVerifier, orderStore));
92
+
93
+ // Start HTTP server
94
+ this.httpServer = http.createServer((req, res) => {
95
+ router.handle(req, res).catch((err) => {
96
+ if (!res.headersSent) {
97
+ res.writeHead(500, { 'Content-Type': 'application/problem+json' });
98
+ res.end(JSON.stringify({
99
+ type: 'urn:ietf:params:acme:error:serverInternal',
100
+ detail: err instanceof Error ? err.message : 'Unknown error',
101
+ status: 500,
102
+ }));
103
+ }
104
+ });
105
+ });
106
+
107
+ await new Promise<void>((resolve) => {
108
+ this.httpServer!.listen(this.options.port, this.options.hostname, () => resolve());
109
+ });
110
+ }
111
+
112
+ async stop(): Promise<void> {
113
+ if (this.httpServer) {
114
+ await new Promise<void>((resolve, reject) => {
115
+ this.httpServer!.close((err) => (err ? reject(err) : resolve()));
116
+ });
117
+ this.httpServer = null;
118
+ }
119
+ }
120
+
121
+ getDirectoryUrl(): string {
122
+ return `${this.baseUrl}/directory`;
123
+ }
124
+
125
+ getCaCertPem(): string {
126
+ return this.ca.getCaCertPem();
127
+ }
128
+ }
@@ -0,0 +1,142 @@
1
+ import * as crypto from 'node:crypto';
2
+
3
+ /**
4
+ * Certificate Authority for the ACME server.
5
+ * Generates a self-signed root CA and signs certificates from CSRs.
6
+ * Uses @peculiar/x509 (already a project dependency).
7
+ */
8
+ export class AcmeServerCA {
9
+ private caKeyPair!: CryptoKeyPair;
10
+ private caCert!: InstanceType<typeof import('@peculiar/x509').X509Certificate>;
11
+ private caCertPem!: string;
12
+ private certValidityDays: number;
13
+
14
+ constructor(private options: { commonName?: string; validityDays?: number; certValidityDays?: number } = {}) {
15
+ this.certValidityDays = options.certValidityDays ?? 90;
16
+ }
17
+
18
+ async init(): Promise<void> {
19
+ const x509 = await import('@peculiar/x509');
20
+ const { webcrypto } = crypto;
21
+ x509.cryptoProvider.set(webcrypto as any);
22
+
23
+ // Generate RSA key pair for the CA
24
+ this.caKeyPair = await webcrypto.subtle.generateKey(
25
+ {
26
+ name: 'RSASSA-PKCS1-v1_5',
27
+ modulusLength: 2048,
28
+ publicExponent: new Uint8Array([1, 0, 1]),
29
+ hash: 'SHA-256',
30
+ },
31
+ true,
32
+ ['sign', 'verify'],
33
+ ) as CryptoKeyPair;
34
+
35
+ const cn = this.options.commonName ?? 'SmartACME Test CA';
36
+ const validityDays = this.options.validityDays ?? 3650;
37
+ const notBefore = new Date();
38
+ const notAfter = new Date();
39
+ notAfter.setDate(notAfter.getDate() + validityDays);
40
+
41
+ // Create self-signed root CA certificate
42
+ this.caCert = await x509.X509CertificateGenerator.createSelfSigned({
43
+ serialNumber: this.randomSerialNumber(),
44
+ name: `CN=${cn}`,
45
+ notBefore,
46
+ notAfter,
47
+ signingAlgorithm: { name: 'RSASSA-PKCS1-v1_5' },
48
+ keys: this.caKeyPair,
49
+ extensions: [
50
+ new x509.BasicConstraintsExtension(true, undefined, true),
51
+ new x509.KeyUsagesExtension(
52
+ x509.KeyUsageFlags.keyCertSign | x509.KeyUsageFlags.cRLSign,
53
+ true,
54
+ ),
55
+ await x509.SubjectKeyIdentifierExtension.create(this.caKeyPair.publicKey),
56
+ ],
57
+ });
58
+
59
+ this.caCertPem = this.caCert.toString('pem');
60
+ }
61
+
62
+ /**
63
+ * Sign a CSR and return a PEM certificate chain (end-entity + root CA).
64
+ * @param csrDerBase64url - The CSR in base64url-encoded DER format (as sent by ACME clients)
65
+ */
66
+ async signCsr(csrDerBase64url: string): Promise<string> {
67
+ const x509 = await import('@peculiar/x509');
68
+ const { webcrypto } = crypto;
69
+ x509.cryptoProvider.set(webcrypto as any);
70
+
71
+ // Parse the CSR
72
+ const csrDer = Buffer.from(csrDerBase64url, 'base64url');
73
+ const csr = new x509.Pkcs10CertificateRequest(csrDer);
74
+
75
+ // Extract Subject Alternative Names from CSR extensions
76
+ const sanNames: { type: 'dns'; value: string }[] = [];
77
+ const sanExt = csr.extensions?.find(
78
+ (ext) => ext.type === '2.5.29.17', // OID for SubjectAlternativeName
79
+ );
80
+
81
+ if (sanExt) {
82
+ const san = new x509.SubjectAlternativeNameExtension(sanExt.rawData);
83
+ if (san.names) {
84
+ const jsonNames = san.names.toJSON();
85
+ for (const name of jsonNames) {
86
+ if (name.type === 'dns') {
87
+ sanNames.push({ type: 'dns', value: name.value });
88
+ }
89
+ }
90
+ }
91
+ }
92
+
93
+ // If no SAN found, use CN from subject
94
+ if (sanNames.length === 0) {
95
+ const cnMatch = csr.subject.match(/CN=([^,]+)/);
96
+ if (cnMatch) {
97
+ sanNames.push({ type: 'dns', value: cnMatch[1] });
98
+ }
99
+ }
100
+
101
+ const notBefore = new Date();
102
+ const notAfter = new Date();
103
+ notAfter.setDate(notAfter.getDate() + this.certValidityDays);
104
+
105
+ // Sign the certificate
106
+ const cert = await x509.X509CertificateGenerator.create({
107
+ serialNumber: this.randomSerialNumber(),
108
+ subject: csr.subject,
109
+ issuer: this.caCert.subject,
110
+ notBefore,
111
+ notAfter,
112
+ signingAlgorithm: { name: 'RSASSA-PKCS1-v1_5' },
113
+ publicKey: csr.publicKey,
114
+ signingKey: this.caKeyPair.privateKey,
115
+ extensions: [
116
+ new x509.BasicConstraintsExtension(false, undefined, true),
117
+ new x509.KeyUsagesExtension(
118
+ x509.KeyUsageFlags.digitalSignature | x509.KeyUsageFlags.keyEncipherment,
119
+ true,
120
+ ),
121
+ new x509.ExtendedKeyUsageExtension(
122
+ ['1.3.6.1.5.5.7.3.1'], // serverAuth
123
+ true,
124
+ ),
125
+ new x509.SubjectAlternativeNameExtension(sanNames),
126
+ await x509.AuthorityKeyIdentifierExtension.create(this.caKeyPair.publicKey),
127
+ ],
128
+ });
129
+
130
+ // Return PEM chain: end-entity cert + root CA cert
131
+ const certPem = cert.toString('pem');
132
+ return `${certPem}\n${this.caCertPem}`;
133
+ }
134
+
135
+ getCaCertPem(): string {
136
+ return this.caCertPem;
137
+ }
138
+
139
+ private randomSerialNumber(): string {
140
+ return crypto.randomBytes(16).toString('hex');
141
+ }
142
+ }