@technomoron/api-server-base 2.0.0-beta.17 → 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 (63) hide show
  1. package/README.txt +48 -35
  2. package/dist/cjs/api-module.cjs +9 -0
  3. package/dist/cjs/api-module.d.ts +4 -2
  4. package/dist/cjs/api-server-base.cjs +178 -57
  5. package/dist/cjs/api-server-base.d.ts +31 -2
  6. package/dist/cjs/auth-api/auth-module.d.ts +12 -1
  7. package/dist/cjs/auth-api/auth-module.js +77 -35
  8. package/dist/cjs/auth-api/mem-auth-store.js +2 -23
  9. package/dist/cjs/auth-api/sql-auth-store.js +4 -31
  10. package/dist/cjs/auth-api/user-id.d.ts +4 -0
  11. package/dist/cjs/auth-api/user-id.js +31 -0
  12. package/dist/cjs/auth-cookie-options.d.ts +11 -0
  13. package/dist/cjs/auth-cookie-options.js +57 -0
  14. package/dist/cjs/oauth/memory.js +4 -10
  15. package/dist/cjs/oauth/models.js +4 -15
  16. package/dist/cjs/oauth/sequelize.js +8 -23
  17. package/dist/cjs/passkey/config.d.ts +2 -0
  18. package/dist/cjs/passkey/config.js +26 -0
  19. package/dist/cjs/passkey/memory.js +2 -9
  20. package/dist/cjs/passkey/models.js +4 -15
  21. package/dist/cjs/passkey/sequelize.js +6 -22
  22. package/dist/cjs/passkey/service.js +1 -1
  23. package/dist/cjs/passkey/types.d.ts +5 -0
  24. package/dist/cjs/sequelize-utils.d.ts +3 -0
  25. package/dist/cjs/sequelize-utils.js +17 -0
  26. package/dist/cjs/token/memory.d.ts +4 -0
  27. package/dist/cjs/token/memory.js +90 -25
  28. package/dist/cjs/token/sequelize.js +16 -22
  29. package/dist/cjs/token/types.d.ts +7 -0
  30. package/dist/cjs/user/memory.js +2 -9
  31. package/dist/cjs/user/sequelize.js +6 -22
  32. package/dist/esm/api-module.d.ts +4 -2
  33. package/dist/esm/api-module.js +9 -0
  34. package/dist/esm/api-server-base.d.ts +31 -2
  35. package/dist/esm/api-server-base.js +178 -57
  36. package/dist/esm/auth-api/auth-module.d.ts +12 -1
  37. package/dist/esm/auth-api/auth-module.js +77 -35
  38. package/dist/esm/auth-api/mem-auth-store.js +1 -22
  39. package/dist/esm/auth-api/sql-auth-store.js +2 -29
  40. package/dist/esm/auth-api/user-id.d.ts +4 -0
  41. package/dist/esm/auth-api/user-id.js +26 -0
  42. package/dist/esm/auth-cookie-options.d.ts +11 -0
  43. package/dist/esm/auth-cookie-options.js +54 -0
  44. package/dist/esm/oauth/memory.js +4 -10
  45. package/dist/esm/oauth/models.js +1 -12
  46. package/dist/esm/oauth/sequelize.js +5 -20
  47. package/dist/esm/passkey/config.d.ts +2 -0
  48. package/dist/esm/passkey/config.js +23 -0
  49. package/dist/esm/passkey/memory.js +2 -9
  50. package/dist/esm/passkey/models.js +1 -12
  51. package/dist/esm/passkey/sequelize.js +3 -19
  52. package/dist/esm/passkey/service.js +1 -1
  53. package/dist/esm/passkey/types.d.ts +5 -0
  54. package/dist/esm/sequelize-utils.d.ts +3 -0
  55. package/dist/esm/sequelize-utils.js +12 -0
  56. package/dist/esm/token/memory.d.ts +4 -0
  57. package/dist/esm/token/memory.js +90 -25
  58. package/dist/esm/token/sequelize.js +12 -18
  59. package/dist/esm/token/types.d.ts +7 -0
  60. package/dist/esm/user/memory.js +2 -9
  61. package/dist/esm/user/sequelize.js +3 -19
  62. package/docs/swagger/openapi.json +11 -145
  63. package/package.json +12 -12
@@ -100,7 +100,21 @@ export interface ApiServerConf {
100
100
  swaggerPath?: string;
101
101
  accessSecret: string;
102
102
  refreshSecret: string;
103
+ /** Cookie domain for auth cookies. Prefer leaving empty for localhost/development. */
103
104
  cookieDomain: string;
105
+ /** Cookie path for auth cookies. */
106
+ cookiePath?: string;
107
+ /** Cookie SameSite attribute for auth cookies. */
108
+ cookieSameSite?: 'lax' | 'strict' | 'none';
109
+ /**
110
+ * Cookie Secure attribute for auth cookies.
111
+ * - true: always secure
112
+ * - false: never secure
113
+ * - 'auto': secure when request is HTTPS (or forwarded as HTTPS)
114
+ */
115
+ cookieSecure?: boolean | 'auto';
116
+ /** Cookie HttpOnly attribute for auth cookies. */
117
+ cookieHttpOnly?: boolean;
104
118
  accessCookie: string;
105
119
  refreshCookie: string;
106
120
  accessExpiry: number;
@@ -115,24 +129,38 @@ export interface ApiServerConf {
115
129
  minClientVersion: string;
116
130
  tokenStore?: TokenStore;
117
131
  authStores?: ApiServerAuthStores;
132
+ onStartError?: (error: Error) => void;
118
133
  }
119
134
  export declare class ApiServer {
120
135
  app: Application;
121
- currReq: ApiRequest | null;
122
136
  readonly config: ApiServerConf;
123
137
  readonly startedAt: number;
124
138
  private readonly apiBasePath;
139
+ private readonly apiRouter;
140
+ private finalized;
125
141
  private storageAdapter;
126
142
  private moduleAdapter;
127
143
  private serverAuthAdapter;
128
144
  private apiNotFoundHandler;
145
+ private apiErrorHandlerInstalled;
129
146
  private tokenStoreAdapter;
130
147
  private userStoreAdapter;
131
148
  private passkeyServiceAdapter;
132
149
  private oauthStoreAdapter;
133
150
  private canImpersonateAdapter;
134
151
  private readonly jwtHelper;
152
+ private currReqDeprecationWarned;
153
+ /**
154
+ * @deprecated ApiServer does not track a global "current request". This value is always null.
155
+ * Use the per-request ApiRequest passed to handlers, or `req.apiReq` / `res.locals.apiReq`
156
+ * when mounting raw Express endpoints.
157
+ */
158
+ get currReq(): ApiRequest | null;
159
+ set currReq(_value: ApiRequest | null);
135
160
  constructor(config?: Partial<ApiServerConf>);
161
+ private assertNotFinalized;
162
+ private toApiRouterPath;
163
+ finalize(): this;
136
164
  authStorage<UserRow, SafeUser>(storage: AuthAdapter<UserRow, SafeUser>): this;
137
165
  /**
138
166
  * @deprecated Use {@link ApiServer.authStorage} instead.
@@ -200,9 +228,10 @@ export declare class ApiServer {
200
228
  private installSwaggerHandler;
201
229
  private normalizeApiBasePath;
202
230
  private installApiNotFoundHandler;
203
- private ensureApiNotFoundOrdering;
231
+ private installApiErrorHandler;
204
232
  private describeMissingEndpoint;
205
233
  start(): this;
234
+ private internalServerErrorMessage;
206
235
  private verifyJWT;
207
236
  private jwtCookieOptions;
208
237
  private setAccessCookie;
@@ -14,6 +14,7 @@ import express from 'express';
14
14
  import multer from 'multer';
15
15
  import { nullAuthModule } from './auth-api/module.js';
16
16
  import { nullAuthAdapter } from './auth-api/storage.js';
17
+ import { buildAuthCookieOptions } from './auth-cookie-options.js';
17
18
  import { TokenStore } from './token/base.js';
18
19
  class JwtHelperStore extends TokenStore {
19
20
  async save() {
@@ -70,6 +71,7 @@ function hydrateGetBody(req) {
70
71
  req.body = { ...query };
71
72
  return;
72
73
  }
74
+ // Keep explicit body fields authoritative when both query and body provide the same key.
73
75
  req.body = { ...query, ...body };
74
76
  }
75
77
  function normalizeIpAddress(candidate) {
@@ -329,6 +331,17 @@ function isApiErrorLike(candidate) {
329
331
  const maybeError = candidate;
330
332
  return typeof maybeError.code === 'number' && typeof maybeError.message === 'string';
331
333
  }
334
+ function asHttpStatus(error) {
335
+ if (!error || typeof error !== 'object') {
336
+ return null;
337
+ }
338
+ const maybe = error;
339
+ const status = typeof maybe.status === 'number' ? maybe.status : maybe.statusCode;
340
+ if (typeof status === 'number' && status >= 400 && status <= 599) {
341
+ return status;
342
+ }
343
+ return null;
344
+ }
332
345
  function fillConfig(config) {
333
346
  return {
334
347
  apiPort: config.apiPort ?? 3101,
@@ -343,7 +356,11 @@ function fillConfig(config) {
343
356
  swaggerPath: config.swaggerPath ?? '',
344
357
  accessSecret: config.accessSecret ?? '',
345
358
  refreshSecret: config.refreshSecret ?? '',
346
- cookieDomain: config.cookieDomain ?? '.somewhere-over-the-rainbow.com',
359
+ cookieDomain: config.cookieDomain ?? '',
360
+ cookiePath: config.cookiePath ?? '/',
361
+ cookieSameSite: config.cookieSameSite ?? 'lax',
362
+ cookieSecure: config.cookieSecure ?? 'auto',
363
+ cookieHttpOnly: config.cookieHttpOnly ?? true,
347
364
  accessCookie: config.accessCookie ?? 'dat',
348
365
  refreshCookie: config.refreshCookie ?? 'drt',
349
366
  accessExpiry: config.accessExpiry ?? 60 * 15,
@@ -357,19 +374,37 @@ function fillConfig(config) {
357
374
  apiVersion: config.apiVersion ?? '',
358
375
  minClientVersion: config.minClientVersion ?? '',
359
376
  tokenStore: config.tokenStore,
360
- authStores: config.authStores
377
+ authStores: config.authStores,
378
+ onStartError: config.onStartError
361
379
  };
362
380
  }
363
381
  export class ApiServer {
382
+ /**
383
+ * @deprecated ApiServer does not track a global "current request". This value is always null.
384
+ * Use the per-request ApiRequest passed to handlers, or `req.apiReq` / `res.locals.apiReq`
385
+ * when mounting raw Express endpoints.
386
+ */
387
+ get currReq() {
388
+ return null;
389
+ }
390
+ set currReq(_value) {
391
+ if (this.config.devMode && !this.currReqDeprecationWarned) {
392
+ this.currReqDeprecationWarned = true;
393
+ console.warn('[api-server-base] ApiServer.currReq is deprecated and always null. Use req.apiReq or res.locals.apiReq.');
394
+ }
395
+ void _value;
396
+ }
364
397
  constructor(config = {}) {
365
- this.currReq = null;
398
+ this.finalized = false;
366
399
  this.serverAuthAdapter = null;
367
400
  this.apiNotFoundHandler = null;
401
+ this.apiErrorHandlerInstalled = false;
368
402
  this.tokenStoreAdapter = null;
369
403
  this.userStoreAdapter = null;
370
404
  this.passkeyServiceAdapter = null;
371
405
  this.oauthStoreAdapter = null;
372
406
  this.canImpersonateAdapter = null;
407
+ this.currReqDeprecationWarned = false;
373
408
  this.config = fillConfig(config);
374
409
  this.apiBasePath = this.normalizeApiBasePath(this.config.apiBasePath);
375
410
  this.startedAt = Date.now();
@@ -387,16 +422,68 @@ export class ApiServer {
387
422
  this.storageAdapter = this.getServerAuthAdapter();
388
423
  }
389
424
  this.app = express();
425
+ // Mount API modules and any custom endpoints under `apiBasePath` on this router so we can keep
426
+ // the API 404 handler ordered last without relying on Express internals.
427
+ this.apiRouter = express.Router();
390
428
  if (config.uploadPath) {
391
- const upload = multer({ dest: config.uploadPath });
429
+ const upload = multer({ dest: config.uploadPath, limits: { fileSize: this.config.uploadMax } });
392
430
  this.app.use(upload.any());
431
+ // Multer errors happen before ApiModule route wrappers; format the common "too large" failure.
432
+ this.app.use((err, _req, res, next) => {
433
+ const code = err && typeof err === 'object' ? err.code : undefined;
434
+ if (code === 'LIMIT_FILE_SIZE') {
435
+ res.status(413).json({
436
+ success: false,
437
+ code: 413,
438
+ message: `Upload exceeds maximum size of ${this.config.uploadMax} bytes`,
439
+ data: null,
440
+ errors: {}
441
+ });
442
+ return;
443
+ }
444
+ next(err);
445
+ });
393
446
  }
394
447
  this.middlewares();
395
448
  this.installStaticDirs();
396
449
  this.installPingHandler();
397
450
  this.installSwaggerHandler();
451
+ this.app.use(this.apiBasePath, this.apiRouter);
398
452
  // addSwaggerUi(this.app);
399
453
  this.installApiNotFoundHandler();
454
+ this.installApiErrorHandler();
455
+ }
456
+ assertNotFinalized(action) {
457
+ if (this.finalized) {
458
+ throw new Error(`Cannot call ApiServer.${action}() after finalize()/start()`);
459
+ }
460
+ }
461
+ toApiRouterPath(candidate) {
462
+ if (typeof candidate !== 'string') {
463
+ return null;
464
+ }
465
+ const trimmed = candidate.trim();
466
+ if (!trimmed) {
467
+ return null;
468
+ }
469
+ const normalized = trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
470
+ const base = this.apiBasePath;
471
+ if (base === '/') {
472
+ return normalized;
473
+ }
474
+ if (normalized === base) {
475
+ return '/';
476
+ }
477
+ if (normalized.startsWith(`${base}/`)) {
478
+ return normalized.slice(base.length) || '/';
479
+ }
480
+ return null;
481
+ }
482
+ finalize() {
483
+ this.installApiNotFoundHandler();
484
+ this.installApiErrorHandler();
485
+ this.finalized = true;
486
+ return this;
400
487
  }
401
488
  authStorage(storage) {
402
489
  this.storageAdapter = storage;
@@ -623,7 +710,7 @@ export class ApiServer {
623
710
  }
624
711
  return false;
625
712
  }
626
- guessExceptionText(error, defMsg = 'Unkown Error') {
713
+ guessExceptionText(error, defMsg = 'Unknown Error') {
627
714
  return guess_exception_text(error, defMsg);
628
715
  }
629
716
  async authorize(apiReq, requiredClass) {
@@ -649,6 +736,26 @@ export class ApiServer {
649
736
  credentials: true
650
737
  };
651
738
  this.app.use(cors(corsOptions));
739
+ // Provide a consistent and non-500 response when requests are blocked by the CORS allowlist.
740
+ this.app.use((err, req, res, next) => {
741
+ const message = err instanceof Error ? err.message : '';
742
+ if (message.includes('Not allowed by CORS')) {
743
+ const isApiRequest = Boolean(req.originalUrl?.startsWith(this.apiBasePath));
744
+ if (isApiRequest) {
745
+ res.status(403).json({
746
+ success: false,
747
+ code: 403,
748
+ message: 'Origin not allowed by CORS',
749
+ data: null,
750
+ errors: {}
751
+ });
752
+ return;
753
+ }
754
+ res.status(403).send('Origin not allowed by CORS');
755
+ return;
756
+ }
757
+ next(err);
758
+ });
652
759
  }
653
760
  installStaticDirs() {
654
761
  const staticDirs = this.config.staticDirs;
@@ -717,8 +824,12 @@ export class ApiServer {
717
824
  const base = this.apiBasePath === '/' ? '' : this.apiBasePath;
718
825
  const resolved = rawPath.length > 0 ? rawPath : `${base}/swagger`;
719
826
  const path = resolved.startsWith('/') ? resolved : `/${resolved}`;
720
- const spec = this.loadSwaggerSpec();
827
+ let specCache;
721
828
  this.app.get(path, (_req, res) => {
829
+ if (specCache === undefined) {
830
+ specCache = this.loadSwaggerSpec();
831
+ }
832
+ const spec = specCache;
722
833
  if (!spec) {
723
834
  res.status(500).json({
724
835
  success: false,
@@ -762,21 +873,12 @@ export class ApiServer {
762
873
  };
763
874
  this.app.use(this.apiBasePath, this.apiNotFoundHandler);
764
875
  }
765
- ensureApiNotFoundOrdering() {
766
- this.installApiNotFoundHandler();
767
- if (!this.apiNotFoundHandler) {
876
+ installApiErrorHandler() {
877
+ if (this.apiErrorHandlerInstalled) {
768
878
  return;
769
879
  }
770
- const stack = this.app._router?.stack;
771
- if (!stack) {
772
- return;
773
- }
774
- const index = stack.findIndex((layer) => layer.handle === this.apiNotFoundHandler);
775
- if (index === -1 || index === stack.length - 1) {
776
- return;
777
- }
778
- const [layer] = stack.splice(index, 1);
779
- stack.push(layer);
880
+ this.apiErrorHandlerInstalled = true;
881
+ this.app.use(this.apiBasePath, this.expressErrorHandler());
780
882
  }
781
883
  describeMissingEndpoint(req) {
782
884
  const method = typeof req.method === 'string' ? req.method.toUpperCase() : 'GET';
@@ -784,6 +886,10 @@ export class ApiServer {
784
886
  return `No such endpoint: ${method} ${target}`;
785
887
  }
786
888
  start() {
889
+ if (!this.finalized) {
890
+ console.warn('[api-server-base] ApiServer.start() called without finalize(); auto-finalizing. Call server.finalize() before start() to silence this warning.');
891
+ this.finalize();
892
+ }
787
893
  this.app
788
894
  .listen({
789
895
  port: this.config.apiPort,
@@ -793,22 +899,32 @@ export class ApiServer {
793
899
  console.log(`Server is running on http://${this.config.apiHost}:${this.config.apiPort}`);
794
900
  })
795
901
  .on('error', (error) => {
902
+ let message;
796
903
  if (error.code === 'EADDRINUSE') {
797
- console.error(`Error: Port ${this.config.apiPort} is already in use.`);
904
+ message = `Port ${this.config.apiPort} is already in use.`;
798
905
  }
799
906
  else if (error.code === 'EACCES') {
800
- console.error(`Error: Insufficient permissions to bind to port ${this.config.apiPort}. Try using a port number >= 1024 or running with elevated privileges.`);
907
+ message = `Insufficient permissions to bind to port ${this.config.apiPort}. Try using a port number >= 1024 or running with elevated privileges.`;
801
908
  }
802
909
  else if (error.code === 'EADDRNOTAVAIL') {
803
- console.error(`Error: Address ${this.config.apiHost} is not available on this machine.`);
910
+ message = `Address ${this.config.apiHost} is not available on this machine.`;
804
911
  }
805
912
  else {
806
- console.error(`Failed to start server: ${error.message}`);
913
+ message = `Failed to start server: ${error.message}`;
807
914
  }
808
- process.exit(1);
915
+ const err = new Error(message);
916
+ err.cause = error;
917
+ if (typeof this.config.onStartError === 'function') {
918
+ this.config.onStartError(err);
919
+ return;
920
+ }
921
+ throw err;
809
922
  });
810
923
  return this;
811
924
  }
925
+ internalServerErrorMessage(error) {
926
+ return this.config.debug ? this.guessExceptionText(error) : 'Internal server error';
927
+ }
812
928
  async verifyJWT(token) {
813
929
  if (!this.config.accessSecret) {
814
930
  return { tokenData: undefined, error: 'JWT authentication disabled; no jwtSecret set', expired: false };
@@ -823,29 +939,7 @@ export class ApiServer {
823
939
  return { tokenData: result.data, error: undefined, expired: false };
824
940
  }
825
941
  jwtCookieOptions(apiReq) {
826
- const conf = this.config;
827
- const forwarded = apiReq.req.headers['x-forwarded-proto'];
828
- const referer = apiReq.req.headers['origin'] ?? apiReq.req.headers['referer'];
829
- const origin = typeof referer === 'string' ? referer : '';
830
- const isHttps = forwarded === 'https' || apiReq.req.protocol === 'https';
831
- const isLocalhost = origin.includes('localhost');
832
- const options = {
833
- httpOnly: true,
834
- secure: true,
835
- sameSite: 'strict',
836
- domain: conf.cookieDomain || undefined,
837
- path: '/',
838
- maxAge: undefined
839
- };
840
- if (conf.devMode) {
841
- options.secure = isHttps;
842
- options.httpOnly = false;
843
- options.sameSite = 'lax';
844
- if (isLocalhost) {
845
- options.domain = undefined;
846
- }
847
- }
848
- return options;
942
+ return buildAuthCookieOptions(this.config, apiReq.req);
849
943
  }
850
944
  setAccessCookie(apiReq, accessToken, sessionCookie) {
851
945
  const conf = this.config;
@@ -991,7 +1085,7 @@ export class ApiServer {
991
1085
  }
992
1086
  }
993
1087
  if (!tokenData) {
994
- throw new ApiError({ code: 401, message: 'Unathorized Access - ' + error });
1088
+ throw new ApiError({ code: 401, message: 'Unauthorized Access - ' + error });
995
1089
  }
996
1090
  const effectiveUserId = this.extractTokenUserId(tokenData);
997
1091
  apiReq.realUid = this.resolveRealUserId(tokenData, effectiveUserId);
@@ -1043,6 +1137,11 @@ export class ApiServer {
1043
1137
  }
1044
1138
  apiReq.token = secret;
1045
1139
  apiReq.apiKey = key;
1140
+ // Treat API keys as authenticated identities, consistent with JWT-based flows.
1141
+ const resolvedUid = this.normalizeAuthIdentifier(key.uid);
1142
+ if (resolvedUid !== null) {
1143
+ apiReq.realUid = resolvedUid;
1144
+ }
1046
1145
  return {
1047
1146
  uid: key.uid,
1048
1147
  domain: '',
@@ -1102,13 +1201,19 @@ export class ApiServer {
1102
1201
  return rawReal;
1103
1202
  }
1104
1203
  useExpress(pathOrHandler, ...handlers) {
1204
+ this.assertNotFinalized('useExpress');
1105
1205
  if (typeof pathOrHandler === 'string') {
1106
- this.app.use(pathOrHandler, ...handlers);
1206
+ const apiPath = this.toApiRouterPath(pathOrHandler);
1207
+ if (apiPath) {
1208
+ this.apiRouter.use(apiPath, ...handlers);
1209
+ }
1210
+ else {
1211
+ this.app.use(pathOrHandler, ...handlers);
1212
+ }
1107
1213
  }
1108
1214
  else {
1109
1215
  this.app.use(pathOrHandler, ...handlers);
1110
1216
  }
1111
- this.ensureApiNotFoundOrdering();
1112
1217
  return this;
1113
1218
  }
1114
1219
  createApiRequest(req, res) {
@@ -1142,7 +1247,6 @@ export class ApiServer {
1142
1247
  const apiReq = this.createApiRequest(req, res);
1143
1248
  req.apiReq = apiReq;
1144
1249
  res.locals.apiReq = apiReq;
1145
- this.currReq = apiReq;
1146
1250
  try {
1147
1251
  if (this.config.hydrateGetBody) {
1148
1252
  hydrateGetBody(req);
@@ -1181,10 +1285,21 @@ export class ApiServer {
1181
1285
  res.status(apiError.code).json(errorPayload);
1182
1286
  return;
1183
1287
  }
1288
+ const status = asHttpStatus(error);
1289
+ if (status) {
1290
+ res.status(status).json({
1291
+ success: false,
1292
+ code: status,
1293
+ message: status === 400 ? 'Invalid request payload' : this.internalServerErrorMessage(error),
1294
+ data: null,
1295
+ errors: {}
1296
+ });
1297
+ return;
1298
+ }
1184
1299
  const errorPayload = {
1185
1300
  success: false,
1186
1301
  code: 500,
1187
- message: this.guessExceptionText(error),
1302
+ message: this.internalServerErrorMessage(error),
1188
1303
  data: null,
1189
1304
  errors: {}
1190
1305
  };
@@ -1195,7 +1310,6 @@ export class ApiServer {
1195
1310
  return async (req, res, next) => {
1196
1311
  void next;
1197
1312
  const apiReq = this.createApiRequest(req, res);
1198
- this.currReq = apiReq;
1199
1313
  try {
1200
1314
  if (this.config.hydrateGetBody) {
1201
1315
  hydrateGetBody(apiReq.req);
@@ -1245,7 +1359,7 @@ export class ApiServer {
1245
1359
  const errorPayload = {
1246
1360
  success: false,
1247
1361
  code: 500,
1248
- message: this.guessExceptionText(error),
1362
+ message: this.internalServerErrorMessage(error),
1249
1363
  data: null,
1250
1364
  errors: {}
1251
1365
  };
@@ -1258,13 +1372,18 @@ export class ApiServer {
1258
1372
  };
1259
1373
  }
1260
1374
  api(module) {
1375
+ this.assertNotFinalized('api');
1261
1376
  const router = express.Router();
1262
1377
  module.server = this;
1263
1378
  const moduleType = module.moduleType;
1264
1379
  if (moduleType === 'auth') {
1265
1380
  this.authModule(module);
1266
1381
  }
1267
- module.checkConfig();
1382
+ const configOk = module.checkConfig();
1383
+ if (configOk === false) {
1384
+ const name = module.constructor?.name ? String(module.constructor.name) : 'ApiModule';
1385
+ throw new Error(`${name}.checkConfig() returned false`);
1386
+ }
1268
1387
  const base = this.apiBasePath;
1269
1388
  const ns = module.namespace;
1270
1389
  const mountPath = `${base}${ns}`;
@@ -1281,6 +1400,9 @@ export class ApiServer {
1281
1400
  case 'put':
1282
1401
  router.put(r.path, handler);
1283
1402
  break;
1403
+ case 'patch':
1404
+ router.patch(r.path, handler);
1405
+ break;
1284
1406
  case 'delete':
1285
1407
  router.delete(r.path, handler);
1286
1408
  break;
@@ -1289,8 +1411,7 @@ export class ApiServer {
1289
1411
  console.log(`Adding ${mountPath}${r.path} (${r.method.toUpperCase()})`);
1290
1412
  }
1291
1413
  });
1292
- this.app.use(mountPath, router);
1293
- this.ensureApiNotFoundOrdering();
1414
+ this.apiRouter.use(ns, router);
1294
1415
  return this;
1295
1416
  }
1296
1417
  dumpRequest(apiReq) {
@@ -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 {};