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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.txt +25 -2
  2. package/dist/cjs/api-server-base.cjs +269 -39
  3. package/dist/cjs/api-server-base.d.ts +27 -7
  4. package/dist/cjs/auth-api/auth-module.d.ts +11 -2
  5. package/dist/cjs/auth-api/auth-module.js +193 -45
  6. package/dist/cjs/auth-api/compat-auth-storage.d.ts +7 -5
  7. package/dist/cjs/auth-api/compat-auth-storage.js +15 -3
  8. package/dist/cjs/auth-api/mem-auth-store.d.ts +5 -3
  9. package/dist/cjs/auth-api/mem-auth-store.js +7 -1
  10. package/dist/cjs/auth-api/sql-auth-store.d.ts +5 -3
  11. package/dist/cjs/auth-api/sql-auth-store.js +7 -1
  12. package/dist/cjs/auth-api/storage.d.ts +6 -4
  13. package/dist/cjs/auth-api/storage.js +15 -5
  14. package/dist/cjs/auth-api/types.d.ts +7 -2
  15. package/dist/cjs/index.cjs +4 -4
  16. package/dist/cjs/index.d.ts +4 -4
  17. package/dist/cjs/oauth/sequelize.js +1 -1
  18. package/dist/cjs/passkey/base.d.ts +1 -0
  19. package/dist/cjs/passkey/memory.d.ts +1 -0
  20. package/dist/cjs/passkey/memory.js +4 -0
  21. package/dist/cjs/passkey/sequelize.d.ts +1 -0
  22. package/dist/cjs/passkey/sequelize.js +11 -2
  23. package/dist/cjs/passkey/service.d.ts +5 -2
  24. package/dist/cjs/passkey/service.js +145 -10
  25. package/dist/cjs/passkey/types.d.ts +3 -0
  26. package/dist/cjs/user/base.js +2 -1
  27. package/dist/esm/api-server-base.d.ts +27 -7
  28. package/dist/esm/api-server-base.js +270 -40
  29. package/dist/esm/auth-api/auth-module.d.ts +11 -2
  30. package/dist/esm/auth-api/auth-module.js +194 -46
  31. package/dist/esm/auth-api/compat-auth-storage.d.ts +7 -5
  32. package/dist/esm/auth-api/compat-auth-storage.js +13 -1
  33. package/dist/esm/auth-api/mem-auth-store.d.ts +5 -3
  34. package/dist/esm/auth-api/mem-auth-store.js +8 -2
  35. package/dist/esm/auth-api/sql-auth-store.d.ts +5 -3
  36. package/dist/esm/auth-api/sql-auth-store.js +8 -2
  37. package/dist/esm/auth-api/storage.d.ts +6 -4
  38. package/dist/esm/auth-api/storage.js +13 -3
  39. package/dist/esm/auth-api/types.d.ts +7 -2
  40. package/dist/esm/index.d.ts +4 -4
  41. package/dist/esm/index.js +2 -2
  42. package/dist/esm/oauth/sequelize.js +1 -1
  43. package/dist/esm/passkey/base.d.ts +1 -0
  44. package/dist/esm/passkey/memory.d.ts +1 -0
  45. package/dist/esm/passkey/memory.js +4 -0
  46. package/dist/esm/passkey/sequelize.d.ts +1 -0
  47. package/dist/esm/passkey/sequelize.js +11 -2
  48. package/dist/esm/passkey/service.d.ts +5 -2
  49. package/dist/esm/passkey/service.js +113 -11
  50. package/dist/esm/passkey/types.d.ts +3 -0
  51. package/dist/esm/user/base.js +2 -1
  52. package/package.json +3 -1
package/README.txt CHANGED
@@ -108,13 +108,14 @@ cookieDomain (string, default '.somewhere-over-the-rainbow.com') Domain applied
108
108
  accessCookie (string, default 'dat') Access token cookie name.
109
109
  refreshCookie (string, default 'drt') Refresh token cookie name.
110
110
  accessExpiry (number, default 60 * 15) Access token lifetime in seconds.
111
- refreshExpiry (number, default 30 * 24 * 60 * 60 * 1000) Refresh token lifetime in milliseconds.
111
+ refreshExpiry (number, default 30 * 24 * 60 * 60) Refresh token lifetime in seconds.
112
112
  sessionRefreshExpiry (number, default 24 * 60 * 60) Session token lifetime in seconds when clients opt out of "remember me" cookies.
113
113
  authApi (boolean, default false) Toggle you can use when mounting auth routes.
114
114
  devMode (boolean, default false) Custom hook for development only features.
115
115
  debug (boolean, default false) When true the server logs inbound requests via dumpRequest.
116
116
  hydrateGetBody (boolean, default true) Copy query parameters into `req.body` for GET requests; set false if you prefer untouched bodies.
117
117
  validateTokens (boolean, default false) When true, every JWT-authenticated request must match a stored token row (access token + user id) before reaching your handler. API keys remain stateless either way.
118
+ refreshMaybe (boolean, default false) When true, `auth: maybe` routes will try to refresh a missing/expired access token using the refresh cookie; if refresh fails, the request stays anonymous.
118
119
 
119
120
  Tip: If you add new configuration fields in downstream projects, extend ApiServerConf and update fillConfig so defaults stay aligned.
120
121
 
@@ -122,7 +123,7 @@ Request Lifecycle
122
123
  -----------------
123
124
  1. Express middlewares (express.json, cookie-parser, optional multer) run before your handler.
124
125
  2. ApiServer wraps the route inside handle_request, setting currReq and logging when debug is enabled.
125
- 3. authenticate enforces the ApiRoute auth type: `none`, `maybe`, `yes`, `strict`, or `apikey`. Bearer JWTs and the `dat` cookie are accepted for `yes`/`strict`, while API key tokens prefixed with `apikey-` always delegate to `getApiKey`. The optional `strict` type (or server-wide `validateTokens` flag) requires the signed JWT to exist in storage; when it does, the persisted row is attached to `apiReq.authToken`. The dedicated `apikey` type simply means “an API key is required”; otherwise API keys are still accepted by `yes`/`strict` routes alongside JWTs, and `apiReq.apiKey` is populated when present.
126
+ 3. authenticate enforces the ApiRoute auth type: `none`, `maybe`, `yes`, `strict`, or `apikey`. Bearer JWTs and the access cookie (`accessCookie`, default `dat`) are accepted for `yes`/`strict`, while API key tokens prefixed with `apikey-` always delegate to `getApiKey`. When `refreshSecret` is configured and your storage supports refresh lookups (`getToken({ refreshToken })` + `updateToken(...)`), `yes`/`strict` routes will automatically mint a new access token when it is missing or expired (and also recover from "Authorization token is no longer valid" by refreshing). `maybe` routes only do the same when `refreshMaybe: true`. The optional `strict` type (or server-wide `validateTokens` flag) requires the signed JWT to exist in storage; when it does, the persisted row is attached to `apiReq.authToken`. The dedicated `apikey` type simply means “an API key is required”; otherwise API keys are still accepted by `yes`/`strict` routes alongside JWTs, and `apiReq.apiKey` is populated when present.
126
127
  4. authorize runs with the requested auth class (any or admin in the base implementation). Override to connect to your role system.
127
128
  5. The handler executes and returns its tuple. Responses are normalized to { code, message, data } JSON.
128
129
  6. Errors bubble into the wrapper. ApiError instances respect the provided status codes; other exceptions result in a 500 with text derived from guessExceptionText.
@@ -140,6 +141,28 @@ Use your storage adapter's filterUser helper to trim sensitive data before retur
140
141
  Provide your own authorize method to enforce role based access control using the ApiAuthClass enum.
141
142
  Create feature modules by extending ApiModule. Use the optional checkConfig hook to validate prerequisites before routes mount.
142
143
 
144
+ Custom Express Endpoints
145
+ ------------------------
146
+ ApiModule routes run inside the tuple wrapper (always responding with a standardized JSON envelope). For endpoints that need raw Express control (streaming, webhooks, tus uploads, etc.), mount your own handlers directly.
147
+
148
+ - `server.useExpress(...)` mounts middleware/routes and keeps the built-in `/api` 404 handler ordered last, so mounts under `apiBasePath` are not intercepted.
149
+ - Protect endpoints by inserting `server.expressAuth({ type, req })` as middleware. It authenticates using the same JWT/cookie/API-key logic as ApiModule routes and then runs `authorize`.
150
+ - On success, `expressAuth` attaches the computed ApiRequest to both `req.apiReq` and `res.locals.apiReq`.
151
+ - If you want the same JSON error envelope for custom endpoints, mount `server.expressErrorHandler()` after your custom routes.
152
+
153
+ Example:
154
+
155
+ server
156
+ .useExpress(
157
+ '/api/custom/optional',
158
+ server.expressAuth({ type: 'maybe', req: 'any' }),
159
+ (req, res) => {
160
+ const apiReq = (req as any).apiReq;
161
+ res.status(200).json({ uid: apiReq.tokenData?.uid ?? null });
162
+ }
163
+ )
164
+ .useExpress(server.expressErrorHandler());
165
+
143
166
 
144
167
  Tooling and Scripts
145
168
  -------------------
@@ -352,6 +352,7 @@ function fillConfig(config) {
352
352
  devMode: config.devMode ?? false,
353
353
  hydrateGetBody: config.hydrateGetBody ?? true,
354
354
  validateTokens: config.validateTokens ?? false,
355
+ refreshMaybe: config.refreshMaybe ?? false,
355
356
  apiVersion: config.apiVersion ?? '',
356
357
  minClientVersion: config.minClientVersion ?? '',
357
358
  tokenStore: config.tokenStore,
@@ -370,7 +371,7 @@ class ApiServer {
370
371
  this.config = fillConfig(config);
371
372
  this.apiBasePath = this.normalizeApiBasePath(this.config.apiBasePath);
372
373
  this.startedAt = Date.now();
373
- this.storageAdapter = storage_js_1.nullAuthStorage;
374
+ this.storageAdapter = storage_js_1.nullAuthAdapter;
374
375
  this.moduleAdapter = module_js_1.nullAuthModule;
375
376
  this.jwtHelper = new JwtHelperStore();
376
377
  this.tokenStoreAdapter = this.config.tokenStore ?? null;
@@ -448,13 +449,19 @@ class ApiServer {
448
449
  }
449
450
  return this.passkeyServiceAdapter;
450
451
  }
452
+ async listUserCredentials(userId) {
453
+ return this.ensurePasskeyService().listUserCredentials(userId);
454
+ }
455
+ async deletePasskeyCredential(credentialId) {
456
+ return this.ensurePasskeyService().deleteCredential(credentialId);
457
+ }
451
458
  ensureOAuthStore() {
452
459
  if (!this.oauthStoreAdapter) {
453
460
  throw new Error('OAuth store is not configured');
454
461
  }
455
462
  return this.oauthStoreAdapter;
456
463
  }
457
- // AuthStorage-compatible helpers (used by AuthModule)
464
+ // AuthAdapter-compatible helpers (used by AuthModule)
458
465
  async getUser(identifier) {
459
466
  return this.userStoreAdapter ? this.userStoreAdapter.findUser(identifier) : null;
460
467
  }
@@ -700,16 +707,117 @@ class ApiServer {
700
707
  }
701
708
  async verifyJWT(token) {
702
709
  if (!this.config.accessSecret) {
703
- return { tokenData: undefined, error: 'JWT authentication disabled; no jwtSecret set' };
710
+ return { tokenData: undefined, error: 'JWT authentication disabled; no jwtSecret set', expired: false };
704
711
  }
705
712
  const result = this.jwtVerify(token, this.config.accessSecret);
706
713
  if (!result.success) {
707
- return { tokenData: undefined, error: result.error };
714
+ return { tokenData: undefined, error: result.error, expired: result.expired };
708
715
  }
709
716
  if (!result.data.uid) {
710
- return { tokenData: undefined, error: 'Missing/bad userid in token' };
717
+ return { tokenData: undefined, error: 'Missing/bad userid in token', expired: false };
718
+ }
719
+ return { tokenData: result.data, error: undefined, expired: false };
720
+ }
721
+ jwtCookieOptions(apiReq) {
722
+ const conf = this.config;
723
+ const forwarded = apiReq.req.headers['x-forwarded-proto'];
724
+ const referer = apiReq.req.headers['origin'] ?? apiReq.req.headers['referer'];
725
+ const origin = typeof referer === 'string' ? referer : '';
726
+ const isHttps = forwarded === 'https' || apiReq.req.protocol === 'https';
727
+ const isLocalhost = origin.includes('localhost');
728
+ const options = {
729
+ httpOnly: true,
730
+ secure: true,
731
+ sameSite: 'strict',
732
+ domain: conf.cookieDomain || undefined,
733
+ path: '/',
734
+ maxAge: undefined
735
+ };
736
+ if (conf.devMode) {
737
+ options.secure = isHttps;
738
+ options.httpOnly = false;
739
+ options.sameSite = 'lax';
740
+ if (isLocalhost) {
741
+ options.domain = undefined;
742
+ }
711
743
  }
712
- return { tokenData: result.data, error: undefined };
744
+ return options;
745
+ }
746
+ setAccessCookie(apiReq, accessToken, sessionCookie) {
747
+ const conf = this.config;
748
+ const options = this.jwtCookieOptions(apiReq);
749
+ const accessMaxAge = Math.max(1, conf.accessExpiry) * 1000;
750
+ const accessOptions = sessionCookie ? options : { ...options, maxAge: accessMaxAge };
751
+ apiReq.res.cookie(conf.accessCookie, accessToken, accessOptions);
752
+ }
753
+ async tryRefreshAccessToken(apiReq) {
754
+ const conf = this.config;
755
+ if (!conf.refreshSecret || !conf.accessSecret) {
756
+ return null;
757
+ }
758
+ const rawRefresh = apiReq.req.cookies?.[conf.refreshCookie];
759
+ if (typeof rawRefresh !== 'string') {
760
+ return null;
761
+ }
762
+ const refreshToken = rawRefresh.trim();
763
+ if (!refreshToken) {
764
+ return null;
765
+ }
766
+ const verify = this.jwtVerify(refreshToken, conf.refreshSecret);
767
+ if (!verify.success || !verify.data) {
768
+ return null;
769
+ }
770
+ let stored = null;
771
+ try {
772
+ stored = await this.storageAdapter.getToken({ refreshToken });
773
+ }
774
+ catch {
775
+ return null;
776
+ }
777
+ if (!stored) {
778
+ return null;
779
+ }
780
+ const storedUid = String(stored.userId);
781
+ const verifyUid = verify.data.uid === undefined || verify.data.uid === null ? null : String(verify.data.uid);
782
+ if (verifyUid && verifyUid !== storedUid) {
783
+ return null;
784
+ }
785
+ const claims = verify.data;
786
+ const { exp: _exp, iat: _iat, nbf: _nbf, ...payload } = claims;
787
+ void _exp;
788
+ void _iat;
789
+ void _nbf;
790
+ // Ensure we never embed token secrets into refreshed access tokens.
791
+ delete payload.accessToken;
792
+ delete payload.refreshToken;
793
+ delete payload.userId;
794
+ delete payload.expires;
795
+ delete payload.issuedAt;
796
+ delete payload.lastSeenAt;
797
+ delete payload.status;
798
+ const access = this.jwtSign(payload, conf.accessSecret, conf.accessExpiry);
799
+ if (!access.success || !access.token) {
800
+ return null;
801
+ }
802
+ const updated = await this.updateToken({
803
+ refreshToken,
804
+ accessToken: access.token,
805
+ lastSeenAt: new Date()
806
+ });
807
+ if (!updated) {
808
+ return null;
809
+ }
810
+ this.setAccessCookie(apiReq, access.token, stored.sessionCookie ?? false);
811
+ if (apiReq.req.cookies) {
812
+ apiReq.req.cookies[conf.accessCookie] = access.token;
813
+ }
814
+ const verifiedAccess = await this.verifyJWT(access.token);
815
+ if (!verifiedAccess.tokenData) {
816
+ return null;
817
+ }
818
+ const refreshedStored = { ...stored, accessToken: access.token };
819
+ apiReq.authToken = refreshedStored;
820
+ return { token: access.token, tokenData: verifiedAccess.tokenData, stored: refreshedStored };
713
821
  }
714
822
  async authenticate(apiReq, authType) {
715
823
  if (authType === 'none') {
@@ -719,6 +827,7 @@ class ApiServer {
719
827
  let token = null;
720
828
  const authHeader = apiReq.req.headers.authorization;
721
829
  const requiresAuthToken = this.requiresAuthToken(authType);
830
+ const allowRefresh = requiresAuthToken || (authType === 'maybe' && this.config.refreshMaybe);
722
831
  const apiKeyAuth = await this.tryAuthenticateApiKey(apiReq, authType, authHeader);
723
832
  if (apiKeyAuth) {
724
833
  return apiKeyAuth;
@@ -727,32 +836,84 @@ class ApiServer {
727
836
  token = authHeader.slice(7).trim();
728
837
  }
729
838
  if (!token) {
730
- const access = apiReq.req.cookies?.dat;
839
+ const access = apiReq.req.cookies?.[this.config.accessCookie];
731
840
  if (access) {
732
841
  token = access;
733
842
  }
734
843
  }
844
+ let tokenData;
845
+ let error;
846
+ let expired = false;
735
847
  if (!token || token === null) {
736
- if (requiresAuthToken) {
737
- throw new ApiError({ code: 401, message: 'Authorization token is required (Bearer/cookie)' });
848
+ if (authType === 'maybe') {
849
+ if (!this.config.refreshMaybe) {
850
+ return null;
851
+ }
852
+ const refreshed = await this.tryRefreshAccessToken(apiReq);
853
+ if (!refreshed) {
854
+ return null;
855
+ }
856
+ token = refreshed.token;
857
+ tokenData = refreshed.tokenData;
858
+ error = undefined;
859
+ expired = false;
860
+ }
861
+ else if (requiresAuthToken) {
862
+ const refreshed = await this.tryRefreshAccessToken(apiReq);
863
+ if (!refreshed) {
864
+ throw new ApiError({ code: 401, message: 'Authorization token is required (Bearer/cookie)' });
865
+ }
866
+ token = refreshed.token;
867
+ tokenData = refreshed.tokenData;
868
+ error = undefined;
869
+ expired = false;
738
870
  }
739
871
  }
740
872
  if (!token) {
741
- if (authType === 'maybe') {
742
- return null;
743
- }
744
- else {
745
- throw new ApiError({ code: 401, message: 'Unauthorized Access - requires authentication' });
873
+ throw new ApiError({ code: 401, message: 'Unauthorized Access - requires authentication' });
874
+ }
875
+ if (!tokenData) {
876
+ const verified = await this.verifyJWT(token);
877
+ tokenData = verified.tokenData;
878
+ error = verified.error;
879
+ expired = verified.expired ?? false;
880
+ }
881
+ if (!tokenData && allowRefresh && expired) {
882
+ const refreshed = await this.tryRefreshAccessToken(apiReq);
883
+ if (refreshed) {
884
+ token = refreshed.token;
885
+ tokenData = refreshed.tokenData;
886
+ error = undefined;
746
887
  }
747
888
  }
748
- const { tokenData, error } = await this.verifyJWT(token);
749
889
  if (!tokenData) {
750
890
  throw new ApiError({ code: 401, message: 'Unathorized Access - ' + error });
751
891
  }
752
892
  const effectiveUserId = this.extractTokenUserId(tokenData);
753
893
  apiReq.realUid = this.resolveRealUserId(tokenData, effectiveUserId);
754
894
  if (this.shouldValidateStoredToken(authType)) {
755
- await this.assertStoredAccessToken(apiReq, token, tokenData);
895
+ try {
896
+ await this.assertStoredAccessToken(apiReq, token, tokenData);
897
+ }
898
+ catch (error) {
899
+ if (allowRefresh &&
900
+ error instanceof ApiError &&
901
+ error.code === 401 &&
902
+ error.message === 'Authorization token is no longer valid') {
903
+ const refreshed = await this.tryRefreshAccessToken(apiReq);
904
+ if (!refreshed) {
905
+ throw error;
906
+ }
907
+ token = refreshed.token;
908
+ tokenData = refreshed.tokenData;
909
+ const refreshedUserId = this.extractTokenUserId(tokenData);
910
+ apiReq.realUid = this.resolveRealUserId(tokenData, refreshedUserId);
911
+ await this.assertStoredAccessToken(apiReq, token, tokenData);
912
+ }
913
+ else {
914
+ throw error;
915
+ }
916
+ }
756
917
  }
757
918
  apiReq.token = token;
758
919
  return tokenData;
@@ -794,6 +955,9 @@ class ApiServer {
794
955
  }
795
956
  async assertStoredAccessToken(apiReq, token, tokenData) {
796
957
  const userId = String(this.extractTokenUserId(tokenData));
958
+ if (apiReq.authToken && apiReq.authToken.accessToken === token && String(apiReq.authToken.userId) === userId) {
959
+ return;
960
+ }
797
961
  const stored = await this.storageAdapter.getToken({
798
962
  accessToken: token,
799
963
  userId
@@ -833,32 +997,98 @@ class ApiServer {
833
997
  }
834
998
  return rawReal;
835
999
  }
836
- handle_request(handler, auth) {
1000
+ useExpress(pathOrHandler, ...handlers) {
1001
+ if (typeof pathOrHandler === 'string') {
1002
+ this.app.use(pathOrHandler, ...handlers);
1003
+ }
1004
+ else {
1005
+ this.app.use(pathOrHandler, ...handlers);
1006
+ }
1007
+ this.ensureApiNotFoundOrdering();
1008
+ return this;
1009
+ }
1010
+ createApiRequest(req, res) {
1011
+ const apiReq = {
1012
+ server: this,
1013
+ req,
1014
+ res,
1015
+ token: '',
1016
+ tokenData: null,
1017
+ realUid: null,
1018
+ getClientInfo: () => ensureClientInfo(apiReq),
1019
+ getClientIp: () => ensureClientInfo(apiReq).ip,
1020
+ getClientIpChain: () => ensureClientInfo(apiReq).ipchain,
1021
+ getRealUid: () => apiReq.realUid ?? null,
1022
+ isImpersonating: () => {
1023
+ const realUid = apiReq.realUid;
1024
+ const tokenUid = apiReq.tokenData?.uid;
1025
+ if (realUid === null || realUid === undefined) {
1026
+ return false;
1027
+ }
1028
+ if (tokenUid === null || tokenUid === undefined) {
1029
+ return false;
1030
+ }
1031
+ return realUid !== tokenUid;
1032
+ }
1033
+ };
1034
+ return apiReq;
1035
+ }
1036
+ expressAuth(auth) {
837
1037
  return async (req, res, next) => {
838
- void next;
839
- const apiReq = {
840
- server: this,
841
- req,
842
- res,
843
- token: '',
844
- tokenData: null,
845
- realUid: null,
846
- getClientInfo: () => ensureClientInfo(apiReq),
847
- getClientIp: () => ensureClientInfo(apiReq).ip,
848
- getClientIpChain: () => ensureClientInfo(apiReq).ipchain,
849
- getRealUid: () => apiReq.realUid ?? null,
850
- isImpersonating: () => {
851
- const realUid = apiReq.realUid;
852
- const tokenUid = apiReq.tokenData?.uid;
853
- if (realUid === null || realUid === undefined) {
854
- return false;
855
- }
856
- if (tokenUid === null || tokenUid === undefined) {
857
- return false;
858
- }
859
- return realUid !== tokenUid;
1038
+ const apiReq = this.createApiRequest(req, res);
1039
+ req.apiReq = apiReq;
1040
+ res.locals.apiReq = apiReq;
1041
+ this.currReq = apiReq;
1042
+ try {
1043
+ if (this.config.hydrateGetBody) {
1044
+ hydrateGetBody(req);
1045
+ }
1046
+ if (this.config.debug) {
1047
+ this.dumpRequest(apiReq);
860
1048
  }
1049
+ apiReq.tokenData = await this.authenticate(apiReq, auth.type);
1050
+ await this.authorize(apiReq, auth.req);
1051
+ next();
1052
+ }
1053
+ catch (error) {
1054
+ next(error);
1055
+ }
1056
+ };
1057
+ }
1058
+ expressErrorHandler() {
1059
+ return (error, _req, res, next) => {
1060
+ void _req;
1061
+ if (res.headersSent) {
1062
+ next(error);
1063
+ return;
1064
+ }
1065
+ if (error instanceof ApiError || isApiErrorLike(error)) {
1066
+ const apiError = error;
1067
+ const normalizedErrors = apiError.errors && typeof apiError.errors === 'object' && !Array.isArray(apiError.errors)
1068
+ ? apiError.errors
1069
+ : {};
1070
+ const errorPayload = {
1071
+ code: apiError.code,
1072
+ message: apiError.message,
1073
+ data: apiError.data ?? null,
1074
+ errors: normalizedErrors
1075
+ };
1076
+ res.status(apiError.code).json(errorPayload);
1077
+ return;
1078
+ }
1079
+ const errorPayload = {
1080
+ code: 500,
1081
+ message: this.guessExceptionText(error),
1082
+ data: null,
1083
+ errors: {}
861
1084
  };
1085
+ res.status(500).json(errorPayload);
1086
+ };
1087
+ }
1088
+ handle_request(handler, auth) {
1089
+ return async (req, res, next) => {
1090
+ void next;
1091
+ const apiReq = this.createApiRequest(req, res);
862
1092
  this.currReq = apiReq;
863
1093
  try {
864
1094
  if (this.config.hydrateGetBody) {
@@ -4,16 +4,16 @@
4
4
  * This source code is licensed under the MIT license found in the
5
5
  * LICENSE file in the root directory of this source tree.
6
6
  */
7
- import { Application, Request, Response } from 'express';
7
+ import { Application, Request, Response, type ErrorRequestHandler, type RequestHandler } from 'express';
8
8
  import { ApiModule } from './api-module.js';
9
9
  import { TokenStore, type JwtDecodeResult, type JwtSignResult, type JwtVerifyResult } from './token/base.js';
10
- import type { ApiAuthClass, ApiKey } from './api-module.js';
10
+ import type { ApiAuthClass, ApiAuthType, ApiKey } from './api-module.js';
11
11
  import type { AuthProviderModule } from './auth-api/module.js';
12
- import type { AuthStorage, AuthIdentifier } from './auth-api/types.js';
12
+ import type { AuthAdapter, AuthIdentifier } from './auth-api/types.js';
13
13
  import type { OAuthStore } from './oauth/base.js';
14
14
  import type { AuthCodeData, AuthCodeRequest, OAuthClient } from './oauth/types.js';
15
15
  import type { PasskeyService } from './passkey/service.js';
16
- import type { PasskeyChallenge, PasskeyChallengeParams, PasskeyVerificationParams, PasskeyVerificationResult } from './passkey/types.js';
16
+ import type { PasskeyChallenge, PasskeyChallengeParams, StoredPasskeyCredential, PasskeyVerificationParams, PasskeyVerificationResult } from './passkey/types.js';
17
17
  import type { Token } from './token/types.js';
18
18
  import type { UserStore } from './user/base.js';
19
19
  import type { JwtPayload, SignOptions, VerifyOptions } from 'jsonwebtoken';
@@ -47,6 +47,12 @@ export interface ApiRequest {
47
47
  getRealUid: () => AuthIdentifier | null;
48
48
  isImpersonating: () => boolean;
49
49
  }
50
+ export interface ExpressApiRequest extends ExtendedReq {
51
+ apiReq?: ApiRequest;
52
+ }
53
+ export interface ExpressApiLocals {
54
+ apiReq?: ApiRequest;
55
+ }
50
56
  export interface ClientAgentProfile {
51
57
  ua: string;
52
58
  browser: string;
@@ -101,6 +107,7 @@ export interface ApiServerConf {
101
107
  devMode: boolean;
102
108
  hydrateGetBody: boolean;
103
109
  validateTokens: boolean;
110
+ refreshMaybe: boolean;
104
111
  apiVersion: string;
105
112
  minClientVersion: string;
106
113
  tokenStore?: TokenStore;
@@ -122,23 +129,25 @@ export declare class ApiServer {
122
129
  private canImpersonateAdapter;
123
130
  private readonly jwtHelper;
124
131
  constructor(config?: Partial<ApiServerConf>);
125
- authStorage<UserRow, SafeUser>(storage: AuthStorage<UserRow, SafeUser>): this;
132
+ authStorage<UserRow, SafeUser>(storage: AuthAdapter<UserRow, SafeUser>): this;
126
133
  /**
127
134
  * @deprecated Use {@link ApiServer.authStorage} instead.
128
135
  */
129
- useAuthStorage<UserRow, SafeUser>(storage: AuthStorage<UserRow, SafeUser>): this;
136
+ useAuthStorage<UserRow, SafeUser>(storage: AuthAdapter<UserRow, SafeUser>): this;
130
137
  authModule<UserRow>(module: AuthProviderModule<UserRow>): this;
131
138
  /**
132
139
  * @deprecated Use {@link ApiServer.authModule} instead.
133
140
  */
134
141
  useAuthModule<UserRow>(module: AuthProviderModule<UserRow>): this;
135
- getAuthStorage(): AuthStorage<any, any>;
142
+ getAuthStorage(): AuthAdapter<any, any>;
136
143
  getAuthModule(): AuthProviderModule<any>;
137
144
  setTokenStore(store: TokenStore): this;
138
145
  getTokenStore(): TokenStore | null;
139
146
  private ensureUserStore;
140
147
  private ensureTokenStore;
141
148
  private ensurePasskeyService;
149
+ listUserCredentials(userId: AuthIdentifier): Promise<StoredPasskeyCredential[]>;
150
+ deletePasskeyCredential(credentialId: Buffer | string): Promise<boolean>;
142
151
  private ensureOAuthStore;
143
152
  getUser(identifier: AuthIdentifier): Promise<any | null>;
144
153
  getUserPasswordHash(user: any): string;
@@ -187,6 +196,9 @@ export declare class ApiServer {
187
196
  private describeMissingEndpoint;
188
197
  start(): this;
189
198
  private verifyJWT;
199
+ private jwtCookieOptions;
200
+ private setAccessCookie;
201
+ private tryRefreshAccessToken;
190
202
  private authenticate;
191
203
  private tryAuthenticateApiKey;
192
204
  private requiresAuthToken;
@@ -195,6 +207,14 @@ export declare class ApiServer {
195
207
  private normalizeAuthIdentifier;
196
208
  private extractTokenUserId;
197
209
  private resolveRealUserId;
210
+ useExpress(path: string, ...handlers: Array<RequestHandler | ErrorRequestHandler>): this;
211
+ useExpress(...handlers: Array<RequestHandler | ErrorRequestHandler>): this;
212
+ private createApiRequest;
213
+ expressAuth(auth: {
214
+ type: ApiAuthType;
215
+ req: ApiAuthClass;
216
+ }): RequestHandler;
217
+ expressErrorHandler(): ErrorRequestHandler;
198
218
  private handle_request;
199
219
  api<T extends ApiModule<any>>(module: T): this;
200
220
  dumpRequest(apiReq: ApiRequest): void;
@@ -1,6 +1,6 @@
1
1
  import { type ApiRequest, type ApiRoute, type ApiServer } from '../api-server-base.js';
2
2
  import { BaseAuthModule, type AuthProviderModule } from './module.js';
3
- import type { AuthIdentifier, AuthStorage } from './types.js';
3
+ import type { AuthAdapter, AuthIdentifier } from './types.js';
4
4
  import type { OAuthCallbackParams, OAuthCallbackResult, OAuthStartParams, OAuthStartResult } from '../oauth/types.js';
5
5
  import type { TokenPair, Token } from '../token/types.js';
6
6
  interface CanImpersonateContext<UserEntity> {
@@ -46,7 +46,7 @@ export default class AuthModule<UserEntity, PublicUser> extends BaseAuthModule<U
46
46
  private readonly defaultDomain?;
47
47
  private readonly canImpersonateHook?;
48
48
  constructor(options?: AuthModuleOptions<UserEntity>);
49
- protected get storage(): AuthStorage<UserEntity, PublicUser>;
49
+ protected get storage(): AuthAdapter<UserEntity, PublicUser>;
50
50
  protected canImpersonate(apiReq: ApiRequest, realUser: UserEntity, targetUser: UserEntity): Promise<boolean>;
51
51
  protected ensureImpersonationAllowed(apiReq: ApiRequest, realUser: UserEntity, targetUser: UserEntity): Promise<void>;
52
52
  protected buildTokenPayload(user: UserEntity, metadata?: TokenMetadata): TokenClaims;
@@ -57,6 +57,9 @@ export default class AuthModule<UserEntity, PublicUser> extends BaseAuthModule<U
57
57
  private resolveSessionPreferences;
58
58
  private mergeSessionPreferences;
59
59
  private sessionPrefsFromRecord;
60
+ private validateCredentialId;
61
+ private normalizeCredentialId;
62
+ private toIsoDate;
60
63
  private cookieOptions;
61
64
  private setJwtCookies;
62
65
  issueTokens(apiReq: ApiRequest, user: UserEntity, metadata?: TokenIssueOptions): Promise<TokenPair>;
@@ -76,6 +79,8 @@ export default class AuthModule<UserEntity, PublicUser> extends BaseAuthModule<U
76
79
  private postWhoAmI;
77
80
  private postPasskeyChallenge;
78
81
  private postPasskeyVerify;
82
+ private getPasskeys;
83
+ private deletePasskey;
79
84
  private postImpersonation;
80
85
  private deleteImpersonation;
81
86
  private getUserFromPasskey;
@@ -91,6 +96,10 @@ export default class AuthModule<UserEntity, PublicUser> extends BaseAuthModule<U
91
96
  private resolveClientAuthentication;
92
97
  private assertRedirectUriAllowed;
93
98
  private resolveUserForOAuth;
99
+ private hasPasskeyService;
100
+ private hasOAuthStore;
101
+ private storageImplements;
102
+ private storageImplementsAll;
94
103
  defineRoutes(): ApiRoute[];
95
104
  }
96
105
  export {};