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 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.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg=="],
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
@@ -2,7 +2,7 @@
2
2
  "name": "spooder",
3
3
  "author": "Kruithne <kruithne@gmail.com>",
4
4
  "type": "module",
5
- "version": "6.1.1",
5
+ "version": "6.1.3",
6
6
  "module": "./src/api.ts",
7
7
  "bin": {
8
8
  "spooder": "./src/cli.ts"
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
@@ -1,10 +0,0 @@
1
- import { SQL } from 'bun';
2
- import * as spooder from 'spooder';
3
-
4
- type TestRow = {
5
- ID: number;
6
- test: string;
7
- };
8
-
9
- const db = new SQL('mysql://test:1141483652@localhost:3306/test');
10
- await spooder.db_schema(db, './db/revisions');