spooder 6.1.1 → 6.1.3
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 +49 -0
- package/bun.lock +1 -1
- package/package.json +1 -1
- package/src/api.ts +44 -0
- package/test.ts +0 -10
package/README.md
CHANGED
|
@@ -104,6 +104,7 @@ The `CLI` component of `spooder` is a global command-line tool for running serve
|
|
|
104
104
|
- [API > HTTP > Webhooks](#api-http-webhooks)
|
|
105
105
|
- [API > HTTP > Websocket Server](#api-http-websockets)
|
|
106
106
|
- [API > HTTP > Bootstrap](#api-http-bootstrap)
|
|
107
|
+
- [API > HTTP > Cookies](#api-http-cookies)
|
|
107
108
|
- [API > Error Handling](#api-error-handling)
|
|
108
109
|
- [API > Workers](#api-workers)
|
|
109
110
|
- [API > Caching](#api-caching)
|
|
@@ -603,6 +604,7 @@ server.stop(immediate: boolean): Promise<void>;
|
|
|
603
604
|
// routing
|
|
604
605
|
server.route(path: string, handler: RequestHandler, method?: HTTP_METHODS);
|
|
605
606
|
server.json(path: string, handler: JSONRequestHandler, method?: HTTP_METHODS);
|
|
607
|
+
server.throttle(delta: number, handler: JSONRequestHandler|RequestHandler);
|
|
606
608
|
server.unroute(path: string);
|
|
607
609
|
|
|
608
610
|
// fallback handlers
|
|
@@ -892,6 +894,24 @@ server.route('/test/route', () => {});
|
|
|
892
894
|
server.unroute('/test/route');
|
|
893
895
|
```
|
|
894
896
|
|
|
897
|
+
### 🔧 `server.throttle(delta: number, handler: JSONRequestHandler|RequestHandler)`
|
|
898
|
+
|
|
899
|
+
Throttles requests going through the provided handler so that they take a **minimum** of `delta` milliseconds. Useful for preventing brute-force of sensitive endpoints.
|
|
900
|
+
|
|
901
|
+
> [!IMPORTANT]
|
|
902
|
+
> This is a rudimentary countermeasure for brute-force attacks, **not** a defence against timing-attacks. Always use constant-time/timing-safe comparison functions in sensitive endpoints.
|
|
903
|
+
|
|
904
|
+
```ts
|
|
905
|
+
server.json('/api/login', server.throttle(1000, (req, url, json) => {
|
|
906
|
+
// this endpoint will always take at least 1000ms to execute
|
|
907
|
+
}));
|
|
908
|
+
|
|
909
|
+
// works with regular routes
|
|
910
|
+
server.route('/reset-password', server.throttle(1000, (req, url) => {
|
|
911
|
+
// this route will also take at least 1000ms to execute
|
|
912
|
+
}));
|
|
913
|
+
```
|
|
914
|
+
|
|
895
915
|
### 🔧 `server.json(path: string, handler: JSONRequestHandler, method?: HTTP_METHODS)`
|
|
896
916
|
|
|
897
917
|
Register a JSON endpoint with automatic content validation. This method automatically validates that the request has the correct `Content-Type: application/json` header and that the request body contains a valid JSON object.
|
|
@@ -1397,6 +1417,35 @@ server.websocket('/path/to/websocket', {
|
|
|
1397
1417
|
> [!IMPORTANT]
|
|
1398
1418
|
> While it is possible to register multiple routes for websockets, the only handler which is unique per route is `accept()`. The last handlers provided to any route (with the exception of `accept()`) will apply to ALL websocket routes. This is a limitation in Bun.
|
|
1399
1419
|
|
|
1420
|
+
<a id="api-http-cookies"></a>
|
|
1421
|
+
## API > HTTP > Cookies
|
|
1422
|
+
|
|
1423
|
+
### 🔧 `cookies_get(req: Request): Bun.CookieMap`
|
|
1424
|
+
|
|
1425
|
+
When called on a request, the `cookies_get` function will return a `Bun.CookieMap` contains all of the cookies parsed from the `Cookie` header on the request.
|
|
1426
|
+
|
|
1427
|
+
```ts
|
|
1428
|
+
server.route('/', (req, url) => {
|
|
1429
|
+
const cookies = cookies_get(req);
|
|
1430
|
+
return `Hello ${cookies.get('person') ?? 'unknown'}`;
|
|
1431
|
+
});
|
|
1432
|
+
```
|
|
1433
|
+
|
|
1434
|
+
The return `Bun.CookieMap` is an iterable map with a custom API for reading/setting cookies. The full API [can be seen here](https://bun.com/docs/runtime/cookies).
|
|
1435
|
+
|
|
1436
|
+
Any changes made to the cookie map (adding, deletion, editing, etc) will be sent as `Set-Cookie` headers on the response automatically. Unchanged cookies are not sent.
|
|
1437
|
+
|
|
1438
|
+
```ts
|
|
1439
|
+
server.route('/', (req, url) => {
|
|
1440
|
+
const cookies = cookies_get(req);
|
|
1441
|
+
cookies.set('test', 'foobar');
|
|
1442
|
+
return 'Hello, world!';
|
|
1443
|
+
|
|
1444
|
+
// the response automatically gets:
|
|
1445
|
+
// Set-Cookie test=foobar; Path=/; SameSite=Lax
|
|
1446
|
+
});
|
|
1447
|
+
```
|
|
1448
|
+
|
|
1400
1449
|
<a id="api-http-bootstrap"></a>
|
|
1401
1450
|
## API > HTTP > Bootstrap
|
|
1402
1451
|
|
package/bun.lock
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"packages": {
|
|
12
12
|
"@types/bun": ["@types/bun@1.3.1", "", { "dependencies": { "bun-types": "1.3.1" } }, "sha512-4jNMk2/K9YJtfqwoAa28c8wK+T7nvJFOjxI4h/7sORWcypRNxBpr+TPNaCfVWq70tLCJsqoFwcf0oI0JU/fvMQ=="],
|
|
13
13
|
|
|
14
|
-
"@types/node": ["@types/node@24.9.
|
|
14
|
+
"@types/node": ["@types/node@24.9.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-uWN8YqxXxqFMX2RqGOrumsKeti4LlmIMIyV0lgut4jx7KQBcBiW6vkDtIBvHnHIquwNfJhk8v2OtmO8zXWHfPA=="],
|
|
15
15
|
|
|
16
16
|
"@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="],
|
|
17
17
|
|
package/package.json
CHANGED
package/src/api.ts
CHANGED
|
@@ -1143,6 +1143,26 @@ export function git_get_hashes_sync(length = 7): Record<string, string> {
|
|
|
1143
1143
|
}
|
|
1144
1144
|
// endregion
|
|
1145
1145
|
|
|
1146
|
+
// region cookies
|
|
1147
|
+
const cookie_map = new WeakMap<Request, Bun.CookieMap>();
|
|
1148
|
+
|
|
1149
|
+
export function cookies_get(req: Request): Bun.CookieMap {
|
|
1150
|
+
const jar = new Bun.CookieMap(req.headers.get('Cookie') ?? undefined);
|
|
1151
|
+
cookie_map.set(req, jar);
|
|
1152
|
+
return jar;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
function apply_cookies(req: Request, res: Response) {
|
|
1156
|
+
const jar = cookie_map.get(req);
|
|
1157
|
+
if (jar === undefined)
|
|
1158
|
+
return;
|
|
1159
|
+
|
|
1160
|
+
const cookies = jar.toSetCookieHeaders();
|
|
1161
|
+
for (const cookie of cookies)
|
|
1162
|
+
res.headers.append('Set-Cookie', cookie);
|
|
1163
|
+
}
|
|
1164
|
+
// endregion
|
|
1165
|
+
|
|
1146
1166
|
// region serving
|
|
1147
1167
|
export const HTTP_STATUS_TEXT: Record<number, string> = {
|
|
1148
1168
|
// 1xx Informational Response
|
|
@@ -1736,6 +1756,8 @@ export function http_serve(port: number, hostname?: string) {
|
|
|
1736
1756
|
|
|
1737
1757
|
const response = await generate_response(req, url);
|
|
1738
1758
|
const request_time = Date.now() - request_start;
|
|
1759
|
+
|
|
1760
|
+
apply_cookies(req, response);
|
|
1739
1761
|
|
|
1740
1762
|
const is_known_slow = slow_requests.has(req);
|
|
1741
1763
|
if (slow_request_callback !== null && request_time > slow_request_threshold && !is_known_slow)
|
|
@@ -1782,6 +1804,11 @@ export function http_serve(port: number, hostname?: string) {
|
|
|
1782
1804
|
|
|
1783
1805
|
log_spooder(`server started on port {${port}} (host: {${hostname ?? 'unspecified'}})`);
|
|
1784
1806
|
|
|
1807
|
+
type ThrottleHandler = {
|
|
1808
|
+
(delta: number, handler: JSONRequestHandler): JSONRequestHandler;
|
|
1809
|
+
(delta: number, handler: RequestHandler): RequestHandler;
|
|
1810
|
+
};
|
|
1811
|
+
|
|
1785
1812
|
return {
|
|
1786
1813
|
/** Register a handler for a specific route. */
|
|
1787
1814
|
route: (path: string, handler: RequestHandler, method: HTTP_METHODS = 'GET'): void => {
|
|
@@ -1789,6 +1816,23 @@ export function http_serve(port: number, hostname?: string) {
|
|
|
1789
1816
|
path = path.slice(0, -1);
|
|
1790
1817
|
routes.push([path.split('/'), handler, method]);
|
|
1791
1818
|
},
|
|
1819
|
+
|
|
1820
|
+
/** Throttles an endpoint to take at least the specified delta time (in ms) */
|
|
1821
|
+
throttle: ((delta: number, handler: JSONRequestHandler | RequestHandler): any => {
|
|
1822
|
+
return async (req: Request, ...args: any[]) => {
|
|
1823
|
+
const t_start = Date.now();
|
|
1824
|
+
const result = await (handler as any)(req, ...args);
|
|
1825
|
+
|
|
1826
|
+
const t_elapsed = Date.now() - t_start;
|
|
1827
|
+
const t_remaining = Math.max(0, delta - t_elapsed);
|
|
1828
|
+
|
|
1829
|
+
if (t_remaining > 0)
|
|
1830
|
+
await Bun.sleep(t_remaining);
|
|
1831
|
+
|
|
1832
|
+
slow_requests.add(req);
|
|
1833
|
+
return result;
|
|
1834
|
+
};
|
|
1835
|
+
}) as ThrottleHandler,
|
|
1792
1836
|
|
|
1793
1837
|
/** Register a JSON endpoint with automatic content validation. */
|
|
1794
1838
|
json: (path: string, handler: JSONRequestHandler, method: HTTP_METHODS = 'POST'): void => {
|
package/test.ts
DELETED