@daloyjs/core 0.35.1 → 0.36.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.
@@ -38,7 +38,7 @@
38
38
  * });
39
39
  * ```
40
40
  *
41
- * @since 0.35.1
41
+ * @since 0.36.0
42
42
  */
43
43
  /** Reason an open-redirect candidate was refused. */
44
44
  export type SafeRedirectBlockReason = "empty-target" | "invalid-control-characters" | "protocol-relative" | "backslash-path" | "path-not-allowed" | "origin-not-allowed" | "scheme-not-allowed" | "parse-failed";
@@ -86,6 +86,6 @@ export interface SafeRedirectOptions {
86
86
  * @param target - User-supplied URL candidate (path or absolute URL).
87
87
  * @param options - Allowlist + response configuration.
88
88
  *
89
- * @since 0.35.1
89
+ * @since 0.36.0
90
90
  */
91
91
  export declare function safeRedirect(target: string, options?: SafeRedirectOptions): Response;
@@ -38,7 +38,7 @@
38
38
  * });
39
39
  * ```
40
40
  *
41
- * @since 0.35.1
41
+ * @since 0.36.0
42
42
  */
43
43
  /** Thrown when {@link safeRedirect} refuses a candidate URL and no `fallback` is configured. */
44
44
  export class OpenRedirectBlockedError extends Error {
@@ -126,7 +126,7 @@ function classify(target, allowedPaths, allowedOrigins) {
126
126
  * @param target - User-supplied URL candidate (path or absolute URL).
127
127
  * @param options - Allowlist + response configuration.
128
128
  *
129
- * @since 0.35.1
129
+ * @since 0.36.0
130
130
  */
131
131
  export function safeRedirect(target, options = {}) {
132
132
  const allowedPaths = options.allowedPaths ?? [];
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "bomFormat": "CycloneDX",
3
3
  "specVersion": "1.5",
4
- "serialNumber": "urn:uuid:3d46c2b6-0b38-564b-9dd2-79098e99155b",
4
+ "serialNumber": "urn:uuid:7079f846-98da-5195-89ad-4f54c8938289",
5
5
  "version": 1,
6
6
  "metadata": {
7
- "timestamp": "2026-05-26T23:20:20.188Z",
7
+ "timestamp": "2026-05-28T20:50:52.884Z",
8
8
  "tools": [
9
9
  {
10
10
  "vendor": "DaloyJS",
11
11
  "name": "daloy-generate-sbom",
12
- "version": "0.35.1"
12
+ "version": "0.36.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.35.1",
22
+ "bom-ref": "pkg:npm/@daloyjs/core@0.36.0",
23
23
  "name": "@daloyjs/core",
24
- "version": "0.35.1",
24
+ "version": "0.36.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.35.1",
26
+ "purl": "pkg:npm/@daloyjs/core@0.36.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.35.1",
49
+ "tagId": "swidtag--daloyjs-core-0.36.0",
50
50
  "name": "@daloyjs/core",
51
- "version": "0.35.1",
51
+ "version": "0.36.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.35.1",
60
+ "ref": "pkg:npm/@daloyjs/core@0.36.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.35.1",
6
- "documentNamespace": "https://github.com/daloyjs/daloy/sbom/@daloyjs/core-0.35.1-3d46c2b6-0b38-564b-9dd2-79098e99155b",
5
+ "name": "@daloyjs/core-0.36.0",
6
+ "documentNamespace": "https://github.com/daloyjs/daloy/sbom/@daloyjs/core-0.36.0-7079f846-98da-5195-89ad-4f54c8938289",
7
7
  "creationInfo": {
8
- "created": "2026-05-26T23:20:20.188Z",
8
+ "created": "2026-05-28T20:50:52.884Z",
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.35.1",
19
+ "versionInfo": "0.36.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.35.1"
30
+ "referenceLocator": "pkg:npm/@daloyjs/core@0.36.0"
31
31
  }
32
32
  ]
33
33
  }
@@ -85,21 +85,6 @@ export declare function sanitizeHeaderValue(value: string): string;
85
85
  * @since 0.1.0
86
86
  */
87
87
  export declare function timingSafeEqual(a: string, b: string): boolean;
88
- /**
89
- * Generate a cryptographically strong, URL-safe identifier (~22 chars).
90
- *
91
- * Uses Web Crypto's `crypto.randomUUID()` when available, falling back to
92
- * 16 random bytes via `crypto.getRandomValues()`. The last-resort fallback
93
- * (timestamp + `Math.random()`) only triggers in environments without
94
- * WebCrypto, which is none of Node 20+/Bun/Deno/Cloudflare Workers/Vercel
95
- * Edge.
96
- *
97
- * Suitable for request ids, session ids, and short-lived correlation tokens.
98
- * Do not use for long-lived secrets unless you also sign or wrap them.
99
- *
100
- * @returns A random URL-safe id string.
101
- * @since 0.1.0
102
- */
103
88
  export declare function randomId(): string;
104
89
  /**
105
90
  * Header names that MUST appear at most once on a request per RFC 7230.
@@ -338,7 +323,7 @@ export declare function signWebhookPayload(opts: {
338
323
  * @returns A sanitized basename safe to combine with a trusted directory.
339
324
  * @throws {BadRequestError} When the input reduces to an empty string or
340
325
  * matches a Windows-reserved device name.
341
- * @since 0.35.1
326
+ * @since 0.35.0
342
327
  */
343
328
  export declare function sanitizeFilename(name: string): string;
344
329
  /**
@@ -365,7 +350,7 @@ export declare function sanitizeFilename(name: string): string;
365
350
  * @param input - The candidate relative path.
366
351
  * @returns The input unchanged when safe.
367
352
  * @throws {BadRequestError} When the path could escape its base directory.
368
- * @since 0.35.1
353
+ * @since 0.35.0
369
354
  */
370
355
  export declare function assertSafeRelativePath(input: string): string;
371
356
  /**
@@ -376,7 +361,7 @@ export declare function assertSafeRelativePath(input: string): string;
376
361
  * Returns `true` on the first hit. Use before passing untrusted data
377
362
  * into a query object that may be interpreted as an operator expression.
378
363
  *
379
- * @since 0.35.1
364
+ * @since 0.35.0
380
365
  */
381
366
  export declare function hasMongoOperatorKeys(value: unknown): boolean;
382
367
  /**
@@ -385,6 +370,6 @@ export declare function hasMongoOperatorKeys(value: unknown): boolean;
385
370
  * threading it into a NoSQL driver — closes the
386
371
  * `{"password": {"$ne": null}}` authentication-bypass class of bug.
387
372
  *
388
- * @since 0.35.1
373
+ * @since 0.35.0
389
374
  */
390
375
  export declare function assertNoMongoOperators(value: unknown): void;
package/dist/security.js CHANGED
@@ -9,6 +9,12 @@
9
9
  * - randomId: cryptographically strong request id.
10
10
  */
11
11
  import { PayloadTooLargeError, BadRequestError, } from "./errors.js";
12
+ // Resolved once at module load. Mirror of `DALOY_REQUEST_RAW_BODY` in
13
+ // app.ts; defined here via the global Symbol registry to avoid an import
14
+ // cycle (app.ts -> security.ts). Adapters attach a pre-validated
15
+ // Uint8Array under this key so readBodyLimited can skip the WHATWG stream
16
+ // reader loop.
17
+ const REQUEST_RAW_BODY = Symbol.for("daloyjs.request.rawBody");
12
18
  /**
13
19
  * Read a `Request` body to a `Uint8Array` while enforcing a hard byte cap.
14
20
  *
@@ -36,6 +42,15 @@ export async function readBodyLimited(req, limit) {
36
42
  if (n > limit)
37
43
  throw new PayloadTooLargeError(limit);
38
44
  }
45
+ // Fast path: adapter pre-buffered the body and stashed it via the
46
+ // REQUEST_RAW_BODY symbol. Re-check the limit (defense-in-depth) and
47
+ // return zero-copy. Skips the WHATWG ReadableStream reader loop entirely.
48
+ const cached = req[REQUEST_RAW_BODY];
49
+ if (cached instanceof Uint8Array) {
50
+ if (cached.byteLength > limit)
51
+ throw new PayloadTooLargeError(limit);
52
+ return cached;
53
+ }
39
54
  if (!req.body)
40
55
  return new Uint8Array(0);
41
56
  const reader = req.body.getReader();
@@ -177,8 +192,20 @@ export function timingSafeEqual(a, b) {
177
192
  * @returns A random URL-safe id string.
178
193
  * @since 0.1.0
179
194
  */
195
+ // Cache the Web Crypto entry points at module load so randomId() doesn't pay
196
+ // for a `globalThis.crypto` + optional-chain property lookup per request.
197
+ // Falls back to the runtime lookup path if Web Crypto is patched/replaced
198
+ // after module load (test harnesses, custom runtimes) — the cache is only
199
+ // trusted when `globalThis.crypto` is still the same reference, otherwise
200
+ // the stubbed object would be silently bypassed.
201
+ const _webCrypto = globalThis.crypto;
202
+ const _randomUUID = _webCrypto && typeof _webCrypto.randomUUID === "function"
203
+ ? _webCrypto.randomUUID.bind(_webCrypto)
204
+ : undefined;
180
205
  export function randomId() {
181
206
  const c = globalThis.crypto;
207
+ if (c === _webCrypto && _randomUUID !== undefined)
208
+ return _randomUUID();
182
209
  if (c?.randomUUID)
183
210
  return c.randomUUID();
184
211
  if (c?.getRandomValues) {
@@ -263,11 +290,13 @@ export const RESERVED_INBOUND_HEADER_PREFIXES = Object.freeze([
263
290
  * @since 0.36.0
264
291
  */
265
292
  export function assertNoReservedInternalHeaders(headers) {
293
+ // WHATWG Headers.forEach() yields lowercased names, so the per-header
294
+ // .toLowerCase() call this function used to do was dead work on every
295
+ // request. Skip it.
266
296
  headers.forEach((_value, name) => {
267
- const lower = name.toLowerCase();
268
297
  for (const prefix of RESERVED_INBOUND_HEADER_PREFIXES) {
269
- if (lower.startsWith(prefix)) {
270
- throw new BadRequestError(`Reserved internal header rejected: ${lower}`);
298
+ if (name.startsWith(prefix)) {
299
+ throw new BadRequestError(`Reserved internal header rejected: ${name}`);
271
300
  }
272
301
  }
273
302
  });
@@ -631,7 +660,7 @@ const WINDOWS_RESERVED_NAMES = new Set([
631
660
  * @returns A sanitized basename safe to combine with a trusted directory.
632
661
  * @throws {BadRequestError} When the input reduces to an empty string or
633
662
  * matches a Windows-reserved device name.
634
- * @since 0.35.1
663
+ * @since 0.35.0
635
664
  */
636
665
  export function sanitizeFilename(name) {
637
666
  if (typeof name !== "string") {
@@ -683,7 +712,7 @@ export function sanitizeFilename(name) {
683
712
  * @param input - The candidate relative path.
684
713
  * @returns The input unchanged when safe.
685
714
  * @throws {BadRequestError} When the path could escape its base directory.
686
- * @since 0.35.1
715
+ * @since 0.35.0
687
716
  */
688
717
  export function assertSafeRelativePath(input) {
689
718
  if (typeof input !== "string" || input.length === 0) {
@@ -744,7 +773,7 @@ function walkForMongoOperators(value) {
744
773
  * Returns `true` on the first hit. Use before passing untrusted data
745
774
  * into a query object that may be interpreted as an operator expression.
746
775
  *
747
- * @since 0.35.1
776
+ * @since 0.35.0
748
777
  */
749
778
  export function hasMongoOperatorKeys(value) {
750
779
  return walkForMongoOperators(value);
@@ -755,7 +784,7 @@ export function hasMongoOperatorKeys(value) {
755
784
  * threading it into a NoSQL driver — closes the
756
785
  * `{"password": {"$ne": null}}` authentication-bypass class of bug.
757
786
  *
758
- * @since 0.35.1
787
+ * @since 0.35.0
759
788
  */
760
789
  export function assertNoMongoOperators(value) {
761
790
  if (walkForMongoOperators(value)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@daloyjs/core",
3
- "version": "0.35.1",
3
+ "version": "0.36.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": {