@increase21/simplenodejs 1.0.30 → 1.1.0

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
@@ -105,51 +105,27 @@ controllers/
105
105
  accountProfiles → /drivers/account-profiles
106
106
  ```
107
107
 
108
- ### Using __run
109
-
110
- `__run` dispatches to the correct handler based on the HTTP method and enforces method-level ID validation.
108
+ Controllers are plain classes — no base class required. Each method represents an endpoint and returns an array of `SimpleJsEndpointDescriptor` objects that declare which HTTP methods are supported and which handler to call.
111
109
 
112
110
  ```ts
113
111
  // controllers/drivers/auths.ts
114
- import { SimpleNodeJsController, SimpleJsPrivateMethodProps } from "@increase21/simplenodejs";
115
-
116
- export default class AuthController extends SimpleNodeJsController {
117
- async login() {
118
- return this.__run({
119
- post: () => { // handle POST /drivers/auths/login
120
- return { token: "..." };
121
- },
122
- });
123
- }
124
-
125
- async account(id: string) {
126
- return this.__run({
127
- get: () => {
128
- // handle GET /drivers/auths/account/:id
129
- },
130
- put: () => {
131
- // handle PUT /drivers/auths/account/:id
132
- },
133
- delete: () => {
134
- // handle DELETE /drivers/auths/account/:id
135
- },
136
- id:{get:"optional", delete:"required",put:"required"}
137
- },
138
- );
139
- }
140
- }
141
- ```
112
+ import { SimpleJsCtx, SimpleJsEndpointDescriptor } from "@increase21/simplenodejs";
142
113
 
143
- ### Without __run
114
+ export default class AuthController {
144
115
 
145
- ```ts
146
- export default class AuthController extends SimpleNodeJsController {
147
- async login() {
148
- if (this.method !== "post") return this.res.status(405).json({ error: "Method Not Allowed" });
116
+ async login(_ctx: SimpleJsCtx): Promise<SimpleJsEndpointDescriptor[]> {
117
+ return [
118
+ { method: "get", handler: getLogin, middleware:LoginGetMiddleware },
119
+ { method: "post", handler: postLogin, middleware:LoginPostMiddleware },
120
+ ];
121
+ }
149
122
 
150
- const { email, password } = this.body;
151
- // ... your logic
152
- return this.res.status(200).json({ token: "..." });
123
+ async account(_ctx: SimpleJsCtx, id: string): Promise<SimpleJsEndpointDescriptor[]> {
124
+ return [
125
+ { method: "get", id: "optional", handler: getAccount },
126
+ { method: "put", id: "required", handler: updateAccount },
127
+ { method: "delete", id: "required", handler: deleteAccount },
128
+ ];
153
129
  }
154
130
  }
155
131
  ```
@@ -162,13 +138,29 @@ Controller methods use **camelCase** and are exposed as **kebab-case** URLs.
162
138
  |---|---|
163
139
  | `async index()` | `/drivers/auths` |
164
140
  | `async login()` | `/drivers/auths/login` |
165
- | `async vehicleList(id)` | `/drivers/auths/vehicle-list` or `/drivers/auths/vehicle-list/:id` |
141
+ | `async vehicleList()` | `/drivers/auths/vehicle-list` |
142
+
143
+ ### ID Parameters
144
+
145
+ Declare `id` in the endpoint method signature to indicate it accepts an ID segment. Use the descriptor's `id` field to enforce whether it is required or optional at the routing level.
146
+
147
+ ```ts
148
+ // GET /drivers/auths/account → id is optional
149
+ // GET /drivers/auths/account/123 → id = "123"
150
+ // PUT /drivers/auths/account/123 → required, 404 if missing
151
+ async account(_ctx: SimpleJsCtx, id?: string): Promise<SimpleJsEndpointDescriptor[]> {
152
+ return [
153
+ { method: "get", id: "optional", handler: getAccount },
154
+ { method: "put", id: "required", handler: updateAccount },
155
+ ];
156
+ }
157
+ ```
166
158
 
167
159
  ---
168
160
 
169
- ## SimpleJsPrivateMethodProps
161
+ ## SimpleJsCtx
170
162
 
171
- Properties available inside `__run` handlers.
163
+ The context object passed to every endpoint method and handler.
172
164
 
173
165
  | Property | Type | Description |
174
166
  |---|---|---|
@@ -176,9 +168,19 @@ Properties available inside `__run` handlers.
176
168
  | `res` | `ResponseObject` | Raw response object |
177
169
  | `body` | `object` | Parsed request body |
178
170
  | `query` | `object` | Parsed query string |
179
- | `id` | `string \| undefined` | URL path parameter |
180
171
  | `customData` | `any` | Data attached by plugins/middlewares via `req._custom_data` |
181
172
 
173
+ ## SimpleJsEndpointDescriptor
174
+
175
+ Returned by endpoint methods to declare HTTP method handlers.
176
+
177
+ | Property | Type | Required | Description |
178
+ |---|---|---|---|
179
+ | `method` | `HttpMethod` | ✅ | HTTP verb: `"get"`, `"post"`, `"put"`, `"patch"`, `"delete"` |
180
+ | `handler` | `(ctx, id?) => any` | ✅ | Method reference to call for this HTTP verb |
181
+ | `id` | `"required" \| "optional"` | ❌ | ID routing rule. Omit if the endpoint never uses an ID |
182
+ | `middleware` | `(req, res, next)` | ❌ | A function to execute before the handler is called |
183
+
182
184
  ---
183
185
 
184
186
  ## RequestObject (req)
@@ -266,7 +268,7 @@ app.registerPlugin(app => SimpleJsSecurityPlugin(app, opt));
266
268
 
267
269
  ## SetBodyParser(options)
268
270
 
269
- Parses the request body. Must be registered before controllers access `this.body`.
271
+ Parses the request body. Must be registered before controllers access `ctx.body`.
270
272
 
271
273
  | Param | Type | Description |
272
274
  |---|---|---|
@@ -283,7 +285,13 @@ For context where you need direct stream access (e.g. passing the request to a l
283
285
 
284
286
  ```ts
285
287
  // Path-prefix list — skip body parsing for any URL under /upload
286
- app.use(SetBodyParser({ limit: "10mb", ignoreStream: [{url:"/upload", method:"post"}, {url:"/files/profile-picture", method:"post"}] }));
288
+ app.use(SetBodyParser({
289
+ limit: "10mb",
290
+ ignoreStream: [
291
+ {url:"/files/", method:"post", type:"prefix"},
292
+ {url:"/files/profile-picture", method:"post", type:"exact"}
293
+ ]
294
+ }));
287
295
 
288
296
  // Predicate function — full control over which requests are skipped
289
297
  app.use(SetBodyParser({
@@ -294,22 +302,6 @@ app.use(SetBodyParser({
294
302
 
295
303
  When a request is ignored, `next()` is called immediately with the stream untouched. Your handler is then responsible for consuming it:
296
304
 
297
- ```ts
298
- import formidable from "formidable";
299
-
300
- // Inside your controller handler
301
- const form = formidable({ maxTotalFileSize: 10 * 1024 * 1024 });
302
- form.parse(req, (err, fields, files) => {
303
- if (err) {
304
- // err.code 1009 = file too large, 1015 = total too large
305
- if (err.code === 1009 || err.code === 1015)
306
- return res.status(413).end("Payload Too Large");
307
- return res.status(400).end("Upload Error");
308
- }
309
- res.json({ fields, files });
310
- });
311
- ```
312
-
313
305
  ---
314
306
 
315
307
  ## SetCORS(options?)
@@ -448,12 +440,12 @@ app.registerPlugin(app => SimpleJsCookiePlugin(app, {
448
440
  secret: process.env.COOKIE_SECRET,
449
441
  }));
450
442
 
451
- // Set a signed cookie in a controller
443
+ // Set a signed cookie in a handler
452
444
  const signed = SignCookie(sessionId, process.env.COOKIE_SECRET!);
453
- this.res.setHeader("Set-Cookie", `session=${signed}; HttpOnly; Secure; SameSite=Strict`);
445
+ ctx.res.setHeader("Set-Cookie", `session=${signed}; HttpOnly; Secure; SameSite=Strict`);
454
446
 
455
- // Read cookie in any controller
456
- const { session } = this._custom_data.cookies;
447
+ // Read cookie in any handler
448
+ const { session } = ctx.customData.cookies;
457
449
  ```
458
450
 
459
451
  ---
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- export { SimpleNodeJsController } from "./utils/simpleController";
2
1
  export { CreateSimpleJsHttpServer, CreateSimpleJsHttpsServer } from "./server";
3
2
  export { SetCORS, SetHSTS, SetCSP, SetFrameGuard, SetNoSniff, SetReferrerPolicy, SetPermissionsPolicy, SetCOEP, SetCOOP, SetHelmet, SetRateLimiter, SetBodyParser, } from "./utils/simpleMiddleware";
4
3
  export * from "./utils/simplePlugins";
5
- export type { SimpleJsPrivateMethodProps, Middleware as SimpleJsMiddleware, SimpleJsHttpsServer, RequestObject, ResponseObject } from "./typings/general";
4
+ export type { RequestObject, ResponseObject } from "./typings/general";
5
+ export type { SimpleJsCtx, SimpleJsEndpointDescriptor, SimpleJsHttpsServer, Middleware as SimpleJsMiddleware, ErrorMiddleware as SimpleJsErrorMiddleware } from "./typings/simpletypes";
package/dist/index.js CHANGED
@@ -14,9 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.SetBodyParser = exports.SetRateLimiter = exports.SetHelmet = exports.SetCOOP = exports.SetCOEP = exports.SetPermissionsPolicy = exports.SetReferrerPolicy = exports.SetNoSniff = exports.SetFrameGuard = exports.SetCSP = exports.SetHSTS = exports.SetCORS = exports.CreateSimpleJsHttpsServer = exports.CreateSimpleJsHttpServer = exports.SimpleNodeJsController = void 0;
18
- var simpleController_1 = require("./utils/simpleController");
19
- Object.defineProperty(exports, "SimpleNodeJsController", { enumerable: true, get: function () { return simpleController_1.SimpleNodeJsController; } });
17
+ exports.SetBodyParser = exports.SetRateLimiter = exports.SetHelmet = exports.SetCOOP = exports.SetCOEP = exports.SetPermissionsPolicy = exports.SetReferrerPolicy = exports.SetNoSniff = exports.SetFrameGuard = exports.SetCSP = exports.SetHSTS = exports.SetCORS = exports.CreateSimpleJsHttpsServer = exports.CreateSimpleJsHttpServer = void 0;
20
18
  var server_1 = require("./server");
21
19
  Object.defineProperty(exports, "CreateSimpleJsHttpServer", { enumerable: true, get: function () { return server_1.CreateSimpleJsHttpServer; } });
22
20
  Object.defineProperty(exports, "CreateSimpleJsHttpsServer", { enumerable: true, get: function () { return server_1.CreateSimpleJsHttpsServer; } });
package/dist/router.js CHANGED
@@ -3,11 +3,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.setControllersDir = setControllersDir;
4
4
  exports.route = route;
5
5
  const helpers_1 = require("./utils/helpers");
6
- const simpleController_1 = require("./utils/simpleController");
7
6
  let controllers = new Map();
8
7
  const UNSAFE_METHODS = new Set([
9
8
  ...Object.getOwnPropertyNames(Object.prototype),
10
- ...Object.getOwnPropertyNames(simpleController_1.SimpleNodeJsController.prototype),
11
9
  ]);
12
10
  function setControllersDir(dir) {
13
11
  controllers = (0, helpers_1.loadControllers)(dir);
@@ -17,55 +15,52 @@ async function route(req, res) {
17
15
  let controllerPath = (parts.length > 2 ? "/" + parts.slice(0, 2).join("/") : `/${parts.join("/")}`).toLowerCase().replace(/\-{1}\w{1}/g, match => match.replace("-", "").toUpperCase());
18
16
  let methodName = parts.length > 2 ? parts[2] : "index";
19
17
  let id = methodName !== "index" ? parts.slice(3) : [];
20
- let httpMethod = (req.method || "").toLowerCase();
18
+ const httpMethod = (req.method || "").toLowerCase();
21
19
  const meta = controllers.get(controllerPath);
22
- //if the controller is not available or not found
23
20
  if (!meta || !meta.name || !meta.Controller)
24
21
  return (0, helpers_1.throwHttpError)(404, "The requested resource does not exist");
25
22
  const ControllerClass = meta.Controller;
26
23
  const controller = new ControllerClass();
27
- //Update the method name to the framework pattern
28
24
  methodName = (methodName || "").replace(/\-{1}\w{1}/g, match => match.replace("-", "").toUpperCase());
29
- // Block Object.prototype methods (constructor, toString, etc.) and __private convention
25
+ // Block Object.prototype methods and __private convention
30
26
  if (methodName.startsWith("__") || UNSAFE_METHODS.has(methodName)) {
31
27
  return (0, helpers_1.throwHttpError)(404, "The requested resource does not exist");
32
28
  }
33
- //if the endpoint not a function
29
+ const ctx = {
30
+ req, res,
31
+ body: req.body,
32
+ query: req.query,
33
+ customData: req._custom_data,
34
+ };
35
+ // Fallback to index if method not found (treat path segment as id)
34
36
  if (typeof controller[methodName] !== "function") {
35
37
  if (typeof controller["index"] === "function" && parts.length === 3) {
38
+ id = parts.slice(2);
36
39
  methodName = "index";
37
- id = parts.slice(2) || []; // pass the rest of the path as ID;
38
40
  }
39
41
  else {
40
42
  return (0, helpers_1.throwHttpError)(404, "The requested resource does not exist");
41
43
  }
42
44
  }
43
- //if the data require params but there's no matching params
44
- if (id && id.length && (!controller[methodName].length || controller[methodName].length < id.length)) {
45
- return (0, helpers_1.throwHttpError)(404, "The requested resource does not exist");
45
+ //checking if the method does not require id but id is provided, if so, return 404
46
+ if (id.length && (!controller[methodName].length || controller[methodName].length === 1)) {
47
+ return (0, helpers_1.throwHttpError)(404, "Resource not found");
46
48
  }
47
- //bind the controller to use the global properties
48
- controller.__bindContext({ req, res });
49
- let result = await controller[methodName](...id);
50
- //if the cycle has ended
49
+ const descriptors = await controller[methodName](ctx, ...id);
51
50
  if (res.writableEnded)
52
51
  return;
53
- //if the controller returned nothing and response is not ended, end it
54
- if (!result && !res.writableEnded)
52
+ if (!descriptors || !Array.isArray(descriptors))
55
53
  return res.end();
56
- //if there's no method defined for the http verb, return 405
57
- if (result && typeof result[httpMethod] !== "function")
54
+ const descriptor = descriptors.find((d) => d.method === httpMethod);
55
+ if (!descriptor)
58
56
  return (0, helpers_1.throwHttpError)(405, "Method Not Allowed");
59
- // ID validation rules
60
- if (id.length && (!result.id || !result.id[httpMethod]))
57
+ // Id validation
58
+ if (id.length && !descriptor.id)
61
59
  return (0, helpers_1.throwHttpError)(404, "Resource not found");
62
- if (result.id && result.id[httpMethod] === "required" && !id.length)
60
+ if (descriptor.id === "required" && !id.length)
63
61
  return (0, helpers_1.throwHttpError)(404, "Resource not found");
64
- result = await result[httpMethod]({
65
- req, res, query: controller.query, body: controller.body,
66
- id: id.join("/"), customData: controller._custom_data
67
- });
68
- //if not responded
62
+ // bind to controller so `this` works in regular methods too
63
+ await descriptor.handler.bind(controller)(ctx, ...id);
69
64
  if (!res.writableEnded)
70
65
  res.end("");
71
66
  }
package/dist/server.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import https from "node:https";
2
- import { SimpleJsHttpsServer, SimpleJsServer } from "./typings/general";
2
+ import { SimpleJsHttpsServer, SimpleJsServer } from "./typings/simpletypes";
3
3
  type ServerOptions = {
4
4
  controllersDir?: string;
5
5
  tlsOpts?: https.ServerOptions;
@@ -1,7 +1,4 @@
1
- import http, { IncomingMessage, ServerResponse } from "http";
2
- import https from "node:https";
3
- export type Next = () => Promise<any> | void;
4
- export type Plugin = (app: SimpleJsServer, opts?: any) => Promise<any> | void;
1
+ import { IncomingMessage, ServerResponse } from "http";
5
2
  export type HttpMethod = "get" | "post" | "put" | "patch" | "delete";
6
3
  export type ObjectPayload = {
7
4
  [key: string]: any;
@@ -18,23 +15,3 @@ export type ResponseObject = ServerResponse & {
18
15
  json: (value: object) => void;
19
16
  text: (value?: string) => void;
20
17
  };
21
- export type Middleware = (req: RequestObject, res: ResponseObject, next: () => Promise<any> | void) => Promise<any> | void;
22
- export type ErrorMiddleware = (err: any, req: RequestObject, res: ResponseObject, next: Next) => Promise<boolean> | void;
23
- export interface SimpleJsServer extends http.Server {
24
- use(mw: Middleware): Promise<any> | void;
25
- useError: (mw: ErrorMiddleware) => void;
26
- registerPlugin: (plugin: Plugin) => Promise<any> | void;
27
- }
28
- export interface SimpleJsHttpsServer extends https.Server {
29
- use(mw: Middleware): Promise<any> | void;
30
- useError: (mw: ErrorMiddleware) => void;
31
- registerPlugin: (plugin: Plugin) => Promise<any> | void;
32
- }
33
- export interface SimpleJsPrivateMethodProps {
34
- body: ObjectPayload;
35
- res: ResponseObject;
36
- req: RequestObject;
37
- query: ObjectPayload;
38
- customData: any;
39
- id?: string;
40
- }
@@ -1,4 +1,8 @@
1
- import { HttpMethod, RequestObject, SimpleJsPrivateMethodProps } from "./general";
1
+ import { HttpMethod, ObjectPayload, RequestObject, ResponseObject } from "./general";
2
+ import http from "node:http";
3
+ import https from "node:https";
4
+ export type Next = () => Promise<any> | void;
5
+ export type Plugin = (app: SimpleJsServer, opts?: any) => Promise<any> | void;
2
6
  export type SimpleJSRateLimitType = {
3
7
  windowMs: number;
4
8
  max: number;
@@ -23,11 +27,27 @@ export interface SimpleJsControllerMeta {
23
27
  name: string;
24
28
  Controller: any;
25
29
  }
26
- export interface SubRequestHandler {
27
- post?: (params: SimpleJsPrivateMethodProps) => void;
28
- put?: (params: SimpleJsPrivateMethodProps) => void;
29
- get?: (params: SimpleJsPrivateMethodProps) => void;
30
- delete?: (params: SimpleJsPrivateMethodProps) => void;
31
- patch?: (params: SimpleJsPrivateMethodProps) => void;
32
- id?: Partial<Record<HttpMethod, "required" | "optional">>;
30
+ export type Middleware = (req: RequestObject, res: ResponseObject, next: () => Promise<any> | void) => Promise<any> | void;
31
+ export type ErrorMiddleware = (err: any, req: RequestObject, res: ResponseObject, next: Next) => Promise<boolean> | void;
32
+ export interface SimpleJsServer extends http.Server {
33
+ use(mw: Middleware): Promise<any> | void;
34
+ useError: (mw: ErrorMiddleware) => void;
35
+ registerPlugin: (plugin: Plugin) => Promise<any> | void;
36
+ }
37
+ export interface SimpleJsHttpsServer extends https.Server {
38
+ use(mw: Middleware): Promise<any> | void;
39
+ useError: (mw: ErrorMiddleware) => void;
40
+ registerPlugin: (plugin: Plugin) => Promise<any> | void;
41
+ }
42
+ export interface SimpleJsCtx {
43
+ body: ObjectPayload;
44
+ res: ResponseObject;
45
+ req: RequestObject;
46
+ query: ObjectPayload;
47
+ customData: any;
48
+ }
49
+ export interface SimpleJsEndpointDescriptor {
50
+ method: HttpMethod;
51
+ id?: "required" | "optional";
52
+ handler: (ctx: SimpleJsCtx, id?: string) => any;
33
53
  }
@@ -1,5 +1,5 @@
1
- import { ErrorMiddleware, Middleware, RequestObject, ResponseObject } from "../typings/general";
2
- import { SimpleJsControllerMeta } from "../typings/simpletypes";
1
+ import { RequestObject, ResponseObject } from "../typings/general";
2
+ import { ErrorMiddleware, Middleware, SimpleJsControllerMeta } from "../typings/simpletypes";
3
3
  export declare function composeMiddleware(middlewares: Middleware[]): (req: RequestObject, res: ResponseObject) => Promise<void>;
4
4
  export declare function runErrorMiddlewares(err: unknown, errorMiddlewares: ErrorMiddleware[], req: RequestObject, res: ResponseObject): Promise<void>;
5
5
  export declare function throwHttpError(code: number, message: string): never;
@@ -53,8 +53,8 @@ function loadControllers(root = "controllers") {
53
53
  const realBase = node_fs_1.default.realpathSync(base); // resolve the base itself (may be a symlink)
54
54
  const map = new Map();
55
55
  function walk(dir) {
56
- for (const file of node_fs_1.default.readdirSync(dir)) {
57
- const full = node_path_1.default.join(dir, file);
56
+ for (const entry of node_fs_1.default.readdirSync(dir, { withFileTypes: true })) {
57
+ const full = node_path_1.default.join(dir, entry.name);
58
58
  let realFull;
59
59
  try {
60
60
  realFull = node_fs_1.default.realpathSync(full); // resolve actual disk path, following all symlinks
@@ -65,10 +65,11 @@ function loadControllers(root = "controllers") {
65
65
  // Block anything whose real path is outside the controllers directory
66
66
  if (!realFull.startsWith(realBase + node_path_1.default.sep))
67
67
  continue;
68
- if (node_fs_1.default.statSync(full).isDirectory())
68
+ const isDir = entry.isDirectory() || (entry.isSymbolicLink() && node_fs_1.default.statSync(realFull).isDirectory());
69
+ if (isDir)
69
70
  walk(full);
70
- else if (file.endsWith(".js") || file.endsWith(".ts")) {
71
- const Controller = require(full)?.default;
71
+ else if (entry.name.endsWith(".js") || entry.name.endsWith(".ts")) {
72
+ const Controller = require(realFull)?.default; // use realFull to prevent TOCTOU
72
73
  if (typeof Controller !== "function")
73
74
  continue;
74
75
  const key = full.slice(base.length).replace(/\\/g, "/").replace(/\.(ts|js)$/, "");
@@ -52,8 +52,9 @@ function SetHSTS(opts) {
52
52
  }
53
53
  // ─── Content Security Policy ──────────────────────────────────────────────────
54
54
  function SetCSP(policy = "default-src 'none'") {
55
+ const safePolicy = policy.replace(/[\r\n]/g, "");
55
56
  return async (_req, res, next) => {
56
- res.setHeader("Content-Security-Policy", policy);
57
+ res.setHeader("Content-Security-Policy", safePolicy);
57
58
  await next();
58
59
  };
59
60
  }
@@ -73,29 +74,33 @@ function SetNoSniff() {
73
74
  }
74
75
  // ─── Referrer-Policy ──────────────────────────────────────────────────────────
75
76
  function SetReferrerPolicy(policy = "no-referrer") {
77
+ const safePolicy = policy.replace(/[\r\n]/g, "");
76
78
  return async (_req, res, next) => {
77
- res.setHeader("Referrer-Policy", policy);
79
+ res.setHeader("Referrer-Policy", safePolicy);
78
80
  await next();
79
81
  };
80
82
  }
81
83
  // ─── Permissions-Policy (browser feature control) ────────────────────────────
82
84
  function SetPermissionsPolicy(policy = "camera=(), microphone=(), geolocation=(), payment=(), usb=(), display-capture=()") {
85
+ const safePolicy = policy.replace(/[\r\n]/g, "");
83
86
  return async (_req, res, next) => {
84
- res.setHeader("Permissions-Policy", policy);
87
+ res.setHeader("Permissions-Policy", safePolicy);
85
88
  await next();
86
89
  };
87
90
  }
88
91
  // ─── Cross-Origin-Embedder-Policy ────────────────────────────────────────────
89
92
  function SetCOEP(value = "require-corp") {
93
+ const safeValue = value.replace(/[\r\n]/g, "");
90
94
  return async (_req, res, next) => {
91
- res.setHeader("Cross-Origin-Embedder-Policy", value);
95
+ res.setHeader("Cross-Origin-Embedder-Policy", safeValue);
92
96
  await next();
93
97
  };
94
98
  }
95
99
  // ─── Cross-Origin-Opener-Policy ──────────────────────────────────────────────
96
100
  function SetCOOP(value = "same-origin") {
101
+ const safeValue = value.replace(/[\r\n]/g, "");
97
102
  return async (_req, res, next) => {
98
- res.setHeader("Cross-Origin-Opener-Policy", value);
103
+ res.setHeader("Cross-Origin-Opener-Policy", safeValue);
99
104
  await next();
100
105
  };
101
106
  }
@@ -105,11 +110,11 @@ function SetHelmet(opts) {
105
110
  if (opts?.noSniff !== false)
106
111
  res.setHeader("X-Content-Type-Options", "nosniff");
107
112
  if (opts?.frameGuard !== false)
108
- res.setHeader("X-Frame-Options", typeof opts?.frameGuard === "string" ? opts.frameGuard : "DENY");
113
+ res.setHeader("X-Frame-Options", typeof opts?.frameGuard === "string" ? opts.frameGuard.replace(/[\r\n]/g, "") : "DENY");
109
114
  if (opts?.referrerPolicy !== false)
110
- res.setHeader("Referrer-Policy", typeof opts?.referrerPolicy === "string" ? opts.referrerPolicy : "no-referrer");
115
+ res.setHeader("Referrer-Policy", typeof opts?.referrerPolicy === "string" ? opts.referrerPolicy.replace(/[\r\n]/g, "") : "no-referrer");
111
116
  if (opts?.csp !== false)
112
- res.setHeader("Content-Security-Policy", typeof opts?.csp === "string" ? opts.csp : "default-src 'none'");
117
+ res.setHeader("Content-Security-Policy", typeof opts?.csp === "string" ? opts.csp.replace(/[\r\n]/g, "") : "default-src 'none'");
113
118
  if (opts?.hsts !== false) {
114
119
  const hsts = (opts?.hsts && typeof opts.hsts === "object") ? opts.hsts : {};
115
120
  let hstsValue = `max-age=${hsts.maxAge ?? 31536000}`;
@@ -120,11 +125,11 @@ function SetHelmet(opts) {
120
125
  res.setHeader("Strict-Transport-Security", hstsValue);
121
126
  }
122
127
  if (opts?.permissionsPolicy !== false)
123
- res.setHeader("Permissions-Policy", typeof opts?.permissionsPolicy === "string" ? opts.permissionsPolicy : "camera=(), microphone=(), geolocation=(), payment=(), usb=(), display-capture=()");
128
+ res.setHeader("Permissions-Policy", typeof opts?.permissionsPolicy === "string" ? opts.permissionsPolicy.replace(/[\r\n]/g, "") : "camera=(), microphone=(), geolocation=(), payment=(), usb=(), display-capture=()");
124
129
  if (opts?.coep !== false)
125
- res.setHeader("Cross-Origin-Embedder-Policy", typeof opts?.coep === "string" ? opts.coep : "require-corp");
130
+ res.setHeader("Cross-Origin-Embedder-Policy", typeof opts?.coep === "string" ? opts.coep.replace(/[\r\n]/g, "") : "require-corp");
126
131
  if (opts?.coop !== false)
127
- res.setHeader("Cross-Origin-Opener-Policy", typeof opts?.coop === "string" ? opts.coop : "same-origin");
132
+ res.setHeader("Cross-Origin-Opener-Policy", typeof opts?.coop === "string" ? opts.coop.replace(/[\r\n]/g, "") : "same-origin");
128
133
  await next();
129
134
  };
130
135
  }
@@ -141,10 +146,11 @@ function SetRateLimiter(opts) {
141
146
  }, opts.windowMs);
142
147
  timer.unref();
143
148
  return async (req, res, next) => {
149
+ const xff = String(req.headers["x-forwarded-for"] || "");
144
150
  const ip = opts.trustProxy
145
151
  ? (Array.isArray(req.headers["x-forwarded-for"])
146
152
  ? req.headers["x-forwarded-for"][0]
147
- : String(req.headers["x-forwarded-for"] || "").split(",")[0].trim()) || req.socket.remoteAddress || "unknown"
153
+ : (xff.indexOf(",") >= 0 ? xff.slice(0, xff.indexOf(",")) : xff).trim()) || req.socket.remoteAddress || "unknown"
148
154
  : req.socket.remoteAddress || "unknown";
149
155
  const key = String(opts.keyGenerator?.(req) || ip || "unknown");
150
156
  const now = Date.now();
@@ -199,7 +205,7 @@ function SetBodyParser(opts) {
199
205
  if (shouldIgnoreStream)
200
206
  return resolve(next());
201
207
  let size = 0;
202
- let body = "";
208
+ const chunks = [];
203
209
  req.on("data", chunk => {
204
210
  size += chunk.length;
205
211
  if (maxSize && size > maxSize) {
@@ -210,12 +216,13 @@ function SetBodyParser(opts) {
210
216
  req.socket.destroy();
211
217
  return;
212
218
  }
213
- body += chunk;
219
+ chunks.push(chunk);
214
220
  });
215
221
  req.on("end", () => {
216
222
  if (res.writableEnded)
217
223
  return resolve();
218
224
  try {
225
+ const body = Buffer.concat(chunks).toString();
219
226
  if (body && contentType.includes("application/json")) {
220
227
  req.body = JSON.parse(body);
221
228
  }
@@ -1,5 +1,4 @@
1
- import { SimpleJsServer } from "../typings/general";
2
- import { SimpleJSRateLimitType } from "../typings/simpletypes";
1
+ import { SimpleJSRateLimitType, SimpleJsServer } from "../typings/simpletypes";
3
2
  import { SetCORS, SetHelmet } from "./simpleMiddleware";
4
3
  export declare function SimpleJsSecurityPlugin(app: SimpleJsServer, opts: {
5
4
  cors?: Parameters<typeof SetCORS>[0];
@@ -40,10 +40,11 @@ function SimpleJsIPWhitelistPlugin(app, opts) {
40
40
  const mode = opts.mode || "allow";
41
41
  const ipSet = new Set(opts.ips);
42
42
  app.use(async (req, _res, next) => {
43
+ const xff = String(req.headers["x-forwarded-for"] || "");
43
44
  const raw = opts.trustProxy
44
45
  ? (Array.isArray(req.headers["x-forwarded-for"])
45
46
  ? req.headers["x-forwarded-for"][0]
46
- : String(req.headers["x-forwarded-for"] || "").split(",")[0].trim()) || req.socket.remoteAddress || ""
47
+ : (xff.indexOf(",") >= 0 ? xff.slice(0, xff.indexOf(",")) : xff).trim()) || req.socket.remoteAddress || ""
47
48
  : req.socket.remoteAddress || "";
48
49
  const ip = normalizeIP(raw);
49
50
  const inList = ipSet.has(ip);
@@ -56,7 +57,7 @@ function SimpleJsIPWhitelistPlugin(app, opts) {
56
57
  }
57
58
  // ─── Cookie Plugin ────────────────────────────────────────────────────────────
58
59
  function parseCookieHeader(header) {
59
- const result = {};
60
+ const result = Object.create(null);
60
61
  for (const part of header.split(";")) {
61
62
  const idx = part.indexOf("=");
62
63
  if (idx < 0)
@@ -91,7 +92,7 @@ function SimpleJsCookiePlugin(app, opts) {
91
92
  app.use(async (req, _res, next) => {
92
93
  const raw = parseCookieHeader(req.headers.cookie || "");
93
94
  if (opts?.secret) {
94
- const verified = {};
95
+ const verified = Object.create(null);
95
96
  for (const [k, v] of Object.entries(raw)) {
96
97
  if (v.startsWith("s:")) {
97
98
  const inner = v.slice(2);
@@ -192,10 +193,11 @@ function SimpleJsMaintenanceModePlugin(app, opts) {
192
193
  app.use(async (req, res, next) => {
193
194
  if (!opts.enabled)
194
195
  return next();
196
+ const xff2 = String(req.headers["x-forwarded-for"] || "");
195
197
  const raw = opts.trustProxy
196
198
  ? (Array.isArray(req.headers["x-forwarded-for"])
197
199
  ? req.headers["x-forwarded-for"][0]
198
- : String(req.headers["x-forwarded-for"] || "").split(",")[0].trim()) || req.socket.remoteAddress || ""
200
+ : (xff2.indexOf(",") >= 0 ? xff2.slice(0, xff2.indexOf(",")) : xff2).trim()) || req.socket.remoteAddress || ""
199
201
  : req.socket.remoteAddress || "";
200
202
  const ip = normalizeIP(raw);
201
203
  if (opts.allowIPs?.includes(ip))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@increase21/simplenodejs",
3
- "version": "1.0.30",
3
+ "version": "1.1.0",
4
4
  "description": "Lightweight Node.js HTTP framework with middlewares and plugins",
5
5
  "dev": "dist/index.js",
6
6
  "bugs": "https://github.com/increase21/simplenodejs/issues",
@@ -40,4 +40,4 @@
40
40
  "devDependencies": {
41
41
  "typescript": "^5.3.0"
42
42
  }
43
- }
43
+ }
@@ -1,14 +0,0 @@
1
- import { HttpMethod, ObjectPayload, RequestObject, ResponseObject } from "../typings/general";
2
- import { SubRequestHandler } from "../typings/simpletypes";
3
- export declare class SimpleNodeJsController {
4
- protected req: RequestObject;
5
- protected res: ResponseObject;
6
- protected body: ObjectPayload;
7
- protected query: ObjectPayload;
8
- protected method: HttpMethod;
9
- protected _custom_data: any;
10
- /** @internal */
11
- private __bindContext;
12
- protected __checkContext(): void;
13
- protected __run(handlers: SubRequestHandler): SubRequestHandler;
14
- }
@@ -1,20 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.SimpleNodeJsController = void 0;
4
- class SimpleNodeJsController {
5
- /** @internal */
6
- __bindContext(ctx) {
7
- this.req = ctx.req;
8
- this.res = ctx.res;
9
- this.body = ctx.req.body;
10
- this.query = ctx.req.query;
11
- this.method = (ctx.req.method || "").toLocaleLowerCase();
12
- this._custom_data = ctx.req._custom_data;
13
- this.__checkContext();
14
- }
15
- __checkContext() { }
16
- __run(handlers) {
17
- return handlers;
18
- }
19
- }
20
- exports.SimpleNodeJsController = SimpleNodeJsController;