@technomoron/api-server-base 2.0.0-beta.22 → 2.0.0-beta.24

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.
Files changed (114) hide show
  1. package/dist/cjs/api-module.cjs +8 -0
  2. package/dist/cjs/api-module.d.ts +12 -0
  3. package/dist/cjs/api-server-base.cjs +573 -615
  4. package/dist/cjs/api-server-base.d.ts +97 -87
  5. package/dist/cjs/auth-api/{auth-module.js → auth-module.cjs} +96 -76
  6. package/dist/cjs/auth-api/auth-module.d.ts +1 -1
  7. package/dist/cjs/auth-api/{compat-auth-storage.js → compat-auth-storage.cjs} +4 -4
  8. package/dist/cjs/auth-api/{mem-auth-store.js → mem-auth-store.cjs} +7 -7
  9. package/dist/cjs/auth-api/{module.js → module.cjs} +1 -1
  10. package/dist/cjs/auth-api/schemas.cjs +171 -0
  11. package/dist/cjs/auth-api/schemas.d.ts +21 -0
  12. package/dist/cjs/auth-api/{sql-auth-store.js → sql-auth-store.cjs} +8 -8
  13. package/dist/cjs/auth-api/{user-id.js → user-id.cjs} +12 -3
  14. package/dist/cjs/auth-cookie-options.d.ts +5 -3
  15. package/dist/cjs/base/client-info.cjs +285 -0
  16. package/dist/cjs/base/client-info.d.ts +27 -0
  17. package/dist/cjs/base/error-utils.cjs +50 -0
  18. package/dist/cjs/base/error-utils.d.ts +16 -0
  19. package/dist/cjs/base/request-utils.cjs +27 -0
  20. package/dist/cjs/base/request-utils.d.ts +8 -0
  21. package/dist/cjs/index.cjs +24 -15
  22. package/dist/cjs/index.d.ts +7 -0
  23. package/dist/cjs/limiter/auth-rate-limiter.cjs +35 -0
  24. package/dist/cjs/limiter/auth-rate-limiter.d.ts +12 -0
  25. package/dist/cjs/limiter/fixed-window.cjs +41 -0
  26. package/dist/cjs/limiter/fixed-window.d.ts +11 -0
  27. package/dist/cjs/oauth/{base.js → base.cjs} +1 -0
  28. package/dist/cjs/oauth/base.d.ts +8 -1
  29. package/dist/cjs/oauth/{memory.js → memory.cjs} +7 -4
  30. package/dist/cjs/oauth/memory.d.ts +1 -1
  31. package/dist/cjs/oauth/{models.js → models.cjs} +2 -2
  32. package/dist/cjs/oauth/{sequelize.js → sequelize.cjs} +11 -7
  33. package/dist/cjs/oauth/sequelize.d.ts +1 -1
  34. package/dist/cjs/passkey/{base.js → base.cjs} +1 -0
  35. package/dist/cjs/passkey/base.d.ts +11 -0
  36. package/dist/cjs/passkey/{memory.js → memory.cjs} +2 -2
  37. package/dist/cjs/passkey/{models.js → models.cjs} +1 -1
  38. package/dist/cjs/passkey/{sequelize.js → sequelize.cjs} +3 -3
  39. package/dist/cjs/passkey/{service.js → service.cjs} +17 -3
  40. package/dist/cjs/passkey/service.d.ts +1 -1
  41. package/dist/cjs/{sequelize-utils.js → sequelize-utils.cjs} +4 -5
  42. package/dist/cjs/token/{base.js → base.cjs} +4 -0
  43. package/dist/cjs/token/base.d.ts +7 -0
  44. package/dist/cjs/token/{memory.js → memory.cjs} +15 -20
  45. package/dist/cjs/token/{sequelize.js → sequelize.cjs} +25 -11
  46. package/dist/cjs/upload/memory.cjs +92 -0
  47. package/dist/cjs/upload/memory.d.ts +17 -0
  48. package/dist/cjs/upload/tus-module.cjs +270 -0
  49. package/dist/cjs/upload/tus-module.d.ts +38 -0
  50. package/dist/cjs/upload/types.d.ts +28 -0
  51. package/dist/cjs/user/{base.js → base.cjs} +1 -0
  52. package/dist/cjs/user/base.d.ts +9 -0
  53. package/dist/cjs/user/{memory.js → memory.cjs} +29 -7
  54. package/dist/cjs/user/{sequelize.js → sequelize.cjs} +33 -8
  55. package/dist/cjs/user/types.cjs +2 -0
  56. package/dist/esm/api-module.d.ts +12 -0
  57. package/dist/esm/api-module.js +8 -0
  58. package/dist/esm/api-server-base.d.ts +97 -87
  59. package/dist/esm/api-server-base.js +562 -604
  60. package/dist/esm/auth-api/auth-module.d.ts +1 -1
  61. package/dist/esm/auth-api/auth-module.js +92 -72
  62. package/dist/esm/auth-api/compat-auth-storage.js +3 -3
  63. package/dist/esm/auth-api/schemas.d.ts +21 -0
  64. package/dist/esm/auth-api/schemas.js +168 -0
  65. package/dist/esm/auth-api/user-id.js +12 -3
  66. package/dist/esm/auth-cookie-options.d.ts +5 -3
  67. package/dist/esm/base/client-info.d.ts +27 -0
  68. package/dist/esm/base/client-info.js +282 -0
  69. package/dist/esm/base/error-utils.d.ts +16 -0
  70. package/dist/esm/base/error-utils.js +44 -0
  71. package/dist/esm/base/request-utils.d.ts +8 -0
  72. package/dist/esm/base/request-utils.js +23 -0
  73. package/dist/esm/index.d.ts +7 -0
  74. package/dist/esm/index.js +4 -0
  75. package/dist/esm/limiter/auth-rate-limiter.d.ts +12 -0
  76. package/dist/esm/limiter/auth-rate-limiter.js +32 -0
  77. package/dist/esm/limiter/fixed-window.d.ts +11 -0
  78. package/dist/esm/limiter/fixed-window.js +37 -0
  79. package/dist/esm/oauth/base.d.ts +8 -1
  80. package/dist/esm/oauth/base.js +1 -0
  81. package/dist/esm/oauth/memory.d.ts +1 -1
  82. package/dist/esm/oauth/memory.js +5 -2
  83. package/dist/esm/oauth/sequelize.d.ts +1 -1
  84. package/dist/esm/oauth/sequelize.js +6 -2
  85. package/dist/esm/passkey/base.d.ts +11 -0
  86. package/dist/esm/passkey/base.js +1 -0
  87. package/dist/esm/passkey/service.d.ts +1 -1
  88. package/dist/esm/passkey/service.js +17 -3
  89. package/dist/esm/sequelize-utils.js +4 -5
  90. package/dist/esm/token/base.d.ts +7 -0
  91. package/dist/esm/token/base.js +4 -0
  92. package/dist/esm/token/memory.js +14 -19
  93. package/dist/esm/token/sequelize.js +22 -8
  94. package/dist/esm/upload/memory.d.ts +17 -0
  95. package/dist/esm/upload/memory.js +86 -0
  96. package/dist/esm/upload/tus-module.d.ts +38 -0
  97. package/dist/esm/upload/tus-module.js +266 -0
  98. package/dist/esm/upload/types.d.ts +28 -0
  99. package/dist/esm/upload/types.js +1 -0
  100. package/dist/esm/user/base.d.ts +9 -0
  101. package/dist/esm/user/base.js +1 -0
  102. package/dist/esm/user/memory.js +27 -5
  103. package/dist/esm/user/sequelize.js +30 -5
  104. package/docs/swagger/openapi.json +1 -1
  105. package/package.json +18 -17
  106. package/README.txt +0 -216
  107. /package/dist/cjs/auth-api/{storage.js → storage.cjs} +0 -0
  108. /package/dist/cjs/auth-api/{types.js → types.cjs} +0 -0
  109. /package/dist/cjs/{auth-cookie-options.js → auth-cookie-options.cjs} +0 -0
  110. /package/dist/cjs/oauth/{types.js → types.cjs} +0 -0
  111. /package/dist/cjs/passkey/{config.js → config.cjs} +0 -0
  112. /package/dist/cjs/passkey/{types.js → types.cjs} +0 -0
  113. /package/dist/cjs/token/{types.js → types.cjs} +0 -0
  114. /package/dist/cjs/{user/types.js → upload/types.cjs} +0 -0
@@ -10,19 +10,53 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.ApiServer = exports.ApiError = exports.ApiModule = void 0;
13
- const node_crypto_1 = require("node:crypto");
14
13
  const promises_1 = require("node:fs/promises");
15
14
  const node_module_1 = require("node:module");
16
15
  const node_path_1 = __importDefault(require("node:path"));
17
- const cookie_parser_1 = __importDefault(require("cookie-parser"));
18
- const cors_1 = __importDefault(require("cors"));
19
- const express_1 = __importDefault(require("express"));
20
- const multer_1 = __importDefault(require("multer"));
21
- const module_js_1 = require("./auth-api/module.js");
22
- const storage_js_1 = require("./auth-api/storage.js");
23
- const user_id_js_1 = require("./auth-api/user-id.js");
24
- const auth_cookie_options_js_1 = require("./auth-cookie-options.js");
25
- const base_js_1 = require("./token/base.js");
16
+ const cookie_1 = __importDefault(require("@fastify/cookie"));
17
+ const cors_1 = __importDefault(require("@fastify/cors"));
18
+ const multipart_1 = __importDefault(require("@fastify/multipart"));
19
+ const static_1 = __importDefault(require("@fastify/static"));
20
+ const fastify_1 = __importDefault(require("fastify"));
21
+ const module_js_1 = require("./auth-api/module.cjs");
22
+ const storage_js_1 = require("./auth-api/storage.cjs");
23
+ const user_id_js_1 = require("./auth-api/user-id.cjs");
24
+ const auth_cookie_options_js_1 = require("./auth-cookie-options.cjs");
25
+ const client_info_js_1 = require("./base/client-info.cjs");
26
+ const error_utils_js_1 = require("./base/error-utils.cjs");
27
+ const request_utils_js_1 = require("./base/request-utils.cjs");
28
+ const base_js_1 = require("./token/base.cjs");
29
+ class FastifyResponseAdapter {
30
+ constructor(reply) {
31
+ this.reply = reply;
32
+ this.locals = {};
33
+ this.statusCode = 200;
34
+ }
35
+ get headersSent() {
36
+ return this.reply.sent;
37
+ }
38
+ status(code) {
39
+ this.statusCode = code;
40
+ this.reply.code(code);
41
+ return this;
42
+ }
43
+ json(payload) {
44
+ if (!this.reply.sent) {
45
+ this.reply.code(this.statusCode).send(payload);
46
+ }
47
+ }
48
+ send(payload) {
49
+ if (!this.reply.sent) {
50
+ this.reply.code(this.statusCode).send(payload);
51
+ }
52
+ }
53
+ cookie(name, value, options = {}) {
54
+ this.reply.setCookie(name, value, options);
55
+ }
56
+ clearCookie(name, options = {}) {
57
+ this.reply.clearCookie(name, options);
58
+ }
59
+ }
26
60
  class JwtHelperStore extends base_js_1.TokenStore {
27
61
  async save() {
28
62
  throw new Error('Token store is not configured');
@@ -45,288 +79,9 @@ class JwtHelperStore extends base_js_1.TokenStore {
45
79
  }
46
80
  var api_module_js_1 = require("./api-module.cjs");
47
81
  Object.defineProperty(exports, "ApiModule", { enumerable: true, get: function () { return api_module_js_1.ApiModule; } });
48
- function guess_exception_text(error, defMsg = 'Unknown Error') {
49
- const msg = [];
50
- if (typeof error === 'string' && error.trim() !== '') {
51
- msg.push(error);
52
- }
53
- else if (error && typeof error === 'object') {
54
- const errorDetails = error;
55
- if (typeof errorDetails.message === 'string' && errorDetails.message.trim() !== '') {
56
- msg.push(errorDetails.message);
57
- }
58
- if (errorDetails.parent &&
59
- typeof errorDetails.parent.message === 'string' &&
60
- errorDetails.parent.message.trim() !== '') {
61
- msg.push(errorDetails.parent.message);
62
- }
63
- }
64
- return msg.length > 0 ? msg.join(' / ') : defMsg;
65
- }
66
- function isPlainObject(value) {
67
- return !!value && typeof value === 'object' && !Array.isArray(value);
68
- }
69
- function hydrateGetBody(req) {
70
- if ((req.method ?? '').toUpperCase() !== 'GET') {
71
- return;
72
- }
73
- const query = isPlainObject(req.query) ? req.query : null;
74
- if (!query || Object.keys(query).length === 0) {
75
- return;
76
- }
77
- const body = isPlainObject(req.body) ? req.body : null;
78
- if (!body || Object.keys(body).length === 0) {
79
- req.body = { ...query };
80
- return;
81
- }
82
- // Keep explicit body fields authoritative when both query and body provide the same key.
83
- req.body = { ...query, ...body };
84
- }
85
- function normalizeIpAddress(candidate) {
86
- let value = candidate.trim();
87
- if (!value) {
88
- return null;
89
- }
90
- value = value.replace(/^"+|"+$/g, '').replace(/^'+|'+$/g, '');
91
- if (value.startsWith('::ffff:')) {
92
- value = value.slice(7);
93
- }
94
- if (value.startsWith('[') && value.endsWith(']')) {
95
- value = value.slice(1, -1);
96
- }
97
- const firstColon = value.indexOf(':');
98
- const lastColon = value.lastIndexOf(':');
99
- if (firstColon !== -1 && firstColon === lastColon) {
100
- const maybePort = value.slice(lastColon + 1);
101
- if (/^\d+$/.test(maybePort)) {
102
- value = value.slice(0, lastColon);
103
- }
104
- }
105
- value = value.trim();
106
- return value || null;
107
- }
108
- function extractForwardedFor(header) {
109
- if (!header) {
110
- return [];
111
- }
112
- const values = Array.isArray(header) ? header : [header];
113
- const ips = [];
114
- for (const entry of values) {
115
- for (const part of entry.split(',')) {
116
- const normalized = normalizeIpAddress(part);
117
- if (normalized) {
118
- ips.push(normalized);
119
- }
120
- }
121
- }
122
- return ips;
123
- }
124
- function extractForwardedHeader(header) {
125
- if (!header) {
126
- return [];
127
- }
128
- const values = Array.isArray(header) ? header : [header];
129
- const ips = [];
130
- for (const entry of values) {
131
- for (const part of entry.split(',')) {
132
- const match = part.match(/for=([^;]+)/i);
133
- if (match) {
134
- const normalized = normalizeIpAddress(match[1]);
135
- if (normalized) {
136
- ips.push(normalized);
137
- }
138
- }
139
- }
140
- }
141
- return ips;
142
- }
143
- function detectBrowser(userAgent) {
144
- const browserMatchers = [
145
- { label: 'Edge', pattern: /(Edg|Edge|EdgiOS|EdgA)\/([\d.]+)/i, versionGroup: 2 },
146
- { label: 'Chrome', pattern: /(Chrome|CriOS)\/([\d.]+)/i, versionGroup: 2 },
147
- { label: 'Firefox', pattern: /(Firefox|FxiOS)\/([\d.]+)/i, versionGroup: 2 },
148
- { label: 'Safari', pattern: /Version\/([\d.]+).*Safari/i, versionGroup: 1 },
149
- { label: 'Opera', pattern: /(OPR|Opera)\/([\d.]+)/i, versionGroup: 2 },
150
- { label: 'Brave', pattern: /Brave\/([\d.]+)/i, versionGroup: 1 },
151
- { label: 'Vivaldi', pattern: /Vivaldi\/([\d.]+)/i, versionGroup: 1 },
152
- { label: 'Electron', pattern: /Electron\/([\d.]+)/i, versionGroup: 1 },
153
- { label: 'Node', pattern: /Node\.js\/([\d.]+)/i, versionGroup: 1 },
154
- { label: 'IE', pattern: /MSIE ([\d.]+)/i, versionGroup: 1 },
155
- { label: 'IE', pattern: /Trident\/.*rv:([\d.]+)/i, versionGroup: 1 }
156
- ];
157
- for (const matcher of browserMatchers) {
158
- const m = userAgent.match(matcher.pattern);
159
- if (m) {
160
- const version = matcher.versionGroup ? m[matcher.versionGroup] : '';
161
- return version ? `${matcher.label} ${version}` : matcher.label;
162
- }
163
- }
164
- return '';
165
- }
166
- function detectOs(userAgent) {
167
- const osMatchers = [
168
- {
169
- label: 'Windows',
170
- pattern: /Windows NT ([\d.]+)/i,
171
- transform: (match) => `Windows ${match[1]}`
172
- },
173
- {
174
- label: 'iOS',
175
- pattern: /iPhone OS ([\d_]+)/i,
176
- transform: (match) => `iOS ${match[1].replace(/_/g, '.')}`
177
- },
178
- {
179
- label: 'iPadOS',
180
- pattern: /iPad; CPU OS ([\d_]+)/i,
181
- transform: (match) => `iPadOS ${match[1].replace(/_/g, '.')}`
182
- },
183
- {
184
- label: 'macOS',
185
- pattern: /Mac OS X ([\d_]+)/i,
186
- transform: (match) => `macOS ${match[1].replace(/_/g, '.')}`
187
- },
188
- {
189
- label: 'Android',
190
- pattern: /Android ([\d.]+)/i,
191
- transform: (match) => `Android ${match[1]}`
192
- },
193
- {
194
- label: 'ChromeOS',
195
- pattern: /CrOS [^ ]+ ([\d.]+)/i,
196
- transform: (match) => `ChromeOS ${match[1]}`
197
- },
198
- { label: 'Linux', pattern: /Linux/i },
199
- { label: 'Unix', pattern: /X11/i }
200
- ];
201
- for (const matcher of osMatchers) {
202
- const m = userAgent.match(matcher.pattern);
203
- if (m) {
204
- return matcher.transform ? matcher.transform(m) : matcher.label;
205
- }
206
- }
207
- return '';
208
- }
209
- function detectDevice(userAgent, osLabel) {
210
- if (/iPhone/i.test(userAgent)) {
211
- return 'iPhone';
212
- }
213
- if (/iPad/i.test(userAgent)) {
214
- return 'iPad';
215
- }
216
- if (/iPod/i.test(userAgent)) {
217
- return 'iPod';
218
- }
219
- if (/Android/i.test(userAgent)) {
220
- const match = userAgent.match(/;\s*([^;]+)\s+Build/i);
221
- if (match) {
222
- return match[1];
223
- }
224
- return 'Android Device';
225
- }
226
- if (/Macintosh/i.test(userAgent)) {
227
- return 'Mac';
228
- }
229
- if (/Windows/i.test(userAgent)) {
230
- return 'PC';
231
- }
232
- if (/CrOS/i.test(userAgent)) {
233
- return 'Chromebook';
234
- }
235
- return osLabel;
236
- }
237
- function parseClientAgent(userAgentHeader) {
238
- const raw = Array.isArray(userAgentHeader) ? userAgentHeader[0] : userAgentHeader;
239
- const ua = typeof raw === 'string' ? raw.trim() : '';
240
- if (!ua) {
241
- return { ua: '', browser: '', os: '', device: '' };
242
- }
243
- const os = detectOs(ua);
244
- const browser = detectBrowser(ua);
245
- const device = detectDevice(ua, os);
246
- return { ua, browser, os, device };
247
- }
248
- function isLoopbackAddress(ip) {
249
- if (ip === '::1' || ip === '0:0:0:0:0:0:0:1') {
250
- return true;
251
- }
252
- if (ip === '0.0.0.0' || ip === '127.0.0.1') {
253
- return true;
254
- }
255
- if (ip.startsWith('127.')) {
256
- return true;
257
- }
258
- return false;
259
- }
260
- function collectClientIpChain(req) {
261
- const seen = new Set();
262
- const result = [];
263
- const pushNormalized = (ip) => {
264
- if (!ip || seen.has(ip)) {
265
- return;
266
- }
267
- seen.add(ip);
268
- result.push(ip);
269
- };
270
- for (const ip of extractForwardedFor(req.headers['x-forwarded-for'])) {
271
- pushNormalized(ip);
272
- }
273
- for (const ip of extractForwardedHeader(req.headers['forwarded'])) {
274
- pushNormalized(ip);
275
- }
276
- const realIp = req.headers['x-real-ip'];
277
- if (Array.isArray(realIp)) {
278
- for (const value of realIp) {
279
- pushNormalized(normalizeIpAddress(value));
280
- }
281
- }
282
- else if (typeof realIp === 'string') {
283
- pushNormalized(normalizeIpAddress(realIp));
284
- }
285
- if (Array.isArray(req.ips)) {
286
- for (const ip of req.ips) {
287
- pushNormalized(normalizeIpAddress(ip));
288
- }
289
- }
290
- if (typeof req.ip === 'string') {
291
- pushNormalized(normalizeIpAddress(req.ip));
292
- }
293
- const socketAddress = req.socket?.remoteAddress;
294
- if (typeof socketAddress === 'string') {
295
- pushNormalized(normalizeIpAddress(socketAddress));
296
- }
297
- const connectionAddress = req.connection?.remoteAddress;
298
- if (typeof connectionAddress === 'string') {
299
- pushNormalized(normalizeIpAddress(connectionAddress));
300
- }
301
- return result;
302
- }
303
- function selectClientIp(chain) {
304
- for (const ip of chain) {
305
- if (!isLoopbackAddress(ip)) {
306
- return ip;
307
- }
308
- }
309
- return chain[0] ?? null;
310
- }
311
- function buildClientInfo(req) {
312
- const agent = parseClientAgent(req.headers['user-agent']);
313
- const ipchain = collectClientIpChain(req);
314
- const ip = selectClientIp(ipchain);
315
- return {
316
- ...agent,
317
- ip,
318
- ipchain
319
- };
320
- }
321
- function ensureClientInfo(apiReq) {
322
- if (!apiReq.clientInfo) {
323
- apiReq.clientInfo = buildClientInfo(apiReq.req);
324
- }
325
- return apiReq.clientInfo;
326
- }
327
82
  class ApiError extends Error {
328
83
  constructor({ code, message, data, errors }) {
329
- const msg = guess_exception_text(message, '[Unknown error (null/undefined)]');
84
+ const msg = (0, error_utils_js_1.guessExceptionText)(message, '[Unknown error (null/undefined)]');
330
85
  super(msg);
331
86
  this.message = msg;
332
87
  this.code = typeof code === 'number' ? code : 500;
@@ -335,24 +90,6 @@ class ApiError extends Error {
335
90
  }
336
91
  }
337
92
  exports.ApiError = ApiError;
338
- function isApiErrorLike(candidate) {
339
- if (!candidate || typeof candidate !== 'object') {
340
- return false;
341
- }
342
- const maybeError = candidate;
343
- return typeof maybeError.code === 'number' && typeof maybeError.message === 'string';
344
- }
345
- function asHttpStatus(error) {
346
- if (!error || typeof error !== 'object') {
347
- return null;
348
- }
349
- const maybe = error;
350
- const status = typeof maybe.status === 'number' ? maybe.status : maybe.statusCode;
351
- if (typeof status === 'number' && status >= 400 && status <= 599) {
352
- return status;
353
- }
354
- return null;
355
- }
356
93
  function fillConfig(config) {
357
94
  return {
358
95
  apiPort: config.apiPort ?? 3101,
@@ -389,36 +126,27 @@ function fillConfig(config) {
389
126
  apiVersion: config.apiVersion ?? '',
390
127
  minClientVersion: config.minClientVersion ?? '',
391
128
  tokenStore: config.tokenStore,
392
- authStores: config.authStores,
393
- onStartError: config.onStartError
129
+ onStartError: config.onStartError,
130
+ trustProxy: config.trustProxy ?? true
394
131
  };
395
132
  }
133
+ /** Core Fastify-based API server with module mounting and auth integration hooks. */
396
134
  class ApiServer {
397
- /**
398
- * @deprecated ApiServer does not track a global "current request". This value is always null.
399
- * Use the per-request ApiRequest passed to handlers, or `req.apiReq` / `res.locals.apiReq`
400
- * when mounting raw Express endpoints.
401
- */
402
135
  get currReq() {
403
136
  return null;
404
137
  }
405
138
  set currReq(_value) {
406
139
  if (this.config.devMode && !this.currReqDeprecationWarned) {
407
140
  this.currReqDeprecationWarned = true;
408
- console.warn('[api-server-base] ApiServer.currReq is deprecated and always null. Use req.apiReq or res.locals.apiReq.');
141
+ console.warn('[api-server-base] ApiServer.currReq is deprecated and always null. Use per-request ApiRequest in handlers.');
409
142
  }
410
143
  void _value;
411
144
  }
412
145
  constructor(config = {}) {
413
146
  this.finalized = false;
414
- this.serverAuthAdapter = null;
415
- this.apiNotFoundHandler = null;
416
147
  this.apiErrorHandlerInstalled = false;
417
148
  this.tokenStoreAdapter = null;
418
- this.userStoreAdapter = null;
419
- this.passkeyServiceAdapter = null;
420
- this.oauthStoreAdapter = null;
421
- this.canImpersonateAdapter = null;
149
+ this.compatGlobalErrorHandler = null;
422
150
  this.currReqDeprecationWarned = false;
423
151
  this.config = fillConfig(config);
424
152
  this.apiBasePath = this.normalizeApiBasePath(this.config.apiBasePath);
@@ -427,100 +155,153 @@ class ApiServer {
427
155
  this.moduleAdapter = module_js_1.nullAuthModule;
428
156
  this.jwtHelper = new JwtHelperStore();
429
157
  this.tokenStoreAdapter = this.config.tokenStore ?? null;
430
- if (this.config.authStores) {
431
- const { userStore, tokenStore, passkeyService, oauthStore, canImpersonate } = this.config.authStores;
432
- this.userStoreAdapter = userStore;
433
- this.tokenStoreAdapter = tokenStore;
434
- this.passkeyServiceAdapter = passkeyService ?? null;
435
- this.oauthStoreAdapter = oauthStore ?? null;
436
- this.canImpersonateAdapter = canImpersonate ?? null;
437
- this.storageAdapter = this.getServerAuthAdapter();
438
- }
439
- if ((this.config.authApi || this.config.authStores) &&
440
- (!this.config.accessSecret || !this.config.refreshSecret)) {
158
+ if (this.config.authApi && (!this.config.accessSecret || !this.config.refreshSecret)) {
441
159
  console.warn('[api-server-base] Auth is enabled but accessSecret and/or refreshSecret are empty. JWT signing will fail at runtime.');
442
160
  }
443
- this.app = (0, express_1.default)();
444
- // Mount API modules and any custom endpoints under `apiBasePath` on this router so we can keep
445
- // the API 404 handler ordered last without relying on Express internals.
446
- this.apiRouter = express_1.default.Router();
447
- if (config.uploadPath) {
448
- const upload = (0, multer_1.default)({ dest: config.uploadPath, limits: { fileSize: this.config.uploadMax } });
449
- this.app.use(upload.any());
450
- // Multer errors happen before ApiModule route wrappers; format the common "too large" failure.
451
- this.app.use((err, _req, res, next) => {
452
- const code = err && typeof err === 'object' ? err.code : undefined;
453
- if (code === 'LIMIT_FILE_SIZE') {
454
- res.status(413).json({
455
- success: false,
456
- code: 413,
457
- message: `Upload exceeds maximum size of ${this.config.uploadMax} bytes`,
458
- data: null,
459
- errors: {}
460
- });
461
- return;
161
+ this.fastify = (0, fastify_1.default)({ logger: false, ajv: { customOptions: { allErrors: true, allowUnionTypes: true } } });
162
+ this.setupRuntime();
163
+ this.readyPromise = Promise.resolve(this.fastify.ready()).then(() => undefined);
164
+ const appHandler = (req, res) => {
165
+ void this.readyPromise
166
+ .then(() => {
167
+ this.fastify.routing(req, res);
168
+ })
169
+ .catch((error) => {
170
+ const message = this.internalServerErrorMessage(error);
171
+ res.statusCode = 500;
172
+ res.setHeader('content-type', 'application/json');
173
+ res.end(JSON.stringify({ success: false, code: 500, message, data: null, errors: {} }));
174
+ });
175
+ };
176
+ appHandler.listen = (...args) => {
177
+ const server = this.fastify.server;
178
+ void this.readyPromise.then(() => {
179
+ if (!server.listening) {
180
+ server.listen(...args);
462
181
  }
463
- next(err);
464
182
  });
465
- }
466
- this.middlewares();
183
+ return server;
184
+ };
185
+ this.app = appHandler;
186
+ }
187
+ setupRuntime() {
188
+ this.fastify.register(cookie_1.default);
189
+ this.fastify.register(cors_1.default, {
190
+ origin: (origin, callback) => {
191
+ // No Origin header means a non-browser client (curl, server-to-server).
192
+ // CORS is a browser security feature; non-browser clients can bypass it
193
+ // by simply omitting the header, so blocking them here adds no security.
194
+ if (!origin) {
195
+ callback(null, true);
196
+ return;
197
+ }
198
+ // NOTE: An empty origins list means "allow all origins" (open/dev mode).
199
+ // Configure a non-empty origins list in production to restrict access.
200
+ if (this.config.origins.length > 0 && !this.config.origins.includes(origin)) {
201
+ callback(new Error('Not allowed by CORS'), false);
202
+ return;
203
+ }
204
+ callback(null, true);
205
+ },
206
+ credentials: true
207
+ });
208
+ this.fastify.register(multipart_1.default, {
209
+ limits: { fileSize: this.config.uploadMax }
210
+ });
211
+ this.fastify.addHook('preValidation', async (request) => {
212
+ if (request.method === 'GET' &&
213
+ request.body === undefined &&
214
+ typeof request.headers['content-length'] === 'string' &&
215
+ Number.parseInt(request.headers['content-length'], 10) > 0) {
216
+ const rawBody = await this.readRawBody(request.raw);
217
+ if (rawBody) {
218
+ const contentType = (request.headers['content-type'] ?? '').toString().toLowerCase();
219
+ request.body = this.parseRawBody(rawBody, contentType);
220
+ }
221
+ }
222
+ if (!request.isMultipart()) {
223
+ return;
224
+ }
225
+ const files = [];
226
+ const body = {};
227
+ for await (const part of request.parts()) {
228
+ if (part.type === 'file') {
229
+ const buffer = await part.toBuffer();
230
+ files.push({
231
+ fieldname: part.fieldname,
232
+ originalname: part.filename,
233
+ encoding: part.encoding,
234
+ mimetype: part.mimetype,
235
+ size: buffer.length,
236
+ buffer
237
+ });
238
+ }
239
+ else {
240
+ body[part.fieldname] = part.value;
241
+ }
242
+ }
243
+ request.__apiParsedBody = body;
244
+ request.__apiParsedFiles = files;
245
+ });
467
246
  this.installStaticDirs();
468
247
  this.installPingHandler();
469
248
  this.installSwaggerHandler();
470
- this.app.use(this.apiBasePath, this.apiRouter);
471
- // addSwaggerUi(this.app);
472
249
  this.installApiNotFoundHandler();
473
250
  this.installApiErrorHandler();
474
251
  }
475
- assertNotFinalized(action) {
476
- if (this.finalized) {
477
- throw new Error(`Cannot call ApiServer.${action}() after finalize()/start()`);
252
+ async readRawBody(req, maxBytes = 1048576) {
253
+ const chunks = [];
254
+ let total = 0;
255
+ for await (const chunk of req) {
256
+ const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));
257
+ total += buf.length;
258
+ if (total > maxBytes) {
259
+ throw new ApiError({ code: 413, message: `Request body exceeds maximum size of ${maxBytes} bytes` });
260
+ }
261
+ chunks.push(buf);
478
262
  }
263
+ return Buffer.concat(chunks).toString('utf8');
479
264
  }
480
- toApiRouterPath(candidate) {
481
- if (typeof candidate !== 'string') {
482
- return null;
265
+ parseRawBody(raw, contentType) {
266
+ if (!raw) {
267
+ return {};
483
268
  }
484
- const trimmed = candidate.trim();
485
- if (!trimmed) {
486
- return null;
487
- }
488
- const normalized = trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
489
- const base = this.apiBasePath;
490
- if (base === '/') {
491
- return normalized;
269
+ if (contentType.includes('application/json')) {
270
+ try {
271
+ return JSON.parse(raw);
272
+ }
273
+ catch {
274
+ return {};
275
+ }
492
276
  }
493
- if (normalized === base) {
494
- return '/';
277
+ if (contentType.includes('application/x-www-form-urlencoded')) {
278
+ const params = new URLSearchParams(raw);
279
+ return Object.fromEntries(params.entries());
495
280
  }
496
- if (normalized.startsWith(`${base}/`)) {
497
- return normalized.slice(base.length) || '/';
281
+ return raw;
282
+ }
283
+ assertNotFinalized(action) {
284
+ if (this.finalized) {
285
+ throw new Error(`Cannot call ApiServer.${action}() after finalize()/start()`);
498
286
  }
499
- return null;
500
287
  }
501
288
  finalize() {
502
- this.installApiNotFoundHandler();
503
- this.installApiErrorHandler();
504
289
  this.finalized = true;
505
290
  return this;
506
291
  }
507
292
  authStorage(storage) {
293
+ this.assertNotFinalized('authStorage');
508
294
  this.storageAdapter = storage;
509
295
  return this;
510
296
  }
511
- /**
512
- * @deprecated Use {@link ApiServer.authStorage} instead.
513
- */
514
297
  useAuthStorage(storage) {
515
298
  return this.authStorage(storage);
516
299
  }
517
300
  authModule(module) {
301
+ this.assertNotFinalized('authModule');
518
302
  this.moduleAdapter = module;
519
303
  return this;
520
304
  }
521
- /**
522
- * @deprecated Use {@link ApiServer.authModule} instead.
523
- */
524
305
  useAuthModule(module) {
525
306
  return this.authModule(module);
526
307
  }
@@ -531,88 +312,39 @@ class ApiServer {
531
312
  return this.moduleAdapter;
532
313
  }
533
314
  setTokenStore(store) {
315
+ this.assertNotFinalized('setTokenStore');
534
316
  this.tokenStoreAdapter = store;
535
- // If using direct stores, expose the server-backed auth adapter.
536
- if (this.userStoreAdapter) {
537
- this.storageAdapter = this.getServerAuthAdapter();
538
- }
539
317
  return this;
540
318
  }
541
319
  getTokenStore() {
542
320
  return this.tokenStoreAdapter;
543
321
  }
544
- ensureUserStore() {
545
- if (!this.userStoreAdapter) {
546
- throw new Error('User store is not configured');
547
- }
548
- return this.userStoreAdapter;
549
- }
550
- ensureTokenStore() {
551
- if (!this.tokenStoreAdapter) {
552
- throw new Error('Token store is not configured');
553
- }
554
- return this.tokenStoreAdapter;
555
- }
556
- ensurePasskeyService() {
557
- if (!this.passkeyServiceAdapter) {
322
+ async listUserCredentials(userId) {
323
+ if (typeof this.storageAdapter.listUserCredentials !== 'function') {
558
324
  throw new Error('Passkey service is not configured');
559
325
  }
560
- return this.passkeyServiceAdapter;
561
- }
562
- async listUserCredentials(userId) {
563
- return this.ensurePasskeyService().listUserCredentials(userId);
326
+ return this.storageAdapter.listUserCredentials(userId);
564
327
  }
565
328
  async deletePasskeyCredential(credentialId) {
566
- return this.ensurePasskeyService().deleteCredential(credentialId);
567
- }
568
- ensureOAuthStore() {
569
- if (!this.oauthStoreAdapter) {
570
- throw new Error('OAuth store is not configured');
329
+ if (typeof this.storageAdapter.deletePasskeyCredential !== 'function') {
330
+ throw new Error('Passkey service is not configured');
571
331
  }
572
- return this.oauthStoreAdapter;
573
- }
574
- getServerAuthAdapter() {
575
- if (this.serverAuthAdapter) {
576
- return this.serverAuthAdapter;
577
- }
578
- const server = this;
579
- this.serverAuthAdapter = {
580
- getUser: (identifier) => server.getUser(identifier),
581
- getUserPasswordHash: (user) => server.getUserPasswordHash(user),
582
- getUserId: (user) => server.getUserId(user),
583
- filterUser: (user) => server.filterUser(user),
584
- verifyPassword: (password, hash) => server.verifyPassword(password, hash),
585
- storeToken: (data) => server.storeToken(data),
586
- getToken: (query, opts) => server.getToken(query, opts),
587
- deleteToken: (query) => server.deleteToken(query),
588
- updateToken: (updates) => server.updateToken(updates),
589
- createPasskeyChallenge: (params) => server.createPasskeyChallenge(params),
590
- verifyPasskeyResponse: (params) => server.verifyPasskeyResponse(params),
591
- listUserCredentials: (userId) => server.listUserCredentials(userId),
592
- deletePasskeyCredential: (credentialId) => server.deletePasskeyCredential(credentialId),
593
- getClient: (clientId) => server.getClient(clientId),
594
- verifyClientSecret: (client, clientSecret) => server.verifyClientSecret(client, clientSecret),
595
- createAuthCode: (request) => server.createAuthCode(request),
596
- consumeAuthCode: (code, clientId) => server.consumeAuthCode(code, clientId),
597
- canImpersonate: (params) => server.canImpersonate(params)
598
- };
599
- return this.serverAuthAdapter;
332
+ return this.storageAdapter.deletePasskeyCredential(credentialId);
600
333
  }
601
- // AuthAdapter-compatible helpers (used by AuthModule)
602
334
  async getUser(identifier) {
603
- return this.userStoreAdapter ? this.userStoreAdapter.findUser(identifier) : null;
335
+ return this.storageAdapter.getUser(identifier);
604
336
  }
605
337
  getUserPasswordHash(user) {
606
- return this.ensureUserStore().getPasswordHash(user) ?? '';
338
+ return this.storageAdapter.getUserPasswordHash(user);
607
339
  }
608
340
  getUserId(user) {
609
- return this.ensureUserStore().getUserId(user);
341
+ return this.storageAdapter.getUserId(user);
610
342
  }
611
343
  filterUser(user) {
612
- return this.ensureUserStore().toPublic(user);
344
+ return this.storageAdapter.filterUser(user);
613
345
  }
614
346
  async verifyPassword(password, hash) {
615
- return this.ensureUserStore().verifyPassword(password, hash);
347
+ return this.storageAdapter.verifyPassword(password, hash);
616
348
  }
617
349
  async storeToken(data) {
618
350
  if (this.tokenStoreAdapter) {
@@ -655,47 +387,55 @@ class ApiServer {
655
387
  return 0;
656
388
  }
657
389
  async createPasskeyChallenge(params) {
658
- return this.ensurePasskeyService().createChallenge(params);
390
+ if (typeof this.storageAdapter.createPasskeyChallenge !== 'function') {
391
+ throw new Error('Passkey service is not configured');
392
+ }
393
+ return this.storageAdapter.createPasskeyChallenge(params);
659
394
  }
660
395
  async verifyPasskeyResponse(params) {
661
- return this.ensurePasskeyService().verifyResponse(params);
396
+ if (typeof this.storageAdapter.verifyPasskeyResponse !== 'function') {
397
+ throw new Error('Passkey service is not configured');
398
+ }
399
+ return this.storageAdapter.verifyPasskeyResponse(params);
662
400
  }
663
401
  async getClient(clientId) {
664
- return this.oauthStoreAdapter ? this.oauthStoreAdapter.getClient(clientId) : null;
402
+ if (typeof this.storageAdapter.getClient !== 'function') {
403
+ return null;
404
+ }
405
+ return this.storageAdapter.getClient(clientId);
665
406
  }
666
407
  async verifyClientSecret(client, clientSecret) {
667
- return this.ensureOAuthStore().verifyClientSecret(client.clientId, clientSecret);
408
+ if (typeof this.storageAdapter.verifyClientSecret !== 'function') {
409
+ throw new Error('OAuth store is not configured');
410
+ }
411
+ return this.storageAdapter.verifyClientSecret(client, clientSecret);
668
412
  }
669
413
  async createAuthCode(request) {
670
- const expiresAt = new Date(Date.now() + (request.expiresInSeconds ?? 300) * 1000);
671
- const code = request.code ?? (0, node_crypto_1.randomUUID)();
672
- await this.ensureOAuthStore().createAuthCode({ ...request, code, expiresAt });
673
- return {
674
- code,
675
- clientId: request.clientId,
676
- userId: request.userId,
677
- redirectUri: request.redirectUri,
678
- scope: request.scope ?? [],
679
- codeChallenge: request.codeChallenge,
680
- codeChallengeMethod: request.codeChallengeMethod,
681
- expiresAt,
682
- metadata: request.metadata
683
- };
414
+ if (typeof this.storageAdapter.createAuthCode !== 'function') {
415
+ throw new Error('OAuth store is not configured');
416
+ }
417
+ return this.storageAdapter.createAuthCode(request);
684
418
  }
685
419
  async consumeAuthCode(code, clientId) {
686
- const consumed = await this.ensureOAuthStore().consumeAuthCode(code);
420
+ if (typeof this.storageAdapter.consumeAuthCode !== 'function') {
421
+ return null;
422
+ }
423
+ const consumed = await this.storageAdapter.consumeAuthCode(code, clientId);
687
424
  if (!consumed || consumed.clientId !== clientId) {
688
425
  return null;
689
426
  }
690
427
  return consumed;
691
428
  }
692
429
  async canImpersonate(params) {
693
- if (this.canImpersonateAdapter) {
694
- return !!(await this.canImpersonateAdapter(params));
430
+ if (typeof this.storageAdapter.canImpersonate === 'function') {
431
+ return !!(await this.storageAdapter.canImpersonate(params));
695
432
  }
696
433
  return params.realUserId === params.effectiveUserId;
697
434
  }
698
435
  jwtSign(payload, secret, expiresInSeconds, options) {
436
+ if (!secret) {
437
+ return { success: false, error: 'JWT secret is not configured' };
438
+ }
699
439
  return (this.tokenStoreAdapter ?? this.jwtHelper).jwtSign(payload, secret, expiresInSeconds, options);
700
440
  }
701
441
  jwtVerify(token, secret, options) {
@@ -706,6 +446,8 @@ class ApiServer {
706
446
  }
707
447
  async getApiKey(token) {
708
448
  void token;
449
+ console.warn('[api-server-base] getApiKey() is not implemented. ' +
450
+ 'Override getApiKey() in your ApiServer subclass to support API key authentication.');
709
451
  return null;
710
452
  }
711
453
  async authenticateUser(params) {
@@ -730,55 +472,34 @@ class ApiServer {
730
472
  return false;
731
473
  }
732
474
  guessExceptionText(error, defMsg = 'Unknown Error') {
733
- return guess_exception_text(error, defMsg);
475
+ return (0, error_utils_js_1.guessExceptionText)(error, defMsg);
734
476
  }
735
477
  async authorize(apiReq, requiredClass) {
736
478
  void apiReq;
737
- void requiredClass;
479
+ if (requiredClass && requiredClass !== 'any') {
480
+ console.warn(`[api-server-base] Route requires auth class "${requiredClass}" but the base authorize() is not overridden. ` +
481
+ 'Override authorize() in your ApiServer subclass to enforce role/class requirements.');
482
+ }
738
483
  }
739
- middlewares() {
740
- this.app.use(express_1.default.json());
741
- this.app.use((0, cookie_parser_1.default)());
742
- const corsOptions = {
743
- origin: (origin, callback) => {
744
- if (!origin) {
745
- return callback(null, true);
746
- }
747
- if (this.config.origins && this.config.origins.length > 0) {
748
- if (this.config.origins.includes(origin)) {
749
- return callback(null, true);
750
- }
751
- return callback(new Error(`${origin} Not allowed by CORS`));
752
- }
753
- return callback(null, true);
754
- },
755
- credentials: true
756
- };
757
- this.app.use((0, cors_1.default)(corsOptions));
758
- // Provide a consistent and non-500 response when requests are blocked by the CORS allowlist.
759
- this.app.use((err, req, res, next) => {
760
- const message = err instanceof Error ? err.message : '';
761
- if (message.includes('Not allowed by CORS')) {
762
- const isApiRequest = Boolean(req.originalUrl?.startsWith(this.apiBasePath));
763
- if (isApiRequest) {
764
- res.status(403).json({
765
- success: false,
766
- code: 403,
767
- message: 'Origin not allowed by CORS',
768
- data: null,
769
- errors: {}
770
- });
771
- return;
772
- }
773
- res.status(403).send('Origin not allowed by CORS');
774
- return;
775
- }
776
- next(err);
777
- });
484
+ /**
485
+ * Authenticate and authorise an incoming Fastify request outside of the
486
+ * standard `defineRoutes` pipeline (e.g. TUS upload routes that need full
487
+ * control over their own response format).
488
+ *
489
+ * Throws `ApiError` on auth failure — callers should catch it and respond
490
+ * with the appropriate HTTP status code.
491
+ */
492
+ async resolveRequest(request, reply, auth) {
493
+ const req = this.toExtendedReq(request);
494
+ const res = new FastifyResponseAdapter(reply);
495
+ const apiReq = this.createApiRequest(req, res);
496
+ apiReq.tokenData = await this.authenticate(apiReq, auth.type);
497
+ await this.authorize(apiReq, auth.req ?? 'any');
498
+ return apiReq;
778
499
  }
779
500
  installStaticDirs() {
780
501
  const staticDirs = this.config.staticDirs;
781
- if (!staticDirs || !isPlainObject(staticDirs)) {
502
+ if (!staticDirs || !(0, request_utils_js_1.isPlainObject)(staticDirs)) {
782
503
  return;
783
504
  }
784
505
  for (const [mountRaw, dirRaw] of Object.entries(staticDirs)) {
@@ -788,12 +509,15 @@ class ApiServer {
788
509
  continue;
789
510
  }
790
511
  const resolvedMount = mount.startsWith('/') ? mount : `/${mount}`;
791
- this.app.use(resolvedMount, express_1.default.static(dir));
512
+ void this.fastify.register(static_1.default, {
513
+ root: dir,
514
+ prefix: resolvedMount.endsWith('/') ? resolvedMount : `${resolvedMount}/`,
515
+ decorateReply: false
516
+ });
792
517
  }
793
518
  }
794
519
  installPingHandler() {
795
- const path = `${this.apiBasePath}/v1/ping`;
796
- this.app.get(path, (_req, res) => {
520
+ this.fastify.get(`${this.apiBasePath}/v1/ping`, async () => {
797
521
  const payload = {
798
522
  success: true,
799
523
  status: 'ok',
@@ -803,7 +527,7 @@ class ApiServer {
803
527
  startedAt: this.startedAt,
804
528
  timestamp: new Date().toISOString()
805
529
  };
806
- res.status(200).json({ success: true, code: 200, message: 'Success', data: payload, errors: {} });
530
+ return { success: true, code: 200, message: 'Success', data: payload, errors: {} };
807
531
  });
808
532
  }
809
533
  async loadSwaggerSpec() {
@@ -811,10 +535,11 @@ class ApiServer {
811
535
  if (typeof __dirname === 'string') {
812
536
  candidates.push(node_path_1.default.resolve(__dirname, '../../docs/swagger/openapi.json'));
813
537
  }
538
+ let packageRoot;
814
539
  try {
815
540
  const require = (0, node_module_1.createRequire)(node_path_1.default.join(process.cwd(), 'package.json'));
816
541
  const entry = require.resolve('@technomoron/api-server-base');
817
- const packageRoot = node_path_1.default.resolve(node_path_1.default.dirname(entry), '..', '..');
542
+ packageRoot = node_path_1.default.resolve(node_path_1.default.dirname(entry), '..', '..');
818
543
  candidates.push(node_path_1.default.join(packageRoot, 'docs/swagger/openapi.json'));
819
544
  }
820
545
  catch {
@@ -829,7 +554,13 @@ class ApiServer {
829
554
  }
830
555
  try {
831
556
  const raw = await (0, promises_1.readFile)(candidate, 'utf8');
832
- return JSON.parse(raw);
557
+ const spec = JSON.parse(raw);
558
+ // Inject version from package.json at runtime
559
+ const version = await this.readPackageVersion(node_path_1.default.dirname(candidate));
560
+ if (version && spec.info && typeof spec.info === 'object') {
561
+ spec.info.version = version;
562
+ }
563
+ return spec;
833
564
  }
834
565
  catch {
835
566
  return null;
@@ -837,6 +568,27 @@ class ApiServer {
837
568
  }
838
569
  return null;
839
570
  }
571
+ async readPackageVersion(specDir) {
572
+ // Walk up from the spec directory looking for package.json
573
+ const candidates = [
574
+ node_path_1.default.resolve(specDir, '../../package.json'),
575
+ node_path_1.default.resolve(specDir, '../package.json'),
576
+ node_path_1.default.resolve(process.cwd(), 'package.json')
577
+ ];
578
+ for (const candidate of candidates) {
579
+ try {
580
+ const raw = await (0, promises_1.readFile)(candidate, 'utf8');
581
+ const pkg = JSON.parse(raw);
582
+ if (typeof pkg.version === 'string') {
583
+ return pkg.version;
584
+ }
585
+ }
586
+ catch {
587
+ continue;
588
+ }
589
+ }
590
+ return null;
591
+ }
840
592
  installSwaggerHandler() {
841
593
  const rawPath = typeof this.config.swaggerPath === 'string' ? this.config.swaggerPath.trim() : '';
842
594
  const enabled = Boolean(this.config.swaggerEnabled) || rawPath.length > 0;
@@ -845,31 +597,35 @@ class ApiServer {
845
597
  }
846
598
  const base = this.apiBasePath === '/' ? '' : this.apiBasePath;
847
599
  const resolved = rawPath.length > 0 ? rawPath : `${base}/swagger`;
848
- const path = resolved.startsWith('/') ? resolved : `/${resolved}`;
600
+ const routePath = resolved.startsWith('/') ? resolved : `/${resolved}`;
849
601
  let specPromise;
850
- this.app.get(path, async (_req, res) => {
602
+ this.fastify.get(routePath, async (_request, reply) => {
851
603
  if (!specPromise) {
852
604
  specPromise = this.loadSwaggerSpec();
853
605
  }
854
606
  const spec = await specPromise;
855
607
  if (!spec) {
856
- res.status(500).json({
608
+ // Clear cached failure so next request retries
609
+ specPromise = undefined;
610
+ }
611
+ if (!spec) {
612
+ reply.code(500);
613
+ return {
857
614
  success: false,
858
615
  code: 500,
859
616
  message: 'Swagger spec is unavailable',
860
617
  data: null,
861
618
  errors: {}
862
- });
863
- return;
619
+ };
864
620
  }
865
- res.status(200).json(spec);
621
+ return spec;
866
622
  });
867
623
  }
868
- normalizeApiBasePath(path) {
869
- if (!path || typeof path !== 'string') {
624
+ normalizeApiBasePath(routePath) {
625
+ if (!routePath || typeof routePath !== 'string') {
870
626
  return '/api';
871
627
  }
872
- const trimmed = path.trim();
628
+ const trimmed = routePath.trim();
873
629
  if (!trimmed) {
874
630
  return '/api';
875
631
  }
@@ -880,47 +636,121 @@ class ApiServer {
880
636
  return withLeadingSlash.replace(/\/+$/, '') || '/api';
881
637
  }
882
638
  installApiNotFoundHandler() {
883
- if (this.apiNotFoundHandler) {
884
- return;
885
- }
886
- this.apiNotFoundHandler = (req, res) => {
887
- const payload = {
639
+ this.fastify.setNotFoundHandler((request, reply) => {
640
+ const target = request.raw.url ?? request.url;
641
+ if (!target.startsWith(this.apiBasePath)) {
642
+ reply.code(404).send('Not Found');
643
+ return;
644
+ }
645
+ const method = request.method.toUpperCase();
646
+ reply.code(404).send({
888
647
  success: false,
889
648
  code: 404,
890
- message: this.describeMissingEndpoint(req),
649
+ message: `No such endpoint: ${method} ${target}`,
891
650
  data: null,
892
651
  errors: {}
893
- };
894
- res.status(404).json(payload);
895
- };
896
- this.app.use(this.apiBasePath, this.apiNotFoundHandler);
652
+ });
653
+ });
897
654
  }
898
655
  installApiErrorHandler() {
899
656
  if (this.apiErrorHandlerInstalled) {
900
657
  return;
901
658
  }
902
659
  this.apiErrorHandlerInstalled = true;
903
- this.app.use(this.apiBasePath, this.expressErrorHandler());
904
- }
905
- describeMissingEndpoint(req) {
906
- const method = typeof req.method === 'string' ? req.method.toUpperCase() : 'GET';
907
- const target = req.originalUrl || req.url || this.apiBasePath;
908
- return `No such endpoint: ${method} ${target}`;
660
+ this.fastify.setErrorHandler((error, request, reply) => {
661
+ const message = error instanceof Error ? error.message : '';
662
+ if (message.includes('Not allowed by CORS')) {
663
+ const target = request.raw.url ?? request.url;
664
+ if (target.startsWith(this.apiBasePath)) {
665
+ reply.code(403).send({
666
+ success: false,
667
+ code: 403,
668
+ message: 'Origin not allowed by CORS',
669
+ data: null,
670
+ errors: {}
671
+ });
672
+ return;
673
+ }
674
+ reply.code(403).send('Origin not allowed by CORS');
675
+ return;
676
+ }
677
+ const validation = error.validation;
678
+ if (Array.isArray(validation)) {
679
+ const fieldErrors = {};
680
+ for (const v of validation) {
681
+ const field = v.params?.missingProperty ?? (v.instancePath?.replace(/^\//, '') || 'body');
682
+ fieldErrors[field] = v.message ?? 'Invalid value';
683
+ }
684
+ reply.code(400).send({
685
+ success: false,
686
+ code: 400,
687
+ message: 'Validation failed',
688
+ data: null,
689
+ errors: fieldErrors
690
+ });
691
+ return;
692
+ }
693
+ if (error instanceof ApiError || (0, error_utils_js_1.isApiErrorLike)(error)) {
694
+ const apiError = error;
695
+ reply.code(apiError.code).send({
696
+ success: false,
697
+ code: apiError.code,
698
+ message: apiError.message,
699
+ data: apiError.data ?? null,
700
+ errors: apiError.errors && typeof apiError.errors === 'object' && !Array.isArray(apiError.errors)
701
+ ? apiError.errors
702
+ : {}
703
+ });
704
+ return;
705
+ }
706
+ const status = (0, error_utils_js_1.asHttpStatus)(error);
707
+ if (status) {
708
+ if (status >= 500) {
709
+ this.logUnhandledError(`Unhandled Fastify error mapped to HTTP ${status}`, error);
710
+ }
711
+ if (status === 413) {
712
+ reply.code(413).send({
713
+ success: false,
714
+ code: 413,
715
+ message: `Upload exceeds maximum size of ${this.config.uploadMax} bytes`,
716
+ data: null,
717
+ errors: {}
718
+ });
719
+ return;
720
+ }
721
+ reply.code(status).send({
722
+ success: false,
723
+ code: status,
724
+ message: status === 400 ? 'Invalid request payload' : this.internalServerErrorMessage(error),
725
+ data: null,
726
+ errors: {}
727
+ });
728
+ return;
729
+ }
730
+ this.logUnhandledError('Unhandled Fastify error fallback to HTTP 500', error);
731
+ reply.code(500).send({
732
+ success: false,
733
+ code: 500,
734
+ message: this.internalServerErrorMessage(error),
735
+ data: null,
736
+ errors: {}
737
+ });
738
+ });
909
739
  }
910
740
  start() {
911
741
  if (!this.finalized) {
912
742
  console.warn('[api-server-base] ApiServer.start() called without finalize(); auto-finalizing. Call server.finalize() before start() to silence this warning.');
913
743
  this.finalize();
914
744
  }
915
- this.app
745
+ void this.fastify
916
746
  .listen({
917
747
  port: this.config.apiPort,
918
748
  host: this.config.apiHost
919
749
  })
920
- .on('listening', () => {
750
+ .then(() => {
921
751
  console.log(`Server is running on http://${this.config.apiHost}:${this.config.apiPort}`);
922
752
  })
923
- .on('error', (error) => {
753
+ .catch((error) => {
924
754
  let message;
925
755
  if (error.code === 'EADDRINUSE') {
926
756
  message = `Port ${this.config.apiPort} is already in use.`;
@@ -940,13 +770,17 @@ class ApiServer {
940
770
  this.config.onStartError(err);
941
771
  return;
942
772
  }
943
- throw err;
773
+ this.logUnhandledError('Server startup failed', err);
774
+ process.exitCode = 1;
944
775
  });
945
776
  return this;
946
777
  }
947
778
  internalServerErrorMessage(error) {
948
779
  return this.config.debug ? this.guessExceptionText(error) : 'Internal server error';
949
780
  }
781
+ logUnhandledError(context, error) {
782
+ console.error(`[ApiServer] ${context}`, error);
783
+ }
950
784
  async verifyJWT(token) {
951
785
  if (!this.config.accessSecret) {
952
786
  return { tokenData: undefined, error: 'JWT authentication disabled; no jwtSecret set', expired: false };
@@ -955,7 +789,7 @@ class ApiServer {
955
789
  if (!result.success) {
956
790
  return { tokenData: undefined, error: result.error, expired: result.expired };
957
791
  }
958
- if (!result.data.uid) {
792
+ if (result.data.uid == null) {
959
793
  return { tokenData: undefined, error: 'Missing/bad userid in token', expired: false };
960
794
  }
961
795
  return { tokenData: result.data, error: undefined, expired: false };
@@ -998,8 +832,9 @@ class ApiServer {
998
832
  return null;
999
833
  }
1000
834
  const storedUid = String(stored.userId);
1001
- const verifyUid = verify.data.uid === undefined || verify.data.uid === null ? null : String(verify.data.uid);
1002
- if (verifyUid && verifyUid !== storedUid) {
835
+ const verifyUid = verify.data.uid != null ? String(verify.data.uid) : null;
836
+ // A missing uid claim is treated as a binding failure, not a skip.
837
+ if (!verifyUid || verifyUid !== storedUid) {
1003
838
  return null;
1004
839
  }
1005
840
  const claims = verify.data;
@@ -1007,7 +842,6 @@ class ApiServer {
1007
842
  void _exp;
1008
843
  void _iat;
1009
844
  void _nbf;
1010
- // Ensure we never embed token secrets into refreshed access tokens.
1011
845
  delete payload.accessToken;
1012
846
  delete payload.refreshToken;
1013
847
  delete payload.userId;
@@ -1046,7 +880,8 @@ class ApiServer {
1046
880
  }
1047
881
  let token = null;
1048
882
  const authHeader = apiReq.req.headers.authorization;
1049
- const bearerToken = authHeader?.startsWith('Bearer ') ? authHeader.slice(7).trim() || null : null;
883
+ const headerValue = Array.isArray(authHeader) ? authHeader[0] : authHeader;
884
+ const bearerToken = headerValue?.startsWith('Bearer ') ? headerValue.slice(7).trim() || null : null;
1050
885
  const paramToken = bearerToken ? null : this.resolveTokenFromRequest(apiReq.req);
1051
886
  const requiresAuthToken = this.requiresAuthToken(authType);
1052
887
  const allowRefresh = requiresAuthToken || (authType === 'maybe' && this.config.refreshMaybe);
@@ -1173,7 +1008,6 @@ class ApiServer {
1173
1008
  apiReq.token = secret;
1174
1009
  apiReq.apiKey = key;
1175
1010
  apiReq.authMethod = 'apikey';
1176
- // uid is the real identity; euid (if set) is the effective/impersonated identity.
1177
1011
  const resolvedRuid = this.normalizeAuthIdentifier(key.uid);
1178
1012
  const resolvedEuid = key.euid !== undefined ? this.normalizeAuthIdentifier(key.euid) : null;
1179
1013
  const effectiveUid = resolvedEuid ?? resolvedRuid;
@@ -1271,21 +1105,25 @@ class ApiServer {
1271
1105
  }
1272
1106
  return rawReal;
1273
1107
  }
1274
- useExpress(pathOrHandler, ...handlers) {
1275
- this.assertNotFinalized('useExpress');
1276
- if (typeof pathOrHandler === 'string') {
1277
- const apiPath = this.toApiRouterPath(pathOrHandler);
1278
- if (apiPath) {
1279
- this.apiRouter.use(apiPath, ...handlers);
1280
- }
1281
- else {
1282
- this.app.use(pathOrHandler, ...handlers);
1283
- }
1284
- }
1285
- else {
1286
- this.app.use(pathOrHandler, ...handlers);
1287
- }
1288
- return this;
1108
+ toExtendedReq(request) {
1109
+ const parsedBody = request.__apiParsedBody;
1110
+ const parsedFiles = request.__apiParsedFiles;
1111
+ const headers = request.headers;
1112
+ return {
1113
+ method: request.method,
1114
+ url: request.url,
1115
+ originalUrl: request.raw.url,
1116
+ headers,
1117
+ query: (request.query ?? {}),
1118
+ body: parsedBody ?? request.body,
1119
+ params: (request.params ?? {}),
1120
+ cookies: (request.cookies ?? {}),
1121
+ ip: request.ip,
1122
+ ips: request.ips,
1123
+ socket: request.socket,
1124
+ protocol: request.protocol,
1125
+ files: parsedFiles
1126
+ };
1289
1127
  }
1290
1128
  createApiRequest(req, res) {
1291
1129
  const apiReq = {
@@ -1296,9 +1134,9 @@ class ApiServer {
1296
1134
  tokenData: null,
1297
1135
  authMethod: null,
1298
1136
  realUid: null,
1299
- getClientInfo: () => ensureClientInfo(apiReq),
1300
- getClientIp: () => ensureClientInfo(apiReq).ip,
1301
- getClientIpChain: () => ensureClientInfo(apiReq).ipchain,
1137
+ getClientInfo: () => (0, client_info_js_1.ensureClientInfo)(apiReq, this.config.trustProxy),
1138
+ getClientIp: () => (0, client_info_js_1.ensureClientInfo)(apiReq, this.config.trustProxy).ip,
1139
+ getClientIpChain: () => (0, client_info_js_1.ensureClientInfo)(apiReq, this.config.trustProxy).ipchain,
1302
1140
  getRealUid: () => apiReq.realUid ?? null,
1303
1141
  isImpersonating: () => {
1304
1142
  const realUid = apiReq.realUid;
@@ -1309,11 +1147,101 @@ class ApiServer {
1309
1147
  if (tokenUid === null || tokenUid === undefined) {
1310
1148
  return false;
1311
1149
  }
1312
- return realUid !== tokenUid;
1150
+ // Normalise both sides to strings before comparing so that
1151
+ // numeric 42 and string "42" are treated as the same identity.
1152
+ return String(realUid) !== String(tokenUid);
1313
1153
  }
1314
1154
  };
1315
1155
  return apiReq;
1316
1156
  }
1157
+ useExpress(pathOrHandler, ...handlers) {
1158
+ this.assertNotFinalized('useExpress');
1159
+ if (typeof pathOrHandler !== 'string') {
1160
+ if (pathOrHandler.length === 4) {
1161
+ this.compatGlobalErrorHandler = pathOrHandler;
1162
+ }
1163
+ return this;
1164
+ }
1165
+ const path = pathOrHandler;
1166
+ const stack = handlers;
1167
+ this.fastify.all(path, async (request, reply) => {
1168
+ const req = this.toExtendedReq(request);
1169
+ const res = new FastifyResponseAdapter(reply);
1170
+ try {
1171
+ await this.runCompatHandlers(stack, req, res);
1172
+ }
1173
+ catch (error) {
1174
+ await this.runCompatErrorHandlers(error, stack, req, res);
1175
+ }
1176
+ });
1177
+ return this;
1178
+ }
1179
+ async runCompatHandlers(stack, req, res) {
1180
+ const handlers = stack.filter((fn) => fn.length !== 4);
1181
+ for (const fn of handlers) {
1182
+ await new Promise((resolve, reject) => {
1183
+ let nextCalled = false;
1184
+ const next = (error) => {
1185
+ nextCalled = true;
1186
+ if (error) {
1187
+ reject(error);
1188
+ return;
1189
+ }
1190
+ resolve();
1191
+ };
1192
+ try {
1193
+ const out = fn(req, res, next);
1194
+ Promise.resolve(out)
1195
+ .then(() => {
1196
+ if (!nextCalled) {
1197
+ resolve();
1198
+ }
1199
+ })
1200
+ .catch(reject);
1201
+ }
1202
+ catch (error) {
1203
+ reject(error);
1204
+ }
1205
+ });
1206
+ if (res.headersSent) {
1207
+ return;
1208
+ }
1209
+ }
1210
+ }
1211
+ async runCompatErrorHandlers(error, stack, req, res) {
1212
+ const errorHandlers = stack.filter((fn) => fn.length === 4);
1213
+ if (this.compatGlobalErrorHandler) {
1214
+ errorHandlers.push(this.compatGlobalErrorHandler);
1215
+ }
1216
+ if (errorHandlers.length === 0) {
1217
+ throw error;
1218
+ }
1219
+ let currentError = error;
1220
+ for (const handler of errorHandlers) {
1221
+ await new Promise((resolve, reject) => {
1222
+ const next = (nextError) => {
1223
+ if (nextError) {
1224
+ currentError = nextError;
1225
+ }
1226
+ resolve();
1227
+ };
1228
+ try {
1229
+ Promise.resolve(handler(currentError, req, res, next))
1230
+ .then(() => resolve())
1231
+ .catch(reject);
1232
+ }
1233
+ catch (caught) {
1234
+ reject(caught);
1235
+ }
1236
+ });
1237
+ if (res.headersSent) {
1238
+ return;
1239
+ }
1240
+ }
1241
+ if (!res.headersSent) {
1242
+ throw currentError;
1243
+ }
1244
+ }
1317
1245
  expressAuth(auth) {
1318
1246
  return async (req, res, next) => {
1319
1247
  const apiReq = this.createApiRequest(req, res);
@@ -1321,7 +1249,7 @@ class ApiServer {
1321
1249
  res.locals.apiReq = apiReq;
1322
1250
  try {
1323
1251
  if (this.config.hydrateGetBody) {
1324
- hydrateGetBody(req);
1252
+ (0, request_utils_js_1.hydrateGetBody)(req);
1325
1253
  }
1326
1254
  if (this.config.debug) {
1327
1255
  this.dumpRequest(apiReq);
@@ -1342,7 +1270,7 @@ class ApiServer {
1342
1270
  next(error);
1343
1271
  return;
1344
1272
  }
1345
- if (error instanceof ApiError || isApiErrorLike(error)) {
1273
+ if (error instanceof ApiError || (0, error_utils_js_1.isApiErrorLike)(error)) {
1346
1274
  const apiError = error;
1347
1275
  const normalizedErrors = apiError.errors && typeof apiError.errors === 'object' && !Array.isArray(apiError.errors)
1348
1276
  ? apiError.errors
@@ -1357,8 +1285,11 @@ class ApiServer {
1357
1285
  res.status(apiError.code).json(errorPayload);
1358
1286
  return;
1359
1287
  }
1360
- const status = asHttpStatus(error);
1288
+ const status = (0, error_utils_js_1.asHttpStatus)(error);
1361
1289
  if (status) {
1290
+ if (status >= 500) {
1291
+ this.logUnhandledError(`Unhandled compat middleware error mapped to HTTP ${status}`, error);
1292
+ }
1362
1293
  res.status(status).json({
1363
1294
  success: false,
1364
1295
  code: status,
@@ -1368,23 +1299,24 @@ class ApiServer {
1368
1299
  });
1369
1300
  return;
1370
1301
  }
1371
- const errorPayload = {
1302
+ this.logUnhandledError('Unhandled compat middleware error fallback to HTTP 500', error);
1303
+ res.status(500).json({
1372
1304
  success: false,
1373
1305
  code: 500,
1374
1306
  message: this.internalServerErrorMessage(error),
1375
1307
  data: null,
1376
1308
  errors: {}
1377
- };
1378
- res.status(500).json(errorPayload);
1309
+ });
1379
1310
  };
1380
1311
  }
1381
- handle_request(handler, auth) {
1382
- return async (req, res, next) => {
1383
- void next;
1312
+ handleRequest(handler, auth) {
1313
+ return async (request, reply) => {
1314
+ const req = this.toExtendedReq(request);
1315
+ const res = new FastifyResponseAdapter(reply);
1384
1316
  const apiReq = this.createApiRequest(req, res);
1385
1317
  try {
1386
1318
  if (this.config.hydrateGetBody) {
1387
- hydrateGetBody(apiReq.req);
1319
+ (0, request_utils_js_1.hydrateGetBody)(apiReq.req);
1388
1320
  }
1389
1321
  if (this.config.debug) {
1390
1322
  this.dumpRequest(apiReq);
@@ -1403,14 +1335,14 @@ class ApiServer {
1403
1335
  throw new ApiError({ code: 500, message: 'Handler result must start with a numeric status code' });
1404
1336
  }
1405
1337
  const message = typeof rawMessage === 'string' ? rawMessage : 'Success';
1406
- const responsePayload = { success: true, code, message, data, errors: {} };
1338
+ const responsePayload = { success: code < 400, code, message, data, errors: {} };
1407
1339
  if (this.config.debug) {
1408
1340
  this.dumpResponse(apiReq, responsePayload, code);
1409
1341
  }
1410
- res.status(code).json(responsePayload);
1342
+ reply.code(code).send(responsePayload);
1411
1343
  }
1412
1344
  catch (error) {
1413
- if (error instanceof ApiError || isApiErrorLike(error)) {
1345
+ if (error instanceof ApiError || (0, error_utils_js_1.isApiErrorLike)(error)) {
1414
1346
  const apiError = error;
1415
1347
  const normalizedErrors = apiError.errors && typeof apiError.errors === 'object' && !Array.isArray(apiError.errors)
1416
1348
  ? apiError.errors
@@ -1425,27 +1357,53 @@ class ApiServer {
1425
1357
  if (this.config.debug) {
1426
1358
  this.dumpResponse(apiReq, errorPayload, apiError.code);
1427
1359
  }
1428
- res.status(apiError.code).json(errorPayload);
1360
+ reply.code(apiError.code).send(errorPayload);
1429
1361
  }
1430
1362
  else {
1363
+ const status = (0, error_utils_js_1.asHttpStatus)(error) ?? 500;
1364
+ if (status >= 500) {
1365
+ this.logUnhandledError('Unhandled API route error fallback to HTTP 500', error);
1366
+ }
1367
+ const uploadTooLarge = error &&
1368
+ typeof error === 'object' &&
1369
+ ('code' in error ? error.code === 'FST_REQ_FILE_TOO_LARGE' : false);
1431
1370
  const errorPayload = {
1432
1371
  success: false,
1433
- code: 500,
1434
- message: this.internalServerErrorMessage(error),
1372
+ code: uploadTooLarge ? 413 : status,
1373
+ message: uploadTooLarge
1374
+ ? `Upload exceeds maximum size of ${this.config.uploadMax} bytes`
1375
+ : this.internalServerErrorMessage(error),
1435
1376
  data: null,
1436
1377
  errors: {}
1437
1378
  };
1438
1379
  if (this.config.debug) {
1439
- this.dumpResponse(apiReq, errorPayload, 500);
1380
+ this.dumpResponse(apiReq, errorPayload, uploadTooLarge ? 413 : status);
1440
1381
  }
1441
- res.status(500).json(errorPayload);
1382
+ reply.code(uploadTooLarge ? 413 : status).send(errorPayload);
1383
+ }
1384
+ }
1385
+ };
1386
+ }
1387
+ // Test/compat shim for direct unit-level request wrapping without Fastify runtime.
1388
+ handle_request(handler, auth) {
1389
+ return async (req, res, next) => {
1390
+ const apiReq = this.createApiRequest(req, res);
1391
+ try {
1392
+ if (this.config.hydrateGetBody) {
1393
+ (0, request_utils_js_1.hydrateGetBody)(apiReq.req);
1442
1394
  }
1395
+ apiReq.tokenData = await this.authenticate(apiReq, auth.type);
1396
+ await this.authorize(apiReq, auth.req);
1397
+ await handler(apiReq);
1398
+ next();
1399
+ }
1400
+ catch (error) {
1401
+ next(error);
1443
1402
  }
1444
1403
  };
1445
1404
  }
1446
1405
  api(module) {
1447
1406
  this.assertNotFinalized('api');
1448
- const router = express_1.default.Router();
1449
1407
  module.server = this;
1450
1408
  const moduleType = module.moduleType;
1451
1409
  if (moduleType === 'auth') {
@@ -1456,36 +1414,33 @@ class ApiServer {
1456
1414
  const name = module.constructor?.name ? String(module.constructor.name) : 'ApiModule';
1457
1415
  throw new Error(`${name}.checkConfig() returned false`);
1458
1416
  }
1459
- const base = this.apiBasePath;
1417
+ module.onMount();
1460
1418
  const ns = module.namespace;
1461
- const mountPath = `${base}${ns}`;
1419
+ const mountPath = `${this.apiBasePath}${ns}`;
1462
1420
  module.mountpath = mountPath;
1463
- module.defineRoutes().forEach((r) => {
1464
- const handler = this.handle_request(r.handler, r.auth);
1465
- switch (r.method) {
1466
- case 'get':
1467
- router.get(r.path, handler);
1468
- break;
1469
- case 'post':
1470
- router.post(r.path, handler);
1471
- break;
1472
- case 'put':
1473
- router.put(r.path, handler);
1474
- break;
1475
- case 'patch':
1476
- router.patch(r.path, handler);
1477
- break;
1478
- case 'delete':
1479
- router.delete(r.path, handler);
1480
- break;
1481
- }
1421
+ for (const route of module.defineRoutes()) {
1422
+ const handler = this.handleRequest(route.handler, route.auth);
1423
+ const method = route.method.toUpperCase();
1424
+ const fullPath = this.joinRoutePath(this.apiBasePath, ns, route.path);
1425
+ this.fastify.route({ method, url: fullPath, handler, ...(route.schema ? { schema: route.schema } : {}) });
1482
1426
  if (this.config.debug) {
1483
- console.log(`Adding ${mountPath}${r.path} (${r.method.toUpperCase()})`);
1427
+ console.log(`Adding ${fullPath} (${method})`);
1484
1428
  }
1485
- });
1486
- this.apiRouter.use(ns, router);
1429
+ }
1487
1430
  return this;
1488
1431
  }
1432
+ joinRoutePath(base, namespace, routePath) {
1433
+ const parts = [base, namespace, routePath]
1434
+ .filter((value) => typeof value === 'string' && value.length > 0)
1435
+ .map((value, index) => {
1436
+ if (index === 0) {
1437
+ return value.startsWith('/') ? value : `/${value}`;
1438
+ }
1439
+ return value.replace(/^\/+/, '').replace(/\/+$/, '');
1440
+ });
1441
+ const joined = parts.join('/').replace(/\/+/g, '/');
1442
+ return joined.startsWith('/') ? joined : `/${joined}`;
1443
+ }
1489
1444
  dumpRequest(apiReq) {
1490
1445
  const req = apiReq.req;
1491
1446
  const tokenParam = this.config.tokenParam.trim();
@@ -1493,10 +1448,7 @@ class ApiServer {
1493
1448
  const url = req.originalUrl || req.url;
1494
1449
  console.log('URL:', url);
1495
1450
  console.log('Method:', req.method);
1496
- if (tokenParam &&
1497
- req.query &&
1498
- typeof req.query === 'object' &&
1499
- tokenParam in req.query) {
1451
+ if (tokenParam && req.query && tokenParam in req.query) {
1500
1452
  const maskedQuery = { ...req.query, [tokenParam]: '[REDACTED]' };
1501
1453
  console.log('Query Params:', maskedQuery);
1502
1454
  }
@@ -1528,6 +1480,12 @@ class ApiServer {
1528
1480
  if (headers.authorization) {
1529
1481
  headers.authorization = '[REDACTED]';
1530
1482
  }
1483
+ if (headers['x-api-key']) {
1484
+ headers['x-api-key'] = '[REDACTED]';
1485
+ }
1486
+ if (headers.cookie) {
1487
+ headers.cookie = '[REDACTED]';
1488
+ }
1531
1489
  console.log('Headers:', headers);
1532
1490
  console.log('------------------------');
1533
1491
  }