@technomoron/api-server-base 1.0.42 → 1.0.43

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.txt CHANGED
@@ -105,6 +105,10 @@ Request Lifecycle
105
105
  5. The handler executes and returns its tuple. Responses are normalized to { code, message, data } JSON.
106
106
  6. Errors bubble into the wrapper. ApiError instances respect the provided status codes; other exceptions result in a 500 with text derived from guessExceptionText.
107
107
 
108
+ Client IP Helpers
109
+ -----------------
110
+ Use getClientIp(req) to obtain the most likely client address, skipping loopback entries collected from proxy headers. Call getClientIpChain(req) when you need the de-duplicated sequence gathered from the standard Forwarded/X-Forwarded-For/X-Real-IP headers as well as Express' req.ip/req.ips and the underlying socket.
111
+
108
112
  Extending the Base Classes
109
113
  --------------------------
110
114
  Override getApiKey, getUser, authenticateUser, storeToken, getToken, updateToken, deleteToken, and verifyPassword to integrate with your persistence layer.
@@ -62,6 +62,76 @@ function hydrateGetBody(req) {
62
62
  }
63
63
  req.body = { ...query, ...body };
64
64
  }
65
+ function normalizeIpAddress(candidate) {
66
+ let value = candidate.trim();
67
+ if (!value) {
68
+ return null;
69
+ }
70
+ value = value.replace(/^"+|"+$/g, '').replace(/^'+|'+$/g, '');
71
+ if (value.startsWith('::ffff:')) {
72
+ value = value.slice(7);
73
+ }
74
+ if (value.startsWith('[') && value.endsWith(']')) {
75
+ value = value.slice(1, -1);
76
+ }
77
+ const firstColon = value.indexOf(':');
78
+ const lastColon = value.lastIndexOf(':');
79
+ if (firstColon !== -1 && firstColon === lastColon) {
80
+ const maybePort = value.slice(lastColon + 1);
81
+ if (/^\d+$/.test(maybePort)) {
82
+ value = value.slice(0, lastColon);
83
+ }
84
+ }
85
+ value = value.trim();
86
+ return value || null;
87
+ }
88
+ function extractForwardedFor(header) {
89
+ if (!header) {
90
+ return [];
91
+ }
92
+ const values = Array.isArray(header) ? header : [header];
93
+ const ips = [];
94
+ for (const entry of values) {
95
+ for (const part of entry.split(',')) {
96
+ const normalized = normalizeIpAddress(part);
97
+ if (normalized) {
98
+ ips.push(normalized);
99
+ }
100
+ }
101
+ }
102
+ return ips;
103
+ }
104
+ function extractForwardedHeader(header) {
105
+ if (!header) {
106
+ return [];
107
+ }
108
+ const values = Array.isArray(header) ? header : [header];
109
+ const ips = [];
110
+ for (const entry of values) {
111
+ for (const part of entry.split(',')) {
112
+ const match = part.match(/for=([^;]+)/i);
113
+ if (match) {
114
+ const normalized = normalizeIpAddress(match[1]);
115
+ if (normalized) {
116
+ ips.push(normalized);
117
+ }
118
+ }
119
+ }
120
+ }
121
+ return ips;
122
+ }
123
+ function isLoopbackAddress(ip) {
124
+ if (ip === '::1' || ip === '0:0:0:0:0:0:0:1') {
125
+ return true;
126
+ }
127
+ if (ip === '0.0.0.0' || ip === '127.0.0.1') {
128
+ return true;
129
+ }
130
+ if (ip.startsWith('127.')) {
131
+ return true;
132
+ }
133
+ return false;
134
+ }
65
135
  class ApiError extends Error {
66
136
  constructor({ code, message, data, errors }) {
67
137
  const msg = guess_exception_text(message, '[Unknown error (null/undefined)]');
@@ -174,27 +244,36 @@ class ApiServer {
174
244
  }
175
245
  }
176
246
  async getApiKey(token) {
247
+ void token;
177
248
  return null;
178
249
  }
179
250
  async getUser(uid) {
251
+ void uid;
180
252
  throw new Error('getUser() not implemented');
181
253
  }
182
254
  async authenticateUser(params) {
255
+ void params;
183
256
  throw new Error('authenticateUser() not implemented');
184
257
  }
185
258
  async storeToken(params) {
259
+ void params;
186
260
  throw new Error('storeToken() not implemented');
187
261
  }
188
262
  async getToken(params) {
263
+ void params;
189
264
  throw new Error('getToken() not implemented');
190
265
  }
191
266
  async updateToken(params) {
267
+ void params;
192
268
  throw new Error('updateToken() not implemented');
193
269
  }
194
270
  async deleteToken(params) {
271
+ void params;
195
272
  throw new Error('deleteToken() not implemented');
196
273
  }
197
274
  async verifyPassword(password, hash) {
275
+ void password;
276
+ void hash;
198
277
  throw new Error('verifyPassword() not implemented');
199
278
  }
200
279
  filterUser(fullUser) {
@@ -203,7 +282,10 @@ class ApiServer {
203
282
  guessExceptionText(error, defMsg = 'Unkown Error') {
204
283
  return guess_exception_text(error, defMsg);
205
284
  }
206
- async authorize(apiReq, requiredClass) { }
285
+ async authorize(apiReq, requiredClass) {
286
+ void apiReq;
287
+ void requiredClass;
288
+ }
207
289
  middlewares() {
208
290
  this.app.use(express_1.default.json());
209
291
  this.app.use((0, cookie_parser_1.default)());
@@ -320,6 +402,7 @@ class ApiServer {
320
402
  }
321
403
  handle_request(handler, auth) {
322
404
  return async (req, res, next) => {
405
+ void next;
323
406
  try {
324
407
  const apiReq = (this.currReq = {
325
408
  server: this,
@@ -359,6 +442,56 @@ class ApiServer {
359
442
  }
360
443
  };
361
444
  }
445
+ getClientIp(req) {
446
+ const chain = this.getClientIpChain(req);
447
+ for (const ip of chain) {
448
+ if (!isLoopbackAddress(ip)) {
449
+ return ip;
450
+ }
451
+ }
452
+ return chain[0] ?? null;
453
+ }
454
+ getClientIpChain(req) {
455
+ const seen = new Set();
456
+ const result = [];
457
+ const pushNormalized = (ip) => {
458
+ if (!ip || seen.has(ip)) {
459
+ return;
460
+ }
461
+ seen.add(ip);
462
+ result.push(ip);
463
+ };
464
+ for (const ip of extractForwardedFor(req.headers['x-forwarded-for'])) {
465
+ pushNormalized(ip);
466
+ }
467
+ for (const ip of extractForwardedHeader(req.headers['forwarded'])) {
468
+ pushNormalized(ip);
469
+ }
470
+ const realIp = req.headers['x-real-ip'];
471
+ if (Array.isArray(realIp)) {
472
+ realIp.forEach((value) => pushNormalized(normalizeIpAddress(value)));
473
+ }
474
+ else if (typeof realIp === 'string') {
475
+ pushNormalized(normalizeIpAddress(realIp));
476
+ }
477
+ if (Array.isArray(req.ips)) {
478
+ for (const ip of req.ips) {
479
+ pushNormalized(normalizeIpAddress(ip));
480
+ }
481
+ }
482
+ if (typeof req.ip === 'string') {
483
+ pushNormalized(normalizeIpAddress(req.ip));
484
+ }
485
+ const socketAddress = req.socket?.remoteAddress;
486
+ if (typeof socketAddress === 'string') {
487
+ pushNormalized(normalizeIpAddress(socketAddress));
488
+ }
489
+ const connectionAddress = req.connection?.remoteAddress;
490
+ if (typeof connectionAddress === 'string') {
491
+ pushNormalized(normalizeIpAddress(connectionAddress));
492
+ }
493
+ return result;
494
+ }
362
495
  api(module) {
363
496
  const router = express_1.default.Router();
364
497
  module.server = this;
@@ -146,6 +146,8 @@ export declare class ApiServer {
146
146
  private verifyJWT;
147
147
  private authenticate;
148
148
  private handle_request;
149
+ getClientIp(req: RequestWithStuff): string | null;
150
+ getClientIpChain(req: RequestWithStuff): string[];
149
151
  api<T extends ApiModule<any>>(module: T): this;
150
152
  dumpRequest(apiReq: ApiRequest): void;
151
153
  }
@@ -146,6 +146,8 @@ export declare class ApiServer {
146
146
  private verifyJWT;
147
147
  private authenticate;
148
148
  private handle_request;
149
+ getClientIp(req: RequestWithStuff): string | null;
150
+ getClientIpChain(req: RequestWithStuff): string[];
149
151
  api<T extends ApiModule<any>>(module: T): this;
150
152
  dumpRequest(apiReq: ApiRequest): void;
151
153
  }
@@ -55,6 +55,76 @@ function hydrateGetBody(req) {
55
55
  }
56
56
  req.body = { ...query, ...body };
57
57
  }
58
+ function normalizeIpAddress(candidate) {
59
+ let value = candidate.trim();
60
+ if (!value) {
61
+ return null;
62
+ }
63
+ value = value.replace(/^"+|"+$/g, '').replace(/^'+|'+$/g, '');
64
+ if (value.startsWith('::ffff:')) {
65
+ value = value.slice(7);
66
+ }
67
+ if (value.startsWith('[') && value.endsWith(']')) {
68
+ value = value.slice(1, -1);
69
+ }
70
+ const firstColon = value.indexOf(':');
71
+ const lastColon = value.lastIndexOf(':');
72
+ if (firstColon !== -1 && firstColon === lastColon) {
73
+ const maybePort = value.slice(lastColon + 1);
74
+ if (/^\d+$/.test(maybePort)) {
75
+ value = value.slice(0, lastColon);
76
+ }
77
+ }
78
+ value = value.trim();
79
+ return value || null;
80
+ }
81
+ function extractForwardedFor(header) {
82
+ if (!header) {
83
+ return [];
84
+ }
85
+ const values = Array.isArray(header) ? header : [header];
86
+ const ips = [];
87
+ for (const entry of values) {
88
+ for (const part of entry.split(',')) {
89
+ const normalized = normalizeIpAddress(part);
90
+ if (normalized) {
91
+ ips.push(normalized);
92
+ }
93
+ }
94
+ }
95
+ return ips;
96
+ }
97
+ function extractForwardedHeader(header) {
98
+ if (!header) {
99
+ return [];
100
+ }
101
+ const values = Array.isArray(header) ? header : [header];
102
+ const ips = [];
103
+ for (const entry of values) {
104
+ for (const part of entry.split(',')) {
105
+ const match = part.match(/for=([^;]+)/i);
106
+ if (match) {
107
+ const normalized = normalizeIpAddress(match[1]);
108
+ if (normalized) {
109
+ ips.push(normalized);
110
+ }
111
+ }
112
+ }
113
+ }
114
+ return ips;
115
+ }
116
+ function isLoopbackAddress(ip) {
117
+ if (ip === '::1' || ip === '0:0:0:0:0:0:0:1') {
118
+ return true;
119
+ }
120
+ if (ip === '0.0.0.0' || ip === '127.0.0.1') {
121
+ return true;
122
+ }
123
+ if (ip.startsWith('127.')) {
124
+ return true;
125
+ }
126
+ return false;
127
+ }
58
128
  export class ApiError extends Error {
59
129
  constructor({ code, message, data, errors }) {
60
130
  const msg = guess_exception_text(message, '[Unknown error (null/undefined)]');
@@ -166,27 +236,36 @@ export class ApiServer {
166
236
  }
167
237
  }
168
238
  async getApiKey(token) {
239
+ void token;
169
240
  return null;
170
241
  }
171
242
  async getUser(uid) {
243
+ void uid;
172
244
  throw new Error('getUser() not implemented');
173
245
  }
174
246
  async authenticateUser(params) {
247
+ void params;
175
248
  throw new Error('authenticateUser() not implemented');
176
249
  }
177
250
  async storeToken(params) {
251
+ void params;
178
252
  throw new Error('storeToken() not implemented');
179
253
  }
180
254
  async getToken(params) {
255
+ void params;
181
256
  throw new Error('getToken() not implemented');
182
257
  }
183
258
  async updateToken(params) {
259
+ void params;
184
260
  throw new Error('updateToken() not implemented');
185
261
  }
186
262
  async deleteToken(params) {
263
+ void params;
187
264
  throw new Error('deleteToken() not implemented');
188
265
  }
189
266
  async verifyPassword(password, hash) {
267
+ void password;
268
+ void hash;
190
269
  throw new Error('verifyPassword() not implemented');
191
270
  }
192
271
  filterUser(fullUser) {
@@ -195,7 +274,10 @@ export class ApiServer {
195
274
  guessExceptionText(error, defMsg = 'Unkown Error') {
196
275
  return guess_exception_text(error, defMsg);
197
276
  }
198
- async authorize(apiReq, requiredClass) { }
277
+ async authorize(apiReq, requiredClass) {
278
+ void apiReq;
279
+ void requiredClass;
280
+ }
199
281
  middlewares() {
200
282
  this.app.use(express.json());
201
283
  this.app.use(cookieParser());
@@ -312,6 +394,7 @@ export class ApiServer {
312
394
  }
313
395
  handle_request(handler, auth) {
314
396
  return async (req, res, next) => {
397
+ void next;
315
398
  try {
316
399
  const apiReq = (this.currReq = {
317
400
  server: this,
@@ -351,6 +434,56 @@ export class ApiServer {
351
434
  }
352
435
  };
353
436
  }
437
+ getClientIp(req) {
438
+ const chain = this.getClientIpChain(req);
439
+ for (const ip of chain) {
440
+ if (!isLoopbackAddress(ip)) {
441
+ return ip;
442
+ }
443
+ }
444
+ return chain[0] ?? null;
445
+ }
446
+ getClientIpChain(req) {
447
+ const seen = new Set();
448
+ const result = [];
449
+ const pushNormalized = (ip) => {
450
+ if (!ip || seen.has(ip)) {
451
+ return;
452
+ }
453
+ seen.add(ip);
454
+ result.push(ip);
455
+ };
456
+ for (const ip of extractForwardedFor(req.headers['x-forwarded-for'])) {
457
+ pushNormalized(ip);
458
+ }
459
+ for (const ip of extractForwardedHeader(req.headers['forwarded'])) {
460
+ pushNormalized(ip);
461
+ }
462
+ const realIp = req.headers['x-real-ip'];
463
+ if (Array.isArray(realIp)) {
464
+ realIp.forEach((value) => pushNormalized(normalizeIpAddress(value)));
465
+ }
466
+ else if (typeof realIp === 'string') {
467
+ pushNormalized(normalizeIpAddress(realIp));
468
+ }
469
+ if (Array.isArray(req.ips)) {
470
+ for (const ip of req.ips) {
471
+ pushNormalized(normalizeIpAddress(ip));
472
+ }
473
+ }
474
+ if (typeof req.ip === 'string') {
475
+ pushNormalized(normalizeIpAddress(req.ip));
476
+ }
477
+ const socketAddress = req.socket?.remoteAddress;
478
+ if (typeof socketAddress === 'string') {
479
+ pushNormalized(normalizeIpAddress(socketAddress));
480
+ }
481
+ const connectionAddress = req.connection?.remoteAddress;
482
+ if (typeof connectionAddress === 'string') {
483
+ pushNormalized(normalizeIpAddress(connectionAddress));
484
+ }
485
+ return result;
486
+ }
354
487
  api(module) {
355
488
  const router = express.Router();
356
489
  module.server = this;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@technomoron/api-server-base",
3
- "version": "1.0.42",
3
+ "version": "1.0.43",
4
4
  "description": "Api Server Skeleton / Base Class",
5
5
  "type": "module",
6
6
  "main": "./dist/cjs/index.cjs",
@@ -35,34 +35,34 @@
35
35
  "lint": "eslint --ext .js,.ts,.vue ./",
36
36
  "lintfix": "eslint --fix --ext .js,.ts,.vue ./",
37
37
  "format": "eslint --fix --ext .js,.ts,.vue ./ && prettier --write \"**/*.{js,jsx,ts,tsx,vue,json,css,scss,md}\"",
38
- "cleanbuild": "rm -rf ./dist/ && eslint --fix --ext .js,.ts,.vue ./ && prettier --write \"**/*.{js,jsx,ts,tsx,vue,json,css,scss,md}\" && tsc --project tsconfig/tsconfig.cjs.json && tsc --project tsconfig/tsconfig.esm.json",
38
+ "cleanbuild": "rm -rf ./dist/ && eslint --fix --ext .js,.ts,.vue ./ && prettier --write \"**/*.{js,jsx,ts,tsx,vue,json,css,scss,md}\" && npm run build",
39
39
  "pretty": "prettier --write \"**/*.{js,jsx,ts,tsx,vue,json,css,scss,md}\""
40
40
  },
41
41
  "dependencies": {
42
42
  "@types/cookie-parser": "^1.4.9",
43
43
  "@types/cors": "^2.8.19",
44
- "@types/express": "^4.17.21",
45
- "@types/jsonwebtoken": "^9.0.9",
44
+ "@types/express": "^4.17.23",
45
+ "@types/jsonwebtoken": "^9.0.10",
46
46
  "@types/multer": "^1.4.13",
47
47
  "cookie-parser": "^1.4.7",
48
48
  "cors": "^2.8.5",
49
49
  "express": "^4.21.2",
50
50
  "jsonwebtoken": "^9.0.2",
51
- "multer": "^2.0.1"
51
+ "multer": "^2.0.2"
52
52
  },
53
53
  "devDependencies": {
54
- "@types/express-serve-static-core": "^5.0.6",
54
+ "@types/express-serve-static-core": "^5.1.0",
55
55
  "@types/supertest": "^6.0.3",
56
- "@typescript-eslint/eslint-plugin": "^8.33.0",
57
- "@typescript-eslint/parser": "^8.33.0",
56
+ "@typescript-eslint/eslint-plugin": "^8.46.1",
57
+ "@typescript-eslint/parser": "^8.46.1",
58
58
  "@vue/eslint-config-prettier": "10.2.0",
59
59
  "@vue/eslint-config-typescript": "14.5.0",
60
- "eslint": "^9.28.0",
61
- "eslint-plugin-import": "^2.31.0",
62
- "eslint-plugin-vue": "^10.1.0",
63
- "prettier": "^3.5.3",
60
+ "eslint": "^9.37.0",
61
+ "eslint-plugin-import": "^2.32.0",
62
+ "eslint-plugin-vue": "^10.5.1",
63
+ "prettier": "^3.6.2",
64
64
  "supertest": "^7.1.4",
65
- "typescript": "^5.8.3",
65
+ "typescript": "^5.9.3",
66
66
  "vitest": "^3.2.4"
67
67
  },
68
68
  "files": [