@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.
- package/README.md +1 -0
- package/dist/adapters/node.d.ts +11 -0
- package/dist/adapters/node.js +90 -28
- package/dist/app.d.ts +113 -0
- package/dist/app.js +372 -96
- package/dist/errors.js +6 -2
- package/dist/index.d.ts +1 -1
- package/dist/safe-redirect.d.ts +2 -2
- package/dist/safe-redirect.js +2 -2
- package/dist/sbom.cdx.json +9 -9
- package/dist/sbom.spdx.json +5 -5
- package/dist/security.d.ts +4 -19
- package/dist/security.js +36 -7
- package/package.json +1 -1
package/dist/safe-redirect.d.ts
CHANGED
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
* });
|
|
39
39
|
* ```
|
|
40
40
|
*
|
|
41
|
-
* @since 0.
|
|
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.
|
|
89
|
+
* @since 0.36.0
|
|
90
90
|
*/
|
|
91
91
|
export declare function safeRedirect(target: string, options?: SafeRedirectOptions): Response;
|
package/dist/safe-redirect.js
CHANGED
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
* });
|
|
39
39
|
* ```
|
|
40
40
|
*
|
|
41
|
-
* @since 0.
|
|
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.
|
|
129
|
+
* @since 0.36.0
|
|
130
130
|
*/
|
|
131
131
|
export function safeRedirect(target, options = {}) {
|
|
132
132
|
const allowedPaths = options.allowedPaths ?? [];
|
package/dist/sbom.cdx.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"bomFormat": "CycloneDX",
|
|
3
3
|
"specVersion": "1.5",
|
|
4
|
-
"serialNumber": "urn:uuid:
|
|
4
|
+
"serialNumber": "urn:uuid:7079f846-98da-5195-89ad-4f54c8938289",
|
|
5
5
|
"version": 1,
|
|
6
6
|
"metadata": {
|
|
7
|
-
"timestamp": "2026-05-
|
|
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.
|
|
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.
|
|
22
|
+
"bom-ref": "pkg:npm/@daloyjs/core@0.36.0",
|
|
23
23
|
"name": "@daloyjs/core",
|
|
24
|
-
"version": "0.
|
|
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.
|
|
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.
|
|
49
|
+
"tagId": "swidtag--daloyjs-core-0.36.0",
|
|
50
50
|
"name": "@daloyjs/core",
|
|
51
|
-
"version": "0.
|
|
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.
|
|
60
|
+
"ref": "pkg:npm/@daloyjs/core@0.36.0",
|
|
61
61
|
"dependsOn": []
|
|
62
62
|
}
|
|
63
63
|
]
|
package/dist/sbom.spdx.json
CHANGED
|
@@ -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.
|
|
6
|
-
"documentNamespace": "https://github.com/daloyjs/daloy/sbom/@daloyjs/core-0.
|
|
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-
|
|
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.
|
|
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.
|
|
30
|
+
"referenceLocator": "pkg:npm/@daloyjs/core@0.36.0"
|
|
31
31
|
}
|
|
32
32
|
]
|
|
33
33
|
}
|
package/dist/security.d.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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 (
|
|
270
|
-
throw new BadRequestError(`Reserved internal header rejected: ${
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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": {
|