@usetoki/toki 0.1.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 +177 -0
- package/dist/app.d.ts +137 -0
- package/dist/app.js +608 -0
- package/dist/app.js.map +1 -0
- package/dist/cookies.d.ts +17 -0
- package/dist/cookies.js +54 -0
- package/dist/cookies.js.map +1 -0
- package/dist/forms.d.ts +13 -0
- package/dist/forms.js +64 -0
- package/dist/forms.js.map +1 -0
- package/dist/group.d.ts +19 -0
- package/dist/group.js +51 -0
- package/dist/group.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/inject.d.ts +19 -0
- package/dist/inject.js +56 -0
- package/dist/inject.js.map +1 -0
- package/dist/jwt.d.ts +57 -0
- package/dist/jwt.js +128 -0
- package/dist/jwt.js.map +1 -0
- package/dist/logger.d.ts +7 -0
- package/dist/logger.js +45 -0
- package/dist/logger.js.map +1 -0
- package/dist/middleware.d.ts +38 -0
- package/dist/middleware.js +133 -0
- package/dist/middleware.js.map +1 -0
- package/dist/native.d.ts +81 -0
- package/dist/native.js +38 -0
- package/dist/native.js.map +1 -0
- package/dist/pipeline.d.ts +16 -0
- package/dist/pipeline.js +125 -0
- package/dist/pipeline.js.map +1 -0
- package/dist/request.d.ts +65 -0
- package/dist/request.js +170 -0
- package/dist/request.js.map +1 -0
- package/dist/response.d.ts +33 -0
- package/dist/response.js +92 -0
- package/dist/response.js.map +1 -0
- package/dist/schema.d.ts +45 -0
- package/dist/schema.js +179 -0
- package/dist/schema.js.map +1 -0
- package/dist/static.d.ts +20 -0
- package/dist/static.js +105 -0
- package/dist/static.js.map +1 -0
- package/dist/types.d.ts +88 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +63 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { brotliCompress, constants, gzip } from "node:zlib";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import { rawResponse, reply } from "./response.js";
|
|
4
|
+
const gzipAsync = promisify(gzip);
|
|
5
|
+
const brotliAsync = promisify(brotliCompress);
|
|
6
|
+
// text + text-like only; compressing already-compressed bytes wastes cpu
|
|
7
|
+
function isCompressible(contentType) {
|
|
8
|
+
return /^text\/|^application\/(json|javascript|xml|wasm|.*\+json|.*\+xml)|^image\/svg/.test(contentType);
|
|
9
|
+
}
|
|
10
|
+
// coding allowed when its token (or `*`) is present with q != 0 — mirrors the
|
|
11
|
+
// native static negotiator so dynamic responses honor `Accept-Encoding: br;q=0`.
|
|
12
|
+
function acceptsEncoding(accept, coding) {
|
|
13
|
+
for (const part of accept.split(",")) {
|
|
14
|
+
const segment = part.trim();
|
|
15
|
+
const semi = segment.indexOf(";");
|
|
16
|
+
const name = (semi === -1 ? segment : segment.slice(0, semi)).trim().toLowerCase();
|
|
17
|
+
if (name !== coding && name !== "*") {
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
if (semi !== -1) {
|
|
21
|
+
const q = /q=([\d.]+)/.exec(segment.slice(semi + 1));
|
|
22
|
+
if (q !== null && Number(q[1]) === 0) {
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
const DEFAULT_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"];
|
|
31
|
+
// returns the ACAO value, or null to omit the header entirely
|
|
32
|
+
function resolveOrigin(requestOrigin, options) {
|
|
33
|
+
const allowed = options.origin ?? "*";
|
|
34
|
+
if (allowed === "*") {
|
|
35
|
+
return "*";
|
|
36
|
+
}
|
|
37
|
+
if (requestOrigin === null) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
if (typeof allowed === "string") {
|
|
41
|
+
return allowed;
|
|
42
|
+
}
|
|
43
|
+
if (Array.isArray(allowed)) {
|
|
44
|
+
return allowed.includes(requestOrigin) ? requestOrigin : null;
|
|
45
|
+
}
|
|
46
|
+
return allowed(requestOrigin) ? requestOrigin : null;
|
|
47
|
+
}
|
|
48
|
+
/** CORS headers for actual (non-preflight) requests. */
|
|
49
|
+
export function corsHeaders(options = {}) {
|
|
50
|
+
return (req) => {
|
|
51
|
+
const origin = resolveOrigin(req.headers.get("origin"), options);
|
|
52
|
+
if (origin !== null) {
|
|
53
|
+
req.setResponseHeader("Access-Control-Allow-Origin", origin);
|
|
54
|
+
if (origin !== "*") {
|
|
55
|
+
// reflected origin varies per request; caches must key on it
|
|
56
|
+
req.appendResponseHeader("Vary", "Origin");
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (options.credentials) {
|
|
60
|
+
req.setResponseHeader("Access-Control-Allow-Credentials", "true");
|
|
61
|
+
}
|
|
62
|
+
if (options.exposedHeaders?.length) {
|
|
63
|
+
req.setResponseHeader("Access-Control-Expose-Headers", options.exposedHeaders.join(", "));
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/** Answers a CORS preflight `OPTIONS` with `204`. */
|
|
68
|
+
export function corsPreflight(options = {}) {
|
|
69
|
+
return (req) => {
|
|
70
|
+
const origin = resolveOrigin(req.headers.get("origin"), options);
|
|
71
|
+
if (origin !== null) {
|
|
72
|
+
req.setResponseHeader("Access-Control-Allow-Origin", origin);
|
|
73
|
+
}
|
|
74
|
+
if (options.credentials) {
|
|
75
|
+
req.setResponseHeader("Access-Control-Allow-Credentials", "true");
|
|
76
|
+
}
|
|
77
|
+
req.setResponseHeader("Access-Control-Allow-Methods", (options.methods ?? DEFAULT_METHODS).join(", "));
|
|
78
|
+
const requested = req.headers.get("access-control-request-headers");
|
|
79
|
+
req.setResponseHeader("Access-Control-Allow-Headers", options.allowedHeaders?.join(", ") ?? requested ?? "*");
|
|
80
|
+
if (options.maxAge !== undefined) {
|
|
81
|
+
req.setResponseHeader("Access-Control-Max-Age", String(options.maxAge));
|
|
82
|
+
}
|
|
83
|
+
return reply.empty(204);
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/** onResponse hook: brotli/gzip per `Accept-Encoding`, off the event loop (libuv pool). */
|
|
87
|
+
export function compression(options = {}) {
|
|
88
|
+
const threshold = options.threshold ?? 1024;
|
|
89
|
+
const gzipLevel = options.gzipLevel ?? 6;
|
|
90
|
+
const brotliQuality = options.brotliQuality ?? 5;
|
|
91
|
+
return async (req, res) => {
|
|
92
|
+
if (typeof res.body !== "string" || Buffer.byteLength(res.body) < threshold) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (!isCompressible(res.contentType)) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const accept = req.headers.get("accept-encoding") ?? "";
|
|
99
|
+
const encoding = acceptsEncoding(accept, "br")
|
|
100
|
+
? "br"
|
|
101
|
+
: acceptsEncoding(accept, "gzip")
|
|
102
|
+
? "gzip"
|
|
103
|
+
: null;
|
|
104
|
+
if (encoding === null) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const input = Buffer.from(res.body);
|
|
108
|
+
const compressed = encoding === "br"
|
|
109
|
+
? await brotliAsync(input, { params: { [constants.BROTLI_PARAM_QUALITY]: brotliQuality } })
|
|
110
|
+
: await gzipAsync(input, { level: gzipLevel });
|
|
111
|
+
return rawResponse(res.status, res.contentType, new Uint8Array(compressed), [
|
|
112
|
+
...res.headers,
|
|
113
|
+
["Content-Encoding", encoding],
|
|
114
|
+
["Vary", "Accept-Encoding"],
|
|
115
|
+
]);
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
/** Baseline hardening headers on every response. */
|
|
119
|
+
export function securityHeaders(options = {}) {
|
|
120
|
+
return (req) => {
|
|
121
|
+
req.setResponseHeader("X-Content-Type-Options", "nosniff");
|
|
122
|
+
req.setResponseHeader("X-Frame-Options", options.frameOptions ?? "DENY");
|
|
123
|
+
req.setResponseHeader("Referrer-Policy", options.referrerPolicy ?? "no-referrer");
|
|
124
|
+
if (options.hsts) {
|
|
125
|
+
const maxAge = options.hsts === true ? 15552000 : options.hsts;
|
|
126
|
+
req.setResponseHeader("Strict-Transport-Security", `max-age=${maxAge}; includeSubDomains`);
|
|
127
|
+
}
|
|
128
|
+
if (options.csp) {
|
|
129
|
+
req.setResponseHeader("Content-Security-Policy", options.csp);
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
//# sourceMappingURL=middleware.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"middleware.js","sourceRoot":"","sources":["../ts/middleware.ts"],"names":[],"mappings":"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"}
|
package/dist/native.d.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/** request handed to the dispatcher; strings decoded lazily by JS */
|
|
2
|
+
export interface NativeRequest {
|
|
3
|
+
readonly method: string;
|
|
4
|
+
readonly path: string;
|
|
5
|
+
/** raw query after `?`, empty when absent */
|
|
6
|
+
readonly query: string;
|
|
7
|
+
/** `Name: Value\r\n` lines */
|
|
8
|
+
readonly rawHeaders: string;
|
|
9
|
+
/** index into handler array; `0xFFFFFFFF` is the not-found handler */
|
|
10
|
+
readonly routeIndex: number;
|
|
11
|
+
/** token for {@link Native.submitResponse} */
|
|
12
|
+
readonly dispatchId: number;
|
|
13
|
+
readonly ip: string;
|
|
14
|
+
/** captured `:param`/`*` values, `null` for routes with none */
|
|
15
|
+
readonly params: Record<string, string> | null;
|
|
16
|
+
/** external view over the engine's buffer; valid only during dispatch */
|
|
17
|
+
readonly body: Uint8Array | null;
|
|
18
|
+
}
|
|
19
|
+
/** dispatcher return; `headers` is a pre-joined block, engine owns status line + Content-Length + Connection */
|
|
20
|
+
export interface NativeResponse {
|
|
21
|
+
status?: number;
|
|
22
|
+
headers?: string;
|
|
23
|
+
body?: string | Uint8Array;
|
|
24
|
+
}
|
|
25
|
+
/** static file for the engine; native derives MIME/ETag/headers, TS supplies bytes + mtime + cache policy */
|
|
26
|
+
export interface StaticEntry {
|
|
27
|
+
readonly path: string;
|
|
28
|
+
readonly body: Uint8Array;
|
|
29
|
+
/** also feeds the ETag */
|
|
30
|
+
readonly mtimeMs: number;
|
|
31
|
+
readonly cacheControl: string;
|
|
32
|
+
/** served when `Accept-Encoding` permits */
|
|
33
|
+
readonly gzip?: Uint8Array;
|
|
34
|
+
readonly brotli?: Uint8Array;
|
|
35
|
+
}
|
|
36
|
+
/** server tuning for {@link Toki.listen} */
|
|
37
|
+
export interface ServerOptions {
|
|
38
|
+
/** default `"0.0.0.0"` */
|
|
39
|
+
host?: string;
|
|
40
|
+
/** default 1 MiB */
|
|
41
|
+
maxBodyBytes?: number;
|
|
42
|
+
/** close a stalled (no-data) connection after this; 0 disables */
|
|
43
|
+
headerTimeoutMs?: number;
|
|
44
|
+
/** default 128 */
|
|
45
|
+
maxHeaders?: number;
|
|
46
|
+
/** pending-connection queue, default 512 */
|
|
47
|
+
backlog?: number;
|
|
48
|
+
/** `SO_REUSEPORT` so workers can share the port (kernel-balanced; Linux/BSD) */
|
|
49
|
+
reusePort?: boolean;
|
|
50
|
+
/** dispatch unmatched routes to JS; set automatically when a not-found handler exists */
|
|
51
|
+
notFound?: boolean;
|
|
52
|
+
/** native per-IP limit: `max` per `windowMs`, over-limit gets a native 429 before JS */
|
|
53
|
+
rateLimit?: {
|
|
54
|
+
max: number;
|
|
55
|
+
windowMs: number;
|
|
56
|
+
};
|
|
57
|
+
/** @internal flattened {@link ServerOptions.rateLimit} */
|
|
58
|
+
rateLimitMax?: number;
|
|
59
|
+
/** @internal */
|
|
60
|
+
rateLimitWindowMs?: number;
|
|
61
|
+
}
|
|
62
|
+
interface Native {
|
|
63
|
+
/**
|
|
64
|
+
* Serve routes (parallel `methods`/`paths`) plus `staticEntries`. `dispatch` returns a
|
|
65
|
+
* {@link NativeResponse} for sync, or `undefined` to defer to {@link Native.submitResponse};
|
|
66
|
+
* a GET/HEAD route miss falls back to the static table.
|
|
67
|
+
*/
|
|
68
|
+
listen(port: number, methods: string[], paths: string[], dispatch: (req: NativeRequest) => NativeResponse | undefined, staticEntries: StaticEntry[], options: ServerOptions): number;
|
|
69
|
+
/** complete a deferred handler */
|
|
70
|
+
submitResponse(dispatchId: number, response: NativeResponse): void;
|
|
71
|
+
/** begin a chunked response; writes the head */
|
|
72
|
+
startStream(dispatchId: number, status: number, headers: string): void;
|
|
73
|
+
/** write one chunk; returns queued-byte backlog, or -1 if the connection is gone */
|
|
74
|
+
writeStreamChunk(dispatchId: number, chunk: Uint8Array): number;
|
|
75
|
+
/** terminating chunk, then resume/close the connection */
|
|
76
|
+
endStream(dispatchId: number): void;
|
|
77
|
+
/** stop accepting and close every live connection */
|
|
78
|
+
close(): void;
|
|
79
|
+
}
|
|
80
|
+
export declare const native: Native;
|
|
81
|
+
export {};
|
package/dist/native.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// sole entry point to the Zig addon
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
const require = createRequire(import.meta.url);
|
|
6
|
+
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
7
|
+
// process.platform/arch → the package + filename suffix the release builds publish.
|
|
8
|
+
function nativeTriple() {
|
|
9
|
+
const { platform, arch } = process;
|
|
10
|
+
if (platform === "win32")
|
|
11
|
+
return `win32-${arch}-msvc`;
|
|
12
|
+
if (platform === "linux")
|
|
13
|
+
return `linux-${arch}-gnu`;
|
|
14
|
+
return `${platform}-${arch}`;
|
|
15
|
+
}
|
|
16
|
+
// Local source build (zig-out) first, then a binary bundled beside the loader, then
|
|
17
|
+
// the per-platform npm package installed as an optional dependency.
|
|
18
|
+
function loadNative() {
|
|
19
|
+
const triple = nativeTriple();
|
|
20
|
+
const attempts = [
|
|
21
|
+
["zig-out/toki.node", () => require(join(root, "zig-out", "toki.node"))],
|
|
22
|
+
[`toki.${triple}.node`, () => require(join(root, `toki.${triple}.node`))],
|
|
23
|
+
[`@usetoki/toki-${triple}`, () => require(`@usetoki/toki-${triple}`)],
|
|
24
|
+
];
|
|
25
|
+
const errors = [];
|
|
26
|
+
for (const [name, load] of attempts) {
|
|
27
|
+
try {
|
|
28
|
+
return load();
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
errors.push(` ${name}: ${error.message}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
throw new Error(`toki: no native binary for ${process.platform}-${process.arch}. Tried:\n${errors.join("\n")}\n` +
|
|
35
|
+
"Use a supported platform, or build from source with `npm run build`.");
|
|
36
|
+
}
|
|
37
|
+
export const native = loadNative();
|
|
38
|
+
//# sourceMappingURL=native.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"native.js","sourceRoot":"","sources":["../ts/native.ts"],"names":[],"mappings":"AAAA,oCAAoC;AACpC,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,MAAM,OAAO,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAC/C,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;AA0FjE,oFAAoF;AACpF,SAAS,YAAY;IACnB,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC;IACnC,IAAI,QAAQ,KAAK,OAAO;QAAE,OAAO,SAAS,IAAI,OAAO,CAAC;IACtD,IAAI,QAAQ,KAAK,OAAO;QAAE,OAAO,SAAS,IAAI,MAAM,CAAC;IACrD,OAAO,GAAG,QAAQ,IAAI,IAAI,EAAE,CAAC;AAC/B,CAAC;AAED,oFAAoF;AACpF,oEAAoE;AACpE,SAAS,UAAU;IACjB,MAAM,MAAM,GAAG,YAAY,EAAE,CAAC;IAC9B,MAAM,QAAQ,GAA4C;QACxD,CAAC,mBAAmB,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,SAAS,EAAE,WAAW,CAAC,CAAC,CAAC;QACxE,CAAC,QAAQ,MAAM,OAAO,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,QAAQ,MAAM,OAAO,CAAC,CAAC,CAAC;QACzE,CAAC,iBAAiB,MAAM,EAAE,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,iBAAiB,MAAM,EAAE,CAAC,CAAC;KACtE,CAAC;IACF,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,KAAK,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,QAAQ,EAAE,CAAC;QACpC,IAAI,CAAC;YACH,OAAO,IAAI,EAAY,CAAC;QAC1B,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,CAAC,IAAI,CAAC,KAAK,IAAI,KAAM,KAAe,CAAC,OAAO,EAAE,CAAC,CAAC;QACxD,CAAC;IACH,CAAC;IACD,MAAM,IAAI,KAAK,CACb,8BAA8B,OAAO,CAAC,QAAQ,IAAI,OAAO,CAAC,IAAI,aAAa,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI;QAC9F,sEAAsE,CACzE,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,MAAM,MAAM,GAAW,UAAU,EAAE,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { TokiRequest } from "./request.js";
|
|
2
|
+
import { type JSONSchema, type RouteSchema } from "./schema.js";
|
|
3
|
+
import type { HandlerResult, Middleware, ResponseHook, SerializationHook, TokiResponse } from "./types.js";
|
|
4
|
+
export declare function isThenable<T>(value: unknown): value is Promise<T>;
|
|
5
|
+
/** What `materialize` needs off a compiled route — a CompiledRoute satisfies it. */
|
|
6
|
+
interface ResultShape {
|
|
7
|
+
readonly responseSchemas?: Record<number, JSONSchema>;
|
|
8
|
+
readonly defaultStatus?: number;
|
|
9
|
+
}
|
|
10
|
+
export declare function materialize(result: HandlerResult, route: ResultShape): TokiResponse;
|
|
11
|
+
export declare function validationStep(schema: RouteSchema): Middleware;
|
|
12
|
+
export declare function contentTypeMatcher(type: string | RegExp): (contentType: string) => boolean;
|
|
13
|
+
export declare function runBefore(steps: readonly Middleware[], req: TokiRequest, start: number): HandlerResult | undefined | Promise<HandlerResult | undefined>;
|
|
14
|
+
export declare function runSerialization(hooks: readonly SerializationHook[], req: TokiRequest, payload: unknown, start: number): unknown | Promise<unknown>;
|
|
15
|
+
export declare function runAfter(hooks: readonly ResponseHook[], req: TokiRequest, res: TokiResponse, start: number): TokiResponse | Promise<TokiResponse>;
|
|
16
|
+
export {};
|
package/dist/pipeline.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { isTokiResponse, jsonResponse, normalize, reply } from "./response.js";
|
|
2
|
+
import { serialize, validate } from "./schema.js";
|
|
3
|
+
export function isThenable(value) {
|
|
4
|
+
return (value != null &&
|
|
5
|
+
(typeof value === "object" || typeof value === "function") &&
|
|
6
|
+
typeof value.then === "function");
|
|
7
|
+
}
|
|
8
|
+
// turn a handler result into a response; plain values serialize as JSON, via the
|
|
9
|
+
// route's response schema when one is present
|
|
10
|
+
export function materialize(result, route) {
|
|
11
|
+
if (isTokiResponse(result)) {
|
|
12
|
+
return result;
|
|
13
|
+
}
|
|
14
|
+
const status = route.defaultStatus ?? 200;
|
|
15
|
+
if (typeof result === "string") {
|
|
16
|
+
return reply.text(result, status);
|
|
17
|
+
}
|
|
18
|
+
const schema = route.responseSchemas?.[status];
|
|
19
|
+
return jsonResponse(schema ? serialize(schema, result) : JSON.stringify(result), status);
|
|
20
|
+
}
|
|
21
|
+
// before-step: validate the request against `schema`, short-circuiting 400
|
|
22
|
+
export function validationStep(schema) {
|
|
23
|
+
return (req) => {
|
|
24
|
+
const errors = validateRequest(req, schema);
|
|
25
|
+
if (errors.length > 0) {
|
|
26
|
+
return reply.json({ statusCode: 400, error: "Bad Request", message: errors.join("; "), errors }, 400);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function validateRequest(req, schema) {
|
|
31
|
+
const errors = [];
|
|
32
|
+
if (schema.params) {
|
|
33
|
+
errors.push(...validate(schema.params, coerce(schema.params, { ...req.params }), "params"));
|
|
34
|
+
}
|
|
35
|
+
if (schema.query) {
|
|
36
|
+
errors.push(...validate(schema.query, coerce(schema.query, fromEntries(req.query)), "query"));
|
|
37
|
+
}
|
|
38
|
+
if (schema.headers) {
|
|
39
|
+
errors.push(...validate(schema.headers, fromEntries(req.headers), "headers"));
|
|
40
|
+
}
|
|
41
|
+
if (schema.body) {
|
|
42
|
+
let body;
|
|
43
|
+
try {
|
|
44
|
+
body = req.json();
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return [...errors, "body must be valid JSON"];
|
|
48
|
+
}
|
|
49
|
+
errors.push(...validate(schema.body, body, "body"));
|
|
50
|
+
}
|
|
51
|
+
return errors;
|
|
52
|
+
}
|
|
53
|
+
function fromEntries(source) {
|
|
54
|
+
return Object.fromEntries(source.entries());
|
|
55
|
+
}
|
|
56
|
+
// coerce string-valued fields (query/params) to the type their schema declares
|
|
57
|
+
function coerce(schema, obj) {
|
|
58
|
+
if (!schema.properties) {
|
|
59
|
+
return obj;
|
|
60
|
+
}
|
|
61
|
+
const out = { ...obj };
|
|
62
|
+
for (const [key, sub] of Object.entries(schema.properties)) {
|
|
63
|
+
const value = out[key];
|
|
64
|
+
if (typeof value !== "string") {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (sub.type === "number" || sub.type === "integer") {
|
|
68
|
+
const n = Number(value);
|
|
69
|
+
if (!Number.isNaN(n))
|
|
70
|
+
out[key] = n;
|
|
71
|
+
}
|
|
72
|
+
else if (sub.type === "boolean" && (value === "true" || value === "false")) {
|
|
73
|
+
out[key] = value === "true";
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
// content-type predicate: a RegExp, "*" for any, or a case-insensitive prefix
|
|
79
|
+
export function contentTypeMatcher(type) {
|
|
80
|
+
if (type instanceof RegExp) {
|
|
81
|
+
return (contentType) => type.test(contentType);
|
|
82
|
+
}
|
|
83
|
+
if (type === "*") {
|
|
84
|
+
return () => true;
|
|
85
|
+
}
|
|
86
|
+
const lower = type.toLowerCase();
|
|
87
|
+
return (contentType) => contentType.startsWith(lower);
|
|
88
|
+
}
|
|
89
|
+
// pipeline runners — each stays sync until a step returns a Promise, then chains
|
|
90
|
+
export function runBefore(steps, req, start) {
|
|
91
|
+
for (let i = start; i < steps.length; i++) {
|
|
92
|
+
const out = steps[i](req);
|
|
93
|
+
if (isThenable(out)) {
|
|
94
|
+
return out.then((value) => (value != null ? value : runBefore(steps, req, i + 1)));
|
|
95
|
+
}
|
|
96
|
+
if (out != null) {
|
|
97
|
+
return out;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
// thread a payload through the preSerialization hooks; each returns the next payload
|
|
103
|
+
export function runSerialization(hooks, req, payload, start) {
|
|
104
|
+
for (let i = start; i < hooks.length; i++) {
|
|
105
|
+
const out = hooks[i](req, payload);
|
|
106
|
+
if (isThenable(out)) {
|
|
107
|
+
return out.then((value) => runSerialization(hooks, req, value, i + 1));
|
|
108
|
+
}
|
|
109
|
+
payload = out;
|
|
110
|
+
}
|
|
111
|
+
return payload;
|
|
112
|
+
}
|
|
113
|
+
export function runAfter(hooks, req, res, start) {
|
|
114
|
+
for (let i = start; i < hooks.length; i++) {
|
|
115
|
+
const out = hooks[i](req, res);
|
|
116
|
+
if (isThenable(out)) {
|
|
117
|
+
return out.then((value) => runAfter(hooks, req, value != null ? normalize(value) : res, i + 1));
|
|
118
|
+
}
|
|
119
|
+
if (out != null) {
|
|
120
|
+
res = normalize(out);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return res;
|
|
124
|
+
}
|
|
125
|
+
//# sourceMappingURL=pipeline.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pipeline.js","sourceRoot":"","sources":["../ts/pipeline.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { type CookieOptions } from "./cookies.js";
|
|
2
|
+
import { type ParsedForm } from "./forms.js";
|
|
3
|
+
import type { NativeRequest } from "./native.js";
|
|
4
|
+
import type { ContentTypeParserEntry, Logger, RouteMethod } from "./types.js";
|
|
5
|
+
/** Per-request context the dispatcher supplies; kept off the hot fields. */
|
|
6
|
+
interface RequestContext {
|
|
7
|
+
readonly log: Logger;
|
|
8
|
+
/** Content-type parsers in effect for this route (nearest scope first). */
|
|
9
|
+
readonly parsers?: readonly ContentTypeParserEntry[];
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* An incoming request handed to a handler. Query/headers parsed lazily.
|
|
13
|
+
* Backed by an engine-owned buffer — valid only during the handler call;
|
|
14
|
+
* read the body before any `await`.
|
|
15
|
+
*/
|
|
16
|
+
export declare class TokiRequest {
|
|
17
|
+
#private;
|
|
18
|
+
/** The HTTP method, e.g. `"GET"`. */
|
|
19
|
+
readonly method: RouteMethod;
|
|
20
|
+
/** The path, without the query string, e.g. `"/users/42"`. */
|
|
21
|
+
readonly path: string;
|
|
22
|
+
/** The raw request body, or `null` when there is none. */
|
|
23
|
+
readonly body: Uint8Array | null;
|
|
24
|
+
/** The peer IP address. */
|
|
25
|
+
readonly ip: string;
|
|
26
|
+
/** A {@link Logger} for this request; silent unless a logger was configured. */
|
|
27
|
+
readonly log: Logger;
|
|
28
|
+
constructor(raw: NativeRequest, context?: RequestContext);
|
|
29
|
+
/** The request host (from the `Host` header), without the port. */
|
|
30
|
+
get hostname(): string;
|
|
31
|
+
/** `"https"` when forwarded as such by a proxy, else `"http"`. */
|
|
32
|
+
get protocol(): string;
|
|
33
|
+
/** A short id unique within this process run; generated on first read. */
|
|
34
|
+
get id(): string;
|
|
35
|
+
/** Params captured from the route pattern (`:id`, `*`); an absent key reads as `undefined`. */
|
|
36
|
+
get params(): Readonly<Record<string, string | undefined>>;
|
|
37
|
+
/** The parsed query string. */
|
|
38
|
+
get query(): URLSearchParams;
|
|
39
|
+
/** The request headers. */
|
|
40
|
+
get headers(): Headers;
|
|
41
|
+
/** Stage a response header, replacing any previously staged value for `name`. */
|
|
42
|
+
setResponseHeader(name: string, value: string): this;
|
|
43
|
+
/** Stage a response header, appending rather than replacing (e.g. `Set-Cookie`). */
|
|
44
|
+
appendResponseHeader(name: string, value: string): this;
|
|
45
|
+
/** Cookies parsed from the `Cookie` header; an absent key reads as `undefined`. */
|
|
46
|
+
get cookies(): Readonly<Record<string, string | undefined>>;
|
|
47
|
+
/** Set a cookie on the response (staged as a `Set-Cookie` header). */
|
|
48
|
+
setCookie(name: string, value: string, options?: CookieOptions): this;
|
|
49
|
+
/** Clear a cookie by setting it expired. */
|
|
50
|
+
clearCookie(name: string, options?: CookieOptions): this;
|
|
51
|
+
/** @internal Read by the dispatcher; not intended for handler use. */
|
|
52
|
+
get stagedResponseHeaders(): ReadonlyArray<readonly [string, string]>;
|
|
53
|
+
/** Decode the body as UTF-8 text, or `""` when there is none. */
|
|
54
|
+
text(): string;
|
|
55
|
+
/** Parse the body as JSON. `T` is an unchecked assertion; throws on malformed input. */
|
|
56
|
+
json<T = unknown>(): T;
|
|
57
|
+
/** Body parsed as a form (urlencoded/multipart); null for any other content type. */
|
|
58
|
+
get form(): ParsedForm | null;
|
|
59
|
+
/**
|
|
60
|
+
* Body parsed by the matching content-type parser, falling back to built-ins
|
|
61
|
+
* (JSON/text/forms) else raw bytes. Cached; `undefined` when no body. `T` unchecked.
|
|
62
|
+
*/
|
|
63
|
+
parseBody<T = unknown>(): Promise<T>;
|
|
64
|
+
}
|
|
65
|
+
export {};
|
package/dist/request.js
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { parseCookies, serializeCookie } from "./cookies.js";
|
|
2
|
+
import { parseForm } from "./forms.js";
|
|
3
|
+
import { silentLogger } from "./logger.js";
|
|
4
|
+
let sequence = 0;
|
|
5
|
+
const NO_PARSERS = [];
|
|
6
|
+
/**
|
|
7
|
+
* An incoming request handed to a handler. Query/headers parsed lazily.
|
|
8
|
+
* Backed by an engine-owned buffer — valid only during the handler call;
|
|
9
|
+
* read the body before any `await`.
|
|
10
|
+
*/
|
|
11
|
+
export class TokiRequest {
|
|
12
|
+
/** The HTTP method, e.g. `"GET"`. */
|
|
13
|
+
method;
|
|
14
|
+
/** The path, without the query string, e.g. `"/users/42"`. */
|
|
15
|
+
path;
|
|
16
|
+
/** The raw request body, or `null` when there is none. */
|
|
17
|
+
body;
|
|
18
|
+
/** The peer IP address. */
|
|
19
|
+
ip;
|
|
20
|
+
/** A {@link Logger} for this request; silent unless a logger was configured. */
|
|
21
|
+
log;
|
|
22
|
+
// backs the lazy accessors; reshaped only on first read
|
|
23
|
+
#raw;
|
|
24
|
+
#parsers;
|
|
25
|
+
constructor(raw, context) {
|
|
26
|
+
this.#raw = raw;
|
|
27
|
+
this.method = raw.method;
|
|
28
|
+
this.path = raw.path;
|
|
29
|
+
this.body = raw.body;
|
|
30
|
+
this.ip = raw.ip;
|
|
31
|
+
this.log = context?.log ?? silentLogger;
|
|
32
|
+
this.#parsers = context?.parsers ?? NO_PARSERS;
|
|
33
|
+
}
|
|
34
|
+
/** The request host (from the `Host` header), without the port. */
|
|
35
|
+
get hostname() {
|
|
36
|
+
const host = this.headers.get("host") ?? "";
|
|
37
|
+
const colon = host.lastIndexOf(":");
|
|
38
|
+
return colon === -1 ? host : host.slice(0, colon);
|
|
39
|
+
}
|
|
40
|
+
/** `"https"` when forwarded as such by a proxy, else `"http"`. */
|
|
41
|
+
get protocol() {
|
|
42
|
+
return this.headers.get("x-forwarded-proto") ?? "http";
|
|
43
|
+
}
|
|
44
|
+
#id;
|
|
45
|
+
#params;
|
|
46
|
+
#query;
|
|
47
|
+
#headers;
|
|
48
|
+
#cookies;
|
|
49
|
+
/** A short id unique within this process run; generated on first read. */
|
|
50
|
+
get id() {
|
|
51
|
+
return (this.#id ??= (sequence++).toString(36));
|
|
52
|
+
}
|
|
53
|
+
/** Params captured from the route pattern (`:id`, `*`); an absent key reads as `undefined`. */
|
|
54
|
+
get params() {
|
|
55
|
+
return (this.#params ??= Object.freeze(this.#raw.params ?? {}));
|
|
56
|
+
}
|
|
57
|
+
/** The parsed query string. */
|
|
58
|
+
get query() {
|
|
59
|
+
return (this.#query ??= new URLSearchParams(this.#raw.query));
|
|
60
|
+
}
|
|
61
|
+
/** The request headers. */
|
|
62
|
+
get headers() {
|
|
63
|
+
if (this.#headers === undefined) {
|
|
64
|
+
const headers = new Headers();
|
|
65
|
+
for (const line of this.#raw.rawHeaders.split("\r\n")) {
|
|
66
|
+
const colon = line.indexOf(":");
|
|
67
|
+
if (colon === -1) {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
headers.append(line.slice(0, colon), line.slice(colon + 1).trimStart());
|
|
71
|
+
}
|
|
72
|
+
this.#headers = headers;
|
|
73
|
+
}
|
|
74
|
+
return this.#headers;
|
|
75
|
+
}
|
|
76
|
+
// pre-handler can stage headers before the response exists; dispatcher merges them,
|
|
77
|
+
// response's own headers win on conflict
|
|
78
|
+
#staged;
|
|
79
|
+
/** Stage a response header, replacing any previously staged value for `name`. */
|
|
80
|
+
setResponseHeader(name, value) {
|
|
81
|
+
const staged = (this.#staged ??= []);
|
|
82
|
+
const lower = name.toLowerCase();
|
|
83
|
+
for (let i = 0; i < staged.length; i++) {
|
|
84
|
+
if (staged[i][0].toLowerCase() === lower) {
|
|
85
|
+
staged[i] = [name, value];
|
|
86
|
+
return this;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
staged.push([name, value]);
|
|
90
|
+
return this;
|
|
91
|
+
}
|
|
92
|
+
/** Stage a response header, appending rather than replacing (e.g. `Set-Cookie`). */
|
|
93
|
+
appendResponseHeader(name, value) {
|
|
94
|
+
(this.#staged ??= []).push([name, value]);
|
|
95
|
+
return this;
|
|
96
|
+
}
|
|
97
|
+
/** Cookies parsed from the `Cookie` header; an absent key reads as `undefined`. */
|
|
98
|
+
get cookies() {
|
|
99
|
+
if (this.#cookies === undefined) {
|
|
100
|
+
const header = this.headers.get("cookie");
|
|
101
|
+
this.#cookies = header ? parseCookies(header) : Object.freeze({});
|
|
102
|
+
}
|
|
103
|
+
return this.#cookies;
|
|
104
|
+
}
|
|
105
|
+
/** Set a cookie on the response (staged as a `Set-Cookie` header). */
|
|
106
|
+
setCookie(name, value, options) {
|
|
107
|
+
return this.appendResponseHeader("Set-Cookie", serializeCookie(name, value, options));
|
|
108
|
+
}
|
|
109
|
+
/** Clear a cookie by setting it expired. */
|
|
110
|
+
clearCookie(name, options) {
|
|
111
|
+
return this.setCookie(name, "", { ...options, maxAge: 0, expires: new Date(0) });
|
|
112
|
+
}
|
|
113
|
+
/** @internal Read by the dispatcher; not intended for handler use. */
|
|
114
|
+
get stagedResponseHeaders() {
|
|
115
|
+
return this.#staged ?? [];
|
|
116
|
+
}
|
|
117
|
+
/** Decode the body as UTF-8 text, or `""` when there is none. */
|
|
118
|
+
text() {
|
|
119
|
+
return this.body ? new TextDecoder().decode(this.body) : "";
|
|
120
|
+
}
|
|
121
|
+
/** Parse the body as JSON. `T` is an unchecked assertion; throws on malformed input. */
|
|
122
|
+
json() {
|
|
123
|
+
return JSON.parse(this.text());
|
|
124
|
+
}
|
|
125
|
+
#form;
|
|
126
|
+
/** Body parsed as a form (urlencoded/multipart); null for any other content type. */
|
|
127
|
+
get form() {
|
|
128
|
+
if (this.#form === undefined) {
|
|
129
|
+
this.#form = this.body ? parseForm(this.headers.get("content-type") ?? "", this.body) : null;
|
|
130
|
+
}
|
|
131
|
+
return this.#form;
|
|
132
|
+
}
|
|
133
|
+
#parsedDone = false;
|
|
134
|
+
#parsedValue;
|
|
135
|
+
/**
|
|
136
|
+
* Body parsed by the matching content-type parser, falling back to built-ins
|
|
137
|
+
* (JSON/text/forms) else raw bytes. Cached; `undefined` when no body. `T` unchecked.
|
|
138
|
+
*/
|
|
139
|
+
async parseBody() {
|
|
140
|
+
if (!this.#parsedDone) {
|
|
141
|
+
this.#parsedValue = await this.#runParse();
|
|
142
|
+
this.#parsedDone = true;
|
|
143
|
+
}
|
|
144
|
+
return this.#parsedValue;
|
|
145
|
+
}
|
|
146
|
+
#runParse() {
|
|
147
|
+
const body = this.body;
|
|
148
|
+
if (!body) {
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
const contentType = (this.headers.get("content-type") ?? "").toLowerCase();
|
|
152
|
+
for (const entry of this.#parsers) {
|
|
153
|
+
if (entry.test(contentType)) {
|
|
154
|
+
return entry.parser(this, body);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (contentType.startsWith("application/json") || contentType.includes("+json")) {
|
|
158
|
+
return this.json();
|
|
159
|
+
}
|
|
160
|
+
if (contentType.startsWith("text/")) {
|
|
161
|
+
return this.text();
|
|
162
|
+
}
|
|
163
|
+
if (contentType.startsWith("application/x-www-form-urlencoded") ||
|
|
164
|
+
contentType.startsWith("multipart/form-data")) {
|
|
165
|
+
return this.form;
|
|
166
|
+
}
|
|
167
|
+
return body;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
//# sourceMappingURL=request.js.map
|