tr-fetch 0.9.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Timo J. Rinne
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,473 @@
1
+ # tr-fetch
2
+
3
+ Node.js `fetch()` with certificate revocation list (CRL) and OCSP checking.
4
+ Plain JavaScript, CommonJS, no build step. Requires Node.js 26 or later.
5
+
6
+ ```js
7
+ const trFetch = require('tr-fetch');
8
+
9
+ const response = await trFetch('https://example.com/');
10
+ console.log(await response.text());
11
+ ```
12
+
13
+ ES module default imports work too:
14
+
15
+ ```js
16
+ import trFetch from 'tr-fetch';
17
+ ```
18
+
19
+ The function calls Node's system `fetch()` and returns its native `Response`.
20
+ It accepts the usual string, `URL` or `Request` input and fetch options,
21
+ including streaming bodies, redirects and abort signals.
22
+
23
+ ## Options
24
+
25
+ ```js
26
+ const response = await trFetch(url, {
27
+ method: 'GET',
28
+ trFetchDebug: false,
29
+ trFetchCrlPolicy: {
30
+ disabled: false,
31
+ missingCrlDistributionPoint: 'ignore',
32
+ unreachableCrlDistributionPoint: 'reject',
33
+ invalidCrl: 'reject',
34
+ revokedCertificate: 'reject'
35
+ },
36
+ trFetchCrlCacheSize: 32,
37
+ trFetchCrlCacheTTL: 1800,
38
+ trFetchCrlCheckDepth: 0,
39
+ trFetchOcspPolicy: {
40
+ disabled: false,
41
+ missingOcspUri: 'ignore',
42
+ unreachableOcspUri: 'reject',
43
+ rejectedCertificate: 'reject'
44
+ },
45
+ trFetchOcspCheckDepth: 0
46
+ });
47
+ ```
48
+
49
+ These are the defaults. Policy objects may specify only the properties to
50
+ change. Both checks are enabled by default and operate independently. Each
51
+ failure policy accepts `'ignore'`, `'warn'` or `'reject'`.
52
+
53
+ | Policy | Condition |
54
+ |---|---|
55
+ | `missingCrlDistributionPoint` | The checked certificate has no CRL distribution points extension and no applicable override. |
56
+ | `unreachableCrlDistributionPoint` | No usable network location, a forbidden URI scheme, download failure, non-200 response, timeout, excessive redirects or download size. |
57
+ | `invalidCrl` | Malformed data, wrong issuer, bad signature, invalid signing permission, invalid dates, expired CRL, mismatched scope or unsupported CRL features. |
58
+ | `revokedCertificate` | The certificate serial is present in a successfully authenticated, applicable CRL. |
59
+ | `missingOcspUri` | No OCSP access location is advertised in the certificate's Authority Information Access extension, and no applicable override is set. |
60
+ | `unreachableOcspUri` | Forbidden URI scheme, download failure, non-200 HTTP response, timeout, excessive redirects or response size. |
61
+ | `rejectedCertificate` | OCSP reports revoked or unknown; or the response is malformed, unsuccessful, stale, incorrectly signed, unauthorized, mismatched or otherwise unsupported. |
62
+
63
+ `ignore` continues without a warning. `warn` emits a Node.js process warning and
64
+ continues. `reject` rejects the fetch promise before sending the HTTP request
65
+ on that connection. These policies apply only to revocation checks; they cannot
66
+ relax normal TLS verification.
67
+
68
+ ### Disabling a check
69
+
70
+ Both policy objects accept `disabled`. It defaults to `false`; `undefined` and
71
+ `null` also mean `false`. Only boolean `true` disables the corresponding check.
72
+ Other types reject with `TypeError`.
73
+
74
+ ```js
75
+ await trFetch(url, {
76
+ trFetchCrlPolicy: { disabled: true },
77
+ trFetchOcspPolicy: { disabled: true }
78
+ });
79
+ ```
80
+
81
+ A disabled check performs no certificate revocation inspection, cache access,
82
+ responder lookup or warning emission. Its depth and overrides are not used.
83
+ Options are still validated. Disabling either check leaves the other enabled;
84
+ disabling both retains mandatory normal TLS trust and hostname verification.
85
+
86
+ Unknown `trFetch...` options, unknown policy keys and invalid option values
87
+ reject with `TypeError`. Custom options are removed before the request reaches
88
+ system fetch. The caller's options are not modified.
89
+
90
+ ### Debug output
91
+
92
+ Set `trFetchDebug: true` to write verification diagnostics to **stderr**:
93
+
94
+ ```js
95
+ await trFetch('https://example.com/', { trFetchDebug: true });
96
+ ```
97
+
98
+ The default is `false` (also when omitted or `undefined`); other values,
99
+ including `null`, reject with `TypeError`. This option is removed before
100
+ calling system fetch.
101
+
102
+ Each line starts with `[trFetch debug #N]`, followed by an event and JSON details.
103
+ The ID groups events from the same fetch, including redirects, when calls run
104
+ concurrently. Certificate events identify the hostname, serial number, and
105
+ `leaf` at depth `0` or `intermediate CA` at depth `1` and above.
106
+
107
+ Diagnostics cover discovered CRL distribution points and OCSP URIs, overrides,
108
+ downloads and redirects, parsing, authenticated serial/status results, and
109
+ policy actions (`ignore`, `warn`, `reject`, or `continue` for successful checks).
110
+ Cache events report hits, misses, storage, bypass, expiration by TTL or CRL
111
+ validity, and LRU purges. Expiration and purge events appear when cache access
112
+ actually removes entries; there is no background expiration timer.
113
+
114
+ For example, a successful OCSP result includes:
115
+
116
+ ```text
117
+ [trFetch debug #1] OCSP check completed {"hostname":"example.com","certificate":"leaf","depth":0,"serial":"2A","source":"https://ca.example/ocsp","result":"good","authenticated":true,"policy":"rejectedCertificate","configuredAction":"reject","action":"continue","nextUpdate":1790500000000}
118
+ ```
119
+
120
+ An unusable candidate is logged as a detected condition; its failure policy is
121
+ applied only if no usable alternative exists. Parsing alone does not indicate
122
+ authentication. Disabled checks and excluded trust anchors are identified.
123
+ Debugging does not change verification, caching, warnings or rejection behavior,
124
+ and enabling it for one fetch does not enable it for other calls.
125
+
126
+ Logged URLs omit credentials, query strings and fragments. Request headers,
127
+ request/response bodies and raw certificate/CRL data are not logged. Hostnames,
128
+ URL paths and certificate serial numbers remain visible.
129
+
130
+ ### Check depth
131
+
132
+ `trFetchCrlCheckDepth` and `trFetchOcspCheckDepth` are independent top-level
133
+ options with the same values:
134
+
135
+ | Value | Certificates checked |
136
+ |---|---|
137
+ | `0` or `'leaf'` (default) | The server certificate. |
138
+ | `1` | The server certificate and its first intermediate CA. |
139
+ | `n` | The server certificate and up to `n` intermediate CAs. |
140
+ | `'full-chain'` | All certificates exposed by the verified TLS chain, excluding its trust anchor. |
141
+
142
+ Numeric depths must be nonnegative safe integers. The trust anchor is excluded
143
+ at every depth; trust in it is established by Node's CA configuration. The
144
+ server certificate is checked whenever that check is enabled, including when
145
+ explicitly trusted and self-signed. Chain traversal has a defensive limit of
146
+ 32 certificates.
147
+
148
+ The same policies apply at every selected depth. Each intermediate uses its
149
+ own distribution points or OCSP URIs. Overrides apply only to the server
150
+ certificate. A successful check does not override a rejection from the other
151
+ enabled check.
152
+
153
+ ### Overrides
154
+
155
+ Set **one** of these options:
156
+
157
+ ```js
158
+ await trFetch(url, {
159
+ trFetchCrlDistributionPointOverride: 'https://ca.example.com/current.crl'
160
+ });
161
+
162
+ await trFetch(url, {
163
+ trFetchCrlOverride: crlBytes
164
+ });
165
+ ```
166
+
167
+ `trFetchCrlDistributionPointOverride` accepts an absolute URL string or `URL`.
168
+ `trFetchCrlOverride` accepts a PEM string, `Buffer`, `Uint8Array` or
169
+ `ArrayBuffer` containing one complete PEM or DER CRL. It is data, never a file
170
+ path. Supplying both options rejects with `TypeError`.
171
+
172
+ Either override bypasses the server certificate's advertised distribution
173
+ points. Override CRLs still require a valid signature from the actual issuer,
174
+ appropriate signing permission, valid dates and applicable scope. A directly
175
+ supplied CRL with a named issuing distribution point cannot establish a match
176
+ to an effective network distribution point and is rejected as `invalidCrl`.
177
+ Direct override data is copied and is not put in the download cache.
178
+
179
+ `trFetchOcspUriOverride` independently accepts an absolute URL string or `URL`:
180
+
181
+ ```js
182
+ await trFetch(url, {
183
+ trFetchOcspUriOverride: 'https://ca.example.com/ocsp'
184
+ });
185
+ ```
186
+
187
+ It replaces the leaf certificate's advertised OCSP URIs. It can be used with
188
+ either CRL override. Responses from the forced URI still require authentication
189
+ and a matching certificate identifier. Intermediates use their own OCSP URIs.
190
+
191
+ ### Cache
192
+
193
+ Fetched and validated CRLs share an in-memory LRU cache within the loaded
194
+ module. Cache keys include the issuer certificate and distribution URL.
195
+ Certificate decisions and failed lookups are never cached. A cached CRL is
196
+ validated against the current certificate and current time on every use.
197
+
198
+ - `trFetchCrlCacheSize` defaults to `32` entries. Any integer **0 or less**
199
+ disables caching for that call.
200
+ - `trFetchCrlCacheTTL` defaults to `1800` **seconds**. `0` disables caching;
201
+ `-1` removes the TTL limit. Other values must be nonnegative safe integers.
202
+ - Entries expire at the earlier of their original TTL deadline and CRL
203
+ `nextUpdate`. Reading an entry does not extend its lifetime.
204
+ - A later caller's shorter TTL also limits the age of an existing entry.
205
+ A longer TTL cannot extend an entry's original expiry.
206
+ - The calling request's size limit is applied whenever the shared cache is
207
+ accessed or updated. Reducing the size evicts the least recently used entries.
208
+ - Expired entries are removed lazily on cache access. They are never reused.
209
+
210
+ With TTL `-1`, CRLs still expire at `nextUpdate` and can be evicted by LRU.
211
+ Concurrent misses can download the same CRL independently; cancellation and
212
+ policy decisions remain local to each request.
213
+
214
+ OCSP responses are not cached. Every checked certificate gets a fresh OCSP
215
+ request; CRL cache options affect only CRLs.
216
+
217
+ ## Errors and warnings
218
+
219
+ CRL rejections are `trFetch.TrFetchCrlError` instances; OCSP rejections are
220
+ `trFetch.TrFetchOcspError` instances. Both are surfaced directly
221
+ instead of being hidden beneath fetch's generic `TypeError: fetch failed`.
222
+
223
+ | `code` | Policy |
224
+ |---|---|
225
+ | `TR_FETCH_CRL_MISSING_DISTRIBUTION_POINT` | `missingCrlDistributionPoint` |
226
+ | `TR_FETCH_CRL_UNREACHABLE_DISTRIBUTION_POINT` | `unreachableCrlDistributionPoint` |
227
+ | `TR_FETCH_CRL_INVALID` | `invalidCrl` |
228
+ | `TR_FETCH_CERTIFICATE_REVOKED` | `revokedCertificate` |
229
+ | `TR_FETCH_OCSP_MISSING_URI` | `missingOcspUri` |
230
+ | `TR_FETCH_OCSP_UNREACHABLE_URI` | `unreachableOcspUri` |
231
+ | `TR_FETCH_OCSP_CERTIFICATE_REJECTED` | `rejectedCertificate` |
232
+
233
+ Messages explain the specific failure. Certificate errors include `policyKey`,
234
+ `hostname`, `serialNumber`, `fingerprint256`, and, when applicable,
235
+ `distributionPoint` or `ocspUri`, and `cause`. OCSP rejection details include
236
+ `ocspStatus`: `'revoked'`, `'unknown'` or `'invalid-response'`.
237
+
238
+ ```js
239
+ try {
240
+ await trFetch(url);
241
+ } catch (error) {
242
+ if ((error instanceof trFetch.TrFetchCrlError) || (error instanceof trFetch.TrFetchOcspError)) {
243
+ console.error(error.code, error.message);
244
+ } else {
245
+ throw error;
246
+ }
247
+ }
248
+
249
+ process.on('warning', function(warning) {
250
+ if ([ 'TrFetchCrlWarning', 'TrFetchOcspWarning' ].includes(warning.name)) {
251
+ console.error(warning.code, warning.message);
252
+ }
253
+ });
254
+ ```
255
+
256
+ Warnings use the same codes and context, with the names `TrFetchCrlWarning`
257
+ and `TrFetchOcspWarning`.
258
+ Normal TLS/network failures keep native fetch's error behavior. Cancellation
259
+ preserves the caller's abort reason.
260
+
261
+ ## Connection and revocation behavior
262
+
263
+ An Undici connector performs ordinary Node TLS verification with
264
+ `rejectUnauthorized: true`. It then performs the enabled checks before handing
265
+ that **same socket** to system fetch. There is no separate TLS probe and no second,
266
+ unchecked connection carrying the application request. HTTPS redirect
267
+ destinations go through the same checks. HTTP requests have no certificate
268
+ to check; standard fetch redirect behavior otherwise applies.
269
+
270
+ Each fetch call owns an agent. Request sockets and TLS sessions are not reused;
271
+ redirects also establish checked connections. This avoids stale revocation
272
+ decisions on pooled connections, with extra handshake overhead. Response bodies
273
+ remain streamable while the agent closes gracefully after their consumption
274
+ or cancellation.
275
+
276
+ The wrapper retains Node's default CA trust, certificate validity, hostname
277
+ verification and TLS settings. It does not install a global dispatcher or
278
+ change TLS verification settings. Its Undici dependency may initialize the
279
+ normal global dispatcher if one has not yet been created. Explicit custom
280
+ dispatchers, custom global dispatchers, and unrecognized dispatcher
281
+ configurations are rejected: silently replacing custom trust, pinning or proxy
282
+ settings could weaken the application's security. Proxy agents and custom
283
+ per-agent TLS configuration are therefore unsupported. Default-dispatcher
284
+ recognition inspects Undici configuration and fails closed on unknown layouts.
285
+
286
+ System fetch drives the package's Undici agent through the dispatcher API of
287
+ the Undici version bundled with Node.js. Their major versions must match;
288
+ otherwise trFetch rejects with `TypeError` before connecting.
289
+
290
+ trFetch calls `globalThis.fetch`. Wrappers that pass the `dispatcher` option
291
+ on, such as most instrumentation, keep working. If a replacement drops it, the
292
+ request is sent over an unchecked connection. trFetch detects this and fails
293
+ closed: an HTTP(S) response is accepted only if both the request URL and the
294
+ final response URL were reached over connections that its own agent opened and
295
+ verified. Otherwise the fetch rejects with `TypeError`, and a CRL or OCSP
296
+ download counts as unreachable. The request may already have been sent by then,
297
+ so do not replace system fetch with an implementation that ignores
298
+ `dispatcher`.
299
+
300
+ CRL retrieval:
301
+
302
+ - Accepts only `http:` and `https:` URLs. Files, local paths, LDAP, LDAPS, FTP,
303
+ data URLs and URLs containing credentials are unreachable distribution points.
304
+ - Validates every redirect target and uses normal, mandatory TLS verification
305
+ for HTTPS downloads. Download failures use `unreachableCrlDistributionPoint`.
306
+ - Does not copy application cookies, authorization headers or request bodies
307
+ into CRL requests.
308
+ - Allows at most five redirects, 16 MiB of decoded response data and ten seconds
309
+ per download including redirects and body reading. Caller cancellation also
310
+ cancels CRL retrieval. At most 32 distribution URIs are tried per certificate.
311
+ - Allows private-network HTTP(S) endpoints, including loopback, for private PKI.
312
+ It does not perform filesystem or LDAP lookup.
313
+ - Does not recursively check the CRL download server's CRLs or OCSP status.
314
+ Download HTTPS trust is verified normally, and CRL contents must independently
315
+ authenticate under the checked certificate's issuer.
316
+
317
+ Alternative distribution URIs are tried until a valid, applicable complete CRL
318
+ is obtained. Only when all alternatives fail are their failure policies applied.
319
+ A successfully authenticated CRL listing the certificate immediately invokes
320
+ the revocation policy; it does not trigger a search for a different answer.
321
+
322
+ Supported CRLs are direct, complete CRLs with SHA-256, SHA-384 or SHA-512
323
+ signatures supported by PKIjs/Web Crypto, including RSA and ECDSA. Validation
324
+ checks issuer binding, signature algorithm consistency, `cRLSign` key usage
325
+ when present, authority key identifiers when available, dates and scope.
326
+ `nextUpdate` is required. Matching named issuing distribution points and
327
+ user/CA certificate scopes are supported.
328
+
329
+ Delta CRLs, indirect CRLs, reason-limited distribution points or CRLs,
330
+ attribute-certificate CRLs, unsupported critical extensions and weak or
331
+ unsupported signature algorithms use `invalidCrl`. This implementation does
332
+ not merge delta CRLs.
333
+
334
+ ### OCSP behavior
335
+
336
+ The library reads OCSP URIs from the certificate's Authority Information Access
337
+ extension and sends a DER request using HTTP POST with
338
+ `Content-Type: application/ocsp-request`. Requests contain the certificate's
339
+ serial and issuer hashes, with a fresh 32-byte nonce. SHA-1 is used for the
340
+ standard issuer identifier; response signatures must use supported SHA-256,
341
+ SHA-384 or SHA-512 algorithms.
342
+
343
+ Only HTTP(S) responder URIs are accepted. Each lookup has a ten-second deadline
344
+ and a 1 MiB response limit. HTTP 307/308 redirects preserve the POST body, with
345
+ at most five redirects; other redirect codes use `unreachableOcspUri`. Every
346
+ redirect URI is validated. Caller cancellation interrupts the lookup. Up to
347
+ 32 advertised URIs are tried per certificate. Requests contain no application
348
+ authorization headers, cookies or body. Private-network HTTP(S) is allowed.
349
+
350
+ A successful response must contain exactly one matching certificate identifier
351
+ and a signature authorized by that certificate's actual issuer. An issuer may
352
+ sign directly, or delegate to a certificate it issued directly. A delegated
353
+ responder must have OCSP signing extended key usage, digital-signature key usage
354
+ when present, current certificate validity, and `id-pkix-ocsp-nocheck`. Delegates
355
+ without no-check are currently unsupported because their own revocation status
356
+ would require a separate validation path. Weak delegate certificate signatures
357
+ and RSA keys shorter than 2048 bits are rejected.
358
+
359
+ Response `producedAt` and `thisUpdate` must not be in the future. `nextUpdate`,
360
+ when present, must not have passed. When it is absent, the response is accepted
361
+ for at most five minutes after `thisUpdate`. Signer certificate expiry also
362
+ bounds validity. Unknown critical extensions and duplicate or mismatched
363
+ certificate identifiers reject. If a responder returns a nonce, it must match
364
+ the request; responders that omit it remain subject to the validity-time checks.
365
+
366
+ Unusable alternatives are tried until an authenticated status is obtained.
367
+ An authenticated revoked or unknown status immediately invokes
368
+ `rejectedCertificate`; the client does not seek a different answer elsewhere.
369
+ Malformed, unsuccessful or unauthenticated responses also use that policy when
370
+ no usable alternative exists, with a distinct explanatory message.
371
+
372
+ OCSP responder HTTPS connections receive normal TLS verification. Their own
373
+ revocation endpoints are not recursively queried. This is active OCSP lookup;
374
+ TLS-stapled responses are not consumed.
375
+
376
+ The transport uses Node's documented
377
+ [fetch dispatcher interface](https://nodejs.org/api/globals.html#custom-dispatcher)
378
+ and [TLS certificate inspection](https://nodejs.org/api/tls.html#tlssocketgetpeercertificatedetailed).
379
+ CRL format and scope rules follow the applicable parts of
380
+ [RFC 5280](https://www.rfc-editor.org/rfc/rfc5280). OCSP verification follows
381
+ the applicable parts of [RFC 6960](https://www.rfc-editor.org/rfc/rfc6960).
382
+
383
+ ## tr-curl
384
+
385
+ The package installs `tr-curl`, a small `curl` replacement built on trFetch.
386
+ It is mainly a test tool: every HTTPS connection gets trFetch's CRL and OCSP
387
+ checks. Only `http:` and `https:` URLs are supported; a URL without a scheme
388
+ defaults to `http://`.
389
+
390
+ ```sh
391
+ npx tr-curl -v https://example.com/
392
+ tr-curl -fsSL -o page.html https://example.com/
393
+ tr-curl --json '{"a":1}' -u user:password https://api.example.com/items
394
+ tr-curl --cacert private-ca.pem --crlfile current.crl https://internal.example/
395
+ ```
396
+
397
+ `tr-curl --help` lists the options. Their meaning follows curl:
398
+
399
+ | Area | Options |
400
+ |---|---|
401
+ | Request | `-X/--request`, `-H/--header`, `-A/--user-agent`, `-e/--referer`, `-r/--range`, `--etag-compare`, `-G/--get`, `-I/--head` |
402
+ | Data | `-d/--data`, `--data-ascii`, `--data-binary`, `--data-raw`, `--data-urlencode`, `--json` |
403
+ | Authentication | `-u/--user`, `--oauth2-bearer`, credentials in the URL |
404
+ | Redirects | `-L/--location`, `--location-trusted`, `--max-redirs` |
405
+ | Output | `-o/--output`, `-O/--remote-name`, `--remote-name-all`, `--output-dir`, `--create-dirs`, `--remove-on-error`, `-i/--include` |
406
+ | Failure and limits | `-f/--fail`, `--fail-with-body`, `--fail-early`, `--max-filesize`, `-m/--max-time` |
407
+ | Messages | `-v/--verbose`, `-s/--silent`, `-S/--show-error`, `--no-progress-meter`, `-#/--progress-bar`, `-h/--help`, `-V/--version` |
408
+ | TLS | `-k/--insecure`, `--cacert`, `--crlfile`, `-1/--tlsv1`, `--tlsv1.0` … `--tlsv1.3`, `--tls-max`, `--ciphers`, `--tls13-ciphers` |
409
+ | trFetch | `--trfetch-options <json>` |
410
+
411
+ `--verbose` prints the request and response headers, prefixed with `>` and
412
+ `<` like curl, and also enables `trFetchDebug`, so the revocation diagnostics
413
+ appear on stderr. `--trfetch-options` takes a JSON object of trFetch options,
414
+ for example `'{"trFetchCrlCheckDepth":"full-chain"}'`. The option can be
415
+ repeated, and later objects override earlier keys. `--crlfile` sets
416
+ `trFetchCrlOverride`. `--cacert` replaces Node's default CA certificates for
417
+ the process.
418
+
419
+ `--insecure` cannot be implemented through trFetch, which never relaxes TLS
420
+ verification. With `-k`, tr-curl uses plain fetch with an unverified TLS
421
+ connection instead, so no CRL or OCSP checks are made.
422
+
423
+ Exit codes follow curl, for example: 1 unsupported protocol, 2 usage error,
424
+ 3 malformed URL, 6 unresolvable host, 7 connection failure, 18 partial
425
+ transfer, 22 HTTP error with `--fail`, 23 write error, 28 timeout, 35 TLS
426
+ handshake failure, 47 too many redirects, 60 certificate verification failure
427
+ or CRL rejection, 63 maximum file size exceeded, and 91 OCSP rejection.
428
+
429
+ Differences from curl:
430
+
431
+ - Options are parsed with [Optist](https://www.npmjs.com/package/optist).
432
+ `--name=value` is accepted, short options that take an argument cannot be
433
+ combined with other short options (`-o file`, not `-ofile`), and single-value
434
+ options may be given only once. Options may follow URLs, and `--` ends
435
+ option parsing.
436
+ - The `-o` and `-O` options cannot be mixed. `-o` names pair with URLs in
437
+ order, and `-O` applies to as many URLs as it is given.
438
+ - Requests use HTTP/1.1 through fetch. Fetch sets `Host`, `Connection` and
439
+ `Content-Length` itself, adds `Accept-Language`, `Sec-Fetch-Mode` and
440
+ `Accept-Encoding`, and decodes compressed responses. `-H "Name:"` removes
441
+ only headers that tr-curl itself would add. Response header names appear in
442
+ lowercase.
443
+ - TLS-SRP (`--tlsuser`, `--tlspassword`, `--tlsauthtype`) and other TLS
444
+ backend features that Node.js does not provide are not supported. URL
445
+ globbing, cookies, proxies, uploads and non-HTTP protocols are not
446
+ implemented either.
447
+
448
+ ## Development
449
+
450
+ ```sh
451
+ npm install
452
+ npm test
453
+ ```
454
+
455
+ Enable fetch diagnostics during tests with:
456
+
457
+ ```sh
458
+ TR_FETCH_TEST_DEBUG=yes npm test
459
+ ```
460
+
461
+ `TR_FETCH_TEST_DEBUG` accepts `y`, `yes`, `true` or `1`, case-insensitively.
462
+ Other values (or an unset variable) leave test debugging off. This sets
463
+ `trFetchDebug: true` by default for test fetch calls; explicit options in tests
464
+ that verify debugging behavior still take precedence. The variable affects
465
+ only the test suite, not the library's runtime defaults.
466
+
467
+ Tests use Node's built-in test runner and generate temporary signing keys,
468
+ certificates, CRLs and OCSP responses in memory. Integration tests run local
469
+ HTTP/HTTPS servers; they do not require public network access.
470
+
471
+ Runtime dependencies: `undici` for the transport adapter, `pkijs` and `asn1js`
472
+ for X.509/CRL/OCSP parsing and signature verification, and `optist` for
473
+ tr-curl's command line. No compiler or external OpenSSL executable is required.
package/bin/tr-curl.js ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const { main } = require('../tr-curl/main');
5
+
6
+ main(process.argv.slice(2)).then(function(code) {
7
+ process.exitCode = code;
8
+ }, function(error) {
9
+ process.stderr.write(`tr-curl: ${error?.stack ?? error}\n`);
10
+ process.exitCode = 2;
11
+ });
package/cache.js ADDED
@@ -0,0 +1,70 @@
1
+ 'use strict';
2
+
3
+ const { debugUrl } = require('./debug');
4
+
5
+ function cacheEntry(key) {
6
+ const separator = key.indexOf(':');
7
+ return { issuer: key.slice(0, separator), source: debugUrl(key.slice(separator + 1)) };
8
+ }
9
+
10
+ // Cache verified CRLs, never a policy decision or a failed lookup. A caller's
11
+ // shorter TTL also applies to entries populated by an earlier caller.
12
+ class CrlCache {
13
+ #entries = new Map();
14
+
15
+ prune(size, now = Date.now(), debug) {
16
+ for (const [key, entry] of this.#entries) {
17
+ if (now >= entry.expiresAt) {
18
+ this.#entries.delete(key);
19
+ debug?.('CRL cache expired', { ...cacheEntry(key),
20
+ reason: (entry.expiresAt === entry.nextUpdate) ? 'CRL nextUpdate' : 'TTL', expiresAt: entry.expiresAt });
21
+ }
22
+ }
23
+ while (this.#entries.size > Math.max(0, size)) {
24
+ const key = this.#entries.keys().next().value;
25
+ this.#entries.delete(key);
26
+ debug?.('CRL cache purged', { ...cacheEntry(key), reason: (size <= 0) ? 'cache disabled' : 'LRU capacity', size });
27
+ }
28
+ }
29
+
30
+ get(key, size, ttl, now = Date.now(), debug) {
31
+ this.prune(size, now, debug);
32
+ if ((size <= 0) || (ttl === 0)) {
33
+ debug?.('CRL cache bypassed', { ...cacheEntry(key), reason: 'cache disabled', size, ttl });
34
+ return undefined;
35
+ }
36
+ const entry = this.#entries.get(key);
37
+ if (! entry) {
38
+ debug?.('CRL cache miss', cacheEntry(key));
39
+ return undefined;
40
+ }
41
+ if ((ttl !== -1) && ((now - entry.fetchedAt) >= (ttl * 1000))) {
42
+ this.#entries.delete(key);
43
+ debug?.('CRL cache expired', { ...cacheEntry(key), reason: 'caller TTL', ttl, ageSeconds: (now - entry.fetchedAt) / 1000 });
44
+ return undefined;
45
+ }
46
+ this.#entries.delete(key);
47
+ this.#entries.set(key, entry);
48
+ debug?.('CRL cache hit', { ...cacheEntry(key), ageSeconds: (now - entry.fetchedAt) / 1000, expiresAt: entry.expiresAt });
49
+ return entry.value;
50
+ }
51
+
52
+ set(key, value, nextUpdate, fetchedAt, size, ttl, now = Date.now(), debug) {
53
+ this.prune(size, now, debug);
54
+ if ((size <= 0) || (ttl === 0)) {
55
+ debug?.('CRL cache store skipped', { ...cacheEntry(key), reason: 'cache disabled', size, ttl });
56
+ return;
57
+ }
58
+ const expiresAt = Math.min(nextUpdate, (ttl === -1) ? Infinity : fetchedAt + (ttl * 1000));
59
+ if (expiresAt <= now) {
60
+ debug?.('CRL cache store skipped', { ...cacheEntry(key), reason: 'already expired', expiresAt });
61
+ return;
62
+ }
63
+ this.#entries.delete(key);
64
+ this.#entries.set(key, { value, fetchedAt, expiresAt, nextUpdate });
65
+ debug?.('CRL cache stored', { ...cacheEntry(key), expiresAt, nextUpdate, ttl, size });
66
+ this.prune(size, now, debug);
67
+ }
68
+ }
69
+
70
+ module.exports = CrlCache;