@daloyjs/core 0.42.0 → 0.44.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
@@ -516,7 +516,7 @@ DaloyJS is in **public preview** (`0.x`). The public API may still change betwee
516
516
 
517
517
  - Adapters for Node (Heroku, Railway, Render, Fly.io), Bun, Deno, Cloudflare Workers, Vercel Node / Edge / Next.js / Netlify Edge, Fastly Compute, and AWS Lambda / Netlify Functions / Lambda Function URLs.
518
518
  - `daloy dev` watch loop delegates to the host runtime's native watcher (`node --import tsx --watch`, `bun --hot`, or `deno run --watch`) with a `--runtime` override for cross-runtime `package.json` scripts.
519
- - `pnpm create daloy` scaffolder with Node, Bun, Deno, Cloudflare Worker, and Vercel Edge templates, plus optional `--with-ci` GitHub Actions / Dependabot / CODEOWNERS / SECURITY.md hardening.
519
+ - `pnpm create daloy` scaffolder with Node, Bun, Deno, Cloudflare Worker, and Vercel Edge templates, plus optional `--with-ci` GitHub Actions / Dependabot / CODEOWNERS / SECURITY.md hardening. The completion summary surfaces official install links (nodejs.org, pnpm.io, bun.sh) for any runtime or package manager your selections need but that is missing from `PATH`, and skips a doomed dependency install when the chosen package manager is absent.
520
520
  - Container-first templates: `HEALTHCHECK` to `/readyz`, `STOPSIGNAL SIGTERM`, non-root user, `tini` as PID 1.
521
521
  - Generated `deploy.yml` for container templates signs every pushed GHCR image with **Sigstore Cosign** (keyless OIDC) and attaches an **SPDX SBOM attestation** so consumers can `cosign verify` and `cosign verify-attestation --type spdxjson` instead of trusting the registry alone.
522
522
  - Pretty `printStartupBanner()` / `formatStartupBanner()` helpers at `@daloyjs/core/banner`, used by every starter template (TTY + `NO_COLOR` / `FORCE_COLOR` aware, ASCII fallback for dumb terminals).
@@ -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.
package/dist/banner.js CHANGED
@@ -36,24 +36,44 @@ const GLYPHS_ASCII = {
36
36
  mid: "-",
37
37
  };
38
38
  const ANSI_REGEX = /\u001b\[[0-9;]*m/g;
39
+ /**
40
+ * Read a single environment variable defensively. On runtimes with a
41
+ * capability-based permission model (Deno), reading an env var the process was
42
+ * not granted via `--allow-env` throws a `NotCapable` error. The startup
43
+ * banner is purely cosmetic, so a missing color/locale hint must never crash
44
+ * the host application — swallow the denial and treat the variable as unset.
45
+ * On Node and Bun `process.env` access never throws, so this is a no-op there.
46
+ *
47
+ * @param key - Environment variable name.
48
+ * @returns The value, or `undefined` when unset or inaccessible.
49
+ */
50
+ function readEnv(key) {
51
+ try {
52
+ return process.env[key];
53
+ }
54
+ catch {
55
+ return undefined;
56
+ }
57
+ }
39
58
  function detectColor() {
40
- if (process.env.NO_COLOR)
59
+ if (readEnv("NO_COLOR"))
41
60
  return false;
42
- if (process.env.FORCE_COLOR && process.env.FORCE_COLOR !== "0")
61
+ const force = readEnv("FORCE_COLOR");
62
+ if (force && force !== "0")
43
63
  return true;
44
64
  const stdout = process.stdout;
45
65
  return Boolean(stdout && stdout.isTTY);
46
66
  }
47
67
  function detectAscii() {
48
- if (process.env.DALOY_ASCII)
68
+ if (readEnv("DALOY_ASCII"))
49
69
  return true;
50
70
  if (process.platform === "win32") {
51
- return !(process.env.WT_SESSION || process.env.TERM_PROGRAM);
71
+ return !(readEnv("WT_SESSION") || readEnv("TERM_PROGRAM"));
52
72
  }
53
- const lang = process.env.LANG ?? process.env.LC_ALL ?? "";
73
+ const lang = readEnv("LANG") ?? readEnv("LC_ALL") ?? "";
54
74
  if (/UTF-?8/i.test(lang))
55
75
  return false;
56
- if (process.env.TERM_PROGRAM)
76
+ if (readEnv("TERM_PROGRAM"))
57
77
  return false;
58
78
  return true;
59
79
  }
package/dist/cli.js CHANGED
@@ -547,9 +547,14 @@ async function runDiff(opts, io) {
547
547
  }
548
548
  /**
549
549
  * `daloy doctor` — boot-time + CLI audit. Loads the user's
550
- * App entry and runs the secure-by-default checklist. Exits non-zero on any
551
- * finding so the command can guard container `HEALTHCHECK` and CI deploy
552
- * steps.
550
+ * App entry and runs the secure-by-default checklist so the command can guard
551
+ * container `HEALTHCHECK` and CI deploy steps.
552
+ *
553
+ * Exit code: non-zero (`1`) only when at least one **`error`-level** finding is
554
+ * present; `warn`-level findings are advisory and leave the exit code at `0`.
555
+ * The `--json` output reports `ok: true` only when there are **no findings at
556
+ * all** (any level), so `ok` is a stricter signal than the exit code — a
557
+ * warn-only run prints `ok: false` but still exits `0`.
553
558
  *
554
559
  * @internal
555
560
  */
@@ -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:c3e0bc4b-403c-5789-b2e4-dbafa73b03ee",
4
+ "serialNumber": "urn:uuid:b8d26872-a6bb-52d0-b454-592bdf7f82cc",
5
5
  "version": 1,
6
6
  "metadata": {
7
- "timestamp": "2026-06-19T08:56:49.249Z",
7
+ "timestamp": "2026-06-20T23:19:34.215Z",
8
8
  "tools": [
9
9
  {
10
10
  "vendor": "DaloyJS",
11
11
  "name": "daloy-generate-sbom",
12
- "version": "0.42.0"
12
+ "version": "0.44.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.42.0",
22
+ "bom-ref": "pkg:npm/@daloyjs/core@0.44.0",
23
23
  "name": "@daloyjs/core",
24
- "version": "0.42.0",
24
+ "version": "0.44.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.42.0",
26
+ "purl": "pkg:npm/@daloyjs/core@0.44.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.42.0",
49
+ "tagId": "swidtag--daloyjs-core-0.44.0",
50
50
  "name": "@daloyjs/core",
51
- "version": "0.42.0",
51
+ "version": "0.44.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.42.0",
60
+ "ref": "pkg:npm/@daloyjs/core@0.44.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.42.0",
6
- "documentNamespace": "https://github.com/daloyjs/daloy/sbom/@daloyjs/core-0.42.0-c3e0bc4b-403c-5789-b2e4-dbafa73b03ee",
5
+ "name": "@daloyjs/core-0.44.0",
6
+ "documentNamespace": "https://github.com/daloyjs/daloy/sbom/@daloyjs/core-0.44.0-b8d26872-a6bb-52d0-b454-592bdf7f82cc",
7
7
  "creationInfo": {
8
- "created": "2026-06-19T08:56:49.249Z",
8
+ "created": "2026-06-20T23:19:34.215Z",
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.42.0",
19
+ "versionInfo": "0.44.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.42.0"
30
+ "referenceLocator": "pkg:npm/@daloyjs/core@0.44.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.42.0",
3
+ "version": "0.44.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": {
@@ -45,10 +45,6 @@
45
45
  "types": "./dist/index.d.ts",
46
46
  "import": "./dist/index.js"
47
47
  },
48
- "./app": {
49
- "types": "./dist/app.d.ts",
50
- "import": "./dist/app.js"
51
- },
52
48
  "./node": {
53
49
  "types": "./dist/adapters/node.d.ts",
54
50
  "import": "./dist/adapters/node.js"
@@ -245,7 +241,8 @@
245
241
  "example": "node --import tsx examples/basic.ts",
246
242
  "bench": "node --import tsx bench/router.bench.ts",
247
243
  "test": "node --import tsx --test tests/**/*.test.ts",
248
- "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",
249
246
  "coverage": "node --import tsx --test --experimental-test-coverage --test-coverage-include='src/**' --test-coverage-lines=90 --test-coverage-functions=90 tests/**/*.test.ts",
250
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",
251
248
  "typecheck": "tsc --noEmit && tsc -p tsconfig.typetest.json && tsc -p tests/tsconfig.json --noEmit",