@daloyjs/core 1.3.3 → 1.3.4
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 +2 -1
- package/dist/router.d.ts +24 -6
- package/dist/router.js +91 -24
- package/dist/sbom.cdx.json +9 -9
- package/dist/sbom.spdx.json +5 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -491,7 +491,8 @@ miss 7,742,635 ops/sec
|
|
|
491
491
|
|
|
492
492
|
- After traversal checks, exact static routes resolve with an allocation-free
|
|
493
493
|
`Map.get` fast path — **~26M ops/sec**.
|
|
494
|
-
- Dynamic routes walk a trie
|
|
494
|
+
- Dynamic routes walk a segment trie in path-length time without backtracking;
|
|
495
|
+
overlapping routes can require visiting additional branches.
|
|
495
496
|
- Body parsing is lazy and only runs when a route declares a body schema.
|
|
496
497
|
- Path normalization and splitting use index/character scans rather than
|
|
497
498
|
regular expressions.
|
package/dist/router.d.ts
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Performance:
|
|
5
5
|
* - Exact static (parameter-free) paths resolve via a Map lookup — O(1).
|
|
6
|
-
* - Dynamic paths walk a trie,
|
|
6
|
+
* - Dynamic paths walk a segment trie, visiting static branches before
|
|
7
|
+
* parameters and wildcards. Backtracking cost depends on overlapping routes.
|
|
7
8
|
* - Path normalization and splitting avoid regular expressions.
|
|
8
9
|
*
|
|
9
10
|
* Safety:
|
|
@@ -23,12 +24,15 @@ export interface RouteMatch<T> {
|
|
|
23
24
|
/**
|
|
24
25
|
* Trie/radix router with a static-route fast path. Registers handlers via
|
|
25
26
|
* {@link Router.add} and resolves them with {@link Router.find}. Rejects
|
|
26
|
-
* duplicate routes, duplicate operationIds, conflicting
|
|
27
|
-
* path-traversal lookups.
|
|
27
|
+
* duplicate routes, duplicate operationIds, conflicting or unsafe capture names,
|
|
28
|
+
* and raw path-traversal lookups.
|
|
28
29
|
*/
|
|
29
30
|
export declare class Router<T> {
|
|
30
31
|
private root;
|
|
31
32
|
private operationIds;
|
|
33
|
+
private hasDynamicRoutes;
|
|
34
|
+
private revision;
|
|
35
|
+
private staticMethods;
|
|
32
36
|
/** Static (no-param/no-wildcard) routes for O(1) lookup. */
|
|
33
37
|
private staticTable;
|
|
34
38
|
/**
|
|
@@ -38,16 +42,22 @@ export declare class Router<T> {
|
|
|
38
42
|
*
|
|
39
43
|
* @param method - HTTP method to register the handler under.
|
|
40
44
|
* @param path - Route path; supports `:param` and a trailing `*wildcard`.
|
|
41
|
-
* @param handler - Value returned by {@link Router.find} on a match
|
|
45
|
+
* @param handler - Value returned by {@link Router.find} on a match, including
|
|
46
|
+
* falsy values or `undefined`.
|
|
42
47
|
* @param operationId - Optional unique id; tracked to reject duplicates.
|
|
43
48
|
* @throws Error on a duplicate route, duplicate `operationId`, or conflicting
|
|
44
|
-
*
|
|
49
|
+
* parameter or wildcard names at the same trie position, empty, repeated,
|
|
50
|
+
* or prototype-sensitive capture names, or a nonterminal wildcard.
|
|
51
|
+
* Failed registration does not reserve the `operationId`.
|
|
45
52
|
*/
|
|
46
53
|
add(method: HttpMethod, path: string, handler: T, operationId?: string): void;
|
|
47
54
|
/**
|
|
48
55
|
* Look up the handler registered for the given method and path. Tries the
|
|
49
56
|
* static fast path first, then walks the trie, extracting and decoding path
|
|
50
57
|
* params. Path-traversal lookups (`..`, `//`) are rejected up front.
|
|
58
|
+
* Handler tables inherit only from an empty, frozen, prototype-free base:
|
|
59
|
+
* Object.prototype properties never count as routes, including for untyped
|
|
60
|
+
* runtime method values.
|
|
51
61
|
*
|
|
52
62
|
* @param method - HTTP method to match.
|
|
53
63
|
* @param path - Request path to resolve, including any dynamic segments.
|
|
@@ -55,7 +65,15 @@ export declare class Router<T> {
|
|
|
55
65
|
* route matches the method + path.
|
|
56
66
|
*/
|
|
57
67
|
find(method: HttpMethod, path: string): RouteMatch<T> | undefined;
|
|
58
|
-
/**
|
|
68
|
+
/**
|
|
69
|
+
* Return the methods registered at the matched path for 405 responses.
|
|
70
|
+
* Static-only routers skip trie traversal. Results are fresh arrays and
|
|
71
|
+
* reflect routes registered after earlier lookups. Only validated registered
|
|
72
|
+
* static paths are cached, bounding cache size by the static route count.
|
|
73
|
+
* @param path - Request path, subject to the same traversal and empty-segment
|
|
74
|
+
* rejection as {@link Router.find}.
|
|
75
|
+
* @returns Registered methods, or an empty array for rejected or unmatched paths.
|
|
76
|
+
*/
|
|
59
77
|
allowedMethods(path: string): HttpMethod[];
|
|
60
78
|
private walk;
|
|
61
79
|
}
|
package/dist/router.js
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Performance:
|
|
5
5
|
* - Exact static (parameter-free) paths resolve via a Map lookup — O(1).
|
|
6
|
-
* - Dynamic paths walk a trie,
|
|
6
|
+
* - Dynamic paths walk a segment trie, visiting static branches before
|
|
7
|
+
* parameters and wildcards. Backtracking cost depends on overlapping routes.
|
|
7
8
|
* - Path normalization and splitting avoid regular expressions.
|
|
8
9
|
*
|
|
9
10
|
* Safety:
|
|
@@ -12,18 +13,23 @@
|
|
|
12
13
|
* - Duplicate routes and duplicate operationIds throw at registration.
|
|
13
14
|
* - Wildcard segments must be terminal.
|
|
14
15
|
*/
|
|
16
|
+
import { isForbiddenObjectKey } from "./security.js";
|
|
17
|
+
const handlerPrototype = Object.freeze(Object.create(null));
|
|
15
18
|
function createNode() {
|
|
16
|
-
return { children: new Map(), handlers:
|
|
19
|
+
return { children: new Map(), handlers: undefined };
|
|
17
20
|
}
|
|
18
21
|
/**
|
|
19
22
|
* Trie/radix router with a static-route fast path. Registers handlers via
|
|
20
23
|
* {@link Router.add} and resolves them with {@link Router.find}. Rejects
|
|
21
|
-
* duplicate routes, duplicate operationIds, conflicting
|
|
22
|
-
* path-traversal lookups.
|
|
24
|
+
* duplicate routes, duplicate operationIds, conflicting or unsafe capture names,
|
|
25
|
+
* and raw path-traversal lookups.
|
|
23
26
|
*/
|
|
24
27
|
export class Router {
|
|
25
28
|
root = createNode();
|
|
26
29
|
operationIds = new Set();
|
|
30
|
+
hasDynamicRoutes = false;
|
|
31
|
+
revision = 0;
|
|
32
|
+
staticMethods = new Map();
|
|
27
33
|
/** Static (no-param/no-wildcard) routes for O(1) lookup. */
|
|
28
34
|
staticTable = new Map();
|
|
29
35
|
/**
|
|
@@ -33,28 +39,47 @@ export class Router {
|
|
|
33
39
|
*
|
|
34
40
|
* @param method - HTTP method to register the handler under.
|
|
35
41
|
* @param path - Route path; supports `:param` and a trailing `*wildcard`.
|
|
36
|
-
* @param handler - Value returned by {@link Router.find} on a match
|
|
42
|
+
* @param handler - Value returned by {@link Router.find} on a match, including
|
|
43
|
+
* falsy values or `undefined`.
|
|
37
44
|
* @param operationId - Optional unique id; tracked to reject duplicates.
|
|
38
45
|
* @throws Error on a duplicate route, duplicate `operationId`, or conflicting
|
|
39
|
-
*
|
|
46
|
+
* parameter or wildcard names at the same trie position, empty, repeated,
|
|
47
|
+
* or prototype-sensitive capture names, or a nonterminal wildcard.
|
|
48
|
+
* Failed registration does not reserve the `operationId`.
|
|
40
49
|
*/
|
|
41
50
|
add(method, path, handler, operationId) {
|
|
42
51
|
const segments = splitPath(path);
|
|
43
52
|
if (operationId && this.operationIds.has(operationId))
|
|
44
53
|
throw new Error(`Duplicate operationId: "${operationId}"`);
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
54
|
+
let captureNames;
|
|
55
|
+
for (let index = 0; index < segments.length; index++) {
|
|
56
|
+
const segment = segments[index];
|
|
57
|
+
const wildcard = segment.startsWith("*");
|
|
58
|
+
if (wildcard && index !== segments.length - 1) {
|
|
59
|
+
throw new Error(`Wildcard must be the terminal segment: ${path}`);
|
|
60
|
+
}
|
|
61
|
+
if (wildcard || segment.startsWith(":")) {
|
|
62
|
+
const name = wildcard && segment.length === 1 ? "wildcard" : segment.slice(1);
|
|
63
|
+
if (!name || isForbiddenObjectKey(name) || captureNames?.has(name)) {
|
|
64
|
+
throw new Error(`Invalid or duplicate capture name: "${name}" in ${path}`);
|
|
65
|
+
}
|
|
66
|
+
(captureNames ??= new Set()).add(name);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const isStatic = captureNames === undefined;
|
|
48
70
|
const normalized = "/" + segments.join("/");
|
|
49
71
|
if (isStatic) {
|
|
50
72
|
let entry = this.staticTable.get(normalized);
|
|
51
73
|
if (!entry) {
|
|
52
|
-
entry =
|
|
74
|
+
entry = Object.create(handlerPrototype);
|
|
53
75
|
this.staticTable.set(normalized, entry);
|
|
54
76
|
}
|
|
55
|
-
if (entry
|
|
77
|
+
if (Object.hasOwn(entry, method))
|
|
56
78
|
throw new Error(`Duplicate route: ${method} ${path}`);
|
|
57
79
|
entry[method] = handler;
|
|
80
|
+
this.revision++;
|
|
81
|
+
if (operationId)
|
|
82
|
+
this.operationIds.add(operationId);
|
|
58
83
|
return;
|
|
59
84
|
}
|
|
60
85
|
let node = this.root;
|
|
@@ -71,7 +96,12 @@ export class Router {
|
|
|
71
96
|
}
|
|
72
97
|
else if (seg.startsWith("*")) {
|
|
73
98
|
const name = seg.length > 1 ? seg.slice(1) : "wildcard";
|
|
74
|
-
node.wildcardChild
|
|
99
|
+
if (!node.wildcardChild) {
|
|
100
|
+
node.wildcardChild = { name, node: createNode() };
|
|
101
|
+
}
|
|
102
|
+
else if (node.wildcardChild.name !== name) {
|
|
103
|
+
throw new Error(`Conflicting wildcard names at same position: "${node.wildcardChild.name}" vs "${name}"`);
|
|
104
|
+
}
|
|
75
105
|
node = node.wildcardChild.node;
|
|
76
106
|
break;
|
|
77
107
|
}
|
|
@@ -84,14 +114,22 @@ export class Router {
|
|
|
84
114
|
node = next;
|
|
85
115
|
}
|
|
86
116
|
}
|
|
87
|
-
|
|
117
|
+
node.handlers ??= Object.create(handlerPrototype);
|
|
118
|
+
if (Object.hasOwn(node.handlers, method))
|
|
88
119
|
throw new Error(`Duplicate route: ${method} ${path}`);
|
|
89
120
|
node.handlers[method] = handler;
|
|
121
|
+
this.hasDynamicRoutes = true;
|
|
122
|
+
this.revision++;
|
|
123
|
+
if (operationId)
|
|
124
|
+
this.operationIds.add(operationId);
|
|
90
125
|
}
|
|
91
126
|
/**
|
|
92
127
|
* Look up the handler registered for the given method and path. Tries the
|
|
93
128
|
* static fast path first, then walks the trie, extracting and decoding path
|
|
94
129
|
* params. Path-traversal lookups (`..`, `//`) are rejected up front.
|
|
130
|
+
* Handler tables inherit only from an empty, frozen, prototype-free base:
|
|
131
|
+
* Object.prototype properties never count as routes, including for untyped
|
|
132
|
+
* runtime method values.
|
|
95
133
|
*
|
|
96
134
|
* @param method - HTTP method to match.
|
|
97
135
|
* @param path - Request path to resolve, including any dynamic segments.
|
|
@@ -109,8 +147,11 @@ export class Router {
|
|
|
109
147
|
if (!staticEntry && path.endsWith("/")) {
|
|
110
148
|
staticEntry = this.staticTable.get(trimTrailingSlashes(path));
|
|
111
149
|
}
|
|
112
|
-
if (staticEntry
|
|
113
|
-
|
|
150
|
+
if (staticEntry) {
|
|
151
|
+
const handler = staticEntry[method];
|
|
152
|
+
if (handler !== undefined || Object.hasOwn(staticEntry, method)) {
|
|
153
|
+
return { handler: handler, params: {} };
|
|
154
|
+
}
|
|
114
155
|
}
|
|
115
156
|
const segments = splitPath(path);
|
|
116
157
|
const params = {};
|
|
@@ -118,25 +159,51 @@ export class Router {
|
|
|
118
159
|
if (!found)
|
|
119
160
|
return undefined;
|
|
120
161
|
const handler = found.handlers[method];
|
|
121
|
-
if (!
|
|
162
|
+
if (handler === undefined && !Object.hasOwn(found.handlers, method))
|
|
122
163
|
return undefined;
|
|
123
|
-
return { handler, params };
|
|
164
|
+
return { handler: handler, params };
|
|
124
165
|
}
|
|
125
|
-
/**
|
|
166
|
+
/**
|
|
167
|
+
* Return the methods registered at the matched path for 405 responses.
|
|
168
|
+
* Static-only routers skip trie traversal. Results are fresh arrays and
|
|
169
|
+
* reflect routes registered after earlier lookups. Only validated registered
|
|
170
|
+
* static paths are cached, bounding cache size by the static route count.
|
|
171
|
+
* @param path - Request path, subject to the same traversal and empty-segment
|
|
172
|
+
* rejection as {@link Router.find}.
|
|
173
|
+
* @returns Registered methods, or an empty array for rejected or unmatched paths.
|
|
174
|
+
*/
|
|
126
175
|
allowedMethods(path) {
|
|
176
|
+
const cached = this.staticMethods.get(path);
|
|
177
|
+
if (cached?.revision === this.revision)
|
|
178
|
+
return cached.methods.slice();
|
|
179
|
+
if (path.includes("/../") || path.endsWith("/..") || path.includes("//")) {
|
|
180
|
+
return [];
|
|
181
|
+
}
|
|
127
182
|
let fromStatic = this.staticTable.get(path);
|
|
128
183
|
if (!fromStatic && path.endsWith("/")) {
|
|
129
184
|
fromStatic = this.staticTable.get(trimTrailingSlashes(path));
|
|
130
185
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
186
|
+
const found = this.hasDynamicRoutes
|
|
187
|
+
? this.walk(this.root, splitPath(path), 0, {})
|
|
188
|
+
: undefined;
|
|
189
|
+
if (!fromStatic)
|
|
190
|
+
return found ? Object.keys(found.handlers) : [];
|
|
191
|
+
const methods = Object.keys(fromStatic);
|
|
192
|
+
if (found) {
|
|
193
|
+
for (const method of Object.keys(found.handlers)) {
|
|
194
|
+
if (!Object.hasOwn(fromStatic, method))
|
|
195
|
+
methods.push(method);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
this.staticMethods.set(trimTrailingSlashes(path), {
|
|
199
|
+
revision: this.revision,
|
|
200
|
+
methods,
|
|
201
|
+
});
|
|
202
|
+
return methods.slice();
|
|
136
203
|
}
|
|
137
204
|
walk(node, segs, i, params) {
|
|
138
205
|
if (i === segs.length)
|
|
139
|
-
return node;
|
|
206
|
+
return node.handlers ? node : undefined;
|
|
140
207
|
const seg = segs[i];
|
|
141
208
|
const staticNext = node.children.get(seg);
|
|
142
209
|
if (staticNext) {
|
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:ea6ea660-6d6a-5827-8fe5-9def13029bef",
|
|
5
5
|
"version": 1,
|
|
6
6
|
"metadata": {
|
|
7
|
-
"timestamp": "2026-09-
|
|
7
|
+
"timestamp": "2026-09-13T02:28:52.478Z",
|
|
8
8
|
"tools": [
|
|
9
9
|
{
|
|
10
10
|
"vendor": "DaloyJS",
|
|
11
11
|
"name": "daloy-generate-sbom",
|
|
12
|
-
"version": "1.3.
|
|
12
|
+
"version": "1.3.4"
|
|
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@1.3.
|
|
22
|
+
"bom-ref": "pkg:npm/@daloyjs/core@1.3.4",
|
|
23
23
|
"name": "@daloyjs/core",
|
|
24
|
-
"version": "1.3.
|
|
24
|
+
"version": "1.3.4",
|
|
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@1.3.
|
|
26
|
+
"purl": "pkg:npm/@daloyjs/core@1.3.4",
|
|
27
27
|
"licenses": [
|
|
28
28
|
{
|
|
29
29
|
"license": {
|
|
@@ -46,9 +46,9 @@
|
|
|
46
46
|
}
|
|
47
47
|
],
|
|
48
48
|
"swid": {
|
|
49
|
-
"tagId": "swidtag--daloyjs-core-1.3.
|
|
49
|
+
"tagId": "swidtag--daloyjs-core-1.3.4",
|
|
50
50
|
"name": "@daloyjs/core",
|
|
51
|
-
"version": "1.3.
|
|
51
|
+
"version": "1.3.4",
|
|
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@1.3.
|
|
60
|
+
"ref": "pkg:npm/@daloyjs/core@1.3.4",
|
|
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-1.3.
|
|
6
|
-
"documentNamespace": "https://github.com/daloyjs/daloy/sbom/@daloyjs/core-1.3.
|
|
5
|
+
"name": "@daloyjs/core-1.3.4",
|
|
6
|
+
"documentNamespace": "https://github.com/daloyjs/daloy/sbom/@daloyjs/core-1.3.4-ea6ea660-6d6a-5827-8fe5-9def13029bef",
|
|
7
7
|
"creationInfo": {
|
|
8
|
-
"created": "2026-09-
|
|
8
|
+
"created": "2026-09-13T02:28:52.478Z",
|
|
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": "1.3.
|
|
19
|
+
"versionInfo": "1.3.4",
|
|
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@1.3.
|
|
30
|
+
"referenceLocator": "pkg:npm/@daloyjs/core@1.3.4"
|
|
31
31
|
}
|
|
32
32
|
]
|
|
33
33
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@daloyjs/core",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.4",
|
|
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 \u2014 distributed via pnpm.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|