@technomoron/api-server-base 2.0.0-beta.2 → 2.0.0-beta.20

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 (115) hide show
  1. package/README.txt +81 -28
  2. package/dist/cjs/api-module.cjs +9 -0
  3. package/dist/cjs/api-module.d.ts +7 -4
  4. package/dist/cjs/api-server-base.cjs +607 -99
  5. package/dist/cjs/api-server-base.d.ts +80 -23
  6. package/dist/cjs/auth-api/auth-module.d.ts +23 -3
  7. package/dist/cjs/auth-api/auth-module.js +320 -124
  8. package/dist/cjs/auth-api/compat-auth-storage.d.ts +7 -5
  9. package/dist/cjs/auth-api/compat-auth-storage.js +15 -3
  10. package/dist/cjs/auth-api/mem-auth-store.d.ts +5 -3
  11. package/dist/cjs/auth-api/mem-auth-store.js +14 -28
  12. package/dist/cjs/auth-api/module.d.ts +1 -1
  13. package/dist/cjs/auth-api/sql-auth-store.d.ts +16 -4
  14. package/dist/cjs/auth-api/sql-auth-store.js +43 -30
  15. package/dist/cjs/auth-api/storage.d.ts +6 -4
  16. package/dist/cjs/auth-api/storage.js +15 -5
  17. package/dist/cjs/auth-api/types.d.ts +7 -2
  18. package/dist/cjs/auth-api/user-id.d.ts +5 -0
  19. package/dist/cjs/auth-api/user-id.js +38 -0
  20. package/dist/cjs/auth-cookie-options.d.ts +11 -0
  21. package/dist/cjs/auth-cookie-options.js +66 -0
  22. package/dist/cjs/index.cjs +4 -14
  23. package/dist/cjs/index.d.ts +4 -9
  24. package/dist/cjs/oauth/memory.d.ts +6 -0
  25. package/dist/cjs/oauth/memory.js +44 -11
  26. package/dist/cjs/oauth/models.d.ts +7 -2
  27. package/dist/cjs/oauth/models.js +10 -21
  28. package/dist/cjs/oauth/sequelize.d.ts +10 -48
  29. package/dist/cjs/oauth/sequelize.js +44 -99
  30. package/dist/cjs/oauth/types.d.ts +1 -0
  31. package/dist/cjs/passkey/base.d.ts +2 -0
  32. package/dist/cjs/passkey/config.d.ts +2 -0
  33. package/dist/cjs/passkey/config.js +26 -0
  34. package/dist/cjs/passkey/memory.d.ts +8 -0
  35. package/dist/cjs/passkey/memory.js +57 -16
  36. package/dist/cjs/passkey/models.d.ts +13 -4
  37. package/dist/cjs/passkey/models.js +41 -14
  38. package/dist/cjs/passkey/sequelize.d.ts +13 -25
  39. package/dist/cjs/passkey/sequelize.js +68 -153
  40. package/dist/cjs/passkey/service.d.ts +6 -2
  41. package/dist/cjs/passkey/service.js +205 -27
  42. package/dist/cjs/passkey/types.d.ts +18 -9
  43. package/dist/cjs/sequelize-utils.d.ts +8 -0
  44. package/dist/cjs/sequelize-utils.js +57 -0
  45. package/dist/cjs/token/base.d.ts +2 -1
  46. package/dist/cjs/token/base.js +3 -1
  47. package/dist/cjs/token/memory.d.ts +10 -0
  48. package/dist/cjs/token/memory.js +122 -32
  49. package/dist/cjs/token/sequelize.d.ts +4 -4
  50. package/dist/cjs/token/sequelize.js +67 -85
  51. package/dist/cjs/token/types.d.ts +8 -1
  52. package/dist/cjs/user/base.d.ts +1 -0
  53. package/dist/cjs/user/base.js +11 -4
  54. package/dist/cjs/user/memory.d.ts +2 -0
  55. package/dist/cjs/user/memory.js +9 -10
  56. package/dist/cjs/user/sequelize.d.ts +7 -2
  57. package/dist/cjs/user/sequelize.js +19 -32
  58. package/dist/esm/api-module.d.ts +7 -4
  59. package/dist/esm/api-module.js +9 -0
  60. package/dist/esm/api-server-base.d.ts +80 -23
  61. package/dist/esm/api-server-base.js +608 -100
  62. package/dist/esm/auth-api/auth-module.d.ts +23 -3
  63. package/dist/esm/auth-api/auth-module.js +321 -125
  64. package/dist/esm/auth-api/compat-auth-storage.d.ts +7 -5
  65. package/dist/esm/auth-api/compat-auth-storage.js +13 -1
  66. package/dist/esm/auth-api/mem-auth-store.d.ts +5 -3
  67. package/dist/esm/auth-api/mem-auth-store.js +14 -28
  68. package/dist/esm/auth-api/module.d.ts +1 -1
  69. package/dist/esm/auth-api/sql-auth-store.d.ts +16 -4
  70. package/dist/esm/auth-api/sql-auth-store.js +43 -30
  71. package/dist/esm/auth-api/storage.d.ts +6 -4
  72. package/dist/esm/auth-api/storage.js +13 -3
  73. package/dist/esm/auth-api/types.d.ts +7 -2
  74. package/dist/esm/auth-api/user-id.d.ts +5 -0
  75. package/dist/esm/auth-api/user-id.js +32 -0
  76. package/dist/esm/auth-cookie-options.d.ts +11 -0
  77. package/dist/esm/auth-cookie-options.js +63 -0
  78. package/dist/esm/index.d.ts +4 -9
  79. package/dist/esm/index.js +2 -7
  80. package/dist/esm/oauth/memory.d.ts +6 -0
  81. package/dist/esm/oauth/memory.js +44 -11
  82. package/dist/esm/oauth/models.d.ts +7 -2
  83. package/dist/esm/oauth/models.js +6 -19
  84. package/dist/esm/oauth/sequelize.d.ts +10 -48
  85. package/dist/esm/oauth/sequelize.js +32 -87
  86. package/dist/esm/oauth/types.d.ts +1 -0
  87. package/dist/esm/passkey/base.d.ts +2 -0
  88. package/dist/esm/passkey/config.d.ts +2 -0
  89. package/dist/esm/passkey/config.js +23 -0
  90. package/dist/esm/passkey/memory.d.ts +8 -0
  91. package/dist/esm/passkey/memory.js +57 -16
  92. package/dist/esm/passkey/models.d.ts +13 -4
  93. package/dist/esm/passkey/models.js +39 -12
  94. package/dist/esm/passkey/sequelize.d.ts +13 -25
  95. package/dist/esm/passkey/sequelize.js +69 -154
  96. package/dist/esm/passkey/service.d.ts +6 -2
  97. package/dist/esm/passkey/service.js +173 -28
  98. package/dist/esm/passkey/types.d.ts +18 -9
  99. package/dist/esm/sequelize-utils.d.ts +8 -0
  100. package/dist/esm/sequelize-utils.js +48 -0
  101. package/dist/esm/token/base.d.ts +2 -1
  102. package/dist/esm/token/base.js +3 -1
  103. package/dist/esm/token/memory.d.ts +10 -0
  104. package/dist/esm/token/memory.js +122 -32
  105. package/dist/esm/token/sequelize.d.ts +4 -4
  106. package/dist/esm/token/sequelize.js +67 -85
  107. package/dist/esm/token/types.d.ts +8 -1
  108. package/dist/esm/user/base.d.ts +1 -0
  109. package/dist/esm/user/base.js +11 -4
  110. package/dist/esm/user/memory.d.ts +2 -0
  111. package/dist/esm/user/memory.js +9 -10
  112. package/dist/esm/user/sequelize.d.ts +7 -2
  113. package/dist/esm/user/sequelize.js +19 -32
  114. package/docs/swagger/openapi.json +1876 -0
  115. package/package.json +81 -32
@@ -5,12 +5,17 @@
5
5
  * LICENSE file in the root directory of this source tree.
6
6
  */
7
7
  import { randomUUID } from 'node:crypto';
8
+ import { access, readFile } from 'node:fs/promises';
9
+ import { createRequire } from 'node:module';
10
+ import path from 'node:path';
8
11
  import cookieParser from 'cookie-parser';
9
12
  import cors from 'cors';
10
13
  import express from 'express';
11
14
  import multer from 'multer';
12
15
  import { nullAuthModule } from './auth-api/module.js';
13
- import { nullAuthStorage } from './auth-api/storage.js';
16
+ import { nullAuthAdapter } from './auth-api/storage.js';
17
+ import { toOptionalStringId } from './auth-api/user-id.js';
18
+ import { buildAuthCookieOptions } from './auth-cookie-options.js';
14
19
  import { TokenStore } from './token/base.js';
15
20
  class JwtHelperStore extends TokenStore {
16
21
  async save() {
@@ -39,11 +44,14 @@ function guess_exception_text(error, defMsg = 'Unknown Error') {
39
44
  msg.push(error);
40
45
  }
41
46
  else if (error && typeof error === 'object') {
42
- if (typeof error.message === 'string' && error.message.trim() !== '') {
43
- msg.push(error.message);
47
+ const errorDetails = error;
48
+ if (typeof errorDetails.message === 'string' && errorDetails.message.trim() !== '') {
49
+ msg.push(errorDetails.message);
44
50
  }
45
- if (error.parent && typeof error.parent.message === 'string' && error.parent.message.trim() !== '') {
46
- msg.push(error.parent.message);
51
+ if (errorDetails.parent &&
52
+ typeof errorDetails.parent.message === 'string' &&
53
+ errorDetails.parent.message.trim() !== '') {
54
+ msg.push(errorDetails.parent.message);
47
55
  }
48
56
  }
49
57
  return msg.length > 0 ? msg.join(' / ') : defMsg;
@@ -64,6 +72,7 @@ function hydrateGetBody(req) {
64
72
  req.body = { ...query };
65
73
  return;
66
74
  }
75
+ // Keep explicit body fields authoritative when both query and body provide the same key.
67
76
  req.body = { ...query, ...body };
68
77
  }
69
78
  function normalizeIpAddress(candidate) {
@@ -259,7 +268,9 @@ function collectClientIpChain(req) {
259
268
  }
260
269
  const realIp = req.headers['x-real-ip'];
261
270
  if (Array.isArray(realIp)) {
262
- realIp.forEach((value) => pushNormalized(normalizeIpAddress(value)));
271
+ for (const value of realIp) {
272
+ pushNormalized(normalizeIpAddress(value));
273
+ }
263
274
  }
264
275
  else if (typeof realIp === 'string') {
265
276
  pushNormalized(normalizeIpAddress(realIp));
@@ -323,18 +334,36 @@ function isApiErrorLike(candidate) {
323
334
  const maybeError = candidate;
324
335
  return typeof maybeError.code === 'number' && typeof maybeError.message === 'string';
325
336
  }
337
+ function asHttpStatus(error) {
338
+ if (!error || typeof error !== 'object') {
339
+ return null;
340
+ }
341
+ const maybe = error;
342
+ const status = typeof maybe.status === 'number' ? maybe.status : maybe.statusCode;
343
+ if (typeof status === 'number' && status >= 400 && status <= 599) {
344
+ return status;
345
+ }
346
+ return null;
347
+ }
326
348
  function fillConfig(config) {
327
349
  return {
328
350
  apiPort: config.apiPort ?? 3101,
329
351
  apiHost: config.apiHost ?? 'localhost',
330
352
  uploadPath: config.uploadPath ?? '',
331
353
  uploadMax: config.uploadMax ?? 30 * 1024 * 1024,
354
+ staticDirs: config.staticDirs,
332
355
  origins: config.origins ?? [],
333
356
  debug: config.debug ?? false,
334
357
  apiBasePath: config.apiBasePath ?? '/api',
358
+ swaggerEnabled: config.swaggerEnabled ?? false,
359
+ swaggerPath: config.swaggerPath ?? '',
335
360
  accessSecret: config.accessSecret ?? '',
336
361
  refreshSecret: config.refreshSecret ?? '',
337
- cookieDomain: config.cookieDomain ?? '.somewhere-over-the-rainbow.com',
362
+ cookieDomain: config.cookieDomain ?? '',
363
+ cookiePath: config.cookiePath ?? '/',
364
+ cookieSameSite: config.cookieSameSite ?? 'lax',
365
+ cookieSecure: config.cookieSecure ?? 'auto',
366
+ cookieHttpOnly: config.cookieHttpOnly ?? true,
338
367
  accessCookie: config.accessCookie ?? 'dat',
339
368
  refreshCookie: config.refreshCookie ?? 'drt',
340
369
  accessExpiry: config.accessExpiry ?? 60 * 15,
@@ -344,25 +373,45 @@ function fillConfig(config) {
344
373
  devMode: config.devMode ?? false,
345
374
  hydrateGetBody: config.hydrateGetBody ?? true,
346
375
  validateTokens: config.validateTokens ?? false,
376
+ refreshMaybe: config.refreshMaybe ?? false,
347
377
  apiVersion: config.apiVersion ?? '',
348
378
  minClientVersion: config.minClientVersion ?? '',
349
379
  tokenStore: config.tokenStore,
350
- authStores: config.authStores
380
+ authStores: config.authStores,
381
+ onStartError: config.onStartError
351
382
  };
352
383
  }
353
384
  export class ApiServer {
385
+ /**
386
+ * @deprecated ApiServer does not track a global "current request". This value is always null.
387
+ * Use the per-request ApiRequest passed to handlers, or `req.apiReq` / `res.locals.apiReq`
388
+ * when mounting raw Express endpoints.
389
+ */
390
+ get currReq() {
391
+ return null;
392
+ }
393
+ set currReq(_value) {
394
+ if (this.config.devMode && !this.currReqDeprecationWarned) {
395
+ this.currReqDeprecationWarned = true;
396
+ console.warn('[api-server-base] ApiServer.currReq is deprecated and always null. Use req.apiReq or res.locals.apiReq.');
397
+ }
398
+ void _value;
399
+ }
354
400
  constructor(config = {}) {
355
- this.currReq = null;
401
+ this.finalized = false;
402
+ this.serverAuthAdapter = null;
356
403
  this.apiNotFoundHandler = null;
404
+ this.apiErrorHandlerInstalled = false;
357
405
  this.tokenStoreAdapter = null;
358
406
  this.userStoreAdapter = null;
359
407
  this.passkeyServiceAdapter = null;
360
408
  this.oauthStoreAdapter = null;
361
409
  this.canImpersonateAdapter = null;
410
+ this.currReqDeprecationWarned = false;
362
411
  this.config = fillConfig(config);
363
412
  this.apiBasePath = this.normalizeApiBasePath(this.config.apiBasePath);
364
413
  this.startedAt = Date.now();
365
- this.storageAdapter = nullAuthStorage;
414
+ this.storageAdapter = nullAuthAdapter;
366
415
  this.moduleAdapter = nullAuthModule;
367
416
  this.jwtHelper = new JwtHelperStore();
368
417
  this.tokenStoreAdapter = this.config.tokenStore ?? null;
@@ -373,17 +422,75 @@ export class ApiServer {
373
422
  this.passkeyServiceAdapter = passkeyService ?? null;
374
423
  this.oauthStoreAdapter = oauthStore ?? null;
375
424
  this.canImpersonateAdapter = canImpersonate ?? null;
376
- this.storageAdapter = this;
425
+ this.storageAdapter = this.getServerAuthAdapter();
426
+ }
427
+ if ((this.config.authApi || this.config.authStores) &&
428
+ (!this.config.accessSecret || !this.config.refreshSecret)) {
429
+ console.warn('[api-server-base] Auth is enabled but accessSecret and/or refreshSecret are empty. JWT signing will fail at runtime.');
377
430
  }
378
431
  this.app = express();
432
+ // Mount API modules and any custom endpoints under `apiBasePath` on this router so we can keep
433
+ // the API 404 handler ordered last without relying on Express internals.
434
+ this.apiRouter = express.Router();
379
435
  if (config.uploadPath) {
380
- const upload = multer({ dest: config.uploadPath });
436
+ const upload = multer({ dest: config.uploadPath, limits: { fileSize: this.config.uploadMax } });
381
437
  this.app.use(upload.any());
438
+ // Multer errors happen before ApiModule route wrappers; format the common "too large" failure.
439
+ this.app.use((err, _req, res, next) => {
440
+ const code = err && typeof err === 'object' ? err.code : undefined;
441
+ if (code === 'LIMIT_FILE_SIZE') {
442
+ res.status(413).json({
443
+ success: false,
444
+ code: 413,
445
+ message: `Upload exceeds maximum size of ${this.config.uploadMax} bytes`,
446
+ data: null,
447
+ errors: {}
448
+ });
449
+ return;
450
+ }
451
+ next(err);
452
+ });
382
453
  }
383
454
  this.middlewares();
455
+ this.installStaticDirs();
384
456
  this.installPingHandler();
457
+ this.installSwaggerHandler();
458
+ this.app.use(this.apiBasePath, this.apiRouter);
385
459
  // addSwaggerUi(this.app);
386
460
  this.installApiNotFoundHandler();
461
+ this.installApiErrorHandler();
462
+ }
463
+ assertNotFinalized(action) {
464
+ if (this.finalized) {
465
+ throw new Error(`Cannot call ApiServer.${action}() after finalize()/start()`);
466
+ }
467
+ }
468
+ toApiRouterPath(candidate) {
469
+ if (typeof candidate !== 'string') {
470
+ return null;
471
+ }
472
+ const trimmed = candidate.trim();
473
+ if (!trimmed) {
474
+ return null;
475
+ }
476
+ const normalized = trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
477
+ const base = this.apiBasePath;
478
+ if (base === '/') {
479
+ return normalized;
480
+ }
481
+ if (normalized === base) {
482
+ return '/';
483
+ }
484
+ if (normalized.startsWith(`${base}/`)) {
485
+ return normalized.slice(base.length) || '/';
486
+ }
487
+ return null;
488
+ }
489
+ finalize() {
490
+ this.installApiNotFoundHandler();
491
+ this.installApiErrorHandler();
492
+ this.finalized = true;
493
+ return this;
387
494
  }
388
495
  authStorage(storage) {
389
496
  this.storageAdapter = storage;
@@ -413,9 +520,9 @@ export class ApiServer {
413
520
  }
414
521
  setTokenStore(store) {
415
522
  this.tokenStoreAdapter = store;
416
- // If using direct stores, expose self as the auth storage.
523
+ // If using direct stores, expose the server-backed auth adapter.
417
524
  if (this.userStoreAdapter) {
418
- this.storageAdapter = this;
525
+ this.storageAdapter = this.getServerAuthAdapter();
419
526
  }
420
527
  return this;
421
528
  }
@@ -440,13 +547,46 @@ export class ApiServer {
440
547
  }
441
548
  return this.passkeyServiceAdapter;
442
549
  }
550
+ async listUserCredentials(userId) {
551
+ return this.ensurePasskeyService().listUserCredentials(userId);
552
+ }
553
+ async deletePasskeyCredential(credentialId) {
554
+ return this.ensurePasskeyService().deleteCredential(credentialId);
555
+ }
443
556
  ensureOAuthStore() {
444
557
  if (!this.oauthStoreAdapter) {
445
558
  throw new Error('OAuth store is not configured');
446
559
  }
447
560
  return this.oauthStoreAdapter;
448
561
  }
449
- // AuthStorage-compatible helpers (used by AuthModule)
562
+ getServerAuthAdapter() {
563
+ if (this.serverAuthAdapter) {
564
+ return this.serverAuthAdapter;
565
+ }
566
+ const server = this;
567
+ this.serverAuthAdapter = {
568
+ getUser: (identifier) => server.getUser(identifier),
569
+ getUserPasswordHash: (user) => server.getUserPasswordHash(user),
570
+ getUserId: (user) => server.getUserId(user),
571
+ filterUser: (user) => server.filterUser(user),
572
+ verifyPassword: (password, hash) => server.verifyPassword(password, hash),
573
+ storeToken: (data) => server.storeToken(data),
574
+ getToken: (query, opts) => server.getToken(query, opts),
575
+ deleteToken: (query) => server.deleteToken(query),
576
+ updateToken: (updates) => server.updateToken(updates),
577
+ createPasskeyChallenge: (params) => server.createPasskeyChallenge(params),
578
+ verifyPasskeyResponse: (params) => server.verifyPasskeyResponse(params),
579
+ listUserCredentials: (userId) => server.listUserCredentials(userId),
580
+ deletePasskeyCredential: (credentialId) => server.deletePasskeyCredential(credentialId),
581
+ getClient: (clientId) => server.getClient(clientId),
582
+ verifyClientSecret: (client, clientSecret) => server.verifyClientSecret(client, clientSecret),
583
+ createAuthCode: (request) => server.createAuthCode(request),
584
+ consumeAuthCode: (code, clientId) => server.consumeAuthCode(code, clientId),
585
+ canImpersonate: (params) => server.canImpersonate(params)
586
+ };
587
+ return this.serverAuthAdapter;
588
+ }
589
+ // AuthAdapter-compatible helpers (used by AuthModule)
450
590
  async getUser(identifier) {
451
591
  return this.userStoreAdapter ? this.userStoreAdapter.findUser(identifier) : null;
452
592
  }
@@ -466,36 +606,39 @@ export class ApiServer {
466
606
  if (this.tokenStoreAdapter) {
467
607
  return this.tokenStoreAdapter.save(data);
468
608
  }
469
- if (typeof this.storageAdapter.storeToken === 'function') {
470
- return this.storageAdapter.storeToken(data);
609
+ const storage = this.storageAdapter;
610
+ if (typeof storage.storeToken === 'function') {
611
+ return storage.storeToken(data);
471
612
  }
472
613
  throw new Error('Token store is not configured');
473
614
  }
474
615
  async getToken(query, opts) {
475
616
  const normalized = {
476
617
  ...query,
477
- userId: query.userId !== undefined && query.userId !== null ? String(query.userId) : undefined,
478
- ruid: query.ruid !== undefined && query.ruid !== null ? String(query.ruid) : undefined
618
+ userId: toOptionalStringId(query.userId),
619
+ ruid: toOptionalStringId(query.ruid)
479
620
  };
480
621
  if (this.tokenStoreAdapter) {
481
622
  return this.tokenStoreAdapter.get(normalized, opts);
482
623
  }
483
- if (typeof this.storageAdapter.getToken === 'function') {
484
- return this.storageAdapter.getToken(normalized, opts);
624
+ const storage = this.storageAdapter;
625
+ if (typeof storage.getToken === 'function') {
626
+ return storage.getToken(normalized, opts);
485
627
  }
486
628
  return null;
487
629
  }
488
630
  async deleteToken(query) {
489
631
  const normalized = {
490
632
  ...query,
491
- userId: query.userId !== undefined && query.userId !== null ? String(query.userId) : undefined,
492
- ruid: query.ruid !== undefined && query.ruid !== null ? String(query.ruid) : undefined
633
+ userId: toOptionalStringId(query.userId),
634
+ ruid: toOptionalStringId(query.ruid)
493
635
  };
494
636
  if (this.tokenStoreAdapter) {
495
637
  return this.tokenStoreAdapter.delete(normalized);
496
638
  }
497
- if (typeof this.storageAdapter.deleteToken === 'function') {
498
- return this.storageAdapter.deleteToken(normalized);
639
+ const storage = this.storageAdapter;
640
+ if (typeof storage.deleteToken === 'function') {
641
+ return storage.deleteToken(normalized);
499
642
  }
500
643
  return 0;
501
644
  }
@@ -568,12 +711,13 @@ export class ApiServer {
568
711
  if (this.tokenStoreAdapter) {
569
712
  return this.tokenStoreAdapter.update(updates);
570
713
  }
571
- if (typeof this.storageAdapter.updateToken === 'function') {
572
- return this.storageAdapter.updateToken(updates);
714
+ const storage = this.storageAdapter;
715
+ if (typeof storage.updateToken === 'function') {
716
+ return storage.updateToken(updates);
573
717
  }
574
718
  return false;
575
719
  }
576
- guessExceptionText(error, defMsg = 'Unkown Error') {
720
+ guessExceptionText(error, defMsg = 'Unknown Error') {
577
721
  return guess_exception_text(error, defMsg);
578
722
  }
579
723
  async authorize(apiReq, requiredClass) {
@@ -599,11 +743,47 @@ export class ApiServer {
599
743
  credentials: true
600
744
  };
601
745
  this.app.use(cors(corsOptions));
746
+ // Provide a consistent and non-500 response when requests are blocked by the CORS allowlist.
747
+ this.app.use((err, req, res, next) => {
748
+ const message = err instanceof Error ? err.message : '';
749
+ if (message.includes('Not allowed by CORS')) {
750
+ const isApiRequest = Boolean(req.originalUrl?.startsWith(this.apiBasePath));
751
+ if (isApiRequest) {
752
+ res.status(403).json({
753
+ success: false,
754
+ code: 403,
755
+ message: 'Origin not allowed by CORS',
756
+ data: null,
757
+ errors: {}
758
+ });
759
+ return;
760
+ }
761
+ res.status(403).send('Origin not allowed by CORS');
762
+ return;
763
+ }
764
+ next(err);
765
+ });
766
+ }
767
+ installStaticDirs() {
768
+ const staticDirs = this.config.staticDirs;
769
+ if (!staticDirs || !isPlainObject(staticDirs)) {
770
+ return;
771
+ }
772
+ for (const [mountRaw, dirRaw] of Object.entries(staticDirs)) {
773
+ const mount = typeof mountRaw === 'string' ? mountRaw.trim() : '';
774
+ const dir = typeof dirRaw === 'string' ? dirRaw.trim() : '';
775
+ if (!mount || !dir) {
776
+ continue;
777
+ }
778
+ const resolvedMount = mount.startsWith('/') ? mount : `/${mount}`;
779
+ this.app.use(resolvedMount, express.static(dir));
780
+ }
602
781
  }
603
782
  installPingHandler() {
604
783
  const path = `${this.apiBasePath}/v1/ping`;
605
784
  this.app.get(path, (_req, res) => {
606
785
  const payload = {
786
+ success: true,
607
787
  status: 'ok',
608
788
  apiVersion: this.config.apiVersion ?? '',
609
789
  minClientVersion: this.config.minClientVersion ?? '',
@@ -611,7 +791,66 @@ export class ApiServer {
611
791
  startedAt: this.startedAt,
612
792
  timestamp: new Date().toISOString()
613
793
  };
614
- res.status(200).json({ code: 200, message: 'Success', data: payload });
794
+ res.status(200).json({ success: true, code: 200, message: 'Success', data: payload, errors: {} });
795
+ });
796
+ }
797
+ async loadSwaggerSpec() {
798
+ const candidates = [path.resolve(process.cwd(), 'docs/swagger/openapi.json')];
799
+ if (typeof __dirname === 'string') {
800
+ candidates.push(path.resolve(__dirname, '../../docs/swagger/openapi.json'));
801
+ }
802
+ try {
803
+ const require = createRequire(path.join(process.cwd(), 'package.json'));
804
+ const entry = require.resolve('@technomoron/api-server-base');
805
+ const packageRoot = path.resolve(path.dirname(entry), '..', '..');
806
+ candidates.push(path.join(packageRoot, 'docs/swagger/openapi.json'));
807
+ }
808
+ catch {
809
+ // Ignore resolution failures; fall back to any existing candidate.
810
+ }
811
+ for (const candidate of candidates) {
812
+ try {
813
+ await access(candidate);
814
+ }
815
+ catch {
816
+ continue;
817
+ }
818
+ try {
819
+ const raw = await readFile(candidate, 'utf8');
820
+ return JSON.parse(raw);
821
+ }
822
+ catch {
823
+ return null;
824
+ }
825
+ }
826
+ return null;
827
+ }
828
+ installSwaggerHandler() {
829
+ const rawPath = typeof this.config.swaggerPath === 'string' ? this.config.swaggerPath.trim() : '';
830
+ const enabled = Boolean(this.config.swaggerEnabled) || rawPath.length > 0;
831
+ if (!enabled) {
832
+ return;
833
+ }
834
+ const base = this.apiBasePath === '/' ? '' : this.apiBasePath;
835
+ const resolved = rawPath.length > 0 ? rawPath : `${base}/swagger`;
836
+ const path = resolved.startsWith('/') ? resolved : `/${resolved}`;
837
+ let specPromise;
838
+ this.app.get(path, async (_req, res) => {
839
+ if (!specPromise) {
840
+ specPromise = this.loadSwaggerSpec();
841
+ }
842
+ const spec = await specPromise;
843
+ if (!spec) {
844
+ res.status(500).json({
845
+ success: false,
846
+ code: 500,
847
+ message: 'Swagger spec is unavailable',
848
+ data: null,
849
+ errors: {}
850
+ });
851
+ return;
852
+ }
853
+ res.status(200).json(spec);
615
854
  });
616
855
  }
617
856
  normalizeApiBasePath(path) {
@@ -634,6 +873,7 @@ export class ApiServer {
634
873
  }
635
874
  this.apiNotFoundHandler = (req, res) => {
636
875
  const payload = {
876
+ success: false,
637
877
  code: 404,
638
878
  message: this.describeMissingEndpoint(req),
639
879
  data: null,
@@ -643,21 +883,12 @@ export class ApiServer {
643
883
  };
644
884
  this.app.use(this.apiBasePath, this.apiNotFoundHandler);
645
885
  }
646
- ensureApiNotFoundOrdering() {
647
- this.installApiNotFoundHandler();
648
- if (!this.apiNotFoundHandler) {
649
- return;
650
- }
651
- const stack = this.app?._router?.stack;
652
- if (!stack) {
653
- return;
654
- }
655
- const index = stack.findIndex((layer) => layer.handle === this.apiNotFoundHandler);
656
- if (index === -1 || index === stack.length - 1) {
886
+ installApiErrorHandler() {
887
+ if (this.apiErrorHandlerInstalled) {
657
888
  return;
658
889
  }
659
- const [layer] = stack.splice(index, 1);
660
- stack.push(layer);
890
+ this.apiErrorHandlerInstalled = true;
891
+ this.app.use(this.apiBasePath, this.expressErrorHandler());
661
892
  }
662
893
  describeMissingEndpoint(req) {
663
894
  const method = typeof req.method === 'string' ? req.method.toUpperCase() : 'GET';
@@ -665,6 +896,10 @@ export class ApiServer {
665
896
  return `No such endpoint: ${method} ${target}`;
666
897
  }
667
898
  start() {
899
+ if (!this.finalized) {
900
+ console.warn('[api-server-base] ApiServer.start() called without finalize(); auto-finalizing. Call server.finalize() before start() to silence this warning.');
901
+ this.finalize();
902
+ }
668
903
  this.app
669
904
  .listen({
670
905
  port: this.config.apiPort,
@@ -674,34 +909,123 @@ export class ApiServer {
674
909
  console.log(`Server is running on http://${this.config.apiHost}:${this.config.apiPort}`);
675
910
  })
676
911
  .on('error', (error) => {
912
+ let message;
677
913
  if (error.code === 'EADDRINUSE') {
678
- console.error(`Error: Port ${this.config.apiPort} is already in use.`);
914
+ message = `Port ${this.config.apiPort} is already in use.`;
679
915
  }
680
916
  else if (error.code === 'EACCES') {
681
- console.error(`Error: Insufficient permissions to bind to port ${this.config.apiPort}. Try using a port number >= 1024 or running with elevated privileges.`);
917
+ message = `Insufficient permissions to bind to port ${this.config.apiPort}. Try using a port number >= 1024 or running with elevated privileges.`;
682
918
  }
683
919
  else if (error.code === 'EADDRNOTAVAIL') {
684
- console.error(`Error: Address ${this.config.apiHost} is not available on this machine.`);
920
+ message = `Address ${this.config.apiHost} is not available on this machine.`;
685
921
  }
686
922
  else {
687
- console.error(`Failed to start server: ${error.message}`);
923
+ message = `Failed to start server: ${error.message}`;
924
+ }
925
+ const err = new Error(message);
926
+ err.cause = error;
927
+ if (typeof this.config.onStartError === 'function') {
928
+ this.config.onStartError(err);
929
+ return;
688
930
  }
689
- process.exit(1);
931
+ throw err;
690
932
  });
691
933
  return this;
692
934
  }
935
+ internalServerErrorMessage(error) {
936
+ return this.config.debug ? this.guessExceptionText(error) : 'Internal server error';
937
+ }
693
938
  async verifyJWT(token) {
694
939
  if (!this.config.accessSecret) {
695
- return { tokenData: undefined, error: 'JWT authentication disabled; no jwtSecret set' };
940
+ return { tokenData: undefined, error: 'JWT authentication disabled; no jwtSecret set', expired: false };
696
941
  }
697
942
  const result = this.jwtVerify(token, this.config.accessSecret);
698
943
  if (!result.success) {
699
- return { tokenData: undefined, error: result.error };
944
+ return { tokenData: undefined, error: result.error, expired: result.expired };
700
945
  }
701
946
  if (!result.data.uid) {
702
- return { tokenData: undefined, error: 'Missing/bad userid in token' };
947
+ return { tokenData: undefined, error: 'Missing/bad userid in token', expired: false };
948
+ }
949
+ return { tokenData: result.data, error: undefined, expired: false };
950
+ }
951
+ jwtCookieOptions(apiReq) {
952
+ return buildAuthCookieOptions(this.config, apiReq.req);
953
+ }
954
+ setAccessCookie(apiReq, accessToken, sessionCookie) {
955
+ const conf = this.config;
956
+ const options = this.jwtCookieOptions(apiReq);
957
+ const accessMaxAge = Math.max(1, conf.accessExpiry) * 1000;
958
+ const accessOptions = sessionCookie ? options : { ...options, maxAge: accessMaxAge };
959
+ apiReq.res.cookie(conf.accessCookie, accessToken, accessOptions);
960
+ }
961
+ async tryRefreshAccessToken(apiReq) {
962
+ const conf = this.config;
963
+ if (!conf.refreshSecret || !conf.accessSecret) {
964
+ return null;
965
+ }
966
+ const rawRefresh = apiReq.req.cookies?.[conf.refreshCookie];
967
+ if (typeof rawRefresh !== 'string') {
968
+ return null;
969
+ }
970
+ const refreshToken = rawRefresh.trim();
971
+ if (!refreshToken) {
972
+ return null;
973
+ }
974
+ const verify = this.jwtVerify(refreshToken, conf.refreshSecret);
975
+ if (!verify.success || !verify.data) {
976
+ return null;
977
+ }
978
+ let stored = null;
979
+ try {
980
+ stored = await this.storageAdapter.getToken({ refreshToken });
703
981
  }
704
- return { tokenData: result.data, error: undefined };
982
+ catch {
983
+ return null;
984
+ }
985
+ if (!stored) {
986
+ return null;
987
+ }
988
+ const storedUid = String(stored.userId);
989
+ const verifyUid = verify.data.uid === undefined || verify.data.uid === null ? null : String(verify.data.uid);
990
+ if (verifyUid && verifyUid !== storedUid) {
991
+ return null;
992
+ }
993
+ const claims = verify.data;
994
+ const { exp: _exp, iat: _iat, nbf: _nbf, ...payload } = claims;
995
+ void _exp;
996
+ void _iat;
997
+ void _nbf;
998
+ // Ensure we never embed token secrets into refreshed access tokens.
999
+ delete payload.accessToken;
1000
+ delete payload.refreshToken;
1001
+ delete payload.userId;
1002
+ delete payload.expires;
1003
+ delete payload.issuedAt;
1004
+ delete payload.lastSeenAt;
1005
+ delete payload.status;
1006
+ const access = this.jwtSign(payload, conf.accessSecret, conf.accessExpiry);
1007
+ if (!access.success || !access.token) {
1008
+ return null;
1009
+ }
1010
+ const updated = await this.updateToken({
1011
+ refreshToken,
1012
+ accessToken: access.token,
1013
+ lastSeenAt: new Date()
1014
+ });
1015
+ if (!updated) {
1016
+ return null;
1017
+ }
1018
+ this.setAccessCookie(apiReq, access.token, stored.sessionCookie ?? false);
1019
+ if (apiReq.req.cookies) {
1020
+ apiReq.req.cookies[conf.accessCookie] = access.token;
1021
+ }
1022
+ const verifiedAccess = await this.verifyJWT(access.token);
1023
+ if (!verifiedAccess.tokenData) {
1024
+ return null;
1025
+ }
1026
+ const refreshedStored = { ...stored, accessToken: access.token };
1027
+ apiReq.authToken = refreshedStored;
1028
+ return { token: access.token, tokenData: verifiedAccess.tokenData, stored: refreshedStored };
705
1029
  }
706
1030
  async authenticate(apiReq, authType) {
707
1031
  if (authType === 'none') {
@@ -711,6 +1035,7 @@ export class ApiServer {
711
1035
  let token = null;
712
1036
  const authHeader = apiReq.req.headers.authorization;
713
1037
  const requiresAuthToken = this.requiresAuthToken(authType);
1038
+ const allowRefresh = requiresAuthToken || (authType === 'maybe' && this.config.refreshMaybe);
714
1039
  const apiKeyAuth = await this.tryAuthenticateApiKey(apiReq, authType, authHeader);
715
1040
  if (apiKeyAuth) {
716
1041
  return apiKeyAuth;
@@ -719,32 +1044,84 @@ export class ApiServer {
719
1044
  token = authHeader.slice(7).trim();
720
1045
  }
721
1046
  if (!token) {
722
- const access = apiReq.req.cookies?.dat;
1047
+ const access = apiReq.req.cookies?.[this.config.accessCookie];
723
1048
  if (access) {
724
1049
  token = access;
725
1050
  }
726
1051
  }
727
- if (!token || token === null) {
728
- if (requiresAuthToken) {
729
- throw new ApiError({ code: 401, message: 'Authorization token is required (Bearer/cookie)' });
730
- }
731
- }
1052
+ let tokenData;
1053
+ let error;
1054
+ let expired = false;
732
1055
  if (!token) {
733
1056
  if (authType === 'maybe') {
734
- return null;
1057
+ if (!this.config.refreshMaybe) {
1058
+ return null;
1059
+ }
1060
+ const refreshed = await this.tryRefreshAccessToken(apiReq);
1061
+ if (!refreshed) {
1062
+ return null;
1063
+ }
1064
+ token = refreshed.token;
1065
+ tokenData = refreshed.tokenData;
1066
+ error = undefined;
1067
+ expired = false;
735
1068
  }
736
- else {
737
- throw new ApiError({ code: 401, message: 'Unauthorized Access - requires authentication' });
1069
+ else if (requiresAuthToken) {
1070
+ const refreshed = await this.tryRefreshAccessToken(apiReq);
1071
+ if (!refreshed) {
1072
+ throw new ApiError({ code: 401, message: 'Authorization token is required (Bearer/cookie)' });
1073
+ }
1074
+ token = refreshed.token;
1075
+ tokenData = refreshed.tokenData;
1076
+ error = undefined;
1077
+ expired = false;
1078
+ }
1079
+ }
1080
+ if (!token) {
1081
+ throw new ApiError({ code: 401, message: 'Unauthorized Access - requires authentication' });
1082
+ }
1083
+ if (!tokenData) {
1084
+ const verified = await this.verifyJWT(token);
1085
+ tokenData = verified.tokenData;
1086
+ error = verified.error;
1087
+ expired = verified.expired ?? false;
1088
+ }
1089
+ if (!tokenData && allowRefresh && expired) {
1090
+ const refreshed = await this.tryRefreshAccessToken(apiReq);
1091
+ if (refreshed) {
1092
+ token = refreshed.token;
1093
+ tokenData = refreshed.tokenData;
1094
+ error = undefined;
738
1095
  }
739
1096
  }
740
- const { tokenData, error } = await this.verifyJWT(token);
741
1097
  if (!tokenData) {
742
- throw new ApiError({ code: 401, message: 'Unathorized Access - ' + error });
1098
+ throw new ApiError({ code: 401, message: 'Unauthorized Access - ' + error });
743
1099
  }
744
1100
  const effectiveUserId = this.extractTokenUserId(tokenData);
745
1101
  apiReq.realUid = this.resolveRealUserId(tokenData, effectiveUserId);
746
1102
  if (this.shouldValidateStoredToken(authType)) {
747
- await this.assertStoredAccessToken(apiReq, token, tokenData);
1103
+ try {
1104
+ await this.assertStoredAccessToken(apiReq, token, tokenData);
1105
+ }
1106
+ catch (error) {
1107
+ if (allowRefresh &&
1108
+ error instanceof ApiError &&
1109
+ error.code === 401 &&
1110
+ error.message === 'Authorization token is no longer valid') {
1111
+ const refreshed = await this.tryRefreshAccessToken(apiReq);
1112
+ if (!refreshed) {
1113
+ throw error;
1114
+ }
1115
+ token = refreshed.token;
1116
+ tokenData = refreshed.tokenData;
1117
+ const refreshedUserId = this.extractTokenUserId(tokenData);
1118
+ apiReq.realUid = this.resolveRealUserId(tokenData, refreshedUserId);
1119
+ await this.assertStoredAccessToken(apiReq, token, tokenData);
1120
+ }
1121
+ else {
1122
+ throw error;
1123
+ }
1124
+ }
748
1125
  }
749
1126
  apiReq.token = token;
750
1127
  return tokenData;
@@ -770,6 +1147,11 @@ export class ApiServer {
770
1147
  }
771
1148
  apiReq.token = secret;
772
1149
  apiReq.apiKey = key;
1150
+ // Treat API keys as authenticated identities, consistent with JWT-based flows.
1151
+ const resolvedUid = this.normalizeAuthIdentifier(key.uid);
1152
+ if (resolvedUid !== null) {
1153
+ apiReq.realUid = resolvedUid;
1154
+ }
773
1155
  return {
774
1156
  uid: key.uid,
775
1157
  domain: '',
@@ -786,6 +1168,9 @@ export class ApiServer {
786
1168
  }
787
1169
  async assertStoredAccessToken(apiReq, token, tokenData) {
788
1170
  const userId = String(this.extractTokenUserId(tokenData));
1171
+ if (apiReq.authToken && apiReq.authToken.accessToken === token && String(apiReq.authToken.userId) === userId) {
1172
+ return;
1173
+ }
789
1174
  const stored = await this.storageAdapter.getToken({
790
1175
  accessToken: token,
791
1176
  userId
@@ -820,38 +1205,118 @@ export class ApiServer {
820
1205
  if (rawReal === null) {
821
1206
  return effectiveUserId;
822
1207
  }
823
- if (typeof rawReal === 'number' && rawReal === 0) {
824
- return effectiveUserId;
825
- }
826
1208
  return rawReal;
827
1209
  }
828
- handle_request(handler, auth) {
1210
+ useExpress(pathOrHandler, ...handlers) {
1211
+ this.assertNotFinalized('useExpress');
1212
+ if (typeof pathOrHandler === 'string') {
1213
+ const apiPath = this.toApiRouterPath(pathOrHandler);
1214
+ if (apiPath) {
1215
+ this.apiRouter.use(apiPath, ...handlers);
1216
+ }
1217
+ else {
1218
+ this.app.use(pathOrHandler, ...handlers);
1219
+ }
1220
+ }
1221
+ else {
1222
+ this.app.use(pathOrHandler, ...handlers);
1223
+ }
1224
+ return this;
1225
+ }
1226
+ createApiRequest(req, res) {
1227
+ const apiReq = {
1228
+ server: this,
1229
+ req,
1230
+ res,
1231
+ token: '',
1232
+ tokenData: null,
1233
+ realUid: null,
1234
+ getClientInfo: () => ensureClientInfo(apiReq),
1235
+ getClientIp: () => ensureClientInfo(apiReq).ip,
1236
+ getClientIpChain: () => ensureClientInfo(apiReq).ipchain,
1237
+ getRealUid: () => apiReq.realUid ?? null,
1238
+ isImpersonating: () => {
1239
+ const realUid = apiReq.realUid;
1240
+ const tokenUid = apiReq.tokenData?.uid;
1241
+ if (realUid === null || realUid === undefined) {
1242
+ return false;
1243
+ }
1244
+ if (tokenUid === null || tokenUid === undefined) {
1245
+ return false;
1246
+ }
1247
+ return realUid !== tokenUid;
1248
+ }
1249
+ };
1250
+ return apiReq;
1251
+ }
1252
+ expressAuth(auth) {
829
1253
  return async (req, res, next) => {
830
- void next;
831
- const apiReq = {
832
- server: this,
833
- req,
834
- res,
835
- token: '',
836
- tokenData: null,
837
- realUid: null,
838
- getClientInfo: () => ensureClientInfo(apiReq),
839
- getClientIp: () => ensureClientInfo(apiReq).ip,
840
- getClientIpChain: () => ensureClientInfo(apiReq).ipchain,
841
- getRealUid: () => apiReq.realUid ?? null,
842
- isImpersonating: () => {
843
- const realUid = apiReq.realUid;
844
- const tokenUid = apiReq.tokenData?.uid;
845
- if (realUid === null || realUid === undefined) {
846
- return false;
847
- }
848
- if (tokenUid === null || tokenUid === undefined) {
849
- return false;
850
- }
851
- return realUid !== tokenUid;
1254
+ const apiReq = this.createApiRequest(req, res);
1255
+ req.apiReq = apiReq;
1256
+ res.locals.apiReq = apiReq;
1257
+ try {
1258
+ if (this.config.hydrateGetBody) {
1259
+ hydrateGetBody(req);
852
1260
  }
1261
+ if (this.config.debug) {
1262
+ this.dumpRequest(apiReq);
1263
+ }
1264
+ apiReq.tokenData = await this.authenticate(apiReq, auth.type);
1265
+ await this.authorize(apiReq, auth.req);
1266
+ next();
1267
+ }
1268
+ catch (error) {
1269
+ next(error);
1270
+ }
1271
+ };
1272
+ }
1273
+ expressErrorHandler() {
1274
+ return (error, _req, res, next) => {
1275
+ void _req;
1276
+ if (res.headersSent) {
1277
+ next(error);
1278
+ return;
1279
+ }
1280
+ if (error instanceof ApiError || isApiErrorLike(error)) {
1281
+ const apiError = error;
1282
+ const normalizedErrors = apiError.errors && typeof apiError.errors === 'object' && !Array.isArray(apiError.errors)
1283
+ ? apiError.errors
1284
+ : {};
1285
+ const errorPayload = {
1286
+ success: false,
1287
+ code: apiError.code,
1288
+ message: apiError.message,
1289
+ data: apiError.data ?? null,
1290
+ errors: normalizedErrors
1291
+ };
1292
+ res.status(apiError.code).json(errorPayload);
1293
+ return;
1294
+ }
1295
+ const status = asHttpStatus(error);
1296
+ if (status) {
1297
+ res.status(status).json({
1298
+ success: false,
1299
+ code: status,
1300
+ message: status === 400 ? 'Invalid request payload' : this.internalServerErrorMessage(error),
1301
+ data: null,
1302
+ errors: {}
1303
+ });
1304
+ return;
1305
+ }
1306
+ const errorPayload = {
1307
+ success: false,
1308
+ code: 500,
1309
+ message: this.internalServerErrorMessage(error),
1310
+ data: null,
1311
+ errors: {}
853
1312
  };
854
- this.currReq = apiReq;
1313
+ res.status(500).json(errorPayload);
1314
+ };
1315
+ }
1316
+ handle_request(handler, auth) {
1317
+ return async (req, res, next) => {
1318
+ void next;
1319
+ const apiReq = this.createApiRequest(req, res);
855
1320
  try {
856
1321
  if (this.config.hydrateGetBody) {
857
1322
  hydrateGetBody(apiReq.req);
@@ -873,7 +1338,7 @@ export class ApiServer {
873
1338
  throw new ApiError({ code: 500, message: 'Handler result must start with a numeric status code' });
874
1339
  }
875
1340
  const message = typeof rawMessage === 'string' ? rawMessage : 'Success';
876
- const responsePayload = { code, message, data };
1341
+ const responsePayload = { success: true, code, message, data, errors: {} };
877
1342
  if (this.config.debug) {
878
1343
  this.dumpResponse(apiReq, responsePayload, code);
879
1344
  }
@@ -886,6 +1351,7 @@ export class ApiServer {
886
1351
  ? apiError.errors
887
1352
  : {};
888
1353
  const errorPayload = {
1354
+ success: false,
889
1355
  code: apiError.code,
890
1356
  message: apiError.message,
891
1357
  data: apiError.data ?? null,
@@ -898,8 +1364,9 @@ export class ApiServer {
898
1364
  }
899
1365
  else {
900
1366
  const errorPayload = {
1367
+ success: false,
901
1368
  code: 500,
902
- message: this.guessExceptionText(error),
1369
+ message: this.internalServerErrorMessage(error),
903
1370
  data: null,
904
1371
  errors: {}
905
1372
  };
@@ -912,25 +1379,46 @@ export class ApiServer {
912
1379
  };
913
1380
  }
914
1381
  api(module) {
1382
+ this.assertNotFinalized('api');
915
1383
  const router = express.Router();
916
1384
  module.server = this;
917
- if (module?.moduleType === 'auth') {
1385
+ const moduleType = module.moduleType;
1386
+ if (moduleType === 'auth') {
918
1387
  this.authModule(module);
919
1388
  }
920
- module.checkConfig();
1389
+ const configOk = module.checkConfig();
1390
+ if (configOk === false) {
1391
+ const name = module.constructor?.name ? String(module.constructor.name) : 'ApiModule';
1392
+ throw new Error(`${name}.checkConfig() returned false`);
1393
+ }
921
1394
  const base = this.apiBasePath;
922
1395
  const ns = module.namespace;
923
1396
  const mountPath = `${base}${ns}`;
924
1397
  module.mountpath = mountPath;
925
1398
  module.defineRoutes().forEach((r) => {
926
1399
  const handler = this.handle_request(r.handler, r.auth);
927
- router[r.method](r.path, handler);
1400
+ switch (r.method) {
1401
+ case 'get':
1402
+ router.get(r.path, handler);
1403
+ break;
1404
+ case 'post':
1405
+ router.post(r.path, handler);
1406
+ break;
1407
+ case 'put':
1408
+ router.put(r.path, handler);
1409
+ break;
1410
+ case 'patch':
1411
+ router.patch(r.path, handler);
1412
+ break;
1413
+ case 'delete':
1414
+ router.delete(r.path, handler);
1415
+ break;
1416
+ }
928
1417
  if (this.config.debug) {
929
1418
  console.log(`Adding ${mountPath}${r.path} (${r.method.toUpperCase()})`);
930
1419
  }
931
1420
  });
932
- this.app.use(mountPath, router);
933
- this.ensureApiNotFoundOrdering();
1421
+ this.apiRouter.use(ns, router);
934
1422
  return this;
935
1423
  }
936
1424
  dumpRequest(apiReq) {
@@ -940,9 +1428,29 @@ export class ApiServer {
940
1428
  console.log('URL:', url);
941
1429
  console.log('Method:', req.method);
942
1430
  console.log('Query Params:', req.query || {});
943
- console.log('Body Params:', req.body || {});
944
- console.log('Cookies:', req.cookies || {});
945
- console.log('Headers:', req.headers);
1431
+ const sensitiveBodyKeys = ['password', 'client_secret', 'clientSecret', 'secret'];
1432
+ const body = req.body && typeof req.body === 'object' ? { ...req.body } : req.body;
1433
+ if (body && typeof body === 'object') {
1434
+ for (const key of sensitiveBodyKeys) {
1435
+ if (key in body) {
1436
+ body[key] = '[REDACTED]';
1437
+ }
1438
+ }
1439
+ }
1440
+ console.log('Body Params:', body || {});
1441
+ const cookies = req.cookies ? { ...req.cookies } : {};
1442
+ const sensitiveCookieKeys = [this.config.accessCookie, this.config.refreshCookie];
1443
+ for (const key of sensitiveCookieKeys) {
1444
+ if (key in cookies) {
1445
+ cookies[key] = '[REDACTED]';
1446
+ }
1447
+ }
1448
+ console.log('Cookies:', cookies);
1449
+ const headers = { ...req.headers };
1450
+ if (headers.authorization) {
1451
+ headers.authorization = '[REDACTED]';
1452
+ }
1453
+ console.log('Headers:', headers);
946
1454
  console.log('------------------------');
947
1455
  }
948
1456
  formatDebugValue(value, maxLength = 50, seen = new WeakSet()) {