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

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 +277 -41
  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 +215 -46
  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 +278 -42
  29. package/dist/esm/auth-api/auth-module.d.ts +11 -2
  30. package/dist/esm/auth-api/auth-module.js +216 -47
  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 +13 -11
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
  }
@@ -612,6 +619,7 @@ class ApiServer {
612
619
  const path = `${this.apiBasePath}/v1/ping`;
613
620
  this.app.get(path, (_req, res) => {
614
621
  const payload = {
622
+ success: true,
615
623
  status: 'ok',
616
624
  apiVersion: this.config.apiVersion ?? '',
617
625
  minClientVersion: this.config.minClientVersion ?? '',
@@ -619,7 +627,7 @@ class ApiServer {
619
627
  startedAt: this.startedAt,
620
628
  timestamp: new Date().toISOString()
621
629
  };
622
- res.status(200).json({ code: 200, message: 'Success', data: payload });
630
+ res.status(200).json({ success: true, code: 200, message: 'Success', data: payload, errors: {} });
623
631
  });
624
632
  }
625
633
  normalizeApiBasePath(path) {
@@ -642,6 +650,7 @@ class ApiServer {
642
650
  }
643
651
  this.apiNotFoundHandler = (req, res) => {
644
652
  const payload = {
653
+ success: false,
645
654
  code: 404,
646
655
  message: this.describeMissingEndpoint(req),
647
656
  data: null,
@@ -700,16 +709,117 @@ class ApiServer {
700
709
  }
701
710
  async verifyJWT(token) {
702
711
  if (!this.config.accessSecret) {
703
- return { tokenData: undefined, error: 'JWT authentication disabled; no jwtSecret set' };
712
+ return { tokenData: undefined, error: 'JWT authentication disabled; no jwtSecret set', expired: false };
704
713
  }
705
714
  const result = this.jwtVerify(token, this.config.accessSecret);
706
715
  if (!result.success) {
707
- return { tokenData: undefined, error: result.error };
716
+ return { tokenData: undefined, error: result.error, expired: result.expired };
708
717
  }
709
718
  if (!result.data.uid) {
710
- return { tokenData: undefined, error: 'Missing/bad userid in token' };
719
+ return { tokenData: undefined, error: 'Missing/bad userid in token', expired: false };
720
+ }
721
+ return { tokenData: result.data, error: undefined, expired: false };
722
+ }
723
+ jwtCookieOptions(apiReq) {
724
+ const conf = this.config;
725
+ const forwarded = apiReq.req.headers['x-forwarded-proto'];
726
+ const referer = apiReq.req.headers['origin'] ?? apiReq.req.headers['referer'];
727
+ const origin = typeof referer === 'string' ? referer : '';
728
+ const isHttps = forwarded === 'https' || apiReq.req.protocol === 'https';
729
+ const isLocalhost = origin.includes('localhost');
730
+ const options = {
731
+ httpOnly: true,
732
+ secure: true,
733
+ sameSite: 'strict',
734
+ domain: conf.cookieDomain || undefined,
735
+ path: '/',
736
+ maxAge: undefined
737
+ };
738
+ if (conf.devMode) {
739
+ options.secure = isHttps;
740
+ options.httpOnly = false;
741
+ options.sameSite = 'lax';
742
+ if (isLocalhost) {
743
+ options.domain = undefined;
744
+ }
711
745
  }
712
- return { tokenData: result.data, error: undefined };
746
+ return options;
747
+ }
748
+ setAccessCookie(apiReq, accessToken, sessionCookie) {
749
+ const conf = this.config;
750
+ const options = this.jwtCookieOptions(apiReq);
751
+ const accessMaxAge = Math.max(1, conf.accessExpiry) * 1000;
752
+ const accessOptions = sessionCookie ? options : { ...options, maxAge: accessMaxAge };
753
+ apiReq.res.cookie(conf.accessCookie, accessToken, accessOptions);
754
+ }
755
+ async tryRefreshAccessToken(apiReq) {
756
+ const conf = this.config;
757
+ if (!conf.refreshSecret || !conf.accessSecret) {
758
+ return null;
759
+ }
760
+ const rawRefresh = apiReq.req.cookies?.[conf.refreshCookie];
761
+ if (typeof rawRefresh !== 'string') {
762
+ return null;
763
+ }
764
+ const refreshToken = rawRefresh.trim();
765
+ if (!refreshToken) {
766
+ return null;
767
+ }
768
+ const verify = this.jwtVerify(refreshToken, conf.refreshSecret);
769
+ if (!verify.success || !verify.data) {
770
+ return null;
771
+ }
772
+ let stored = null;
773
+ try {
774
+ stored = await this.storageAdapter.getToken({ refreshToken });
775
+ }
776
+ catch {
777
+ return null;
778
+ }
779
+ if (!stored) {
780
+ return null;
781
+ }
782
+ const storedUid = String(stored.userId);
783
+ const verifyUid = verify.data.uid === undefined || verify.data.uid === null ? null : String(verify.data.uid);
784
+ if (verifyUid && verifyUid !== storedUid) {
785
+ return null;
786
+ }
787
+ const claims = verify.data;
788
+ const { exp: _exp, iat: _iat, nbf: _nbf, ...payload } = claims;
789
+ void _exp;
790
+ void _iat;
791
+ void _nbf;
792
+ // Ensure we never embed token secrets into refreshed access tokens.
793
+ delete payload.accessToken;
794
+ delete payload.refreshToken;
795
+ delete payload.userId;
796
+ delete payload.expires;
797
+ delete payload.issuedAt;
798
+ delete payload.lastSeenAt;
799
+ delete payload.status;
800
+ const access = this.jwtSign(payload, conf.accessSecret, conf.accessExpiry);
801
+ if (!access.success || !access.token) {
802
+ return null;
803
+ }
804
+ const updated = await this.updateToken({
805
+ refreshToken,
806
+ accessToken: access.token,
807
+ lastSeenAt: new Date()
808
+ });
809
+ if (!updated) {
810
+ return null;
811
+ }
812
+ this.setAccessCookie(apiReq, access.token, stored.sessionCookie ?? false);
813
+ if (apiReq.req.cookies) {
814
+ apiReq.req.cookies[conf.accessCookie] = access.token;
815
+ }
816
+ const verifiedAccess = await this.verifyJWT(access.token);
817
+ if (!verifiedAccess.tokenData) {
818
+ return null;
819
+ }
820
+ const refreshedStored = { ...stored, accessToken: access.token };
821
+ apiReq.authToken = refreshedStored;
822
+ return { token: access.token, tokenData: verifiedAccess.tokenData, stored: refreshedStored };
713
823
  }
714
824
  async authenticate(apiReq, authType) {
715
825
  if (authType === 'none') {
@@ -719,6 +829,7 @@ class ApiServer {
719
829
  let token = null;
720
830
  const authHeader = apiReq.req.headers.authorization;
721
831
  const requiresAuthToken = this.requiresAuthToken(authType);
832
+ const allowRefresh = requiresAuthToken || (authType === 'maybe' && this.config.refreshMaybe);
722
833
  const apiKeyAuth = await this.tryAuthenticateApiKey(apiReq, authType, authHeader);
723
834
  if (apiKeyAuth) {
724
835
  return apiKeyAuth;
@@ -727,32 +838,84 @@ class ApiServer {
727
838
  token = authHeader.slice(7).trim();
728
839
  }
729
840
  if (!token) {
730
- const access = apiReq.req.cookies?.dat;
841
+ const access = apiReq.req.cookies?.[this.config.accessCookie];
731
842
  if (access) {
732
843
  token = access;
733
844
  }
734
845
  }
846
+ let tokenData;
847
+ let error;
848
+ let expired = false;
735
849
  if (!token || token === null) {
736
- if (requiresAuthToken) {
737
- throw new ApiError({ code: 401, message: 'Authorization token is required (Bearer/cookie)' });
850
+ if (authType === 'maybe') {
851
+ if (!this.config.refreshMaybe) {
852
+ return null;
853
+ }
854
+ const refreshed = await this.tryRefreshAccessToken(apiReq);
855
+ if (!refreshed) {
856
+ return null;
857
+ }
858
+ token = refreshed.token;
859
+ tokenData = refreshed.tokenData;
860
+ error = undefined;
861
+ expired = false;
862
+ }
863
+ else if (requiresAuthToken) {
864
+ const refreshed = await this.tryRefreshAccessToken(apiReq);
865
+ if (!refreshed) {
866
+ throw new ApiError({ code: 401, message: 'Authorization token is required (Bearer/cookie)' });
867
+ }
868
+ token = refreshed.token;
869
+ tokenData = refreshed.tokenData;
870
+ error = undefined;
871
+ expired = false;
738
872
  }
739
873
  }
740
874
  if (!token) {
741
- if (authType === 'maybe') {
742
- return null;
743
- }
744
- else {
745
- throw new ApiError({ code: 401, message: 'Unauthorized Access - requires authentication' });
875
+ throw new ApiError({ code: 401, message: 'Unauthorized Access - requires authentication' });
876
+ }
877
+ if (!tokenData) {
878
+ const verified = await this.verifyJWT(token);
879
+ tokenData = verified.tokenData;
880
+ error = verified.error;
881
+ expired = verified.expired ?? false;
882
+ }
883
+ if (!tokenData && allowRefresh && expired) {
884
+ const refreshed = await this.tryRefreshAccessToken(apiReq);
885
+ if (refreshed) {
886
+ token = refreshed.token;
887
+ tokenData = refreshed.tokenData;
888
+ error = undefined;
746
889
  }
747
890
  }
748
- const { tokenData, error } = await this.verifyJWT(token);
749
891
  if (!tokenData) {
750
892
  throw new ApiError({ code: 401, message: 'Unathorized Access - ' + error });
751
893
  }
752
894
  const effectiveUserId = this.extractTokenUserId(tokenData);
753
895
  apiReq.realUid = this.resolveRealUserId(tokenData, effectiveUserId);
754
896
  if (this.shouldValidateStoredToken(authType)) {
755
- await this.assertStoredAccessToken(apiReq, token, tokenData);
897
+ try {
898
+ await this.assertStoredAccessToken(apiReq, token, tokenData);
899
+ }
900
+ catch (error) {
901
+ if (allowRefresh &&
902
+ error instanceof ApiError &&
903
+ error.code === 401 &&
904
+ error.message === 'Authorization token is no longer valid') {
905
+ const refreshed = await this.tryRefreshAccessToken(apiReq);
906
+ if (!refreshed) {
907
+ throw error;
908
+ }
909
+ token = refreshed.token;
910
+ tokenData = refreshed.tokenData;
911
+ const refreshedUserId = this.extractTokenUserId(tokenData);
912
+ apiReq.realUid = this.resolveRealUserId(tokenData, refreshedUserId);
913
+ await this.assertStoredAccessToken(apiReq, token, tokenData);
914
+ }
915
+ else {
916
+ throw error;
917
+ }
918
+ }
756
919
  }
757
920
  apiReq.token = token;
758
921
  return tokenData;
@@ -794,6 +957,9 @@ class ApiServer {
794
957
  }
795
958
  async assertStoredAccessToken(apiReq, token, tokenData) {
796
959
  const userId = String(this.extractTokenUserId(tokenData));
960
+ if (apiReq.authToken && apiReq.authToken.accessToken === token && String(apiReq.authToken.userId) === userId) {
961
+ return;
962
+ }
797
963
  const stored = await this.storageAdapter.getToken({
798
964
  accessToken: token,
799
965
  userId
@@ -833,32 +999,100 @@ class ApiServer {
833
999
  }
834
1000
  return rawReal;
835
1001
  }
836
- handle_request(handler, auth) {
1002
+ useExpress(pathOrHandler, ...handlers) {
1003
+ if (typeof pathOrHandler === 'string') {
1004
+ this.app.use(pathOrHandler, ...handlers);
1005
+ }
1006
+ else {
1007
+ this.app.use(pathOrHandler, ...handlers);
1008
+ }
1009
+ this.ensureApiNotFoundOrdering();
1010
+ return this;
1011
+ }
1012
+ createApiRequest(req, res) {
1013
+ const apiReq = {
1014
+ server: this,
1015
+ req,
1016
+ res,
1017
+ token: '',
1018
+ tokenData: null,
1019
+ realUid: null,
1020
+ getClientInfo: () => ensureClientInfo(apiReq),
1021
+ getClientIp: () => ensureClientInfo(apiReq).ip,
1022
+ getClientIpChain: () => ensureClientInfo(apiReq).ipchain,
1023
+ getRealUid: () => apiReq.realUid ?? null,
1024
+ isImpersonating: () => {
1025
+ const realUid = apiReq.realUid;
1026
+ const tokenUid = apiReq.tokenData?.uid;
1027
+ if (realUid === null || realUid === undefined) {
1028
+ return false;
1029
+ }
1030
+ if (tokenUid === null || tokenUid === undefined) {
1031
+ return false;
1032
+ }
1033
+ return realUid !== tokenUid;
1034
+ }
1035
+ };
1036
+ return apiReq;
1037
+ }
1038
+ expressAuth(auth) {
837
1039
  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;
1040
+ const apiReq = this.createApiRequest(req, res);
1041
+ req.apiReq = apiReq;
1042
+ res.locals.apiReq = apiReq;
1043
+ this.currReq = apiReq;
1044
+ try {
1045
+ if (this.config.hydrateGetBody) {
1046
+ hydrateGetBody(req);
1047
+ }
1048
+ if (this.config.debug) {
1049
+ this.dumpRequest(apiReq);
860
1050
  }
1051
+ apiReq.tokenData = await this.authenticate(apiReq, auth.type);
1052
+ await this.authorize(apiReq, auth.req);
1053
+ next();
1054
+ }
1055
+ catch (error) {
1056
+ next(error);
1057
+ }
1058
+ };
1059
+ }
1060
+ expressErrorHandler() {
1061
+ return (error, _req, res, next) => {
1062
+ void _req;
1063
+ if (res.headersSent) {
1064
+ next(error);
1065
+ return;
1066
+ }
1067
+ if (error instanceof ApiError || isApiErrorLike(error)) {
1068
+ const apiError = error;
1069
+ const normalizedErrors = apiError.errors && typeof apiError.errors === 'object' && !Array.isArray(apiError.errors)
1070
+ ? apiError.errors
1071
+ : {};
1072
+ const errorPayload = {
1073
+ success: false,
1074
+ code: apiError.code,
1075
+ message: apiError.message,
1076
+ data: apiError.data ?? null,
1077
+ errors: normalizedErrors
1078
+ };
1079
+ res.status(apiError.code).json(errorPayload);
1080
+ return;
1081
+ }
1082
+ const errorPayload = {
1083
+ success: false,
1084
+ code: 500,
1085
+ message: this.guessExceptionText(error),
1086
+ data: null,
1087
+ errors: {}
861
1088
  };
1089
+ res.status(500).json(errorPayload);
1090
+ };
1091
+ }
1092
+ handle_request(handler, auth) {
1093
+ return async (req, res, next) => {
1094
+ void next;
1095
+ const apiReq = this.createApiRequest(req, res);
862
1096
  this.currReq = apiReq;
863
1097
  try {
864
1098
  if (this.config.hydrateGetBody) {
@@ -881,7 +1115,7 @@ class ApiServer {
881
1115
  throw new ApiError({ code: 500, message: 'Handler result must start with a numeric status code' });
882
1116
  }
883
1117
  const message = typeof rawMessage === 'string' ? rawMessage : 'Success';
884
- const responsePayload = { code, message, data };
1118
+ const responsePayload = { success: true, code, message, data, errors: {} };
885
1119
  if (this.config.debug) {
886
1120
  this.dumpResponse(apiReq, responsePayload, code);
887
1121
  }
@@ -894,6 +1128,7 @@ class ApiServer {
894
1128
  ? apiError.errors
895
1129
  : {};
896
1130
  const errorPayload = {
1131
+ success: false,
897
1132
  code: apiError.code,
898
1133
  message: apiError.message,
899
1134
  data: apiError.data ?? null,
@@ -906,6 +1141,7 @@ class ApiServer {
906
1141
  }
907
1142
  else {
908
1143
  const errorPayload = {
1144
+ success: false,
909
1145
  code: 500,
910
1146
  message: this.guessExceptionText(error),
911
1147
  data: null,
@@ -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 {};