@daloyjs/core 1.0.0-beta.6 → 1.0.0-rc.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 +2 -2
- package/dist/adapters/bun.d.ts +13 -1
- package/dist/adapters/bun.js +8 -1
- package/dist/adapters/cloudflare.d.ts +7 -1
- package/dist/adapters/cloudflare.js +6 -1
- package/dist/adapters/deno.d.ts +11 -1
- package/dist/adapters/deno.js +8 -1
- package/dist/adapters/fastly.d.ts +12 -2
- package/dist/adapters/fastly.js +12 -2
- package/dist/adapters/lambda.d.ts +37 -1
- package/dist/adapters/lambda.js +6 -1
- package/dist/adapters/node.d.ts +12 -1
- package/dist/adapters/node.js +223 -12
- package/dist/adapters/vercel.d.ts +13 -1
- package/dist/adapters/vercel.js +12 -1
- package/dist/app.d.ts +78 -17
- package/dist/app.js +444 -86
- package/dist/banner.d.ts +6 -0
- package/dist/banner.js +6 -0
- package/dist/cli.d.ts +35 -0
- package/dist/cli.js +23 -1
- package/dist/combine.d.ts +8 -0
- package/dist/combine.js +8 -0
- package/dist/compression.d.ts +3 -0
- package/dist/compression.js +3 -0
- package/dist/config.d.ts +4 -0
- package/dist/config.js +4 -0
- package/dist/conn-info.d.ts +35 -2
- package/dist/conn-info.js +35 -2
- package/dist/contract.d.ts +2 -0
- package/dist/contract.js +2 -0
- package/dist/cookie.d.ts +12 -0
- package/dist/cookie.js +12 -0
- package/dist/dependency.d.ts +4 -0
- package/dist/dependency.js +3 -0
- package/dist/discriminator.d.ts +13 -0
- package/dist/discriminator.js +23 -1
- package/dist/docs.d.ts +78 -0
- package/dist/docs.js +21 -0
- package/dist/errors.d.ts +16 -0
- package/dist/errors.js +14 -0
- package/dist/etag.d.ts +2 -0
- package/dist/etag.js +2 -0
- package/dist/fetch-guard.d.ts +7 -0
- package/dist/fetch-guard.js +7 -0
- package/dist/fetch-resilience.d.ts +4 -0
- package/dist/fetch-resilience.js +4 -0
- package/dist/http-signatures.d.ts +32 -0
- package/dist/http-signatures.js +30 -0
- package/dist/index.d.ts +1 -1
- package/dist/ip-restriction.d.ts +40 -3
- package/dist/ip-restriction.js +35 -3
- package/dist/jwk.d.ts +12 -1
- package/dist/jwk.js +6 -0
- package/dist/jwt.d.ts +14 -0
- package/dist/jwt.js +10 -0
- package/dist/load-shedding.d.ts +3 -0
- package/dist/load-shedding.js +3 -0
- package/dist/logger.d.ts +13 -0
- package/dist/logger.js +3 -0
- package/dist/mcp.d.ts +152 -10
- package/dist/mcp.js +223 -19
- package/dist/middleware.d.ts +68 -0
- package/dist/middleware.js +17 -0
- package/dist/mtls.d.ts +19 -2
- package/dist/mtls.js +12 -2
- package/dist/multipart.d.ts +42 -5
- package/dist/multipart.js +41 -5
- package/dist/openapi.d.ts +15 -9
- package/dist/openapi.js +6 -9
- package/dist/rate-limit-redis.d.ts +21 -2
- package/dist/rate-limit-redis.js +17 -2
- package/dist/safe-redirect.d.ts +6 -0
- package/dist/safe-redirect.js +6 -0
- package/dist/sbom.cdx.json +9 -9
- package/dist/sbom.spdx.json +5 -5
- package/dist/scheduler.d.ts +4 -0
- package/dist/schema.d.ts +25 -0
- package/dist/security-schemes.d.ts +50 -0
- package/dist/security-schemes.js +6 -0
- package/dist/security.d.ts +33 -0
- package/dist/security.js +28 -10
- package/dist/session.d.ts +34 -5
- package/dist/session.js +31 -5
- package/dist/streaming.d.ts +19 -0
- package/dist/streaming.js +16 -0
- package/dist/subdomains.d.ts +4 -0
- package/dist/subdomains.js +4 -0
- package/dist/time-claims.d.ts +22 -2
- package/dist/time-claims.js +6 -0
- package/dist/tracing.d.ts +12 -0
- package/dist/tracing.js +6 -0
- package/dist/types.d.ts +70 -1
- package/dist/waf.js +21 -1
- package/dist/webhook-delivery.d.ts +6 -0
- package/dist/webhook-delivery.js +5 -0
- package/dist/websocket.d.ts +137 -4
- package/dist/websocket.js +105 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -509,7 +509,7 @@ The core only ever sees `Request → Response`. Adapters live at the edge.
|
|
|
509
509
|
|
|
510
510
|
## Status
|
|
511
511
|
|
|
512
|
-
DaloyJS is now
|
|
512
|
+
DaloyJS is now at its **first `1.0.0` release candidate** (`1.0.0-rc.0`). The public API is frozen for the 1.0 line: only bug fixes and documentation land between RC and GA, and from `1.0.0` onward breaking changes follow SemVer with deprecations getting at least one minor cycle. The framework is already in use for production trials.
|
|
513
513
|
|
|
514
514
|
**Release quality bar.** Every release ships with **≥90% line + function coverage and ≥90% branch coverage**, strict TypeScript, OpenSSF Scorecard, CodeQL + Opengrep dual SAST, zizmor workflow linting, and npm provenance. Coverage was relaxed from a former 100% gate so complex security work isn't blocked chasing throwaway tests for unreachable defensive branches or tsx source-map phantoms; see [AGENTS.md](AGENTS.md) for the policy.
|
|
515
515
|
|
|
@@ -520,7 +520,7 @@ DaloyJS is now in the **`1.0.0` beta** (`1.0.0-beta.6`). The public API is featu
|
|
|
520
520
|
- Zero-config OpenAPI `info` autofill from `package.json` (Node / Bun) or `deno.json` / `deno.jsonc` (Deno); explicit `openapi.info` values always win.
|
|
521
521
|
- RFC 7231 + RFC 5789 HTTP-method allowlist enforced inside `app.route()` (WebDAV, `TRACE`, `CONNECT` rejected at the framework boundary).
|
|
522
522
|
- AI-friendly route metadata via optional `meta: { examples, extensions, summary, description, tags }`; examples are validated against your schemas at build time, surfaced as OpenAPI `examples` + `x-daloy-*` extensions, and dumped as `routes.json` / `routes.yaml` via `daloy inspect --ai`.
|
|
523
|
-
- Dependency-free MCP Streamable HTTP server helpers at `@daloyjs/core/mcp`: `createMcpHandler()` exposes tools, resources, and prompts over JSON-RPC 2.0, while `mcpRoutes("/mcp", handler)` mounts the POST / GET / OPTIONS Daloy routes for a dedicated MCP service with the same auth, rate-limit, body-limit, and timeout middleware as any other app.
|
|
523
|
+
- Dependency-free MCP Streamable HTTP server helpers at `@daloyjs/core/mcp`: `createMcpHandler()` exposes tools (with `outputSchema`, `annotations`, and icons), resources, RFC 6570 resource templates, and prompts (with required-argument enforcement) over JSON-RPC 2.0 and validates `Origin` against DNS rebinding (with an `allowedOrigins` allowlist), while `mcpRoutes("/mcp", handler)` mounts the POST / GET / OPTIONS Daloy routes — with the JSON-RPC envelope schema surfaced in OpenAPI — for a dedicated MCP service with the same auth, rate-limit, body-limit, and timeout middleware as any other app.
|
|
524
524
|
- API lifecycle and breaking-change detection: mark routes `deprecated` or give them a `sunset` date to emit RFC 8594 `Deprecation` / `Sunset` headers and an `x-sunset` OpenAPI extension, then gate CI with `diffOpenAPI()` / the `daloy diff` command, which fail on a breaking change versus the last published spec.
|
|
525
525
|
- In-process test client (`app.request()`), contract-test runner (gated in CI via `daloy inspect --check` and shipped as a default test in every `create-daloy` template), in-process typed client, and Hey API codegen via `pnpm gen`.
|
|
526
526
|
|
package/dist/adapters/bun.d.ts
CHANGED
|
@@ -18,7 +18,9 @@ export interface BunTLSOptions {
|
|
|
18
18
|
}
|
|
19
19
|
/** Options forwarded to `Bun.serve` by {@link serve}. */
|
|
20
20
|
export interface BunServeOptions {
|
|
21
|
+
/** TCP port to listen on. Ignored when `unix` is set. Defaults to `3000`. */
|
|
21
22
|
port?: number;
|
|
23
|
+
/** Interface to bind. Ignored when `unix` is set. Defaults to `"0.0.0.0"`. */
|
|
22
24
|
hostname?: string;
|
|
23
25
|
/** Maximum request body bytes (Bun-level cap). Default: 16 MiB. */
|
|
24
26
|
maxRequestBodySize?: number;
|
|
@@ -33,9 +35,19 @@ export interface BunServeOptions {
|
|
|
33
35
|
}
|
|
34
36
|
/** Handle returned by {@link serve} for shutdown and listener introspection. */
|
|
35
37
|
export interface BunServerHandle {
|
|
38
|
+
/** Port the server is actually listening on (as reported by Bun). */
|
|
36
39
|
port: number;
|
|
40
|
+
/** Server URL as reported by `Bun.serve` (e.g. for startup logging), if available. */
|
|
37
41
|
url: URL | undefined;
|
|
42
|
+
/** Graceful stop: drains {@link App.shutdown} hooks first, then force-stops the Bun server. */
|
|
38
43
|
stop: () => Promise<void>;
|
|
39
44
|
}
|
|
40
|
-
/**
|
|
45
|
+
/**
|
|
46
|
+
* Start `Bun.serve` bound to the given {@link App}, wiring HTTP and WebSocket routes.
|
|
47
|
+
*
|
|
48
|
+
* @param app - The DaloyJS {@link App} whose `fetch` (and WebSocket routes) serve requests.
|
|
49
|
+
* @param opts - Listener options forwarded to `Bun.serve`; see {@link BunServeOptions}.
|
|
50
|
+
* @returns A {@link BunServerHandle} exposing the bound `port`, `url`, and a graceful `stop()`.
|
|
51
|
+
* @throws Error when the Bun runtime (`globalThis.Bun.serve`) is not detected.
|
|
52
|
+
*/
|
|
41
53
|
export declare function serve(app: App, opts?: BunServeOptions): BunServerHandle;
|
package/dist/adapters/bun.js
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { WS_READY_STATE, WS_CLOSE_CODE, WS_MAX_CONTROL_PAYLOAD, encodeSendPayload, parseSubprotocols, validateSelectedSubprotocol, checkWebSocketOrigin, WebSocketProtocolError, } from "../websocket.js";
|
|
2
|
-
/**
|
|
2
|
+
/**
|
|
3
|
+
* Start `Bun.serve` bound to the given {@link App}, wiring HTTP and WebSocket routes.
|
|
4
|
+
*
|
|
5
|
+
* @param app - The DaloyJS {@link App} whose `fetch` (and WebSocket routes) serve requests.
|
|
6
|
+
* @param opts - Listener options forwarded to `Bun.serve`; see {@link BunServeOptions}.
|
|
7
|
+
* @returns A {@link BunServerHandle} exposing the bound `port`, `url`, and a graceful `stop()`.
|
|
8
|
+
* @throws Error when the Bun runtime (`globalThis.Bun.serve`) is not detected.
|
|
9
|
+
*/
|
|
3
10
|
export function serve(app, opts = {}) {
|
|
4
11
|
const Bun = globalThis.Bun;
|
|
5
12
|
if (!Bun?.serve)
|
|
@@ -18,12 +18,18 @@
|
|
|
18
18
|
import type { App } from "../app.js";
|
|
19
19
|
/** Module shape expected by the Cloudflare Workers runtime as `export default`. */
|
|
20
20
|
export interface ExportedFetchHandler<Env = unknown> {
|
|
21
|
+
/** Worker entry point: forwards the request to {@link App.fetch}. `env`/`ctx` are accepted but unused by the adapter. */
|
|
21
22
|
fetch: (request: Request, env?: Env, ctx?: ExecutionContextLike) => Promise<Response>;
|
|
22
23
|
}
|
|
23
24
|
interface ExecutionContextLike {
|
|
24
25
|
waitUntil?: (promise: Promise<unknown>) => void;
|
|
25
26
|
passThroughOnException?: () => void;
|
|
26
27
|
}
|
|
27
|
-
/**
|
|
28
|
+
/**
|
|
29
|
+
* Wrap an {@link App} in the `{ fetch }` object expected by Cloudflare Workers and other web-standard hosts.
|
|
30
|
+
*
|
|
31
|
+
* @param app - The DaloyJS {@link App} that serves each incoming request.
|
|
32
|
+
* @returns An {@link ExportedFetchHandler} suitable as the module's `export default`.
|
|
33
|
+
*/
|
|
28
34
|
export declare function toFetchHandler<Env = unknown>(app: App): ExportedFetchHandler<Env>;
|
|
29
35
|
export {};
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* Wrap an {@link App} in the `{ fetch }` object expected by Cloudflare Workers and other web-standard hosts.
|
|
3
|
+
*
|
|
4
|
+
* @param app - The DaloyJS {@link App} that serves each incoming request.
|
|
5
|
+
* @returns An {@link ExportedFetchHandler} suitable as the module's `export default`.
|
|
6
|
+
*/
|
|
2
7
|
export function toFetchHandler(app) {
|
|
3
8
|
return {
|
|
4
9
|
fetch: (req) => app.fetch(req),
|
package/dist/adapters/deno.d.ts
CHANGED
|
@@ -9,7 +9,9 @@
|
|
|
9
9
|
import type { App } from "../app.js";
|
|
10
10
|
/** Options forwarded to `Deno.serve` by {@link serve}. */
|
|
11
11
|
export interface DenoServeOptions {
|
|
12
|
+
/** TCP port to listen on. Defaults to `3000`. */
|
|
12
13
|
port?: number;
|
|
14
|
+
/** Interface to bind. Defaults to `"0.0.0.0"`. */
|
|
13
15
|
hostname?: string;
|
|
14
16
|
/** Optional external signal that triggers graceful shutdown. */
|
|
15
17
|
signal?: AbortSignal;
|
|
@@ -31,7 +33,15 @@ export interface DenoServeOptions {
|
|
|
31
33
|
}
|
|
32
34
|
/** Handle returned by {@link serve}; call `shutdown()` to drain. */
|
|
33
35
|
export interface DenoServerHandle {
|
|
36
|
+
/** Graceful shutdown: drains {@link App.shutdown} hooks, then stops the Deno server. Idempotent. */
|
|
34
37
|
shutdown: () => Promise<void>;
|
|
35
38
|
}
|
|
36
|
-
/**
|
|
39
|
+
/**
|
|
40
|
+
* Start `Deno.serve` bound to the given {@link App} with graceful-shutdown wiring.
|
|
41
|
+
*
|
|
42
|
+
* @param app - The DaloyJS {@link App} whose `fetch` serves each request.
|
|
43
|
+
* @param opts - Listener/TLS/shutdown options; see {@link DenoServeOptions}.
|
|
44
|
+
* @returns A {@link DenoServerHandle} whose `shutdown()` drains in-flight requests.
|
|
45
|
+
* @throws Error when the Deno runtime (`globalThis.Deno.serve`) is not detected.
|
|
46
|
+
*/
|
|
37
47
|
export declare function serve(app: App, opts?: DenoServeOptions): DenoServerHandle;
|
package/dist/adapters/deno.js
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* Start `Deno.serve` bound to the given {@link App} with graceful-shutdown wiring.
|
|
3
|
+
*
|
|
4
|
+
* @param app - The DaloyJS {@link App} whose `fetch` serves each request.
|
|
5
|
+
* @param opts - Listener/TLS/shutdown options; see {@link DenoServeOptions}.
|
|
6
|
+
* @returns A {@link DenoServerHandle} whose `shutdown()` drains in-flight requests.
|
|
7
|
+
* @throws Error when the Deno runtime (`globalThis.Deno.serve`) is not detected.
|
|
8
|
+
*/
|
|
2
9
|
export function serve(app, opts = {}) {
|
|
3
10
|
const D = globalThis.Deno;
|
|
4
11
|
const denoServe = D?.serve;
|
|
@@ -18,7 +18,17 @@
|
|
|
18
18
|
* multipart helpers that rely on `node:stream`).
|
|
19
19
|
*/
|
|
20
20
|
import type { App } from "../app.js";
|
|
21
|
-
/**
|
|
21
|
+
/**
|
|
22
|
+
* Wrap an {@link App} in a `(req) => Promise<Response>` function suitable for Fastly Compute.
|
|
23
|
+
*
|
|
24
|
+
* @param app - The DaloyJS {@link App} that serves each incoming request.
|
|
25
|
+
* @returns A web-standard handler delegating to {@link App.fetch}.
|
|
26
|
+
*/
|
|
22
27
|
export declare function toFastlyHandler(app: App): (req: Request) => Promise<Response>;
|
|
23
|
-
/**
|
|
28
|
+
/**
|
|
29
|
+
* Register a Fastly Compute `fetch` event listener that delegates to the given {@link App}.
|
|
30
|
+
*
|
|
31
|
+
* @param app - The DaloyJS {@link App} that serves each incoming request.
|
|
32
|
+
* @throws Error when `globalThis.addEventListener` is missing (not a Fastly Compute runtime).
|
|
33
|
+
*/
|
|
24
34
|
export declare function installFastlyListener(app: App): void;
|
package/dist/adapters/fastly.js
CHANGED
|
@@ -1,8 +1,18 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* Wrap an {@link App} in a `(req) => Promise<Response>` function suitable for Fastly Compute.
|
|
3
|
+
*
|
|
4
|
+
* @param app - The DaloyJS {@link App} that serves each incoming request.
|
|
5
|
+
* @returns A web-standard handler delegating to {@link App.fetch}.
|
|
6
|
+
*/
|
|
2
7
|
export function toFastlyHandler(app) {
|
|
3
8
|
return (req) => app.fetch(req);
|
|
4
9
|
}
|
|
5
|
-
/**
|
|
10
|
+
/**
|
|
11
|
+
* Register a Fastly Compute `fetch` event listener that delegates to the given {@link App}.
|
|
12
|
+
*
|
|
13
|
+
* @param app - The DaloyJS {@link App} that serves each incoming request.
|
|
14
|
+
* @throws Error when `globalThis.addEventListener` is missing (not a Fastly Compute runtime).
|
|
15
|
+
*/
|
|
6
16
|
export function installFastlyListener(app) {
|
|
7
17
|
const g = globalThis;
|
|
8
18
|
if (typeof g.addEventListener !== "function") {
|
|
@@ -13,28 +13,45 @@ import type { App } from "../app.js";
|
|
|
13
13
|
* The adapter performs no Node-only I/O, so it is safe in any runtime that
|
|
14
14
|
* provides the standard `Request`/`Response`/`atob`/`btoa` globals.
|
|
15
15
|
*/
|
|
16
|
+
/** API Gateway REST API event (payload format v1.0). */
|
|
16
17
|
export interface LambdaEventV1 {
|
|
18
|
+
/** Payload format version; `"1.0"` or absent for REST API events. */
|
|
17
19
|
version?: "1.0" | string;
|
|
20
|
+
/** URL path of the request (e.g. `/users/42`). */
|
|
18
21
|
path?: string;
|
|
22
|
+
/** HTTP method (e.g. `GET`). */
|
|
19
23
|
httpMethod?: string;
|
|
24
|
+
/** Request headers, one value per name (last value wins in v1.0). */
|
|
20
25
|
headers?: Record<string, string | undefined>;
|
|
26
|
+
/** Request headers with every value per name; `cookie` values are re-joined with `; `. */
|
|
21
27
|
multiValueHeaders?: Record<string, string[] | undefined>;
|
|
28
|
+
/** Query parameters, one value per name. Used only when the multi-value map is empty. */
|
|
22
29
|
queryStringParameters?: Record<string, string | undefined> | null;
|
|
30
|
+
/** Query parameters with every value per name; preferred over the single-value map. */
|
|
23
31
|
multiValueQueryStringParameters?: Record<string, string[] | undefined> | null;
|
|
32
|
+
/** Request context; `domainName` is the host fallback and `path` the path fallback. */
|
|
24
33
|
requestContext?: {
|
|
25
34
|
domainName?: string;
|
|
26
35
|
path?: string;
|
|
27
36
|
};
|
|
37
|
+
/** Raw request body; base64-encoded when {@link LambdaEventV1.isBase64Encoded} is true. */
|
|
28
38
|
body?: string;
|
|
39
|
+
/** True when `body` is base64-encoded (binary payloads). */
|
|
29
40
|
isBase64Encoded?: boolean;
|
|
30
41
|
}
|
|
31
42
|
/** API Gateway HTTP API or Lambda Function URL event (payload format v2.0). */
|
|
32
43
|
export interface LambdaEventV2 {
|
|
44
|
+
/** Payload format version; `"2.0"` for HTTP API / Function URL events. */
|
|
33
45
|
version?: string;
|
|
46
|
+
/** URL path of the request, without the query string. */
|
|
34
47
|
rawPath?: string;
|
|
48
|
+
/** Raw query string without the leading `?` (empty string when none). */
|
|
35
49
|
rawQueryString?: string;
|
|
50
|
+
/** Request headers; multi-value headers arrive comma-joined in v2.0. */
|
|
36
51
|
headers?: Record<string, string | undefined>;
|
|
52
|
+
/** Request cookies as individual strings; re-joined with `; ` into a `cookie` header. */
|
|
37
53
|
cookies?: string[];
|
|
54
|
+
/** Request context; `http.method`/`http.path` carry the method and path, `domainName` the host fallback. */
|
|
38
55
|
requestContext?: {
|
|
39
56
|
http?: {
|
|
40
57
|
method?: string;
|
|
@@ -42,32 +59,51 @@ export interface LambdaEventV2 {
|
|
|
42
59
|
};
|
|
43
60
|
domainName?: string;
|
|
44
61
|
};
|
|
62
|
+
/** Raw request body; base64-encoded when {@link LambdaEventV2.isBase64Encoded} is true. */
|
|
45
63
|
body?: string;
|
|
64
|
+
/** True when `body` is base64-encoded (binary payloads). */
|
|
46
65
|
isBase64Encoded?: boolean;
|
|
47
66
|
}
|
|
48
67
|
/** Either payload format accepted by {@link toLambdaHandler}. */
|
|
49
68
|
export type LambdaEvent = LambdaEventV1 | LambdaEventV2;
|
|
50
69
|
/** Lambda response shape required by API Gateway REST API (payload format v1.0). */
|
|
51
70
|
export interface LambdaResponseV1 {
|
|
71
|
+
/** HTTP status code of the response. */
|
|
52
72
|
statusCode: number;
|
|
73
|
+
/** Response headers, one value per name (`set-cookie` excluded; see `multiValueHeaders`). */
|
|
53
74
|
headers: Record<string, string>;
|
|
75
|
+
/** Multi-value headers; used to carry each `set-cookie` value separately in v1.0. */
|
|
54
76
|
multiValueHeaders?: Record<string, string[]>;
|
|
77
|
+
/** Never present in v1.0 responses; cookies travel via `multiValueHeaders`. */
|
|
55
78
|
cookies?: never;
|
|
79
|
+
/** Response body; base64-encoded when {@link LambdaResponseV1.isBase64Encoded} is true. */
|
|
56
80
|
body: string;
|
|
81
|
+
/** True when `body` is base64-encoded (non-text content types). */
|
|
57
82
|
isBase64Encoded: boolean;
|
|
58
83
|
}
|
|
59
84
|
/** Lambda response shape required by API Gateway HTTP API and Function URLs (payload format v2.0). */
|
|
60
85
|
export interface LambdaResponseV2 {
|
|
86
|
+
/** HTTP status code of the response. */
|
|
61
87
|
statusCode: number;
|
|
88
|
+
/** Response headers, one value per name (`set-cookie` excluded; see `cookies`). */
|
|
62
89
|
headers: Record<string, string>;
|
|
90
|
+
/** Response cookies, one `set-cookie` value per entry (v2.0's cookie channel). */
|
|
63
91
|
cookies?: string[];
|
|
92
|
+
/** Never present in v2.0 responses; cookies travel via `cookies`. */
|
|
64
93
|
multiValueHeaders?: never;
|
|
94
|
+
/** Response body; base64-encoded when {@link LambdaResponseV2.isBase64Encoded} is true. */
|
|
65
95
|
body: string;
|
|
96
|
+
/** True when `body` is base64-encoded (non-text content types). */
|
|
66
97
|
isBase64Encoded: boolean;
|
|
67
98
|
}
|
|
68
99
|
/** Either response shape produced by {@link toLambdaHandler}, chosen automatically per event. */
|
|
69
100
|
export type LambdaResponse = LambdaResponseV1 | LambdaResponseV2;
|
|
70
101
|
/** Async handler shape consumed by AWS Lambda / Netlify Functions runtimes. */
|
|
71
102
|
export type LambdaHandler = (event: LambdaEvent) => Promise<LambdaResponse>;
|
|
72
|
-
/**
|
|
103
|
+
/**
|
|
104
|
+
* Wrap an {@link App} as a Lambda/Netlify handler accepting either v1.0 or v2.0 event payloads.
|
|
105
|
+
*
|
|
106
|
+
* @param app - The DaloyJS {@link App} that serves each translated request.
|
|
107
|
+
* @returns A {@link LambdaHandler} that converts the event to a `Request`, calls {@link App.fetch}, and emits the matching v1.0/v2.0 response shape.
|
|
108
|
+
*/
|
|
73
109
|
export declare function toLambdaHandler(app: App): LambdaHandler;
|
package/dist/adapters/lambda.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
const TEXT_TYPE_RE = /^(text\/|application\/(json|xml|javascript|x-www-form-urlencoded|.*\+json|.*\+xml))/i;
|
|
2
|
-
/**
|
|
2
|
+
/**
|
|
3
|
+
* Wrap an {@link App} as a Lambda/Netlify handler accepting either v1.0 or v2.0 event payloads.
|
|
4
|
+
*
|
|
5
|
+
* @param app - The DaloyJS {@link App} that serves each translated request.
|
|
6
|
+
* @returns A {@link LambdaHandler} that converts the event to a `Request`, calls {@link App.fetch}, and emits the matching v1.0/v2.0 response shape.
|
|
7
|
+
*/
|
|
3
8
|
export function toLambdaHandler(app) {
|
|
4
9
|
return async (event) => {
|
|
5
10
|
const request = eventToRequest(event);
|
package/dist/adapters/node.d.ts
CHANGED
|
@@ -6,7 +6,9 @@ import { type Server } from "node:http";
|
|
|
6
6
|
import type { App } from "../app.js";
|
|
7
7
|
/** Options for the Node.js {@link serve} entry point. */
|
|
8
8
|
export interface NodeServerOptions {
|
|
9
|
+
/** TCP port to listen on. Defaults to `3000`. */
|
|
9
10
|
port?: number;
|
|
11
|
+
/** Interface to bind. Defaults to `"0.0.0.0"`. */
|
|
10
12
|
hostname?: string;
|
|
11
13
|
/**
|
|
12
14
|
* Connection-level timeout in ms, applied to BOTH `headersTimeout` and
|
|
@@ -72,9 +74,18 @@ export interface NodeServerOptions {
|
|
|
72
74
|
}
|
|
73
75
|
/** Handle returned by {@link serve} exposing the underlying Node `Server` plus a `close()` for graceful shutdown. */
|
|
74
76
|
export interface NodeServerHandle {
|
|
77
|
+
/** The underlying `node:http` `Server` instance, for advanced wiring (extra listeners, address introspection). */
|
|
75
78
|
server: Server;
|
|
79
|
+
/** Port the server was asked to listen on ({@link NodeServerOptions.port}, default `3000`). */
|
|
76
80
|
port: number;
|
|
81
|
+
/** Graceful shutdown: drains {@link App.shutdown} hooks, destroys WebSocket sockets, then closes the server. Idempotent. */
|
|
77
82
|
close(): Promise<void>;
|
|
78
83
|
}
|
|
79
|
-
/**
|
|
84
|
+
/**
|
|
85
|
+
* Start a Node.js HTTP (and optional WebSocket) server bound to the given {@link App}.
|
|
86
|
+
*
|
|
87
|
+
* @param app - The DaloyJS {@link App} whose `fetch` (and WebSocket routes) serve requests.
|
|
88
|
+
* @param opts - Listener, timeout, proxy-trust, and hardening options; see {@link NodeServerOptions}.
|
|
89
|
+
* @returns A {@link NodeServerHandle} exposing the Node `Server`, the `port`, and a graceful `close()`.
|
|
90
|
+
*/
|
|
80
91
|
export declare function serve(app: App, opts?: NodeServerOptions): NodeServerHandle;
|
package/dist/adapters/node.js
CHANGED
|
@@ -4,10 +4,16 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { createServer, } from "node:http";
|
|
6
6
|
import { Readable } from "node:stream";
|
|
7
|
-
import { DALOY_RAW_BODY, DALOY_RAW_STREAM, DALOY_REQUEST_RAW_BODY } from "../app.js";
|
|
7
|
+
import { DALOY_RAW_BODY, DALOY_RAW_STREAM, DALOY_REQUEST_RAW_BODY, DALOY_LIGHT_RESPONSE_OK, } from "../app.js";
|
|
8
8
|
import { setClientCertificate, normalizePeerCertificate, } from "../mtls.js";
|
|
9
9
|
import { FrameSink, encodeFrame, encodeClosePayload, encodeSendPayload, validateUpgrade, validateSelectedSubprotocol, checkWebSocketOrigin, WS_OPCODE, WS_CLOSE_CODE, WS_READY_STATE, WS_MAX_CONTROL_PAYLOAD, WebSocketProtocolError, WebSocketPayloadTooLargeError, } from "../websocket.js";
|
|
10
|
-
/**
|
|
10
|
+
/**
|
|
11
|
+
* Start a Node.js HTTP (and optional WebSocket) server bound to the given {@link App}.
|
|
12
|
+
*
|
|
13
|
+
* @param app - The DaloyJS {@link App} whose `fetch` (and WebSocket routes) serve requests.
|
|
14
|
+
* @param opts - Listener, timeout, proxy-trust, and hardening options; see {@link NodeServerOptions}.
|
|
15
|
+
* @returns A {@link NodeServerHandle} exposing the Node `Server`, the `port`, and a graceful `close()`.
|
|
16
|
+
*/
|
|
11
17
|
export function serve(app, opts = {}) {
|
|
12
18
|
const trustProxy = opts.trustProxy === true;
|
|
13
19
|
const bufferedBodyMaxBytes = typeof opts.bufferedBodyMaxBytes === "number" && opts.bufferedBodyMaxBytes >= 0
|
|
@@ -285,6 +291,209 @@ function writeAdapterError(res, e) {
|
|
|
285
291
|
res.destroy(e);
|
|
286
292
|
}
|
|
287
293
|
}
|
|
294
|
+
/**
|
|
295
|
+
* Lazily-materializing stand-in for an incoming WHATWG `Request`.
|
|
296
|
+
*
|
|
297
|
+
* Constructing a real (undici) `Request` costs ~4µs for GET and far more for
|
|
298
|
+
* POST-with-body (the constructor wraps the body bytes in a WHATWG
|
|
299
|
+
* `ReadableStream` that DaloyJS then bypasses anyway via the
|
|
300
|
+
* `DALOY_REQUEST_RAW_BODY` fast path). The dispatch hot path only ever reads
|
|
301
|
+
* `url`, `method`, `headers`, `signal`, and the raw-body symbol — so this
|
|
302
|
+
* shell carries those directly and defers the real `Request` until one of the
|
|
303
|
+
* less common surfaces (`json()`, `clone()`, `body`, `formData()`, …) is
|
|
304
|
+
* actually touched.
|
|
305
|
+
*
|
|
306
|
+
* Fidelity notes:
|
|
307
|
+
* - `instanceof Request` holds (prototype chain is re-rooted onto
|
|
308
|
+
* `Request.prototype`), and every WHATWG method/getter is overridden here,
|
|
309
|
+
* so nothing hits undici's brand-checked prototype accessors.
|
|
310
|
+
* - `signal` is an inert per-instance `AbortSignal`. This matches the real
|
|
311
|
+
* adapter behaviour today: the Node adapter never wires socket aborts into
|
|
312
|
+
* the request signal, so the signal never fires in either implementation.
|
|
313
|
+
* - Passing this object directly to `fetch()` is not supported (undici
|
|
314
|
+
* brand-checks its input) — forward with `request.clone()` instead, which
|
|
315
|
+
* returns a real `Request`. This mirrors @hono/node-server's shim.
|
|
316
|
+
*
|
|
317
|
+
* Security parity: the `Headers` instance is built eagerly from `rawHeaders`
|
|
318
|
+
* exactly as before, so the duplicate-singleton / reserved-header /
|
|
319
|
+
* header-count guards in `App.dispatch` see the identical view they saw with
|
|
320
|
+
* a real `Request`.
|
|
321
|
+
*/
|
|
322
|
+
/** Shared decoder for LightRequest's direct body reads. */
|
|
323
|
+
const LIGHT_TEXT_DECODER = new TextDecoder();
|
|
324
|
+
class LightRequest {
|
|
325
|
+
#url;
|
|
326
|
+
#method;
|
|
327
|
+
#headers;
|
|
328
|
+
#bodyBytes;
|
|
329
|
+
#real;
|
|
330
|
+
#signal;
|
|
331
|
+
constructor(url, method, headers, bodyBytes) {
|
|
332
|
+
this.#url = url;
|
|
333
|
+
this.#method = method;
|
|
334
|
+
this.#headers = headers;
|
|
335
|
+
this.#bodyBytes = bodyBytes;
|
|
336
|
+
}
|
|
337
|
+
/** Build (once) and return the real undici `Request` for rare surfaces. */
|
|
338
|
+
#materialize() {
|
|
339
|
+
return (this.#real ??=
|
|
340
|
+
this.#bodyBytes !== undefined
|
|
341
|
+
? new Request(this.#url, {
|
|
342
|
+
method: this.#method,
|
|
343
|
+
headers: this.#headers,
|
|
344
|
+
body: this.#bodyBytes,
|
|
345
|
+
})
|
|
346
|
+
: new Request(this.#url, { method: this.#method, headers: this.#headers }));
|
|
347
|
+
}
|
|
348
|
+
get url() {
|
|
349
|
+
return this.#url;
|
|
350
|
+
}
|
|
351
|
+
get method() {
|
|
352
|
+
return this.#method;
|
|
353
|
+
}
|
|
354
|
+
get headers() {
|
|
355
|
+
return this.#headers;
|
|
356
|
+
}
|
|
357
|
+
get signal() {
|
|
358
|
+
// Inert, lazily created: the Node adapter has never wired socket
|
|
359
|
+
// teardown into the request signal, so a never-firing signal is
|
|
360
|
+
// behaviourally identical to the one a real `Request` would carry.
|
|
361
|
+
return (this.#signal ??= new AbortController().signal);
|
|
362
|
+
}
|
|
363
|
+
get body() {
|
|
364
|
+
return this.#materialize().body;
|
|
365
|
+
}
|
|
366
|
+
get bodyUsed() {
|
|
367
|
+
if (this.#real !== undefined)
|
|
368
|
+
return this.#real.bodyUsed;
|
|
369
|
+
return this.#directlyConsumed;
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Set when a body method served the pre-buffered bytes directly (no real
|
|
373
|
+
* `Request` ever existed). Subsequent body reads reject with a `TypeError`
|
|
374
|
+
* exactly like a consumed WHATWG body would.
|
|
375
|
+
*/
|
|
376
|
+
#directlyConsumed = false;
|
|
377
|
+
/**
|
|
378
|
+
* Serve a body read straight from the pre-buffered bytes when possible.
|
|
379
|
+
* Returns `undefined` when the caller must delegate to the materialized
|
|
380
|
+
* real `Request` (no buffered bytes, or a real `Request` already owns the
|
|
381
|
+
* body state). Enforces single-read semantics via {@link #directlyConsumed}.
|
|
382
|
+
*/
|
|
383
|
+
#consumeBytes() {
|
|
384
|
+
if (this.#real !== undefined || this.#bodyBytes === undefined)
|
|
385
|
+
return undefined;
|
|
386
|
+
if (this.#directlyConsumed) {
|
|
387
|
+
throw new TypeError("Body is unusable: Body has already been read");
|
|
388
|
+
}
|
|
389
|
+
this.#directlyConsumed = true;
|
|
390
|
+
return this.#bodyBytes;
|
|
391
|
+
}
|
|
392
|
+
// Spec-constant getters: these are exactly the values undici assigns to a
|
|
393
|
+
// server-side `new Request(url, { method, headers, body })`, hardcoded so
|
|
394
|
+
// reading them does not force materialization.
|
|
395
|
+
get cache() {
|
|
396
|
+
return "default";
|
|
397
|
+
}
|
|
398
|
+
get credentials() {
|
|
399
|
+
return "same-origin";
|
|
400
|
+
}
|
|
401
|
+
get destination() {
|
|
402
|
+
return "";
|
|
403
|
+
}
|
|
404
|
+
get integrity() {
|
|
405
|
+
return "";
|
|
406
|
+
}
|
|
407
|
+
get keepalive() {
|
|
408
|
+
return false;
|
|
409
|
+
}
|
|
410
|
+
get mode() {
|
|
411
|
+
return "cors";
|
|
412
|
+
}
|
|
413
|
+
get redirect() {
|
|
414
|
+
return "follow";
|
|
415
|
+
}
|
|
416
|
+
get referrer() {
|
|
417
|
+
return "about:client";
|
|
418
|
+
}
|
|
419
|
+
get referrerPolicy() {
|
|
420
|
+
return "";
|
|
421
|
+
}
|
|
422
|
+
arrayBuffer() {
|
|
423
|
+
try {
|
|
424
|
+
const bytes = this.#consumeBytes();
|
|
425
|
+
if (bytes === undefined)
|
|
426
|
+
return this.#materialize().arrayBuffer();
|
|
427
|
+
// Copy: the underlying buffer is also the framework's raw-body cache.
|
|
428
|
+
return Promise.resolve(bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength));
|
|
429
|
+
}
|
|
430
|
+
catch (e) {
|
|
431
|
+
return Promise.reject(e);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
blob() {
|
|
435
|
+
return this.#materialize().blob();
|
|
436
|
+
}
|
|
437
|
+
bytes() {
|
|
438
|
+
try {
|
|
439
|
+
const bytes = this.#consumeBytes();
|
|
440
|
+
if (bytes === undefined)
|
|
441
|
+
return this.#materialize().bytes();
|
|
442
|
+
return Promise.resolve(new Uint8Array(bytes));
|
|
443
|
+
}
|
|
444
|
+
catch (e) {
|
|
445
|
+
return Promise.reject(e);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
formData() {
|
|
449
|
+
return this.#materialize().formData();
|
|
450
|
+
}
|
|
451
|
+
json() {
|
|
452
|
+
try {
|
|
453
|
+
const bytes = this.#consumeBytes();
|
|
454
|
+
if (bytes === undefined)
|
|
455
|
+
return this.#materialize().json();
|
|
456
|
+
// JSON.parse (not the framework's safeJsonParse): request.json() is the
|
|
457
|
+
// raw WHATWG surface, and its error semantics (SyntaxError rejection)
|
|
458
|
+
// must match a real Request exactly. Framework-parsed bodies go through
|
|
459
|
+
// readBody()/safeJsonParse and never hit this method.
|
|
460
|
+
return Promise.resolve(JSON.parse(LIGHT_TEXT_DECODER.decode(bytes)));
|
|
461
|
+
}
|
|
462
|
+
catch (e) {
|
|
463
|
+
return Promise.reject(e);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
text() {
|
|
467
|
+
try {
|
|
468
|
+
const bytes = this.#consumeBytes();
|
|
469
|
+
if (bytes === undefined)
|
|
470
|
+
return this.#materialize().text();
|
|
471
|
+
return Promise.resolve(LIGHT_TEXT_DECODER.decode(bytes));
|
|
472
|
+
}
|
|
473
|
+
catch (e) {
|
|
474
|
+
return Promise.reject(e);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* Returns a real, fully-branded `Request` clone — safe to pass to `fetch()`.
|
|
479
|
+
* Throws a `TypeError` if the body has already been read, per spec.
|
|
480
|
+
*/
|
|
481
|
+
clone() {
|
|
482
|
+
if (this.#directlyConsumed) {
|
|
483
|
+
throw new TypeError("Request body is already used");
|
|
484
|
+
}
|
|
485
|
+
return this.#materialize().clone();
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
// `instanceof Request` must hold for handler code and the framework's own
|
|
489
|
+
// checks. Every own getter/method above shadows the brand-checked undici
|
|
490
|
+
// accessors, so the re-rooted chain is only consulted for identity.
|
|
491
|
+
Object.setPrototypeOf(LightRequest.prototype, Request.prototype);
|
|
492
|
+
// This adapter consumes responses via status/headers/DALOY_RAW_BODY only
|
|
493
|
+
// (see sendWebResponse), so serializeResult may skip the undici Response
|
|
494
|
+
// construction for requests dispatched through this shim. Set once on the
|
|
495
|
+
// prototype: zero per-request cost.
|
|
496
|
+
LightRequest.prototype[DALOY_LIGHT_RESPONSE_OK] = true;
|
|
288
497
|
function toWebRequest(req, trustProxy, bufferedBody) {
|
|
289
498
|
const reqHeaders = req.headers;
|
|
290
499
|
const forwardedHost = trustProxy
|
|
@@ -317,18 +526,20 @@ function toWebRequest(req, trustProxy, bufferedBody) {
|
|
|
317
526
|
const headers = new Headers(headerPairs);
|
|
318
527
|
const method = req.method ?? "GET";
|
|
319
528
|
if (method === "GET" || method === "HEAD") {
|
|
320
|
-
|
|
529
|
+
// LightRequest: skips the ~4µs undici Request constructor on the GET
|
|
530
|
+
// hot path. Headers are still built eagerly above, so every header
|
|
531
|
+
// guard sees the same view as before.
|
|
532
|
+
return new LightRequest(url, method, headers, undefined);
|
|
321
533
|
}
|
|
322
534
|
if (bufferedBody !== undefined) {
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
//
|
|
329
|
-
//
|
|
330
|
-
|
|
331
|
-
// here; readBodyLimited re-checks against the caller's limit.
|
|
535
|
+
// LightRequest with the pre-buffered bytes: skips the (much more
|
|
536
|
+
// expensive) body-wrapping undici Request constructor. The bytes are
|
|
537
|
+
// stashed via DALOY_REQUEST_RAW_BODY so readBodyLimited (and any other
|
|
538
|
+
// internal body reader) skips the WHATWG ReadableStream reader loop.
|
|
539
|
+
// The adapter has already enforced BUFFERED_BODY_MAX_BYTES +
|
|
540
|
+
// Content-Length here; readBodyLimited re-checks against the caller's
|
|
541
|
+
// limit.
|
|
542
|
+
const req2 = new LightRequest(url, method, headers, bufferedBody);
|
|
332
543
|
req2[DALOY_REQUEST_RAW_BODY] = bufferedBody;
|
|
333
544
|
return req2;
|
|
334
545
|
}
|
|
@@ -23,16 +23,25 @@ import type { App } from "../app.js";
|
|
|
23
23
|
export type WebHandler = (req: Request) => Promise<Response>;
|
|
24
24
|
/** Default export shape for Vercel's web-standard `{ fetch }` runtime. */
|
|
25
25
|
export interface FetchHandler {
|
|
26
|
+
/** Request entry point: forwards the request to {@link App.fetch}. */
|
|
26
27
|
fetch: WebHandler;
|
|
27
28
|
}
|
|
28
29
|
declare const NEXT_METHODS: readonly ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"];
|
|
29
30
|
/** Record of per-method handlers expected by a Next.js App Router `route.ts` file. */
|
|
30
31
|
export type RouteHandlers = Record<(typeof NEXT_METHODS)[number], WebHandler>;
|
|
31
|
-
/**
|
|
32
|
+
/**
|
|
33
|
+
* Wrap an {@link App} as a single web-standard fetch handler.
|
|
34
|
+
*
|
|
35
|
+
* @param app - The DaloyJS {@link App} that serves each incoming request.
|
|
36
|
+
* @returns A {@link WebHandler} delegating to {@link App.fetch}.
|
|
37
|
+
*/
|
|
32
38
|
export declare function toWebHandler(app: App): WebHandler;
|
|
33
39
|
/**
|
|
34
40
|
* Build the default `{ fetch }` export expected by Vercel Node.js Functions
|
|
35
41
|
* in the `/api` directory.
|
|
42
|
+
*
|
|
43
|
+
* @param app - The DaloyJS {@link App} that serves each incoming request.
|
|
44
|
+
* @returns A {@link FetchHandler} object suitable as the module's `export default`.
|
|
36
45
|
*/
|
|
37
46
|
export declare function toFetchHandler(app: App): FetchHandler;
|
|
38
47
|
/** Backward-compatible alias for {@link toWebHandler}. */
|
|
@@ -40,6 +49,9 @@ export declare const toEdgeHandler: typeof toWebHandler;
|
|
|
40
49
|
/**
|
|
41
50
|
* Build the `{ GET, POST, ... }` object expected by Next.js App Router
|
|
42
51
|
* `route.ts` files when a DaloyJS app is mounted inside an existing Next app.
|
|
52
|
+
*
|
|
53
|
+
* @param app - The DaloyJS {@link App} that serves each incoming request.
|
|
54
|
+
* @returns A {@link RouteHandlers} record mapping every supported HTTP method to the same {@link WebHandler}.
|
|
43
55
|
*/
|
|
44
56
|
export declare function toRouteHandlers(app: App): RouteHandlers;
|
|
45
57
|
export {};
|
package/dist/adapters/vercel.js
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
const NEXT_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"];
|
|
2
|
-
/**
|
|
2
|
+
/**
|
|
3
|
+
* Wrap an {@link App} as a single web-standard fetch handler.
|
|
4
|
+
*
|
|
5
|
+
* @param app - The DaloyJS {@link App} that serves each incoming request.
|
|
6
|
+
* @returns A {@link WebHandler} delegating to {@link App.fetch}.
|
|
7
|
+
*/
|
|
3
8
|
export function toWebHandler(app) {
|
|
4
9
|
return (req) => app.fetch(req);
|
|
5
10
|
}
|
|
6
11
|
/**
|
|
7
12
|
* Build the default `{ fetch }` export expected by Vercel Node.js Functions
|
|
8
13
|
* in the `/api` directory.
|
|
14
|
+
*
|
|
15
|
+
* @param app - The DaloyJS {@link App} that serves each incoming request.
|
|
16
|
+
* @returns A {@link FetchHandler} object suitable as the module's `export default`.
|
|
9
17
|
*/
|
|
10
18
|
export function toFetchHandler(app) {
|
|
11
19
|
return { fetch: toWebHandler(app) };
|
|
@@ -15,6 +23,9 @@ export const toEdgeHandler = toWebHandler;
|
|
|
15
23
|
/**
|
|
16
24
|
* Build the `{ GET, POST, ... }` object expected by Next.js App Router
|
|
17
25
|
* `route.ts` files when a DaloyJS app is mounted inside an existing Next app.
|
|
26
|
+
*
|
|
27
|
+
* @param app - The DaloyJS {@link App} that serves each incoming request.
|
|
28
|
+
* @returns A {@link RouteHandlers} record mapping every supported HTTP method to the same {@link WebHandler}.
|
|
18
29
|
*/
|
|
19
30
|
export function toRouteHandlers(app) {
|
|
20
31
|
const handler = toWebHandler(app);
|