@push.rocks/smartacme 9.4.0 → 9.6.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.
- package/.smartconfig.json +12 -7
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/acme/acme.classes.client.d.ts +2 -0
- package/dist_ts/acme/acme.classes.client.js +5 -2
- package/dist_ts/acme/acme.classes.http-client.d.ts +2 -1
- package/dist_ts/acme/acme.classes.http-client.js +4 -2
- package/dist_ts/acme/acme.classes.order.d.ts +3 -0
- package/dist_ts/acme/acme.classes.order.js +11 -2
- package/dist_ts/classes.exact-certificate-issuer.d.ts +36 -0
- package/dist_ts/classes.exact-certificate-issuer.js +393 -0
- package/dist_ts/index.d.ts +2 -0
- package/dist_ts/index.js +2 -1
- package/dist_ts/interfaces/exact-issuance.d.ts +79 -0
- package/dist_ts/interfaces/exact-issuance.js +2 -0
- package/dist_ts/plugins.d.ts +5 -1
- package/dist_ts/plugins.js +6 -2
- package/dist_ts/smartacme.classes.smartacme.d.ts +22 -0
- package/dist_ts/smartacme.classes.smartacme.js +134 -10
- package/package.json +11 -15
- package/readme.md +75 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/acme/acme.classes.client.ts +6 -1
- package/ts/acme/acme.classes.http-client.ts +2 -1
- package/ts/acme/acme.classes.order.ts +12 -1
- package/ts/classes.exact-certificate-issuer.ts +403 -0
- package/ts/index.ts +2 -0
- package/ts/interfaces/exact-issuance.ts +104 -0
- package/ts/plugins.ts +5 -1
- package/ts/smartacme.classes.smartacme.ts +142 -8
- package/readme.hints.md +0 -81
- package/readme.plan.md +0 -3
|
@@ -3,6 +3,12 @@ import type { ICertManager } from './interfaces/certmanager.js';
|
|
|
3
3
|
import { SmartacmeCertMatcher } from './smartacme.classes.certmatcher.js';
|
|
4
4
|
import { commitinfo } from './00_commitinfo_data.js';
|
|
5
5
|
import { SmartacmeCert } from './smartacme.classes.cert.js';
|
|
6
|
+
import { ExactCertificateIssuer } from './classes.exact-certificate-issuer.js';
|
|
7
|
+
import type {
|
|
8
|
+
IExactCertificateIdentity,
|
|
9
|
+
IExactCertificateRequest, IExactIssuanceRecoveryRequest, IExactIssuanceStore,
|
|
10
|
+
IExactIssuanceInfo, TExactCertificateResult,
|
|
11
|
+
} from './interfaces/exact-issuance.js';
|
|
6
12
|
|
|
7
13
|
// ── Types & constants for certificate issuance task ──────────────────────────
|
|
8
14
|
|
|
@@ -11,6 +17,8 @@ interface ICertIssuanceInput {
|
|
|
11
17
|
domainArg: string;
|
|
12
18
|
isWildcardRequest: boolean;
|
|
13
19
|
includeWildcard: boolean;
|
|
20
|
+
forceRenew: boolean;
|
|
21
|
+
exact?: { request: IExactCertificateRequest; recovery?: IExactIssuanceRecoveryRequest };
|
|
14
22
|
}
|
|
15
23
|
|
|
16
24
|
const CERT_ISSUANCE_STEPS = [
|
|
@@ -32,6 +40,14 @@ export interface ISmartAcmeOptions {
|
|
|
32
40
|
certManager: ICertManager;
|
|
33
41
|
// Removed legacy setChallenge/removeChallenge in favor of `challengeHandlers`
|
|
34
42
|
environment: 'production' | 'integration';
|
|
43
|
+
/** Optional explicit ACME directory, for private CAs and isolated integration. */
|
|
44
|
+
directoryUrl?: string;
|
|
45
|
+
/** Enables isolated explicit-identifier issuance with durable recovery. */
|
|
46
|
+
exactIssuanceStore?: IExactIssuanceStore;
|
|
47
|
+
/** Bounded number of admitted exact-identity operations (default 64). */
|
|
48
|
+
maxPendingExactRequests?: number;
|
|
49
|
+
/** DNS propagation grace period for explicit issuance; defaults to 60 seconds. */
|
|
50
|
+
exactDnsPropagationDelayMs?: number;
|
|
35
51
|
/**
|
|
36
52
|
* Optional retry/backoff configuration for transient failures
|
|
37
53
|
*/
|
|
@@ -91,6 +107,10 @@ export class SmartAcme {
|
|
|
91
107
|
|
|
92
108
|
// the account private key
|
|
93
109
|
private privateKey!: string;
|
|
110
|
+
private exactIssuer?: ExactCertificateIssuer;
|
|
111
|
+
private exactRequests = new Map<string, Promise<TExactCertificateResult>>();
|
|
112
|
+
private lifecycleAbort = new AbortController();
|
|
113
|
+
private started = false;
|
|
94
114
|
|
|
95
115
|
|
|
96
116
|
// certificate manager for persistence (implements ICertManager)
|
|
@@ -123,6 +143,12 @@ export class SmartAcme {
|
|
|
123
143
|
|
|
124
144
|
constructor(optionsArg: ISmartAcmeOptions) {
|
|
125
145
|
this.options = optionsArg;
|
|
146
|
+
if (!Number.isSafeInteger(optionsArg.maxPendingExactRequests ?? 64)
|
|
147
|
+
|| (optionsArg.maxPendingExactRequests ?? 64) < 1
|
|
148
|
+
|| !Number.isSafeInteger(optionsArg.exactDnsPropagationDelayMs ?? 60000)
|
|
149
|
+
|| (optionsArg.exactDnsPropagationDelayMs ?? 60000) < 0) {
|
|
150
|
+
throw new Error('Invalid explicit issuance admission or propagation limits');
|
|
151
|
+
}
|
|
126
152
|
this.logger = plugins.smartlog.Smartlog.createForCommitinfo(commitinfo);
|
|
127
153
|
// enable console output for structured logging
|
|
128
154
|
this.logger.enableConsole();
|
|
@@ -155,10 +181,13 @@ export class SmartAcme {
|
|
|
155
181
|
maxConcurrent: 1,
|
|
156
182
|
resultSharingMode: 'share-latest',
|
|
157
183
|
constraintKeyForExecution: (_task, input?: ICertIssuanceInput) => {
|
|
184
|
+
if (input?.exact) return null;
|
|
158
185
|
return input?.certDomainName ?? null;
|
|
159
186
|
},
|
|
160
187
|
shouldExecute: async (_task, input?: ICertIssuanceInput) => {
|
|
188
|
+
if (input?.exact) return true;
|
|
161
189
|
if (!input?.certDomainName || !this.certmanager) return true;
|
|
190
|
+
if (input.forceRenew) return true;
|
|
162
191
|
// Safety net: if a valid cert is already cached, skip re-issuance
|
|
163
192
|
const existing = await this.certmanager.retrieveCertificate(input.certDomainName);
|
|
164
193
|
if (existing && !existing.shouldBeRenewed()) {
|
|
@@ -186,6 +215,12 @@ export class SmartAcme {
|
|
|
186
215
|
});
|
|
187
216
|
|
|
188
217
|
this.taskManager.addConstraintGroup(certDomainMutex);
|
|
218
|
+
// One fixed key reserves a bounded share of global capacity for testing.
|
|
219
|
+
// Exact results contain private keys and must never enter result-sharing caches.
|
|
220
|
+
this.taskManager.addConstraintGroup(new plugins.taskbuffer.TaskConstraintGroup({
|
|
221
|
+
name: 'exact-issuance-concurrency', maxConcurrent: 2,
|
|
222
|
+
constraintKeyForExecution: (_task, input?: ICertIssuanceInput) => input?.exact ? 'exact' : null,
|
|
223
|
+
}));
|
|
189
224
|
this.taskManager.addConstraintGroup(acmeGlobalConcurrency);
|
|
190
225
|
this.taskManager.addConstraintGroup(acmeAccountRateLimit);
|
|
191
226
|
|
|
@@ -194,6 +229,12 @@ export class SmartAcme {
|
|
|
194
229
|
name: 'cert-issuance',
|
|
195
230
|
steps: CERT_ISSUANCE_STEPS,
|
|
196
231
|
taskFunction: async (input: ICertIssuanceInput) => {
|
|
232
|
+
if (input.exact) {
|
|
233
|
+
const issuer = this.requireExactIssuer();
|
|
234
|
+
return input.exact.recovery
|
|
235
|
+
? issuer.recover(input.exact.recovery)
|
|
236
|
+
: issuer.run(input.exact.request);
|
|
237
|
+
}
|
|
197
238
|
return this.performCertificateIssuance(input);
|
|
198
239
|
},
|
|
199
240
|
});
|
|
@@ -208,8 +249,31 @@ export class SmartAcme {
|
|
|
208
249
|
* ```
|
|
209
250
|
*/
|
|
210
251
|
public async start() {
|
|
252
|
+
if (this.started) throw new Error('SmartAcme is already started');
|
|
253
|
+
this.lifecycleAbort = new AbortController();
|
|
211
254
|
this.privateKey =
|
|
212
255
|
this.options.accountPrivateKey || plugins.acme.AcmeCrypto.createRsaPrivateKey();
|
|
256
|
+
const directoryUrl = this.options.directoryUrl ?? (this.options.environment === 'production'
|
|
257
|
+
? plugins.acme.ACME_DIRECTORY_URLS.letsencrypt.production
|
|
258
|
+
: plugins.acme.ACME_DIRECTORY_URLS.letsencrypt.staging);
|
|
259
|
+
const directory = new URL(directoryUrl);
|
|
260
|
+
if (directory.username || directory.password || directory.hash
|
|
261
|
+
|| (directory.protocol !== 'https:' && !(directory.protocol === 'http:'
|
|
262
|
+
&& ['localhost', '127.0.0.1', '[::1]'].includes(directory.hostname)))) {
|
|
263
|
+
throw new Error('An HTTPS ACME directory or an explicit loopback test directory is required');
|
|
264
|
+
}
|
|
265
|
+
if (this.options.exactIssuanceStore && !this.options.accountPrivateKey) {
|
|
266
|
+
const accountId = plugins.crypto.createHash('sha256')
|
|
267
|
+
.update(JSON.stringify([directoryUrl, this.options.accountEmail.trim().toLowerCase()])).digest('hex');
|
|
268
|
+
const store = this.options.exactIssuanceStore;
|
|
269
|
+
let storedKey = await store.getAccountKey(accountId);
|
|
270
|
+
if (!storedKey) {
|
|
271
|
+
await store.createAccountKey(accountId, this.privateKey);
|
|
272
|
+
storedKey = await store.getAccountKey(accountId);
|
|
273
|
+
}
|
|
274
|
+
if (!storedKey) throw new Error('ACME account key could not be retained durably');
|
|
275
|
+
this.privateKey = storedKey;
|
|
276
|
+
}
|
|
213
277
|
|
|
214
278
|
// Initialize certificate manager
|
|
215
279
|
if (!this.options.certManager) {
|
|
@@ -223,14 +287,9 @@ export class SmartAcme {
|
|
|
223
287
|
|
|
224
288
|
// ACME Client
|
|
225
289
|
this.client = new plugins.acme.AcmeClient({
|
|
226
|
-
directoryUrl
|
|
227
|
-
if (this.options.environment === 'production') {
|
|
228
|
-
return plugins.acme.ACME_DIRECTORY_URLS.letsencrypt.production;
|
|
229
|
-
} else {
|
|
230
|
-
return plugins.acme.ACME_DIRECTORY_URLS.letsencrypt.staging;
|
|
231
|
-
}
|
|
232
|
-
})(),
|
|
290
|
+
directoryUrl,
|
|
233
291
|
accountKeyPem: this.privateKey,
|
|
292
|
+
signal: this.lifecycleAbort.signal,
|
|
234
293
|
logger: (level, message, data) => {
|
|
235
294
|
this.logger.log(level as any, message, data);
|
|
236
295
|
},
|
|
@@ -244,6 +303,15 @@ export class SmartAcme {
|
|
|
244
303
|
|
|
245
304
|
// Start the task manager
|
|
246
305
|
await this.taskManager.start();
|
|
306
|
+
if (this.options.exactIssuanceStore) {
|
|
307
|
+
this.exactIssuer = new ExactCertificateIssuer({
|
|
308
|
+
store: this.options.exactIssuanceStore, client: this.client, directoryUrl,
|
|
309
|
+
accountThumbprint: plugins.acme.AcmeCrypto.getJwkThumbprint(plugins.acme.AcmeCrypto.getJwk(this.privateKey)),
|
|
310
|
+
dns: this.smartdns, handlers: this.challengeHandlers, signal: this.lifecycleAbort.signal,
|
|
311
|
+
dnsPropagationDelayMs: this.options.exactDnsPropagationDelayMs ?? 60000,
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
this.started = true;
|
|
247
315
|
|
|
248
316
|
// Setup graceful shutdown handlers (store references for removal in stop())
|
|
249
317
|
this.boundSigintHandler = () => this.handleSignal('SIGINT');
|
|
@@ -256,6 +324,8 @@ export class SmartAcme {
|
|
|
256
324
|
* Stops the SmartAcme instance and closes certificate store connections.
|
|
257
325
|
*/
|
|
258
326
|
public async stop() {
|
|
327
|
+
this.started = false;
|
|
328
|
+
this.lifecycleAbort.abort();
|
|
259
329
|
// Remove signal handlers so the process can exit cleanly
|
|
260
330
|
if (this.boundSigintHandler) {
|
|
261
331
|
process.removeListener('SIGINT', this.boundSigintHandler);
|
|
@@ -265,8 +335,13 @@ export class SmartAcme {
|
|
|
265
335
|
process.removeListener('SIGTERM', this.boundSigtermHandler);
|
|
266
336
|
this.boundSigtermHandler = null;
|
|
267
337
|
}
|
|
268
|
-
//
|
|
338
|
+
// Observe active requests before rejecting queued work. The task manager
|
|
339
|
+
// must close its queue before we drain callers waiting on that queue.
|
|
340
|
+
const pendingExactRequests = Promise.allSettled([...this.exactRequests.values()]);
|
|
269
341
|
await this.taskManager.stop();
|
|
342
|
+
await pendingExactRequests;
|
|
343
|
+
this.exactRequests.clear();
|
|
344
|
+
this.exactIssuer = undefined;
|
|
270
345
|
// Destroy ACME HTTP transport (closes keep-alive sockets)
|
|
271
346
|
if (this.client) {
|
|
272
347
|
this.client.destroy();
|
|
@@ -279,6 +354,64 @@ export class SmartAcme {
|
|
|
279
354
|
await (this.certmanager as any).close();
|
|
280
355
|
}
|
|
281
356
|
}
|
|
357
|
+
|
|
358
|
+
/** Bind durable work to the current account and issuer without creating an order. */
|
|
359
|
+
public getCertificateIdentity(request: IExactCertificateRequest): IExactCertificateIdentity {
|
|
360
|
+
return this.requireExactIssuer().identity(request);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** Ensure a certificate whose SANs exactly match the explicit identifiers. */
|
|
364
|
+
public async getCertificateForIdentifiers(request: IExactCertificateRequest): Promise<TExactCertificateResult> {
|
|
365
|
+
return await this.runExact(request);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
public async getCertificateIssuanceStatus(request: IExactCertificateRequest): Promise<IExactIssuanceInfo | null> {
|
|
369
|
+
return await this.requireExactIssuer().status(request);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/** Administrative recovery; embedding services must authorize the caller. */
|
|
373
|
+
public async recoverCertificateIssuance(request: IExactIssuanceRecoveryRequest): Promise<TExactCertificateResult> {
|
|
374
|
+
return await this.runExact(request, request);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
private requireExactIssuer(): ExactCertificateIssuer {
|
|
378
|
+
if (!this.started || !this.exactIssuer) throw new Error('SmartAcme explicit issuance is not running or configured');
|
|
379
|
+
return this.exactIssuer;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
private async runExact(request: IExactCertificateRequest, recovery?: IExactIssuanceRecoveryRequest): Promise<TExactCertificateResult> {
|
|
383
|
+
const issuer = this.requireExactIssuer();
|
|
384
|
+
const identity = issuer.identity(request);
|
|
385
|
+
const recoverySnapshot = recovery ? {
|
|
386
|
+
namespace: identity.namespace, identifiers: [...identity.identifiers],
|
|
387
|
+
issuanceId: recovery.issuanceId, expectedRevision: recovery.expectedRevision, action: recovery.action,
|
|
388
|
+
} : undefined;
|
|
389
|
+
const existing = this.exactRequests.get(identity.certificateKey);
|
|
390
|
+
if (existing) {
|
|
391
|
+
if (recovery) throw new Error('Issuance is running; recover after its current operation completes');
|
|
392
|
+
return await existing;
|
|
393
|
+
}
|
|
394
|
+
if (this.exactRequests.size >= (this.options.maxPendingExactRequests ?? 64)) {
|
|
395
|
+
throw new Error('Explicit issuance admission limit reached');
|
|
396
|
+
}
|
|
397
|
+
const operation = (async (): Promise<TExactCertificateResult> => {
|
|
398
|
+
if (!recovery) {
|
|
399
|
+
const cached = await issuer.cached(identity);
|
|
400
|
+
if (cached) return cached;
|
|
401
|
+
}
|
|
402
|
+
this.lifecycleAbort.signal.throwIfAborted();
|
|
403
|
+
const result = await this.taskManager.triggerTaskConstrained(this.certIssuanceTask, {
|
|
404
|
+
certDomainName: identity.certificateKey, domainArg: identity.identifiers[0],
|
|
405
|
+
isWildcardRequest: false, includeWildcard: false, forceRenew: false,
|
|
406
|
+
exact: { request: { namespace: identity.namespace, identifiers: identity.identifiers }, recovery: recoverySnapshot },
|
|
407
|
+
} satisfies ICertIssuanceInput) as TExactCertificateResult | undefined;
|
|
408
|
+
if (!result) throw new Error('Explicit issuance completed without a result');
|
|
409
|
+
return result;
|
|
410
|
+
})();
|
|
411
|
+
this.exactRequests.set(identity.certificateKey, operation);
|
|
412
|
+
try { return await operation; }
|
|
413
|
+
finally { this.exactRequests.delete(identity.certificateKey); }
|
|
414
|
+
}
|
|
282
415
|
/** Retry helper with exponential backoff and AcmeError awareness */
|
|
283
416
|
private async retry<T>(operation: () => Promise<T>, operationName: string = 'operation'): Promise<T> {
|
|
284
417
|
let attempt = 0;
|
|
@@ -397,6 +530,7 @@ export class SmartAcme {
|
|
|
397
530
|
domainArg,
|
|
398
531
|
isWildcardRequest,
|
|
399
532
|
includeWildcard: options?.includeWildcard ?? false,
|
|
533
|
+
forceRenew,
|
|
400
534
|
};
|
|
401
535
|
|
|
402
536
|
const result = await this.taskManager.triggerTaskConstrained(
|
package/readme.hints.md
DELETED
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
- this repo is dependent on letsencrypt and its limits
|
|
2
|
-
- to simpify the outside API, smartacme is stateful, meaning it works with a mongodb and a collection called 'SmartacmeCert'.
|
|
3
|
-
|
|
4
|
-
## Certificate Request Behavior
|
|
5
|
-
|
|
6
|
-
As of v7.4.0, SmartAcme no longer automatically requests wildcard certificates for all domain requests. This change was made to fix issues with HTTP-01 only configurations which cannot validate wildcard domains.
|
|
7
|
-
|
|
8
|
-
- By default, `getCertificateForDomain('example.com')` only requests a certificate for `example.com`
|
|
9
|
-
- To request both regular and wildcard certificates, use `getCertificateForDomain('example.com', { includeWildcard: true })`
|
|
10
|
-
- Wildcard certificates require a DNS-01 challenge handler to be configured
|
|
11
|
-
- Direct wildcard requests like `getCertificateForDomain('*.example.com')` only request the wildcard certificate
|
|
12
|
-
|
|
13
|
-
This change ensures HTTP-01 only configurations work properly while still allowing wildcard certificates when needed and supported.
|
|
14
|
-
|
|
15
|
-
## ACME Protocol Implementation
|
|
16
|
-
|
|
17
|
-
As of v8.1.0, the `acme-client` npm package has been replaced with a custom OOP implementation under `ts/acme/`. This uses `node:crypto` for all cryptographic operations and `@peculiar/x509` solely for CSR generation. The implementation follows RFC 8555.
|
|
18
|
-
|
|
19
|
-
Key files:
|
|
20
|
-
- `ts/acme/acme.classes.client.ts` — Top-level facade (`AcmeClient`), accepts optional `logger` callback
|
|
21
|
-
- `ts/acme/acme.classes.crypto.ts` — Key gen, JWK, JWS signing, CSR (`AcmeCrypto`)
|
|
22
|
-
- `ts/acme/acme.classes.http-client.ts` — JWS-signed HTTP transport with nonce management and logging
|
|
23
|
-
- `ts/acme/acme.classes.error.ts` — Structured `AcmeError` with type URN, subproblems, Retry-After, `isRetryable`/`isRateLimited`
|
|
24
|
-
- `ts/acme/acme.classes.account.ts` — Account registration
|
|
25
|
-
- `ts/acme/acme.classes.order.ts` — Order lifecycle + polling
|
|
26
|
-
- `ts/acme/acme.classes.challenge.ts` — Key authorization + challenge completion
|
|
27
|
-
- `ts/acme/acme.classes.directory.ts` — CA directory URL constants (`ACME_DIRECTORY_URLS`)
|
|
28
|
-
|
|
29
|
-
Usage in `ts/plugins.ts`: `import * as acme from './acme/index.js'` (replaces `acme-client`)
|
|
30
|
-
|
|
31
|
-
## Concurrency & Rate Limiting (taskbuffer integration)
|
|
32
|
-
|
|
33
|
-
As of v9.1.0, `@push.rocks/lik.InterestMap` was replaced with `@push.rocks/taskbuffer.TaskManager` for coordinating concurrent certificate requests. This provides:
|
|
34
|
-
|
|
35
|
-
- **Per-domain mutex** (`cert-domain-mutex`): Only one ACME issuance per TLD at a time, with `resultSharingMode: 'share-latest'` so queued callers get the same result without re-issuing.
|
|
36
|
-
- **Global concurrency cap** (`acme-global-concurrency`): Limits total parallel ACME operations (default 5, configurable via `maxConcurrentIssuances`).
|
|
37
|
-
- **Account-level rate limiting** (`acme-account-rate-limit`): Sliding-window rate limit (default 250 orders per 3 hours, configurable via `maxOrdersPerWindow`/`orderWindowMs`) to stay under Let's Encrypt limits.
|
|
38
|
-
- **Step-based progress**: The cert issuance task uses `notifyStep()` for prepare/authorize/finalize/store phases, observable via `smartAcme.certIssuanceEvents`.
|
|
39
|
-
|
|
40
|
-
Key implementation details:
|
|
41
|
-
- A single reusable `Task` named `cert-issuance` handles all domains via `triggerTaskConstrained()` with different inputs.
|
|
42
|
-
- The `shouldExecute` callback on the domain mutex checks the certmanager cache as a safety net.
|
|
43
|
-
- `TaskManager.start()` is called in `SmartAcme.start()` and `TaskManager.stop()` in `SmartAcme.stop()`.
|
|
44
|
-
- The "no cronjobs specified" log messages during tests come from taskbuffer's internal CronManager polling — harmless noise when no cron tasks are scheduled.
|
|
45
|
-
|
|
46
|
-
## ACME Directory Server (ts_server/)
|
|
47
|
-
|
|
48
|
-
As of v9.2.0, a built-in ACME Directory Server lives under `ts_server/`. This is a full RFC 8555-compliant CA server that allows running your own Certificate Authority.
|
|
49
|
-
|
|
50
|
-
Key files:
|
|
51
|
-
- `ts_server/server.classes.acmeserver.ts` — Top-level `AcmeServer` facade (start/stop/config)
|
|
52
|
-
- `ts_server/server.classes.ca.ts` — Self-signed root CA generation + certificate signing via `@peculiar/x509`
|
|
53
|
-
- `ts_server/server.classes.jws.verifier.ts` — JWS signature verification (inverse of `AcmeCrypto.createJws`)
|
|
54
|
-
- `ts_server/server.classes.router.ts` — Minimal HTTP router with `:param` support using raw `node:http`
|
|
55
|
-
- `ts_server/server.classes.nonce.ts` — Single-use replay nonce management
|
|
56
|
-
- `ts_server/server.classes.challenge.verifier.ts` — HTTP-01/DNS-01 verification (with bypass mode)
|
|
57
|
-
- `ts_server/server.classes.account.store.ts` — In-memory account storage
|
|
58
|
-
- `ts_server/server.classes.order.store.ts` — In-memory order/authz/challenge/cert storage
|
|
59
|
-
- `ts_server/server.handlers.*.ts` — Route handlers for each ACME endpoint
|
|
60
|
-
|
|
61
|
-
Design decisions:
|
|
62
|
-
- Uses raw `node:http` (no framework dependency — `@api.global/typedserver` was explicitly removed in v8.1.0)
|
|
63
|
-
- Zero new dependencies: uses `node:crypto`, `@peculiar/x509`, and existing project deps
|
|
64
|
-
- Reuses `AcmeCrypto` for JWK thumbprint/base64url, ACME interfaces for response types, `AcmeError` patterns
|
|
65
|
-
- `AcmeCrypto.getAlg()` was made public (was private) for use by the JWS verifier
|
|
66
|
-
- Storage interfaces (`IServerAccountStore`, `IServerOrderStore`) are pluggable, with in-memory defaults
|
|
67
|
-
- `challengeVerification: false` option auto-approves challenges for testing
|
|
68
|
-
- `tsbuild tsfolders` automatically compiles `ts_server/` to `dist_ts_server/`
|
|
69
|
-
|
|
70
|
-
## Dependency Notes
|
|
71
|
-
|
|
72
|
-
- `acme-client` was replaced with custom implementation in `ts/acme/` + `@peculiar/x509` for CSR generation
|
|
73
|
-
- `@push.rocks/smartfile`, `@api.global/typedserver`, `@push.rocks/smartrequest`, `@push.rocks/smartpromise` were removed as unused dependencies in v8.1.0
|
|
74
|
-
- The `@apiclient.xyz/cloudflare` `convenience` namespace is deprecated but still functional. The `Dns01Handler` accepts an `IConvenientDnsProvider` interface which remains stable.
|
|
75
|
-
- Test imports use `@git.zone/tstest/tapbundle` (not `@push.rocks/tapbundle`)
|
|
76
|
-
- Build uses `tsbuild tsfolders` (v4.4.0+) — auto-discovers and compiles `ts/` and `ts_server/` directories
|
|
77
|
-
- `@peculiar/x509` v2.0.0 removed `reflect-metadata` from its dependencies. Since `tsyringe` (used internally by `@peculiar/x509`) requires the Reflect polyfill, `reflect-metadata` is now a direct dependency and imported in `ts/acme/acme.classes.crypto.ts` and `ts_server/server.classes.ca.ts`.
|
|
78
|
-
- `@push.rocks/taskbuffer` upgraded from v6 to v8 (required by smartdata 7.1.3). API surface is backward-compatible.
|
|
79
|
-
- TypeScript 6 (via tsbuild 4.4.0) requires `"types": ["node"]` in tsconfig.json for `ts_server/` compilation to resolve `@types/node`.
|
|
80
|
-
- TypeScript 6 deprecated `baseUrl` in tsconfig — removed it since `paths` was empty.
|
|
81
|
-
- Config file renamed from `npmextra.json` to `.smartconfig.json` (ecosystem convention change).
|
package/readme.plan.md
DELETED