@vxnsin/inkan 0.0.0-stage → 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 vensin
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 CHANGED
@@ -1,3 +1,282 @@
1
- # Temporary Holding Version
1
+ <picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/vxnsin/inkan/main/assets/wordmark-dark.svg"><img src="https://raw.githubusercontent.com/vxnsin/inkan/main/assets/wordmark-light.svg" alt="inkan" width="340"></picture>
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ **An API server for Node where the docs can't lie.**
4
+
5
+ [![npm](https://img.shields.io/npm/v/@vxnsin/inkan?color=c4381f&labelColor=2b2420&label=npm)](https://www.npmjs.com/package/@vxnsin/inkan)
6
+ [![CI](https://img.shields.io/github/actions/workflow/status/vxnsin/inkan/ci.yml?branch=main&color=3d7a4b&labelColor=2b2420&label=ci)](https://github.com/vxnsin/inkan/actions/workflows/ci.yml)
7
+ [![dependencies](https://img.shields.io/badge/dependencies-0-ece1cf?labelColor=2b2420)](package.json)
8
+ [![License](https://img.shields.io/badge/license-MIT-a87fe0?labelColor=2b2420)](LICENSE)
9
+ [![supports warden](https://raw.githubusercontent.com/vxnsin/warden/main/assets/supports-warden.svg)](https://github.com/vxnsin/warden)
10
+
11
+ <!-- cozy:cards -->
12
+ <div align="center">
13
+
14
+ <a href="https://github.com/vxnsin/inkan"><picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/vxnsin/inkan/output/repo-dark.svg?v=e2fea4fc22"><img src="https://raw.githubusercontent.com/vxnsin/inkan/output/repo-light.svg?v=e2fea4fc22" width="840" alt="vxnsin/inkan: An API server for Node where the docs can't lie: one contract per route gives you validation, types, OpenAPI, docs and tests."></picture></a>
15
+
16
+ <a href="https://github.com/vxnsin/inkan#install"><picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/vxnsin/inkan/output/nav-start-dark.svg?v=c2e08cb92e"><img src="https://raw.githubusercontent.com/vxnsin/inkan/output/nav-start-light.svg?v=c2e08cb92e" width="95" alt="install →"></picture></a><a href="https://www.npmjs.com/package/@vxnsin/inkan"><picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/vxnsin/inkan/output/nav-npm-dark.svg?v=513902168b"><img src="https://raw.githubusercontent.com/vxnsin/inkan/output/nav-npm-light.svg?v=513902168b" width="50" alt="npm"></picture></a><a href="https://github.com/vxnsin/warden"><picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/vxnsin/inkan/output/nav-warden-dark.svg?v=0e950205da"><img src="https://raw.githubusercontent.com/vxnsin/inkan/output/nav-warden-light.svg?v=0e950205da" width="69" alt="warden"></picture></a>
17
+
18
+ <a href="https://github.com/vxnsin/inkan/commits"><picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/vxnsin/inkan/output/commits-dark.svg?v=5ef5be0d70"><img src="https://raw.githubusercontent.com/vxnsin/inkan/output/commits-light.svg?v=5ef5be0d70" width="840" alt="latest commits of vxnsin/inkan"></picture></a>
19
+
20
+ <a href="https://github.com/vxnsin/inkan/releases"><picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/vxnsin/inkan/output/releases-dark.svg?v=f1fdb7c523"><img src="https://raw.githubusercontent.com/vxnsin/inkan/output/releases-light.svg?v=f1fdb7c523" width="840" alt="releases: none yet"></picture></a>
21
+
22
+ <a href="https://github.com/vxnsin/inkan/graphs/contributors"><picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/vxnsin/inkan/output/contributors-dark.svg?v=8bcda569cd"><img src="https://raw.githubusercontent.com/vxnsin/inkan/output/contributors-light.svg?v=8bcda569cd" width="840" alt="contributors: vxnsin"></picture></a>
23
+
24
+ </div>
25
+ <!-- /cozy:cards -->
26
+
27
+ An *inkan* (印鑑) is the seal a Japanese contract gets stamped with. Here every
28
+ route carries one: params, query, body, responses and a few examples, written
29
+ once. From that one definition inkan checks what comes in, types your handler,
30
+ checks what goes out, writes OpenAPI 3.1, serves the docs, and runs every
31
+ example as a test.
32
+
33
+ ```ts
34
+ import { inkan, problem, t } from "@vxnsin/inkan";
35
+
36
+ const Tea = t.object({ id: t.int(), name: t.string(), kind: t.enum(["green", "black", "oolong"]) });
37
+ const teas = [{ id: 1, name: "Sencha", kind: "green" as const }];
38
+
39
+ const app = inkan({ title: "Tea Shop", version: "1.0.0" });
40
+
41
+ app.get(
42
+ "/teas/:id",
43
+ {
44
+ params: t.object({ id: t.int() }),
45
+ response: { 200: Tea, 404: t.problem() },
46
+ examples: [
47
+ { name: "found", params: { id: 1 }, expect: { name: "Sencha" } },
48
+ { name: "missing", params: { id: 99 }, status: 404 },
49
+ ],
50
+ },
51
+ ({ params }) => {
52
+ const tea = teas.find((x) => x.id === params.id); // params.id is a number, not a string
53
+ if (!tea) throw problem(404, "tea-not-found", `There is no tea with id ${params.id}`);
54
+ return tea; // has to be a Tea, or it does not compile
55
+ },
56
+ );
57
+
58
+ app.listen();
59
+ ```
60
+
61
+ The examples are what the docs show, and they are what this runs. Say the 404 got lost in a refactor:
62
+
63
+ ```sh
64
+ $ npx inkan check src/app.ts
65
+
66
+ 印 inkan check · Tea Shop 1.0.0
67
+
68
+ GET /teas/:id
69
+ ✓ found 200 0.8ms
70
+ ✗ missing 200 0.4ms
71
+ answered 200, expected 404
72
+
73
+ 2 examples · 1 sealed · 1 broken
74
+ ```
75
+
76
+ A docs page that is tested is a docs page you can trust. When the handler
77
+ drifts, the check goes red before anyone reads something false.
78
+
79
+ ## Why another one
80
+
81
+ Express gets out of the way, FastAPI writes your docs. inkan wants both, plus
82
+ the one thing neither does: holding the server to what its docs say.
83
+
84
+ | | Express | Fastify | FastAPI | inkan |
85
+ | --- | :-: | :-: | :-: | :-: |
86
+ | Handler types come from the schema | – | with a type provider | ✓ | ✓ |
87
+ | OpenAPI document | plugin | plugin | ✓ | ✓ |
88
+ | Docs page | plugin | plugin | ✓ | ✓ no CDN, works offline |
89
+ | Answers trimmed to the contract | – | ✓ | ✓ | ✓ and checked in dev |
90
+ | **Examples run as tests** | – | – | – | ✓ `inkan check` |
91
+ | **Live request inspector** | – | – | – | ✓ `/_inkan` |
92
+ | Every error in one shape ([RFC 9457](https://www.rfc-editor.org/rfc/rfc9457)) | – | – | – | ✓ |
93
+ | Runtime dependencies | several | several | several | **none** |
94
+
95
+ ## Install
96
+
97
+ ```sh
98
+ npm install @vxnsin/inkan
99
+ ```
100
+
101
+ The package is scoped because npm keeps the plain name `inkan` free of look-alikes. The command it installs is `inkan` either way.
102
+
103
+ Node 20 or newer. On Node 22.18 and newer, `.ts` files run as they are,
104
+ with no build step and no loader.
105
+
106
+ ## What it does
107
+
108
+ | | |
109
+ | --- | --- |
110
+ | **Checks what comes in** | `params`, `query`, `headers` and `body` are validated together. Path and query strings become the numbers and booleans the schema asks for. Bad input is one 400 that lists every issue, not just the first. |
111
+ | **Types the handler** | No generics to write. `params.id` is what the schema says, and so is the return value. Without a schema, `:id` in the path is still typed. |
112
+ | **Checks what goes out** | In development, an answer that breaks its contract is a 500 that says where, not a silent surprise for the frontend. Keys the contract does not list are dropped, so a `passwordHash` never leaves by accident. |
113
+ | **Writes OpenAPI 3.1** | At `/openapi.json`, from the same schemas. Named schemas land in `components` once. |
114
+ | **Serves the docs** | At `/docs`. Every example has a send button, and a route gets its seal 印 when all of its examples answer as promised. |
115
+ | **Runs examples as tests** | `inkan check` or `app.check()`, in-process, no port. Routes without examples are listed, so nothing hides. |
116
+ | **Shows what happened** | `/_inkan` is a live log of the last 200 requests and what broke the contract. Development only, loopback only, secret headers hidden. |
117
+ | **Errors in one shape** | `throw problem(404, "tea-not-found", "…")` gives an RFC 9457 document. Every built-in error has the same shape, with a stable `type` to switch on. |
118
+
119
+ ## A route
120
+
121
+ ```ts
122
+ app.post(
123
+ "/teas",
124
+ {
125
+ summary: "Add a tea",
126
+ tags: ["teas"],
127
+ body: NewTea,
128
+ response: { 201: Tea, 409: t.problem() },
129
+ examples: [
130
+ { name: "a new oolong", body: { name: "Da Hong Pao", kind: "oolong" }, expect: { id: 2 } },
131
+ { name: "no name", body: { kind: "green" }, status: 400 },
132
+ ],
133
+ },
134
+ ({ body, reply }) => {
135
+ const tea = store.add(body);
136
+ return reply(201, tea, { location: `/teas/${tea.id}` });
137
+ },
138
+ );
139
+ ```
140
+
141
+ | field | |
142
+ | --- | --- |
143
+ | `params`, `query`, `headers`, `body` | schemas for the input. Header names are lowercase. |
144
+ | `response` | status → schema. The first 2xx is the default status, and no body means 204. |
145
+ | `examples` | `{ name, params, query, headers, body, status, expect }`. `status` defaults to the first 2xx. `expect` is a part of the answer that has to be there. |
146
+ | `summary`, `description`, `tags`, `deprecated`, `operationId` | for the docs |
147
+ | `hidden` | keeps the route out of the docs and OpenAPI |
148
+ | `use` | middleware for this route only, after validation |
149
+
150
+ A handler returns a value, or `reply(status, body, headers)` for anything
151
+ else. `ctx.reply` is typed to the statuses in the contract, so `reply(418)`
152
+ on a route that never promised a teapot does not compile.
153
+
154
+ ## Schemas
155
+
156
+ ```ts
157
+ t.string().min(1).max(60).email().uuid().pattern(/x/).format("date-time").trim()
158
+ t.int().min(0) t.number().positive() t.boolean()
159
+ t.enum(["a", "b"]) t.literal("a") t.union(t.string(), t.int())
160
+ t.array(Tea).min(1) t.record(t.int()) t.any() t.empty() t.problem()
161
+ t.object({ … }).strict() .passthrough() .pick() .omit() .extend() .partial()
162
+
163
+ .optional() .nullable() .default(v) .describe("…") .example(v) .named("Tea") .deprecated()
164
+ ```
165
+
166
+ `Infer<typeof Tea>` is the TypeScript type. `Tea.parse(x)` throws a
167
+ `ValidationError`, `Tea.safeParse(x)` does not, and `Tea.toJSONSchema()`
168
+ gives JSON Schema 2020-12.
169
+
170
+ ## Examples are tests
171
+
172
+ ```sh
173
+ npx inkan check src/app.ts # every route
174
+ npx inkan check src/app.ts --only /teas
175
+ npx inkan check src/app.ts --json # for CI
176
+ ```
177
+
178
+ The entry file exports the app (`export default app` or `export const app`).
179
+ `app.listen()` stays quiet while the CLI loads it. If the examples change data,
180
+ export a `beforeEach` that puts it back. It runs before every example:
181
+
182
+ ```ts
183
+ export function beforeEach() {
184
+ store.reset();
185
+ }
186
+ ```
187
+
188
+ Or inside your own tests:
189
+
190
+ ```ts
191
+ import { test } from "node:test";
192
+ import assert from "node:assert/strict";
193
+ import { app, beforeEach } from "./app.ts";
194
+
195
+ test("the contract holds", async () => {
196
+ const report = await app.check({ beforeEach });
197
+ assert.equal(report.failed, 0);
198
+ });
199
+ ```
200
+
201
+ For everything the examples do not cover, `app.inject()` sends a request
202
+ straight in, without a socket:
203
+
204
+ ```ts
205
+ const res = await app.inject({ method: "POST", url: "/teas", body: { name: "Gyokuro" } });
206
+ res.status; // 400
207
+ res.body.errors; // [{ in: "body", path: "kind", message: "is required" }, …]
208
+ ```
209
+
210
+ ## Middleware and groups
211
+
212
+ ```ts
213
+ app.use(async (ctx, next) => {
214
+ const started = Date.now();
215
+ await next();
216
+ ctx.header("server-timing", `app;dur=${Date.now() - started}`);
217
+ });
218
+
219
+ const admin = routes().use(requireAdmin).get("/stats", () => stats());
220
+ app.mount("/admin", admin);
221
+ ```
222
+
223
+ `ctx.state` carries things from middleware to the handler. A thrown
224
+ `problem()` stops everything, wherever it is thrown.
225
+
226
+ ## Running it
227
+
228
+ `app.listen()` takes the port from its argument, then `$PORT`, then 3000. So it
229
+ runs under [warden](https://github.com/vxnsin/warden), a container or a PaaS
230
+ without changes:
231
+
232
+ ```sh
233
+ warden run -- node src/app.ts
234
+ ```
235
+
236
+ On SIGINT or SIGTERM it lets open requests finish (up to ten seconds) before it
237
+ exits. `app.listener` is a plain `(req, res)` function for your own
238
+ `http.createServer`.
239
+
240
+ ```ts
241
+ inkan({
242
+ title: "Tea Shop", version: "1.0.0", description: "…", servers: [{ url: "https://api.example.com" }],
243
+ docs: "/docs", // or false
244
+ openapi: "/openapi.json", // or false
245
+ inspector: "/_inkan", // default: on in development, always loopback only
246
+ validateResponses: true, // default: on in development
247
+ bodyLimit: 1024 * 1024,
248
+ log: true, // one line per request, default: on in development
249
+ gracefulShutdown: true,
250
+ dev: process.env.NODE_ENV !== "production",
251
+ onError: (err, ctx) => report(err),
252
+ });
253
+ ```
254
+
255
+ ## CLI
256
+
257
+ ```sh
258
+ npx inkan check src/app.ts [--only <text>] [--json]
259
+ npx inkan openapi src/app.ts [-o openapi.json]
260
+ npx inkan routes src/app.ts
261
+ ```
262
+
263
+ ## Try the example
264
+
265
+ ```sh
266
+ git clone https://github.com/vxnsin/inkan && cd inkan && npm install
267
+ node examples/shop.ts # then open http://localhost:3000/docs
268
+ node src/cli.ts check examples/shop.ts
269
+ ```
270
+
271
+ ## Not yet
272
+
273
+ - a typed client that reads the routes, with no codegen
274
+ - CORS and `OPTIONS` out of the box
275
+ - streaming answers and file uploads
276
+ - routes from the file tree
277
+
278
+ Ideas and issues are welcome.
279
+
280
+ ## License
281
+
282
+ [MIT](LICENSE)
@@ -0,0 +1,17 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 120" role="img" aria-label="inkan">
2
+ <defs>
3
+ <filter id="ink" x="-10%" y="-10%" width="120%" height="120%">
4
+ <feTurbulence type="fractalNoise" baseFrequency="0.9" numOctaves="2" seed="7" result="noise"/>
5
+ <feDisplacementMap in="SourceGraphic" in2="noise" scale="2.2"/>
6
+ </filter>
7
+ </defs>
8
+ <g transform="rotate(-4 60 60)" filter="url(#ink)">
9
+ <rect x="10" y="10" width="100" height="100" rx="22" fill="#c4381f"/>
10
+ <rect x="20" y="20" width="80" height="80" rx="13" fill="none" stroke="#fbf7ef" stroke-width="3.5"/>
11
+ <g fill="none" stroke="#fbf7ef" stroke-width="6" stroke-linecap="round" stroke-linejoin="round">
12
+ <path d="M44 34 C37 34 36 39 36 46 L36 53 C36 57.5 33.5 60 30 60 C33.5 60 36 62.5 36 67 L36 74 C36 81 37 86 44 86"/>
13
+ <path d="M76 34 C83 34 84 39 84 46 L84 53 C84 57.5 86.5 60 90 60 C86.5 60 84 62.5 84 67 L84 74 C84 81 83 86 76 86"/>
14
+ <path d="M48.5 61 L56.5 69 L72 51"/>
15
+ </g>
16
+ </g>
17
+ </svg>
package/dist/app.d.ts ADDED
@@ -0,0 +1,222 @@
1
+ import { type IncomingMessage, type Server, type ServerResponse } from "node:http";
2
+ import { type Infer, type Schema } from "./schema.ts";
3
+ import { type OpenAPIInfo } from "./openapi.ts";
4
+ import { type CheckOptions, type CheckReport } from "./check.ts";
5
+ export type Responses = {
6
+ [status: number]: Schema<any>;
7
+ };
8
+ export type Example = {
9
+ name?: string;
10
+ params?: Record<string, unknown>;
11
+ query?: Record<string, unknown>;
12
+ headers?: Record<string, string>;
13
+ body?: unknown;
14
+ /** The status this example has to answer with. Defaults to the first 2xx in `response`. */
15
+ status?: number;
16
+ /** A part of the response body that has to be in the answer, compared deeply. */
17
+ expect?: unknown;
18
+ };
19
+ export type RouteSpec<P, Q, B, H, R extends Responses> = {
20
+ summary?: string;
21
+ description?: string;
22
+ tags?: string[];
23
+ operationId?: string;
24
+ deprecated?: boolean;
25
+ /** Keeps the route out of the docs and the OpenAPI document. */
26
+ hidden?: boolean;
27
+ params?: Schema<P>;
28
+ query?: Schema<Q>;
29
+ headers?: Schema<H>;
30
+ body?: Schema<B>;
31
+ response?: R;
32
+ examples?: Example[];
33
+ /** Middleware for this route only. It runs after the input was validated. */
34
+ use?: Middleware[];
35
+ };
36
+ type Simplify<T> = {
37
+ [K in keyof T]: T[K];
38
+ } & {};
39
+ type ParamsOf<P extends string> = P extends `${string}:${infer Name}/${infer Rest}` ? {
40
+ [K in Name]: string;
41
+ } & ParamsOf<`/${Rest}`> : P extends `${string}:${infer Name}` ? {
42
+ [K in Name]: string;
43
+ } : P extends `${string}*${infer W}` ? {
44
+ [K in W extends "" ? "rest" : W]: string;
45
+ } : {};
46
+ export type PathParams<P extends string> = string extends P ? Record<string, string> : Simplify<ParamsOf<P>>;
47
+ type RawQuery = Record<string, string | string[]>;
48
+ type RawHeaders = Record<string, string | undefined>;
49
+ type SuccessStatus = 200 | 201 | 202 | 203 | 206 | 207;
50
+ type SuccessBody<R> = {} extends R ? unknown : {
51
+ [K in keyof R]: K extends SuccessStatus ? Infer<R[K]> : never;
52
+ }[keyof R];
53
+ export declare class Reply<S extends number = number, Body = unknown> {
54
+ status: S;
55
+ body: Body;
56
+ headers: Record<string, string>;
57
+ constructor(status: S, body: Body, headers?: Record<string, string>);
58
+ }
59
+ /** Answers with a status that is not the default one, or with extra headers. */
60
+ export declare const reply: <S extends number, B>(status: S, body?: B, headers?: Record<string, string>) => Reply<S, B | undefined>;
61
+ export type Context<P = Record<string, string>, Q = RawQuery, B = unknown, H = RawHeaders, R extends Responses = {}> = {
62
+ method: string;
63
+ path: string;
64
+ url: URL;
65
+ params: P;
66
+ query: Q;
67
+ headers: H;
68
+ body: B;
69
+ /** Free space for middleware to hand things to the handler. */
70
+ state: Record<string, unknown>;
71
+ /** The route that matched, as it was written. Undefined before routing. */
72
+ route?: {
73
+ method: string;
74
+ path: string;
75
+ };
76
+ /** Sets the status used when the handler returns a plain value. */
77
+ status(code: number): void;
78
+ header(name: string, value: string): void;
79
+ reply<S extends keyof R & number>(status: S, body: Infer<R[S]>, headers?: Record<string, string>): Reply<S>;
80
+ /** Only there when the request came through a socket, not through `inject`. */
81
+ req?: IncomingMessage;
82
+ res?: ServerResponse;
83
+ };
84
+ type Result<R extends Responses> = SuccessBody<R> | Reply | void;
85
+ export type Handler<P, Q, B, H, R extends Responses> = (ctx: Context<P, Q, B, H, R>) => Result<R> | Promise<Result<R>>;
86
+ export type Middleware = (ctx: Context<any, any, any, any, any>, next: () => Promise<void>) => unknown;
87
+ export type RouteRecord = {
88
+ method: string;
89
+ path: string;
90
+ spec: RouteSpec<any, any, any, any, Responses>;
91
+ handler: Handler<any, any, any, any, any>;
92
+ use: Middleware[];
93
+ };
94
+ type RouteMethod<Self> = {
95
+ <Path extends string, P = PathParams<Path>, Q = RawQuery, B = undefined, H = RawHeaders, R extends Responses = {}>(path: Path, spec: RouteSpec<P, Q, B, H, R>, handler: Handler<P, Q, B, H, R>): Self;
96
+ <Path extends string>(path: Path, handler: Handler<PathParams<Path>, RawQuery, unknown, RawHeaders, {}>): Self;
97
+ };
98
+ /** A set of routes that can be mounted under a prefix. */
99
+ export declare class Routes {
100
+ /** @internal */
101
+ _records: RouteRecord[];
102
+ /** @internal */
103
+ _use: Middleware[];
104
+ /** For a group, middleware that every route in it gets. Add it before mounting. */
105
+ use(...mw: Middleware[]): this;
106
+ protected define(method: string, path: string, a: unknown, b?: unknown): this;
107
+ /** @internal */
108
+ add(r: RouteRecord): void;
109
+ get: RouteMethod<this>;
110
+ post: RouteMethod<this>;
111
+ put: RouteMethod<this>;
112
+ patch: RouteMethod<this>;
113
+ delete: RouteMethod<this>;
114
+ mount(prefix: string, group: Routes): this;
115
+ }
116
+ export declare const routes: () => Routes;
117
+ export type AppOptions = OpenAPIInfo & {
118
+ /** Where the docs page lives, or false. Default `/docs`. */
119
+ docs?: string | false;
120
+ /** Where the OpenAPI document lives, or false. Default `/openapi.json`. */
121
+ openapi?: string | false;
122
+ /** The request inspector. On by default in development, and it only answers loopback addresses. */
123
+ inspector?: string | false;
124
+ /** Checks what handlers return against `response`. Default: on in development. */
125
+ validateResponses?: boolean;
126
+ /** Largest accepted request body in bytes. Default 1 MiB. */
127
+ bodyLimit?: number;
128
+ /** One line per request on the console. Default: on in development. */
129
+ log?: boolean;
130
+ /** Close in-flight requests cleanly on SIGINT and SIGTERM. Default true. */
131
+ gracefulShutdown?: boolean;
132
+ /** Development mode. Default: NODE_ENV is not "production". */
133
+ dev?: boolean;
134
+ onError?: (error: unknown, ctx: Context<any, any, any, any, any>) => void;
135
+ };
136
+ export type InjectOptions = {
137
+ method?: string;
138
+ url: string;
139
+ headers?: Record<string, string>;
140
+ /** Objects are sent as JSON. */
141
+ body?: unknown;
142
+ };
143
+ export type InjectResponse = {
144
+ status: number;
145
+ headers: Record<string, string>;
146
+ text: string;
147
+ /** Parsed JSON when the answer was JSON, otherwise the text. */
148
+ body: any;
149
+ };
150
+ type RawRequest = {
151
+ method: string;
152
+ url: string;
153
+ headers: Record<string, string | string[] | undefined>;
154
+ body?: Buffer;
155
+ remote?: string;
156
+ req?: IncomingMessage;
157
+ res?: ServerResponse;
158
+ };
159
+ type RawResponse = {
160
+ status: number;
161
+ headers: Record<string, string>;
162
+ body?: string | Buffer;
163
+ };
164
+ export type LogEntry = {
165
+ id: number;
166
+ at: string;
167
+ method: string;
168
+ path: string;
169
+ route?: string;
170
+ status: number;
171
+ ms: number;
172
+ notes: string[];
173
+ request: {
174
+ headers: Record<string, string>;
175
+ body?: string;
176
+ };
177
+ response: {
178
+ body?: string;
179
+ };
180
+ };
181
+ export declare class App extends Routes {
182
+ options: AppOptions;
183
+ dev: boolean;
184
+ private router;
185
+ private global;
186
+ private log;
187
+ private logId;
188
+ private spec?;
189
+ constructor(options?: AppOptions);
190
+ /** Middleware for every request, before routing. */
191
+ use(...mw: Middleware[]): this;
192
+ /** @internal */
193
+ add(r: RouteRecord): void;
194
+ routes(): RouteRecord[];
195
+ openapi(): Record<string, unknown>;
196
+ /** Runs every route's examples against the app, without a socket. */
197
+ check(opts?: CheckOptions): Promise<CheckReport>;
198
+ /** Sends a request straight into the app. Good for tests: no port, no network. */
199
+ inject(opts: InjectOptions): Promise<InjectResponse>;
200
+ /** @internal */
201
+ handle(raw: RawRequest): Promise<RawResponse>;
202
+ private respond;
203
+ private fail;
204
+ private builtin;
205
+ private remember;
206
+ /** A plain Node request listener, for `http.createServer` or anything that takes one. */
207
+ get listener(): (req: IncomingMessage, res: ServerResponse) => undefined;
208
+ private serve;
209
+ /**
210
+ * Starts listening. The port comes from the argument, then $PORT, then 3000,
211
+ * so it runs under warden, a PaaS or a container without changes.
212
+ */
213
+ listen(port?: number, host?: string): Promise<Server>;
214
+ private banner;
215
+ }
216
+ export declare const inkan: (options?: AppOptions) => App;
217
+ /**
218
+ * The schema a status answers with. A route that takes input also promises
219
+ * a 400 problem for input that breaks the contract, without saying so.
220
+ */
221
+ export declare function contractFor(route: RouteRecord, status: number): Schema<any> | undefined;
222
+ export {};