@technomoron/api-server-base 2.0.0-beta.21 → 2.0.0-beta.23

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 (177) hide show
  1. package/dist/cjs/common/types.cjs +10 -0
  2. package/dist/cjs/common/types.d.ts +137 -0
  3. package/dist/cjs/{api-module.cjs → server/src/api-module.cjs} +8 -0
  4. package/dist/{esm → cjs/server/src}/api-module.d.ts +15 -0
  5. package/dist/cjs/{api-server-base.cjs → server/src/api-server-base.cjs} +669 -627
  6. package/dist/{esm → cjs/server/src}/api-server-base.d.ts +105 -78
  7. package/dist/cjs/{auth-api/auth-module.js → server/src/auth-api/auth-module.cjs} +96 -76
  8. package/dist/cjs/{auth-api → server/src/auth-api}/auth-module.d.ts +1 -1
  9. package/dist/cjs/{auth-api/compat-auth-storage.js → server/src/auth-api/compat-auth-storage.cjs} +4 -4
  10. package/dist/cjs/{auth-api/mem-auth-store.js → server/src/auth-api/mem-auth-store.cjs} +7 -7
  11. package/dist/cjs/{auth-api/module.js → server/src/auth-api/module.cjs} +1 -1
  12. package/dist/cjs/server/src/auth-api/schemas.cjs +171 -0
  13. package/dist/cjs/server/src/auth-api/schemas.d.ts +21 -0
  14. package/dist/cjs/{auth-api/sql-auth-store.js → server/src/auth-api/sql-auth-store.cjs} +8 -8
  15. package/dist/cjs/{auth-api/user-id.js → server/src/auth-api/user-id.cjs} +12 -3
  16. package/dist/{esm → cjs/server/src}/auth-cookie-options.d.ts +5 -3
  17. package/dist/cjs/server/src/base/client-info.cjs +285 -0
  18. package/dist/cjs/server/src/base/client-info.d.ts +27 -0
  19. package/dist/cjs/server/src/base/error-utils.cjs +50 -0
  20. package/dist/cjs/server/src/base/error-utils.d.ts +16 -0
  21. package/dist/cjs/server/src/base/request-utils.cjs +27 -0
  22. package/dist/cjs/server/src/base/request-utils.d.ts +8 -0
  23. package/dist/cjs/{index.cjs → server/src/index.cjs} +24 -15
  24. package/dist/{esm → cjs/server/src}/index.d.ts +7 -0
  25. package/dist/cjs/server/src/limiter/auth-rate-limiter.cjs +35 -0
  26. package/dist/cjs/server/src/limiter/auth-rate-limiter.d.ts +12 -0
  27. package/dist/cjs/server/src/limiter/fixed-window.cjs +41 -0
  28. package/dist/cjs/server/src/limiter/fixed-window.d.ts +11 -0
  29. package/dist/cjs/{oauth/base.js → server/src/oauth/base.cjs} +1 -0
  30. package/dist/cjs/{oauth → server/src/oauth}/base.d.ts +8 -1
  31. package/dist/cjs/{oauth/memory.js → server/src/oauth/memory.cjs} +7 -4
  32. package/dist/{esm → cjs/server/src}/oauth/memory.d.ts +1 -1
  33. package/dist/cjs/{oauth/models.js → server/src/oauth/models.cjs} +2 -2
  34. package/dist/cjs/{oauth/sequelize.js → server/src/oauth/sequelize.cjs} +11 -7
  35. package/dist/{esm → cjs/server/src}/oauth/sequelize.d.ts +1 -1
  36. package/dist/cjs/{passkey/base.js → server/src/passkey/base.cjs} +1 -0
  37. package/dist/{esm → cjs/server/src}/passkey/base.d.ts +11 -0
  38. package/dist/cjs/{passkey/memory.js → server/src/passkey/memory.cjs} +2 -2
  39. package/dist/cjs/{passkey/models.js → server/src/passkey/models.cjs} +1 -1
  40. package/dist/cjs/{passkey/sequelize.js → server/src/passkey/sequelize.cjs} +3 -3
  41. package/dist/cjs/{passkey/service.js → server/src/passkey/service.cjs} +17 -3
  42. package/dist/{esm → cjs/server/src}/passkey/service.d.ts +1 -1
  43. package/dist/cjs/{sequelize-utils.js → server/src/sequelize-utils.cjs} +4 -5
  44. package/dist/cjs/{token/base.js → server/src/token/base.cjs} +4 -0
  45. package/dist/{esm → cjs/server/src}/token/base.d.ts +7 -0
  46. package/dist/cjs/{token/memory.js → server/src/token/memory.cjs} +15 -20
  47. package/dist/cjs/{token/sequelize.js → server/src/token/sequelize.cjs} +25 -11
  48. package/dist/cjs/server/src/upload/memory.cjs +92 -0
  49. package/dist/cjs/server/src/upload/memory.d.ts +17 -0
  50. package/dist/cjs/server/src/upload/tus-module.cjs +270 -0
  51. package/dist/cjs/server/src/upload/tus-module.d.ts +38 -0
  52. package/dist/cjs/server/src/upload/types.d.ts +8 -0
  53. package/dist/cjs/{user/base.js → server/src/user/base.cjs} +1 -0
  54. package/dist/cjs/{user → server/src/user}/base.d.ts +9 -0
  55. package/dist/cjs/{user/memory.js → server/src/user/memory.cjs} +29 -7
  56. package/dist/cjs/{user/sequelize.js → server/src/user/sequelize.cjs} +33 -8
  57. package/dist/cjs/server/src/user/types.cjs +2 -0
  58. package/dist/esm/common/types.d.ts +137 -0
  59. package/dist/esm/common/types.js +9 -0
  60. package/dist/{cjs → esm/server/src}/api-module.d.ts +15 -0
  61. package/dist/esm/{api-module.js → server/src/api-module.js} +8 -0
  62. package/dist/{cjs → esm/server/src}/api-server-base.d.ts +105 -78
  63. package/dist/esm/{api-server-base.js → server/src/api-server-base.js} +658 -616
  64. package/dist/esm/{auth-api → server/src/auth-api}/auth-module.d.ts +1 -1
  65. package/dist/esm/{auth-api → server/src/auth-api}/auth-module.js +92 -72
  66. package/dist/esm/{auth-api → server/src/auth-api}/compat-auth-storage.js +3 -3
  67. package/dist/esm/server/src/auth-api/schemas.d.ts +21 -0
  68. package/dist/esm/server/src/auth-api/schemas.js +168 -0
  69. package/dist/esm/{auth-api → server/src/auth-api}/user-id.js +12 -3
  70. package/dist/{cjs → esm/server/src}/auth-cookie-options.d.ts +5 -3
  71. package/dist/esm/server/src/base/client-info.d.ts +27 -0
  72. package/dist/esm/server/src/base/client-info.js +282 -0
  73. package/dist/esm/server/src/base/error-utils.d.ts +16 -0
  74. package/dist/esm/server/src/base/error-utils.js +44 -0
  75. package/dist/esm/server/src/base/request-utils.d.ts +8 -0
  76. package/dist/esm/server/src/base/request-utils.js +23 -0
  77. package/dist/{cjs → esm/server/src}/index.d.ts +7 -0
  78. package/dist/esm/{index.js → server/src/index.js} +4 -0
  79. package/dist/esm/server/src/limiter/auth-rate-limiter.d.ts +12 -0
  80. package/dist/esm/server/src/limiter/auth-rate-limiter.js +32 -0
  81. package/dist/esm/server/src/limiter/fixed-window.d.ts +11 -0
  82. package/dist/esm/server/src/limiter/fixed-window.js +37 -0
  83. package/dist/esm/{oauth → server/src/oauth}/base.d.ts +8 -1
  84. package/dist/esm/server/src/oauth/base.js +3 -0
  85. package/dist/{cjs → esm/server/src}/oauth/memory.d.ts +1 -1
  86. package/dist/esm/{oauth → server/src/oauth}/memory.js +5 -2
  87. package/dist/{cjs → esm/server/src}/oauth/sequelize.d.ts +1 -1
  88. package/dist/esm/{oauth → server/src/oauth}/sequelize.js +6 -2
  89. package/dist/{cjs → esm/server/src}/passkey/base.d.ts +11 -0
  90. package/dist/esm/server/src/passkey/base.js +3 -0
  91. package/dist/{cjs → esm/server/src}/passkey/service.d.ts +1 -1
  92. package/dist/esm/{passkey → server/src/passkey}/service.js +17 -3
  93. package/dist/esm/{sequelize-utils.js → server/src/sequelize-utils.js} +4 -5
  94. package/dist/{cjs → esm/server/src}/token/base.d.ts +7 -0
  95. package/dist/esm/{token → server/src/token}/base.js +4 -0
  96. package/dist/esm/{token → server/src/token}/memory.js +14 -19
  97. package/dist/esm/{token → server/src/token}/sequelize.js +22 -8
  98. package/dist/esm/server/src/upload/memory.d.ts +17 -0
  99. package/dist/esm/server/src/upload/memory.js +86 -0
  100. package/dist/esm/server/src/upload/tus-module.d.ts +38 -0
  101. package/dist/esm/server/src/upload/tus-module.js +266 -0
  102. package/dist/esm/server/src/upload/types.d.ts +8 -0
  103. package/dist/esm/{user → server/src/user}/base.d.ts +9 -0
  104. package/dist/esm/{user → server/src/user}/base.js +1 -0
  105. package/dist/esm/{user → server/src/user}/memory.js +27 -5
  106. package/dist/esm/{user → server/src/user}/sequelize.js +30 -5
  107. package/dist/esm/server/src/user/types.js +1 -0
  108. package/docs/swagger/openapi.json +411 -125
  109. package/package.json +129 -134
  110. package/README.txt +0 -213
  111. package/dist/esm/oauth/base.js +0 -2
  112. package/dist/esm/passkey/base.js +0 -2
  113. /package/dist/cjs/{auth-api → server/src/auth-api}/compat-auth-storage.d.ts +0 -0
  114. /package/dist/cjs/{auth-api → server/src/auth-api}/mem-auth-store.d.ts +0 -0
  115. /package/dist/cjs/{auth-api → server/src/auth-api}/module.d.ts +0 -0
  116. /package/dist/cjs/{auth-api → server/src/auth-api}/sql-auth-store.d.ts +0 -0
  117. /package/dist/cjs/{auth-api/storage.js → server/src/auth-api/storage.cjs} +0 -0
  118. /package/dist/cjs/{auth-api → server/src/auth-api}/storage.d.ts +0 -0
  119. /package/dist/cjs/{auth-api/types.js → server/src/auth-api/types.cjs} +0 -0
  120. /package/dist/cjs/{auth-api → server/src/auth-api}/types.d.ts +0 -0
  121. /package/dist/cjs/{auth-api → server/src/auth-api}/user-id.d.ts +0 -0
  122. /package/dist/cjs/{auth-cookie-options.js → server/src/auth-cookie-options.cjs} +0 -0
  123. /package/dist/cjs/{oauth → server/src/oauth}/models.d.ts +0 -0
  124. /package/dist/cjs/{oauth/types.js → server/src/oauth/types.cjs} +0 -0
  125. /package/dist/cjs/{oauth → server/src/oauth}/types.d.ts +0 -0
  126. /package/dist/cjs/{passkey/config.js → server/src/passkey/config.cjs} +0 -0
  127. /package/dist/cjs/{passkey → server/src/passkey}/config.d.ts +0 -0
  128. /package/dist/cjs/{passkey → server/src/passkey}/memory.d.ts +0 -0
  129. /package/dist/cjs/{passkey → server/src/passkey}/models.d.ts +0 -0
  130. /package/dist/cjs/{passkey → server/src/passkey}/sequelize.d.ts +0 -0
  131. /package/dist/cjs/{passkey/types.js → server/src/passkey/types.cjs} +0 -0
  132. /package/dist/cjs/{passkey → server/src/passkey}/types.d.ts +0 -0
  133. /package/dist/cjs/{sequelize-utils.d.ts → server/src/sequelize-utils.d.ts} +0 -0
  134. /package/dist/cjs/{token → server/src/token}/memory.d.ts +0 -0
  135. /package/dist/cjs/{token → server/src/token}/sequelize.d.ts +0 -0
  136. /package/dist/cjs/{token/types.js → server/src/token/types.cjs} +0 -0
  137. /package/dist/cjs/{token → server/src/token}/types.d.ts +0 -0
  138. /package/dist/cjs/{user/types.js → server/src/upload/types.cjs} +0 -0
  139. /package/dist/cjs/{user → server/src/user}/memory.d.ts +0 -0
  140. /package/dist/cjs/{user → server/src/user}/sequelize.d.ts +0 -0
  141. /package/dist/cjs/{user → server/src/user}/types.d.ts +0 -0
  142. /package/dist/esm/{auth-api → server/src/auth-api}/compat-auth-storage.d.ts +0 -0
  143. /package/dist/esm/{auth-api → server/src/auth-api}/mem-auth-store.d.ts +0 -0
  144. /package/dist/esm/{auth-api → server/src/auth-api}/mem-auth-store.js +0 -0
  145. /package/dist/esm/{auth-api → server/src/auth-api}/module.d.ts +0 -0
  146. /package/dist/esm/{auth-api → server/src/auth-api}/module.js +0 -0
  147. /package/dist/esm/{auth-api → server/src/auth-api}/sql-auth-store.d.ts +0 -0
  148. /package/dist/esm/{auth-api → server/src/auth-api}/sql-auth-store.js +0 -0
  149. /package/dist/esm/{auth-api → server/src/auth-api}/storage.d.ts +0 -0
  150. /package/dist/esm/{auth-api → server/src/auth-api}/storage.js +0 -0
  151. /package/dist/esm/{auth-api → server/src/auth-api}/types.d.ts +0 -0
  152. /package/dist/esm/{auth-api → server/src/auth-api}/types.js +0 -0
  153. /package/dist/esm/{auth-api → server/src/auth-api}/user-id.d.ts +0 -0
  154. /package/dist/esm/{auth-cookie-options.js → server/src/auth-cookie-options.js} +0 -0
  155. /package/dist/esm/{oauth → server/src/oauth}/models.d.ts +0 -0
  156. /package/dist/esm/{oauth → server/src/oauth}/models.js +0 -0
  157. /package/dist/esm/{oauth → server/src/oauth}/types.d.ts +0 -0
  158. /package/dist/esm/{oauth → server/src/oauth}/types.js +0 -0
  159. /package/dist/esm/{passkey → server/src/passkey}/config.d.ts +0 -0
  160. /package/dist/esm/{passkey → server/src/passkey}/config.js +0 -0
  161. /package/dist/esm/{passkey → server/src/passkey}/memory.d.ts +0 -0
  162. /package/dist/esm/{passkey → server/src/passkey}/memory.js +0 -0
  163. /package/dist/esm/{passkey → server/src/passkey}/models.d.ts +0 -0
  164. /package/dist/esm/{passkey → server/src/passkey}/models.js +0 -0
  165. /package/dist/esm/{passkey → server/src/passkey}/sequelize.d.ts +0 -0
  166. /package/dist/esm/{passkey → server/src/passkey}/sequelize.js +0 -0
  167. /package/dist/esm/{passkey → server/src/passkey}/types.d.ts +0 -0
  168. /package/dist/esm/{passkey → server/src/passkey}/types.js +0 -0
  169. /package/dist/esm/{sequelize-utils.d.ts → server/src/sequelize-utils.d.ts} +0 -0
  170. /package/dist/esm/{token → server/src/token}/memory.d.ts +0 -0
  171. /package/dist/esm/{token → server/src/token}/sequelize.d.ts +0 -0
  172. /package/dist/esm/{token → server/src/token}/types.d.ts +0 -0
  173. /package/dist/esm/{token → server/src/token}/types.js +0 -0
  174. /package/dist/esm/{user → server/src/upload}/types.js +0 -0
  175. /package/dist/esm/{user → server/src/user}/memory.d.ts +0 -0
  176. /package/dist/esm/{user → server/src/user}/sequelize.d.ts +0 -0
  177. /package/dist/esm/{user → server/src/user}/types.d.ts +0 -0
@@ -10,7 +10,7 @@ interface CanImpersonateContext<UserEntity> {
10
10
  targetUser: UserEntity;
11
11
  effectiveUserId: AuthIdentifier;
12
12
  }
13
- type AuthRateLimitEndpoint = 'login' | 'passkey-challenge' | 'oauth-token';
13
+ type AuthRateLimitEndpoint = 'login' | 'passkey-challenge' | 'oauth-token' | 'oauth-authorize';
14
14
  interface AuthModuleOptions<UserEntity> {
15
15
  namespace?: string;
16
16
  defaultDomain?: string;
@@ -3,9 +3,14 @@ import { isoBase64URL } from '@simplewebauthn/server/helpers';
3
3
  import { ApiError } from '../api-server-base.js';
4
4
  import { buildAuthCookieOptions } from '../auth-cookie-options.js';
5
5
  import { BaseAuthModule } from './module.js';
6
+ import { loginBodySchema, refreshBodySchema, logoutBodySchema, whoamiBodySchema, passkeyChallengeBodySchema, passkeyVerifyBodySchema, passkeyCredentialParamsSchema, impersonateBodySchema, deleteImpersonationQuerySchema, oauthProviderParamsSchema, oauthStartBodySchema, oauthAuthorizeBodySchema, oauthTokenBodySchema } from './schemas.js';
6
7
  import { BaseAuthAdapter } from './storage.js';
7
8
  function isAuthIdentifier(value) {
8
- return typeof value === 'string' || typeof value === 'number';
9
+ if (typeof value === 'string')
10
+ return value.length > 0;
11
+ if (typeof value === 'number')
12
+ return Number.isFinite(value);
13
+ return false;
9
14
  }
10
15
  function toStringOrNull(value) {
11
16
  if (typeof value === 'string') {
@@ -42,7 +47,7 @@ class AuthModule extends BaseAuthModule {
42
47
  this.defaultDomain = options.defaultDomain;
43
48
  this.canImpersonateHook = options.canImpersonate;
44
49
  this.rateLimitHook = options.rateLimit;
45
- this.allowInsecurePkcePlain = options.allowInsecurePkcePlain ?? true;
50
+ this.allowInsecurePkcePlain = options.allowInsecurePkcePlain ?? false;
46
51
  }
47
52
  get storage() {
48
53
  return this.server.getAuthStorage();
@@ -58,16 +63,17 @@ class AuthModule extends BaseAuthModule {
58
63
  targetUser,
59
64
  effectiveUserId
60
65
  });
61
- if (allowed) {
66
+ if (allowed === true)
62
67
  return true;
63
- }
68
+ if (allowed === false)
69
+ return false;
64
70
  }
65
71
  const storageWithHook = this.storage;
66
72
  if (typeof storageWithHook.canImpersonate === 'function') {
67
73
  const allowed = await storageWithHook.canImpersonate({ realUserId, effectiveUserId });
68
74
  return !!allowed;
69
75
  }
70
- return realUserId === effectiveUserId;
76
+ return String(realUserId) === String(effectiveUserId);
71
77
  }
72
78
  async ensureImpersonationAllowed(apiReq, realUser, targetUser) {
73
79
  const permitted = await this.canImpersonate(apiReq, realUser, targetUser);
@@ -255,6 +261,9 @@ class AuthModule extends BaseAuthModule {
255
261
  const accessMaxAge = Math.max(1, conf.accessExpiry) * 1000;
256
262
  const refreshSeconds = Math.max(1, preferences.refreshTtlSeconds ?? conf.refreshExpiry);
257
263
  const refreshMaxAge = refreshSeconds * 1000;
264
+ // When sessionCookie is true we omit maxAge so the browser deletes the
265
+ // cookie on close. The server-side JWT still has its own expiry, which
266
+ // limits exposure if the browser crashes without running its cleanup.
258
267
  const accessOptions = sessionCookie ? options : { ...options, maxAge: accessMaxAge };
259
268
  const refreshOptions = sessionCookie ? options : { ...options, maxAge: refreshMaxAge };
260
269
  if (tokens.accessToken) {
@@ -344,19 +353,10 @@ class AuthModule extends BaseAuthModule {
344
353
  }
345
354
  parseLoginBody(apiReq) {
346
355
  const body = (apiReq.req.body ?? {});
347
- const login = toStringOrNull(body.login);
348
- const password = toStringOrNull(body.password);
356
+ // login and password are guaranteed present and non-empty by JSON Schema
357
+ const login = body.login;
358
+ const password = body.password;
349
359
  const sessionPrefs = this.resolveSessionPreferences(body.keepSession);
350
- if (!login || !password) {
351
- const errors = {};
352
- if (!login) {
353
- errors.login = 'Login is required';
354
- }
355
- if (!password) {
356
- errors.password = 'Password is required';
357
- }
358
- throw new ApiError({ code: 400, message: 'Missing credentials', errors });
359
- }
360
360
  return {
361
361
  login,
362
362
  password,
@@ -451,7 +451,9 @@ class AuthModule extends BaseAuthModule {
451
451
  const { login, password, ...metadata } = this.parseLoginBody(apiReq);
452
452
  const user = await this.storage.getUser(login);
453
453
  const hash = user ? this.storage.getUserPasswordHash(user) : '';
454
- const verified = user ? await this.storage.verifyPassword(password, hash) : false;
454
+ // Reject users with no password hash (e.g. OAuth/passkey-only accounts) before
455
+ // calling verifyPassword, since bcrypt behaviour on an empty hash is undefined.
456
+ const verified = user && hash ? await this.storage.verifyPassword(password, hash) : false;
455
457
  if (!user || !verified) {
456
458
  throw new ApiError({
457
459
  code: 400,
@@ -482,6 +484,13 @@ class AuthModule extends BaseAuthModule {
482
484
  message: verify.error ?? 'Unable to verify refresh token'
483
485
  });
484
486
  }
487
+ // Delete the token immediately after verification to narrow the TOCTOU window.
488
+ // This must happen before the slower getUserOrThrow call.
489
+ const deleted = await this.storage.deleteToken({ refreshToken: providedToken });
490
+ if (deleted === 0) {
491
+ // Another concurrent request already consumed this refresh token.
492
+ throw new ApiError({ code: 401, message: 'Invalid refresh token' });
493
+ }
485
494
  const user = await this.getUserOrThrow(stored.userId ?? verify.data.uid, 'User not found');
486
495
  const metadata = {
487
496
  domain: stored.domain,
@@ -497,7 +506,6 @@ class AuthModule extends BaseAuthModule {
497
506
  refreshTtlSeconds: sessionPrefs.refreshTtlSeconds ?? stored.refreshTtlSeconds,
498
507
  sessionCookie: sessionPrefs.sessionCookie ?? stored.sessionCookie
499
508
  };
500
- await this.storage.deleteToken({ refreshToken: providedToken });
501
509
  const pair = await this.issueTokens(apiReq, user, metadata);
502
510
  const publicUser = this.storage.filterUser(user);
503
511
  return [200, { ...pair, user: publicUser }];
@@ -537,8 +545,7 @@ class AuthModule extends BaseAuthModule {
537
545
  apiReq.req.cookies[conf.accessCookie].trim().length > 0);
538
546
  const shouldRefresh = Boolean(body.refresh) || !hasAccessToken;
539
547
  if (shouldRefresh) {
540
- const updateToken = this.storage.updateToken;
541
- if (typeof updateToken !== 'function' || !this.storageImplements('updateToken')) {
548
+ if (typeof this.storage.updateToken !== 'function' || !this.storageImplements('updateToken')) {
542
549
  throw new ApiError({ code: 501, message: 'Token update storage is not configured' });
543
550
  }
544
551
  // Sign a new access token without embedding stored token secrets into the JWT payload.
@@ -562,7 +569,7 @@ class AuthModule extends BaseAuthModule {
562
569
  if (!access.success || !access.token) {
563
570
  throw new ApiError({ code: 500, message: access.error ?? 'Unable to sign access token' });
564
571
  }
565
- const updated = await updateToken.call(this.storage, {
572
+ const updated = await this.storage.updateToken({
566
573
  refreshToken,
567
574
  accessToken: access.token,
568
575
  lastSeenAt: new Date()
@@ -608,12 +615,10 @@ class AuthModule extends BaseAuthModule {
608
615
  throw new ApiError({ code: 501, message: 'Passkey support is not configured' });
609
616
  }
610
617
  const body = (apiReq.req.body ?? {});
611
- const action = toStringOrNull(body.action);
612
- if (action !== 'register' && action !== 'authenticate') {
613
- throw new ApiError({ code: 400, message: 'Passkey action must be "register" or "authenticate"' });
614
- }
618
+ // action is guaranteed to be 'register' | 'authenticate' by JSON Schema
619
+ const action = body.action;
615
620
  const params = {
616
- action,
621
+ action: action,
617
622
  login: toStringOrNull(body.login) ?? undefined,
618
623
  userId: isAuthIdentifier(body.userId) ? body.userId : undefined
619
624
  };
@@ -626,11 +631,9 @@ class AuthModule extends BaseAuthModule {
626
631
  }
627
632
  const body = (apiReq.req.body ?? {});
628
633
  const sessionPrefs = this.resolveSessionPreferences(body.keepSession);
629
- const expectedChallenge = toStringOrNull(body.expectedChallenge);
634
+ // expectedChallenge (string) and response (object) are guaranteed by JSON Schema
635
+ const expectedChallenge = body.expectedChallenge;
630
636
  const response = body.response;
631
- if (!expectedChallenge || typeof response !== 'object' || response === null) {
632
- throw new ApiError({ code: 400, message: 'Malformed passkey verification payload' });
633
- }
634
637
  const rawMetadata = {
635
638
  domain: toStringOrNull(body.domain) ?? undefined,
636
639
  fingerprint: toStringOrNull(body.fingerprint) ?? undefined,
@@ -740,6 +743,15 @@ class AuthModule extends BaseAuthModule {
740
743
  }
741
744
  async deleteImpersonation(apiReq) {
742
745
  this.assertAuthReady();
746
+ if (!apiReq.isImpersonating()) {
747
+ throw new ApiError({ code: 400, message: 'Not currently impersonating' });
748
+ }
749
+ // Revoke the active impersonation refresh token before issuing new real-user tokens
750
+ // so that a captured impersonation token cannot be reused after impersonation ends.
751
+ const impersonationRefreshToken = this.extractRefreshToken(apiReq, {});
752
+ if (impersonationRefreshToken) {
753
+ await this.storage.deleteToken({ refreshToken: impersonationRefreshToken });
754
+ }
743
755
  const actor = await this.resolveActorContext(apiReq);
744
756
  const query = (apiReq.req.query ?? {});
745
757
  const metadata = this.buildImpersonationMetadata(query);
@@ -776,9 +788,7 @@ class AuthModule extends BaseAuthModule {
776
788
  ? apiReq.req.body.extras
777
789
  : undefined
778
790
  };
779
- if (!params.provider) {
780
- throw new ApiError({ code: 400, message: 'OAuth provider is required' });
781
- }
791
+ // provider is guaranteed present and non-empty by params schema
782
792
  const result = await this.server.initiateOAuth(params);
783
793
  return [200, result];
784
794
  }
@@ -791,9 +801,6 @@ class AuthModule extends BaseAuthModule {
791
801
  query: apiReq.req.query,
792
802
  body: (apiReq.req.body ?? {})
793
803
  };
794
- if (!params.provider) {
795
- throw new ApiError({ code: 400, message: 'OAuth provider is required' });
796
- }
797
804
  const result = await this.server.completeOAuth(params);
798
805
  if (result.tokens?.accessToken && result.tokens.refreshToken) {
799
806
  this.setJwtCookies(apiReq, {
@@ -807,20 +814,16 @@ class AuthModule extends BaseAuthModule {
807
814
  if (typeof this.storage.getClient !== 'function' || typeof this.storage.createAuthCode !== 'function') {
808
815
  throw new ApiError({ code: 501, message: 'OAuth authorization storage is not configured' });
809
816
  }
817
+ await this.applyRateLimit(apiReq, 'oauth-authorize');
810
818
  const body = (apiReq.req.body ?? {});
811
- const clientId = toStringOrNull(body.clientId);
812
- const redirectUri = toStringOrNull(body.redirectUri);
819
+ // clientId and redirectUri are guaranteed present and non-empty by JSON Schema
820
+ const clientId = body.clientId;
821
+ const redirectUri = body.redirectUri;
813
822
  const scope = toScopeArray(body.scope) ?? [];
814
823
  const state = toStringOrNull(body.state) ?? undefined;
815
824
  const codeChallenge = toStringOrNull(body.codeChallenge) ?? undefined;
816
825
  const codeChallengeMethod = toStringOrNull(body.codeChallengeMethod) ?? undefined;
817
826
  const resolvedCodeChallengeMethod = this.resolvePkceChallengeMethod(codeChallengeMethod);
818
- if (!clientId) {
819
- throw new ApiError({ code: 400, message: 'clientId is required' });
820
- }
821
- if (!redirectUri) {
822
- throw new ApiError({ code: 400, message: 'redirectUri is required' });
823
- }
824
827
  const client = await this.storage.getClient(clientId);
825
828
  if (!client) {
826
829
  throw new ApiError({ code: 400, message: 'Unknown client_id' });
@@ -850,10 +853,8 @@ class AuthModule extends BaseAuthModule {
850
853
  throw new ApiError({ code: 501, message: 'OAuth token storage is not configured' });
851
854
  }
852
855
  const body = (apiReq.req.body ?? {});
853
- const grantType = toStringOrNull(body.grant_type);
854
- if (!grantType) {
855
- throw new ApiError({ code: 400, message: 'grant_type is required' });
856
- }
856
+ // grant_type is guaranteed to be 'authorization_code' | 'refresh_token' by JSON Schema
857
+ const grantType = body.grant_type;
857
858
  const { client, clientSecretProvided } = await this.resolveClientAuthentication(apiReq, body);
858
859
  switch (grantType) {
859
860
  case 'authorization_code':
@@ -907,7 +908,7 @@ class AuthModule extends BaseAuthModule {
907
908
  }
908
909
  }
909
910
  }
910
- else if (!clientSecretProvided && (client.hasSecret ?? Boolean(client.clientSecret))) {
911
+ else if (!clientSecretProvided && (client.hasSecret ?? false)) {
911
912
  throw new ApiError({ code: 400, message: 'Client authentication required when no PKCE challenge present' });
912
913
  }
913
914
  const user = await this.getUserOrThrow(record.userId, 'User not found');
@@ -941,8 +942,12 @@ class AuthModule extends BaseAuthModule {
941
942
  if (stored.clientId && stored.clientId !== client.clientId) {
942
943
  throw new ApiError({ code: 400, message: 'Refresh token issued to another client' });
943
944
  }
945
+ // Delete the token immediately after verification to narrow the TOCTOU window.
946
+ const deleted = await this.storage.deleteToken({ refreshToken });
947
+ if (deleted === 0) {
948
+ throw new ApiError({ code: 401, message: 'Invalid refresh token' });
949
+ }
944
950
  const user = await this.getUserOrThrow(stored.userId ?? verify.data.uid, 'User not found');
945
- await this.storage.deleteToken({ refreshToken });
946
951
  const tokens = await this.issueTokens(apiReq, user, {
947
952
  clientId: client.clientId,
948
953
  scope: stored.scope,
@@ -1014,7 +1019,7 @@ class AuthModule extends BaseAuthModule {
1014
1019
  if (!client) {
1015
1020
  throw new ApiError({ code: 400, message: 'Unknown client_id' });
1016
1021
  }
1017
- const requiresSecret = client.hasSecret ?? Boolean(client.clientSecret);
1022
+ const requiresSecret = client.hasSecret ?? false;
1018
1023
  if (requiresSecret) {
1019
1024
  if (!secretProvided) {
1020
1025
  throw new ApiError({ code: 400, message: 'Client authentication is required' });
@@ -1032,7 +1037,7 @@ class AuthModule extends BaseAuthModule {
1032
1037
  }
1033
1038
  assertRedirectUriAllowed(client, redirectUri) {
1034
1039
  if (client.redirectUris.length === 0) {
1035
- return;
1040
+ throw new ApiError({ code: 400, message: 'Client has no registered redirect URIs' });
1036
1041
  }
1037
1042
  if (!client.redirectUris.includes(redirectUri)) {
1038
1043
  throw new ApiError({ code: 400, message: 'redirect_uri not registered for client' });
@@ -1041,9 +1046,13 @@ class AuthModule extends BaseAuthModule {
1041
1046
  async resolveUserForOAuth(apiReq, body) {
1042
1047
  const refreshToken = this.extractRefreshToken(apiReq, body);
1043
1048
  if (refreshToken) {
1049
+ const verify = this.server.jwtVerify(refreshToken, this.server.config.refreshSecret);
1050
+ if (!verify.success || !verify.data) {
1051
+ throw new ApiError({ code: 401, message: 'Invalid or expired refresh token' });
1052
+ }
1044
1053
  const stored = await this.storage.getToken({ refreshToken });
1045
1054
  if (stored) {
1046
- return this.getUserOrThrow(stored.userId, 'User not found for authorization');
1055
+ return this.getUserOrThrow(stored.userId ?? verify.data.uid, 'User not found for authorization');
1047
1056
  }
1048
1057
  }
1049
1058
  const login = toStringOrNull(body.login);
@@ -1051,7 +1060,7 @@ class AuthModule extends BaseAuthModule {
1051
1060
  if (login && password) {
1052
1061
  const user = await this.storage.getUser(login);
1053
1062
  const hash = user ? this.storage.getUserPasswordHash(user) : '';
1054
- const verified = user ? await this.storage.verifyPassword(password, hash) : false;
1063
+ const verified = user && hash ? await this.storage.verifyPassword(password, hash) : false;
1055
1064
  if (!user || !verified) {
1056
1065
  throw new ApiError({ code: 400, message: 'Invalid credentials' });
1057
1066
  }
@@ -1067,8 +1076,7 @@ class AuthModule extends BaseAuthModule {
1067
1076
  if (storageHints.adapter?.passkeyService || storageHints.adapter?.passkeyStore) {
1068
1077
  return true;
1069
1078
  }
1070
- const serverHints = this.server;
1071
- return !!serverHints.passkeyServiceAdapter;
1079
+ return false;
1072
1080
  }
1073
1081
  hasOAuthStore() {
1074
1082
  const storageHints = this.storage;
@@ -1078,8 +1086,7 @@ class AuthModule extends BaseAuthModule {
1078
1086
  if (storageHints.adapter?.oauthStore) {
1079
1087
  return true;
1080
1088
  }
1081
- const serverHints = this.server;
1082
- return !!serverHints.oauthStoreAdapter;
1089
+ return false;
1083
1090
  }
1084
1091
  storageImplements(key) {
1085
1092
  const candidate = this.storage[key];
@@ -1129,32 +1136,38 @@ class AuthModule extends BaseAuthModule {
1129
1136
  method: 'post',
1130
1137
  path: '/v1/login',
1131
1138
  handler: (req) => this.postLogin(req),
1132
- auth: { type: 'none', req: 'any' }
1139
+ auth: { type: 'none', req: 'any' },
1140
+ schema: { body: loginBodySchema }
1133
1141
  }, {
1134
1142
  method: 'post',
1135
1143
  path: '/v1/refresh',
1136
1144
  handler: (req) => this.postRefresh(req),
1137
- auth: { type: 'none', req: 'any' }
1145
+ auth: { type: 'none', req: 'any' },
1146
+ schema: { body: refreshBodySchema }
1138
1147
  }, {
1139
1148
  method: 'post',
1140
1149
  path: '/v1/logout',
1141
1150
  handler: (req) => this.postLogout(req),
1142
- auth: { type: 'maybe', req: 'any' }
1151
+ auth: { type: 'maybe', req: 'any' },
1152
+ schema: { body: logoutBodySchema }
1143
1153
  }, {
1144
1154
  method: 'post',
1145
1155
  path: '/v1/whoami',
1146
1156
  handler: (req) => this.postWhoAmI(req),
1147
- auth: { type: 'maybe', req: 'any' }
1157
+ auth: { type: 'maybe', req: 'any' },
1158
+ schema: { body: whoamiBodySchema }
1148
1159
  }, {
1149
1160
  method: 'post',
1150
1161
  path: '/v1/impersonations',
1151
1162
  handler: (req) => this.postImpersonation(req),
1152
- auth: { type: 'strict', req: 'any' }
1163
+ auth: { type: 'strict', req: 'any' },
1164
+ schema: { body: impersonateBodySchema }
1153
1165
  }, {
1154
1166
  method: 'delete',
1155
1167
  path: '/v1/impersonations',
1156
1168
  handler: (req) => this.deleteImpersonation(req),
1157
- auth: { type: 'strict', req: 'any' }
1169
+ auth: { type: 'strict', req: 'any' },
1170
+ schema: { querystring: deleteImpersonationQuerySchema }
1158
1171
  });
1159
1172
  const passkeysSupported = this.hasPasskeyService() &&
1160
1173
  this.storageImplements('createPasskeyChallenge') &&
@@ -1167,12 +1180,14 @@ class AuthModule extends BaseAuthModule {
1167
1180
  method: 'post',
1168
1181
  path: '/v1/passkeys/challenge',
1169
1182
  handler: (req) => this.postPasskeyChallenge(req),
1170
- auth: { type: 'none', req: 'any' }
1183
+ auth: { type: 'none', req: 'any' },
1184
+ schema: { body: passkeyChallengeBodySchema }
1171
1185
  }, {
1172
1186
  method: 'post',
1173
1187
  path: '/v1/passkeys/verify',
1174
1188
  handler: (req) => this.postPasskeyVerify(req),
1175
- auth: { type: 'none', req: 'any' }
1189
+ auth: { type: 'none', req: 'any' },
1190
+ schema: { body: passkeyVerifyBodySchema }
1176
1191
  });
1177
1192
  if (passkeyCredentialsSupported) {
1178
1193
  routes.push({
@@ -1184,7 +1199,8 @@ class AuthModule extends BaseAuthModule {
1184
1199
  method: 'delete',
1185
1200
  path: '/v1/passkeys/:credentialId',
1186
1201
  handler: (req) => this.deletePasskey(req),
1187
- auth: { type: 'strict', req: 'any' }
1202
+ auth: { type: 'strict', req: 'any' },
1203
+ schema: { params: passkeyCredentialParamsSchema }
1188
1204
  });
1189
1205
  }
1190
1206
  }
@@ -1194,12 +1210,14 @@ class AuthModule extends BaseAuthModule {
1194
1210
  method: 'post',
1195
1211
  path: '/v1/oauth2/:provider/start',
1196
1212
  handler: (req) => this.postOAuthStart(req),
1197
- auth: { type: 'none', req: 'any' }
1213
+ auth: { type: 'none', req: 'any' },
1214
+ schema: { body: oauthStartBodySchema, params: oauthProviderParamsSchema }
1198
1215
  }, {
1199
1216
  method: 'post',
1200
1217
  path: '/v1/oauth2/:provider/callback',
1201
1218
  handler: (req) => this.postOAuthCallback(req),
1202
- auth: { type: 'none', req: 'any' }
1219
+ auth: { type: 'none', req: 'any' },
1220
+ schema: { params: oauthProviderParamsSchema }
1203
1221
  });
1204
1222
  }
1205
1223
  const oauthStorageSupported = this.hasOAuthStore() &&
@@ -1211,12 +1229,14 @@ class AuthModule extends BaseAuthModule {
1211
1229
  method: 'post',
1212
1230
  path: '/v1/oauth2/authorize',
1213
1231
  handler: (req) => this.postOAuthAuthorize(req),
1214
- auth: { type: 'maybe', req: 'any' }
1232
+ auth: { type: 'maybe', req: 'any' },
1233
+ schema: { body: oauthAuthorizeBodySchema }
1215
1234
  }, {
1216
1235
  method: 'post',
1217
1236
  path: '/v1/oauth2/token',
1218
1237
  handler: (req) => this.postOAuthToken(req),
1219
- auth: { type: 'none', req: 'any' }
1238
+ auth: { type: 'none', req: 'any' },
1239
+ schema: { body: oauthTokenBodySchema }
1220
1240
  });
1221
1241
  }
1222
1242
  return routes;
@@ -109,8 +109,8 @@ export class CompositeAuthAdapter {
109
109
  if (!this.oauthStore) {
110
110
  return null;
111
111
  }
112
- const consumed = await this.oauthStore.consumeAuthCode(code);
113
- if (!consumed || consumed.clientId !== clientId) {
112
+ const consumed = await this.oauthStore.consumeAuthCode(code, clientId);
113
+ if (!consumed) {
114
114
  return null;
115
115
  }
116
116
  return consumed;
@@ -119,6 +119,6 @@ export class CompositeAuthAdapter {
119
119
  if (this.canImpersonateFn) {
120
120
  return !!(await this.canImpersonateFn(params));
121
121
  }
122
- return params.realUserId === params.effectiveUserId;
122
+ return String(params.realUserId) === String(params.effectiveUserId);
123
123
  }
124
124
  }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * JSON Schema definitions for auth module routes.
3
+ * These are the runtime validation source-of-truth; TypeScript interfaces
4
+ * in auth-module.ts remain for handler-internal typing only.
5
+ */
6
+ /** Loose JSON Schema object type used for Fastify route schema definitions. */
7
+ type JsonSchema = Record<string, unknown>;
8
+ export declare const loginBodySchema: JsonSchema;
9
+ export declare const refreshBodySchema: JsonSchema;
10
+ export declare const logoutBodySchema: JsonSchema;
11
+ export declare const whoamiBodySchema: JsonSchema;
12
+ export declare const passkeyChallengeBodySchema: JsonSchema;
13
+ export declare const passkeyVerifyBodySchema: JsonSchema;
14
+ export declare const passkeyCredentialParamsSchema: JsonSchema;
15
+ export declare const impersonateBodySchema: JsonSchema;
16
+ export declare const deleteImpersonationQuerySchema: JsonSchema;
17
+ export declare const oauthProviderParamsSchema: JsonSchema;
18
+ export declare const oauthStartBodySchema: JsonSchema;
19
+ export declare const oauthAuthorizeBodySchema: JsonSchema;
20
+ export declare const oauthTokenBodySchema: JsonSchema;
21
+ export {};
@@ -0,0 +1,168 @@
1
+ /**
2
+ * JSON Schema definitions for auth module routes.
3
+ * These are the runtime validation source-of-truth; TypeScript interfaces
4
+ * in auth-module.ts remain for handler-internal typing only.
5
+ */
6
+ /* ------------------------------------------------------------------ */
7
+ /* Shared fragments */
8
+ /* ------------------------------------------------------------------ */
9
+ const tokenMetadataProperties = {
10
+ domain: { type: 'string' },
11
+ fingerprint: { type: 'string' },
12
+ label: { type: 'string' },
13
+ browser: { type: 'string' },
14
+ device: { type: 'string' },
15
+ ip: { type: 'string' },
16
+ os: { type: 'string' }
17
+ };
18
+ const keepSessionProperty = {
19
+ keepSession: { type: ['boolean', 'number', 'string'] }
20
+ };
21
+ function authIdentifierProperty(name) {
22
+ return { [name]: { type: ['string', 'number'] } };
23
+ }
24
+ /* ------------------------------------------------------------------ */
25
+ /* Auth route schemas */
26
+ /* ------------------------------------------------------------------ */
27
+ export const loginBodySchema = {
28
+ type: 'object',
29
+ required: ['login', 'password'],
30
+ properties: {
31
+ login: { type: 'string', minLength: 1 },
32
+ password: { type: 'string', minLength: 1 },
33
+ ...tokenMetadataProperties,
34
+ ...keepSessionProperty
35
+ },
36
+ additionalProperties: true
37
+ };
38
+ export const refreshBodySchema = {
39
+ type: 'object',
40
+ properties: {
41
+ refreshToken: { type: 'string' },
42
+ domain: { type: 'string' },
43
+ fingerprint: { type: 'string' },
44
+ label: { type: 'string' },
45
+ ...keepSessionProperty
46
+ },
47
+ additionalProperties: false
48
+ };
49
+ export const logoutBodySchema = {
50
+ type: 'object',
51
+ properties: {
52
+ token: { type: 'string' },
53
+ refreshToken: { type: 'string' }
54
+ },
55
+ additionalProperties: false
56
+ };
57
+ export const whoamiBodySchema = {
58
+ type: 'object',
59
+ properties: {
60
+ refreshToken: { type: 'string' },
61
+ refresh: { type: 'boolean' }
62
+ },
63
+ additionalProperties: false
64
+ };
65
+ /* ------------------------------------------------------------------ */
66
+ /* Passkey schemas */
67
+ /* ------------------------------------------------------------------ */
68
+ export const passkeyChallengeBodySchema = {
69
+ type: 'object',
70
+ required: ['action'],
71
+ properties: {
72
+ action: { type: 'string', enum: ['register', 'authenticate'] },
73
+ login: { type: 'string' },
74
+ ...authIdentifierProperty('userId')
75
+ },
76
+ additionalProperties: false
77
+ };
78
+ export const passkeyVerifyBodySchema = {
79
+ type: 'object',
80
+ required: ['expectedChallenge', 'response'],
81
+ properties: {
82
+ expectedChallenge: { type: 'string' },
83
+ response: { type: 'object' },
84
+ login: { type: 'string' },
85
+ ...authIdentifierProperty('userId'),
86
+ userAgent: { type: 'string' },
87
+ ...tokenMetadataProperties,
88
+ ...keepSessionProperty
89
+ },
90
+ additionalProperties: true
91
+ };
92
+ export const passkeyCredentialParamsSchema = {
93
+ type: 'object',
94
+ required: ['credentialId'],
95
+ properties: {
96
+ credentialId: { type: 'string', minLength: 1 }
97
+ }
98
+ };
99
+ /* ------------------------------------------------------------------ */
100
+ /* Impersonation schemas */
101
+ /* ------------------------------------------------------------------ */
102
+ export const impersonateBodySchema = {
103
+ type: 'object',
104
+ properties: {
105
+ ...authIdentifierProperty('userId'),
106
+ login: { type: 'string' },
107
+ ...tokenMetadataProperties,
108
+ ...keepSessionProperty,
109
+ clientId: { type: 'string' },
110
+ scope: {},
111
+ loginType: { type: 'string' }
112
+ },
113
+ additionalProperties: true
114
+ };
115
+ export const deleteImpersonationQuerySchema = {
116
+ type: 'object',
117
+ additionalProperties: true
118
+ };
119
+ /* ------------------------------------------------------------------ */
120
+ /* OAuth schemas */
121
+ /* ------------------------------------------------------------------ */
122
+ export const oauthProviderParamsSchema = {
123
+ type: 'object',
124
+ required: ['provider'],
125
+ properties: {
126
+ provider: { type: 'string', minLength: 1 }
127
+ }
128
+ };
129
+ export const oauthStartBodySchema = {
130
+ type: 'object',
131
+ properties: {
132
+ redirectUri: { type: 'string' },
133
+ scope: {},
134
+ state: { type: 'string' },
135
+ extras: { type: 'object' }
136
+ },
137
+ additionalProperties: true
138
+ };
139
+ export const oauthAuthorizeBodySchema = {
140
+ type: 'object',
141
+ required: ['clientId', 'redirectUri'],
142
+ properties: {
143
+ clientId: { type: 'string', minLength: 1 },
144
+ redirectUri: { type: 'string', minLength: 1 },
145
+ scope: {},
146
+ state: { type: 'string' },
147
+ codeChallenge: { type: 'string' },
148
+ codeChallengeMethod: { type: 'string' },
149
+ login: { type: 'string' },
150
+ password: { type: 'string' }
151
+ },
152
+ additionalProperties: false
153
+ };
154
+ export const oauthTokenBodySchema = {
155
+ type: 'object',
156
+ required: ['grant_type'],
157
+ properties: {
158
+ grant_type: { type: 'string', enum: ['authorization_code', 'refresh_token'] },
159
+ code: { type: 'string' },
160
+ redirect_uri: { type: 'string' },
161
+ code_verifier: { type: 'string' },
162
+ client_id: { type: 'string' },
163
+ client_secret: { type: 'string' },
164
+ refresh_token: { type: 'string' },
165
+ scope: { type: 'string' }
166
+ },
167
+ additionalProperties: false
168
+ };
@@ -8,7 +8,12 @@ export function normalizeComparableUserId(identifier) {
8
8
  throw new Error(`Unable to normalise user identifier: ${identifier}`);
9
9
  }
10
10
  if (/^\d+$/.test(trimmed)) {
11
- return String(Number(trimmed));
11
+ const num = Number(trimmed);
12
+ // Avoid precision loss for large numeric strings
13
+ if (!Number.isSafeInteger(num)) {
14
+ return trimmed;
15
+ }
16
+ return String(num);
12
17
  }
13
18
  return trimmed;
14
19
  }
@@ -17,9 +22,13 @@ export function normalizeComparableUserId(identifier) {
17
22
  export function normalizeNumericUserId(identifier) {
18
23
  const normalized = normalizeComparableUserId(identifier);
19
24
  if (/^\d+$/.test(normalized)) {
20
- return Number(normalized);
25
+ const num = Number(normalized);
26
+ if (!Number.isSafeInteger(num)) {
27
+ throw new Error(`Sequelize OAuth/Passkey store requires numeric user IDs within safe integer range: ${identifier}`);
28
+ }
29
+ return num;
21
30
  }
22
- throw new Error(`Unable to normalise user identifier: ${identifier}`);
31
+ throw new Error(`Sequelize OAuth/Passkey store requires numeric user IDs: ${identifier}`);
23
32
  }
24
33
  export function normalizeStringUserId(identifier) {
25
34
  return normalizeComparableUserId(identifier);