@increase21/simplenodejs 1.0.30 → 1.1.1
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 +57 -65
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -3
- package/dist/router.js +30 -28
- package/dist/server.d.ts +1 -1
- package/dist/typings/general.d.ts +1 -24
- package/dist/typings/simpletypes.d.ts +28 -8
- package/dist/utils/helpers.d.ts +2 -2
- package/dist/utils/helpers.js +6 -5
- package/dist/utils/simpleMiddleware.js +21 -14
- package/dist/utils/simplePlugins.d.ts +1 -2
- package/dist/utils/simplePlugins.js +6 -4
- package/package.json +1 -1
- package/dist/utils/simpleController.d.ts +0 -14
- package/dist/utils/simpleController.js +0 -20
package/README.md
CHANGED
|
@@ -105,51 +105,27 @@ controllers/
|
|
|
105
105
|
accountProfiles → /drivers/account-profiles
|
|
106
106
|
```
|
|
107
107
|
|
|
108
|
-
|
|
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 {
|
|
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
|
-
|
|
114
|
+
export default class AuthController {
|
|
144
115
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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(
|
|
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
|
-
##
|
|
161
|
+
## SimpleJsCtx
|
|
170
162
|
|
|
171
|
-
|
|
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 `
|
|
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({
|
|
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
|
|
443
|
+
// Set a signed cookie in a handler
|
|
452
444
|
const signed = SignCookie(sessionId, process.env.COOKIE_SECRET!);
|
|
453
|
-
|
|
445
|
+
ctx.res.setHeader("Set-Cookie", `session=${signed}; HttpOnly; Secure; SameSite=Strict`);
|
|
454
446
|
|
|
455
|
-
// Read cookie in any
|
|
456
|
-
const { session } =
|
|
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 {
|
|
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 =
|
|
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,59 @@ 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
|
-
|
|
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");
|
|
22
|
+
const ctx = {
|
|
23
|
+
req, res,
|
|
24
|
+
body: req.body,
|
|
25
|
+
query: req.query,
|
|
26
|
+
customData: req._custom_data,
|
|
27
|
+
};
|
|
25
28
|
const ControllerClass = meta.Controller;
|
|
26
|
-
const controller = new ControllerClass();
|
|
27
|
-
//
|
|
29
|
+
const controller = new ControllerClass(ctx);
|
|
30
|
+
//if request has ended, do not proceed
|
|
31
|
+
if (res.writableEnded)
|
|
32
|
+
return;
|
|
33
|
+
//sanitize method name, convert kebab-case to camelCase
|
|
28
34
|
methodName = (methodName || "").replace(/\-{1}\w{1}/g, match => match.replace("-", "").toUpperCase());
|
|
29
|
-
// Block Object.prototype methods
|
|
35
|
+
// Block Object.prototype methods and __private convention
|
|
30
36
|
if (methodName.startsWith("__") || UNSAFE_METHODS.has(methodName)) {
|
|
31
37
|
return (0, helpers_1.throwHttpError)(404, "The requested resource does not exist");
|
|
32
38
|
}
|
|
33
|
-
//if
|
|
39
|
+
// Fallback to index if method not found (treat path segment as id)
|
|
34
40
|
if (typeof controller[methodName] !== "function") {
|
|
35
41
|
if (typeof controller["index"] === "function" && parts.length === 3) {
|
|
42
|
+
id = parts.slice(2);
|
|
36
43
|
methodName = "index";
|
|
37
|
-
id = parts.slice(2) || []; // pass the rest of the path as ID;
|
|
38
44
|
}
|
|
39
45
|
else {
|
|
40
46
|
return (0, helpers_1.throwHttpError)(404, "The requested resource does not exist");
|
|
41
47
|
}
|
|
42
48
|
}
|
|
43
|
-
//if the
|
|
44
|
-
if (id
|
|
45
|
-
return (0, helpers_1.throwHttpError)(404, "
|
|
49
|
+
//checking if the method does not require id but id is provided, if so, return 404
|
|
50
|
+
if (id.length && (!controller[methodName].length || controller[methodName].length === 1)) {
|
|
51
|
+
return (0, helpers_1.throwHttpError)(404, "Resource not found");
|
|
46
52
|
}
|
|
47
|
-
|
|
48
|
-
controller
|
|
49
|
-
let result = await controller[methodName](...id);
|
|
50
|
-
//if the cycle has ended
|
|
53
|
+
const descriptors = await controller[methodName](ctx, ...id);
|
|
54
|
+
// If the controller method has already sent a response, do not proceed
|
|
51
55
|
if (res.writableEnded)
|
|
52
56
|
return;
|
|
53
|
-
//if the
|
|
54
|
-
if (!
|
|
57
|
+
//if the handler returns no descriptors or an invalid format, end the response
|
|
58
|
+
if (!descriptors || !Array.isArray(descriptors))
|
|
55
59
|
return res.end();
|
|
56
|
-
//
|
|
57
|
-
|
|
60
|
+
// Find the descriptor matching the HTTP method
|
|
61
|
+
const descriptor = descriptors.find((d) => d.method === httpMethod);
|
|
62
|
+
if (!descriptor)
|
|
58
63
|
return (0, helpers_1.throwHttpError)(405, "Method Not Allowed");
|
|
59
|
-
//
|
|
60
|
-
if (id.length &&
|
|
64
|
+
// Id validation
|
|
65
|
+
if (id.length && !descriptor.id)
|
|
61
66
|
return (0, helpers_1.throwHttpError)(404, "Resource not found");
|
|
62
|
-
if (
|
|
67
|
+
if (descriptor.id === "required" && !id.length)
|
|
63
68
|
return (0, helpers_1.throwHttpError)(404, "Resource not found");
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
id: id.join("/"), customData: controller._custom_data
|
|
67
|
-
});
|
|
68
|
-
//if not responded
|
|
69
|
+
// bind to controller so `this` works in regular methods too
|
|
70
|
+
await descriptor.handler.bind(controller)(ctx, ...id);
|
|
69
71
|
if (!res.writableEnded)
|
|
70
72
|
res.end("");
|
|
71
73
|
}
|
package/dist/server.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import https from "node:https";
|
|
2
|
-
import { SimpleJsHttpsServer, SimpleJsServer } from "./typings/
|
|
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
|
|
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,
|
|
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
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
}
|
package/dist/utils/helpers.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
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;
|
package/dist/utils/helpers.js
CHANGED
|
@@ -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
|
|
57
|
-
const full = node_path_1.default.join(dir,
|
|
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
|
-
|
|
68
|
+
const isDir = entry.isDirectory() || (entry.isSymbolicLink() && node_fs_1.default.statSync(realFull).isDirectory());
|
|
69
|
+
if (isDir)
|
|
69
70
|
walk(full);
|
|
70
|
-
else if (
|
|
71
|
-
const Controller = require(
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
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
|
-
:
|
|
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
|
-
|
|
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
|
-
|
|
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/
|
|
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
|
-
:
|
|
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
|
-
:
|
|
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,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;
|