@nifrajs/core 0.1.0-alpha.1
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/LICENSE +21 -0
- package/README.md +35 -0
- package/dist/errors.d.ts +21 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +26 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/brand.d.ts +11 -0
- package/dist/internal/brand.d.ts.map +1 -0
- package/dist/internal/brand.js +10 -0
- package/dist/internal/brand.js.map +1 -0
- package/dist/internal/secret.d.ts +9 -0
- package/dist/internal/secret.d.ts.map +1 -0
- package/dist/internal/secret.js +19 -0
- package/dist/internal/secret.js.map +1 -0
- package/dist/router/router.d.ts +61 -0
- package/dist/router/router.d.ts.map +1 -0
- package/dist/router/router.js +324 -0
- package/dist/router/router.js.map +1 -0
- package/dist/schema/standard.d.ts +46 -0
- package/dist/schema/standard.d.ts.map +1 -0
- package/dist/schema/standard.js +20 -0
- package/dist/schema/standard.js.map +1 -0
- package/dist/seo.d.ts +48 -0
- package/dist/seo.d.ts.map +1 -0
- package/dist/seo.js +109 -0
- package/dist/seo.js.map +1 -0
- package/dist/server/body.d.ts +39 -0
- package/dist/server/body.d.ts.map +1 -0
- package/dist/server/body.js +85 -0
- package/dist/server/body.js.map +1 -0
- package/dist/server/context.d.ts +111 -0
- package/dist/server/context.d.ts.map +1 -0
- package/dist/server/context.js +2 -0
- package/dist/server/context.js.map +1 -0
- package/dist/server/contract.d.ts +142 -0
- package/dist/server/contract.d.ts.map +1 -0
- package/dist/server/contract.js +59 -0
- package/dist/server/contract.js.map +1 -0
- package/dist/server/cookies.d.ts +41 -0
- package/dist/server/cookies.d.ts.map +1 -0
- package/dist/server/cookies.js +181 -0
- package/dist/server/cookies.js.map +1 -0
- package/dist/server/logger.d.ts +49 -0
- package/dist/server/logger.d.ts.map +1 -0
- package/dist/server/logger.js +125 -0
- package/dist/server/logger.js.map +1 -0
- package/dist/server/registry.d.ts +46 -0
- package/dist/server/registry.d.ts.map +1 -0
- package/dist/server/registry.js +2 -0
- package/dist/server/registry.js.map +1 -0
- package/dist/server/server.d.ts +528 -0
- package/dist/server/server.d.ts.map +1 -0
- package/dist/server/server.js +1660 -0
- package/dist/server/server.js.map +1 -0
- package/dist/server/sse.d.ts +54 -0
- package/dist/server/sse.d.ts.map +1 -0
- package/dist/server/sse.js +126 -0
- package/dist/server/sse.js.map +1 -0
- package/dist/server/webhook.d.ts +42 -0
- package/dist/server/webhook.d.ts.map +1 -0
- package/dist/server/webhook.js +149 -0
- package/dist/server/webhook.js.map +1 -0
- package/dist/server/websocket.d.ts +152 -0
- package/dist/server/websocket.d.ts.map +1 -0
- package/dist/server/websocket.js +178 -0
- package/dist/server/websocket.js.map +1 -0
- package/dist/server/ws-bun.d.ts +27 -0
- package/dist/server/ws-bun.d.ts.map +1 -0
- package/dist/server/ws-bun.js +82 -0
- package/dist/server/ws-bun.js.map +1 -0
- package/dist/server/ws-hook.d.ts +36 -0
- package/dist/server/ws-hook.d.ts.map +1 -0
- package/dist/server/ws-hook.js +18 -0
- package/dist/server/ws-hook.js.map +1 -0
- package/dist/ws.d.ts +19 -0
- package/dist/ws.d.ts.map +1 -0
- package/dist/ws.js +26 -0
- package/dist/ws.js.map +1 -0
- package/package.json +63 -0
- package/src/errors.ts +41 -0
- package/src/index.ts +115 -0
- package/src/internal/brand.ts +11 -0
- package/src/internal/secret.ts +24 -0
- package/src/router/router.ts +429 -0
- package/src/schema/standard.ts +76 -0
- package/src/seo.ts +155 -0
- package/src/server/body.ts +89 -0
- package/src/server/context.ts +124 -0
- package/src/server/contract.ts +209 -0
- package/src/server/cookies.ts +198 -0
- package/src/server/logger.ts +167 -0
- package/src/server/registry.ts +63 -0
- package/src/server/server.ts +2479 -0
- package/src/server/sse.ts +161 -0
- package/src/server/webhook.ts +207 -0
- package/src/server/websocket.ts +308 -0
- package/src/server/ws-bun.ts +106 -0
- package/src/server/ws-hook.ts +53 -0
- package/src/ws.ts +40 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nifra Authors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# @nifrajs/core
|
|
2
|
+
|
|
3
|
+
The Bun-native, contract-first HTTP framework at the heart of [nifra](../../README.md):
|
|
4
|
+
a radix router, a fully type-inferred server, versionable contracts, lifecycle
|
|
5
|
+
middleware, and production hardening.
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
bun add @nifrajs/core
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { server } from "@nifrajs/core"
|
|
13
|
+
|
|
14
|
+
const app = server()
|
|
15
|
+
.get("/users/:id", (c) => ({ id: c.params.id }))
|
|
16
|
+
.post("/users", { body: nameSchema }, (c) => ({ created: c.body.name }))
|
|
17
|
+
.listen(3000)
|
|
18
|
+
|
|
19
|
+
export type App = typeof app // hand this to @nifrajs/client for end-to-end types
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
- **Inline or contract-first.** Write routes inline (types inferred from the
|
|
23
|
+
builder), or `defineContract(...)` + `implement(...)` for a decoupled, versionable
|
|
24
|
+
surface — handlers lift over unchanged.
|
|
25
|
+
- **Validation at the boundary.** Per-route `body`/`query` is any
|
|
26
|
+
[Standard Schema](https://standardschema.dev) (zod/valibot/arktype, or `@nifrajs/schema`'s
|
|
27
|
+
`t`); invalid input is rejected with a structured `400` before the handler runs.
|
|
28
|
+
- **Lifecycle middleware.** `derive`/`decorate` extend the typed context;
|
|
29
|
+
`onRequest`/`beforeHandle`/`afterHandle`/`onResponse`/`onError` run around handlers;
|
|
30
|
+
`use(middleware)` applies a bundle.
|
|
31
|
+
- **Hardening built in.** `stop({ drainMs })` graceful shutdown (+ opt-in SIGTERM/
|
|
32
|
+
SIGINT), `requestTimeoutMs` (+ `ctx.signal`), a streaming body-size cap, and a
|
|
33
|
+
redacting structured `Logger`.
|
|
34
|
+
|
|
35
|
+
ESM-only; requires Bun at runtime. MIT.
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base class for every error the framework throws. Carries a stable, string
|
|
3
|
+
* `code` so callers can branch on the failure programmatically rather than
|
|
4
|
+
* matching on message text. Messages are prefixed with the brand name.
|
|
5
|
+
*/
|
|
6
|
+
export declare class FrameworkError extends Error {
|
|
7
|
+
readonly code: string;
|
|
8
|
+
constructor(code: string, message: string);
|
|
9
|
+
}
|
|
10
|
+
/** Stable codes for boot-time (L2) route configuration failures. */
|
|
11
|
+
export type RouteConfigErrorCode = "DUPLICATE_ROUTE" | "DUPLICATE_PARAM" | "PARAM_NAME_CONFLICT" | "INVALID_PATH" | "INVALID_PARAM_NAME" | "WILDCARD_NOT_LAST" | "INVALID_METHOD";
|
|
12
|
+
/**
|
|
13
|
+
* Thrown at route registration when a route is misconfigured. This is the
|
|
14
|
+
* boot-time rejection layer: loud and early,
|
|
15
|
+
* never deferred to the first request.
|
|
16
|
+
*/
|
|
17
|
+
export declare class RouteConfigError extends FrameworkError {
|
|
18
|
+
readonly code: RouteConfigErrorCode;
|
|
19
|
+
constructor(code: RouteConfigErrorCode, message: string);
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,qBAAa,cAAe,SAAQ,KAAK;IACvC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;gBAET,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM;CAK1C;AAED,oEAAoE;AACpE,MAAM,MAAM,oBAAoB,GAC5B,iBAAiB,GACjB,iBAAiB,GACjB,qBAAqB,GACrB,cAAc,GACd,oBAAoB,GACpB,mBAAmB,GACnB,gBAAgB,CAAA;AAEpB;;;;GAIG;AACH,qBAAa,gBAAiB,SAAQ,cAAc;IAElD,SAAiB,IAAI,EAAE,oBAAoB,CAAA;gBAE/B,IAAI,EAAE,oBAAoB,EAAE,OAAO,EAAE,MAAM;CAIxD"}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { FRAMEWORK_NAME } from "./internal/brand.js";
|
|
2
|
+
/**
|
|
3
|
+
* Base class for every error the framework throws. Carries a stable, string
|
|
4
|
+
* `code` so callers can branch on the failure programmatically rather than
|
|
5
|
+
* matching on message text. Messages are prefixed with the brand name.
|
|
6
|
+
*/
|
|
7
|
+
export class FrameworkError extends Error {
|
|
8
|
+
code;
|
|
9
|
+
constructor(code, message) {
|
|
10
|
+
super(`[${FRAMEWORK_NAME}] ${message}`);
|
|
11
|
+
this.name = "FrameworkError";
|
|
12
|
+
this.code = code;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Thrown at route registration when a route is misconfigured. This is the
|
|
17
|
+
* boot-time rejection layer: loud and early,
|
|
18
|
+
* never deferred to the first request.
|
|
19
|
+
*/
|
|
20
|
+
export class RouteConfigError extends FrameworkError {
|
|
21
|
+
constructor(code, message) {
|
|
22
|
+
super(code, message);
|
|
23
|
+
this.name = "RouteConfigError";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAA;AAEpD;;;;GAIG;AACH,MAAM,OAAO,cAAe,SAAQ,KAAK;IAC9B,IAAI,CAAQ;IAErB,YAAY,IAAY,EAAE,OAAe;QACvC,KAAK,CAAC,IAAI,cAAc,KAAK,OAAO,EAAE,CAAC,CAAA;QACvC,IAAI,CAAC,IAAI,GAAG,gBAAgB,CAAA;QAC5B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAA;IAClB,CAAC;CACF;AAYD;;;;GAIG;AACH,MAAM,OAAO,gBAAiB,SAAQ,cAAc;IAIlD,YAAY,IAA0B,EAAE,OAAe;QACrD,KAAK,CAAC,IAAI,EAAE,OAAO,CAAC,CAAA;QACpB,IAAI,CAAC,IAAI,GAAG,kBAAkB,CAAA;IAChC,CAAC;CACF"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @nifrajs/core — Bun-native, contract-first HTTP framework.
|
|
3
|
+
*
|
|
4
|
+
* The router is the perf-critical heart; the server and inline API are built on
|
|
5
|
+
* the unified route descriptor.
|
|
6
|
+
*/
|
|
7
|
+
/** Current package version. Kept as a literal so type-level tests can pin it. */
|
|
8
|
+
export declare const VERSION: "0.0.0";
|
|
9
|
+
export type Version = typeof VERSION;
|
|
10
|
+
export { FrameworkError, RouteConfigError, type RouteConfigErrorCode } from "./errors.js";
|
|
11
|
+
export { FRAMEWORK_NAME, type FrameworkName } from "./internal/brand.js";
|
|
12
|
+
export { METHODS, type Method, Router, type RouterMatch } from "./router/router.js";
|
|
13
|
+
export type { InferInput, InferOutput, StandardIssue, StandardResult, StandardSchemaV1, StandardTypes, ValidationOutcome, } from "./schema/standard.js";
|
|
14
|
+
export { validateStandard } from "./schema/standard.js";
|
|
15
|
+
export { type RobotsOptions, type RobotsRule, robots, type SitemapChangeFreq, type SitemapEntry, type SitemapOptions, sitemap, } from "./seo.js";
|
|
16
|
+
export type { Context, Params, Platform, Prettify, ResponseControls, RouteSchema, } from "./server/context.js";
|
|
17
|
+
export { type ContextForOp, type ContractShape, defineContract, type HandlersFor, implement, type OperationDef, type RegistryFor, type RegistryFromImpl, type ResponseDef, } from "./server/contract.js";
|
|
18
|
+
export { type CookieOptions, parseCookies, serializeCookie, signValue, unsignValue, } from "./server/cookies.js";
|
|
19
|
+
export { commonSecretPatterns, jsonLogger, type LogFields, type Logger, type RedactOptions, redactLogFields, silentLogger, } from "./server/logger.js";
|
|
20
|
+
export type { Registry, RouteInfo } from "./server/registry.js";
|
|
21
|
+
export { type AnyServer, type DurableObjectNamespaceLike, defineIdentityPlugin, definePlugin, type ExecutionContext, type Handler, type IdentityPlugin, type Middleware, type NifraPlugin, type NodeServeOutcome, type OnRequestResult, type RouteDescriptor, type RunningServer, type ScheduledController, type ScheduledHandler, Server, type ServerOptions, server, toFetchHandler, } from "./server/server.js";
|
|
22
|
+
export { type SSEContext, type SSEInit, type SSEMessage, type SSEStream, sse, } from "./server/sse.js";
|
|
23
|
+
export { type SignatureEncoding, type VerifyWebhookOptions, verifyWebhook, type WebhookFailureReason, type WebhookProvider, type WebhookResult, } from "./server/webhook.js";
|
|
24
|
+
export type { NifraWebSocket, StandardWebSocket, WebSocketContext, WebSocketData, WebSocketHandler, WebSocketUpgradeOutcome, } from "./server/websocket.js";
|
|
25
|
+
export { attachWebSocket, TopicRegistry } from "./server/websocket.js";
|
|
26
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,iFAAiF;AACjF,eAAO,MAAM,OAAO,EAAG,OAAgB,CAAA;AAEvC,MAAM,MAAM,OAAO,GAAG,OAAO,OAAO,CAAA;AAEpC,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,KAAK,oBAAoB,EAAE,MAAM,aAAa,CAAA;AACzF,OAAO,EAAE,cAAc,EAAE,KAAK,aAAa,EAAE,MAAM,qBAAqB,CAAA;AACxE,OAAO,EAAE,OAAO,EAAE,KAAK,MAAM,EAAE,MAAM,EAAE,KAAK,WAAW,EAAE,MAAM,oBAAoB,CAAA;AACnF,YAAY,EACV,UAAU,EACV,WAAW,EACX,aAAa,EACb,cAAc,EACd,gBAAgB,EAChB,aAAa,EACb,iBAAiB,GAClB,MAAM,sBAAsB,CAAA;AAC7B,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AACvD,OAAO,EACL,KAAK,aAAa,EAClB,KAAK,UAAU,EACf,MAAM,EACN,KAAK,iBAAiB,EACtB,KAAK,YAAY,EACjB,KAAK,cAAc,EACnB,OAAO,GACR,MAAM,UAAU,CAAA;AACjB,YAAY,EACV,OAAO,EACP,MAAM,EACN,QAAQ,EACR,QAAQ,EACR,gBAAgB,EAChB,WAAW,GACZ,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EACL,KAAK,YAAY,EACjB,KAAK,aAAa,EAClB,cAAc,EACd,KAAK,WAAW,EAChB,SAAS,EACT,KAAK,YAAY,EACjB,KAAK,WAAW,EAChB,KAAK,gBAAgB,EACrB,KAAK,WAAW,GACjB,MAAM,sBAAsB,CAAA;AAC7B,OAAO,EACL,KAAK,aAAa,EAClB,YAAY,EACZ,eAAe,EACf,SAAS,EACT,WAAW,GACZ,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EACL,oBAAoB,EACpB,UAAU,EACV,KAAK,SAAS,EACd,KAAK,MAAM,EACX,KAAK,aAAa,EAClB,eAAe,EACf,YAAY,GACb,MAAM,oBAAoB,CAAA;AAC3B,YAAY,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAA;AAC/D,OAAO,EACL,KAAK,SAAS,EACd,KAAK,0BAA0B,EAC/B,oBAAoB,EACpB,YAAY,EACZ,KAAK,gBAAgB,EACrB,KAAK,OAAO,EACZ,KAAK,cAAc,EACnB,KAAK,UAAU,EACf,KAAK,WAAW,EAChB,KAAK,gBAAgB,EACrB,KAAK,eAAe,EACpB,KAAK,eAAe,EACpB,KAAK,aAAa,EAClB,KAAK,mBAAmB,EACxB,KAAK,gBAAgB,EACrB,MAAM,EACN,KAAK,aAAa,EAClB,MAAM,EACN,cAAc,GACf,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EACL,KAAK,UAAU,EACf,KAAK,OAAO,EACZ,KAAK,UAAU,EACf,KAAK,SAAS,EACd,GAAG,GACJ,MAAM,iBAAiB,CAAA;AACxB,OAAO,EACL,KAAK,iBAAiB,EACtB,KAAK,oBAAoB,EACzB,aAAa,EACb,KAAK,oBAAoB,EACzB,KAAK,eAAe,EACpB,KAAK,aAAa,GACnB,MAAM,qBAAqB,CAAA;AAC5B,YAAY,EACV,cAAc,EACd,iBAAiB,EACjB,gBAAgB,EAChB,aAAa,EACb,gBAAgB,EAChB,uBAAuB,GACxB,MAAM,uBAAuB,CAAA;AAC9B,OAAO,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @nifrajs/core — Bun-native, contract-first HTTP framework.
|
|
3
|
+
*
|
|
4
|
+
* The router is the perf-critical heart; the server and inline API are built on
|
|
5
|
+
* the unified route descriptor.
|
|
6
|
+
*/
|
|
7
|
+
/** Current package version. Kept as a literal so type-level tests can pin it. */
|
|
8
|
+
export const VERSION = "0.0.0";
|
|
9
|
+
export { FrameworkError, RouteConfigError } from "./errors.js";
|
|
10
|
+
export { FRAMEWORK_NAME } from "./internal/brand.js";
|
|
11
|
+
export { METHODS, Router } from "./router/router.js";
|
|
12
|
+
export { validateStandard } from "./schema/standard.js";
|
|
13
|
+
export { robots, sitemap, } from "./seo.js";
|
|
14
|
+
export { defineContract, implement, } from "./server/contract.js";
|
|
15
|
+
export { parseCookies, serializeCookie, signValue, unsignValue, } from "./server/cookies.js";
|
|
16
|
+
export { commonSecretPatterns, jsonLogger, redactLogFields, silentLogger, } from "./server/logger.js";
|
|
17
|
+
export { defineIdentityPlugin, definePlugin, Server, server, toFetchHandler, } from "./server/server.js";
|
|
18
|
+
export { sse, } from "./server/sse.js";
|
|
19
|
+
export { verifyWebhook, } from "./server/webhook.js";
|
|
20
|
+
export { attachWebSocket, TopicRegistry } from "./server/websocket.js";
|
|
21
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,iFAAiF;AACjF,MAAM,CAAC,MAAM,OAAO,GAAG,OAAgB,CAAA;AAIvC,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAA6B,MAAM,aAAa,CAAA;AACzF,OAAO,EAAE,cAAc,EAAsB,MAAM,qBAAqB,CAAA;AACxE,OAAO,EAAE,OAAO,EAAe,MAAM,EAAoB,MAAM,oBAAoB,CAAA;AAUnF,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AACvD,OAAO,EAGL,MAAM,EAIN,OAAO,GACR,MAAM,UAAU,CAAA;AASjB,OAAO,EAGL,cAAc,EAEd,SAAS,GAKV,MAAM,sBAAsB,CAAA;AAC7B,OAAO,EAEL,YAAY,EACZ,eAAe,EACf,SAAS,EACT,WAAW,GACZ,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EACL,oBAAoB,EACpB,UAAU,EAIV,eAAe,EACf,YAAY,GACb,MAAM,oBAAoB,CAAA;AAE3B,OAAO,EAGL,oBAAoB,EACpB,YAAY,EAYZ,MAAM,EAEN,MAAM,EACN,cAAc,GACf,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EAKL,GAAG,GACJ,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAGL,aAAa,GAId,MAAM,qBAAqB,CAAA;AAS5B,OAAO,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAA"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single source of truth for the framework's user-facing name.
|
|
3
|
+
*
|
|
4
|
+
* Every developer-facing string (error prefixes, the default OpenAPI title, the
|
|
5
|
+
* logger tag) pulls from here, so renaming the brand is a one-line change. The
|
|
6
|
+
* npm scope (`@nifrajs/*`) is the other naming axis — `scripts/rename.ts` flips
|
|
7
|
+
* both at once.
|
|
8
|
+
*/
|
|
9
|
+
export declare const FRAMEWORK_NAME: "Nifra";
|
|
10
|
+
export type FrameworkName = typeof FRAMEWORK_NAME;
|
|
11
|
+
//# sourceMappingURL=brand.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"brand.d.ts","sourceRoot":"","sources":["../../src/internal/brand.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,EAAG,OAAgB,CAAA;AAE9C,MAAM,MAAM,aAAa,GAAG,OAAO,cAAc,CAAA"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single source of truth for the framework's user-facing name.
|
|
3
|
+
*
|
|
4
|
+
* Every developer-facing string (error prefixes, the default OpenAPI title, the
|
|
5
|
+
* logger tag) pulls from here, so renaming the brand is a one-line change. The
|
|
6
|
+
* npm scope (`@nifrajs/*`) is the other naming axis — `scripts/rename.ts` flips
|
|
7
|
+
* both at once.
|
|
8
|
+
*/
|
|
9
|
+
export const FRAMEWORK_NAME = "Nifra";
|
|
10
|
+
//# sourceMappingURL=brand.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"brand.js","sourceRoot":"","sources":["../../src/internal/brand.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,OAAgB,CAAA"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Boot-time guard: reject an HMAC secret under 256 bits, loudly, at construction — never at the
|
|
3
|
+
* first request. UTF-8 byte length (not char count), so a multibyte secret is measured honestly.
|
|
4
|
+
* The single source of truth for the framework's secret-strength floor; every HMAC entry point
|
|
5
|
+
* (signed cookies, `@nifrajs/auth` sessions, webhook verification, signed upload/image URLs) routes
|
|
6
|
+
* its secret through here so the bar can't drift between them.
|
|
7
|
+
*/
|
|
8
|
+
export declare function requireSecretBytes(secret: string | Uint8Array, label: string): void;
|
|
9
|
+
//# sourceMappingURL=secret.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"secret.d.ts","sourceRoot":"","sources":["../../src/internal/secret.ts"],"names":[],"mappings":"AAQA;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAQnF"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { FRAMEWORK_NAME } from "./brand.js";
|
|
2
|
+
/** Minimum HMAC secret strength: 256 bits, matching the SHA-256 block the framework signs with.
|
|
3
|
+
* A shorter key weakens every signature it backs (cookies, sessions, webhooks, signed URLs). */
|
|
4
|
+
const MIN_SECRET_BYTES = 32;
|
|
5
|
+
const SECRET_ENCODER = new TextEncoder();
|
|
6
|
+
/**
|
|
7
|
+
* Boot-time guard: reject an HMAC secret under 256 bits, loudly, at construction — never at the
|
|
8
|
+
* first request. UTF-8 byte length (not char count), so a multibyte secret is measured honestly.
|
|
9
|
+
* The single source of truth for the framework's secret-strength floor; every HMAC entry point
|
|
10
|
+
* (signed cookies, `@nifrajs/auth` sessions, webhook verification, signed upload/image URLs) routes
|
|
11
|
+
* its secret through here so the bar can't drift between them.
|
|
12
|
+
*/
|
|
13
|
+
export function requireSecretBytes(secret, label) {
|
|
14
|
+
const length = typeof secret === "string" ? SECRET_ENCODER.encode(secret).length : secret.byteLength;
|
|
15
|
+
if (length < MIN_SECRET_BYTES) {
|
|
16
|
+
throw new Error(`[${FRAMEWORK_NAME}] ${label} secret must be at least ${MIN_SECRET_BYTES} bytes (256-bit); got ${length}. Generate one with: openssl rand -base64 32`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=secret.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"secret.js","sourceRoot":"","sources":["../../src/internal/secret.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAE3C;gGACgG;AAChG,MAAM,gBAAgB,GAAG,EAAE,CAAA;AAE3B,MAAM,cAAc,GAAG,IAAI,WAAW,EAAE,CAAA;AAExC;;;;;;GAMG;AACH,MAAM,UAAU,kBAAkB,CAAC,MAA2B,EAAE,KAAa;IAC3E,MAAM,MAAM,GACV,OAAO,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,cAAc,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,UAAU,CAAA;IACvF,IAAI,MAAM,GAAG,gBAAgB,EAAE,CAAC;QAC9B,MAAM,IAAI,KAAK,CACb,IAAI,cAAc,KAAK,KAAK,4BAA4B,gBAAgB,yBAAyB,MAAM,8CAA8C,CACtJ,CAAA;IACH,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/** HTTP methods the router accepts. */
|
|
2
|
+
export declare const METHODS: readonly ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
|
|
3
|
+
export type Method = (typeof METHODS)[number];
|
|
4
|
+
export declare const EMPTY_PARAMS: Record<string, string>;
|
|
5
|
+
/**
|
|
6
|
+
* Result of {@link Router.find}. The `found: false` cases deliberately separate
|
|
7
|
+
* a missing path (404) from a path that exists for other methods (405), so the
|
|
8
|
+
* server layer can answer correctly and populate an `Allow` header.
|
|
9
|
+
*/
|
|
10
|
+
export type RouterMatch<T> = {
|
|
11
|
+
readonly found: true;
|
|
12
|
+
readonly payload: T;
|
|
13
|
+
readonly params: Record<string, string>;
|
|
14
|
+
} | {
|
|
15
|
+
readonly found: false;
|
|
16
|
+
readonly reason: "not-found";
|
|
17
|
+
} | {
|
|
18
|
+
readonly found: false;
|
|
19
|
+
readonly reason: "method-not-allowed";
|
|
20
|
+
readonly allowed: string[];
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Radix-style segment trie router. Matching precedence is static > param >
|
|
24
|
+
* wildcard. Parameter/wildcard values are returned RAW (not percent-decoded);
|
|
25
|
+
* the server boundary decodes and rejects malformed encodings with a 400,
|
|
26
|
+
* keeping this layer pure and allocation-light.
|
|
27
|
+
*
|
|
28
|
+
* @typeParam T - the payload stored per (method, path); a route descriptor in
|
|
29
|
+
* practice, but the router is agnostic to it.
|
|
30
|
+
*/
|
|
31
|
+
export declare class Router<T> {
|
|
32
|
+
private readonly root;
|
|
33
|
+
/**
|
|
34
|
+
* Fast path: fully-static paths (no `:param`/`*`) resolve in a single map
|
|
35
|
+
* lookup keyed by the full path, skipping the trie walk entirely. Values are
|
|
36
|
+
* the same `Terminal` objects held in the trie, so adding a method later
|
|
37
|
+
* updates both views at once.
|
|
38
|
+
*/
|
|
39
|
+
private readonly staticRoutes;
|
|
40
|
+
/**
|
|
41
|
+
* Scratch storage for param/wildcard captures during a synchronous trie walk. `find()` copies the
|
|
42
|
+
* values into the returned params object before clearing this array, so returned matches never share
|
|
43
|
+
* mutable state. Kept per Router instance, not module-global, to avoid cross-app coupling.
|
|
44
|
+
*/
|
|
45
|
+
private readonly paramValuesScratch;
|
|
46
|
+
constructor();
|
|
47
|
+
/**
|
|
48
|
+
* Register a payload for `method` + `path`. Throws {@link RouteConfigError}
|
|
49
|
+
* (boot-time, L2) on a duplicate route, a malformed pattern, or conflicting
|
|
50
|
+
* parameter names for the same path shape.
|
|
51
|
+
*/
|
|
52
|
+
add(method: Method, path: string, payload: T): void;
|
|
53
|
+
/**
|
|
54
|
+
* Resolve `method` + `path`. Tolerant of a missing leading slash and of
|
|
55
|
+
* method casing. Never throws.
|
|
56
|
+
*/
|
|
57
|
+
find(method: string, path: string): RouterMatch<T>;
|
|
58
|
+
private findDynamic;
|
|
59
|
+
private maybeCacheDynamicMatch;
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=router.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../../src/router/router.ts"],"names":[],"mappings":"AAEA,uCAAuC;AACvC,eAAO,MAAM,OAAO,uEAAwE,CAAA;AAE5F,MAAM,MAAM,MAAM,GAAG,CAAC,OAAO,OAAO,CAAC,CAAC,MAAM,CAAC,CAAA;AA0B7C,eAAO,MAAM,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAqB,CAAA;AAGrE;;;;GAIG;AACH,MAAM,MAAM,WAAW,CAAC,CAAC,IACrB;IAAE,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,GACtF;IAAE,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAA;CAAE,GACvD;IAAE,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,oBAAoB,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,CAAA;AAyKhG;;;;;;;;GAQG;AACH,qBAAa,MAAM,CAAC,CAAC;IACnB,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAc;IACnC;;;;;OAKG;IACH,OAAO,CAAC,QAAQ,CAAC,YAAY,CAA0B;IACvD;;;;OAIG;IACH,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAU;;IAQ7C;;;;OAIG;IACH,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,GAAG,IAAI;IA4GnD;;;OAGG;IACH,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,WAAW,CAAC,CAAC,CAAC;IAelD,OAAO,CAAC,WAAW;IA8BnB,OAAO,CAAC,sBAAsB;CAwB/B"}
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import { RouteConfigError } from "../errors.js";
|
|
2
|
+
/** HTTP methods the router accepts. */
|
|
3
|
+
export const METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
|
|
4
|
+
const METHOD_SET = new Set(METHODS);
|
|
5
|
+
function isMethod(value) {
|
|
6
|
+
return METHOD_SET.has(value);
|
|
7
|
+
}
|
|
8
|
+
/** Parameter names must be valid identifiers — mirrors the global identifier rule. */
|
|
9
|
+
const PARAM_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
10
|
+
/** Object-prototype keys are rejected as param names: a route like `/:__proto__` could let a captured
|
|
11
|
+
* value reach a prototype-polluting key. Param *names* come from the route pattern (never from request
|
|
12
|
+
* input) and are validated here at registration, so a reserved key can never reach `buildParams` —
|
|
13
|
+
* this boot-time guard is the whole protection. The runtime params object stays a plain `{}` (only
|
|
14
|
+
* user-controlled *values* land in it, under these validated keys), keeping the param path
|
|
15
|
+
* allocation-cheap; a null-prototype object would add cost for no added safety. */
|
|
16
|
+
const RESERVED_PARAM_NAMES = new Set(["__proto__", "constructor", "prototype"]);
|
|
17
|
+
/** A param name is valid when it's an identifier AND not a reserved object-prototype key. */
|
|
18
|
+
const validParamName = (name) => PARAM_NAME.test(name) && !RESERVED_PARAM_NAMES.has(name);
|
|
19
|
+
const SLASH = 47;
|
|
20
|
+
const COLON = 58;
|
|
21
|
+
const STAR = 42;
|
|
22
|
+
export const EMPTY_PARAMS = Object.freeze({});
|
|
23
|
+
const DYNAMIC_MATCH_CACHE_MAX = 2048;
|
|
24
|
+
const DYNAMIC_MATCH_CACHES = new WeakMap();
|
|
25
|
+
function dynamicMatchCacheStateFor(owner) {
|
|
26
|
+
const existing = DYNAMIC_MATCH_CACHES.get(owner);
|
|
27
|
+
if (existing !== undefined) {
|
|
28
|
+
return existing;
|
|
29
|
+
}
|
|
30
|
+
const state = {
|
|
31
|
+
cache: undefined,
|
|
32
|
+
pendingPath: undefined,
|
|
33
|
+
};
|
|
34
|
+
DYNAMIC_MATCH_CACHES.set(owner, state);
|
|
35
|
+
return state;
|
|
36
|
+
}
|
|
37
|
+
function createNode() {
|
|
38
|
+
return {
|
|
39
|
+
staticChildren: new Map(),
|
|
40
|
+
paramChild: undefined,
|
|
41
|
+
wildcardChild: undefined,
|
|
42
|
+
terminal: undefined,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function sameNames(a, b) {
|
|
46
|
+
if (a.length !== b.length)
|
|
47
|
+
return false;
|
|
48
|
+
for (let i = 0; i < a.length; i++) {
|
|
49
|
+
if (a[i] !== b[i])
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Depth-first match with backtracking, walking the path by character offsets
|
|
56
|
+
* instead of pre-splitting it into an array. `start` is the index of the first
|
|
57
|
+
* char of the current segment; the segment runs to the next `/` (or end). Tries
|
|
58
|
+
* static, then param, then wildcard; if a static branch dead-ends, param/wildcard
|
|
59
|
+
* are still attempted — so `/a/b/c` and `/a/:x/d` coexist and `/a/b/d` resolves
|
|
60
|
+
* to the param route. Fills `paramValues` along the successful path; abandoned
|
|
61
|
+
* branches are popped. Avoiding the split keeps `find` allocation-light: only the
|
|
62
|
+
* segment substrings needed for map lookups (and matched param values) are cut.
|
|
63
|
+
*/
|
|
64
|
+
function matchNode(node, path, start, len, paramValues) {
|
|
65
|
+
const slash = path.indexOf("/", start);
|
|
66
|
+
const isLast = slash === -1;
|
|
67
|
+
const end = isLast ? len : slash;
|
|
68
|
+
const seg = path.slice(start, end);
|
|
69
|
+
const staticChild = node.staticChildren.get(seg);
|
|
70
|
+
if (staticChild !== undefined) {
|
|
71
|
+
if (isLast) {
|
|
72
|
+
if (staticChild.terminal !== undefined)
|
|
73
|
+
return staticChild.terminal;
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
const found = matchNode(staticChild, path, end + 1, len, paramValues);
|
|
77
|
+
if (found !== undefined)
|
|
78
|
+
return found;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const paramChild = node.paramChild;
|
|
82
|
+
// An empty segment (`/users//posts`) never matches a `:param` — matching would hand the handler
|
|
83
|
+
// `id: ""` and downstream code a `WHERE id = ''` class of bug. Peers (Hono, Elysia, Next) 404
|
|
84
|
+
// here too; a static "" child can still match (none is ever registered — INVALID_PATH guards).
|
|
85
|
+
if (paramChild !== undefined && seg.length > 0) {
|
|
86
|
+
paramValues.push(seg);
|
|
87
|
+
if (isLast) {
|
|
88
|
+
if (paramChild.terminal !== undefined)
|
|
89
|
+
return paramChild.terminal;
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
const found = matchNode(paramChild, path, end + 1, len, paramValues);
|
|
93
|
+
if (found !== undefined)
|
|
94
|
+
return found;
|
|
95
|
+
}
|
|
96
|
+
paramValues.pop(); // backtrack: this branch failed deeper down
|
|
97
|
+
}
|
|
98
|
+
const wildcard = node.wildcardChild;
|
|
99
|
+
if (wildcard !== undefined && wildcard.terminal !== undefined) {
|
|
100
|
+
// Capture the rest of the path verbatim from this segment's start.
|
|
101
|
+
paramValues.push(path.slice(start));
|
|
102
|
+
return wildcard.terminal;
|
|
103
|
+
}
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
function buildParams(names, values) {
|
|
107
|
+
if (names.length === 0)
|
|
108
|
+
return EMPTY_PARAMS;
|
|
109
|
+
const params = {};
|
|
110
|
+
for (let i = 0; i < names.length; i++) {
|
|
111
|
+
// names and values align: one value was pushed per param/wildcard node along
|
|
112
|
+
// the matched path, in declaration order.
|
|
113
|
+
params[names[i]] = values[i];
|
|
114
|
+
}
|
|
115
|
+
return params;
|
|
116
|
+
}
|
|
117
|
+
function resolve(terminal, method, params) {
|
|
118
|
+
if (terminal.staticMatches !== undefined && params === EMPTY_PARAMS) {
|
|
119
|
+
const upper = method.toUpperCase();
|
|
120
|
+
const cached = terminal.staticMatches.get(upper);
|
|
121
|
+
if (cached !== undefined)
|
|
122
|
+
return cached;
|
|
123
|
+
if (!terminal.handlers.has(upper))
|
|
124
|
+
return resolveDirect(terminal, method, params);
|
|
125
|
+
const res = resolveDirect(terminal, method, params);
|
|
126
|
+
terminal.staticMatches.set(upper, res);
|
|
127
|
+
return res;
|
|
128
|
+
}
|
|
129
|
+
return resolveDirect(terminal, method, params);
|
|
130
|
+
}
|
|
131
|
+
function resolveDirect(terminal, method, params) {
|
|
132
|
+
if (terminal.handlers.has(method)) {
|
|
133
|
+
return { found: true, payload: terminal.handlers.get(method), params };
|
|
134
|
+
}
|
|
135
|
+
const upper = method.toUpperCase();
|
|
136
|
+
if (upper === method || !terminal.handlers.has(upper)) {
|
|
137
|
+
return { found: false, reason: "method-not-allowed", allowed: [...terminal.handlers.keys()] };
|
|
138
|
+
}
|
|
139
|
+
const payload = terminal.handlers.get(upper); // present by the has() check above
|
|
140
|
+
return { found: true, payload, params };
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Radix-style segment trie router. Matching precedence is static > param >
|
|
144
|
+
* wildcard. Parameter/wildcard values are returned RAW (not percent-decoded);
|
|
145
|
+
* the server boundary decodes and rejects malformed encodings with a 400,
|
|
146
|
+
* keeping this layer pure and allocation-light.
|
|
147
|
+
*
|
|
148
|
+
* @typeParam T - the payload stored per (method, path); a route descriptor in
|
|
149
|
+
* practice, but the router is agnostic to it.
|
|
150
|
+
*/
|
|
151
|
+
export class Router {
|
|
152
|
+
root;
|
|
153
|
+
/**
|
|
154
|
+
* Fast path: fully-static paths (no `:param`/`*`) resolve in a single map
|
|
155
|
+
* lookup keyed by the full path, skipping the trie walk entirely. Values are
|
|
156
|
+
* the same `Terminal` objects held in the trie, so adding a method later
|
|
157
|
+
* updates both views at once.
|
|
158
|
+
*/
|
|
159
|
+
staticRoutes;
|
|
160
|
+
/**
|
|
161
|
+
* Scratch storage for param/wildcard captures during a synchronous trie walk. `find()` copies the
|
|
162
|
+
* values into the returned params object before clearing this array, so returned matches never share
|
|
163
|
+
* mutable state. Kept per Router instance, not module-global, to avoid cross-app coupling.
|
|
164
|
+
*/
|
|
165
|
+
paramValuesScratch;
|
|
166
|
+
constructor() {
|
|
167
|
+
this.root = createNode();
|
|
168
|
+
this.staticRoutes = new Map();
|
|
169
|
+
this.paramValuesScratch = [];
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Register a payload for `method` + `path`. Throws {@link RouteConfigError}
|
|
173
|
+
* (boot-time, L2) on a duplicate route, a malformed pattern, or conflicting
|
|
174
|
+
* parameter names for the same path shape.
|
|
175
|
+
*/
|
|
176
|
+
add(method, path, payload) {
|
|
177
|
+
DYNAMIC_MATCH_CACHES.delete(this);
|
|
178
|
+
const upper = method.toUpperCase();
|
|
179
|
+
// The `Method` parameter type is the compile-time guard; this re-checks at
|
|
180
|
+
// runtime for JS callers and dynamically-computed methods.
|
|
181
|
+
if (!isMethod(upper)) {
|
|
182
|
+
throw new RouteConfigError("INVALID_METHOD", `unsupported HTTP method "${method}"`);
|
|
183
|
+
}
|
|
184
|
+
if (path.length === 0 || path.charCodeAt(0) !== SLASH) {
|
|
185
|
+
throw new RouteConfigError("INVALID_PATH", `path must start with "/": "${path}"`);
|
|
186
|
+
}
|
|
187
|
+
const segments = path === "/" ? [] : path.slice(1).split("/");
|
|
188
|
+
const paramNames = [];
|
|
189
|
+
let isStatic = true;
|
|
190
|
+
let node = this.root;
|
|
191
|
+
for (let i = 0; i < segments.length; i++) {
|
|
192
|
+
const seg = segments[i];
|
|
193
|
+
const marker = seg.charCodeAt(0);
|
|
194
|
+
if (marker === COLON) {
|
|
195
|
+
isStatic = false;
|
|
196
|
+
const name = seg.slice(1);
|
|
197
|
+
if (!validParamName(name)) {
|
|
198
|
+
throw new RouteConfigError("INVALID_PARAM_NAME", `invalid parameter ":${name}" in "${path}"`);
|
|
199
|
+
}
|
|
200
|
+
if (paramNames.includes(name)) {
|
|
201
|
+
throw new RouteConfigError("DUPLICATE_PARAM", `duplicate parameter ":${name}" in "${path}"`);
|
|
202
|
+
}
|
|
203
|
+
paramNames.push(name);
|
|
204
|
+
node.paramChild ??= createNode();
|
|
205
|
+
node = node.paramChild;
|
|
206
|
+
}
|
|
207
|
+
else if (marker === STAR) {
|
|
208
|
+
isStatic = false;
|
|
209
|
+
if (i !== segments.length - 1) {
|
|
210
|
+
throw new RouteConfigError("WILDCARD_NOT_LAST", `wildcard must be the final segment in "${path}"`);
|
|
211
|
+
}
|
|
212
|
+
const name = seg.length === 1 ? "*" : seg.slice(1);
|
|
213
|
+
if (name !== "*" && !validParamName(name)) {
|
|
214
|
+
throw new RouteConfigError("INVALID_PARAM_NAME", `invalid wildcard "*${name}" in "${path}"`);
|
|
215
|
+
}
|
|
216
|
+
if (paramNames.includes(name)) {
|
|
217
|
+
throw new RouteConfigError("DUPLICATE_PARAM", `duplicate parameter "${name}" in "${path}"`);
|
|
218
|
+
}
|
|
219
|
+
paramNames.push(name);
|
|
220
|
+
node.wildcardChild ??= createNode();
|
|
221
|
+
node = node.wildcardChild;
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
let child = node.staticChildren.get(seg);
|
|
225
|
+
if (child === undefined) {
|
|
226
|
+
child = createNode();
|
|
227
|
+
node.staticChildren.set(seg, child);
|
|
228
|
+
}
|
|
229
|
+
node = child;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
const existing = node.terminal;
|
|
233
|
+
let terminal;
|
|
234
|
+
if (existing !== undefined) {
|
|
235
|
+
if (!sameNames(existing.paramNames, paramNames)) {
|
|
236
|
+
throw new RouteConfigError("PARAM_NAME_CONFLICT", `conflicting parameter names for "${path}": [${existing.paramNames.join(", ")}] vs [${paramNames.join(", ")}]`);
|
|
237
|
+
}
|
|
238
|
+
if (existing.handlers.has(upper)) {
|
|
239
|
+
throw new RouteConfigError("DUPLICATE_ROUTE", `duplicate route: ${upper} ${path}`);
|
|
240
|
+
}
|
|
241
|
+
existing.handlers.set(upper, payload);
|
|
242
|
+
existing.staticMatches?.clear();
|
|
243
|
+
terminal = existing;
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
terminal = isStatic
|
|
247
|
+
? {
|
|
248
|
+
handlers: new Map([[upper, payload]]),
|
|
249
|
+
paramNames,
|
|
250
|
+
staticMatches: new Map(),
|
|
251
|
+
}
|
|
252
|
+
: {
|
|
253
|
+
handlers: new Map([[upper, payload]]),
|
|
254
|
+
paramNames,
|
|
255
|
+
};
|
|
256
|
+
node.terminal = terminal;
|
|
257
|
+
}
|
|
258
|
+
// Mirror fully-static routes into the fast-path map (same Terminal object).
|
|
259
|
+
if (isStatic) {
|
|
260
|
+
this.staticRoutes.set(path, terminal);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Resolve `method` + `path`. Tolerant of a missing leading slash and of
|
|
265
|
+
* method casing. Never throws.
|
|
266
|
+
*/
|
|
267
|
+
find(method, path) {
|
|
268
|
+
const len = path.length;
|
|
269
|
+
const leading = len > 0 && path.charCodeAt(0) === SLASH;
|
|
270
|
+
// Fast path: a fully-static path resolves in one map lookup, no trie walk.
|
|
271
|
+
if (leading) {
|
|
272
|
+
const staticTerminal = this.staticRoutes.get(path);
|
|
273
|
+
if (staticTerminal !== undefined) {
|
|
274
|
+
return resolve(staticTerminal, method, EMPTY_PARAMS);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return this.findDynamic(method, path, leading, len);
|
|
278
|
+
}
|
|
279
|
+
findDynamic(method, path, leading, len) {
|
|
280
|
+
if (leading) {
|
|
281
|
+
const cached = DYNAMIC_MATCH_CACHES.get(this)?.cache?.get(path);
|
|
282
|
+
if (cached !== undefined) {
|
|
283
|
+
return resolve(cached.terminal, method, buildParams(cached.terminal.paramNames, cached.values));
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
const paramValues = this.paramValuesScratch;
|
|
287
|
+
paramValues.length = 0;
|
|
288
|
+
try {
|
|
289
|
+
const terminal = matchNode(this.root, path, leading ? 1 : 0, len, paramValues);
|
|
290
|
+
if (terminal === undefined) {
|
|
291
|
+
return { found: false, reason: "not-found" };
|
|
292
|
+
}
|
|
293
|
+
if (leading && terminal.paramNames.length > 0) {
|
|
294
|
+
this.maybeCacheDynamicMatch(path, terminal, paramValues);
|
|
295
|
+
}
|
|
296
|
+
return resolve(terminal, method, buildParams(terminal.paramNames, paramValues));
|
|
297
|
+
}
|
|
298
|
+
finally {
|
|
299
|
+
paramValues.length = 0;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
maybeCacheDynamicMatch(path, terminal, values) {
|
|
303
|
+
const state = dynamicMatchCacheStateFor(this);
|
|
304
|
+
if (state.pendingPath !== path) {
|
|
305
|
+
state.pendingPath = path;
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
let cache = state.cache;
|
|
309
|
+
if (cache === undefined) {
|
|
310
|
+
cache = new Map();
|
|
311
|
+
state.cache = cache;
|
|
312
|
+
}
|
|
313
|
+
if (cache.size >= DYNAMIC_MATCH_CACHE_MAX) {
|
|
314
|
+
const oldest = cache.keys().next().value;
|
|
315
|
+
if (oldest !== undefined)
|
|
316
|
+
cache.delete(oldest);
|
|
317
|
+
}
|
|
318
|
+
cache.set(path, {
|
|
319
|
+
terminal,
|
|
320
|
+
values: values.slice(0, terminal.paramNames.length),
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
//# sourceMappingURL=router.js.map
|