@increase21/simplenodejs 1.0.20 → 1.0.22

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.
@@ -1,41 +1,35 @@
1
1
  "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
2
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.SetRequestCORS = SetRequestCORS;
3
+ exports.SetCORS = SetCORS;
4
+ exports.SetHSTS = SetHSTS;
5
+ exports.SetCSP = SetCSP;
6
+ exports.SetFrameGuard = SetFrameGuard;
7
+ exports.SetNoSniff = SetNoSniff;
8
+ exports.SetReferrerPolicy = SetReferrerPolicy;
9
+ exports.SetPermissionsPolicy = SetPermissionsPolicy;
10
+ exports.SetCOEP = SetCOEP;
11
+ exports.SetCOOP = SetCOOP;
12
+ exports.SetHelmet = SetHelmet;
7
13
  exports.SetRateLimiter = SetRateLimiter;
8
14
  exports.SetBodyParser = SetBodyParser;
9
- const node_querystring_1 = __importDefault(require("node:querystring"));
10
15
  const helpers_1 = require("./helpers");
11
- // core/cors.ts
12
- function SetRequestCORS(opts) {
16
+ // ─── CORS ────────────────────────────────────────────────────────────────────
17
+ function SetCORS(opts) {
13
18
  return async (req, res, next) => {
14
- const defaults = {
15
- "Access-Control-Allow-Origin": "*",
16
- "Access-Control-Allow-Headers": "Origin, X-Requested-With, Content-Type, Accept, Authorization",
17
- "Access-Control-Allow-Methods": "GET, POST, DELETE, PUT, PATCH",
18
- "X-Content-Type-Options": "nosniff",
19
- "X-Frame-Options": "DENY",
20
- "Referrer-Policy": "no-referrer",
21
- "Content-Security-Policy": "default-src 'none'",
22
- };
23
- let merged = { ...defaults };
24
- for (const { name, value } of opts) {
25
- merged[name] = value; // overrides defaults if same key
19
+ if (opts?.credentials) {
20
+ const reqOrigin = req.headers.origin;
21
+ if (!reqOrigin)
22
+ (0, helpers_1.throwHttpError)(403, "CORS Error: Origin header is required when credentials are enabled");
23
+ if (opts.origin && reqOrigin !== opts.origin)
24
+ (0, helpers_1.throwHttpError)(403, "CORS Error: Origin not allowed");
25
+ res.setHeader("Access-Control-Allow-Origin", reqOrigin);
26
+ res.setHeader("Access-Control-Allow-Credentials", "true");
26
27
  }
27
- //allow credentials only when origin specified
28
- if (merged["Access-Control-Allow-Credentials"] === "true") {
29
- const origin = req.headers.origin;
30
- if (!origin) {
31
- (0, helpers_1.throwHttpError)(403, "CORS Error: Origin header is required when Access-Control-Allow-Credentials is true");
32
- return;
33
- }
34
- merged["Access-Control-Allow-Origin"] = origin;
35
- }
36
- for (const [key, value] of Object.entries(merged)) {
37
- res.setHeader(key, value);
28
+ else {
29
+ res.setHeader("Access-Control-Allow-Origin", opts?.origin || "*");
38
30
  }
31
+ res.setHeader("Access-Control-Allow-Headers", opts?.headers || "Origin, X-Requested-With, Content-Type, Accept, Authorization");
32
+ res.setHeader("Access-Control-Allow-Methods", opts?.methods || "GET, POST, DELETE, PUT, PATCH");
39
33
  if (req.method === "OPTIONS") {
40
34
  res.status(204).end();
41
35
  return;
@@ -43,20 +37,115 @@ function SetRequestCORS(opts) {
43
37
  await next();
44
38
  };
45
39
  }
46
- // core/rateLimit.ts
40
+ // ─── HSTS ─────────────────────────────────────────────────────────────────────
41
+ // Only meaningful on HTTPS. Browsers ignore this header over plain HTTP.
42
+ function SetHSTS(opts) {
43
+ let value = `max-age=${opts?.maxAge ?? 31536000}`;
44
+ if (opts?.includeSubDomains !== false)
45
+ value += "; includeSubDomains";
46
+ if (opts?.preload)
47
+ value += "; preload";
48
+ return async (_req, res, next) => {
49
+ res.setHeader("Strict-Transport-Security", value);
50
+ await next();
51
+ };
52
+ }
53
+ // ─── Content Security Policy ──────────────────────────────────────────────────
54
+ function SetCSP(policy = "default-src 'none'") {
55
+ return async (_req, res, next) => {
56
+ res.setHeader("Content-Security-Policy", policy);
57
+ await next();
58
+ };
59
+ }
60
+ // ─── X-Frame-Options (clickjacking) ──────────────────────────────────────────
61
+ function SetFrameGuard(action = "DENY") {
62
+ return async (_req, res, next) => {
63
+ res.setHeader("X-Frame-Options", action);
64
+ await next();
65
+ };
66
+ }
67
+ // ─── X-Content-Type-Options (MIME sniffing) ───────────────────────────────────
68
+ function SetNoSniff() {
69
+ return async (_req, res, next) => {
70
+ res.setHeader("X-Content-Type-Options", "nosniff");
71
+ await next();
72
+ };
73
+ }
74
+ // ─── Referrer-Policy ──────────────────────────────────────────────────────────
75
+ function SetReferrerPolicy(policy = "no-referrer") {
76
+ return async (_req, res, next) => {
77
+ res.setHeader("Referrer-Policy", policy);
78
+ await next();
79
+ };
80
+ }
81
+ // ─── Permissions-Policy (browser feature control) ────────────────────────────
82
+ function SetPermissionsPolicy(policy = "camera=(), microphone=(), geolocation=(), payment=(), usb=(), display-capture=()") {
83
+ return async (_req, res, next) => {
84
+ res.setHeader("Permissions-Policy", policy);
85
+ await next();
86
+ };
87
+ }
88
+ // ─── Cross-Origin-Embedder-Policy ────────────────────────────────────────────
89
+ function SetCOEP(value = "require-corp") {
90
+ return async (_req, res, next) => {
91
+ res.setHeader("Cross-Origin-Embedder-Policy", value);
92
+ await next();
93
+ };
94
+ }
95
+ // ─── Cross-Origin-Opener-Policy ──────────────────────────────────────────────
96
+ function SetCOOP(value = "same-origin") {
97
+ return async (_req, res, next) => {
98
+ res.setHeader("Cross-Origin-Opener-Policy", value);
99
+ await next();
100
+ };
101
+ }
102
+ // ─── Helmet (security headers bundle, excludes CORS) ─────────────────────────
103
+ function SetHelmet(opts) {
104
+ return async (_req, res, next) => {
105
+ if (opts?.noSniff !== false)
106
+ res.setHeader("X-Content-Type-Options", "nosniff");
107
+ if (opts?.frameGuard !== false)
108
+ res.setHeader("X-Frame-Options", typeof opts?.frameGuard === "string" ? opts.frameGuard : "DENY");
109
+ if (opts?.referrerPolicy !== false)
110
+ res.setHeader("Referrer-Policy", typeof opts?.referrerPolicy === "string" ? opts.referrerPolicy : "no-referrer");
111
+ if (opts?.csp !== false)
112
+ res.setHeader("Content-Security-Policy", typeof opts?.csp === "string" ? opts.csp : "default-src 'none'");
113
+ if (opts?.hsts !== false) {
114
+ const hsts = (opts?.hsts && typeof opts.hsts === "object") ? opts.hsts : {};
115
+ let hstsValue = `max-age=${hsts.maxAge ?? 31536000}`;
116
+ if (hsts.includeSubDomains !== false)
117
+ hstsValue += "; includeSubDomains";
118
+ if (hsts.preload)
119
+ hstsValue += "; preload";
120
+ res.setHeader("Strict-Transport-Security", hstsValue);
121
+ }
122
+ if (opts?.permissionsPolicy !== false)
123
+ res.setHeader("Permissions-Policy", typeof opts?.permissionsPolicy === "string" ? opts.permissionsPolicy : "camera=(), microphone=(), geolocation=(), payment=(), usb=(), display-capture=()");
124
+ if (opts?.coep !== false)
125
+ res.setHeader("Cross-Origin-Embedder-Policy", typeof opts?.coep === "string" ? opts.coep : "require-corp");
126
+ if (opts?.coop !== false)
127
+ res.setHeader("Cross-Origin-Opener-Policy", typeof opts?.coop === "string" ? opts.coop : "same-origin");
128
+ await next();
129
+ };
130
+ }
131
+ // ─── Rate Limiter ─────────────────────────────────────────────────────────────
132
+ const RATE_LIMIT_MAX_STORE = 100000;
47
133
  function SetRateLimiter(opts) {
48
134
  const store = new Map();
49
135
  const timer = setInterval(() => {
50
136
  const now = Date.now();
51
137
  for (const [k, v] of store) {
52
- if (now - v.ts > opts.windowMs * 2)
138
+ if (now - v.ts > opts.windowMs)
53
139
  store.delete(k);
54
140
  }
55
141
  }, opts.windowMs);
56
142
  timer.unref();
57
143
  return async (req, res, next) => {
58
- const rawIp = req.headers["x-forwarded-for"] || req.socket.remoteAddress || "unknown";
59
- const ip = Array.isArray(rawIp) ? rawIp[0] : String(rawIp).split(",")[0].trim();
144
+ const ip = opts.trustProxy
145
+ ? (Array.isArray(req.headers["x-forwarded-for"])
146
+ ? req.headers["x-forwarded-for"][0]
147
+ : String(req.headers["x-forwarded-for"] || "").split(",")[0].trim()) || req.socket.remoteAddress || "unknown"
148
+ : req.socket.remoteAddress || "unknown";
60
149
  const key = String(opts.keyGenerator?.(req) || ip || "unknown");
61
150
  const now = Date.now();
62
151
  const entry = store.get(key) || { count: 0, ts: now };
@@ -65,6 +154,9 @@ function SetRateLimiter(opts) {
65
154
  entry.ts = now;
66
155
  }
67
156
  entry.count++;
157
+ if (!store.has(key) && store.size >= RATE_LIMIT_MAX_STORE) {
158
+ store.delete(store.keys().next().value); // evict oldest entry
159
+ }
68
160
  store.set(key, entry);
69
161
  if (entry.count > opts.max) {
70
162
  res.setHeader("Retry-After", Math.ceil(opts.windowMs / 1000));
@@ -75,7 +167,7 @@ function SetRateLimiter(opts) {
75
167
  await next();
76
168
  };
77
169
  }
78
- // core/body.ts
170
+ // ─── Body Parser ──────────────────────────────────────────────────────────────
79
171
  function SetBodyLimit(limit = "1mb") {
80
172
  if (typeof limit === "number")
81
173
  return limit;
@@ -90,15 +182,11 @@ function SetBodyLimit(limit = "1mb") {
90
182
  return n * 1024 * 1024;
91
183
  return n;
92
184
  }
93
- //For
94
185
  function SetBodyParser(opts) {
95
186
  const maxSize = SetBodyLimit(opts.limit);
96
187
  return (req, res, next) => new Promise((resolve, reject) => {
97
- //get the content type of the request
98
188
  const contentType = req.headers["content-type"] || "";
99
- // Skip multipart/form-data (file uploads)
100
- if (contentType.includes("multipart/form-data"))
101
- return resolve(next());
189
+ const isMultipart = contentType.includes("multipart/form-data");
102
190
  let size = 0;
103
191
  let body = "";
104
192
  req.on("data", chunk => {
@@ -111,34 +199,31 @@ function SetBodyParser(opts) {
111
199
  req.socket.destroy();
112
200
  return;
113
201
  }
114
- //add the body
115
- body += chunk;
202
+ if (!isMultipart)
203
+ body += chunk;
116
204
  });
117
205
  req.on("end", () => {
118
206
  if (res.writableEnded)
119
207
  return resolve();
120
208
  try {
121
- if (body && contentType.includes("application/json")) {
209
+ if (!isMultipart && body && contentType.includes("application/json")) {
122
210
  req.body = JSON.parse(body);
123
211
  }
124
- else {
212
+ else if (!isMultipart) {
125
213
  req.body = body;
126
214
  }
127
- //parse query
128
- if (req.query) {
129
- req.query = JSON.parse(JSON.stringify(node_querystring_1.default.parse(req.query)));
130
- }
131
215
  resolve(next());
132
216
  }
133
217
  catch (e) {
218
+ reject({ code: 400, error: "Invalid Payload" });
134
219
  if (!res.writableEnded)
135
- reject({ code: 400, error: "Invalid request body" });
220
+ res.status(400).end("Invalid Payload");
136
221
  }
137
222
  });
138
223
  req.on("error", () => {
224
+ reject({ code: 400, error: "Request stream ended" });
139
225
  if (!res.writableEnded)
140
- reject({ code: 400, error: "Request stream error" });
226
+ res.status(400).end("Request stream ended");
141
227
  });
142
228
  });
143
229
  }
144
- ;
@@ -1,9 +1,72 @@
1
1
  import { SimpleJsServer } from "../typings/general";
2
2
  import { SimpleJSRateLimitType } from "../typings/simpletypes";
3
+ import { SetCORS, SetHelmet } from "./simpleMiddleware";
3
4
  export declare function SimpleJsSecurityPlugin(app: SimpleJsServer, opts: {
4
- cors?: {
5
- name: string;
6
- value: string;
7
- }[];
5
+ cors?: Parameters<typeof SetCORS>[0];
6
+ helmet?: true | Parameters<typeof SetHelmet>[0];
8
7
  rateLimit?: SimpleJSRateLimitType;
9
8
  }): void;
9
+ /**
10
+ * Restricts access based on client IP.
11
+ * mode "allow" = only listed IPs can access (whitelist).
12
+ * mode "deny" = listed IPs are blocked (blacklist).
13
+ */
14
+ export declare function SimpleJsIPWhitelistPlugin(app: SimpleJsServer, opts: {
15
+ ips: string[];
16
+ mode?: "allow" | "deny";
17
+ trustProxy?: boolean;
18
+ }): void;
19
+ /**
20
+ * Creates a signed cookie value. Use this when setting a cookie in a response.
21
+ * The client sends it back as-is; SimpleJsCookiePlugin will verify and strip the signature.
22
+ */
23
+ export declare function SignCookie(value: string, secret: string): string;
24
+ /**
25
+ * Parses the Cookie header on every request.
26
+ * Cookies are available at `this._custom_data[dataKey]` inside controllers.
27
+ * If a secret is provided, signed cookies (prefixed "s:") are verified and unsigned.
28
+ * Cookies with invalid signatures are silently dropped.
29
+ */
30
+ export declare function SimpleJsCookiePlugin(app: SimpleJsServer, opts?: {
31
+ secret?: string;
32
+ /** Key used to attach cookies on _custom_data. Default: "cookies" */
33
+ dataKey?: string;
34
+ }): void;
35
+ /**
36
+ * Logs every request after it completes, including method, URL, status code, and duration.
37
+ */
38
+ export declare function SimpleJsRequestLoggerPlugin(app: SimpleJsServer, opts?: {
39
+ /** Custom log function. Defaults to console.log. */
40
+ logger?: (message: string) => void;
41
+ format?: "simple" | "json";
42
+ }): void;
43
+ /**
44
+ * Automatically closes requests that exceed the configured time limit.
45
+ */
46
+ export declare function SimpleJsTimeoutPlugin(app: SimpleJsServer, opts: {
47
+ /** Timeout in milliseconds */
48
+ ms: number;
49
+ message?: string;
50
+ }): void;
51
+ /**
52
+ * Sets Cache-Control response headers on every request.
53
+ */
54
+ export declare function SimpleJsCachePlugin(app: SimpleJsServer, opts: {
55
+ /** Max age in seconds for public caching */
56
+ maxAge?: number;
57
+ /** Mark response as private (user-specific, not shared caches) */
58
+ private?: boolean;
59
+ /** Disable all caching entirely */
60
+ noStore?: boolean;
61
+ }): void;
62
+ /**
63
+ * Returns 503 for all requests when maintenance mode is enabled.
64
+ * Optionally allow specific IPs to bypass (e.g. for internal testing).
65
+ */
66
+ export declare function SimpleJsMaintenanceModePlugin(app: SimpleJsServer, opts: {
67
+ enabled: boolean;
68
+ message?: string;
69
+ /** IPs that bypass maintenance mode (e.g. your office/server IP) */
70
+ allowIPs?: string[];
71
+ trustProxy?: boolean;
72
+ }): void;
@@ -1,8 +1,206 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.SimpleJsSecurityPlugin = SimpleJsSecurityPlugin;
7
+ exports.SimpleJsIPWhitelistPlugin = SimpleJsIPWhitelistPlugin;
8
+ exports.SignCookie = SignCookie;
9
+ exports.SimpleJsCookiePlugin = SimpleJsCookiePlugin;
10
+ exports.SimpleJsRequestLoggerPlugin = SimpleJsRequestLoggerPlugin;
11
+ exports.SimpleJsTimeoutPlugin = SimpleJsTimeoutPlugin;
12
+ exports.SimpleJsCachePlugin = SimpleJsCachePlugin;
13
+ exports.SimpleJsMaintenanceModePlugin = SimpleJsMaintenanceModePlugin;
14
+ const node_crypto_1 = __importDefault(require("node:crypto"));
15
+ const node_net_1 = __importDefault(require("node:net"));
16
+ const helpers_1 = require("./helpers");
4
17
  const simpleMiddleware_1 = require("./simpleMiddleware");
18
+ // ─── IP normalization ─────────────────────────────────────────────────────────
19
+ // Converts IPv4-mapped IPv6 addresses (e.g. ::ffff:127.0.0.1) to plain IPv4.
20
+ function normalizeIP(raw) {
21
+ const stripped = raw.replace(/^::ffff:/i, "");
22
+ return node_net_1.default.isIPv4(stripped) ? stripped : raw;
23
+ }
24
+ // ─── Security Plugin ──────────────────────────────────────────────────────────
5
25
  function SimpleJsSecurityPlugin(app, opts) {
6
- opts.cors && app.use((0, simpleMiddleware_1.SetRequestCORS)(opts.cors || []));
7
- opts.rateLimit && app.use((0, simpleMiddleware_1.SetRateLimiter)(opts.rateLimit || { windowMs: 1000, max: 100 }));
26
+ if (opts.cors)
27
+ app.use((0, simpleMiddleware_1.SetCORS)(opts.cors));
28
+ if (opts.helmet)
29
+ app.use((0, simpleMiddleware_1.SetHelmet)(opts.helmet === true ? undefined : opts.helmet));
30
+ if (opts.rateLimit)
31
+ app.use((0, simpleMiddleware_1.SetRateLimiter)(opts.rateLimit));
32
+ }
33
+ // ─── IP Whitelist / Blacklist Plugin ──────────────────────────────────────────
34
+ /**
35
+ * Restricts access based on client IP.
36
+ * mode "allow" = only listed IPs can access (whitelist).
37
+ * mode "deny" = listed IPs are blocked (blacklist).
38
+ */
39
+ function SimpleJsIPWhitelistPlugin(app, opts) {
40
+ const mode = opts.mode || "allow";
41
+ const ipSet = new Set(opts.ips);
42
+ app.use(async (req, _res, next) => {
43
+ const raw = opts.trustProxy
44
+ ? (Array.isArray(req.headers["x-forwarded-for"])
45
+ ? req.headers["x-forwarded-for"][0]
46
+ : String(req.headers["x-forwarded-for"] || "").split(",")[0].trim()) || req.socket.remoteAddress || ""
47
+ : req.socket.remoteAddress || "";
48
+ const ip = normalizeIP(raw);
49
+ const inList = ipSet.has(ip);
50
+ if (mode === "allow" && !inList)
51
+ (0, helpers_1.throwHttpError)(403, "Access denied");
52
+ if (mode === "deny" && inList)
53
+ (0, helpers_1.throwHttpError)(403, "Access denied");
54
+ await next();
55
+ });
56
+ }
57
+ // ─── Cookie Plugin ────────────────────────────────────────────────────────────
58
+ function parseCookieHeader(header) {
59
+ const result = {};
60
+ for (const part of header.split(";")) {
61
+ const idx = part.indexOf("=");
62
+ if (idx < 0)
63
+ continue;
64
+ const key = part.slice(0, idx).trim();
65
+ const val = part.slice(idx + 1).trim();
66
+ try {
67
+ result[key] = decodeURIComponent(val);
68
+ }
69
+ catch {
70
+ result[key] = val;
71
+ }
72
+ }
73
+ return result;
74
+ }
75
+ /**
76
+ * Creates a signed cookie value. Use this when setting a cookie in a response.
77
+ * The client sends it back as-is; SimpleJsCookiePlugin will verify and strip the signature.
78
+ */
79
+ function SignCookie(value, secret) {
80
+ const sig = node_crypto_1.default.createHmac("sha256", secret).update(value).digest("base64url");
81
+ return `s:${value}.${sig}`;
82
+ }
83
+ /**
84
+ * Parses the Cookie header on every request.
85
+ * Cookies are available at `this._custom_data[dataKey]` inside controllers.
86
+ * If a secret is provided, signed cookies (prefixed "s:") are verified and unsigned.
87
+ * Cookies with invalid signatures are silently dropped.
88
+ */
89
+ function SimpleJsCookiePlugin(app, opts) {
90
+ const dataKey = opts?.dataKey || "cookies";
91
+ app.use(async (req, _res, next) => {
92
+ const raw = parseCookieHeader(req.headers.cookie || "");
93
+ if (opts?.secret) {
94
+ const verified = {};
95
+ for (const [k, v] of Object.entries(raw)) {
96
+ if (v.startsWith("s:")) {
97
+ const inner = v.slice(2);
98
+ const dotIdx = inner.lastIndexOf(".");
99
+ if (dotIdx < 0)
100
+ continue; // malformed signed cookie — drop
101
+ const val = inner.slice(0, dotIdx);
102
+ const sig = inner.slice(dotIdx + 1);
103
+ const expected = node_crypto_1.default.createHmac("sha256", opts.secret).update(val).digest("base64url");
104
+ const sigBuf = Buffer.from(sig, "base64url");
105
+ const expectedBuf = Buffer.from(expected, "base64url");
106
+ if (sigBuf.length === expectedBuf.length && node_crypto_1.default.timingSafeEqual(sigBuf, expectedBuf)) {
107
+ verified[k] = val; // valid — attach unsigned value
108
+ }
109
+ // invalid signature — silently dropped
110
+ }
111
+ else {
112
+ verified[k] = v; // unsigned cookie — pass through
113
+ }
114
+ }
115
+ req._custom_data = { ...(req._custom_data || {}), [dataKey]: verified };
116
+ }
117
+ else {
118
+ req._custom_data = { ...(req._custom_data || {}), [dataKey]: raw };
119
+ }
120
+ await next();
121
+ });
122
+ }
123
+ // ─── Request Logger Plugin ────────────────────────────────────────────────────
124
+ /**
125
+ * Logs every request after it completes, including method, URL, status code, and duration.
126
+ */
127
+ function SimpleJsRequestLoggerPlugin(app, opts) {
128
+ const log = opts?.logger || console.log;
129
+ const format = opts?.format || "simple";
130
+ app.use(async (req, res, next) => {
131
+ const start = Date.now();
132
+ const method = req.method || "?";
133
+ const url = req.url || "/";
134
+ const path = url.split("?")[0]; // omit query string to avoid logging sensitive params
135
+ res.on("finish", () => {
136
+ const ms = Date.now() - start;
137
+ const status = res.statusCode;
138
+ if (format === "json") {
139
+ log(JSON.stringify({ time: new Date().toISOString(), method, path, status, ms, id: req.id }));
140
+ }
141
+ else {
142
+ log(`[${new Date().toISOString()}] ${method} ${path} ${status} ${ms}ms`);
143
+ }
144
+ });
145
+ await next();
146
+ });
147
+ }
148
+ // ─── Request Timeout Plugin ───────────────────────────────────────────────────
149
+ /**
150
+ * Automatically closes requests that exceed the configured time limit.
151
+ */
152
+ function SimpleJsTimeoutPlugin(app, opts) {
153
+ app.use(async (req, res, next) => {
154
+ const timer = setTimeout(() => {
155
+ if (!res.writableEnded) {
156
+ res.statusCode = 408;
157
+ res.end(opts.message || "Request timeout");
158
+ req.socket.destroy();
159
+ }
160
+ }, opts.ms);
161
+ res.on("finish", () => clearTimeout(timer));
162
+ res.on("close", () => clearTimeout(timer));
163
+ await next();
164
+ });
165
+ }
166
+ // ─── Cache Control Plugin ─────────────────────────────────────────────────────
167
+ /**
168
+ * Sets Cache-Control response headers on every request.
169
+ */
170
+ function SimpleJsCachePlugin(app, opts) {
171
+ let directive;
172
+ if (opts.noStore) {
173
+ directive = "no-store";
174
+ }
175
+ else if (opts.private) {
176
+ directive = `private, max-age=${opts.maxAge ?? 0}`;
177
+ }
178
+ else {
179
+ directive = `public, max-age=${opts.maxAge ?? 0}`;
180
+ }
181
+ app.use(async (_req, res, next) => {
182
+ res.setHeader("Cache-Control", directive);
183
+ await next();
184
+ });
185
+ }
186
+ // ─── Maintenance Mode Plugin ──────────────────────────────────────────────────
187
+ /**
188
+ * Returns 503 for all requests when maintenance mode is enabled.
189
+ * Optionally allow specific IPs to bypass (e.g. for internal testing).
190
+ */
191
+ function SimpleJsMaintenanceModePlugin(app, opts) {
192
+ app.use(async (req, res, next) => {
193
+ if (!opts.enabled)
194
+ return next();
195
+ const raw = opts.trustProxy
196
+ ? (Array.isArray(req.headers["x-forwarded-for"])
197
+ ? req.headers["x-forwarded-for"][0]
198
+ : String(req.headers["x-forwarded-for"] || "").split(",")[0].trim()) || req.socket.remoteAddress || ""
199
+ : req.socket.remoteAddress || "";
200
+ const ip = normalizeIP(raw);
201
+ if (opts.allowIPs?.includes(ip))
202
+ return next();
203
+ res.setHeader("Retry-After", "3600");
204
+ (0, helpers_1.throwHttpError)(503, opts.message || "Service is under maintenance. Please try again later.");
205
+ });
8
206
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@increase21/simplenodejs",
3
- "version": "1.0.20",
3
+ "version": "1.0.22",
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",