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