@ozanarslan/corpus 0.0.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 (87) hide show
  1. package/LICENSE.txt +20 -0
  2. package/README.md +337 -0
  3. package/dist/App/index.d.ts +377 -0
  4. package/dist/App/index.d.ts.map +1 -0
  5. package/dist/C.namespace.d.ts +27 -0
  6. package/dist/C.namespace.d.ts.map +1 -0
  7. package/dist/Config/index.d.ts +163 -0
  8. package/dist/Config/index.d.ts.map +1 -0
  9. package/dist/Context/ContextAccess/index.d.ts +50 -0
  10. package/dist/Context/ContextAccess/index.d.ts.map +1 -0
  11. package/dist/Context/index.d.ts +162 -0
  12. package/dist/Context/index.d.ts.map +1 -0
  13. package/dist/Controller/index.d.ts +164 -0
  14. package/dist/Controller/index.d.ts.map +1 -0
  15. package/dist/Cookies/index.d.ts +81 -0
  16. package/dist/Cookies/index.d.ts.map +1 -0
  17. package/dist/Cors/index.d.ts +135 -0
  18. package/dist/Cors/index.d.ts.map +1 -0
  19. package/dist/Exception/index.d.ts +85 -0
  20. package/dist/Exception/index.d.ts.map +1 -0
  21. package/dist/Globals/AppsRegistry/index.d.ts +48 -0
  22. package/dist/Globals/AppsRegistry/index.d.ts.map +1 -0
  23. package/dist/Globals/ParsersRegistry/index.d.ts +93 -0
  24. package/dist/Globals/ParsersRegistry/index.d.ts.map +1 -0
  25. package/dist/Globals/index.d.ts +109 -0
  26. package/dist/Globals/index.d.ts.map +1 -0
  27. package/dist/Headers/index.d.ts +208 -0
  28. package/dist/Headers/index.d.ts.map +1 -0
  29. package/dist/Middleware/index.d.ts +125 -0
  30. package/dist/Middleware/index.d.ts.map +1 -0
  31. package/dist/ParserBase/BodyParser/index.d.ts +138 -0
  32. package/dist/ParserBase/BodyParser/index.d.ts.map +1 -0
  33. package/dist/ParserBase/FormDataParser/index.d.ts +57 -0
  34. package/dist/ParserBase/FormDataParser/index.d.ts.map +1 -0
  35. package/dist/ParserBase/SchemaParser/index.d.ts +143 -0
  36. package/dist/ParserBase/SchemaParser/index.d.ts.map +1 -0
  37. package/dist/ParserBase/SearchParamsParser/index.d.ts +61 -0
  38. package/dist/ParserBase/SearchParamsParser/index.d.ts.map +1 -0
  39. package/dist/ParserBase/URLParamsParser/index.d.ts +32 -0
  40. package/dist/ParserBase/URLParamsParser/index.d.ts.map +1 -0
  41. package/dist/ParserBase/index.d.ts +92 -0
  42. package/dist/ParserBase/index.d.ts.map +1 -0
  43. package/dist/RateLimiter/index.d.ts +321 -0
  44. package/dist/RateLimiter/index.d.ts.map +1 -0
  45. package/dist/Request/index.d.ts +81 -0
  46. package/dist/Request/index.d.ts.map +1 -0
  47. package/dist/Res/index.d.ts +353 -0
  48. package/dist/Res/index.d.ts.map +1 -0
  49. package/dist/RouteBase/BundleRoute/index.d.ts +268 -0
  50. package/dist/RouteBase/BundleRoute/index.d.ts.map +1 -0
  51. package/dist/RouteBase/FileRoute/index.d.ts +121 -0
  52. package/dist/RouteBase/FileRoute/index.d.ts.map +1 -0
  53. package/dist/RouteBase/Route/index.d.ts +83 -0
  54. package/dist/RouteBase/Route/index.d.ts.map +1 -0
  55. package/dist/RouteBase/StaticRoute/index.d.ts +149 -0
  56. package/dist/RouteBase/StaticRoute/index.d.ts.map +1 -0
  57. package/dist/RouteBase/WebSocketRoute/index.d.ts +117 -0
  58. package/dist/RouteBase/WebSocketRoute/index.d.ts.map +1 -0
  59. package/dist/RouteBase/index.d.ts +206 -0
  60. package/dist/RouteBase/index.d.ts.map +1 -0
  61. package/dist/Server/index.d.ts +44 -0
  62. package/dist/Server/index.d.ts.map +1 -0
  63. package/dist/XFile/index.d.ts +202 -0
  64. package/dist/XFile/index.d.ts.map +1 -0
  65. package/dist/exports.d.ts +11 -0
  66. package/dist/exports.d.ts.map +1 -0
  67. package/dist/index.d.ts +5 -0
  68. package/dist/index.d.ts.map +1 -0
  69. package/dist/index.js +12 -0
  70. package/dist/index.js.map +47 -0
  71. package/dist/initialize.d.ts +22 -0
  72. package/dist/initialize.d.ts.map +1 -0
  73. package/dist/utils/assert.d.ts +7 -0
  74. package/dist/utils/assert.d.ts.map +1 -0
  75. package/dist/utils/is.d.ts +17 -0
  76. package/dist/utils/is.d.ts.map +1 -0
  77. package/dist/utils/lazy.d.ts +27 -0
  78. package/dist/utils/lazy.d.ts.map +1 -0
  79. package/dist/utils/logger.d.ts +31 -0
  80. package/dist/utils/logger.d.ts.map +1 -0
  81. package/dist/utils/object.d.ts +20 -0
  82. package/dist/utils/object.d.ts.map +1 -0
  83. package/dist/utils/path.d.ts +6 -0
  84. package/dist/utils/path.d.ts.map +1 -0
  85. package/dist/utils/tuple.d.ts +3 -0
  86. package/dist/utils/tuple.d.ts.map +1 -0
  87. package/package.json +57 -0
package/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ MIT License
2
+ Copyright (c) 2026 ozanArslan2424
3
+
4
+ Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ of this software and associated documentation files (the "Software"), to deal
6
+ in the Software without restriction, including without limitation the rights
7
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the Software is
9
+ furnished to do so, subject to the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included in all
12
+ copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,337 @@
1
+ # @ozanarslan/corpus
2
+
3
+ [Documentation](https://corpus-docs.fly.dev/)
4
+
5
+ A minimal HTTP framework for [Bun](https://bun.sh), written in TypeScript with zero runtime dependencies.
6
+
7
+ Corpus is built around plain classes. A route is an object you construct, not a callback you hand to a builder — so routes, controllers and middleware are values you can hold, extend, and call directly in a test without starting a server. Request parsing is lazy and analysed ahead of time: a body nobody reads is never parsed, and a route with no schemas skips validation entirely.
8
+
9
+ Validation uses [Standard Schema](https://standardschema.dev), so Zod, Valibot, ArkType and anything else implementing the spec work out of the box, and their inferred types flow into your handlers.
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ bun add @ozanarslan/corpus
15
+ ```
16
+
17
+ Bun 1.2 or newer. Corpus uses `Bun.serve`, `Bun.file` and `Bun.CookieMap`, so it does not run on Node. Node support may be implemented in the future.
18
+
19
+ ## Hello world
20
+
21
+ ```ts
22
+ import { C } from "@ozanarslan/corpus";
23
+
24
+ const app = new C.App({ port: 3000 });
25
+
26
+ new C.Route("GET /", () => ({ message: "hello" }));
27
+
28
+ await app.listen();
29
+ ```
30
+
31
+ ```bash
32
+ bun run index.ts
33
+ ```
34
+
35
+ Constructing a `C.Route` registers it on the most recently constructed `C.App`, so you never pass the app around. Nothing is compiled until `listen()`, which means routes and middleware can be declared in any order across any number of files.
36
+
37
+ ## Responses
38
+
39
+ Return a value and corpus works out how to send it — objects become JSON, strings become text, typed arrays stay binary, streams pass through untouched. Each gets a matching `Content-Type` unless you set one yourself.
40
+
41
+ ```ts
42
+ new C.Route("GET /users", () => users); // application/json
43
+ new C.Route("GET /ping", () => "pong"); // text/plain
44
+ new C.Route("GET /avatar", () => imageBytes); // application/octet-stream
45
+ ```
46
+
47
+ Reach for `C.Res` when you need a status, headers, cookies, or a body form a return value can't express. `Context` holds a fresh value by default so there is no need to construct it again.
48
+
49
+ ```ts
50
+ new C.Route("POST /users", async (c) => {
51
+ const user = await createUser(c.body);
52
+ return new C.Res(user, { status: C.Status.CREATED });
53
+ });
54
+
55
+ new C.Route("GET /report", (c) => c.res.file("./report.pdf"));
56
+ new C.Route("GET /old", (c) => c.res.permanentRedirect("/new"));
57
+
58
+ new C.Route("GET /users", (c) => {
59
+ c.res.headers.set("X-Total-Count", users.length);
60
+ c.res.cookies.set("seen", "1");
61
+ return users;
62
+ });
63
+ ```
64
+
65
+ ## The context
66
+
67
+ Every handler receives a `Context` holding the request and its parsed views.
68
+
69
+ ```ts
70
+ new C.Route("POST /users/:id/notes", (c) => {
71
+ c.params; // parsed path parameters
72
+ c.search; // parsed query string
73
+ c.body; // parsed request body
74
+ c.data; // request-scoped state, shared with middleware
75
+ c.req; // the untouched Request
76
+ c.res; // the response being built
77
+ c.url; // the Request's URL object
78
+ });
79
+ ```
80
+
81
+ `params`, `search` and `body` are populated only when your handler chain actually reads them — corpus inspects the handlers at startup to decide. Query strings and form bodies support bracket nesting, so `?filter[status]=open&tags[0]=a` arrives as a real object, and repeated keys collect into arrays.
82
+
83
+ ## Validation
84
+
85
+ Pass schemas as the third argument. They validate the request and type the context:
86
+
87
+ ```ts
88
+ import { z } from "zod";
89
+
90
+ new C.Route(
91
+ "POST /users",
92
+ (c) => createUser(c.body), // c.body is typed from the schema
93
+ {
94
+ body: z.object({ email: z.string().email(), age: z.number() }),
95
+ search: z.object({ notify: z.boolean().optional() }),
96
+ },
97
+ );
98
+ ```
99
+
100
+ A failure responds with `422` and a message naming the offending fields and what was received.
101
+
102
+ The same object also takes `maxRequestBodySize`, to tighten the app-wide limit for one endpoint:
103
+
104
+ ```ts
105
+ new C.Route("POST /upload", handler, { maxRequestBodySize: 50 * 1024 * 1024 });
106
+ ```
107
+
108
+ ## Errors
109
+
110
+ Throw a `C.Exception` to end a request with a status. Anything else that throws becomes an opaque `500`, so an accidental `TypeError` never leaks its message. Replace `app.handleError` to change how errors are rendered.
111
+
112
+ ```ts
113
+ new C.Route("GET /users/:id", (c) => {
114
+ const user = findUser(c.params.id);
115
+ if (!user) throw new C.Exception("User not found", C.Status.NOT_FOUND);
116
+ return user;
117
+ });
118
+ ```
119
+
120
+ ## Middleware
121
+
122
+ A middleware runs around whatever comes after it. Everything before `await next()` happens on the way in, everything after it on the way out. Return early instead of calling `next()` to short-circuit the chain.
123
+
124
+ ```ts
125
+ new C.Middleware({
126
+ handler: async (c, next) => {
127
+ const started = performance.now();
128
+ const result = await next();
129
+ c.res.headers.set("X-Response-Time", performance.now() - started);
130
+ return result;
131
+ },
132
+ });
133
+ ```
134
+
135
+ With no `useOn`, it applies to every route. Target specific ones by passing routes, controllers, or route ids:
136
+
137
+ ```ts
138
+ const usersRoute = new C.Route("GET /users", handler);
139
+
140
+ new C.Middleware({
141
+ useOn: usersRoute,
142
+ handler: (c, next) => {
143
+ if (!isAuthorized(c.req)) throw new C.Exception("Unauthorized", C.Status.UNAUTHORIZED);
144
+ return next();
145
+ },
146
+ });
147
+ ```
148
+
149
+ Use `c.data` to pass state downstream. Augment `ContextDataInterface` to type it:
150
+
151
+ ```ts
152
+ declare module "@ozanarslan/corpus" {
153
+ interface ContextDataInterface {
154
+ user: User;
155
+ }
156
+ }
157
+ ```
158
+
159
+ ## Controllers
160
+
161
+ A `C.Controller` groups routes under a shared prefix and an optional preamble. Subclassing is recommended.
162
+
163
+ ```ts
164
+ const users = new C.Controller("/users");
165
+ users.beforeEach = (c) => authenticate(c);
166
+
167
+ users.route("GET /", () => listUsers());
168
+ users.route("GET /:id", (c) => findUser(c.params.id));
169
+ users.route("POST /", (c) => createUser(c.body), { body: userSchema });
170
+
171
+ class UserController extends C.Controller {
172
+ constructor(private readonly service: UserService) {
173
+ super("/users");
174
+ }
175
+
176
+ list = this.route("GET /", () => this.service.list());
177
+ }
178
+ ```
179
+
180
+ The methods mirror the route constructors exactly, so moving a route into a controller only changes the call site. `users.routeIds` gives you the whole group to target with middleware.
181
+
182
+ ## Serving files
183
+
184
+ Four route kinds, differing in what they read and when.
185
+
186
+ ```ts
187
+ // One file, read per request.
188
+ new C.FileRoute("GET /robots.txt", "./public/robots.txt");
189
+ new C.FileRoute("GET /report", { filePath: "./report.pdf", disposition: "attachment" });
190
+
191
+ // One file, read once at startup and held in memory.
192
+ new C.StaticRoute("GET /about", "./pages/about.html");
193
+
194
+ // ...optionally passed through a handler first, for templating.
195
+ new C.StaticRoute("GET /", "./pages/index.html", (c, html) => html.replace("{{title}}", title));
196
+
197
+ // A whole directory of built files, with per-file-class caching.
198
+ new C.BundleRoute("/*", "./dist");
199
+ ```
200
+
201
+ `C.BundleRoute` answers unresolvable paths with the entry document instead of a 404, which is what keeps a single-page app working on a hard refresh. Hashed assets are cached immutably, the entry document is revalidated every time.
202
+
203
+ ## Streaming
204
+
205
+ ```ts
206
+ // Server-sent events
207
+ new C.Route("GET /events", (c) =>
208
+ c.res.sse(async (send) => {
209
+ for await (const event of source) send({ data: event, event: "update" });
210
+ }),
211
+ );
212
+
213
+ // Newline-delimited JSON
214
+ new C.Route("GET /export", (c) =>
215
+ c.res.ndjson(async (send) => {
216
+ for await (const row of rows) send(row);
217
+ }),
218
+ );
219
+
220
+ // A file, without buffering it
221
+ new C.Route("GET /video", (c) => c.res.streamFile("./clip.mp4", "inline"));
222
+ ```
223
+
224
+ Return a cleanup function from an `sse` or `ndjson` source to keep the stream open indefinitely; it runs when the client disconnects.
225
+
226
+ ## WebSockets
227
+
228
+ ```ts
229
+ new C.WebSocketRoute("/chat", {
230
+ onOpen: (ws) => ws.subscribe("room"),
231
+ onMessage: (ws, message) => ws.publish("room", message),
232
+ onClose: (ws) => ws.unsubscribe("room"),
233
+ });
234
+ ```
235
+
236
+ One route instance backs every connection, so per-socket state belongs on the socket, not on the route.
237
+
238
+ ## CORS
239
+
240
+ ```ts
241
+ new C.Cors({
242
+ allowedOrigins: ["https://example.com"],
243
+ allowedMethods: ["GET", "POST"],
244
+ credentials: true,
245
+ });
246
+ ```
247
+
248
+ CORS runs after the middleware chain, separately from it, so a short-circuiting middleware can't drop the headers. Without a `C.Cors`, preflights get a bare `204` and no CORS headers are sent.
249
+
250
+ ## Rate limiting
251
+
252
+ ```ts
253
+ new C.RateLimiter({
254
+ windowMs: 60_000,
255
+ limits: { authenticated: 120, ipBased: 60, fingerprint: 20 },
256
+ });
257
+ ```
258
+
259
+ Callers are identified by what the request proves about itself — a token, then an IP, then a header fingerprint — each with its own limit, since they differ in how forgeable they are. Identifiers are hashed with a rotating salt, so no token or address is ever held in memory.
260
+
261
+ Construct it after your routes; it targets the ones registered so far, excluding bundle routes. Counting is in-process by default — pass a `store` implementing `RateLimiterStoreInterface` to share state across instances.
262
+
263
+ ## Testing routes
264
+
265
+ Routes are objects, so call one directly. No server, no HTTP:
266
+
267
+ ```ts
268
+ import { expect, test } from "bun:test";
269
+
270
+ const route = new C.Route("GET /users/:id", (c) => findUser(c.params.id));
271
+
272
+ test("finds a user", async () => {
273
+ const result = await route.handle({ params: { id: 1 } });
274
+ expect(result).toEqual({ id: 1, name: "Ada" });
275
+ });
276
+ ```
277
+
278
+ `handle()` takes already-parsed values and runs the handler alone — middleware and validation are the app's job, not the route's. To exercise the full pipeline, use `app.handle(request)`.
279
+
280
+ ## Configuration
281
+
282
+ `C.Config` reads environment variables with parsing and fallbacks:
283
+
284
+ ```ts
285
+ C.Config.get("PORT", { parser: Number, fallback: 3000 });
286
+ C.Config.require("DATABASE_URL"); // throws if unset
287
+ C.Config.isProd;
288
+ ```
289
+
290
+ Augment `Env` to get autocompletion for your own variables:
291
+
292
+ ```ts
293
+ declare module "@ozanarslan/corpus" {
294
+ interface Env {
295
+ DATABASE_URL: string;
296
+ JWT_SECRET: string;
297
+ }
298
+ }
299
+ ```
300
+
301
+ ## Replacing the parsers
302
+
303
+ Every parsing decision resolves through one registry, and every slot is typed as an interface rather than a class — so a replacement only has to satisfy the contract.
304
+
305
+ ```ts
306
+ C.setParsersRegistry({ bodyParser: new MyBodyParser() });
307
+ ```
308
+
309
+ Call it before `app.listen()`. Routes resolve their parsers as they are compiled, so a later override is never reached.
310
+
311
+ ## App options
312
+
313
+ ```ts
314
+ const app = new C.App({
315
+ port: 3000,
316
+ hostname: "0.0.0.0",
317
+ prefix: "/api",
318
+ idleTimeout: 30,
319
+ maxRequestBodySize: 10 * 1024 * 1024,
320
+ tls: { cert, key },
321
+ });
322
+
323
+ app.handleBeforeListen = () => {
324
+ db.connect();
325
+ };
326
+ app.handleBeforeClose = () => {
327
+ db.disconnect();
328
+ };
329
+
330
+ await app.listen();
331
+ ```
332
+
333
+ `listen()` installs `SIGINT` and `SIGTERM` handlers, so the app shuts down cleanly on its own.
334
+
335
+ ## License
336
+
337
+ MIT