@technomoron/api-server-base 1.1.13 → 2.0.0-beta.10

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 (116) hide show
  1. package/README.txt +25 -2
  2. package/dist/cjs/api-server-base.cjs +448 -111
  3. package/dist/cjs/api-server-base.d.ts +91 -34
  4. package/dist/cjs/auth-api/auth-module.d.ts +105 -0
  5. package/dist/cjs/auth-api/auth-module.js +1180 -0
  6. package/dist/cjs/auth-api/compat-auth-storage.d.ts +57 -0
  7. package/dist/cjs/auth-api/compat-auth-storage.js +128 -0
  8. package/dist/cjs/auth-api/mem-auth-store.d.ts +68 -0
  9. package/dist/cjs/auth-api/mem-auth-store.js +141 -0
  10. package/dist/cjs/{auth-module.d.ts → auth-api/module.d.ts} +7 -7
  11. package/dist/cjs/{auth-module.cjs → auth-api/module.js} +1 -1
  12. package/dist/cjs/auth-api/sql-auth-store.d.ts +77 -0
  13. package/dist/cjs/auth-api/sql-auth-store.js +172 -0
  14. package/dist/cjs/auth-api/storage.d.ts +38 -0
  15. package/dist/cjs/{auth-storage.cjs → auth-api/storage.js} +17 -7
  16. package/dist/cjs/auth-api/types.d.ts +34 -0
  17. package/dist/cjs/auth-api/types.js +2 -0
  18. package/dist/cjs/index.cjs +41 -7
  19. package/dist/cjs/index.d.ts +29 -5
  20. package/dist/cjs/oauth/base.d.ts +10 -0
  21. package/dist/cjs/oauth/base.js +6 -0
  22. package/dist/cjs/oauth/memory.d.ts +16 -0
  23. package/dist/cjs/oauth/memory.js +99 -0
  24. package/dist/cjs/oauth/models.d.ts +45 -0
  25. package/dist/cjs/oauth/models.js +58 -0
  26. package/dist/cjs/oauth/sequelize.d.ts +68 -0
  27. package/dist/cjs/oauth/sequelize.js +210 -0
  28. package/dist/cjs/oauth/types.d.ts +50 -0
  29. package/dist/cjs/oauth/types.js +3 -0
  30. package/dist/cjs/passkey/base.d.ts +16 -0
  31. package/dist/cjs/passkey/base.js +6 -0
  32. package/dist/cjs/passkey/memory.d.ts +27 -0
  33. package/dist/cjs/passkey/memory.js +86 -0
  34. package/dist/cjs/passkey/models.d.ts +25 -0
  35. package/dist/cjs/passkey/models.js +115 -0
  36. package/dist/cjs/passkey/sequelize.d.ts +55 -0
  37. package/dist/cjs/passkey/sequelize.js +220 -0
  38. package/dist/cjs/passkey/service.d.ts +20 -0
  39. package/dist/cjs/passkey/service.js +356 -0
  40. package/dist/cjs/passkey/types.d.ts +78 -0
  41. package/dist/cjs/passkey/types.js +2 -0
  42. package/dist/cjs/token/base.d.ts +38 -0
  43. package/dist/cjs/token/base.js +114 -0
  44. package/dist/cjs/token/memory.d.ts +19 -0
  45. package/dist/cjs/token/memory.js +149 -0
  46. package/dist/cjs/token/sequelize.d.ts +58 -0
  47. package/dist/cjs/token/sequelize.js +404 -0
  48. package/dist/cjs/token/types.d.ts +27 -0
  49. package/dist/cjs/token/types.js +2 -0
  50. package/dist/cjs/user/base.d.ts +26 -0
  51. package/dist/cjs/user/base.js +45 -0
  52. package/dist/cjs/user/memory.d.ts +35 -0
  53. package/dist/cjs/user/memory.js +173 -0
  54. package/dist/cjs/user/sequelize.d.ts +41 -0
  55. package/dist/cjs/user/sequelize.js +182 -0
  56. package/dist/cjs/user/types.d.ts +11 -0
  57. package/dist/cjs/user/types.js +2 -0
  58. package/dist/esm/api-server-base.d.ts +91 -34
  59. package/dist/esm/api-server-base.js +447 -110
  60. package/dist/esm/auth-api/auth-module.d.ts +105 -0
  61. package/dist/esm/auth-api/auth-module.js +1178 -0
  62. package/dist/esm/auth-api/compat-auth-storage.d.ts +57 -0
  63. package/dist/esm/auth-api/compat-auth-storage.js +124 -0
  64. package/dist/esm/auth-api/mem-auth-store.d.ts +68 -0
  65. package/dist/esm/auth-api/mem-auth-store.js +137 -0
  66. package/dist/esm/{auth-module.d.ts → auth-api/module.d.ts} +7 -7
  67. package/dist/esm/{auth-module.js → auth-api/module.js} +1 -1
  68. package/dist/esm/auth-api/sql-auth-store.d.ts +77 -0
  69. package/dist/esm/auth-api/sql-auth-store.js +168 -0
  70. package/dist/esm/auth-api/storage.d.ts +38 -0
  71. package/dist/esm/{auth-storage.js → auth-api/storage.js} +15 -5
  72. package/dist/esm/auth-api/types.d.ts +34 -0
  73. package/dist/esm/auth-api/types.js +1 -0
  74. package/dist/esm/index.d.ts +29 -5
  75. package/dist/esm/index.js +19 -2
  76. package/dist/esm/oauth/base.d.ts +10 -0
  77. package/dist/esm/oauth/base.js +2 -0
  78. package/dist/esm/oauth/memory.d.ts +16 -0
  79. package/dist/esm/oauth/memory.js +92 -0
  80. package/dist/esm/oauth/models.d.ts +45 -0
  81. package/dist/esm/oauth/models.js +51 -0
  82. package/dist/esm/oauth/sequelize.d.ts +68 -0
  83. package/dist/esm/oauth/sequelize.js +199 -0
  84. package/dist/esm/oauth/types.d.ts +50 -0
  85. package/dist/esm/oauth/types.js +2 -0
  86. package/dist/esm/passkey/base.d.ts +16 -0
  87. package/dist/esm/passkey/base.js +2 -0
  88. package/dist/esm/passkey/memory.d.ts +27 -0
  89. package/dist/esm/passkey/memory.js +82 -0
  90. package/dist/esm/passkey/models.d.ts +25 -0
  91. package/dist/esm/passkey/models.js +108 -0
  92. package/dist/esm/passkey/sequelize.d.ts +55 -0
  93. package/dist/esm/passkey/sequelize.js +216 -0
  94. package/dist/esm/passkey/service.d.ts +20 -0
  95. package/dist/esm/passkey/service.js +319 -0
  96. package/dist/esm/passkey/types.d.ts +78 -0
  97. package/dist/esm/passkey/types.js +1 -0
  98. package/dist/esm/token/base.d.ts +38 -0
  99. package/dist/esm/token/base.js +107 -0
  100. package/dist/esm/token/memory.d.ts +19 -0
  101. package/dist/esm/token/memory.js +145 -0
  102. package/dist/esm/token/sequelize.d.ts +58 -0
  103. package/dist/esm/token/sequelize.js +400 -0
  104. package/dist/esm/token/types.d.ts +27 -0
  105. package/dist/esm/token/types.js +1 -0
  106. package/dist/esm/user/base.d.ts +26 -0
  107. package/dist/esm/user/base.js +38 -0
  108. package/dist/esm/user/memory.d.ts +35 -0
  109. package/dist/esm/user/memory.js +169 -0
  110. package/dist/esm/user/sequelize.d.ts +41 -0
  111. package/dist/esm/user/sequelize.js +176 -0
  112. package/dist/esm/user/types.d.ts +11 -0
  113. package/dist/esm/user/types.js +1 -0
  114. package/package.json +13 -3
  115. package/dist/cjs/auth-storage.d.ts +0 -133
  116. package/dist/esm/auth-storage.d.ts +0 -133
@@ -10,13 +10,34 @@ 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");
13
14
  const cookie_parser_1 = __importDefault(require("cookie-parser"));
14
15
  const cors_1 = __importDefault(require("cors"));
15
16
  const express_1 = __importDefault(require("express"));
16
- const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
17
17
  const multer_1 = __importDefault(require("multer"));
18
- const auth_module_js_1 = require("./auth-module.cjs");
19
- const auth_storage_js_1 = require("./auth-storage.cjs");
18
+ const module_js_1 = require("./auth-api/module.js");
19
+ const storage_js_1 = require("./auth-api/storage.js");
20
+ const base_js_1 = require("./token/base.js");
21
+ class JwtHelperStore extends base_js_1.TokenStore {
22
+ async save() {
23
+ throw new Error('Token store is not configured');
24
+ }
25
+ async get() {
26
+ throw new Error('Token store is not configured');
27
+ }
28
+ async delete() {
29
+ throw new Error('Token store is not configured');
30
+ }
31
+ async update() {
32
+ throw new Error('Token store is not configured');
33
+ }
34
+ async list() {
35
+ return [];
36
+ }
37
+ async close() {
38
+ return;
39
+ }
40
+ }
20
41
  var api_module_js_1 = require("./api-module.cjs");
21
42
  Object.defineProperty(exports, "ApiModule", { enumerable: true, get: function () { return api_module_js_1.ApiModule; } });
22
43
  function guess_exception_text(error, defMsg = 'Unknown Error') {
@@ -331,19 +352,38 @@ function fillConfig(config) {
331
352
  devMode: config.devMode ?? false,
332
353
  hydrateGetBody: config.hydrateGetBody ?? true,
333
354
  validateTokens: config.validateTokens ?? false,
355
+ refreshMaybe: config.refreshMaybe ?? false,
334
356
  apiVersion: config.apiVersion ?? '',
335
- minClientVersion: config.minClientVersion ?? ''
357
+ minClientVersion: config.minClientVersion ?? '',
358
+ tokenStore: config.tokenStore,
359
+ authStores: config.authStores
336
360
  };
337
361
  }
338
362
  class ApiServer {
339
363
  constructor(config = {}) {
340
364
  this.currReq = null;
341
365
  this.apiNotFoundHandler = null;
366
+ this.tokenStoreAdapter = null;
367
+ this.userStoreAdapter = null;
368
+ this.passkeyServiceAdapter = null;
369
+ this.oauthStoreAdapter = null;
370
+ this.canImpersonateAdapter = null;
342
371
  this.config = fillConfig(config);
343
372
  this.apiBasePath = this.normalizeApiBasePath(this.config.apiBasePath);
344
373
  this.startedAt = Date.now();
345
- this.storageAdapter = auth_storage_js_1.nullAuthStorage;
346
- this.moduleAdapter = auth_module_js_1.nullAuthModule;
374
+ this.storageAdapter = storage_js_1.nullAuthAdapter;
375
+ this.moduleAdapter = module_js_1.nullAuthModule;
376
+ this.jwtHelper = new JwtHelperStore();
377
+ this.tokenStoreAdapter = this.config.tokenStore ?? null;
378
+ if (this.config.authStores) {
379
+ const { userStore, tokenStore, passkeyService, oauthStore, canImpersonate } = this.config.authStores;
380
+ this.userStoreAdapter = userStore;
381
+ this.tokenStoreAdapter = tokenStore;
382
+ this.passkeyServiceAdapter = passkeyService ?? null;
383
+ this.oauthStoreAdapter = oauthStore ?? null;
384
+ this.canImpersonateAdapter = canImpersonate ?? null;
385
+ this.storageAdapter = this;
386
+ }
347
387
  this.app = (0, express_1.default)();
348
388
  if (config.uploadPath) {
349
389
  const upload = (0, multer_1.default)({ dest: config.uploadPath });
@@ -380,72 +420,149 @@ class ApiServer {
380
420
  getAuthModule() {
381
421
  return this.moduleAdapter;
382
422
  }
383
- jwtSign(payload, secret, expiresInSeconds, options) {
384
- options || (options = {});
385
- const opts = { ...options, expiresIn: expiresInSeconds };
386
- try {
387
- const token = jsonwebtoken_1.default.sign(payload, secret, opts);
388
- return {
389
- success: true,
390
- token
391
- };
423
+ setTokenStore(store) {
424
+ this.tokenStoreAdapter = store;
425
+ // If using direct stores, expose self as the auth storage.
426
+ if (this.userStoreAdapter) {
427
+ this.storageAdapter = this;
392
428
  }
393
- catch (error) {
394
- return {
395
- success: false,
396
- error: error instanceof Error ? error.message : String(error)
397
- };
429
+ return this;
430
+ }
431
+ getTokenStore() {
432
+ return this.tokenStoreAdapter;
433
+ }
434
+ ensureUserStore() {
435
+ if (!this.userStoreAdapter) {
436
+ throw new Error('User store is not configured');
398
437
  }
438
+ return this.userStoreAdapter;
399
439
  }
400
- jwtVerify(token, secret, options) {
401
- options || (options = {});
402
- try {
403
- const data = jsonwebtoken_1.default.verify(token, secret, options);
404
- return {
405
- success: true,
406
- data
407
- };
440
+ ensureTokenStore() {
441
+ if (!this.tokenStoreAdapter) {
442
+ throw new Error('Token store is not configured');
408
443
  }
409
- catch (error) {
410
- if (error instanceof jsonwebtoken_1.default.TokenExpiredError) {
411
- return {
412
- success: false,
413
- expired: true,
414
- error: 'Token expired'
415
- };
416
- }
417
- else {
418
- return {
419
- success: false,
420
- expired: false,
421
- error: error instanceof Error ? error.message : String(error)
422
- };
423
- }
444
+ return this.tokenStoreAdapter;
445
+ }
446
+ ensurePasskeyService() {
447
+ if (!this.passkeyServiceAdapter) {
448
+ throw new Error('Passkey service is not configured');
424
449
  }
450
+ return this.passkeyServiceAdapter;
425
451
  }
426
- jwtDecode(token, options) {
427
- options || (options = {});
428
- try {
429
- const data = jsonwebtoken_1.default.decode(token, options);
430
- // jwt.decode returns null for invalid tokens rather than throwing
431
- if (data === null) {
432
- return {
433
- success: false,
434
- error: 'Invalid token format'
435
- };
436
- }
437
- return {
438
- success: true,
439
- data
440
- };
452
+ async listUserCredentials(userId) {
453
+ return this.ensurePasskeyService().listUserCredentials(userId);
454
+ }
455
+ async deletePasskeyCredential(credentialId) {
456
+ return this.ensurePasskeyService().deleteCredential(credentialId);
457
+ }
458
+ ensureOAuthStore() {
459
+ if (!this.oauthStoreAdapter) {
460
+ throw new Error('OAuth store is not configured');
441
461
  }
442
- catch (error) {
443
- // jwt.decode rarely throws, but might for severely malformed tokens
444
- return {
445
- success: false,
446
- error: error instanceof Error ? error.message : String(error)
447
- };
462
+ return this.oauthStoreAdapter;
463
+ }
464
+ // AuthAdapter-compatible helpers (used by AuthModule)
465
+ async getUser(identifier) {
466
+ return this.userStoreAdapter ? this.userStoreAdapter.findUser(identifier) : null;
467
+ }
468
+ getUserPasswordHash(user) {
469
+ return this.ensureUserStore().getPasswordHash(user) ?? '';
470
+ }
471
+ getUserId(user) {
472
+ return this.ensureUserStore().getUserId(user);
473
+ }
474
+ filterUser(user) {
475
+ return this.ensureUserStore().toPublic(user);
476
+ }
477
+ async verifyPassword(password, hash) {
478
+ return this.ensureUserStore().verifyPassword(password, hash);
479
+ }
480
+ async storeToken(data) {
481
+ if (this.tokenStoreAdapter) {
482
+ return this.tokenStoreAdapter.save(data);
448
483
  }
484
+ if (typeof this.storageAdapter.storeToken === 'function') {
485
+ return this.storageAdapter.storeToken(data);
486
+ }
487
+ throw new Error('Token store is not configured');
488
+ }
489
+ async getToken(query, opts) {
490
+ const normalized = {
491
+ ...query,
492
+ userId: query.userId !== undefined && query.userId !== null ? String(query.userId) : undefined,
493
+ ruid: query.ruid !== undefined && query.ruid !== null ? String(query.ruid) : undefined
494
+ };
495
+ if (this.tokenStoreAdapter) {
496
+ return this.tokenStoreAdapter.get(normalized, opts);
497
+ }
498
+ if (typeof this.storageAdapter.getToken === 'function') {
499
+ return this.storageAdapter.getToken(normalized, opts);
500
+ }
501
+ return null;
502
+ }
503
+ async deleteToken(query) {
504
+ const normalized = {
505
+ ...query,
506
+ userId: query.userId !== undefined && query.userId !== null ? String(query.userId) : undefined,
507
+ ruid: query.ruid !== undefined && query.ruid !== null ? String(query.ruid) : undefined
508
+ };
509
+ if (this.tokenStoreAdapter) {
510
+ return this.tokenStoreAdapter.delete(normalized);
511
+ }
512
+ if (typeof this.storageAdapter.deleteToken === 'function') {
513
+ return this.storageAdapter.deleteToken(normalized);
514
+ }
515
+ return 0;
516
+ }
517
+ async createPasskeyChallenge(params) {
518
+ return this.ensurePasskeyService().createChallenge(params);
519
+ }
520
+ async verifyPasskeyResponse(params) {
521
+ return this.ensurePasskeyService().verifyResponse(params);
522
+ }
523
+ async getClient(clientId) {
524
+ return this.oauthStoreAdapter ? this.oauthStoreAdapter.getClient(clientId) : null;
525
+ }
526
+ async verifyClientSecret(client, clientSecret) {
527
+ return this.ensureOAuthStore().verifyClientSecret(client.clientId, clientSecret);
528
+ }
529
+ async createAuthCode(request) {
530
+ const expiresAt = new Date(Date.now() + (request.expiresInSeconds ?? 300) * 1000);
531
+ const code = request.code ?? (0, node_crypto_1.randomUUID)();
532
+ await this.ensureOAuthStore().createAuthCode({ ...request, code, expiresAt });
533
+ return {
534
+ code,
535
+ clientId: request.clientId,
536
+ userId: request.userId,
537
+ redirectUri: request.redirectUri,
538
+ scope: request.scope ?? [],
539
+ codeChallenge: request.codeChallenge,
540
+ codeChallengeMethod: request.codeChallengeMethod,
541
+ expiresAt,
542
+ metadata: request.metadata
543
+ };
544
+ }
545
+ async consumeAuthCode(code, clientId) {
546
+ const consumed = await this.ensureOAuthStore().consumeAuthCode(code);
547
+ if (!consumed || consumed.clientId !== clientId) {
548
+ return null;
549
+ }
550
+ return consumed;
551
+ }
552
+ async canImpersonate(params) {
553
+ if (this.canImpersonateAdapter) {
554
+ return !!(await this.canImpersonateAdapter(params));
555
+ }
556
+ return params.realUserId === params.effectiveUserId;
557
+ }
558
+ jwtSign(payload, secret, expiresInSeconds, options) {
559
+ return (this.tokenStoreAdapter ?? this.jwtHelper).jwtSign(payload, secret, expiresInSeconds, options);
560
+ }
561
+ jwtVerify(token, secret, options) {
562
+ return (this.tokenStoreAdapter ?? this.jwtHelper).jwtVerify(token, secret, options);
563
+ }
564
+ jwtDecode(token, options) {
565
+ return (this.tokenStoreAdapter ?? this.jwtHelper).jwtDecode(token, options);
449
566
  }
450
567
  async getApiKey(token) {
451
568
  void token;
@@ -463,16 +580,13 @@ class ApiServer {
463
580
  return this.storageAdapter.verifyPassword(params.password, hash);
464
581
  }
465
582
  async updateToken(updates) {
466
- if (typeof this.storageAdapter.updateToken !== 'function') {
467
- return false;
583
+ if (this.tokenStoreAdapter) {
584
+ return this.tokenStoreAdapter.update(updates);
468
585
  }
469
- return this.storageAdapter.updateToken({
470
- refreshToken: updates.refreshToken,
471
- access: updates.accessToken,
472
- expires: updates.expires,
473
- clientId: updates.clientId,
474
- scope: updates.scope
475
- });
586
+ if (typeof this.storageAdapter.updateToken === 'function') {
587
+ return this.storageAdapter.updateToken(updates);
588
+ }
589
+ return false;
476
590
  }
477
591
  guessExceptionText(error, defMsg = 'Unkown Error') {
478
592
  return guess_exception_text(error, defMsg);
@@ -593,16 +707,117 @@ class ApiServer {
593
707
  }
594
708
  async verifyJWT(token) {
595
709
  if (!this.config.accessSecret) {
596
- return { tokenData: undefined, error: 'JWT authentication disabled; no jwtSecret set' };
710
+ return { tokenData: undefined, error: 'JWT authentication disabled; no jwtSecret set', expired: false };
597
711
  }
598
712
  const result = this.jwtVerify(token, this.config.accessSecret);
599
713
  if (!result.success) {
600
- return { tokenData: undefined, error: result.error };
714
+ return { tokenData: undefined, error: result.error, expired: result.expired };
601
715
  }
602
716
  if (!result.data.uid) {
603
- return { tokenData: undefined, error: 'Missing/bad userid in token' };
717
+ return { tokenData: undefined, error: 'Missing/bad userid in token', expired: false };
718
+ }
719
+ return { tokenData: result.data, error: undefined, expired: false };
720
+ }
721
+ jwtCookieOptions(apiReq) {
722
+ const conf = this.config;
723
+ const forwarded = apiReq.req.headers['x-forwarded-proto'];
724
+ const referer = apiReq.req.headers['origin'] ?? apiReq.req.headers['referer'];
725
+ const origin = typeof referer === 'string' ? referer : '';
726
+ const isHttps = forwarded === 'https' || apiReq.req.protocol === 'https';
727
+ const isLocalhost = origin.includes('localhost');
728
+ const options = {
729
+ httpOnly: true,
730
+ secure: true,
731
+ sameSite: 'strict',
732
+ domain: conf.cookieDomain || undefined,
733
+ path: '/',
734
+ maxAge: undefined
735
+ };
736
+ if (conf.devMode) {
737
+ options.secure = isHttps;
738
+ options.httpOnly = false;
739
+ options.sameSite = 'lax';
740
+ if (isLocalhost) {
741
+ options.domain = undefined;
742
+ }
743
+ }
744
+ return options;
745
+ }
746
+ setAccessCookie(apiReq, accessToken, sessionCookie) {
747
+ const conf = this.config;
748
+ const options = this.jwtCookieOptions(apiReq);
749
+ const accessMaxAge = Math.max(1, conf.accessExpiry) * 1000;
750
+ const accessOptions = sessionCookie ? options : { ...options, maxAge: accessMaxAge };
751
+ apiReq.res.cookie(conf.accessCookie, accessToken, accessOptions);
752
+ }
753
+ async tryRefreshAccessToken(apiReq) {
754
+ const conf = this.config;
755
+ if (!conf.refreshSecret || !conf.accessSecret) {
756
+ return null;
757
+ }
758
+ const rawRefresh = apiReq.req.cookies?.[conf.refreshCookie];
759
+ if (typeof rawRefresh !== 'string') {
760
+ return null;
604
761
  }
605
- return { tokenData: result.data, error: undefined };
762
+ const refreshToken = rawRefresh.trim();
763
+ if (!refreshToken) {
764
+ return null;
765
+ }
766
+ const verify = this.jwtVerify(refreshToken, conf.refreshSecret);
767
+ if (!verify.success || !verify.data) {
768
+ return null;
769
+ }
770
+ let stored = null;
771
+ try {
772
+ stored = await this.storageAdapter.getToken({ refreshToken });
773
+ }
774
+ catch {
775
+ return null;
776
+ }
777
+ if (!stored) {
778
+ return null;
779
+ }
780
+ const storedUid = String(stored.userId);
781
+ const verifyUid = verify.data.uid === undefined || verify.data.uid === null ? null : String(verify.data.uid);
782
+ if (verifyUid && verifyUid !== storedUid) {
783
+ return null;
784
+ }
785
+ const claims = verify.data;
786
+ const { exp: _exp, iat: _iat, nbf: _nbf, ...payload } = claims;
787
+ void _exp;
788
+ void _iat;
789
+ void _nbf;
790
+ // Ensure we never embed token secrets into refreshed access tokens.
791
+ delete payload.accessToken;
792
+ delete payload.refreshToken;
793
+ delete payload.userId;
794
+ delete payload.expires;
795
+ delete payload.issuedAt;
796
+ delete payload.lastSeenAt;
797
+ delete payload.status;
798
+ const access = this.jwtSign(payload, conf.accessSecret, conf.accessExpiry);
799
+ if (!access.success || !access.token) {
800
+ return null;
801
+ }
802
+ const updated = await this.updateToken({
803
+ refreshToken,
804
+ accessToken: access.token,
805
+ lastSeenAt: new Date()
806
+ });
807
+ if (!updated) {
808
+ return null;
809
+ }
810
+ this.setAccessCookie(apiReq, access.token, stored.sessionCookie ?? false);
811
+ if (apiReq.req.cookies) {
812
+ apiReq.req.cookies[conf.accessCookie] = access.token;
813
+ }
814
+ const verifiedAccess = await this.verifyJWT(access.token);
815
+ if (!verifiedAccess.tokenData) {
816
+ return null;
817
+ }
818
+ const refreshedStored = { ...stored, accessToken: access.token };
819
+ apiReq.authToken = refreshedStored;
820
+ return { token: access.token, tokenData: verifiedAccess.tokenData, stored: refreshedStored };
606
821
  }
607
822
  async authenticate(apiReq, authType) {
608
823
  if (authType === 'none') {
@@ -612,6 +827,7 @@ class ApiServer {
612
827
  let token = null;
613
828
  const authHeader = apiReq.req.headers.authorization;
614
829
  const requiresAuthToken = this.requiresAuthToken(authType);
830
+ const allowRefresh = requiresAuthToken || (authType === 'maybe' && this.config.refreshMaybe);
615
831
  const apiKeyAuth = await this.tryAuthenticateApiKey(apiReq, authType, authHeader);
616
832
  if (apiKeyAuth) {
617
833
  return apiKeyAuth;
@@ -620,32 +836,84 @@ class ApiServer {
620
836
  token = authHeader.slice(7).trim();
621
837
  }
622
838
  if (!token) {
623
- const access = apiReq.req.cookies?.dat;
839
+ const access = apiReq.req.cookies?.[this.config.accessCookie];
624
840
  if (access) {
625
841
  token = access;
626
842
  }
627
843
  }
844
+ let tokenData;
845
+ let error;
846
+ let expired = false;
628
847
  if (!token || token === null) {
629
- if (requiresAuthToken) {
630
- throw new ApiError({ code: 401, message: 'Authorization token is required (Bearer/cookie)' });
848
+ if (authType === 'maybe') {
849
+ if (!this.config.refreshMaybe) {
850
+ return null;
851
+ }
852
+ const refreshed = await this.tryRefreshAccessToken(apiReq);
853
+ if (!refreshed) {
854
+ return null;
855
+ }
856
+ token = refreshed.token;
857
+ tokenData = refreshed.tokenData;
858
+ error = undefined;
859
+ expired = false;
860
+ }
861
+ else if (requiresAuthToken) {
862
+ const refreshed = await this.tryRefreshAccessToken(apiReq);
863
+ if (!refreshed) {
864
+ throw new ApiError({ code: 401, message: 'Authorization token is required (Bearer/cookie)' });
865
+ }
866
+ token = refreshed.token;
867
+ tokenData = refreshed.tokenData;
868
+ error = undefined;
869
+ expired = false;
631
870
  }
632
871
  }
633
872
  if (!token) {
634
- if (authType === 'maybe') {
635
- return null;
636
- }
637
- else {
638
- throw new ApiError({ code: 401, message: 'Unauthorized Access - requires authentication' });
873
+ throw new ApiError({ code: 401, message: 'Unauthorized Access - requires authentication' });
874
+ }
875
+ if (!tokenData) {
876
+ const verified = await this.verifyJWT(token);
877
+ tokenData = verified.tokenData;
878
+ error = verified.error;
879
+ expired = verified.expired ?? false;
880
+ }
881
+ if (!tokenData && allowRefresh && expired) {
882
+ const refreshed = await this.tryRefreshAccessToken(apiReq);
883
+ if (refreshed) {
884
+ token = refreshed.token;
885
+ tokenData = refreshed.tokenData;
886
+ error = undefined;
639
887
  }
640
888
  }
641
- const { tokenData, error } = await this.verifyJWT(token);
642
889
  if (!tokenData) {
643
890
  throw new ApiError({ code: 401, message: 'Unathorized Access - ' + error });
644
891
  }
645
892
  const effectiveUserId = this.extractTokenUserId(tokenData);
646
893
  apiReq.realUid = this.resolveRealUserId(tokenData, effectiveUserId);
647
894
  if (this.shouldValidateStoredToken(authType)) {
648
- await this.assertStoredAccessToken(apiReq, token, tokenData);
895
+ try {
896
+ await this.assertStoredAccessToken(apiReq, token, tokenData);
897
+ }
898
+ catch (error) {
899
+ if (allowRefresh &&
900
+ error instanceof ApiError &&
901
+ error.code === 401 &&
902
+ error.message === 'Authorization token is no longer valid') {
903
+ const refreshed = await this.tryRefreshAccessToken(apiReq);
904
+ if (!refreshed) {
905
+ throw error;
906
+ }
907
+ token = refreshed.token;
908
+ tokenData = refreshed.tokenData;
909
+ const refreshedUserId = this.extractTokenUserId(tokenData);
910
+ apiReq.realUid = this.resolveRealUserId(tokenData, refreshedUserId);
911
+ await this.assertStoredAccessToken(apiReq, token, tokenData);
912
+ }
913
+ else {
914
+ throw error;
915
+ }
916
+ }
649
917
  }
650
918
  apiReq.token = token;
651
919
  return tokenData;
@@ -686,7 +954,10 @@ class ApiServer {
686
954
  return this.config.validateTokens || authType === 'strict';
687
955
  }
688
956
  async assertStoredAccessToken(apiReq, token, tokenData) {
689
- const userId = this.extractTokenUserId(tokenData);
957
+ const userId = String(this.extractTokenUserId(tokenData));
958
+ if (apiReq.authToken && apiReq.authToken.accessToken === token && String(apiReq.authToken.userId) === userId) {
959
+ return;
960
+ }
690
961
  const stored = await this.storageAdapter.getToken({
691
962
  accessToken: token,
692
963
  userId
@@ -726,32 +997,98 @@ class ApiServer {
726
997
  }
727
998
  return rawReal;
728
999
  }
729
- handle_request(handler, auth) {
1000
+ useExpress(pathOrHandler, ...handlers) {
1001
+ if (typeof pathOrHandler === 'string') {
1002
+ this.app.use(pathOrHandler, ...handlers);
1003
+ }
1004
+ else {
1005
+ this.app.use(pathOrHandler, ...handlers);
1006
+ }
1007
+ this.ensureApiNotFoundOrdering();
1008
+ return this;
1009
+ }
1010
+ createApiRequest(req, res) {
1011
+ const apiReq = {
1012
+ server: this,
1013
+ req,
1014
+ res,
1015
+ token: '',
1016
+ tokenData: null,
1017
+ realUid: null,
1018
+ getClientInfo: () => ensureClientInfo(apiReq),
1019
+ getClientIp: () => ensureClientInfo(apiReq).ip,
1020
+ getClientIpChain: () => ensureClientInfo(apiReq).ipchain,
1021
+ getRealUid: () => apiReq.realUid ?? null,
1022
+ isImpersonating: () => {
1023
+ const realUid = apiReq.realUid;
1024
+ const tokenUid = apiReq.tokenData?.uid;
1025
+ if (realUid === null || realUid === undefined) {
1026
+ return false;
1027
+ }
1028
+ if (tokenUid === null || tokenUid === undefined) {
1029
+ return false;
1030
+ }
1031
+ return realUid !== tokenUid;
1032
+ }
1033
+ };
1034
+ return apiReq;
1035
+ }
1036
+ expressAuth(auth) {
730
1037
  return async (req, res, next) => {
731
- void next;
732
- const apiReq = {
733
- server: this,
734
- req,
735
- res,
736
- token: '',
737
- tokenData: null,
738
- realUid: null,
739
- getClientInfo: () => ensureClientInfo(apiReq),
740
- getClientIp: () => ensureClientInfo(apiReq).ip,
741
- getClientIpChain: () => ensureClientInfo(apiReq).ipchain,
742
- getRealUid: () => apiReq.realUid ?? null,
743
- isImpersonating: () => {
744
- const realUid = apiReq.realUid;
745
- const tokenUid = apiReq.tokenData?.uid;
746
- if (realUid === null || realUid === undefined) {
747
- return false;
748
- }
749
- if (tokenUid === null || tokenUid === undefined) {
750
- return false;
751
- }
752
- return realUid !== tokenUid;
1038
+ const apiReq = this.createApiRequest(req, res);
1039
+ req.apiReq = apiReq;
1040
+ res.locals.apiReq = apiReq;
1041
+ this.currReq = apiReq;
1042
+ try {
1043
+ if (this.config.hydrateGetBody) {
1044
+ hydrateGetBody(req);
753
1045
  }
1046
+ if (this.config.debug) {
1047
+ this.dumpRequest(apiReq);
1048
+ }
1049
+ apiReq.tokenData = await this.authenticate(apiReq, auth.type);
1050
+ await this.authorize(apiReq, auth.req);
1051
+ next();
1052
+ }
1053
+ catch (error) {
1054
+ next(error);
1055
+ }
1056
+ };
1057
+ }
1058
+ expressErrorHandler() {
1059
+ return (error, _req, res, next) => {
1060
+ void _req;
1061
+ if (res.headersSent) {
1062
+ next(error);
1063
+ return;
1064
+ }
1065
+ if (error instanceof ApiError || isApiErrorLike(error)) {
1066
+ const apiError = error;
1067
+ const normalizedErrors = apiError.errors && typeof apiError.errors === 'object' && !Array.isArray(apiError.errors)
1068
+ ? apiError.errors
1069
+ : {};
1070
+ const errorPayload = {
1071
+ code: apiError.code,
1072
+ message: apiError.message,
1073
+ data: apiError.data ?? null,
1074
+ errors: normalizedErrors
1075
+ };
1076
+ res.status(apiError.code).json(errorPayload);
1077
+ return;
1078
+ }
1079
+ const errorPayload = {
1080
+ code: 500,
1081
+ message: this.guessExceptionText(error),
1082
+ data: null,
1083
+ errors: {}
754
1084
  };
1085
+ res.status(500).json(errorPayload);
1086
+ };
1087
+ }
1088
+ handle_request(handler, auth) {
1089
+ return async (req, res, next) => {
1090
+ void next;
1091
+ const apiReq = this.createApiRequest(req, res);
755
1092
  this.currReq = apiReq;
756
1093
  try {
757
1094
  if (this.config.hydrateGetBody) {