@sigitex/route 1.0.0 → 1.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.
- package/README.md +382 -2
- package/package.json +6 -6
- package/src/cloudflare/env.d.ts +1 -0
- package/src/bun/tsconfig.json +0 -8
- package/src/cloudflare/tsconfig.json +0 -8
package/README.md
CHANGED
|
@@ -1,7 +1,387 @@
|
|
|
1
1
|
# @sigitex/route
|
|
2
2
|
|
|
3
|
-
`bun add @sigitex/route`
|
|
4
|
-
|
|
5
3
|
A server-side web framework.
|
|
6
4
|
|
|
5
|
+
`bun add @sigitex/route`
|
|
6
|
+
|
|
7
7
|
> **Note:** This package currently exports TypeScript sources directly. A TypeScript-compatible runtime or bundler (Bun, etc.) is required.
|
|
8
|
+
|
|
9
|
+
## Quick Start
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { route, get, post, prefix, cors, hardened, cookies } from "@sigitex/route"
|
|
13
|
+
import { bun } from "@sigitex/route/bun"
|
|
14
|
+
|
|
15
|
+
const fetch = route(
|
|
16
|
+
bun({ assets: "./public" }),
|
|
17
|
+
get("/health", () => ({ status: "ok" })),
|
|
18
|
+
prefix("/api/", [cors(), cookies(), hardened()],
|
|
19
|
+
get("/users", async () => {
|
|
20
|
+
return Response.json(await getUsers())
|
|
21
|
+
}),
|
|
22
|
+
get("/users/:id", async ({ params }: { params: { id: string } }) => {
|
|
23
|
+
return Response.json(await getUser(params.id))
|
|
24
|
+
}),
|
|
25
|
+
post("/users", async ({ request }: { request: Request }) => {
|
|
26
|
+
const body = await request.json()
|
|
27
|
+
return Response.json(await createUser(body))
|
|
28
|
+
}),
|
|
29
|
+
),
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
Bun.serve({ fetch })
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
On Cloudflare Workers:
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
import { route, get } from "@sigitex/route"
|
|
39
|
+
import { cloudflare } from "@sigitex/route/cloudflare"
|
|
40
|
+
|
|
41
|
+
export default {
|
|
42
|
+
fetch: route(
|
|
43
|
+
cloudflare(),
|
|
44
|
+
get("/hello", () => ({ hello: "world" })),
|
|
45
|
+
),
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Entry Points
|
|
50
|
+
|
|
51
|
+
| Import | Description |
|
|
52
|
+
| --------------------------- | ----------------------------------- |
|
|
53
|
+
| `@sigitex/route` | Core router, handlers, middleware |
|
|
54
|
+
| `@sigitex/route/bun` | Bun runtime adapter |
|
|
55
|
+
| `@sigitex/route/cloudflare` | Cloudflare Workers runtime adapter |
|
|
56
|
+
|
|
57
|
+
## `route(...handlers)`
|
|
58
|
+
|
|
59
|
+
Creates a fetch function `(request: Request, env: Env) => Promise<Response>` from a list of handlers. Handlers are tried in order; the first to return a value produces the response. If none match, a 404 is returned.
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
const fetch = route(handler1, handler2, handler3)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
An optional `RouterOptions` object can be passed as the first argument:
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
const fetch = route({ container, middlewares: [cors()] }, handler1, handler2)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Falsy values (`null`, `undefined`, `false`, `0`) are silently ignored, allowing conditional handlers:
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
route(
|
|
75
|
+
isDev && get("/debug", debugHandler),
|
|
76
|
+
get("/", homeHandler),
|
|
77
|
+
)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### `RouterOptions`
|
|
81
|
+
|
|
82
|
+
| Option | Type | Description |
|
|
83
|
+
| ------------- | ----------------- | ------------------------------------------------------ |
|
|
84
|
+
| `container` | `Container` | A `@sigitex/bind` IoC container for dependency injection |
|
|
85
|
+
| `middlewares` | `RouteMiddleware[]` | Global middlewares applied to every dispatched handler |
|
|
86
|
+
|
|
87
|
+
## Handlers
|
|
88
|
+
|
|
89
|
+
Handlers are functions that receive a context object and return a `Response`, a JSON-serializable value, or `undefined` to skip.
|
|
90
|
+
|
|
91
|
+
### `get(path, handler, ...middlewares)`
|
|
92
|
+
|
|
93
|
+
Matches GET requests against `path`. Path parameters use `regexparam` syntax (`:param`, `*`).
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
get("/users/:id", ({ params }: { params: { id: string } }) => {
|
|
97
|
+
return Response.json({ id: params.id })
|
|
98
|
+
})
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### `post(path, handler, ...middlewares)`
|
|
102
|
+
|
|
103
|
+
Matches POST requests.
|
|
104
|
+
|
|
105
|
+
### `put(path, handler, ...middlewares)`
|
|
106
|
+
|
|
107
|
+
Matches PUT requests.
|
|
108
|
+
|
|
109
|
+
### `del(path, handler, ...middlewares)`
|
|
110
|
+
|
|
111
|
+
Matches DELETE requests.
|
|
112
|
+
|
|
113
|
+
### `patch(path, handler, ...middlewares)`
|
|
114
|
+
|
|
115
|
+
Matches PATCH requests.
|
|
116
|
+
|
|
117
|
+
### `pattern(method, path, handler, ...middlewares)`
|
|
118
|
+
|
|
119
|
+
Generic version -- pass `null` as the method to match any HTTP method.
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
pattern(null, "/any-method/:id", handler)
|
|
123
|
+
pattern("GET", "/explicit", handler)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### `prefix(prefix, ...handlers)`
|
|
127
|
+
|
|
128
|
+
Groups handlers under a URL prefix. The prefix is stripped from the URL before child handlers see it.
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
prefix("/api/v1/",
|
|
132
|
+
get("/users", listUsers), // matches /api/v1/users
|
|
133
|
+
get("/posts", listPosts), // matches /api/v1/posts
|
|
134
|
+
)
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Accepts an optional middlewares array as the second argument:
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
prefix("/api/", [cors(), bodyLimit()],
|
|
141
|
+
post("/upload", uploadHandler),
|
|
142
|
+
)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### `mount(fetchFn)`
|
|
146
|
+
|
|
147
|
+
Wraps a standard `(request: Request) => Promise<Response>` function as a handler. Useful for mounting sub-applications or external fetch handlers.
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
mount(subApp.fetch)
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### `assets()`
|
|
154
|
+
|
|
155
|
+
Serves static files via the platform's `Assets` binding. Returns `undefined` on 404 so subsequent handlers can match.
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
route(bun(), assets(), get("/", homeHandler))
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### `app(routes)`
|
|
162
|
+
|
|
163
|
+
Serves `index.html` for paths matching a client-side `RouteTree`. Intended for single-page applications where the client handles routing.
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
app({
|
|
167
|
+
users: "/users",
|
|
168
|
+
user: "/users/:id",
|
|
169
|
+
settings: { general: "/settings/general" },
|
|
170
|
+
})
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### `noop`
|
|
174
|
+
|
|
175
|
+
A handler that does nothing and returns `undefined`. Used internally by middleware composition.
|
|
176
|
+
|
|
177
|
+
### `use(middlewares, ...handlers)`
|
|
178
|
+
|
|
179
|
+
Applies a set of middlewares to a group of handlers without creating a prefix.
|
|
180
|
+
|
|
181
|
+
```ts
|
|
182
|
+
use([cors(), cookies()],
|
|
183
|
+
get("/a", handlerA),
|
|
184
|
+
get("/b", handlerB),
|
|
185
|
+
)
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### `filter(predicate)`
|
|
189
|
+
|
|
190
|
+
Higher-order function that conditionally runs a handler based on a predicate.
|
|
191
|
+
|
|
192
|
+
```ts
|
|
193
|
+
const onlyJson = filter(({ request }) =>
|
|
194
|
+
request.headers.get("Accept")?.includes("application/json") ?? false
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
onlyJson(get("/data", dataHandler))
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### `https()`
|
|
201
|
+
|
|
202
|
+
Redirects HTTP requests to HTTPS with a 301.
|
|
203
|
+
|
|
204
|
+
```ts
|
|
205
|
+
route(https(), get("/", homeHandler))
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### `www(options)`
|
|
209
|
+
|
|
210
|
+
Redirects non-www requests to the www subdomain.
|
|
211
|
+
|
|
212
|
+
```ts
|
|
213
|
+
www({ secure: true }) // also upgrades to https
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## Middleware
|
|
217
|
+
|
|
218
|
+
Middlewares are objects with optional `before` and `after` hooks. `before` runs before the handler; `after` runs after. Either can return a `Response` to short-circuit.
|
|
219
|
+
|
|
220
|
+
```ts
|
|
221
|
+
const myMiddleware: RouteMiddleware = {
|
|
222
|
+
before: ({ request, bind }) => {
|
|
223
|
+
bind({ startTime: Date.now() })
|
|
224
|
+
},
|
|
225
|
+
after: ({ response, startTime }) => {
|
|
226
|
+
response.headers.set("X-Duration", String(Date.now() - startTime))
|
|
227
|
+
},
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### `cors(options?)`
|
|
232
|
+
|
|
233
|
+
Handles CORS preflight and response headers.
|
|
234
|
+
|
|
235
|
+
| Option | Type | Default |
|
|
236
|
+
| --------------- | ------------------------------------------------- | ---------- |
|
|
237
|
+
| `origin` | `string \| string[] \| (origin: string) => boolean` | `"*"` |
|
|
238
|
+
| `methods` | `string[]` | all standard |
|
|
239
|
+
| `allowHeaders` | `string[]` | mirrors request |
|
|
240
|
+
| `exposeHeaders` | `string[]` | -- |
|
|
241
|
+
| `credentials` | `boolean` | `false` |
|
|
242
|
+
| `maxAge` | `number` | -- |
|
|
243
|
+
|
|
244
|
+
### `cookies()`
|
|
245
|
+
|
|
246
|
+
Parses request cookies and collects `Set-Cookie` headers on the response. Binds a `Cookies` object to context:
|
|
247
|
+
|
|
248
|
+
```ts
|
|
249
|
+
cookies.get("session") // read
|
|
250
|
+
cookies.set("session", token, { // write
|
|
251
|
+
httpOnly: true,
|
|
252
|
+
secure: true,
|
|
253
|
+
sameSite: "strict",
|
|
254
|
+
maxAge: 86400,
|
|
255
|
+
path: "/",
|
|
256
|
+
})
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
#### `CookieOptions`
|
|
260
|
+
|
|
261
|
+
`domain`, `expires`, `httpOnly`, `maxAge`, `path`, `sameSite` (`"strict" | "lax" | "none"`), `secure`.
|
|
262
|
+
|
|
263
|
+
### `bodyLimit(options?)`
|
|
264
|
+
|
|
265
|
+
Rejects requests exceeding a body size or with disallowed content types.
|
|
266
|
+
|
|
267
|
+
| Option | Type | Default |
|
|
268
|
+
| -------------- | ---------- | --------- |
|
|
269
|
+
| `maxSize` | `number` | 1 MB |
|
|
270
|
+
| `contentTypes` | `string[]` | any |
|
|
271
|
+
|
|
272
|
+
### `cache(options)`
|
|
273
|
+
|
|
274
|
+
Sets `Cache-Control` (and optionally `Vary`) headers on responses.
|
|
275
|
+
|
|
276
|
+
```ts
|
|
277
|
+
cache({ public: true, maxAge: 3600 })
|
|
278
|
+
cache("no-store")
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
#### `CacheOptions`
|
|
282
|
+
|
|
283
|
+
`public`, `private`, `maxAge`, `sMaxAge`, `noCache`, `noStore`, `mustRevalidate`, `proxyRevalidate`, `immutable`, `staleWhileRevalidate`, `staleIfError`, `vary`.
|
|
284
|
+
|
|
285
|
+
### `csp(options)`
|
|
286
|
+
|
|
287
|
+
Sets `Content-Security-Policy` headers. Supports automatic nonce generation via the `CSP.nonce` symbol -- the nonce is bound to context as `cspNonce`.
|
|
288
|
+
|
|
289
|
+
```ts
|
|
290
|
+
import { CSP } from "@sigitex/route"
|
|
291
|
+
|
|
292
|
+
csp({
|
|
293
|
+
defaultSrc: [CSP.self],
|
|
294
|
+
scriptSrc: [CSP.self, CSP.nonce],
|
|
295
|
+
styleSrc: [CSP.self, CSP.unsafeInline],
|
|
296
|
+
imgSrc: [CSP.self, CSP.data],
|
|
297
|
+
reportOnly: true,
|
|
298
|
+
})
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
### `csrf(options?)`
|
|
302
|
+
|
|
303
|
+
Double-submit cookie CSRF protection. Requires `cookies()` in the middleware stack.
|
|
304
|
+
|
|
305
|
+
| Option | Type | Default |
|
|
306
|
+
| --------- | ---------- | -------------------------------- |
|
|
307
|
+
| `cookie` | `string` | `"csrf-token"` |
|
|
308
|
+
| `header` | `string` | `"X-CSRF-Token"` |
|
|
309
|
+
| `methods` | `string[]` | POST, PUT, PATCH, DELETE |
|
|
310
|
+
|
|
311
|
+
### `rateLimit(options?)`
|
|
312
|
+
|
|
313
|
+
IP-based rate limiting with pluggable storage.
|
|
314
|
+
|
|
315
|
+
| Option | Type | Default |
|
|
316
|
+
| --------- | ------------------ | ------------------------ |
|
|
317
|
+
| `window` | `number` (seconds) | `60` |
|
|
318
|
+
| `max` | `number` | `100` |
|
|
319
|
+
| `key` | `(ctx) => string` | `rateLimit.ip` |
|
|
320
|
+
| `store` | `RateLimitStore` | `rateLimit.memory()` |
|
|
321
|
+
| `headers` | `boolean` | `true` |
|
|
322
|
+
|
|
323
|
+
Built-in helpers:
|
|
324
|
+
|
|
325
|
+
- `rateLimit.ip` -- key extractor using `CF-Connecting-IP` or `X-Forwarded-For`
|
|
326
|
+
- `rateLimit.memory()` -- in-memory sliding window store
|
|
327
|
+
|
|
328
|
+
### `hardened(options?)`
|
|
329
|
+
|
|
330
|
+
Convenience bundle applying `noSniff`, `frameGuard`, `referrerPolicy`, and `hsts`. Any can be disabled:
|
|
331
|
+
|
|
332
|
+
```ts
|
|
333
|
+
hardened() // all defaults
|
|
334
|
+
hardened({ hsts: false }) // skip HSTS
|
|
335
|
+
hardened({ frameGuard: "sameOrigin" }) // override frame guard
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
### `hsts(options?)`
|
|
339
|
+
|
|
340
|
+
Sets `Strict-Transport-Security`. Defaults: `max-age=31536000; includeSubDomains`.
|
|
341
|
+
|
|
342
|
+
| Option | Type | Default |
|
|
343
|
+
| ------------------- | --------- | ----------- |
|
|
344
|
+
| `maxAge` | `number` | `31536000` |
|
|
345
|
+
| `includeSubDomains` | `boolean` | `true` |
|
|
346
|
+
| `preload` | `boolean` | `false` |
|
|
347
|
+
|
|
348
|
+
### `noSniff`
|
|
349
|
+
|
|
350
|
+
Sets `X-Content-Type-Options: nosniff`.
|
|
351
|
+
|
|
352
|
+
### `frameGuard.deny` / `frameGuard.sameOrigin`
|
|
353
|
+
|
|
354
|
+
Sets `X-Frame-Options`.
|
|
355
|
+
|
|
356
|
+
### `referrerPolicy.*`
|
|
357
|
+
|
|
358
|
+
Pre-built policies: `noReferrer`, `noReferrerWhenDowngrade`, `origin`, `originWhenCrossOrigin`, `sameOrigin`, `strictOrigin`, `strictOriginWhenCrossOrigin`, `unsafeUrl`.
|
|
359
|
+
|
|
360
|
+
### `requestId(options?)`
|
|
361
|
+
|
|
362
|
+
Reads or generates a request ID, binds it to context as `requestId`, and echoes it on the response.
|
|
363
|
+
|
|
364
|
+
| Option | Type | Default |
|
|
365
|
+
| ---------- | -------------- | -------------------- |
|
|
366
|
+
| `header` | `string` | `"X-Request-Id"` |
|
|
367
|
+
| `generate` | `() => string` | `crypto.randomUUID` |
|
|
368
|
+
|
|
369
|
+
### `setHeader(header, value)`
|
|
370
|
+
|
|
371
|
+
Sets a header on every response. Returns a `ResponseHandler` (use as an `after` hook).
|
|
372
|
+
|
|
373
|
+
```ts
|
|
374
|
+
{ after: setHeader("X-Powered-By", "sigitex") }
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
## Errors
|
|
378
|
+
|
|
379
|
+
Error classes that can be thrown from handlers:
|
|
380
|
+
|
|
381
|
+
| Class | Status | Default Message |
|
|
382
|
+
| ------------------ | ------ | ------------------------ |
|
|
383
|
+
| `RouterError` | any | -- |
|
|
384
|
+
| `NotFound` | 404 | `"Not found."` |
|
|
385
|
+
| `MethodNotAllowed` | 405 | `"Method not allowed."` |
|
|
386
|
+
| `InvalidRequest` | 400 | `"Invalid request."` |
|
|
387
|
+
| `ServerError` | -- | `"Internal error."` |
|
package/package.json
CHANGED
|
@@ -21,9 +21,9 @@
|
|
|
21
21
|
"@commitlint/config-conventional": "^20.5.3",
|
|
22
22
|
"@markwylde/semantic-release-gitea": "^2.2.0",
|
|
23
23
|
"@semantic-release/exec": "^7.0.3",
|
|
24
|
-
"@sigitex/bind": "
|
|
25
|
-
"@sigitex/ssjs": "
|
|
26
|
-
"@types/bun": "1.3.
|
|
24
|
+
"@sigitex/bind": "^1.0.0",
|
|
25
|
+
"@sigitex/ssjs": "^1.1.0",
|
|
26
|
+
"@types/bun": "^1.3.13",
|
|
27
27
|
"@typescript/native-preview": "beta",
|
|
28
28
|
"husky": "^9.1.7",
|
|
29
29
|
"multi-semantic-release": "^3.1.0",
|
|
@@ -35,13 +35,13 @@
|
|
|
35
35
|
"regexparam": "3.0.0"
|
|
36
36
|
},
|
|
37
37
|
"scripts": {
|
|
38
|
-
"check": "tsgo --
|
|
39
|
-
"test": "bun test --pass-with-no-tests",
|
|
38
|
+
"check": "tsgo --build",
|
|
39
|
+
"test": "bun test --pass-with-no-tests --tsconfig-override tsconfig.test.json",
|
|
40
40
|
"prepare": "husky",
|
|
41
41
|
"lint": "oxlint"
|
|
42
42
|
},
|
|
43
43
|
"files": [
|
|
44
44
|
"src"
|
|
45
45
|
],
|
|
46
|
-
"version": "1.0.
|
|
46
|
+
"version": "1.0.1"
|
|
47
47
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
interface Env {}
|
package/src/bun/tsconfig.json
DELETED