@bepalo/spine 1.5.16 → 2.6.18
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 +533 -81
- package/dist/auth-middlewares.d.ts.map +1 -1
- package/dist/auth-middlewares.js +1 -0
- package/dist/auth-middlewares.js.map +1 -1
- package/dist/cache.d.ts +72 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +387 -0
- package/dist/cache.js.map +1 -0
- package/dist/cjs/auth-middlewares.d.ts.map +1 -1
- package/dist/cjs/auth-middlewares.js +1 -0
- package/dist/cjs/auth-middlewares.js.map +1 -1
- package/dist/cjs/cache.d.ts +72 -0
- package/dist/cjs/cache.d.ts.map +1 -0
- package/dist/cjs/cache.js +387 -0
- package/dist/cjs/cache.js.map +1 -0
- package/dist/cjs/exp-cache.d.ts +101 -0
- package/dist/cjs/exp-cache.d.ts.map +1 -0
- package/dist/cjs/exp-cache.js +551 -0
- package/dist/cjs/exp-cache.js.map +1 -0
- package/dist/cjs/generator.d.ts +142 -19
- package/dist/cjs/generator.d.ts.map +1 -1
- package/dist/cjs/generator.js +415 -35
- package/dist/cjs/generator.js.map +1 -1
- package/dist/cjs/helpers.d.ts +49 -2
- package/dist/cjs/helpers.d.ts.map +1 -1
- package/dist/cjs/helpers.js +169 -14
- package/dist/cjs/helpers.js.map +1 -1
- package/dist/cjs/index.d.ts +2 -0
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +3 -0
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/linked-list.d.ts +35 -0
- package/dist/cjs/linked-list.d.ts.map +1 -0
- package/dist/cjs/linked-list.js +300 -0
- package/dist/cjs/linked-list.js.map +1 -0
- package/dist/cjs/middlewares.d.ts +102 -12
- package/dist/cjs/middlewares.d.ts.map +1 -1
- package/dist/cjs/middlewares.js +119 -22
- package/dist/cjs/middlewares.js.map +1 -1
- package/dist/cjs/parsers.d.ts.map +1 -1
- package/dist/cjs/parsers.js +1 -0
- package/dist/cjs/parsers.js.map +1 -1
- package/dist/cjs/router.d.ts +5 -6
- package/dist/cjs/router.d.ts.map +1 -1
- package/dist/cjs/router.js +271 -148
- package/dist/cjs/router.js.map +1 -1
- package/dist/cjs/status.d.ts.map +1 -1
- package/dist/cjs/status.js +80 -79
- package/dist/cjs/status.js.map +1 -1
- package/dist/cjs/types.d.ts +74 -2
- package/dist/cjs/types.d.ts.map +1 -1
- package/dist/cjs/types.js +15 -2
- package/dist/cjs/types.js.map +1 -1
- package/dist/cjs/utils.d.ts.map +1 -1
- package/dist/cjs/utils.js +1 -0
- package/dist/cjs/utils.js.map +1 -1
- package/dist/cjs/utils.node.d.ts.map +1 -1
- package/dist/cjs/utils.node.js +1 -0
- package/dist/cjs/utils.node.js.map +1 -1
- package/dist/exp-cache.d.ts +101 -0
- package/dist/exp-cache.d.ts.map +1 -0
- package/dist/exp-cache.js +551 -0
- package/dist/exp-cache.js.map +1 -0
- package/dist/generator.d.ts +142 -19
- package/dist/generator.d.ts.map +1 -1
- package/dist/generator.js +415 -35
- package/dist/generator.js.map +1 -1
- package/dist/helpers.d.ts +49 -2
- package/dist/helpers.d.ts.map +1 -1
- package/dist/helpers.js +169 -14
- package/dist/helpers.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/linked-list.d.ts +35 -0
- package/dist/linked-list.d.ts.map +1 -0
- package/dist/linked-list.js +300 -0
- package/dist/linked-list.js.map +1 -0
- package/dist/middlewares.d.ts +102 -12
- package/dist/middlewares.d.ts.map +1 -1
- package/dist/middlewares.js +119 -22
- package/dist/middlewares.js.map +1 -1
- package/dist/parsers.d.ts.map +1 -1
- package/dist/parsers.js +1 -0
- package/dist/parsers.js.map +1 -1
- package/dist/router.d.ts +5 -6
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +271 -148
- package/dist/router.js.map +1 -1
- package/dist/status.d.ts.map +1 -1
- package/dist/status.js +80 -79
- package/dist/status.js.map +1 -1
- package/dist/types.d.ts +74 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +15 -2
- package/dist/types.js.map +1 -1
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +1 -0
- package/dist/utils.js.map +1 -1
- package/dist/utils.node.d.ts.map +1 -1
- package/dist/utils.node.js +1 -0
- package/dist/utils.node.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -73,14 +73,14 @@ Average 10.05k 8.13k 7.65k ┌────────
|
|
|
73
73
|
- 🧠 **TypeScript-first** — extend the request context with your own application data
|
|
74
74
|
- 🌐 **Runtime agnostic** — works with Bun, Deno, Node.js, and other Web API-compatible runtimes
|
|
75
75
|
- 📁 **File-based routing** — optionally load routes directly from a directory structure
|
|
76
|
-
- 📖
|
|
76
|
+
- 📖 OpenAPI 3.0 generation — route metadata, tags, parameters, schemas, security, components, and configurable sorting
|
|
77
77
|
- 🛠️ **Built-in utilities** — request parsing, responses, CORS, rate limiting, and authentication
|
|
78
78
|
- 🪶 **No server lock-in** — Spine only deals with `Request` in and `Response` out
|
|
79
79
|
|
|
80
80
|
## 📑 Table of Contents
|
|
81
81
|
|
|
82
|
-
- [What is new?](#what-is-new)
|
|
83
82
|
- [Quick Start](#quick-start)
|
|
83
|
+
- [Real World Usecase Example](#real-world-usecase-example)
|
|
84
84
|
- [Routing](#routing)
|
|
85
85
|
- [Parameters](#parameters)
|
|
86
86
|
- [Alternatives](#alternatives)
|
|
@@ -106,85 +106,6 @@ Average 10.05k 8.13k 7.65k ┌────────
|
|
|
106
106
|
- [Thanks and Enjoy](#️-thanks-and-enjoy)
|
|
107
107
|
- [Be a Sponsor](#-be-a-sponsor)
|
|
108
108
|
|
|
109
|
-
## What is new v1.2.10?
|
|
110
|
-
|
|
111
|
-
- Implemented static route generator function `generateStaticRoutesImporter` to use with nextjs, cloudflare workers and more which do not do accept dynamic imports.
|
|
112
|
-
|
|
113
|
-
```ts
|
|
114
|
-
// src/server/utils/gen-routes.ts
|
|
115
|
-
import Router from "@bepalo/spine";
|
|
116
|
-
import generateStaticRoutesImporter from "@bepalo/spine";
|
|
117
|
-
|
|
118
|
-
await generateStaticRoutesImporter(
|
|
119
|
-
new Router(),
|
|
120
|
-
"./src/server/routes",
|
|
121
|
-
"./routes/",
|
|
122
|
-
).then((content) =>
|
|
123
|
-
Bun.write("./src/server/index.ts", "// src/server/index.ts\n" + content),
|
|
124
|
-
);
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
```ts
|
|
128
|
-
// src/server/index.ts
|
|
129
|
-
// This is auto-generated for static importing of @bepalo/spine file-based routes
|
|
130
|
-
import {
|
|
131
|
-
Router,
|
|
132
|
-
Handler,
|
|
133
|
-
HandlerRegisterPiplineOptions,
|
|
134
|
-
Pipeline,
|
|
135
|
-
} from "@bepalo/spine";
|
|
136
|
-
import * as route__index from "./routes/_index";
|
|
137
|
-
import * as route_api_$$$path from "./routes/api/[## path]";
|
|
138
|
-
import * as route_api_$ from "./routes/api/[[#]]";
|
|
139
|
-
import * as route_api_fruits from "./routes/api/fruits";
|
|
140
|
-
import * as route_api_users_$$$usersPath from "./routes/api/users/[## usersPath]";
|
|
141
|
-
import * as route_api_users_$$$id from "./routes/api/users/[id]";
|
|
142
|
-
import * as route_pages_$$ from "./routes/pages/[[##]]";
|
|
143
|
-
|
|
144
|
-
const getOptions = <ExtendContext extends Record<string, unknown>>(
|
|
145
|
-
def: {
|
|
146
|
-
pipe: Handler<ExtendContext> | Pipeline<ExtendContext>;
|
|
147
|
-
} & HandlerRegisterPiplineOptions,
|
|
148
|
-
) => {
|
|
149
|
-
const { pipe, ...options } = def;
|
|
150
|
-
return options;
|
|
151
|
-
};
|
|
152
|
-
|
|
153
|
-
export const setRoutes = async <ExtendContext extends Record<string, unknown>>(
|
|
154
|
-
router: Router<ExtendContext>,
|
|
155
|
-
) => {
|
|
156
|
-
// /
|
|
157
|
-
router.handleGet(
|
|
158
|
-
"/_index",
|
|
159
|
-
route__index.Get.pipe,
|
|
160
|
-
getOptions(route__index.Get),
|
|
161
|
-
);
|
|
162
|
-
router.filterGet("/_index", route__index.Get_Filter);
|
|
163
|
-
// /api
|
|
164
|
-
router.filterGet("/api/::path", route_api_$$$path.Get_Filter);
|
|
165
|
-
router.filterGet("/api/*!", route_api_$.Get_Filter);
|
|
166
|
-
router.handleDelete("/api/fruits", route_api_fruits.Delete);
|
|
167
|
-
router.handleGet("/api/fruits", route_api_fruits.Get);
|
|
168
|
-
router.handlePatch("/api/fruits", route_api_fruits.Patch);
|
|
169
|
-
router.handlePost("/api/fruits", route_api_fruits.Post);
|
|
170
|
-
// /api/users
|
|
171
|
-
router.filterGet(
|
|
172
|
-
"/api/users/::usersPath",
|
|
173
|
-
route_api_users_$$$usersPath.Get_Filter,
|
|
174
|
-
);
|
|
175
|
-
router.handleGet(
|
|
176
|
-
"/api/users/:id",
|
|
177
|
-
route_api_users_$$$id.Get.pipe,
|
|
178
|
-
getOptions(route_api_users_$$$id.Get),
|
|
179
|
-
);
|
|
180
|
-
// /pages
|
|
181
|
-
router.handleGet("/pages/**!", route_pages_$$.Get);
|
|
182
|
-
};
|
|
183
|
-
|
|
184
|
-
// Use this to set the routes in your router
|
|
185
|
-
export default setRoutes;
|
|
186
|
-
```
|
|
187
|
-
|
|
188
109
|
## Quick Start
|
|
189
110
|
|
|
190
111
|
Install
|
|
@@ -262,6 +183,537 @@ const response = await spine.respond(request);
|
|
|
262
183
|
|
|
263
184
|
This makes the spine easy to embed into servers, frameworks, workers, and custom runtimes.
|
|
264
185
|
|
|
186
|
+
## Sneek peek of what is possible
|
|
187
|
+
|
|
188
|
+
### `src/utils/generate.ts`
|
|
189
|
+
|
|
190
|
+
<details open>
|
|
191
|
+
|
|
192
|
+
<summary> Generator utilities to watch for changes and generate static-routes-imports and static-assets-manifest.</summary>
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
// src/utils/generate.ts
|
|
196
|
+
import {
|
|
197
|
+
generateStaticAssetsManifestWatcher,
|
|
198
|
+
generateStaticRoutesWatcher,
|
|
199
|
+
} from "@bepalo/spine";
|
|
200
|
+
import { writeFile } from "node:fs/promises";
|
|
201
|
+
import { readFile } from "node:fs/promises";
|
|
202
|
+
|
|
203
|
+
const abortController = new AbortController();
|
|
204
|
+
|
|
205
|
+
// setTimeout(() => abortController.abort(), 10000);
|
|
206
|
+
|
|
207
|
+
generateStaticRoutesWatcher({
|
|
208
|
+
routesPath: "./routes",
|
|
209
|
+
importRoot: "./routes/",
|
|
210
|
+
output: "./routes.ts",
|
|
211
|
+
read: (filepath) => readFile(filepath, { encoding: "utf-8" }),
|
|
212
|
+
write: (filepath, content) =>
|
|
213
|
+
writeFile(filepath, content, { encoding: "utf-8" }),
|
|
214
|
+
abortSignal: abortController.signal,
|
|
215
|
+
// generateDelay: 1000,
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
generateStaticAssetsManifestWatcher({
|
|
219
|
+
assetsPath: "./public",
|
|
220
|
+
output: "./static-assets.json",
|
|
221
|
+
abortSignal: abortController.signal,
|
|
222
|
+
sortOrder: 1,
|
|
223
|
+
// exclude: ({ name, ext }) => !name || ext === ".env",
|
|
224
|
+
read: (filepath) => readFile(filepath, { encoding: "utf-8" }),
|
|
225
|
+
write: (filepath, content) =>
|
|
226
|
+
writeFile(filepath, content, { encoding: "utf-8" }),
|
|
227
|
+
// generateDelay: 1000,
|
|
228
|
+
});
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
</details>
|
|
232
|
+
|
|
233
|
+
### Static assets
|
|
234
|
+
|
|
235
|
+
#### `404.html`
|
|
236
|
+
|
|
237
|
+
<details>
|
|
238
|
+
<summary>404.html</summary>
|
|
239
|
+
|
|
240
|
+
```html
|
|
241
|
+
<!DOCTYPE html>
|
|
242
|
+
<html lang="en">
|
|
243
|
+
<head>
|
|
244
|
+
<meta charset="UTF-8" />
|
|
245
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
246
|
+
<title>Document</title>
|
|
247
|
+
</head>
|
|
248
|
+
|
|
249
|
+
<body>
|
|
250
|
+
<h1>404 Page not found!</h1>
|
|
251
|
+
<p>We couldn't locate the page you were looking for</p>
|
|
252
|
+
</body>
|
|
253
|
+
</html>
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
</details>
|
|
257
|
+
|
|
258
|
+
#### `500.html`
|
|
259
|
+
|
|
260
|
+
</details>
|
|
261
|
+
|
|
262
|
+
<details>
|
|
263
|
+
<summary>500.html</summary>
|
|
264
|
+
|
|
265
|
+
```html
|
|
266
|
+
<!DOCTYPE html>
|
|
267
|
+
<html lang="en">
|
|
268
|
+
<head>
|
|
269
|
+
<meta charset="UTF-8" />
|
|
270
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
271
|
+
<title>Document</title>
|
|
272
|
+
</head>
|
|
273
|
+
|
|
274
|
+
<body>
|
|
275
|
+
<h1>{{STATUS}} {{STATUS_TEXT}}</h1>
|
|
276
|
+
<p><strong>{{ERROR}}!</strong></p>
|
|
277
|
+
</body>
|
|
278
|
+
</html>
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
</details>
|
|
282
|
+
|
|
283
|
+
#### `Swagger`
|
|
284
|
+
|
|
285
|
+
<details>
|
|
286
|
+
<summary> public/openapi/index.html</summary>
|
|
287
|
+
|
|
288
|
+
```html
|
|
289
|
+
<!DOCTYPE html>
|
|
290
|
+
<html lang="en">
|
|
291
|
+
<head>
|
|
292
|
+
<meta charset="utf-8" />
|
|
293
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
294
|
+
<meta name="description" content="SwaggerUI" />
|
|
295
|
+
<title>SwaggerUI</title>
|
|
296
|
+
<link
|
|
297
|
+
rel="stylesheet"
|
|
298
|
+
href="https://unpkg.com/swagger-ui-dist@5.11.0/swagger-ui.css"
|
|
299
|
+
/>
|
|
300
|
+
</head>
|
|
301
|
+
|
|
302
|
+
<body>
|
|
303
|
+
<div id="swagger-ui"></div>
|
|
304
|
+
<script
|
|
305
|
+
src="https://unpkg.com/swagger-ui-dist@5.11.0/swagger-ui-bundle.js"
|
|
306
|
+
crossorigin
|
|
307
|
+
></script>
|
|
308
|
+
<script>
|
|
309
|
+
window.onload = () => {
|
|
310
|
+
window.ui = SwaggerUIBundle({
|
|
311
|
+
url: "/openapi/doc.json",
|
|
312
|
+
dom_id: "#swagger-ui",
|
|
313
|
+
});
|
|
314
|
+
};
|
|
315
|
+
</script>
|
|
316
|
+
</body>
|
|
317
|
+
</html>
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
</details>
|
|
321
|
+
|
|
322
|
+
### Source Codes
|
|
323
|
+
|
|
324
|
+
### `/user/:id Route`
|
|
325
|
+
|
|
326
|
+
<details>
|
|
327
|
+
<summary>src/routes/users/[id].ts</summary>
|
|
328
|
+
|
|
329
|
+
```ts
|
|
330
|
+
// src/routes/user/[id].ts
|
|
331
|
+
|
|
332
|
+
import {
|
|
333
|
+
json,
|
|
334
|
+
parseBody,
|
|
335
|
+
type CTBody,
|
|
336
|
+
type CTParams,
|
|
337
|
+
type HandlerDef,
|
|
338
|
+
type PipeDef,
|
|
339
|
+
} from "@bepalo/spine";
|
|
340
|
+
import { ArkErrors, type } from "arktype";
|
|
341
|
+
|
|
342
|
+
export const get_filter: HandlerDef = [
|
|
343
|
+
({ params }) => {
|
|
344
|
+
// valdate params
|
|
345
|
+
const r = type({
|
|
346
|
+
id: "3 <= string.numeric <= 5",
|
|
347
|
+
}).assert(params);
|
|
348
|
+
if (r instanceof ArkErrors) {
|
|
349
|
+
return json({ error: r.toJSON() }, { status: 400 });
|
|
350
|
+
}
|
|
351
|
+
},
|
|
352
|
+
];
|
|
353
|
+
|
|
354
|
+
export const get: PipeDef<CTParams<"id">> = {
|
|
355
|
+
pipe: ({ params: { id } }) => {
|
|
356
|
+
return parseInt(id) < 0
|
|
357
|
+
? json({ error: "User not found" })
|
|
358
|
+
: json({ user: { name: `user-${id}` } });
|
|
359
|
+
},
|
|
360
|
+
|
|
361
|
+
openApi: {
|
|
362
|
+
summary: "Get user by ID",
|
|
363
|
+
responses: {
|
|
364
|
+
"200": {
|
|
365
|
+
description: "Successfull response",
|
|
366
|
+
content: {
|
|
367
|
+
"application/json": {
|
|
368
|
+
schema: {
|
|
369
|
+
type: "object",
|
|
370
|
+
properties: {
|
|
371
|
+
user: {
|
|
372
|
+
type: "object",
|
|
373
|
+
properties: {
|
|
374
|
+
name: {
|
|
375
|
+
type: "string",
|
|
376
|
+
},
|
|
377
|
+
},
|
|
378
|
+
},
|
|
379
|
+
},
|
|
380
|
+
},
|
|
381
|
+
},
|
|
382
|
+
},
|
|
383
|
+
},
|
|
384
|
+
"404": {
|
|
385
|
+
description: "User not found",
|
|
386
|
+
content: {
|
|
387
|
+
"application/json": {
|
|
388
|
+
schema: {
|
|
389
|
+
type: "object",
|
|
390
|
+
properties: {
|
|
391
|
+
error: {
|
|
392
|
+
type: "string",
|
|
393
|
+
},
|
|
394
|
+
},
|
|
395
|
+
},
|
|
396
|
+
},
|
|
397
|
+
},
|
|
398
|
+
},
|
|
399
|
+
"400": {
|
|
400
|
+
description: "Bad request",
|
|
401
|
+
content: {
|
|
402
|
+
"application/json": {
|
|
403
|
+
schema: {
|
|
404
|
+
type: "object",
|
|
405
|
+
properties: {
|
|
406
|
+
error: {
|
|
407
|
+
type: "string",
|
|
408
|
+
},
|
|
409
|
+
},
|
|
410
|
+
},
|
|
411
|
+
},
|
|
412
|
+
},
|
|
413
|
+
},
|
|
414
|
+
},
|
|
415
|
+
},
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
export const post_filter: HandlerDef<CTBody> = [parseBody({ maxSize: 1024 })];
|
|
419
|
+
|
|
420
|
+
export const post: HandlerDef<CTBody> = ({
|
|
421
|
+
url: { pathname },
|
|
422
|
+
params,
|
|
423
|
+
body,
|
|
424
|
+
}) => {
|
|
425
|
+
return json({ pathname, params, body });
|
|
426
|
+
};
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
</details>
|
|
430
|
+
|
|
431
|
+
#### `Main`
|
|
432
|
+
|
|
433
|
+
<details>
|
|
434
|
+
<summary>src/index.ts</summary>
|
|
435
|
+
|
|
436
|
+
```ts
|
|
437
|
+
import type { Path, StaticAssetsManifestFile } from "@bepalo/spine";
|
|
438
|
+
import {
|
|
439
|
+
Router,
|
|
440
|
+
ExpCache,
|
|
441
|
+
HttpError,
|
|
442
|
+
Status,
|
|
443
|
+
json,
|
|
444
|
+
cors,
|
|
445
|
+
limitRate,
|
|
446
|
+
getHttpStatusText,
|
|
447
|
+
status,
|
|
448
|
+
securityHeaders,
|
|
449
|
+
Break_Pipeline,
|
|
450
|
+
} from "@bepalo/spine";
|
|
451
|
+
import { readFileSync } from "node:fs";
|
|
452
|
+
import { writeFile } from "node:fs/promises";
|
|
453
|
+
// Import generated static routes imports
|
|
454
|
+
import setRoutes from "./routes";
|
|
455
|
+
// Import generated static assets manifest
|
|
456
|
+
import staticAssetsManifest from "./static-assets.json";
|
|
457
|
+
|
|
458
|
+
const {
|
|
459
|
+
"/404": notFoundAsset,
|
|
460
|
+
"/500": serverErrorAsset,
|
|
461
|
+
...staticAssets
|
|
462
|
+
} = staticAssetsManifest.files;
|
|
463
|
+
|
|
464
|
+
// lru-exp cache for static assets
|
|
465
|
+
const assetsCache: ExpCache<string, Buffer<ArrayBuffer>> = new ExpCache({
|
|
466
|
+
maxMemory: 32 * 1024 * 1024, // 32Mb
|
|
467
|
+
onMiss(key, entry, reason, cache) {
|
|
468
|
+
if (!(key in staticAssetsManifest.files)) return;
|
|
469
|
+
const asset = (staticAssetsManifest.files as any)[key];
|
|
470
|
+
// Read from file into cache because it is missing.
|
|
471
|
+
// You only have to call `assetsCache.get` elsewhere
|
|
472
|
+
// as this will automatically load it on cache-miss.
|
|
473
|
+
cache.set(
|
|
474
|
+
key,
|
|
475
|
+
readFileSync(asset.path, { encoding: undefined }),
|
|
476
|
+
asset.size,
|
|
477
|
+
{
|
|
478
|
+
ttl: 3_600_000, // 1 hour
|
|
479
|
+
},
|
|
480
|
+
);
|
|
481
|
+
return true;
|
|
482
|
+
},
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
// Static assets cache cleanup timer.
|
|
486
|
+
// You could also setup a cron api.
|
|
487
|
+
setInterval(() => {
|
|
488
|
+
console.log("Cleared ", assetsCache.evictExpired());
|
|
489
|
+
}, 3_600_000);
|
|
490
|
+
|
|
491
|
+
export type CTMain = { clientIP: string };
|
|
492
|
+
|
|
493
|
+
// Create router instance
|
|
494
|
+
export const spine = new Router<CTMain>({
|
|
495
|
+
maxPath: 10,
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
// Serve
|
|
499
|
+
const server = Bun.serve({
|
|
500
|
+
port: 3000,
|
|
501
|
+
fetch: (request, server) =>
|
|
502
|
+
spine.respond(request, {
|
|
503
|
+
clientIP: server.requestIP(request)?.address || "anonymous",
|
|
504
|
+
}),
|
|
505
|
+
});
|
|
506
|
+
console.log(`Listening on ${server.url}`);
|
|
507
|
+
|
|
508
|
+
//////////////////////////////////////////////
|
|
509
|
+
|
|
510
|
+
// Set generated dynamic-routes' static-imports
|
|
511
|
+
setRoutes(spine);
|
|
512
|
+
|
|
513
|
+
// Security Headers, CORS, Rate Limiting, ... for /**
|
|
514
|
+
spine.filterAll("/**", [
|
|
515
|
+
// forceHttps({ toPort: server.port }),
|
|
516
|
+
limitRate<CTMain>({
|
|
517
|
+
key: ({ clientIP }) => clientIP,
|
|
518
|
+
maxTokens: 100,
|
|
519
|
+
refillInterval: 60 * 1000, // every minute
|
|
520
|
+
// refillRate:
|
|
521
|
+
setXRateLimitHeaders: process.env.NODE_ENV !== "production",
|
|
522
|
+
}),
|
|
523
|
+
securityHeaders({
|
|
524
|
+
headers: {
|
|
525
|
+
"Reporting-Endpoints": `coep-endpoint="${server.url.origin + "/coep"}"`,
|
|
526
|
+
},
|
|
527
|
+
crossOriginEmbedderPolicy: 'credentialless; report-to="coep-endpoint"',
|
|
528
|
+
crossOriginResourcePolicy: "same-site",
|
|
529
|
+
crossOriginOpenerPolicy: "same-origin-allow-popups",
|
|
530
|
+
referrerPolicy: "strict-origin-when-cross-origin",
|
|
531
|
+
xFrameOptions: "DENY",
|
|
532
|
+
contentSecurityPolicy: [
|
|
533
|
+
["default-src", "'self'"],
|
|
534
|
+
["object-src", "'none'"],
|
|
535
|
+
["frame-ancestors", "'none'"],
|
|
536
|
+
[
|
|
537
|
+
"script-src style-src font-src",
|
|
538
|
+
"'self'",
|
|
539
|
+
"https://unpkg.com",
|
|
540
|
+
"'unsafe-inline'",
|
|
541
|
+
],
|
|
542
|
+
["script-src", "'self'", "'strict-dynamic'", "'unsafe-inline'"],
|
|
543
|
+
["img-src", "'self'", "data:", "'unsafe-inline'"],
|
|
544
|
+
["upgrade-insecure-requests"],
|
|
545
|
+
],
|
|
546
|
+
}),
|
|
547
|
+
cors({
|
|
548
|
+
origins: ["https://example.com", server.url.origin],
|
|
549
|
+
methods: ["Get", "Head", "Options"],
|
|
550
|
+
allowedHeaders: ["Authorization", "X-API-Key"],
|
|
551
|
+
credentials: true,
|
|
552
|
+
maxAge: 60 * 60,
|
|
553
|
+
}),
|
|
554
|
+
]);
|
|
555
|
+
|
|
556
|
+
// Security Headers, CORS, Rate Limiting, ... for /api/**
|
|
557
|
+
spine.filterAll("/api/**", [
|
|
558
|
+
// forceHttps({ toPort: server.port }),
|
|
559
|
+
limitRate<CTMain>({
|
|
560
|
+
key: ({ clientIP }) => clientIP,
|
|
561
|
+
maxTokens: 300,
|
|
562
|
+
refillInterval: 60 * 1000, // every minute
|
|
563
|
+
refillRate: 100, // 100 tokens every minute
|
|
564
|
+
setXRateLimitHeaders: process.env.NODE_ENV !== "production",
|
|
565
|
+
}),
|
|
566
|
+
securityHeaders({
|
|
567
|
+
// crossOriginResourcePolicy: "same-site",
|
|
568
|
+
// referrerPolicy: "strict-origin-when-cross-origin",
|
|
569
|
+
xFrameOptions: "DENY",
|
|
570
|
+
contentSecurityPolicy: [["upgrade-insecure-requests"]],
|
|
571
|
+
}),
|
|
572
|
+
cors({
|
|
573
|
+
origins: ["https://example.com", server.url.origin],
|
|
574
|
+
methods: ["Get", "Post", "Put", "Patch", "Delete", "Head", "Options"],
|
|
575
|
+
allowedHeaders: ["Content-Type", "Authorization", "X-API-Key"],
|
|
576
|
+
credentials: true,
|
|
577
|
+
maxAge: 60 * 60,
|
|
578
|
+
}),
|
|
579
|
+
// do not bubble to other matching filters such as /**
|
|
580
|
+
() => Break_Pipeline,
|
|
581
|
+
]);
|
|
582
|
+
|
|
583
|
+
// Handle Options for all /** to return no content
|
|
584
|
+
spine.handleOptions("/**", () => status(204, null));
|
|
585
|
+
|
|
586
|
+
// Handler get and head of static assets using generated manifest
|
|
587
|
+
spine.handle(
|
|
588
|
+
[["Head", "Get"], ...(Object.keys(staticAssets) as Path[])],
|
|
589
|
+
({ url, request, headers }) => {
|
|
590
|
+
const asset: StaticAssetsManifestFile = (staticAssets as any)[url.pathname];
|
|
591
|
+
headers.set("Content-Type", asset.contentType);
|
|
592
|
+
headers.set("Content-Length", asset.size.toFixed());
|
|
593
|
+
const fileContent = assetsCache.get(asset.pathname);
|
|
594
|
+
if (request.method === "HEAD") {
|
|
595
|
+
return status(200, null);
|
|
596
|
+
}
|
|
597
|
+
return new Response(fileContent);
|
|
598
|
+
},
|
|
599
|
+
);
|
|
600
|
+
|
|
601
|
+
// Handle fallbacks of get /** using 404.html static page
|
|
602
|
+
spine.fallback([["Head", "Get"], "/**"], ({ url, request, headers }) => {
|
|
603
|
+
const asset: StaticAssetsManifestFile = notFoundAsset;
|
|
604
|
+
headers.set("Content-Type", asset.contentType);
|
|
605
|
+
headers.set("Content-Length", asset.size.toFixed());
|
|
606
|
+
if (request.method === "HEAD") {
|
|
607
|
+
return status(200, null);
|
|
608
|
+
}
|
|
609
|
+
const fileContent = assetsCache.get(notFoundAsset.pathname);
|
|
610
|
+
return new Response(fileContent, { status: 404 });
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
// Set error handler of All /**
|
|
614
|
+
// Note: The string replacement is just for demonstration.
|
|
615
|
+
// You are probably going to use a framework like
|
|
616
|
+
// pug, react, Nextjs or others.
|
|
617
|
+
spine.catchAll("/**", ({ error, request, headers }) => {
|
|
618
|
+
// process.env.NODE_ENV !== "production" && console.error(error);
|
|
619
|
+
const statusCode = (error as HttpError).status || 500;
|
|
620
|
+
const asset: StaticAssetsManifestFile = serverErrorAsset;
|
|
621
|
+
headers.set("Content-Type", asset.contentType);
|
|
622
|
+
headers.set("Content-Length", asset.size.toFixed());
|
|
623
|
+
if (request.method === "HEAD") {
|
|
624
|
+
return status(200, null);
|
|
625
|
+
}
|
|
626
|
+
const fileContent = assetsCache.get(asset.pathname);
|
|
627
|
+
const vars = {
|
|
628
|
+
STATUS: String(statusCode),
|
|
629
|
+
STATUS_TEXT: getHttpStatusText(statusCode),
|
|
630
|
+
ERROR: error.message,
|
|
631
|
+
};
|
|
632
|
+
return new Response(
|
|
633
|
+
fileContent!
|
|
634
|
+
.toString()
|
|
635
|
+
.replace(/(\\)?\{\{(.+?)\}\}/g, (match, escape, id) =>
|
|
636
|
+
escape ? match : ((vars as any)[id] ?? match),
|
|
637
|
+
),
|
|
638
|
+
{ status: statusCode },
|
|
639
|
+
);
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
// Set error handler of All /api/**
|
|
643
|
+
// Takes precedence over /**
|
|
644
|
+
spine.catchAll("/api/**", ({ error }) => {
|
|
645
|
+
process.env.NODE_ENV !== "production" && console.error(error);
|
|
646
|
+
const status = (error as HttpError).status || 500;
|
|
647
|
+
return json({ error: error.message }, { status });
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
// Set fallback handler of (Get,Post,Put,Patch,Delete) /api/**
|
|
651
|
+
spine.fallbackCrud("/api/**", () =>
|
|
652
|
+
json({ error: "Not found" }, { status: 404 }),
|
|
653
|
+
);
|
|
654
|
+
|
|
655
|
+
// Error test
|
|
656
|
+
spine.get(["/error", "/api/error"], () => {
|
|
657
|
+
throw new HttpError(Status._503_ServiceUnavailable, "Come back tomorrow");
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
// Stats
|
|
661
|
+
spine.get("/api/stats", () => json({ staticAssetsCache: assetsCache.stats }));
|
|
662
|
+
|
|
663
|
+
spine.afterAll(
|
|
664
|
+
"/**",
|
|
665
|
+
({
|
|
666
|
+
request: { method },
|
|
667
|
+
response: { status, statusText, headers, body },
|
|
668
|
+
url,
|
|
669
|
+
timestamps,
|
|
670
|
+
}) => {
|
|
671
|
+
const { request, start, end } = timestamps;
|
|
672
|
+
const size = ["OPTIONS", "HEAD"].includes(method)
|
|
673
|
+
? 0
|
|
674
|
+
: Number(headers.get("Content-Length") || "0");
|
|
675
|
+
const kbSize = ((size ?? 0) / 1024).toFixed(2).padStart(5);
|
|
676
|
+
const time = (end - start).toFixed(3).padStart(6);
|
|
677
|
+
let logstr = `[${new Date(request).toISOString()}]`;
|
|
678
|
+
logstr += `[${status}]`;
|
|
679
|
+
logstr += ` ${time}ms ${kbSize}KB`;
|
|
680
|
+
logstr += ` -- ${method} ${url.pathname} ${url.search}`;
|
|
681
|
+
logstr += ` -- ${statusText}`;
|
|
682
|
+
console.log(logstr);
|
|
683
|
+
},
|
|
684
|
+
);
|
|
685
|
+
|
|
686
|
+
//////////////////////////////////////////
|
|
687
|
+
|
|
688
|
+
// generate public/openapi/doc.json
|
|
689
|
+
spine
|
|
690
|
+
.generateOpenAPI(
|
|
691
|
+
{
|
|
692
|
+
title: "@bepalo/spine Demo",
|
|
693
|
+
version: "1.0.0",
|
|
694
|
+
// ...
|
|
695
|
+
},
|
|
696
|
+
{
|
|
697
|
+
pick: ({ path }) => path.startsWith("/api"),
|
|
698
|
+
// autoTag: false,
|
|
699
|
+
// autoSummary: false,
|
|
700
|
+
includeOperationId: true,
|
|
701
|
+
sortTagsOrder: 1,
|
|
702
|
+
sortPathnameOrder: 1,
|
|
703
|
+
sortMethodOrder: 1,
|
|
704
|
+
},
|
|
705
|
+
)
|
|
706
|
+
.then(async (openapi) => {
|
|
707
|
+
const output = "./public/openapi/doc.json";
|
|
708
|
+
const content = JSON.stringify(openapi, null, 2);
|
|
709
|
+
await writeFile(output, content, { encoding: "utf-8" });
|
|
710
|
+
});
|
|
711
|
+
```
|
|
712
|
+
|
|
713
|
+
</details>
|
|
714
|
+
|
|
715
|
+
---
|
|
716
|
+
|
|
265
717
|
## Routing
|
|
266
718
|
|
|
267
719
|
Use the convenient HTTP method helpers:
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth-middlewares.d.ts","sourceRoot":"","sources":["../src/auth-middlewares.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"auth-middlewares.d.ts","sourceRoot":"","sources":["../src/auth-middlewares.ts"],"names":[],"mappings":"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"}
|
package/dist/auth-middlewares.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
// src/auth-middlewares.ts
|
|
2
3
|
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
4
|
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
5
|
return new (P || (P = Promise))(function (resolve, reject) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth-middlewares.js","sourceRoot":"","sources":["../src/auth-middlewares.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"auth-middlewares.js","sourceRoot":"","sources":["../src/auth-middlewares.ts"],"names":[],"mappings":"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"}
|