@harperfast/integration-testing 0.1.0 → 0.3.0-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 (56) hide show
  1. package/README.md +71 -5
  2. package/dist/harperLifecycle.d.ts +176 -0
  3. package/dist/harperLifecycle.js +71 -40
  4. package/dist/harperLifecycle.js.map +1 -1
  5. package/dist/index.d.ts +3 -0
  6. package/dist/index.js +1 -1
  7. package/dist/index.js.map +1 -1
  8. package/dist/loopbackAddressPool.d.ts +50 -0
  9. package/dist/run.d.ts +2 -0
  10. package/dist/security/certGenUtils.d.ts +52 -0
  11. package/dist/security/certGenUtils.js +170 -0
  12. package/dist/security/certGenUtils.js.map +1 -0
  13. package/dist/security/crl/generate-test-certs.d.ts +34 -0
  14. package/dist/security/crl/generate-test-certs.js +81 -0
  15. package/dist/security/crl/generate-test-certs.js.map +1 -0
  16. package/dist/security/ocsp/generate-test-certs.d.ts +55 -0
  17. package/dist/security/ocsp/generate-test-certs.js +106 -0
  18. package/dist/security/ocsp/generate-test-certs.js.map +1 -0
  19. package/dist/security/ocspServer.d.ts +18 -0
  20. package/dist/security/ocspServer.js +132 -0
  21. package/dist/security/ocspServer.js.map +1 -0
  22. package/dist/securityServices.d.ts +49 -0
  23. package/dist/securityServices.js +120 -0
  24. package/dist/securityServices.js.map +1 -0
  25. package/dist/src/harperLifecycle.d.ts +156 -0
  26. package/dist/src/harperLifecycle.js +315 -0
  27. package/dist/src/harperLifecycle.js.map +1 -0
  28. package/dist/src/index.d.ts +3 -0
  29. package/dist/src/index.js +4 -0
  30. package/dist/src/index.js.map +1 -0
  31. package/dist/src/loopbackAddressPool.d.ts +50 -0
  32. package/dist/src/loopbackAddressPool.js +337 -0
  33. package/dist/src/loopbackAddressPool.js.map +1 -0
  34. package/dist/src/run.d.ts +2 -0
  35. package/dist/src/run.js +94 -0
  36. package/dist/src/run.js.map +1 -0
  37. package/dist/src/security/certGenUtils.d.ts +52 -0
  38. package/dist/src/security/certGenUtils.js +170 -0
  39. package/dist/src/security/certGenUtils.js.map +1 -0
  40. package/dist/src/security/crl/generate-test-certs.d.ts +34 -0
  41. package/dist/src/security/crl/generate-test-certs.js +81 -0
  42. package/dist/src/security/crl/generate-test-certs.js.map +1 -0
  43. package/dist/src/security/ocsp/generate-test-certs.d.ts +55 -0
  44. package/dist/src/security/ocsp/generate-test-certs.js +106 -0
  45. package/dist/src/security/ocsp/generate-test-certs.js.map +1 -0
  46. package/dist/src/security/ocspServer.d.ts +18 -0
  47. package/dist/src/security/ocspServer.js +132 -0
  48. package/dist/src/security/ocspServer.js.map +1 -0
  49. package/dist/src/securityServices.d.ts +49 -0
  50. package/dist/src/securityServices.js +120 -0
  51. package/dist/src/securityServices.js.map +1 -0
  52. package/dist/src/targz.d.ts +6 -0
  53. package/dist/src/targz.js +20 -0
  54. package/dist/src/targz.js.map +1 -0
  55. package/dist/targz.d.ts +6 -0
  56. package/package.json +5 -3
@@ -0,0 +1,337 @@
1
+ import { setTimeout as sleep } from 'node:timers/promises';
2
+ import { join } from 'node:path';
3
+ import { tmpdir } from 'node:os';
4
+ import { open, readFile, stat, unlink, writeFile } from 'node:fs/promises';
5
+ import { createServer } from 'node:net';
6
+ // Configuration constants
7
+ const HARPER_LOOPBACK_POOL_COUNT = process.env.HARPER_INTEGRATION_TEST_LOOPBACK_POOL_COUNT
8
+ ? parseInt(process.env.HARPER_INTEGRATION_TEST_LOOPBACK_POOL_COUNT, 10)
9
+ : 32;
10
+ if (HARPER_LOOPBACK_POOL_COUNT < 1 || HARPER_LOOPBACK_POOL_COUNT > 255) {
11
+ throw new Error('HARPER_INTEGRATION_TEST_LOOPBACK_POOL_COUNT must be between 1 and 255');
12
+ }
13
+ const HARPER_LOOPBACK_POOL_PATH = join(tmpdir(), 'harper-integration-test-loopback-pool.json');
14
+ const HARPER_LOOPBACK_POOL_LOCK_PATH = join(tmpdir(), 'harper-integration-test-loopback-pool.lock');
15
+ // Constants for timeouts and retries
16
+ const LOCK_STALE_TIMEOUT_MS = 10000;
17
+ const RETRY_DELAY_MS = 1000;
18
+ // Custom error classes
19
+ class LoopbackAddressValidationError extends Error {
20
+ constructor(address, cause) {
21
+ super(`Failed to validate loopback address ${address}. This likely means your system does not have the required loopback addresses configured/enabled. Refer to the Harper Integration Test documentation (integrationTests/README.md) for more information.`);
22
+ this.name = 'LoopbackAddressValidationError';
23
+ if (cause) {
24
+ this.cause = cause;
25
+ }
26
+ }
27
+ }
28
+ class InvalidLoopbackAddressError extends Error {
29
+ constructor(address) {
30
+ super(`Invalid loopback address format: ${address}. Expected format: 127.0.0.X where X is between 1 and ${HARPER_LOOPBACK_POOL_COUNT}`);
31
+ this.name = 'InvalidLoopbackAddressError';
32
+ }
33
+ }
34
+ /**
35
+ * Acquires a file-based lock by creating the lock file. This enables safe concurrent
36
+ * access to the loopback pool across multiple test processes.
37
+ *
38
+ * Uses the 'wx' file flag which atomically fails if the file already exists, providing
39
+ * a simple but effective cross-process mutex. Handles stale locks by removing lock files
40
+ * older than LOCK_STALE_TIMEOUT_MS (10 seconds).
41
+ *
42
+ * @returns A promise that resolves when the lock is acquired
43
+ */
44
+ async function acquireLock() {
45
+ while (true) {
46
+ try {
47
+ // The 'wx' flag causes the open to fail if the file already exists
48
+ const lockFileHandle = await open(HARPER_LOOPBACK_POOL_LOCK_PATH, 'wx');
49
+ // We have the lock - close the handle as we don't intend to write to it
50
+ await lockFileHandle.close();
51
+ return;
52
+ }
53
+ catch (error) {
54
+ // If the lock file already exists, it's either stale or we wait for it to be released
55
+ if (error.code === 'EEXIST') {
56
+ try {
57
+ const lockFileStat = await stat(HARPER_LOOPBACK_POOL_LOCK_PATH);
58
+ // If the lock file is older than the timeout, consider it stale and remove it
59
+ if (Date.now() - lockFileStat.mtimeMs > LOCK_STALE_TIMEOUT_MS) {
60
+ await unlink(HARPER_LOOPBACK_POOL_LOCK_PATH);
61
+ }
62
+ }
63
+ catch {
64
+ // Lock file may have been removed by another process, continue
65
+ }
66
+ await sleep(RETRY_DELAY_MS);
67
+ continue;
68
+ }
69
+ // Rethrow other errors
70
+ throw error;
71
+ }
72
+ }
73
+ }
74
+ /**
75
+ * Releases the file-based lock by deleting the lock file.
76
+ */
77
+ async function releaseLock() {
78
+ try {
79
+ await unlink(HARPER_LOOPBACK_POOL_LOCK_PATH);
80
+ }
81
+ catch {
82
+ // Ignore errors if lock file is already gone
83
+ }
84
+ }
85
+ /**
86
+ * Executes a callback function while holding the lock. Automatically acquires
87
+ * and releases the lock, ensuring the lock is always released even if the callback
88
+ * throws an error.
89
+ *
90
+ * @param callback The async function to execute while holding the lock
91
+ * @returns The result of the callback function
92
+ */
93
+ async function withLock(callback) {
94
+ await acquireLock();
95
+ try {
96
+ return await callback();
97
+ }
98
+ finally {
99
+ await releaseLock();
100
+ }
101
+ }
102
+ /**
103
+ * Reads the loopback pool from the pool file. The pool is a JSON array where each
104
+ * index represents a loopback address (127.0.0.1, 127.0.0.2, etc.) and the value
105
+ * is either null (available) or a process PID (in use).
106
+ *
107
+ * If the file doesn't exist, creates and returns a new empty pool with all addresses
108
+ * marked as available (null).
109
+ *
110
+ * @returns The loopback pool array
111
+ */
112
+ async function readPoolFile() {
113
+ try {
114
+ const content = await readFile(HARPER_LOOPBACK_POOL_PATH, 'utf-8');
115
+ return JSON.parse(content);
116
+ }
117
+ catch (error) {
118
+ if (error.code === 'ENOENT') {
119
+ // If the pool file doesn't exist yet, create it with null entries
120
+ return Array(HARPER_LOOPBACK_POOL_COUNT).fill(null);
121
+ }
122
+ throw error;
123
+ }
124
+ }
125
+ /**
126
+ * Writes the loopback pool to the pool file as JSON.
127
+ *
128
+ * @param pool The loopback pool array to persist
129
+ */
130
+ async function writePoolFile(pool) {
131
+ await writeFile(HARPER_LOOPBACK_POOL_PATH, JSON.stringify(pool));
132
+ }
133
+ /**
134
+ * Finds the first available (null) index in the pool. This implements a simple
135
+ * first-available allocation strategy.
136
+ *
137
+ * @param pool The loopback pool array
138
+ * @returns The first available index, or null if the pool is full
139
+ */
140
+ function findAvailableIndex(pool) {
141
+ for (let i = 0; i < pool.length; i++) {
142
+ if (pool[i] === null) {
143
+ return i;
144
+ }
145
+ }
146
+ return null;
147
+ }
148
+ /**
149
+ * Validates the format of a loopback address and extracts the pool index.
150
+ *
151
+ * Expects addresses in the format "127.0.0.X" where X is between 1 and the pool count.
152
+ * The returned index is 0-based (e.g., "127.0.0.1" returns index 0).
153
+ *
154
+ * @param address The loopback address to parse (e.g., "127.0.0.2")
155
+ * @returns The 0-based pool index for this address
156
+ * @throws {InvalidLoopbackAddressError} If the address format is invalid or out of range
157
+ */
158
+ function parseLoopbackAddress(address) {
159
+ const parts = address.split('.');
160
+ if (parts.length !== 4 || parts[0] !== '127' || parts[1] !== '0' || parts[2] !== '0') {
161
+ throw new InvalidLoopbackAddressError(address);
162
+ }
163
+ const index = parseInt(parts[3], 10) - 1;
164
+ if (isNaN(index) || index < 0 || index >= HARPER_LOOPBACK_POOL_COUNT) {
165
+ throw new InvalidLoopbackAddressError(address);
166
+ }
167
+ return index;
168
+ }
169
+ /**
170
+ * Validates that a given loopback address can be bound to by creating a temporary
171
+ * TCP server on that address. This ensures the loopback address is actually configured
172
+ * and available on the system before allocating it to a test process.
173
+ *
174
+ * The server is bound to port 0 (random port) just to verify the address exists,
175
+ * then immediately closed.
176
+ *
177
+ * @param loopbackAddress The loopback IP address to validate (e.g., "127.0.0.2")
178
+ * @returns A promise that resolves with the address if valid
179
+ * @throws An error with the loopbackAddress property if binding fails
180
+ */
181
+ function validateLoopbackAddress(loopbackAddress) {
182
+ return new Promise((resolve, reject) => {
183
+ const server = createServer();
184
+ server.once('error', (error) => {
185
+ const enhancedError = error;
186
+ enhancedError.loopbackAddress = loopbackAddress;
187
+ reject(enhancedError);
188
+ });
189
+ server.listen(0, loopbackAddress, () => {
190
+ server.close(() => {
191
+ resolve(loopbackAddress);
192
+ });
193
+ });
194
+ });
195
+ }
196
+ /**
197
+ * This method attempts to validate all loopback addresses in the pool by trying to
198
+ * bind to each one. It returns an object containing arrays of successfully bound
199
+ * loopback addresses and those that failed along with their errors.
200
+ *
201
+ * It will check all loopback addresses from 127.0.0.1 to 127.0.0.32 (by default).
202
+ *
203
+ * Use the HARPER_INTEGRATION_TEST_LOOPBACK_POOL_COUNT environment variable to
204
+ * adjust the number of loopback addresses to validate (up to 255).
205
+ */
206
+ export async function validateLoopbackAddressPool() {
207
+ return Promise.allSettled(Array.from({ length: HARPER_LOOPBACK_POOL_COUNT }, (_, i) => validateLoopbackAddress(`127.0.0.${i + 1}`))).then((results) => results.reduce((acc, result) => {
208
+ if (result.status === 'fulfilled') {
209
+ acc.successful.push(result.value);
210
+ }
211
+ else {
212
+ const error = result.reason;
213
+ acc.failed.push({ loopbackAddress: error.loopbackAddress, error });
214
+ }
215
+ return acc;
216
+ }, { successful: [], failed: [] }));
217
+ }
218
+ /**
219
+ * Retrieves the next available loopback address from the pool using a file-based
220
+ * locking mechanism to safely allocate addresses across concurrent test processes.
221
+ *
222
+ * **How it works:**
223
+ * 1. Acquires a file-based lock to prevent race conditions with other processes
224
+ * 2. Reads the pool state (a JSON array of process IDs, with null for available slots)
225
+ * 3. Finds the first available (null) slot and assigns the current process PID to it
226
+ * 4. Writes the updated pool back to disk and releases the lock
227
+ * 5. Validates that the allocated address can actually be bound to
228
+ * 6. Returns the loopback address (e.g., "127.0.0.2")
229
+ *
230
+ * If no addresses are available, waits and retries until one becomes available.
231
+ *
232
+ * **Pool file location:** `${tmpdir()}/harper-integration-test-loopback-pool.json`
233
+ * **Lock file location:** `${tmpdir()}/harper-integration-test-loopback-pool.lock`
234
+ *
235
+ * @returns A promise that resolves with an allocated loopback address
236
+ * @throws {LoopbackAddressValidationError} If the allocated address cannot be bound to
237
+ */
238
+ export async function getNextAvailableLoopbackAddress() {
239
+ // Each index+1 is a different loopback address that a test process will be assigned to
240
+ // So if the first test process number is 42, it would be assigned to index 0 associated with address 127.0.0.1
241
+ // [42, null, null, ...];
242
+ // Then the next process (call is 43) gets the next available, so index 1 -> 127.0.0.2
243
+ // [42, 43, null, ...];
244
+ // And so on...
245
+ // As processes exit and release their loopback addresses, those addresses become available for new processes to use
246
+ // [42, null, 44, ...];
247
+ // Next process (45) gets index 1 again ->
248
+ // [42, 45, 44, ...];
249
+ // This continues until all loopback addresses are used, at which point new processes will wait until an address becomes available
250
+ // Since multiple processes may be trying to get a loopback address at the same time, we need to implement a simple file-based locking mechanism to prevent race conditions
251
+ while (true) {
252
+ const assignedIndex = await withLock(async () => {
253
+ // Read the pool file
254
+ const loopbackPool = await readPoolFile();
255
+ // Find the first available index
256
+ const index = findAvailableIndex(loopbackPool);
257
+ if (index === null) {
258
+ // No available addresses - remove any dead processes from the pool and wait for one to become available
259
+ removeDeadProcessesFromPool(loopbackPool);
260
+ }
261
+ else {
262
+ // Assign the process PID to that index to mark it as used
263
+ loopbackPool[index] = process.pid;
264
+ }
265
+ // Write the updated pool back to the file
266
+ await writePoolFile(loopbackPool);
267
+ return index;
268
+ });
269
+ // If we got an index, validate and return the address
270
+ if (assignedIndex !== null) {
271
+ const loopbackAddress = `127.0.0.${assignedIndex + 1}`;
272
+ try {
273
+ await validateLoopbackAddress(loopbackAddress);
274
+ return loopbackAddress;
275
+ }
276
+ catch (error) {
277
+ // Validation failed - throw a proper error instead of breaking
278
+ throw new LoopbackAddressValidationError(loopbackAddress, error);
279
+ }
280
+ }
281
+ // No available addresses; wait and retry
282
+ await sleep(RETRY_DELAY_MS);
283
+ }
284
+ }
285
+ /**
286
+ * Removes any dead processes from the loopback pool.
287
+ * @param loopbackPool
288
+ */
289
+ function removeDeadProcessesFromPool(loopbackPool) {
290
+ loopbackPool.forEach((pid, index) => {
291
+ if (pid === null)
292
+ return;
293
+ try {
294
+ process.kill(pid, 0);
295
+ }
296
+ catch {
297
+ loopbackPool[index] = null;
298
+ }
299
+ });
300
+ }
301
+ /**
302
+ * Releases a loopback address back to the pool, making it available for other processes.
303
+ *
304
+ * @param address The loopback address to release (e.g., "127.0.0.1")
305
+ * @throws InvalidLoopbackAddressError if the address format is invalid
306
+ */
307
+ export async function releaseLoopbackAddress(address) {
308
+ // Validate and parse the address
309
+ const index = parseLoopbackAddress(address);
310
+ await withLock(async () => {
311
+ // Read the pool file
312
+ const loopbackPool = await readPoolFile();
313
+ // Release the address by setting it to null
314
+ loopbackPool[index] = null;
315
+ // Write the updated pool back to the file
316
+ await writePoolFile(loopbackPool);
317
+ });
318
+ }
319
+ /**
320
+ * Releases all loopback addresses assigned to the current process.
321
+ * Useful for cleanup during graceful shutdown.
322
+ */
323
+ export async function releaseAllLoopbackAddressesForCurrentProcess() {
324
+ await withLock(async () => {
325
+ // Read the pool file
326
+ const loopbackPool = await readPoolFile();
327
+ // Find and release all addresses assigned to this process
328
+ for (let i = 0; i < loopbackPool.length; i++) {
329
+ if (loopbackPool[i] === process.pid) {
330
+ loopbackPool[i] = null;
331
+ }
332
+ }
333
+ // Write the updated pool back to the file
334
+ await writePoolFile(loopbackPool);
335
+ });
336
+ }
337
+ //# sourceMappingURL=loopbackAddressPool.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loopbackAddressPool.js","sourceRoot":"","sources":["../../src/loopbackAddressPool.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env node
2
+ import { run } from 'node:test';
3
+ import { availableParallelism } from 'node:os';
4
+ import { spec } from 'node:test/reporters';
5
+ import { parseArgs } from 'node:util';
6
+ import { validateLoopbackAddressPool } from "./loopbackAddressPool.js";
7
+ /**
8
+ * Important! This script should not be required to execute integration tests.
9
+ * Thus, it should not be responsible for any stateful management or setup/teardown logic.
10
+ * All such logic should be contained within the individual test suites or utility functions.
11
+ * Tests (individuals or multiples) should be executable directly via the Node.js Test Runner CLI and
12
+ * parallelization should still work.
13
+ *
14
+ * The main purpose of this script is to reduce the boilerplate required to run integration tests, or having
15
+ * developers manually specify CLI arguments each time.
16
+ *
17
+ * This script configures and runs the Node.js Test Runner with sensible defaults for Harper integration tests.
18
+ *
19
+ * It supports environment variables to override defaults, allowing flexibility for CI environments or specific use cases.
20
+ *
21
+ * Usage: harper-integration-test-run [options] <glob-pattern> [<glob-pattern> ...]
22
+ *
23
+ * At least one glob pattern positional argument is required.
24
+ */
25
+ // Imitating the Node.js Test Runner CLI arguments for consistency except we drop the `test-` prefix
26
+ const { values, positionals } = parseArgs({
27
+ options: {
28
+ concurrency: { type: 'string' },
29
+ isolation: { type: 'string' },
30
+ shard: { type: 'string' },
31
+ only: { type: 'boolean' },
32
+ },
33
+ allowPositionals: true,
34
+ });
35
+ if (positionals.length === 0) {
36
+ console.error('Error: At least one glob pattern is required.\n' +
37
+ 'Usage: harper-integration-test-run [options] <glob-pattern> [<glob-pattern> ...]\n' +
38
+ 'Example: harper-integration-test-run "integrationTests/**/*.test.ts"');
39
+ process.exit(1);
40
+ }
41
+ // https://nodejs.org/docs/latest-v24.x/api/cli.html#--test-concurrency
42
+ const concurrencyOption = process.env.HARPER_INTEGRATION_TEST_CONCURRENCY || values.concurrency;
43
+ const CONCURRENCY = concurrencyOption
44
+ ? parseInt(concurrencyOption, 10)
45
+ : Math.max(1, Math.floor(availableParallelism() / 2) + 1);
46
+ // https://nodejs.org/docs/latest-v24.x/api/cli.html#--test-isolationmode
47
+ const ISOLATION = process.env.HARPER_INTEGRATION_TEST_ISOLATION || values.isolation || 'process';
48
+ // https://nodejs.org/docs/latest-v24.x/api/cli.html#--test-shard
49
+ const [SHARD_INDEX, SHARD_TOTAL] = (process.env.HARPER_INTEGRATION_TEST_SHARD || values.shard || '1/1')
50
+ .split('/')
51
+ .map((v) => parseInt(v, 10));
52
+ // https://nodejs.org/docs/latest-v24.x/api/cli.html#--test-only
53
+ const ONLY = parseBoolean(process.env.HARPER_INTEGRATION_TEST_ONLY) ?? values.only ?? false;
54
+ const TEST_FILES = positionals;
55
+ // Loopback Address Check
56
+ if (ISOLATION !== 'none' && CONCURRENCY > 1) {
57
+ const result = await validateLoopbackAddressPool();
58
+ if (result.failed.length > 0) {
59
+ console.error('Failed to bind loopback address pool required for integration tests:');
60
+ for (const failure of result.failed) {
61
+ console.error(`- ${failure.loopbackAddress}: ${failure.error.message}`);
62
+ }
63
+ console.error('Run the setup script to configure loopback addresses:\n' +
64
+ ' harper-integration-test-setup-loopback\n' +
65
+ 'Or run integration tests sequentially using `--isolation=none` to avoid this requirement.');
66
+ process.exit(1);
67
+ }
68
+ }
69
+ run({
70
+ concurrency: ISOLATION === 'none' ? undefined : CONCURRENCY,
71
+ // @ts-expect-error - ignore until we do better env var / cli arg handling/validation
72
+ isolation: ISOLATION,
73
+ globPatterns: TEST_FILES,
74
+ only: ONLY,
75
+ shard: {
76
+ index: SHARD_INDEX,
77
+ total: SHARD_TOTAL,
78
+ },
79
+ })
80
+ .on('test:fail', () => {
81
+ process.exitCode = 1;
82
+ })
83
+ .compose(spec)
84
+ .pipe(process.stdout);
85
+ function parseBoolean(value) {
86
+ if (value === undefined)
87
+ return undefined;
88
+ if (value.toLowerCase() === 'true' || value === '1')
89
+ return true;
90
+ if (value.toLowerCase() === 'false' || value === '0')
91
+ return false;
92
+ return undefined;
93
+ }
94
+ //# sourceMappingURL=run.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run.js","sourceRoot":"","sources":["../../src/run.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Pure Node.js X.509 certificate and CRL generation utilities
3
+ *
4
+ * Uses Node.js built-in webcrypto (Ed25519) + pkijs for ASN.1 encoding.
5
+ * No openssl CLI dependency — fully portable across macOS and Linux.
6
+ */
7
+ import * as pkijs from 'pkijs';
8
+ declare const OCSP_SIGNING_OID = "1.3.6.1.5.5.7.3.9";
9
+ declare const CLIENT_AUTH_OID = "1.3.6.1.5.5.7.3.2";
10
+ export interface Ed25519KeyPair {
11
+ privateKey: CryptoKey;
12
+ publicKey: CryptoKey;
13
+ /** PKCS8 PEM string */
14
+ privateKeyPem: string;
15
+ }
16
+ export declare function generateEd25519KeyPair(): Promise<Ed25519KeyPair>;
17
+ /** Convert a pkijs Certificate to PEM string */
18
+ export declare function certToPem(cert: pkijs.Certificate): string;
19
+ /** Convert a pkijs CertificateRevocationList to PEM string */
20
+ export declare function crlToPem(crl: pkijs.CertificateRevocationList): string;
21
+ interface CertOptions {
22
+ serialNumber: number;
23
+ subject: {
24
+ CN: string;
25
+ O?: string;
26
+ };
27
+ issuer: {
28
+ CN: string;
29
+ O?: string;
30
+ };
31
+ validDays: number;
32
+ issuerKey: CryptoKey;
33
+ subjectPublicKey: CryptoKey;
34
+ isCA?: boolean;
35
+ extensions?: pkijs.Extension[];
36
+ }
37
+ /** Create a signed Ed25519 X.509 v3 certificate */
38
+ export declare function createCertificate(opts: CertOptions): Promise<pkijs.Certificate>;
39
+ /** Create a CRL Distribution Points extension */
40
+ export declare function makeCRLDistributionPointsExt(url: string): pkijs.Extension;
41
+ /** Create an Authority Info Access extension with an OCSP URL */
42
+ export declare function makeOCSPAIAExt(url: string): pkijs.Extension;
43
+ /** Create an Extended Key Usage extension */
44
+ export declare function makeExtKeyUsageExt(oids: string[]): pkijs.Extension;
45
+ export { OCSP_SIGNING_OID, CLIENT_AUTH_OID };
46
+ /** Create a signed X.509v2 CRL */
47
+ export declare function createCRL(issuerCert: pkijs.Certificate, issuerKey: CryptoKey, revokedSerials: number[]): Promise<pkijs.CertificateRevocationList>;
48
+ /**
49
+ * Sign a pkijs BasicOCSPResponse with an Ed25519 key.
50
+ * Must be called after tbsResponseData.responses is populated.
51
+ */
52
+ export declare function signBasicOCSPResponse(basicResponse: pkijs.BasicOCSPResponse, privateKey: CryptoKey): Promise<void>;
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Pure Node.js X.509 certificate and CRL generation utilities
3
+ *
4
+ * Uses Node.js built-in webcrypto (Ed25519) + pkijs for ASN.1 encoding.
5
+ * No openssl CLI dependency — fully portable across macOS and Linux.
6
+ */
7
+ import { webcrypto } from 'node:crypto';
8
+ import * as pkijs from 'pkijs';
9
+ import * as asn1js from 'asn1js';
10
+ // Ed25519 OID (RFC 8410)
11
+ const ED25519_OID = '1.3.101.112';
12
+ // Standard extension OIDs
13
+ const BASIC_CONSTRAINTS_OID = '2.5.29.19';
14
+ const CRL_DISTRIBUTION_POINTS_OID = '2.5.29.31';
15
+ const AUTHORITY_INFO_ACCESS_OID = '1.3.6.1.5.5.7.1.1';
16
+ const EXT_KEY_USAGE_OID = '2.5.29.37';
17
+ const OCSP_SIGNING_OID = '1.3.6.1.5.5.7.3.9';
18
+ const CLIENT_AUTH_OID = '1.3.6.1.5.5.7.3.2';
19
+ const OCSP_ACCESS_METHOD_OID = '1.3.6.1.5.5.7.48.1';
20
+ // Configure pkijs to use Node.js webcrypto
21
+ pkijs.setEngine('node', new pkijs.CryptoEngine({ name: 'node', crypto: webcrypto }));
22
+ export async function generateEd25519KeyPair() {
23
+ // Ed25519 is supported at runtime (Node 20+) but @types/node lacks dedicated types for it
24
+ const keyPair = (await webcrypto.subtle.generateKey({ name: 'Ed25519' }, true, [
25
+ 'sign',
26
+ 'verify',
27
+ ]));
28
+ const pkcs8 = await webcrypto.subtle.exportKey('pkcs8', keyPair.privateKey);
29
+ const b64 = Buffer.from(pkcs8).toString('base64');
30
+ const lines = b64.match(/.{1,64}/g).join('\n');
31
+ const privateKeyPem = `-----BEGIN PRIVATE KEY-----\n${lines}\n-----END PRIVATE KEY-----\n`;
32
+ return { privateKey: keyPair.privateKey, publicKey: keyPair.publicKey, privateKeyPem };
33
+ }
34
+ /**
35
+ * Sign a pkijs Certificate or CertificateRevocationList with an Ed25519 key.
36
+ * Bypasses pkijs's sign() method which does not support Ed25519.
37
+ */
38
+ async function signWithEd25519(obj, privateKey) {
39
+ const algId = new pkijs.AlgorithmIdentifier({ algorithmId: ED25519_OID });
40
+ obj.signature = algId;
41
+ obj.signatureAlgorithm = algId;
42
+ const tbsBer = obj.encodeTBS().toBER();
43
+ obj.tbsView = new Uint8Array(tbsBer);
44
+ const sig = await webcrypto.subtle.sign('Ed25519', privateKey, tbsBer);
45
+ obj.signatureValue = new asn1js.BitString({ valueHex: sig });
46
+ }
47
+ /** Convert a pkijs Certificate to PEM string */
48
+ export function certToPem(cert) {
49
+ const der = cert.toSchema(false).toBER();
50
+ const b64 = Buffer.from(der).toString('base64');
51
+ return `-----BEGIN CERTIFICATE-----\n${b64.match(/.{1,64}/g).join('\n')}\n-----END CERTIFICATE-----\n`;
52
+ }
53
+ /** Convert a pkijs CertificateRevocationList to PEM string */
54
+ export function crlToPem(crl) {
55
+ const der = crl.toSchema(false).toBER();
56
+ const b64 = Buffer.from(der).toString('base64');
57
+ return `-----BEGIN X509 CRL-----\n${b64.match(/.{1,64}/g).join('\n')}\n-----END X509 CRL-----\n`;
58
+ }
59
+ /** Create a signed Ed25519 X.509 v3 certificate */
60
+ export async function createCertificate(opts) {
61
+ const cert = new pkijs.Certificate();
62
+ cert.version = 2; // v3
63
+ cert.serialNumber = new asn1js.Integer({ value: opts.serialNumber });
64
+ const now = new Date();
65
+ cert.notBefore.value = now;
66
+ cert.notAfter.value = new Date(now.getTime() + opts.validDays * 24 * 60 * 60 * 1000);
67
+ // Build subject RDN
68
+ const buildRDN = (dn, target) => {
69
+ target.typesAndValues.push(new pkijs.AttributeTypeAndValue({
70
+ type: '2.5.4.3',
71
+ value: new asn1js.Utf8String({ value: dn.CN }),
72
+ }));
73
+ if (dn.O) {
74
+ target.typesAndValues.push(new pkijs.AttributeTypeAndValue({
75
+ type: '2.5.4.10',
76
+ value: new asn1js.Utf8String({ value: dn.O }),
77
+ }));
78
+ }
79
+ };
80
+ buildRDN(opts.subject, cert.subject);
81
+ buildRDN(opts.issuer, cert.issuer);
82
+ // Import public key into SubjectPublicKeyInfo
83
+ const spki = await webcrypto.subtle.exportKey('spki', opts.subjectPublicKey);
84
+ const spkiAsn1 = asn1js.fromBER(spki);
85
+ cert.subjectPublicKeyInfo.fromSchema(spkiAsn1.result);
86
+ // Add basic constraints extension (always present)
87
+ cert.extensions = [];
88
+ const bc = new pkijs.BasicConstraints({ cA: opts.isCA === true });
89
+ cert.extensions.push(new pkijs.Extension({
90
+ extnID: BASIC_CONSTRAINTS_OID,
91
+ critical: true,
92
+ extnValue: bc.toSchema().toBER(),
93
+ }));
94
+ // Add caller-supplied extensions
95
+ if (opts.extensions) {
96
+ cert.extensions.push(...opts.extensions);
97
+ }
98
+ await signWithEd25519(cert, opts.issuerKey);
99
+ return cert;
100
+ }
101
+ /** Create a CRL Distribution Points extension */
102
+ export function makeCRLDistributionPointsExt(url) {
103
+ const dp = new pkijs.DistributionPoint({
104
+ distributionPoint: [new pkijs.GeneralName({ type: 6, value: url })],
105
+ });
106
+ const cdp = new pkijs.CRLDistributionPoints({ distributionPoints: [dp] });
107
+ return new pkijs.Extension({
108
+ extnID: CRL_DISTRIBUTION_POINTS_OID,
109
+ critical: false,
110
+ extnValue: cdp.toSchema().toBER(),
111
+ });
112
+ }
113
+ /** Create an Authority Info Access extension with an OCSP URL */
114
+ export function makeOCSPAIAExt(url) {
115
+ const desc = new pkijs.AccessDescription({
116
+ accessMethod: OCSP_ACCESS_METHOD_OID,
117
+ accessLocation: new pkijs.GeneralName({ type: 6, value: url }),
118
+ });
119
+ const aia = new pkijs.InfoAccess({ accessDescriptions: [desc] });
120
+ return new pkijs.Extension({
121
+ extnID: AUTHORITY_INFO_ACCESS_OID,
122
+ critical: false,
123
+ extnValue: aia.toSchema().toBER(),
124
+ });
125
+ }
126
+ /** Create an Extended Key Usage extension */
127
+ export function makeExtKeyUsageExt(oids) {
128
+ const seq = new asn1js.Sequence({
129
+ value: oids.map((oid) => new asn1js.ObjectIdentifier({ value: oid })),
130
+ });
131
+ return new pkijs.Extension({
132
+ extnID: EXT_KEY_USAGE_OID,
133
+ critical: false,
134
+ extnValue: seq.toBER(),
135
+ });
136
+ }
137
+ export { OCSP_SIGNING_OID, CLIENT_AUTH_OID };
138
+ /** Create a signed X.509v2 CRL */
139
+ export async function createCRL(issuerCert, issuerKey, revokedSerials) {
140
+ const crl = new pkijs.CertificateRevocationList();
141
+ crl.version = 1; // CRLv2 = version field value 1
142
+ // Copy issuer from CA cert subject
143
+ crl.issuer = issuerCert.subject;
144
+ const now = new Date();
145
+ const next = new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000);
146
+ crl.thisUpdate = new pkijs.Time({ type: 0, value: now });
147
+ crl.nextUpdate = new pkijs.Time({ type: 0, value: next });
148
+ if (revokedSerials.length > 0) {
149
+ crl.revokedCertificates = revokedSerials.map((serial) => new pkijs.RevokedCertificate({
150
+ userCertificate: new asn1js.Integer({ value: serial }),
151
+ revocationDate: new pkijs.Time({ type: 0, value: now }),
152
+ }));
153
+ }
154
+ await signWithEd25519(crl, issuerKey);
155
+ return crl;
156
+ }
157
+ /**
158
+ * Sign a pkijs BasicOCSPResponse with an Ed25519 key.
159
+ * Must be called after tbsResponseData.responses is populated.
160
+ */
161
+ export async function signBasicOCSPResponse(basicResponse, privateKey) {
162
+ const algId = new pkijs.AlgorithmIdentifier({ algorithmId: ED25519_OID });
163
+ basicResponse.signatureAlgorithm = algId;
164
+ // Encode the TBS response data
165
+ const tbsDer = basicResponse.tbsResponseData.toSchema(true).toBER();
166
+ basicResponse.tbsResponseData.tbsView = new Uint8Array(tbsDer);
167
+ const sig = await webcrypto.subtle.sign('Ed25519', privateKey, tbsDer);
168
+ basicResponse.signature = new asn1js.BitString({ valueHex: sig });
169
+ }
170
+ //# sourceMappingURL=certGenUtils.js.map