@daloyjs/core 0.43.0 → 1.0.0-beta.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/README.md CHANGED
@@ -498,7 +498,7 @@ The core only ever sees `Request → Response`. Adapters live at the edge.
498
498
 
499
499
  ## Status
500
500
 
501
- DaloyJS is in **public preview** (`0.x`). The public API may still change between minor versions; deprecations will get at least one minor cycle once `1.0.0` ships. The framework is already in use for production trials.
501
+ DaloyJS is now in the **`1.0.0` beta** (`1.0.0-beta.0`). The public API is feature-complete and stable for the 1.0 line; from `1.0.0` onward, breaking changes follow SemVer and deprecations get at least one minor cycle. Small adjustments are still possible before the `1.0.0` GA if beta feedback surfaces something. The framework is already in use for production trials.
502
502
 
503
503
  **Release quality bar.** Every release ships with **≥90% line + function coverage and ≥90% branch coverage**, strict TypeScript, OpenSSF Scorecard, CodeQL + Opengrep dual SAST, zizmor workflow linting, and npm provenance. Coverage was relaxed from a former 100% gate so complex security work isn't blocked chasing throwaway tests for unreachable defensive branches or tsx source-map phantoms; see [AGENTS.md](AGENTS.md) for the policy.
504
504
 
@@ -8,7 +8,17 @@ import type { App } from "../app.js";
8
8
  export interface NodeServerOptions {
9
9
  port?: number;
10
10
  hostname?: string;
11
- /** Connection-level timeout in ms. Default: 30000. */
11
+ /**
12
+ * Connection-level timeout in ms, applied to BOTH `headersTimeout` and
13
+ * `requestTimeout`. A client that has not finished sending its request
14
+ * headers (or whole request) within this window is hung up on with `408`.
15
+ * This is the slowloris / slow-header defense: the adapter also lowers
16
+ * Node's `connectionsCheckingInterval` to a fraction of this value so the
17
+ * timeout is actually *enforced* near when it elapses, rather than only at
18
+ * Node's default 30s sweep (which would let a stalled or byte-trickling
19
+ * connection hold a socket open far past the configured timeout). Set `0`
20
+ * to disable timeouts entirely. Default: 30000.
21
+ */
12
22
  connectionTimeoutMs?: number;
13
23
  /** Drain timeout for graceful shutdown. Default: 10000. */
14
24
  shutdownTimeoutMs?: number;
@@ -13,7 +13,23 @@ export function serve(app, opts = {}) {
13
13
  const bufferedBodyMaxBytes = typeof opts.bufferedBodyMaxBytes === "number" && opts.bufferedBodyMaxBytes >= 0
14
14
  ? opts.bufferedBodyMaxBytes
15
15
  : DEFAULT_BUFFERED_BODY_MAX_BYTES;
16
- const server = createServer({ maxHeaderSize: opts.maxHeaderBytes ?? 16 * 1024 }, (req, res) => {
16
+ const connectionTimeoutMs = opts.connectionTimeoutMs ?? 30_000;
17
+ // Node only *enforces* `headersTimeout` / `requestTimeout` when its periodic
18
+ // connection checker runs, and that checker's default interval is 30s. With
19
+ // the default interval a slowloris that never completes its headers — idle
20
+ // OR trickling a byte at a time — survives until the next 30s sweep, far
21
+ // past the configured timeout, holding a socket open the whole time. Polling
22
+ // at a fraction of the timeout (bounded to [1s, 5s]) makes the configured
23
+ // timeout actually honored, so a stalled connection is reaped with `408`
24
+ // close to when it should be. Skipped only when timeouts are disabled
25
+ // (`connectionTimeoutMs: 0`), where there is nothing to reap.
26
+ const connectionsCheckingInterval = connectionTimeoutMs > 0
27
+ ? Math.max(1_000, Math.min(5_000, Math.floor(connectionTimeoutMs / 2)))
28
+ : undefined;
29
+ const server = createServer({
30
+ maxHeaderSize: opts.maxHeaderBytes ?? 16 * 1024,
31
+ ...(connectionsCheckingInterval !== undefined ? { connectionsCheckingInterval } : {}),
32
+ }, (req, res) => {
17
33
  // GET/HEAD: no body work, dispatch directly. Keep this first so the GET
18
34
  // hot path doesn't pay for any of the buffering bookkeeping below.
19
35
  const method = req.method;
@@ -33,8 +49,8 @@ export function serve(app, opts = {}) {
33
49
  }
34
50
  dispatchToApp(app, req, res, trustProxy, undefined);
35
51
  });
36
- server.requestTimeout = opts.connectionTimeoutMs ?? 30_000;
37
- server.headersTimeout = opts.connectionTimeoutMs ?? 30_000;
52
+ server.requestTimeout = connectionTimeoutMs;
53
+ server.headersTimeout = connectionTimeoutMs;
38
54
  server.keepAliveTimeout = 5_000;
39
55
  // Native parser-level header-count cap. Drops header-count floods (the
40
56
  // "HTTP/2 Bomb" amplification dimension) before they become a Request.
@@ -52,18 +52,21 @@
52
52
  * its **own** DNS lookup when it opens the socket. An attacker that
53
53
  * controls a TTL=0 record can return a public IP at validation time and
54
54
  * a `127.0.0.1` / `169.254.169.254` at connect time, slipping past the
55
- * library-level check. This residual TOCTOU window cannot be closed at
56
- * the `fetch()` boundary without injecting a custom HTTP dispatcher
57
- * (e.g. `undici.Agent({ connect: { lookup } })`) — and Daloy
58
- * deliberately ships **zero** runtime dependencies, so we do not bundle
59
- * `undici`. To fully close the window:
60
- *
55
+ * library-level check. To close the window:
56
+ *
57
+ * 0. **Built-in, `http:` only** (recommended for cloud-metadata defense):
58
+ * set {@link FetchGuardOptions.pinDns} `: true`. On Node, `http:`
59
+ * requests are then dispatched through `node:http` with the socket
60
+ * pinned to the validated IP (and the original `Host` header
61
+ * preserved), so there is no connect-time re-resolution to rebind.
62
+ * `https:` is not pinned by this knob — see its docs — so the items
63
+ * below still matter for TLS upstreams.
61
64
  * 1. **Operator-side** (recommended): block egress to RFC1918 /
62
65
  * loopback / link-local at the VPC / firewall layer. This neutralises
63
66
  * the rebinding even if the application is naïve.
64
- * 2. **Caller-side, Node only**: install `undici` and pass a custom
65
- * `fetch` that wires a dispatcher with a pinned-IP `connect.lookup`,
66
- * e.g.
67
+ * 2. **Caller-side, Node only** (covers `https:` too): install `undici`
68
+ * and pass a custom `fetch` that wires a dispatcher with a pinned-IP
69
+ * `connect.lookup`, e.g.
67
70
  * ```ts
68
71
  * import { Agent, fetch as undiciFetch } from "undici";
69
72
  * import * as dns from "node:dns/promises";
@@ -177,6 +180,41 @@ export interface FetchGuardOptions {
177
180
  * without `--allow-net`) or to enforce an in-memory test fixture.
178
181
  */
179
182
  resolve?: (hostname: string) => Promise<readonly string[]>;
183
+ /**
184
+ * Close the residual DNS-rebinding (TOCTOU) window for **`http:`** requests
185
+ * by connecting the socket to the exact IP that was validated, instead of
186
+ * letting the underlying client re-resolve the hostname at connect time.
187
+ *
188
+ * When `true` (default `false`), a request to a hostname that resolves to a
189
+ * validated address is dispatched through Node's built-in `node:http` with
190
+ * the connection pinned to that address and the original `Host` header
191
+ * preserved — so virtual-host routing still works while an attacker's
192
+ * TTL=0 rebinding to `127.0.0.1` / `169.254.169.254` can no longer take
193
+ * effect between validation and connect.
194
+ *
195
+ * **Scope and caveats** (read before enabling):
196
+ *
197
+ * - **`http:` only.** `https:` is intentionally NOT pinned here: pinning a
198
+ * TLS connection to an IP while keeping hostname-based SNI / certificate
199
+ * validation needs more machinery than this knob provides, so `https:`
200
+ * requests fall through to the normal validated-then-fetch path and retain
201
+ * the documented TOCTOU caveat. The prime rebinding target — cloud
202
+ * metadata at `http://169.254.169.254` — is `http:`, so this still closes
203
+ * the highest-value vector.
204
+ * - **Node only.** It uses `node:http`; on runtimes without it (Workers,
205
+ * some edge sandboxes) an `http:` pinned dispatch throws a clear error so
206
+ * the misconfiguration is loud rather than a silent no-op.
207
+ * - **Bypasses `options.fetch`** for the pinned `http:` path (it must own the
208
+ * socket), and negotiates no response compression (`Accept-Encoding:
209
+ * identity`) so body semantics match a plain `fetch`.
210
+ *
211
+ * Requests to a literal-IP host or an `allowHosts` entry are never pinned
212
+ * (the former already connects to an exact IP; the latter is an explicit
213
+ * operator trust). Default `false`.
214
+ *
215
+ * @since 0.44.0
216
+ */
217
+ pinDns?: boolean;
180
218
  }
181
219
  /**
182
220
  * Wrap `fetch` with an SSRF-hardened guard. The returned function has
@@ -52,18 +52,21 @@
52
52
  * its **own** DNS lookup when it opens the socket. An attacker that
53
53
  * controls a TTL=0 record can return a public IP at validation time and
54
54
  * a `127.0.0.1` / `169.254.169.254` at connect time, slipping past the
55
- * library-level check. This residual TOCTOU window cannot be closed at
56
- * the `fetch()` boundary without injecting a custom HTTP dispatcher
57
- * (e.g. `undici.Agent({ connect: { lookup } })`) — and Daloy
58
- * deliberately ships **zero** runtime dependencies, so we do not bundle
59
- * `undici`. To fully close the window:
55
+ * library-level check. To close the window:
60
56
  *
57
+ * 0. **Built-in, `http:` only** (recommended for cloud-metadata defense):
58
+ * set {@link FetchGuardOptions.pinDns} `: true`. On Node, `http:`
59
+ * requests are then dispatched through `node:http` with the socket
60
+ * pinned to the validated IP (and the original `Host` header
61
+ * preserved), so there is no connect-time re-resolution to rebind.
62
+ * `https:` is not pinned by this knob — see its docs — so the items
63
+ * below still matter for TLS upstreams.
61
64
  * 1. **Operator-side** (recommended): block egress to RFC1918 /
62
65
  * loopback / link-local at the VPC / firewall layer. This neutralises
63
66
  * the rebinding even if the application is naïve.
64
- * 2. **Caller-side, Node only**: install `undici` and pass a custom
65
- * `fetch` that wires a dispatcher with a pinned-IP `connect.lookup`,
66
- * e.g.
67
+ * 2. **Caller-side, Node only** (covers `https:` too): install `undici`
68
+ * and pass a custom `fetch` that wires a dispatcher with a pinned-IP
69
+ * `connect.lookup`, e.g.
67
70
  * ```ts
68
71
  * import { Agent, fetch as undiciFetch } from "undici";
69
72
  * import * as dns from "node:dns/promises";
@@ -175,6 +178,7 @@ export function fetchGuard(options = {}) {
175
178
  throw new Error("fetchGuard(): no global fetch available; pass options.fetch.");
176
179
  }
177
180
  const resolveFn = options.resolve ?? createDefaultResolver();
181
+ const pinDns = options.pinDns === true;
178
182
  for (const c of ALWAYS_DENY)
179
183
  hardDenyMatchers.push(compileCidrMatcher(c));
180
184
  for (const c of options.denyAddresses ?? [])
@@ -206,6 +210,17 @@ export function fetchGuard(options = {}) {
206
210
  return true;
207
211
  return !softDenyMatchers.some((m) => matchesMatcher(parsed, m));
208
212
  }
213
+ /**
214
+ * Validate a URL's destination against the deny/allow policy.
215
+ *
216
+ * @returns the single validated IP the connection should be **pinned** to
217
+ * (the first resolved address), or `null` when pinning does not apply —
218
+ * the host is an `allowHosts` entry (explicit operator trust) or already a
219
+ * literal IP (the socket connects to that exact address, so there is no
220
+ * re-resolution window to close).
221
+ * @throws {SsrfBlockedError} when the protocol, host, or any resolved
222
+ * address is not permitted.
223
+ */
209
224
  async function validateUrl(url) {
210
225
  const proto = url.protocol.toLowerCase();
211
226
  if (!allowProtocols.has(proto)) {
@@ -217,13 +232,13 @@ export function fetchGuard(options = {}) {
217
232
  throw new SsrfBlockedError(url.toString(), "invalid-url");
218
233
  }
219
234
  if (allowHosts.has(hostname.toLowerCase()))
220
- return;
235
+ return null;
221
236
  const literal = parseIp(hostname);
222
237
  if (literal) {
223
238
  if (!isAddressAllowed(literal)) {
224
239
  throw new SsrfBlockedError(url.toString(), "address-not-allowed", hostname);
225
240
  }
226
- return;
241
+ return null;
227
242
  }
228
243
  let addrs;
229
244
  try {
@@ -244,6 +259,7 @@ export function fetchGuard(options = {}) {
244
259
  throw new SsrfBlockedError(url.toString(), "address-not-allowed", a);
245
260
  }
246
261
  }
262
+ return addrs[0];
247
263
  }
248
264
  const guarded = async (input, init) => {
249
265
  let request = new Request(input, init);
@@ -258,10 +274,17 @@ export function fetchGuard(options = {}) {
258
274
  throw new SsrfBlockedError(String(input), "invalid-url");
259
275
  }
260
276
  for (let hop = 0;; hop++) {
261
- await validateUrl(currentUrl);
277
+ const pinnedIp = await validateUrl(currentUrl);
262
278
  const dispatchInit = { redirect: "manual" };
263
279
  const dispatchReq = new Request(request, dispatchInit);
264
- const res = await baseFetch(dispatchReq);
280
+ // When DNS pinning is enabled, dispatch `http:` requests through a
281
+ // socket bound to the exact validated IP so the underlying client cannot
282
+ // re-resolve the hostname to an internal address between validation and
283
+ // connect (DNS-rebinding TOCTOU). `https:` keeps the normal path — see
284
+ // the `pinDns` option docs for why.
285
+ const res = pinDns && pinnedIp !== null && currentUrl.protocol === "http:"
286
+ ? await pinnedHttpFetch(dispatchReq, pinnedIp)
287
+ : await baseFetch(dispatchReq);
265
288
  if (!isRedirect(res.status))
266
289
  return res;
267
290
  if (userRedirect === "error") {
@@ -307,6 +330,85 @@ export function fetchGuard(options = {}) {
307
330
  function isRedirect(status) {
308
331
  return status === 301 || status === 302 || status === 303 || status === 307 || status === 308;
309
332
  }
333
+ /**
334
+ * Dispatch an `http:` request with the TCP connection **pinned** to a single,
335
+ * already-validated IP address — closing the DNS-rebinding (TOCTOU) window that
336
+ * exists when the underlying `fetch` re-resolves the hostname at connect time.
337
+ *
338
+ * Uses Node's built-in `node:http` (no runtime dependency) so the `Host` header
339
+ * can be set to the original authority — preserving virtual-host routing while
340
+ * the socket targets `ip`, never a re-resolved address. The response body is
341
+ * streamed (not buffered) so large downloads keep the same memory profile as a
342
+ * plain `fetch`. `Accept-Encoding: identity` is forced so the returned body is
343
+ * not silently left compressed (Node's `http` does not auto-decode).
344
+ *
345
+ * @param req - The request to dispatch (its URL supplies the path + `Host`).
346
+ * @param ip - The validated address to connect to.
347
+ * @returns A web {@link Response} mirroring the upstream reply.
348
+ * @throws when `node:http` is unavailable (non-Node runtime) or the socket errors.
349
+ */
350
+ async function pinnedHttpFetch(req, ip) {
351
+ let httpRequest;
352
+ let toWeb;
353
+ try {
354
+ ({ request: httpRequest } = await import("node:http"));
355
+ ({ toWeb } = (await import("node:stream")).Readable);
356
+ }
357
+ catch {
358
+ throw new Error("fetchGuard({ pinDns: true }): node:http is unavailable on this runtime; " +
359
+ "DNS pinning for http: requires Node. Disable pinDns or run on Node.");
360
+ }
361
+ const url = new URL(req.url);
362
+ const headers = {};
363
+ req.headers.forEach((value, key) => {
364
+ headers[key] = value;
365
+ });
366
+ // Preserve the original authority so virtual-host routing still works even
367
+ // though the socket connects to a raw IP.
368
+ headers["host"] = url.host;
369
+ // Do not negotiate compression: node:http won't auto-decode it, and a
370
+ // silently-compressed body would break `res.text()` / `res.json()` callers.
371
+ headers["accept-encoding"] = "identity";
372
+ const method = req.method.toUpperCase();
373
+ const bodyBytes = method === "GET" || method === "HEAD" ? undefined : new Uint8Array(await req.arrayBuffer());
374
+ return await new Promise((resolve, reject) => {
375
+ const clientReq = httpRequest({
376
+ host: ip,
377
+ port: url.port ? Number(url.port) : 80,
378
+ method,
379
+ path: `${url.pathname}${url.search}`,
380
+ headers,
381
+ }, (res) => {
382
+ const responseHeaders = new Headers();
383
+ for (const [key, value] of Object.entries(res.headers)) {
384
+ if (Array.isArray(value)) {
385
+ for (const v of value)
386
+ responseHeaders.append(key, v);
387
+ }
388
+ else if (typeof value === "string") {
389
+ responseHeaders.set(key, value);
390
+ }
391
+ }
392
+ const status = res.statusCode ?? 502;
393
+ // 204/304 and HEAD responses must carry a null body per the spec.
394
+ const nullBody = status === 204 || status === 304 || method === "HEAD";
395
+ if (nullBody) {
396
+ res.resume(); // drain so the socket can be released
397
+ resolve(new Response(null, { status, statusText: res.statusMessage, headers: responseHeaders }));
398
+ return;
399
+ }
400
+ resolve(new Response(toWeb(res), {
401
+ status,
402
+ statusText: res.statusMessage,
403
+ headers: responseHeaders,
404
+ }));
405
+ });
406
+ clientReq.on("error", reject);
407
+ if (bodyBytes && bodyBytes.byteLength > 0)
408
+ clientReq.write(bodyBytes);
409
+ clientReq.end();
410
+ });
411
+ }
310
412
  function stripBodyHeaders(headers) {
311
413
  const out = new Headers(headers);
312
414
  out.delete("content-length");
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "bomFormat": "CycloneDX",
3
3
  "specVersion": "1.5",
4
- "serialNumber": "urn:uuid:66a4adf1-88f7-56e9-a00f-95633f473b23",
4
+ "serialNumber": "urn:uuid:24c97016-8019-52fa-bf46-65a4325559ec",
5
5
  "version": 1,
6
6
  "metadata": {
7
- "timestamp": "2026-06-20T12:10:13.811Z",
7
+ "timestamp": "2026-06-21T08:21:23.570Z",
8
8
  "tools": [
9
9
  {
10
10
  "vendor": "DaloyJS",
11
11
  "name": "daloy-generate-sbom",
12
- "version": "0.43.0"
12
+ "version": "1.0.0-beta.0"
13
13
  }
14
14
  ],
15
15
  "authors": [
@@ -19,11 +19,11 @@
19
19
  ],
20
20
  "component": {
21
21
  "type": "library",
22
- "bom-ref": "pkg:npm/@daloyjs/core@0.43.0",
22
+ "bom-ref": "pkg:npm/@daloyjs/core@1.0.0-beta.0",
23
23
  "name": "@daloyjs/core",
24
- "version": "0.43.0",
24
+ "version": "1.0.0-beta.0",
25
25
  "description": "DaloyJS is a runtime-portable, contract-first TypeScript web framework with built-in OpenAPI (Hey API), typed client generation, large-scale maintainability, and security-first defaults. Hono-grade portability, Elysia-grade DX, FastAPI-grade docs, Fastify-grade ops — distributed via pnpm.",
26
- "purl": "pkg:npm/@daloyjs/core@0.43.0",
26
+ "purl": "pkg:npm/@daloyjs/core@1.0.0-beta.0",
27
27
  "licenses": [
28
28
  {
29
29
  "license": {
@@ -46,9 +46,9 @@
46
46
  }
47
47
  ],
48
48
  "swid": {
49
- "tagId": "swidtag--daloyjs-core-0.43.0",
49
+ "tagId": "swidtag--daloyjs-core-1.0.0-beta.0",
50
50
  "name": "@daloyjs/core",
51
- "version": "0.43.0",
51
+ "version": "1.0.0-beta.0",
52
52
  "tagVersion": 0,
53
53
  "patch": false
54
54
  }
@@ -57,7 +57,7 @@
57
57
  "components": [],
58
58
  "dependencies": [
59
59
  {
60
- "ref": "pkg:npm/@daloyjs/core@0.43.0",
60
+ "ref": "pkg:npm/@daloyjs/core@1.0.0-beta.0",
61
61
  "dependsOn": []
62
62
  }
63
63
  ]
@@ -2,10 +2,10 @@
2
2
  "spdxVersion": "SPDX-2.3",
3
3
  "dataLicense": "CC0-1.0",
4
4
  "SPDXID": "SPDXRef-DOCUMENT",
5
- "name": "@daloyjs/core-0.43.0",
6
- "documentNamespace": "https://github.com/daloyjs/daloy/sbom/@daloyjs/core-0.43.0-66a4adf1-88f7-56e9-a00f-95633f473b23",
5
+ "name": "@daloyjs/core-1.0.0-beta.0",
6
+ "documentNamespace": "https://github.com/daloyjs/daloy/sbom/@daloyjs/core-1.0.0-beta.0-24c97016-8019-52fa-bf46-65a4325559ec",
7
7
  "creationInfo": {
8
- "created": "2026-06-20T12:10:13.811Z",
8
+ "created": "2026-06-21T08:21:23.570Z",
9
9
  "creators": [
10
10
  "Tool: daloy-generate-sbom",
11
11
  "Organization: DaloyJS"
@@ -16,7 +16,7 @@
16
16
  {
17
17
  "SPDXID": "SPDXRef-Package--daloyjs-core",
18
18
  "name": "@daloyjs/core",
19
- "versionInfo": "0.43.0",
19
+ "versionInfo": "1.0.0-beta.0",
20
20
  "downloadLocation": "https://github.com/daloyjs/daloy",
21
21
  "filesAnalyzed": false,
22
22
  "licenseConcluded": "MIT",
@@ -27,7 +27,7 @@
27
27
  {
28
28
  "referenceCategory": "PACKAGE-MANAGER",
29
29
  "referenceType": "purl",
30
- "referenceLocator": "pkg:npm/@daloyjs/core@0.43.0"
30
+ "referenceLocator": "pkg:npm/@daloyjs/core@1.0.0-beta.0"
31
31
  }
32
32
  ]
33
33
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@daloyjs/core",
3
- "version": "0.43.0",
3
+ "version": "1.0.0-beta.0",
4
4
  "description": "DaloyJS is a runtime-portable, contract-first TypeScript web framework with built-in OpenAPI (Hey API), typed client generation, large-scale maintainability, and security-first defaults. Hono-grade portability, Elysia-grade DX, FastAPI-grade docs, Fastify-grade ops — distributed via pnpm.",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -241,7 +241,8 @@
241
241
  "example": "node --import tsx examples/basic.ts",
242
242
  "bench": "node --import tsx bench/router.bench.ts",
243
243
  "test": "node --import tsx --test tests/**/*.test.ts",
244
- "test:red-team": "node --import tsx --test tests/red-team-attacks.test.ts tests/red-team-attacks-2.test.ts tests/red-team-attacks-3.test.ts tests/red-team-attacks-4.test.ts tests/red-team-attacks-5.test.ts tests/red-team-attacks-6.test.ts tests/red-team-attacks-7.test.ts",
244
+ "test:red-team": "node --import tsx --test tests/red-team-attacks.test.ts tests/red-team-attacks-2.test.ts tests/red-team-attacks-3.test.ts tests/red-team-attacks-4.test.ts tests/red-team-attacks-5.test.ts tests/red-team-attacks-6.test.ts tests/red-team-attacks-7.test.ts tests/red-team-attacks-8.test.ts tests/red-team-attacks-9.test.ts tests/red-team-attacks-10.test.ts",
245
+ "red-team:live": "node --import tsx red-team-live/run.ts",
245
246
  "coverage": "node --import tsx --test --experimental-test-coverage --test-coverage-include='src/**' --test-coverage-lines=90 --test-coverage-functions=90 tests/**/*.test.ts",
246
247
  "coverage:branches": "tsc -p tsconfig.coverage.json && node --test --experimental-test-coverage --test-coverage-include='dist-coverage/src/**' --test-coverage-branches=92 dist-coverage/tests/**/*.test.js",
247
248
  "typecheck": "tsc --noEmit && tsc -p tsconfig.typetest.json && tsc -p tests/tsconfig.json --noEmit",