spooder 6.1.0 → 6.1.2

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
@@ -603,6 +603,7 @@ server.stop(immediate: boolean): Promise<void>;
603
603
  // routing
604
604
  server.route(path: string, handler: RequestHandler, method?: HTTP_METHODS);
605
605
  server.json(path: string, handler: JSONRequestHandler, method?: HTTP_METHODS);
606
+ server.throttle(delta: number, handler: JSONRequestHandler|RequestHandler);
606
607
  server.unroute(path: string);
607
608
 
608
609
  // fallback handlers
@@ -681,8 +682,8 @@ db_set_serialize<T extends string>(set: Iterable<T> | null): string;
681
682
 
682
683
  // database schema
683
684
  type SchemaOptions = {
684
- schema_table: string;
685
- recursive: boolean;
685
+ schema_table?: string;
686
+ recursive?: boolean;
686
687
  };
687
688
 
688
689
  db_get_schema_revision(db: SQL): Promise<number|null>;
@@ -892,6 +893,24 @@ server.route('/test/route', () => {});
892
893
  server.unroute('/test/route');
893
894
  ```
894
895
 
896
+ ### 🔧 `server.throttle(delta: number, handler: JSONRequestHandler|RequestHandler)`
897
+
898
+ Throttles requests going through the provided handler so that they take a **minimum** of `delta` milliseconds. Useful for preventing brute-force of sensitive endpoints.
899
+
900
+ > [!IMPORTANT]
901
+ > 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.
902
+
903
+ ```ts
904
+ server.json('/api/login', server.throttle(1000, (req, url, json) => {
905
+ // this endpoint will always take at least 1000ms to execute
906
+ }));
907
+
908
+ // works with regular routes
909
+ server.route('/reset-password', server.throttle(1000, (req, url) => {
910
+ // this route will also take at least 1000ms to execute
911
+ }));
912
+ ```
913
+
895
914
  ### 🔧 `server.json(path: string, handler: JSONRequestHandler, method?: HTTP_METHODS)`
896
915
 
897
916
  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.
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.0",
5
+ "version": "6.1.2",
6
6
  "module": "./src/api.ts",
7
7
  "bin": {
8
8
  "spooder": "./src/cli.ts"
package/src/api.ts CHANGED
@@ -1782,6 +1782,11 @@ export function http_serve(port: number, hostname?: string) {
1782
1782
 
1783
1783
  log_spooder(`server started on port {${port}} (host: {${hostname ?? 'unspecified'}})`);
1784
1784
 
1785
+ type ThrottleHandler = {
1786
+ (delta: number, handler: JSONRequestHandler): JSONRequestHandler;
1787
+ (delta: number, handler: RequestHandler): RequestHandler;
1788
+ };
1789
+
1785
1790
  return {
1786
1791
  /** Register a handler for a specific route. */
1787
1792
  route: (path: string, handler: RequestHandler, method: HTTP_METHODS = 'GET'): void => {
@@ -1789,6 +1794,23 @@ export function http_serve(port: number, hostname?: string) {
1789
1794
  path = path.slice(0, -1);
1790
1795
  routes.push([path.split('/'), handler, method]);
1791
1796
  },
1797
+
1798
+ /** Throttles an endpoint to take at least the specified delta time (in ms) */
1799
+ throttle: ((delta: number, handler: JSONRequestHandler | RequestHandler): any => {
1800
+ return async (req: Request, ...args: any[]) => {
1801
+ const t_start = Date.now();
1802
+ const result = await (handler as any)(req, ...args);
1803
+
1804
+ const t_elapsed = Date.now() - t_start;
1805
+ const t_remaining = Math.max(0, delta - t_elapsed);
1806
+
1807
+ if (t_remaining > 0)
1808
+ await Bun.sleep(t_remaining);
1809
+
1810
+ slow_requests.add(req);
1811
+ return result;
1812
+ };
1813
+ }) as ThrottleHandler,
1792
1814
 
1793
1815
  /** Register a JSON endpoint with automatic content validation. */
1794
1816
  json: (path: string, handler: JSONRequestHandler, method: HTTP_METHODS = 'POST'): void => {
@@ -2114,9 +2136,8 @@ export function http_serve(port: number, hostname?: string) {
2114
2136
 
2115
2137
  // region db
2116
2138
  type SchemaOptions = {
2117
- schema_table: string;
2118
- recursive: boolean;
2119
- throw_on_skip: boolean;
2139
+ schema_table?: string;
2140
+ recursive?: boolean;
2120
2141
  };
2121
2142
 
2122
2143
  type TableRevision = {