@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.
Files changed (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +177 -0
  3. package/dist/app.d.ts +137 -0
  4. package/dist/app.js +608 -0
  5. package/dist/app.js.map +1 -0
  6. package/dist/cookies.d.ts +17 -0
  7. package/dist/cookies.js +54 -0
  8. package/dist/cookies.js.map +1 -0
  9. package/dist/forms.d.ts +13 -0
  10. package/dist/forms.js +64 -0
  11. package/dist/forms.js.map +1 -0
  12. package/dist/group.d.ts +19 -0
  13. package/dist/group.js +51 -0
  14. package/dist/group.js.map +1 -0
  15. package/dist/index.d.ts +19 -0
  16. package/dist/index.js +11 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/inject.d.ts +19 -0
  19. package/dist/inject.js +56 -0
  20. package/dist/inject.js.map +1 -0
  21. package/dist/jwt.d.ts +57 -0
  22. package/dist/jwt.js +128 -0
  23. package/dist/jwt.js.map +1 -0
  24. package/dist/logger.d.ts +7 -0
  25. package/dist/logger.js +45 -0
  26. package/dist/logger.js.map +1 -0
  27. package/dist/middleware.d.ts +38 -0
  28. package/dist/middleware.js +133 -0
  29. package/dist/middleware.js.map +1 -0
  30. package/dist/native.d.ts +81 -0
  31. package/dist/native.js +38 -0
  32. package/dist/native.js.map +1 -0
  33. package/dist/pipeline.d.ts +16 -0
  34. package/dist/pipeline.js +125 -0
  35. package/dist/pipeline.js.map +1 -0
  36. package/dist/request.d.ts +65 -0
  37. package/dist/request.js +170 -0
  38. package/dist/request.js.map +1 -0
  39. package/dist/response.d.ts +33 -0
  40. package/dist/response.js +92 -0
  41. package/dist/response.js.map +1 -0
  42. package/dist/schema.d.ts +45 -0
  43. package/dist/schema.js +179 -0
  44. package/dist/schema.js.map +1 -0
  45. package/dist/static.d.ts +20 -0
  46. package/dist/static.js +105 -0
  47. package/dist/static.js.map +1 -0
  48. package/dist/types.d.ts +88 -0
  49. package/dist/types.js +2 -0
  50. package/dist/types.js.map +1 -0
  51. package/package.json +63 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Toki contributors
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,177 @@
1
+ <p align="center">
2
+ <img src="assets/toki-logo.png" alt="Toki" width="280">
3
+ </p>
4
+
5
+ <p align="center">
6
+ <strong>A blazing-fast HTTP framework for Node.js.</strong> ⚡
7
+ </p>
8
+
9
+ <p align="center">
10
+ A clean, fully-typed TypeScript API on top of a native HTTP engine written in
11
+ <strong>Zig</strong>. Parsing, routing, static files, and compression run in native
12
+ code; your handlers stay in JavaScript.
13
+ </p>
14
+
15
+ The engine runs on Node's **own libuv loop** and calls handlers **synchronously** on
16
+ the JS thread — no worker threads, no `ThreadsafeFunction`, no cross-thread hop. On
17
+ the plaintext benchmark it sustains ~99k req/s at ~49 MB RSS on a single thread.
18
+
19
+ ```ts
20
+ import { createApp, reply } from "@usetoki/toki";
21
+
22
+ const app = createApp();
23
+
24
+ app.get("/", () => reply.text("Hello, World!"));
25
+ app.get("/users/:id", (req) => reply.json({ id: req.params.id }));
26
+
27
+ app.listen(3000);
28
+ console.log("listening on http://127.0.0.1:3000");
29
+ ```
30
+
31
+ ## ✨ Why Toki
32
+
33
+ - ⚡ **Native engine** — HTTP/1.1 parsing, routing, and I/O run in Zig, on Node's own loop.
34
+ - 🪶 **Tiny footprint** — a single-thread server in ~49 MB, ~2× leaner than `node:http`.
35
+ - 🧩 **Fully typed** — strict TypeScript, no `any`, real editor autocompletion.
36
+ - 🔌 **Batteries included** — routing, hooks, middleware, route groups, plugins, cookies, CORS, security headers, logging, `req.id` / `req.ip`.
37
+ - 📦 **Body parsing** — JSON, urlencoded, and `multipart/form-data` uploads, plus pluggable content-type parsers.
38
+ - 🗂️ **Static files** — MIME, `ETag` / `304`, `HEAD`, traversal-safe, with pre-computed gzip/brotli.
39
+ - 🗜️ **Compression** — gzip + brotli, negotiated per `Accept-Encoding`, off the event loop.
40
+ - 🌊 **Streaming** — `reply.stream` over chunked transfer encoding, with native backpressure.
41
+ - 🛡️ **Hardened** — schema validation, JWT, a native per-IP rate limiter, slowloris guard, configurable limits.
42
+ - 🧪 **Testable** — `app.inject()` runs a real request in-process, no port needed.
43
+
44
+ ## 🚀 Install
45
+
46
+ ```bash
47
+ npm install @usetoki/toki
48
+ ```
49
+
50
+ Or build from source — see [Build from source](#-build-from-source). You'll need
51
+ [Zig](https://ziglang.org) 0.16.0 and Node.js 20+.
52
+
53
+ ## 📖 Features at a glance
54
+
55
+ ```ts
56
+ import { createApp, reply, cors, securityHeaders, compression, jwtAuth } from "@usetoki/toki";
57
+
58
+ const app = createApp({ logger: "info" });
59
+
60
+ // Middleware + the full hook pipeline
61
+ app.use(cors({ origin: ["https://app.example"] }));
62
+ app.use(securityHeaders({ hsts: true }));
63
+ app.addHook("onResponse", compression());
64
+
65
+ // Route groups with a shared prefix + scoped hooks
66
+ app.group("/api/v1", (api) => {
67
+ api.use(jwtAuth({ secret: process.env.JWT_SECRET! }));
68
+ api.get("/me", (req) => reply.json(req.user));
69
+ });
70
+
71
+ // Encapsulated plugins
72
+ app.register(async (instance) => {
73
+ instance.get("/health", () => ({ status: "ok" }));
74
+ }, { prefix: "/internal" });
75
+
76
+ // Schema validation (custom messages) + response serialization
77
+ app.post("/users", {
78
+ schema: {
79
+ body: {
80
+ type: "object",
81
+ required: ["name"],
82
+ properties: { name: { type: "string", minLength: 2 } },
83
+ errorMessage: { required: { name: "name is required" } },
84
+ },
85
+ },
86
+ }, (req) => reply.json({ created: req.json<{ name: string }>().name }, 201));
87
+
88
+ // Uploads (req.form), static files, streaming, rate limiting
89
+ app.post("/upload", (req) => reply.json({ files: req.form?.files.length ?? 0 }));
90
+ app.static("/assets", "./public");
91
+ app.get("/events", () => reply.stream(sse(), { contentType: "text/event-stream" }));
92
+
93
+ app.listen(3000, { rateLimit: { max: 100, windowMs: 60_000 } });
94
+ ```
95
+
96
+ **Request:** `req.method`, `req.path`, `req.params`, `req.query`, `req.headers`,
97
+ `req.cookies`, `req.body`, `req.form`, `req.ip`, `req.hostname`, `req.protocol`,
98
+ `req.id`, `req.log`, `req.text()`, `req.json<T>()`, `await req.parseBody()`.
99
+
100
+ **Reply:** `reply.text / html / json / empty / redirect / bytes / stream`.
101
+
102
+ **Built in:** `cors()`, `securityHeaders()`, `compression()`, `signJwt` / `verifyJwt`
103
+ / `jwtAuth`, `setErrorHandler`, `setNotFoundHandler`, `addContentTypeParser`.
104
+
105
+ ## ⚙️ `listen` options
106
+
107
+ ```ts
108
+ app.listen(3000, { host: "0.0.0.0", maxBodyBytes: 5_000_000 });
109
+ ```
110
+
111
+ | Option | Default | Description |
112
+ | --- | --- | --- |
113
+ | `host` | `0.0.0.0` | Bind interface. Pass `0` as the port to pick a free one. |
114
+ | `maxBodyBytes` | 1 MiB | Largest accepted request body (`413` above it). |
115
+ | `maxHeaders` | 128 | Max header lines per request. |
116
+ | `backlog` | 512 | Listen backlog. |
117
+ | `headerTimeoutMs` | 0 | Close a connection stalled mid-request, in ms; `0` disables (slowloris guard). |
118
+ | `reusePort` | `false` | `SO_REUSEPORT` for kernel-balanced multi-worker scaling (Linux/BSD). |
119
+ | `rateLimit` | — | `{ max, windowMs }` — native per-IP limiter; over-limit requests get a `429` before reaching JS. |
120
+
121
+ `createApp({ logger, requestTimeoutMs })` configures the app; `app.listen` returns a
122
+ handle whose `close()` shuts the server down gracefully.
123
+
124
+ ## 🧭 Native vs JavaScript — the boundary
125
+
126
+ The shared, heavy logic is native: HTTP parsing, routing, the MIME table, ETag and
127
+ header assembly, status phrases, static serving, and compression negotiation. The
128
+ TypeScript layer is the developer API plus the unavoidable Node bits (the handler
129
+ pipeline, `fs`/`zlib` for static assets).
130
+
131
+ That line is drawn on purpose, and it's measured. Building a parsed request object in
132
+ Zig and handing it to V8 means ~20 N-API calls per request — slower than letting V8's
133
+ own C++ `URLSearchParams` / `Headers` / `JSON` do it. So query/header/cookie/JSON
134
+ parsing and schema validation stay in TypeScript: crossing the N-API boundary to
135
+ "go native" there would make it slower, which is the opposite of the point.
136
+
137
+ ## 🔧 Build from source
138
+
139
+ ```bash
140
+ npm install
141
+ npm run build # native addon (ReleaseFast) + TypeScript → dist/
142
+ npm test # zig build test + the Node suite
143
+ npm run lint # zig fmt + tsc + prettier
144
+ ```
145
+
146
+ `npm run build:debug` is a faster, unoptimized native build for iteration. The build
147
+ cross-compiles: `zig build -Dtarget=aarch64-linux-gnu` (and friends) produces the
148
+ addon for any platform from one host.
149
+
150
+ ## 📂 Layout
151
+
152
+ | Folder | What |
153
+ | --- | --- |
154
+ | `src/` | Zig engine (Node-API addon) — parser, router, response, static, streaming, rate limiter. |
155
+ | `ts/` | TypeScript framework layer → `dist/`. |
156
+ | `__test__/` | Tests (`node:test`, run as `.ts`). |
157
+ | `examples/` | A runnable, self-checking example per feature. |
158
+
159
+ ## 🧪 Examples
160
+
161
+ ```bash
162
+ node examples/routing.ts
163
+ ```
164
+
165
+ Browse [`examples/`](./examples) for routing, async handlers, hooks, groups, plugins,
166
+ validation, cookies, static files, forms, CORS, compression, rate limiting, JWT,
167
+ streaming, and graceful shutdown.
168
+
169
+ ## Scope
170
+
171
+ Toki speaks HTTP/1.1. TLS and HTTP/2 are intentionally out of scope — terminate them
172
+ at a reverse proxy (nginx, Caddy), the standard production setup for Node. WebSockets
173
+ are not included.
174
+
175
+ ## License
176
+
177
+ MIT — see [LICENSE](./LICENSE).
package/dist/app.d.ts ADDED
@@ -0,0 +1,137 @@
1
+ import { RouteGroup } from "./group.js";
2
+ import { type InjectOptions, type InjectResponse } from "./inject.js";
3
+ import { type CorsOptions } from "./middleware.js";
4
+ import { type ServerOptions } from "./native.js";
5
+ import { type StaticOptions } from "./static.js";
6
+ import type { BodyParser, ContentTypeParserEntry, ErrorHandler, Handler, LifecycleHook, ListenHandle, Logger, Middleware, PluginOptions, ResponseHook, RouteMethod, RouteOptions, SerializationHook, TimeoutHook, TokiOptions } from "./types.js";
7
+ interface Route {
8
+ readonly method: RouteMethod;
9
+ readonly path: string;
10
+ readonly handler: Handler;
11
+ readonly options: RouteOptions;
12
+ readonly groupMiddleware: readonly Middleware[];
13
+ readonly scope: Scope;
14
+ }
15
+ interface PluginEntry {
16
+ readonly plugin: TokiPlugin;
17
+ readonly opts: PluginOptions;
18
+ readonly scope: Scope;
19
+ }
20
+ /** Registers routes/hooks/decorators on the encapsulated `instance`. */
21
+ export type TokiPlugin = (instance: TokiInstance, opts: PluginOptions) => void | Promise<void>;
22
+ /**
23
+ * Encapsulated registration scope: own hooks, middleware, request decorators,
24
+ * error handler, and path prefix. Routes here run the hook chains of this scope
25
+ * and all ancestors. The root app ({@link Toki}) is itself a scope; {@link
26
+ * Scope.register} creates children.
27
+ */
28
+ export declare class Scope {
29
+ #private;
30
+ readonly parent: Scope | undefined;
31
+ readonly prefix: string;
32
+ readonly onRequest: Middleware[];
33
+ readonly preParsing: Middleware[];
34
+ readonly middleware: Middleware[];
35
+ readonly preValidation: Middleware[];
36
+ readonly preHandler: Middleware[];
37
+ readonly preSerialization: SerializationHook[];
38
+ readonly onResponse: ResponseHook[];
39
+ readonly onSend: ResponseHook[];
40
+ readonly onTimeout: TimeoutHook[];
41
+ readonly requestDecorators: Record<string, unknown>;
42
+ readonly contentTypeParsers: ContentTypeParserEntry[];
43
+ readonly plugins: PluginEntry[];
44
+ errorHandler?: ErrorHandler;
45
+ constructor(parent: Scope | undefined, prefix: string);
46
+ get root(): Toki;
47
+ get log(): Logger;
48
+ get(path: string, handler: Handler): this;
49
+ get(path: string, options: RouteOptions, handler: Handler): this;
50
+ post(path: string, handler: Handler): this;
51
+ post(path: string, options: RouteOptions, handler: Handler): this;
52
+ put(path: string, handler: Handler): this;
53
+ put(path: string, options: RouteOptions, handler: Handler): this;
54
+ patch(path: string, handler: Handler): this;
55
+ patch(path: string, options: RouteOptions, handler: Handler): this;
56
+ delete(path: string, handler: Handler): this;
57
+ delete(path: string, options: RouteOptions, handler: Handler): this;
58
+ head(path: string, handler: Handler): this;
59
+ options(path: string, handler: Handler): this;
60
+ route(method: RouteMethod, path: string, handler: Handler): this;
61
+ /** Add middleware; runs before every handler in this scope and descendants. */
62
+ use(middleware: Middleware): this;
63
+ /** Register a lifecycle hook scoped to this instance. */
64
+ addHook(name: "onRequest" | "preParsing" | "preValidation" | "preHandler", fn: Middleware): this;
65
+ addHook(name: "preSerialization", fn: SerializationHook): this;
66
+ addHook(name: "onResponse" | "onSend", fn: ResponseHook): this;
67
+ addHook(name: "onTimeout", fn: TimeoutHook): this;
68
+ /** Set the error handler for routes in this scope (alias: `onError`). */
69
+ setErrorHandler(handler: ErrorHandler): this;
70
+ onError(handler: ErrorHandler): this;
71
+ /** Attach a property to every request handled in this scope. */
72
+ decorateRequest(name: string, value: unknown): this;
73
+ /**
74
+ * Register a body parser for one or more content types, consulted by
75
+ * `req.parseBody()`. `type` matches case-insensitively as a prefix (e.g.
76
+ * `"application/xml"`), a `RegExp`, or `"*"` for any. A child scope overrides
77
+ * an ancestor for the same type.
78
+ */
79
+ addContentTypeParser(type: string | string[] | RegExp, parser: BodyParser): this;
80
+ /** Attach a property to the application instance (app-global, not encapsulated). */
81
+ decorate(name: string, value: unknown): this;
82
+ /** Set the handler for unmatched routes (app-global). */
83
+ setNotFoundHandler(handler: Handler): this;
84
+ /** Enable CORS: stage headers on every response and answer preflight `OPTIONS`. */
85
+ cors(options?: CorsOptions): this;
86
+ /** Serve files under `dir` at `urlPrefix` (joined with this scope's prefix). */
87
+ static(urlPrefix: string, dir: string, options?: StaticOptions): this;
88
+ /** Register a prefixed group of routes with its own middleware. */
89
+ group(prefix: string, build: (group: RouteGroup) => void): this;
90
+ /**
91
+ * Register a plugin into a fresh child scope. The plugin receives that scope as
92
+ * its `instance`; routes/hooks/decorators it adds are encapsulated there (and
93
+ * inherited by its own children), not leaked to the parent. Async plugins load
94
+ * during {@link Toki.ready}. `opts.prefix` mounts the plugin's routes under a path.
95
+ */
96
+ register(plugin: TokiPlugin, opts?: PluginOptions): this;
97
+ }
98
+ /** The registration surface a plugin receives (the application or a child scope). */
99
+ export type TokiInstance = Scope;
100
+ /**
101
+ * The application. Register routes (optionally with a validation schema and
102
+ * route-scoped hooks), global hooks, middleware, and plugins, then
103
+ * {@link Toki.listen}. The pipeline stays synchronous until a step returns a
104
+ * `Promise`, keeping the common sync request on the fast path.
105
+ */
106
+ export declare class Toki extends Scope {
107
+ #private;
108
+ constructor(options?: TokiOptions);
109
+ /** @internal */
110
+ get _logger(): Logger;
111
+ /** @internal */
112
+ _pushRoute(route: Route): void;
113
+ /** @internal */
114
+ _decorate(name: string, value: unknown): void;
115
+ /** @internal */
116
+ _setNotFound(handler: Handler): void;
117
+ /** @internal */
118
+ _mountStatic(prefix: string, dir: string, options?: StaticOptions): void;
119
+ /** Run `fn` once at {@link Toki.listen}, before serving. */
120
+ onReady(fn: LifecycleHook): this;
121
+ /** Run `fn` when the server closes. */
122
+ onClose(fn: LifecycleHook): this;
123
+ /** Load all registered plugins, awaiting async ones. Call before {@link Toki.listen}
124
+ * when any plugin is async; `listen` loads sync plugins on its own. */
125
+ ready(): Promise<void>;
126
+ /** Bind `port` and serve. Returns a handle whose `close()` stops the server. */
127
+ listen(port: number, options?: ServerOptions): ListenHandle;
128
+ /**
129
+ * Inject a request in-process for testing. Sends a real request over loopback so
130
+ * the full native path runs, auto-binding an ephemeral port when not already
131
+ * listening. Returns the captured response.
132
+ */
133
+ inject(options: InjectOptions | string): Promise<InjectResponse>;
134
+ }
135
+ /** Create a new application. */
136
+ export declare function createApp(options?: TokiOptions): Toki;
137
+ export {};