@technomoron/api-server-base 2.0.0-beta.18 → 2.0.0-beta.19

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 (58) hide show
  1. package/dist/cjs/api-module.cjs +9 -0
  2. package/dist/cjs/api-module.d.ts +4 -2
  3. package/dist/cjs/api-server-base.cjs +59 -37
  4. package/dist/cjs/api-server-base.d.ts +5 -0
  5. package/dist/cjs/auth-api/auth-module.d.ts +12 -1
  6. package/dist/cjs/auth-api/auth-module.js +42 -34
  7. package/dist/cjs/auth-api/mem-auth-store.js +2 -24
  8. package/dist/cjs/auth-api/sql-auth-store.js +4 -32
  9. package/dist/cjs/auth-api/user-id.d.ts +4 -0
  10. package/dist/cjs/auth-api/user-id.js +31 -0
  11. package/dist/cjs/auth-cookie-options.d.ts +11 -0
  12. package/dist/cjs/auth-cookie-options.js +57 -0
  13. package/dist/cjs/oauth/memory.js +2 -9
  14. package/dist/cjs/oauth/models.js +4 -15
  15. package/dist/cjs/oauth/sequelize.js +6 -22
  16. package/dist/cjs/passkey/config.d.ts +2 -0
  17. package/dist/cjs/passkey/config.js +26 -0
  18. package/dist/cjs/passkey/memory.js +2 -9
  19. package/dist/cjs/passkey/models.js +4 -15
  20. package/dist/cjs/passkey/sequelize.js +6 -22
  21. package/dist/cjs/sequelize-utils.d.ts +3 -0
  22. package/dist/cjs/sequelize-utils.js +17 -0
  23. package/dist/cjs/token/memory.d.ts +4 -0
  24. package/dist/cjs/token/memory.js +90 -25
  25. package/dist/cjs/token/sequelize.js +16 -22
  26. package/dist/cjs/token/types.d.ts +7 -0
  27. package/dist/cjs/user/memory.js +2 -9
  28. package/dist/cjs/user/sequelize.js +6 -22
  29. package/dist/esm/api-module.d.ts +4 -2
  30. package/dist/esm/api-module.js +9 -0
  31. package/dist/esm/api-server-base.d.ts +5 -0
  32. package/dist/esm/api-server-base.js +59 -37
  33. package/dist/esm/auth-api/auth-module.d.ts +12 -1
  34. package/dist/esm/auth-api/auth-module.js +42 -34
  35. package/dist/esm/auth-api/mem-auth-store.js +1 -23
  36. package/dist/esm/auth-api/sql-auth-store.js +2 -30
  37. package/dist/esm/auth-api/user-id.d.ts +4 -0
  38. package/dist/esm/auth-api/user-id.js +26 -0
  39. package/dist/esm/auth-cookie-options.d.ts +11 -0
  40. package/dist/esm/auth-cookie-options.js +54 -0
  41. package/dist/esm/oauth/memory.js +2 -9
  42. package/dist/esm/oauth/models.js +1 -12
  43. package/dist/esm/oauth/sequelize.js +3 -19
  44. package/dist/esm/passkey/config.d.ts +2 -0
  45. package/dist/esm/passkey/config.js +23 -0
  46. package/dist/esm/passkey/memory.js +2 -9
  47. package/dist/esm/passkey/models.js +1 -12
  48. package/dist/esm/passkey/sequelize.js +3 -19
  49. package/dist/esm/sequelize-utils.d.ts +3 -0
  50. package/dist/esm/sequelize-utils.js +12 -0
  51. package/dist/esm/token/memory.d.ts +4 -0
  52. package/dist/esm/token/memory.js +90 -25
  53. package/dist/esm/token/sequelize.js +12 -18
  54. package/dist/esm/token/types.d.ts +7 -0
  55. package/dist/esm/user/memory.js +2 -9
  56. package/dist/esm/user/sequelize.js +3 -19
  57. package/docs/swagger/openapi.json +1 -1
  58. package/package.json +1 -1
@@ -2,6 +2,15 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ApiModule = void 0;
4
4
  class ApiModule {
5
+ get server() {
6
+ if (this._server === undefined) {
7
+ throw new Error('ApiModule.server is not set. Mount the module with ApiServer.api(...) before using it.');
8
+ }
9
+ return this._server;
10
+ }
11
+ set server(value) {
12
+ this._server = value;
13
+ }
5
14
  constructor(opts = {}) {
6
15
  this.mountpath = '';
7
16
  this.namespace = opts.namespace ?? this.constructor.defaultNamespace ?? '';
@@ -7,7 +7,7 @@ export interface ApiKey {
7
7
  uid: unknown;
8
8
  }
9
9
  export type ApiRoute = {
10
- method: 'get' | 'post' | 'put' | 'delete';
10
+ method: 'get' | 'post' | 'put' | 'patch' | 'delete';
11
11
  path: string;
12
12
  handler: ApiHandler;
13
13
  auth: {
@@ -16,7 +16,9 @@ export type ApiRoute = {
16
16
  };
17
17
  };
18
18
  export declare class ApiModule<T = unknown> {
19
- server: T;
19
+ private _server?;
20
+ get server(): T;
21
+ set server(value: T);
20
22
  namespace: string;
21
23
  mountpath: string;
22
24
  static defaultNamespace: string;
@@ -20,6 +20,7 @@ const express_1 = __importDefault(require("express"));
20
20
  const multer_1 = __importDefault(require("multer"));
21
21
  const module_js_1 = require("./auth-api/module.js");
22
22
  const storage_js_1 = require("./auth-api/storage.js");
23
+ const auth_cookie_options_js_1 = require("./auth-cookie-options.js");
23
24
  const base_js_1 = require("./token/base.js");
24
25
  class JwtHelperStore extends base_js_1.TokenStore {
25
26
  async save() {
@@ -77,6 +78,7 @@ function hydrateGetBody(req) {
77
78
  req.body = { ...query };
78
79
  return;
79
80
  }
81
+ // Keep explicit body fields authoritative when both query and body provide the same key.
80
82
  req.body = { ...query, ...body };
81
83
  }
82
84
  function normalizeIpAddress(candidate) {
@@ -337,6 +339,17 @@ function isApiErrorLike(candidate) {
337
339
  const maybeError = candidate;
338
340
  return typeof maybeError.code === 'number' && typeof maybeError.message === 'string';
339
341
  }
342
+ function asHttpStatus(error) {
343
+ if (!error || typeof error !== 'object') {
344
+ return null;
345
+ }
346
+ const maybe = error;
347
+ const status = typeof maybe.status === 'number' ? maybe.status : maybe.statusCode;
348
+ if (typeof status === 'number' && status >= 400 && status <= 599) {
349
+ return status;
350
+ }
351
+ return null;
352
+ }
340
353
  function fillConfig(config) {
341
354
  return {
342
355
  apiPort: config.apiPort ?? 3101,
@@ -369,7 +382,8 @@ function fillConfig(config) {
369
382
  apiVersion: config.apiVersion ?? '',
370
383
  minClientVersion: config.minClientVersion ?? '',
371
384
  tokenStore: config.tokenStore,
372
- authStores: config.authStores
385
+ authStores: config.authStores,
386
+ onStartError: config.onStartError
373
387
  };
374
388
  }
375
389
  class ApiServer {
@@ -382,17 +396,23 @@ class ApiServer {
382
396
  return null;
383
397
  }
384
398
  set currReq(_value) {
399
+ if (this.config.devMode && !this.currReqDeprecationWarned) {
400
+ this.currReqDeprecationWarned = true;
401
+ console.warn('[api-server-base] ApiServer.currReq is deprecated and always null. Use req.apiReq or res.locals.apiReq.');
402
+ }
385
403
  void _value;
386
404
  }
387
405
  constructor(config = {}) {
388
406
  this.finalized = false;
389
407
  this.serverAuthAdapter = null;
390
408
  this.apiNotFoundHandler = null;
409
+ this.apiErrorHandlerInstalled = false;
391
410
  this.tokenStoreAdapter = null;
392
411
  this.userStoreAdapter = null;
393
412
  this.passkeyServiceAdapter = null;
394
413
  this.oauthStoreAdapter = null;
395
414
  this.canImpersonateAdapter = null;
415
+ this.currReqDeprecationWarned = false;
396
416
  this.config = fillConfig(config);
397
417
  this.apiBasePath = this.normalizeApiBasePath(this.config.apiBasePath);
398
418
  this.startedAt = Date.now();
@@ -439,6 +459,7 @@ class ApiServer {
439
459
  this.app.use(this.apiBasePath, this.apiRouter);
440
460
  // addSwaggerUi(this.app);
441
461
  this.installApiNotFoundHandler();
462
+ this.installApiErrorHandler();
442
463
  }
443
464
  assertNotFinalized(action) {
444
465
  if (this.finalized) {
@@ -468,6 +489,7 @@ class ApiServer {
468
489
  }
469
490
  finalize() {
470
491
  this.installApiNotFoundHandler();
492
+ this.installApiErrorHandler();
471
493
  this.finalized = true;
472
494
  return this;
473
495
  }
@@ -810,8 +832,12 @@ class ApiServer {
810
832
  const base = this.apiBasePath === '/' ? '' : this.apiBasePath;
811
833
  const resolved = rawPath.length > 0 ? rawPath : `${base}/swagger`;
812
834
  const path = resolved.startsWith('/') ? resolved : `/${resolved}`;
813
- const spec = this.loadSwaggerSpec();
835
+ let specCache;
814
836
  this.app.get(path, (_req, res) => {
837
+ if (specCache === undefined) {
838
+ specCache = this.loadSwaggerSpec();
839
+ }
840
+ const spec = specCache;
815
841
  if (!spec) {
816
842
  res.status(500).json({
817
843
  success: false,
@@ -855,6 +881,13 @@ class ApiServer {
855
881
  };
856
882
  this.app.use(this.apiBasePath, this.apiNotFoundHandler);
857
883
  }
884
+ installApiErrorHandler() {
885
+ if (this.apiErrorHandlerInstalled) {
886
+ return;
887
+ }
888
+ this.apiErrorHandlerInstalled = true;
889
+ this.app.use(this.apiBasePath, this.expressErrorHandler());
890
+ }
858
891
  describeMissingEndpoint(req) {
859
892
  const method = typeof req.method === 'string' ? req.method.toUpperCase() : 'GET';
860
893
  const target = req.originalUrl || req.url || this.apiBasePath;
@@ -889,10 +922,17 @@ class ApiServer {
889
922
  }
890
923
  const err = new Error(message);
891
924
  err.cause = error;
925
+ if (typeof this.config.onStartError === 'function') {
926
+ this.config.onStartError(err);
927
+ return;
928
+ }
892
929
  throw err;
893
930
  });
894
931
  return this;
895
932
  }
933
+ internalServerErrorMessage(error) {
934
+ return this.config.debug ? this.guessExceptionText(error) : 'Internal server error';
935
+ }
896
936
  async verifyJWT(token) {
897
937
  if (!this.config.accessSecret) {
898
938
  return { tokenData: undefined, error: 'JWT authentication disabled; no jwtSecret set', expired: false };
@@ -907,39 +947,7 @@ class ApiServer {
907
947
  return { tokenData: result.data, error: undefined, expired: false };
908
948
  }
909
949
  jwtCookieOptions(apiReq) {
910
- const conf = this.config;
911
- const forwarded = apiReq.req.headers['x-forwarded-proto'];
912
- const referer = apiReq.req.headers['origin'] ?? apiReq.req.headers['referer'];
913
- const origin = typeof referer === 'string' ? referer : '';
914
- const forwardedProto = (typeof forwarded === 'string' ? forwarded : Array.isArray(forwarded) ? (forwarded[0] ?? '') : '')
915
- .split(',')[0]
916
- .trim()
917
- .toLowerCase();
918
- const isHttps = forwardedProto === 'https' || apiReq.req.protocol === 'https';
919
- const isLocalhost = origin.includes('localhost');
920
- const secure = conf.cookieSecure === true ? true : conf.cookieSecure === false ? false : /* auto */ Boolean(isHttps);
921
- let sameSite = conf.cookieSameSite ?? 'lax';
922
- if (sameSite !== 'lax' && sameSite !== 'strict' && sameSite !== 'none') {
923
- sameSite = 'lax';
924
- }
925
- let resolvedSecure = secure;
926
- if (sameSite === 'none' && resolvedSecure !== true) {
927
- // Modern browsers reject SameSite=None cookies unless Secure is set.
928
- resolvedSecure = true;
929
- }
930
- const options = {
931
- httpOnly: conf.cookieHttpOnly ?? true,
932
- secure: resolvedSecure,
933
- sameSite,
934
- domain: conf.cookieDomain || undefined,
935
- path: conf.cookiePath || '/',
936
- maxAge: undefined
937
- };
938
- if (conf.devMode && isLocalhost) {
939
- // Domain cookies do not work on localhost; avoid breaking local development when cookieDomain is set.
940
- options.domain = undefined;
941
- }
942
- return options;
950
+ return (0, auth_cookie_options_js_1.buildAuthCookieOptions)(this.config, apiReq.req);
943
951
  }
944
952
  setAccessCookie(apiReq, accessToken, sessionCookie) {
945
953
  const conf = this.config;
@@ -1285,10 +1293,21 @@ class ApiServer {
1285
1293
  res.status(apiError.code).json(errorPayload);
1286
1294
  return;
1287
1295
  }
1296
+ const status = asHttpStatus(error);
1297
+ if (status) {
1298
+ res.status(status).json({
1299
+ success: false,
1300
+ code: status,
1301
+ message: status === 400 ? 'Invalid request payload' : this.internalServerErrorMessage(error),
1302
+ data: null,
1303
+ errors: {}
1304
+ });
1305
+ return;
1306
+ }
1288
1307
  const errorPayload = {
1289
1308
  success: false,
1290
1309
  code: 500,
1291
- message: this.guessExceptionText(error),
1310
+ message: this.internalServerErrorMessage(error),
1292
1311
  data: null,
1293
1312
  errors: {}
1294
1313
  };
@@ -1348,7 +1367,7 @@ class ApiServer {
1348
1367
  const errorPayload = {
1349
1368
  success: false,
1350
1369
  code: 500,
1351
- message: this.guessExceptionText(error),
1370
+ message: this.internalServerErrorMessage(error),
1352
1371
  data: null,
1353
1372
  errors: {}
1354
1373
  };
@@ -1389,6 +1408,9 @@ class ApiServer {
1389
1408
  case 'put':
1390
1409
  router.put(r.path, handler);
1391
1410
  break;
1411
+ case 'patch':
1412
+ router.patch(r.path, handler);
1413
+ break;
1392
1414
  case 'delete':
1393
1415
  router.delete(r.path, handler);
1394
1416
  break;
@@ -129,6 +129,7 @@ export interface ApiServerConf {
129
129
  minClientVersion: string;
130
130
  tokenStore?: TokenStore;
131
131
  authStores?: ApiServerAuthStores;
132
+ onStartError?: (error: Error) => void;
132
133
  }
133
134
  export declare class ApiServer {
134
135
  app: Application;
@@ -141,12 +142,14 @@ export declare class ApiServer {
141
142
  private moduleAdapter;
142
143
  private serverAuthAdapter;
143
144
  private apiNotFoundHandler;
145
+ private apiErrorHandlerInstalled;
144
146
  private tokenStoreAdapter;
145
147
  private userStoreAdapter;
146
148
  private passkeyServiceAdapter;
147
149
  private oauthStoreAdapter;
148
150
  private canImpersonateAdapter;
149
151
  private readonly jwtHelper;
152
+ private currReqDeprecationWarned;
150
153
  /**
151
154
  * @deprecated ApiServer does not track a global "current request". This value is always null.
152
155
  * Use the per-request ApiRequest passed to handlers, or `req.apiReq` / `res.locals.apiReq`
@@ -225,8 +228,10 @@ export declare class ApiServer {
225
228
  private installSwaggerHandler;
226
229
  private normalizeApiBasePath;
227
230
  private installApiNotFoundHandler;
231
+ private installApiErrorHandler;
228
232
  private describeMissingEndpoint;
229
233
  start(): this;
234
+ private internalServerErrorMessage;
230
235
  private verifyJWT;
231
236
  private jwtCookieOptions;
232
237
  private setAccessCookie;
@@ -10,10 +10,16 @@ interface CanImpersonateContext<UserEntity> {
10
10
  targetUser: UserEntity;
11
11
  effectiveUserId: AuthIdentifier;
12
12
  }
13
+ type AuthRateLimitEndpoint = 'login' | 'passkey-challenge' | 'oauth-token';
13
14
  interface AuthModuleOptions<UserEntity> {
14
15
  namespace?: string;
15
16
  defaultDomain?: string;
16
17
  canImpersonate?: (context: CanImpersonateContext<UserEntity>) => Promise<boolean> | boolean;
18
+ rateLimit?: (context: {
19
+ apiReq: ApiRequest;
20
+ endpoint: AuthRateLimitEndpoint;
21
+ }) => Promise<void> | void;
22
+ allowInsecurePkcePlain?: boolean;
17
23
  }
18
24
  type TokenMetadata = Partial<Token> & {
19
25
  sessionCookie?: boolean;
@@ -42,9 +48,12 @@ type AuthCapableServer<PublicUser> = ApiServer & {
42
48
  };
43
49
  export default class AuthModule<UserEntity, PublicUser> extends BaseAuthModule<UserEntity> implements AuthProviderModule<UserEntity> {
44
50
  static defaultNamespace: string;
45
- server: AuthCapableServer<PublicUser>;
51
+ get server(): AuthCapableServer<PublicUser>;
52
+ set server(value: AuthCapableServer<PublicUser>);
46
53
  private readonly defaultDomain?;
47
54
  private readonly canImpersonateHook?;
55
+ private readonly rateLimitHook?;
56
+ private readonly allowInsecurePkcePlain;
48
57
  constructor(options?: AuthModuleOptions<UserEntity>);
49
58
  protected get storage(): AuthAdapter<UserEntity, PublicUser>;
50
59
  protected canImpersonate(apiReq: ApiRequest, realUser: UserEntity, targetUser: UserEntity): Promise<boolean>;
@@ -100,6 +109,8 @@ export default class AuthModule<UserEntity, PublicUser> extends BaseAuthModule<U
100
109
  private hasOAuthStore;
101
110
  private storageImplements;
102
111
  private storageImplementsAll;
112
+ private applyRateLimit;
113
+ private resolvePkceChallengeMethod;
103
114
  defineRoutes(): ApiRoute[];
104
115
  }
105
116
  export {};
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const node_crypto_1 = require("node:crypto");
4
4
  const helpers_1 = require("@simplewebauthn/server/helpers");
5
5
  const api_server_base_js_1 = require("../api-server-base.js");
6
+ const auth_cookie_options_js_1 = require("../auth-cookie-options.js");
6
7
  const module_js_1 = require("./module.js");
7
8
  const storage_js_1 = require("./storage.js");
8
9
  function isAuthIdentifier(value) {
@@ -32,10 +33,18 @@ function sha256Base64Url(value) {
32
33
  return base64UrlEncode(hash);
33
34
  }
34
35
  class AuthModule extends module_js_1.BaseAuthModule {
36
+ get server() {
37
+ return super.server;
38
+ }
39
+ set server(value) {
40
+ super.server = value;
41
+ }
35
42
  constructor(options = {}) {
36
43
  super({ namespace: options.namespace ?? AuthModule.defaultNamespace });
37
44
  this.defaultDomain = options.defaultDomain;
38
45
  this.canImpersonateHook = options.canImpersonate;
46
+ this.rateLimitHook = options.rateLimit;
47
+ this.allowInsecurePkcePlain = options.allowInsecurePkcePlain ?? true;
39
48
  }
40
49
  get storage() {
41
50
  return this.server.getAuthStorage();
@@ -236,37 +245,7 @@ class AuthModule extends module_js_1.BaseAuthModule {
236
245
  return Number.isNaN(date.getTime()) ? undefined : date.toISOString();
237
246
  }
238
247
  cookieOptions(apiReq) {
239
- const conf = this.server.config;
240
- const forwarded = apiReq.req.headers['x-forwarded-proto'];
241
- const referer = apiReq.req.headers['origin'] ?? apiReq.req.headers['referer'];
242
- const origin = typeof referer === 'string' ? referer : '';
243
- const forwardedProto = (typeof forwarded === 'string' ? forwarded : Array.isArray(forwarded) ? (forwarded[0] ?? '') : '')
244
- .split(',')[0]
245
- .trim()
246
- .toLowerCase();
247
- const isHttps = forwardedProto === 'https' || apiReq.req.protocol === 'https';
248
- const isLocalhost = origin.includes('localhost');
249
- const secure = conf.cookieSecure === true ? true : conf.cookieSecure === false ? false : /* auto */ Boolean(isHttps);
250
- let sameSite = conf.cookieSameSite ?? 'lax';
251
- if (sameSite !== 'lax' && sameSite !== 'strict' && sameSite !== 'none') {
252
- sameSite = 'lax';
253
- }
254
- let resolvedSecure = secure;
255
- if (sameSite === 'none' && resolvedSecure !== true) {
256
- resolvedSecure = true;
257
- }
258
- const options = {
259
- httpOnly: conf.cookieHttpOnly ?? true,
260
- secure: resolvedSecure,
261
- sameSite,
262
- domain: conf.cookieDomain || undefined,
263
- path: conf.cookiePath || '/',
264
- maxAge: undefined
265
- };
266
- if (conf.devMode && isLocalhost) {
267
- options.domain = undefined;
268
- }
269
- return options;
248
+ return (0, auth_cookie_options_js_1.buildAuthCookieOptions)(this.server.config, apiReq.req);
270
249
  }
271
250
  setJwtCookies(apiReq, tokens, preferences = {}) {
272
251
  const conf = this.server.config;
@@ -293,7 +272,10 @@ class AuthModule extends module_js_1.BaseAuthModule {
293
272
  async issueTokens(apiReq, user, metadata = {}) {
294
273
  const conf = this.server.config;
295
274
  const enrichedMetadata = this.enrichTokenMetadata(apiReq, metadata);
296
- const payload = this.buildTokenPayload(user, enrichedMetadata);
275
+ const payload = {
276
+ ...this.buildTokenPayload(user, enrichedMetadata),
277
+ jti: (0, node_crypto_1.randomUUID)()
278
+ };
297
279
  const access = this.server.jwtSign(payload, conf.accessSecret, conf.accessExpiry);
298
280
  if (!access.success || !access.token) {
299
281
  throw new api_server_base_js_1.ApiError({ code: 500, message: access.error ?? 'Unable to sign access token' });
@@ -463,6 +445,7 @@ class AuthModule extends module_js_1.BaseAuthModule {
463
445
  return undefined;
464
446
  }
465
447
  async postLogin(apiReq) {
448
+ await this.applyRateLimit(apiReq, 'login');
466
449
  this.assertAuthReady();
467
450
  const { login, password, ...metadata } = this.parseLoginBody(apiReq);
468
451
  const user = await this.storage.getUser(login);
@@ -626,6 +609,7 @@ class AuthModule extends module_js_1.BaseAuthModule {
626
609
  ];
627
610
  }
628
611
  async postPasskeyChallenge(apiReq) {
612
+ await this.applyRateLimit(apiReq, 'passkey-challenge');
629
613
  if (typeof this.storage.createPasskeyChallenge !== 'function') {
630
614
  throw new api_server_base_js_1.ApiError({ code: 501, message: 'Passkey support is not configured' });
631
615
  }
@@ -763,7 +747,8 @@ class AuthModule extends module_js_1.BaseAuthModule {
763
747
  async deleteImpersonation(apiReq) {
764
748
  this.assertAuthReady();
765
749
  const actor = await this.resolveActorContext(apiReq);
766
- const metadata = this.buildImpersonationMetadata((apiReq.req.body ?? {}));
750
+ const query = (apiReq.req.query ?? {});
751
+ const metadata = this.buildImpersonationMetadata(query);
767
752
  metadata.loginType = metadata.loginType ?? 'impersonation-end';
768
753
  const tokens = await this.issueTokens(apiReq, actor.user, metadata);
769
754
  const publicUser = this.storage.filterUser(actor.user);
@@ -835,6 +820,7 @@ class AuthModule extends module_js_1.BaseAuthModule {
835
820
  const state = toStringOrNull(body.state) ?? undefined;
836
821
  const codeChallenge = toStringOrNull(body.codeChallenge) ?? undefined;
837
822
  const codeChallengeMethod = toStringOrNull(body.codeChallengeMethod) ?? undefined;
823
+ const resolvedCodeChallengeMethod = this.resolvePkceChallengeMethod(codeChallengeMethod);
838
824
  if (!clientId) {
839
825
  throw new api_server_base_js_1.ApiError({ code: 400, message: 'clientId is required' });
840
826
  }
@@ -854,7 +840,7 @@ class AuthModule extends module_js_1.BaseAuthModule {
854
840
  redirectUri,
855
841
  scope: resolvedScope,
856
842
  codeChallenge,
857
- codeChallengeMethod: codeChallengeMethod === 'S256' ? 'S256' : codeChallengeMethod === 'plain' ? 'plain' : undefined,
843
+ codeChallengeMethod: resolvedCodeChallengeMethod,
858
844
  expiresInSeconds: 300
859
845
  });
860
846
  const redirect = new URL(redirectUri);
@@ -865,6 +851,7 @@ class AuthModule extends module_js_1.BaseAuthModule {
865
851
  return [200, { code: codeRecord.code, redirectUri: redirect.toString(), state }];
866
852
  }
867
853
  async postOAuthToken(apiReq) {
854
+ await this.applyRateLimit(apiReq, 'oauth-token');
868
855
  if (typeof this.storage.getClient !== 'function' || typeof this.storage.consumeAuthCode !== 'function') {
869
856
  throw new api_server_base_js_1.ApiError({ code: 501, message: 'OAuth token storage is not configured' });
870
857
  }
@@ -918,6 +905,9 @@ class AuthModule extends module_js_1.BaseAuthModule {
918
905
  }
919
906
  }
920
907
  else if (record.codeChallengeMethod === 'plain') {
908
+ if (!this.allowInsecurePkcePlain) {
909
+ throw new api_server_base_js_1.ApiError({ code: 400, message: 'PKCE plain is not permitted' });
910
+ }
921
911
  if (codeVerifier !== record.codeChallenge) {
922
912
  throw new api_server_base_js_1.ApiError({ code: 400, message: 'code_verifier does not match challenge' });
923
913
  }
@@ -1118,6 +1108,24 @@ class AuthModule extends module_js_1.BaseAuthModule {
1118
1108
  storageImplementsAll(keys) {
1119
1109
  return keys.every((key) => this.storageImplements(key));
1120
1110
  }
1111
+ async applyRateLimit(apiReq, endpoint) {
1112
+ if (!this.rateLimitHook) {
1113
+ return;
1114
+ }
1115
+ await this.rateLimitHook({ apiReq, endpoint });
1116
+ }
1117
+ resolvePkceChallengeMethod(value) {
1118
+ if (value === 'S256') {
1119
+ return 'S256';
1120
+ }
1121
+ if (value === 'plain') {
1122
+ if (!this.allowInsecurePkcePlain) {
1123
+ throw new api_server_base_js_1.ApiError({ code: 400, message: 'PKCE plain is not permitted' });
1124
+ }
1125
+ return 'plain';
1126
+ }
1127
+ return undefined;
1128
+ }
1121
1129
  defineRoutes() {
1122
1130
  const routes = [];
1123
1131
  const coreAuthSupported = this.storageImplementsAll([
@@ -2,33 +2,11 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.MemAuthStore = void 0;
4
4
  const memory_js_1 = require("../oauth/memory.js");
5
+ const config_js_1 = require("../passkey/config.js");
5
6
  const memory_js_2 = require("../passkey/memory.js");
6
7
  const memory_js_3 = require("../token/memory.js");
7
8
  const memory_js_4 = require("../user/memory.js");
8
9
  const compat_auth_storage_js_1 = require("./compat-auth-storage.js");
9
- const DEFAULT_PASSKEY_CONFIG = {
10
- rpId: 'localhost',
11
- rpName: 'API Server',
12
- origins: ['http://localhost:5173'],
13
- timeoutMs: 5 * 60 * 1000,
14
- userVerification: 'preferred'
15
- };
16
- function isOriginString(origin) {
17
- return typeof origin === 'string' && origin.trim().length > 0;
18
- }
19
- function normalizePasskeyConfig(config = {}) {
20
- const candidateOrigins = Array.isArray(config.origins) && config.origins.length > 0 ? config.origins.filter(isOriginString) : null;
21
- return {
22
- rpId: config.rpId?.trim() || DEFAULT_PASSKEY_CONFIG.rpId,
23
- rpName: config.rpName?.trim() || DEFAULT_PASSKEY_CONFIG.rpName,
24
- origins: candidateOrigins ? candidateOrigins.map((origin) => origin.trim()) : DEFAULT_PASSKEY_CONFIG.origins,
25
- timeoutMs: typeof config.timeoutMs === 'number' && config.timeoutMs > 0
26
- ? config.timeoutMs
27
- : DEFAULT_PASSKEY_CONFIG.timeoutMs,
28
- userVerification: config.userVerification ?? DEFAULT_PASSKEY_CONFIG.userVerification,
29
- debug: Boolean(config.debug)
30
- };
31
- }
32
10
  class MemAuthStore {
33
11
  constructor(params = {}) {
34
12
  this.userStore = new memory_js_4.MemoryUserStore({
@@ -43,7 +21,7 @@ class MemAuthStore {
43
21
  let passkeyStore;
44
22
  let passkeyConfig;
45
23
  if (params.passkeys !== false) {
46
- passkeyConfig = normalizePasskeyConfig(params.passkeys ?? {});
24
+ passkeyConfig = (0, config_js_1.normalizePasskeyConfig)(params.passkeys ?? {});
47
25
  const resolveUser = async (lookup) => {
48
26
  const found = await this.userStore.findUser(lookup.userId ?? lookup.login ?? '');
49
27
  if (!found) {
@@ -2,49 +2,21 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.SqlAuthStore = void 0;
4
4
  const sequelize_js_1 = require("../oauth/sequelize.js");
5
+ const config_js_1 = require("../passkey/config.js");
5
6
  const sequelize_js_2 = require("../passkey/sequelize.js");
7
+ const sequelize_utils_js_1 = require("../sequelize-utils.js");
6
8
  const sequelize_js_3 = require("../token/sequelize.js");
7
9
  const sequelize_js_4 = require("../user/sequelize.js");
8
10
  const compat_auth_storage_js_1 = require("./compat-auth-storage.js");
9
- const DEFAULT_PASSKEY_CONFIG = {
10
- rpId: 'localhost',
11
- rpName: 'API Server',
12
- origins: ['http://localhost:5173'],
13
- timeoutMs: 5 * 60 * 1000,
14
- userVerification: 'preferred'
15
- };
16
- function normalizeTablePrefix(prefix) {
17
- if (!prefix) {
18
- return undefined;
19
- }
20
- const trimmed = prefix.trim();
21
- return trimmed.length > 0 ? trimmed : undefined;
22
- }
23
11
  function resolveTablePrefix(...prefixes) {
24
12
  for (const prefix of prefixes) {
25
- const normalized = normalizeTablePrefix(prefix);
13
+ const normalized = (0, sequelize_utils_js_1.normalizeTablePrefix)(prefix);
26
14
  if (normalized) {
27
15
  return normalized;
28
16
  }
29
17
  }
30
18
  return undefined;
31
19
  }
32
- function isOriginString(origin) {
33
- return typeof origin === 'string' && origin.trim().length > 0;
34
- }
35
- function normalizePasskeyConfig(config = {}) {
36
- const candidateOrigins = Array.isArray(config.origins) && config.origins.length > 0 ? config.origins.filter(isOriginString) : null;
37
- return {
38
- rpId: config.rpId?.trim() || DEFAULT_PASSKEY_CONFIG.rpId,
39
- rpName: config.rpName?.trim() || DEFAULT_PASSKEY_CONFIG.rpName,
40
- origins: candidateOrigins ? candidateOrigins.map((origin) => origin.trim()) : DEFAULT_PASSKEY_CONFIG.origins,
41
- timeoutMs: typeof config.timeoutMs === 'number' && config.timeoutMs > 0
42
- ? config.timeoutMs
43
- : DEFAULT_PASSKEY_CONFIG.timeoutMs,
44
- userVerification: config.userVerification ?? DEFAULT_PASSKEY_CONFIG.userVerification,
45
- debug: Boolean(config.debug)
46
- };
47
- }
48
20
  class SqlAuthStore {
49
21
  constructor(params) {
50
22
  this.closed = false;
@@ -84,7 +56,7 @@ class SqlAuthStore {
84
56
  let passkeyConfig;
85
57
  if (params.passkeys !== false) {
86
58
  const passkeyTablePrefix = resolveTablePrefix(moduleTablePrefixes.passkey, params.tablePrefix);
87
- passkeyConfig = normalizePasskeyConfig(params.passkeys ?? {});
59
+ passkeyConfig = (0, config_js_1.normalizePasskeyConfig)(params.passkeys ?? {});
88
60
  const resolveUser = async (lookup) => {
89
61
  const found = await this.userStore.findUser(lookup.userId ?? lookup.login ?? '');
90
62
  if (!found) {
@@ -0,0 +1,4 @@
1
+ import type { AuthIdentifier } from './types.js';
2
+ export declare function normalizeComparableUserId(identifier: AuthIdentifier): string;
3
+ export declare function normalizeNumericUserId(identifier: AuthIdentifier): number;
4
+ export declare function normalizeStringUserId(identifier: AuthIdentifier): string;
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.normalizeComparableUserId = normalizeComparableUserId;
4
+ exports.normalizeNumericUserId = normalizeNumericUserId;
5
+ exports.normalizeStringUserId = normalizeStringUserId;
6
+ function normalizeComparableUserId(identifier) {
7
+ if (typeof identifier === 'number' && Number.isFinite(identifier)) {
8
+ return String(identifier);
9
+ }
10
+ if (typeof identifier === 'string') {
11
+ const trimmed = identifier.trim();
12
+ if (trimmed.length === 0) {
13
+ throw new Error(`Unable to normalise user identifier: ${identifier}`);
14
+ }
15
+ if (/^\d+$/.test(trimmed)) {
16
+ return String(Number(trimmed));
17
+ }
18
+ return trimmed;
19
+ }
20
+ throw new Error(`Unable to normalise user identifier: ${identifier}`);
21
+ }
22
+ function normalizeNumericUserId(identifier) {
23
+ const normalized = normalizeComparableUserId(identifier);
24
+ if (/^\d+$/.test(normalized)) {
25
+ return Number(normalized);
26
+ }
27
+ throw new Error(`Unable to normalise user identifier: ${identifier}`);
28
+ }
29
+ function normalizeStringUserId(identifier) {
30
+ return normalizeComparableUserId(identifier);
31
+ }
@@ -0,0 +1,11 @@
1
+ import type { Request } from 'express';
2
+ import type { CookieOptions } from 'express-serve-static-core';
3
+ export interface AuthCookieConfig {
4
+ cookieSecure?: boolean | 'auto';
5
+ cookieSameSite?: 'lax' | 'strict' | 'none';
6
+ cookieHttpOnly?: boolean;
7
+ cookieDomain?: string;
8
+ cookiePath?: string;
9
+ devMode?: boolean;
10
+ }
11
+ export declare function buildAuthCookieOptions(config: AuthCookieConfig, req: Pick<Request, 'headers' | 'protocol'>): CookieOptions;