@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
@@ -4,13 +4,34 @@
4
4
  * This source code is licensed under the MIT license found in the
5
5
  * LICENSE file in the root directory of this source tree.
6
6
  */
7
+ import { randomUUID } from 'node:crypto';
7
8
  import cookieParser from 'cookie-parser';
8
9
  import cors from 'cors';
9
10
  import express from 'express';
10
- import jwt from 'jsonwebtoken';
11
11
  import multer from 'multer';
12
- import { nullAuthModule } from './auth-module.js';
13
- import { nullAuthStorage } from './auth-storage.js';
12
+ import { nullAuthModule } from './auth-api/module.js';
13
+ import { nullAuthAdapter } from './auth-api/storage.js';
14
+ import { TokenStore } from './token/base.js';
15
+ class JwtHelperStore extends TokenStore {
16
+ async save() {
17
+ throw new Error('Token store is not configured');
18
+ }
19
+ async get() {
20
+ throw new Error('Token store is not configured');
21
+ }
22
+ async delete() {
23
+ throw new Error('Token store is not configured');
24
+ }
25
+ async update() {
26
+ throw new Error('Token store is not configured');
27
+ }
28
+ async list() {
29
+ return [];
30
+ }
31
+ async close() {
32
+ return;
33
+ }
34
+ }
14
35
  export { ApiModule } from './api-module.js';
15
36
  function guess_exception_text(error, defMsg = 'Unknown Error') {
16
37
  const msg = [];
@@ -323,19 +344,38 @@ function fillConfig(config) {
323
344
  devMode: config.devMode ?? false,
324
345
  hydrateGetBody: config.hydrateGetBody ?? true,
325
346
  validateTokens: config.validateTokens ?? false,
347
+ refreshMaybe: config.refreshMaybe ?? false,
326
348
  apiVersion: config.apiVersion ?? '',
327
- minClientVersion: config.minClientVersion ?? ''
349
+ minClientVersion: config.minClientVersion ?? '',
350
+ tokenStore: config.tokenStore,
351
+ authStores: config.authStores
328
352
  };
329
353
  }
330
354
  export class ApiServer {
331
355
  constructor(config = {}) {
332
356
  this.currReq = null;
333
357
  this.apiNotFoundHandler = null;
358
+ this.tokenStoreAdapter = null;
359
+ this.userStoreAdapter = null;
360
+ this.passkeyServiceAdapter = null;
361
+ this.oauthStoreAdapter = null;
362
+ this.canImpersonateAdapter = null;
334
363
  this.config = fillConfig(config);
335
364
  this.apiBasePath = this.normalizeApiBasePath(this.config.apiBasePath);
336
365
  this.startedAt = Date.now();
337
- this.storageAdapter = nullAuthStorage;
366
+ this.storageAdapter = nullAuthAdapter;
338
367
  this.moduleAdapter = nullAuthModule;
368
+ this.jwtHelper = new JwtHelperStore();
369
+ this.tokenStoreAdapter = this.config.tokenStore ?? null;
370
+ if (this.config.authStores) {
371
+ const { userStore, tokenStore, passkeyService, oauthStore, canImpersonate } = this.config.authStores;
372
+ this.userStoreAdapter = userStore;
373
+ this.tokenStoreAdapter = tokenStore;
374
+ this.passkeyServiceAdapter = passkeyService ?? null;
375
+ this.oauthStoreAdapter = oauthStore ?? null;
376
+ this.canImpersonateAdapter = canImpersonate ?? null;
377
+ this.storageAdapter = this;
378
+ }
339
379
  this.app = express();
340
380
  if (config.uploadPath) {
341
381
  const upload = multer({ dest: config.uploadPath });
@@ -372,72 +412,149 @@ export class ApiServer {
372
412
  getAuthModule() {
373
413
  return this.moduleAdapter;
374
414
  }
375
- jwtSign(payload, secret, expiresInSeconds, options) {
376
- options || (options = {});
377
- const opts = { ...options, expiresIn: expiresInSeconds };
378
- try {
379
- const token = jwt.sign(payload, secret, opts);
380
- return {
381
- success: true,
382
- token
383
- };
415
+ setTokenStore(store) {
416
+ this.tokenStoreAdapter = store;
417
+ // If using direct stores, expose self as the auth storage.
418
+ if (this.userStoreAdapter) {
419
+ this.storageAdapter = this;
384
420
  }
385
- catch (error) {
386
- return {
387
- success: false,
388
- error: error instanceof Error ? error.message : String(error)
389
- };
421
+ return this;
422
+ }
423
+ getTokenStore() {
424
+ return this.tokenStoreAdapter;
425
+ }
426
+ ensureUserStore() {
427
+ if (!this.userStoreAdapter) {
428
+ throw new Error('User store is not configured');
390
429
  }
430
+ return this.userStoreAdapter;
391
431
  }
392
- jwtVerify(token, secret, options) {
393
- options || (options = {});
394
- try {
395
- const data = jwt.verify(token, secret, options);
396
- return {
397
- success: true,
398
- data
399
- };
432
+ ensureTokenStore() {
433
+ if (!this.tokenStoreAdapter) {
434
+ throw new Error('Token store is not configured');
400
435
  }
401
- catch (error) {
402
- if (error instanceof jwt.TokenExpiredError) {
403
- return {
404
- success: false,
405
- expired: true,
406
- error: 'Token expired'
407
- };
408
- }
409
- else {
410
- return {
411
- success: false,
412
- expired: false,
413
- error: error instanceof Error ? error.message : String(error)
414
- };
415
- }
436
+ return this.tokenStoreAdapter;
437
+ }
438
+ ensurePasskeyService() {
439
+ if (!this.passkeyServiceAdapter) {
440
+ throw new Error('Passkey service is not configured');
416
441
  }
442
+ return this.passkeyServiceAdapter;
417
443
  }
418
- jwtDecode(token, options) {
419
- options || (options = {});
420
- try {
421
- const data = jwt.decode(token, options);
422
- // jwt.decode returns null for invalid tokens rather than throwing
423
- if (data === null) {
424
- return {
425
- success: false,
426
- error: 'Invalid token format'
427
- };
428
- }
429
- return {
430
- success: true,
431
- data
432
- };
444
+ async listUserCredentials(userId) {
445
+ return this.ensurePasskeyService().listUserCredentials(userId);
446
+ }
447
+ async deletePasskeyCredential(credentialId) {
448
+ return this.ensurePasskeyService().deleteCredential(credentialId);
449
+ }
450
+ ensureOAuthStore() {
451
+ if (!this.oauthStoreAdapter) {
452
+ throw new Error('OAuth store is not configured');
433
453
  }
434
- catch (error) {
435
- // jwt.decode rarely throws, but might for severely malformed tokens
436
- return {
437
- success: false,
438
- error: error instanceof Error ? error.message : String(error)
439
- };
454
+ return this.oauthStoreAdapter;
455
+ }
456
+ // AuthAdapter-compatible helpers (used by AuthModule)
457
+ async getUser(identifier) {
458
+ return this.userStoreAdapter ? this.userStoreAdapter.findUser(identifier) : null;
459
+ }
460
+ getUserPasswordHash(user) {
461
+ return this.ensureUserStore().getPasswordHash(user) ?? '';
462
+ }
463
+ getUserId(user) {
464
+ return this.ensureUserStore().getUserId(user);
465
+ }
466
+ filterUser(user) {
467
+ return this.ensureUserStore().toPublic(user);
468
+ }
469
+ async verifyPassword(password, hash) {
470
+ return this.ensureUserStore().verifyPassword(password, hash);
471
+ }
472
+ async storeToken(data) {
473
+ if (this.tokenStoreAdapter) {
474
+ return this.tokenStoreAdapter.save(data);
440
475
  }
476
+ if (typeof this.storageAdapter.storeToken === 'function') {
477
+ return this.storageAdapter.storeToken(data);
478
+ }
479
+ throw new Error('Token store is not configured');
480
+ }
481
+ async getToken(query, opts) {
482
+ const normalized = {
483
+ ...query,
484
+ userId: query.userId !== undefined && query.userId !== null ? String(query.userId) : undefined,
485
+ ruid: query.ruid !== undefined && query.ruid !== null ? String(query.ruid) : undefined
486
+ };
487
+ if (this.tokenStoreAdapter) {
488
+ return this.tokenStoreAdapter.get(normalized, opts);
489
+ }
490
+ if (typeof this.storageAdapter.getToken === 'function') {
491
+ return this.storageAdapter.getToken(normalized, opts);
492
+ }
493
+ return null;
494
+ }
495
+ async deleteToken(query) {
496
+ const normalized = {
497
+ ...query,
498
+ userId: query.userId !== undefined && query.userId !== null ? String(query.userId) : undefined,
499
+ ruid: query.ruid !== undefined && query.ruid !== null ? String(query.ruid) : undefined
500
+ };
501
+ if (this.tokenStoreAdapter) {
502
+ return this.tokenStoreAdapter.delete(normalized);
503
+ }
504
+ if (typeof this.storageAdapter.deleteToken === 'function') {
505
+ return this.storageAdapter.deleteToken(normalized);
506
+ }
507
+ return 0;
508
+ }
509
+ async createPasskeyChallenge(params) {
510
+ return this.ensurePasskeyService().createChallenge(params);
511
+ }
512
+ async verifyPasskeyResponse(params) {
513
+ return this.ensurePasskeyService().verifyResponse(params);
514
+ }
515
+ async getClient(clientId) {
516
+ return this.oauthStoreAdapter ? this.oauthStoreAdapter.getClient(clientId) : null;
517
+ }
518
+ async verifyClientSecret(client, clientSecret) {
519
+ return this.ensureOAuthStore().verifyClientSecret(client.clientId, clientSecret);
520
+ }
521
+ async createAuthCode(request) {
522
+ const expiresAt = new Date(Date.now() + (request.expiresInSeconds ?? 300) * 1000);
523
+ const code = request.code ?? randomUUID();
524
+ await this.ensureOAuthStore().createAuthCode({ ...request, code, expiresAt });
525
+ return {
526
+ code,
527
+ clientId: request.clientId,
528
+ userId: request.userId,
529
+ redirectUri: request.redirectUri,
530
+ scope: request.scope ?? [],
531
+ codeChallenge: request.codeChallenge,
532
+ codeChallengeMethod: request.codeChallengeMethod,
533
+ expiresAt,
534
+ metadata: request.metadata
535
+ };
536
+ }
537
+ async consumeAuthCode(code, clientId) {
538
+ const consumed = await this.ensureOAuthStore().consumeAuthCode(code);
539
+ if (!consumed || consumed.clientId !== clientId) {
540
+ return null;
541
+ }
542
+ return consumed;
543
+ }
544
+ async canImpersonate(params) {
545
+ if (this.canImpersonateAdapter) {
546
+ return !!(await this.canImpersonateAdapter(params));
547
+ }
548
+ return params.realUserId === params.effectiveUserId;
549
+ }
550
+ jwtSign(payload, secret, expiresInSeconds, options) {
551
+ return (this.tokenStoreAdapter ?? this.jwtHelper).jwtSign(payload, secret, expiresInSeconds, options);
552
+ }
553
+ jwtVerify(token, secret, options) {
554
+ return (this.tokenStoreAdapter ?? this.jwtHelper).jwtVerify(token, secret, options);
555
+ }
556
+ jwtDecode(token, options) {
557
+ return (this.tokenStoreAdapter ?? this.jwtHelper).jwtDecode(token, options);
441
558
  }
442
559
  async getApiKey(token) {
443
560
  void token;
@@ -455,16 +572,13 @@ export class ApiServer {
455
572
  return this.storageAdapter.verifyPassword(params.password, hash);
456
573
  }
457
574
  async updateToken(updates) {
458
- if (typeof this.storageAdapter.updateToken !== 'function') {
459
- return false;
575
+ if (this.tokenStoreAdapter) {
576
+ return this.tokenStoreAdapter.update(updates);
460
577
  }
461
- return this.storageAdapter.updateToken({
462
- refreshToken: updates.refreshToken,
463
- access: updates.accessToken,
464
- expires: updates.expires,
465
- clientId: updates.clientId,
466
- scope: updates.scope
467
- });
578
+ if (typeof this.storageAdapter.updateToken === 'function') {
579
+ return this.storageAdapter.updateToken(updates);
580
+ }
581
+ return false;
468
582
  }
469
583
  guessExceptionText(error, defMsg = 'Unkown Error') {
470
584
  return guess_exception_text(error, defMsg);
@@ -585,16 +699,117 @@ export class ApiServer {
585
699
  }
586
700
  async verifyJWT(token) {
587
701
  if (!this.config.accessSecret) {
588
- return { tokenData: undefined, error: 'JWT authentication disabled; no jwtSecret set' };
702
+ return { tokenData: undefined, error: 'JWT authentication disabled; no jwtSecret set', expired: false };
589
703
  }
590
704
  const result = this.jwtVerify(token, this.config.accessSecret);
591
705
  if (!result.success) {
592
- return { tokenData: undefined, error: result.error };
706
+ return { tokenData: undefined, error: result.error, expired: result.expired };
593
707
  }
594
708
  if (!result.data.uid) {
595
- return { tokenData: undefined, error: 'Missing/bad userid in token' };
709
+ return { tokenData: undefined, error: 'Missing/bad userid in token', expired: false };
710
+ }
711
+ return { tokenData: result.data, error: undefined, expired: false };
712
+ }
713
+ jwtCookieOptions(apiReq) {
714
+ const conf = this.config;
715
+ const forwarded = apiReq.req.headers['x-forwarded-proto'];
716
+ const referer = apiReq.req.headers['origin'] ?? apiReq.req.headers['referer'];
717
+ const origin = typeof referer === 'string' ? referer : '';
718
+ const isHttps = forwarded === 'https' || apiReq.req.protocol === 'https';
719
+ const isLocalhost = origin.includes('localhost');
720
+ const options = {
721
+ httpOnly: true,
722
+ secure: true,
723
+ sameSite: 'strict',
724
+ domain: conf.cookieDomain || undefined,
725
+ path: '/',
726
+ maxAge: undefined
727
+ };
728
+ if (conf.devMode) {
729
+ options.secure = isHttps;
730
+ options.httpOnly = false;
731
+ options.sameSite = 'lax';
732
+ if (isLocalhost) {
733
+ options.domain = undefined;
734
+ }
735
+ }
736
+ return options;
737
+ }
738
+ setAccessCookie(apiReq, accessToken, sessionCookie) {
739
+ const conf = this.config;
740
+ const options = this.jwtCookieOptions(apiReq);
741
+ const accessMaxAge = Math.max(1, conf.accessExpiry) * 1000;
742
+ const accessOptions = sessionCookie ? options : { ...options, maxAge: accessMaxAge };
743
+ apiReq.res.cookie(conf.accessCookie, accessToken, accessOptions);
744
+ }
745
+ async tryRefreshAccessToken(apiReq) {
746
+ const conf = this.config;
747
+ if (!conf.refreshSecret || !conf.accessSecret) {
748
+ return null;
749
+ }
750
+ const rawRefresh = apiReq.req.cookies?.[conf.refreshCookie];
751
+ if (typeof rawRefresh !== 'string') {
752
+ return null;
596
753
  }
597
- return { tokenData: result.data, error: undefined };
754
+ const refreshToken = rawRefresh.trim();
755
+ if (!refreshToken) {
756
+ return null;
757
+ }
758
+ const verify = this.jwtVerify(refreshToken, conf.refreshSecret);
759
+ if (!verify.success || !verify.data) {
760
+ return null;
761
+ }
762
+ let stored = null;
763
+ try {
764
+ stored = await this.storageAdapter.getToken({ refreshToken });
765
+ }
766
+ catch {
767
+ return null;
768
+ }
769
+ if (!stored) {
770
+ return null;
771
+ }
772
+ const storedUid = String(stored.userId);
773
+ const verifyUid = verify.data.uid === undefined || verify.data.uid === null ? null : String(verify.data.uid);
774
+ if (verifyUid && verifyUid !== storedUid) {
775
+ return null;
776
+ }
777
+ const claims = verify.data;
778
+ const { exp: _exp, iat: _iat, nbf: _nbf, ...payload } = claims;
779
+ void _exp;
780
+ void _iat;
781
+ void _nbf;
782
+ // Ensure we never embed token secrets into refreshed access tokens.
783
+ delete payload.accessToken;
784
+ delete payload.refreshToken;
785
+ delete payload.userId;
786
+ delete payload.expires;
787
+ delete payload.issuedAt;
788
+ delete payload.lastSeenAt;
789
+ delete payload.status;
790
+ const access = this.jwtSign(payload, conf.accessSecret, conf.accessExpiry);
791
+ if (!access.success || !access.token) {
792
+ return null;
793
+ }
794
+ const updated = await this.updateToken({
795
+ refreshToken,
796
+ accessToken: access.token,
797
+ lastSeenAt: new Date()
798
+ });
799
+ if (!updated) {
800
+ return null;
801
+ }
802
+ this.setAccessCookie(apiReq, access.token, stored.sessionCookie ?? false);
803
+ if (apiReq.req.cookies) {
804
+ apiReq.req.cookies[conf.accessCookie] = access.token;
805
+ }
806
+ const verifiedAccess = await this.verifyJWT(access.token);
807
+ if (!verifiedAccess.tokenData) {
808
+ return null;
809
+ }
810
+ const refreshedStored = { ...stored, accessToken: access.token };
811
+ apiReq.authToken = refreshedStored;
812
+ return { token: access.token, tokenData: verifiedAccess.tokenData, stored: refreshedStored };
598
813
  }
599
814
  async authenticate(apiReq, authType) {
600
815
  if (authType === 'none') {
@@ -604,6 +819,7 @@ export class ApiServer {
604
819
  let token = null;
605
820
  const authHeader = apiReq.req.headers.authorization;
606
821
  const requiresAuthToken = this.requiresAuthToken(authType);
822
+ const allowRefresh = requiresAuthToken || (authType === 'maybe' && this.config.refreshMaybe);
607
823
  const apiKeyAuth = await this.tryAuthenticateApiKey(apiReq, authType, authHeader);
608
824
  if (apiKeyAuth) {
609
825
  return apiKeyAuth;
@@ -612,32 +828,84 @@ export class ApiServer {
612
828
  token = authHeader.slice(7).trim();
613
829
  }
614
830
  if (!token) {
615
- const access = apiReq.req.cookies?.dat;
831
+ const access = apiReq.req.cookies?.[this.config.accessCookie];
616
832
  if (access) {
617
833
  token = access;
618
834
  }
619
835
  }
836
+ let tokenData;
837
+ let error;
838
+ let expired = false;
620
839
  if (!token || token === null) {
621
- if (requiresAuthToken) {
622
- throw new ApiError({ code: 401, message: 'Authorization token is required (Bearer/cookie)' });
840
+ if (authType === 'maybe') {
841
+ if (!this.config.refreshMaybe) {
842
+ return null;
843
+ }
844
+ const refreshed = await this.tryRefreshAccessToken(apiReq);
845
+ if (!refreshed) {
846
+ return null;
847
+ }
848
+ token = refreshed.token;
849
+ tokenData = refreshed.tokenData;
850
+ error = undefined;
851
+ expired = false;
852
+ }
853
+ else if (requiresAuthToken) {
854
+ const refreshed = await this.tryRefreshAccessToken(apiReq);
855
+ if (!refreshed) {
856
+ throw new ApiError({ code: 401, message: 'Authorization token is required (Bearer/cookie)' });
857
+ }
858
+ token = refreshed.token;
859
+ tokenData = refreshed.tokenData;
860
+ error = undefined;
861
+ expired = false;
623
862
  }
624
863
  }
625
864
  if (!token) {
626
- if (authType === 'maybe') {
627
- return null;
628
- }
629
- else {
630
- throw new ApiError({ code: 401, message: 'Unauthorized Access - requires authentication' });
865
+ throw new ApiError({ code: 401, message: 'Unauthorized Access - requires authentication' });
866
+ }
867
+ if (!tokenData) {
868
+ const verified = await this.verifyJWT(token);
869
+ tokenData = verified.tokenData;
870
+ error = verified.error;
871
+ expired = verified.expired ?? false;
872
+ }
873
+ if (!tokenData && allowRefresh && expired) {
874
+ const refreshed = await this.tryRefreshAccessToken(apiReq);
875
+ if (refreshed) {
876
+ token = refreshed.token;
877
+ tokenData = refreshed.tokenData;
878
+ error = undefined;
631
879
  }
632
880
  }
633
- const { tokenData, error } = await this.verifyJWT(token);
634
881
  if (!tokenData) {
635
882
  throw new ApiError({ code: 401, message: 'Unathorized Access - ' + error });
636
883
  }
637
884
  const effectiveUserId = this.extractTokenUserId(tokenData);
638
885
  apiReq.realUid = this.resolveRealUserId(tokenData, effectiveUserId);
639
886
  if (this.shouldValidateStoredToken(authType)) {
640
- await this.assertStoredAccessToken(apiReq, token, tokenData);
887
+ try {
888
+ await this.assertStoredAccessToken(apiReq, token, tokenData);
889
+ }
890
+ catch (error) {
891
+ if (allowRefresh &&
892
+ error instanceof ApiError &&
893
+ error.code === 401 &&
894
+ error.message === 'Authorization token is no longer valid') {
895
+ const refreshed = await this.tryRefreshAccessToken(apiReq);
896
+ if (!refreshed) {
897
+ throw error;
898
+ }
899
+ token = refreshed.token;
900
+ tokenData = refreshed.tokenData;
901
+ const refreshedUserId = this.extractTokenUserId(tokenData);
902
+ apiReq.realUid = this.resolveRealUserId(tokenData, refreshedUserId);
903
+ await this.assertStoredAccessToken(apiReq, token, tokenData);
904
+ }
905
+ else {
906
+ throw error;
907
+ }
908
+ }
641
909
  }
642
910
  apiReq.token = token;
643
911
  return tokenData;
@@ -678,7 +946,10 @@ export class ApiServer {
678
946
  return this.config.validateTokens || authType === 'strict';
679
947
  }
680
948
  async assertStoredAccessToken(apiReq, token, tokenData) {
681
- const userId = this.extractTokenUserId(tokenData);
949
+ const userId = String(this.extractTokenUserId(tokenData));
950
+ if (apiReq.authToken && apiReq.authToken.accessToken === token && String(apiReq.authToken.userId) === userId) {
951
+ return;
952
+ }
682
953
  const stored = await this.storageAdapter.getToken({
683
954
  accessToken: token,
684
955
  userId
@@ -718,32 +989,98 @@ export class ApiServer {
718
989
  }
719
990
  return rawReal;
720
991
  }
721
- handle_request(handler, auth) {
992
+ useExpress(pathOrHandler, ...handlers) {
993
+ if (typeof pathOrHandler === 'string') {
994
+ this.app.use(pathOrHandler, ...handlers);
995
+ }
996
+ else {
997
+ this.app.use(pathOrHandler, ...handlers);
998
+ }
999
+ this.ensureApiNotFoundOrdering();
1000
+ return this;
1001
+ }
1002
+ createApiRequest(req, res) {
1003
+ const apiReq = {
1004
+ server: this,
1005
+ req,
1006
+ res,
1007
+ token: '',
1008
+ tokenData: null,
1009
+ realUid: null,
1010
+ getClientInfo: () => ensureClientInfo(apiReq),
1011
+ getClientIp: () => ensureClientInfo(apiReq).ip,
1012
+ getClientIpChain: () => ensureClientInfo(apiReq).ipchain,
1013
+ getRealUid: () => apiReq.realUid ?? null,
1014
+ isImpersonating: () => {
1015
+ const realUid = apiReq.realUid;
1016
+ const tokenUid = apiReq.tokenData?.uid;
1017
+ if (realUid === null || realUid === undefined) {
1018
+ return false;
1019
+ }
1020
+ if (tokenUid === null || tokenUid === undefined) {
1021
+ return false;
1022
+ }
1023
+ return realUid !== tokenUid;
1024
+ }
1025
+ };
1026
+ return apiReq;
1027
+ }
1028
+ expressAuth(auth) {
722
1029
  return async (req, res, next) => {
723
- void next;
724
- const apiReq = {
725
- server: this,
726
- req,
727
- res,
728
- token: '',
729
- tokenData: null,
730
- realUid: null,
731
- getClientInfo: () => ensureClientInfo(apiReq),
732
- getClientIp: () => ensureClientInfo(apiReq).ip,
733
- getClientIpChain: () => ensureClientInfo(apiReq).ipchain,
734
- getRealUid: () => apiReq.realUid ?? null,
735
- isImpersonating: () => {
736
- const realUid = apiReq.realUid;
737
- const tokenUid = apiReq.tokenData?.uid;
738
- if (realUid === null || realUid === undefined) {
739
- return false;
740
- }
741
- if (tokenUid === null || tokenUid === undefined) {
742
- return false;
743
- }
744
- return realUid !== tokenUid;
1030
+ const apiReq = this.createApiRequest(req, res);
1031
+ req.apiReq = apiReq;
1032
+ res.locals.apiReq = apiReq;
1033
+ this.currReq = apiReq;
1034
+ try {
1035
+ if (this.config.hydrateGetBody) {
1036
+ hydrateGetBody(req);
745
1037
  }
1038
+ if (this.config.debug) {
1039
+ this.dumpRequest(apiReq);
1040
+ }
1041
+ apiReq.tokenData = await this.authenticate(apiReq, auth.type);
1042
+ await this.authorize(apiReq, auth.req);
1043
+ next();
1044
+ }
1045
+ catch (error) {
1046
+ next(error);
1047
+ }
1048
+ };
1049
+ }
1050
+ expressErrorHandler() {
1051
+ return (error, _req, res, next) => {
1052
+ void _req;
1053
+ if (res.headersSent) {
1054
+ next(error);
1055
+ return;
1056
+ }
1057
+ if (error instanceof ApiError || isApiErrorLike(error)) {
1058
+ const apiError = error;
1059
+ const normalizedErrors = apiError.errors && typeof apiError.errors === 'object' && !Array.isArray(apiError.errors)
1060
+ ? apiError.errors
1061
+ : {};
1062
+ const errorPayload = {
1063
+ code: apiError.code,
1064
+ message: apiError.message,
1065
+ data: apiError.data ?? null,
1066
+ errors: normalizedErrors
1067
+ };
1068
+ res.status(apiError.code).json(errorPayload);
1069
+ return;
1070
+ }
1071
+ const errorPayload = {
1072
+ code: 500,
1073
+ message: this.guessExceptionText(error),
1074
+ data: null,
1075
+ errors: {}
746
1076
  };
1077
+ res.status(500).json(errorPayload);
1078
+ };
1079
+ }
1080
+ handle_request(handler, auth) {
1081
+ return async (req, res, next) => {
1082
+ void next;
1083
+ const apiReq = this.createApiRequest(req, res);
747
1084
  this.currReq = apiReq;
748
1085
  try {
749
1086
  if (this.config.hydrateGetBody) {