@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.
- package/README.txt +25 -2
- package/dist/cjs/api-server-base.cjs +277 -41
- package/dist/cjs/api-server-base.d.ts +27 -7
- package/dist/cjs/auth-api/auth-module.d.ts +11 -2
- package/dist/cjs/auth-api/auth-module.js +215 -46
- package/dist/cjs/auth-api/compat-auth-storage.d.ts +7 -5
- package/dist/cjs/auth-api/compat-auth-storage.js +15 -3
- package/dist/cjs/auth-api/mem-auth-store.d.ts +5 -3
- package/dist/cjs/auth-api/mem-auth-store.js +7 -1
- package/dist/cjs/auth-api/sql-auth-store.d.ts +5 -3
- package/dist/cjs/auth-api/sql-auth-store.js +7 -1
- package/dist/cjs/auth-api/storage.d.ts +6 -4
- package/dist/cjs/auth-api/storage.js +15 -5
- package/dist/cjs/auth-api/types.d.ts +7 -2
- package/dist/cjs/index.cjs +4 -4
- package/dist/cjs/index.d.ts +4 -4
- package/dist/cjs/oauth/sequelize.js +1 -1
- package/dist/cjs/passkey/base.d.ts +1 -0
- package/dist/cjs/passkey/memory.d.ts +1 -0
- package/dist/cjs/passkey/memory.js +4 -0
- package/dist/cjs/passkey/sequelize.d.ts +1 -0
- package/dist/cjs/passkey/sequelize.js +11 -2
- package/dist/cjs/passkey/service.d.ts +5 -2
- package/dist/cjs/passkey/service.js +145 -10
- package/dist/cjs/passkey/types.d.ts +3 -0
- package/dist/cjs/user/base.js +2 -1
- package/dist/esm/api-server-base.d.ts +27 -7
- package/dist/esm/api-server-base.js +278 -42
- package/dist/esm/auth-api/auth-module.d.ts +11 -2
- package/dist/esm/auth-api/auth-module.js +216 -47
- package/dist/esm/auth-api/compat-auth-storage.d.ts +7 -5
- package/dist/esm/auth-api/compat-auth-storage.js +13 -1
- package/dist/esm/auth-api/mem-auth-store.d.ts +5 -3
- package/dist/esm/auth-api/mem-auth-store.js +8 -2
- package/dist/esm/auth-api/sql-auth-store.d.ts +5 -3
- package/dist/esm/auth-api/sql-auth-store.js +8 -2
- package/dist/esm/auth-api/storage.d.ts +6 -4
- package/dist/esm/auth-api/storage.js +13 -3
- package/dist/esm/auth-api/types.d.ts +7 -2
- package/dist/esm/index.d.ts +4 -4
- package/dist/esm/index.js +2 -2
- package/dist/esm/oauth/sequelize.js +1 -1
- package/dist/esm/passkey/base.d.ts +1 -0
- package/dist/esm/passkey/memory.d.ts +1 -0
- package/dist/esm/passkey/memory.js +4 -0
- package/dist/esm/passkey/sequelize.d.ts +1 -0
- package/dist/esm/passkey/sequelize.js +11 -2
- package/dist/esm/passkey/service.d.ts +5 -2
- package/dist/esm/passkey/service.js +113 -11
- package/dist/esm/passkey/types.d.ts +3 -0
- package/dist/esm/user/base.js +2 -1
- package/package.json +13 -11
|
@@ -10,7 +10,7 @@ import cors from 'cors';
|
|
|
10
10
|
import express from 'express';
|
|
11
11
|
import multer from 'multer';
|
|
12
12
|
import { nullAuthModule } from './auth-api/module.js';
|
|
13
|
-
import {
|
|
13
|
+
import { nullAuthAdapter } from './auth-api/storage.js';
|
|
14
14
|
import { TokenStore } from './token/base.js';
|
|
15
15
|
class JwtHelperStore extends TokenStore {
|
|
16
16
|
async save() {
|
|
@@ -344,6 +344,7 @@ function fillConfig(config) {
|
|
|
344
344
|
devMode: config.devMode ?? false,
|
|
345
345
|
hydrateGetBody: config.hydrateGetBody ?? true,
|
|
346
346
|
validateTokens: config.validateTokens ?? false,
|
|
347
|
+
refreshMaybe: config.refreshMaybe ?? false,
|
|
347
348
|
apiVersion: config.apiVersion ?? '',
|
|
348
349
|
minClientVersion: config.minClientVersion ?? '',
|
|
349
350
|
tokenStore: config.tokenStore,
|
|
@@ -362,7 +363,7 @@ export class ApiServer {
|
|
|
362
363
|
this.config = fillConfig(config);
|
|
363
364
|
this.apiBasePath = this.normalizeApiBasePath(this.config.apiBasePath);
|
|
364
365
|
this.startedAt = Date.now();
|
|
365
|
-
this.storageAdapter =
|
|
366
|
+
this.storageAdapter = nullAuthAdapter;
|
|
366
367
|
this.moduleAdapter = nullAuthModule;
|
|
367
368
|
this.jwtHelper = new JwtHelperStore();
|
|
368
369
|
this.tokenStoreAdapter = this.config.tokenStore ?? null;
|
|
@@ -440,13 +441,19 @@ export class ApiServer {
|
|
|
440
441
|
}
|
|
441
442
|
return this.passkeyServiceAdapter;
|
|
442
443
|
}
|
|
444
|
+
async listUserCredentials(userId) {
|
|
445
|
+
return this.ensurePasskeyService().listUserCredentials(userId);
|
|
446
|
+
}
|
|
447
|
+
async deletePasskeyCredential(credentialId) {
|
|
448
|
+
return this.ensurePasskeyService().deleteCredential(credentialId);
|
|
449
|
+
}
|
|
443
450
|
ensureOAuthStore() {
|
|
444
451
|
if (!this.oauthStoreAdapter) {
|
|
445
452
|
throw new Error('OAuth store is not configured');
|
|
446
453
|
}
|
|
447
454
|
return this.oauthStoreAdapter;
|
|
448
455
|
}
|
|
449
|
-
//
|
|
456
|
+
// AuthAdapter-compatible helpers (used by AuthModule)
|
|
450
457
|
async getUser(identifier) {
|
|
451
458
|
return this.userStoreAdapter ? this.userStoreAdapter.findUser(identifier) : null;
|
|
452
459
|
}
|
|
@@ -604,6 +611,7 @@ export class ApiServer {
|
|
|
604
611
|
const path = `${this.apiBasePath}/v1/ping`;
|
|
605
612
|
this.app.get(path, (_req, res) => {
|
|
606
613
|
const payload = {
|
|
614
|
+
success: true,
|
|
607
615
|
status: 'ok',
|
|
608
616
|
apiVersion: this.config.apiVersion ?? '',
|
|
609
617
|
minClientVersion: this.config.minClientVersion ?? '',
|
|
@@ -611,7 +619,7 @@ export class ApiServer {
|
|
|
611
619
|
startedAt: this.startedAt,
|
|
612
620
|
timestamp: new Date().toISOString()
|
|
613
621
|
};
|
|
614
|
-
res.status(200).json({ code: 200, message: 'Success', data: payload });
|
|
622
|
+
res.status(200).json({ success: true, code: 200, message: 'Success', data: payload, errors: {} });
|
|
615
623
|
});
|
|
616
624
|
}
|
|
617
625
|
normalizeApiBasePath(path) {
|
|
@@ -634,6 +642,7 @@ export class ApiServer {
|
|
|
634
642
|
}
|
|
635
643
|
this.apiNotFoundHandler = (req, res) => {
|
|
636
644
|
const payload = {
|
|
645
|
+
success: false,
|
|
637
646
|
code: 404,
|
|
638
647
|
message: this.describeMissingEndpoint(req),
|
|
639
648
|
data: null,
|
|
@@ -692,16 +701,117 @@ export class ApiServer {
|
|
|
692
701
|
}
|
|
693
702
|
async verifyJWT(token) {
|
|
694
703
|
if (!this.config.accessSecret) {
|
|
695
|
-
return { tokenData: undefined, error: 'JWT authentication disabled; no jwtSecret set' };
|
|
704
|
+
return { tokenData: undefined, error: 'JWT authentication disabled; no jwtSecret set', expired: false };
|
|
696
705
|
}
|
|
697
706
|
const result = this.jwtVerify(token, this.config.accessSecret);
|
|
698
707
|
if (!result.success) {
|
|
699
|
-
return { tokenData: undefined, error: result.error };
|
|
708
|
+
return { tokenData: undefined, error: result.error, expired: result.expired };
|
|
700
709
|
}
|
|
701
710
|
if (!result.data.uid) {
|
|
702
|
-
return { tokenData: undefined, error: 'Missing/bad userid in token' };
|
|
711
|
+
return { tokenData: undefined, error: 'Missing/bad userid in token', expired: false };
|
|
712
|
+
}
|
|
713
|
+
return { tokenData: result.data, error: undefined, expired: false };
|
|
714
|
+
}
|
|
715
|
+
jwtCookieOptions(apiReq) {
|
|
716
|
+
const conf = this.config;
|
|
717
|
+
const forwarded = apiReq.req.headers['x-forwarded-proto'];
|
|
718
|
+
const referer = apiReq.req.headers['origin'] ?? apiReq.req.headers['referer'];
|
|
719
|
+
const origin = typeof referer === 'string' ? referer : '';
|
|
720
|
+
const isHttps = forwarded === 'https' || apiReq.req.protocol === 'https';
|
|
721
|
+
const isLocalhost = origin.includes('localhost');
|
|
722
|
+
const options = {
|
|
723
|
+
httpOnly: true,
|
|
724
|
+
secure: true,
|
|
725
|
+
sameSite: 'strict',
|
|
726
|
+
domain: conf.cookieDomain || undefined,
|
|
727
|
+
path: '/',
|
|
728
|
+
maxAge: undefined
|
|
729
|
+
};
|
|
730
|
+
if (conf.devMode) {
|
|
731
|
+
options.secure = isHttps;
|
|
732
|
+
options.httpOnly = false;
|
|
733
|
+
options.sameSite = 'lax';
|
|
734
|
+
if (isLocalhost) {
|
|
735
|
+
options.domain = undefined;
|
|
736
|
+
}
|
|
703
737
|
}
|
|
704
|
-
return
|
|
738
|
+
return options;
|
|
739
|
+
}
|
|
740
|
+
setAccessCookie(apiReq, accessToken, sessionCookie) {
|
|
741
|
+
const conf = this.config;
|
|
742
|
+
const options = this.jwtCookieOptions(apiReq);
|
|
743
|
+
const accessMaxAge = Math.max(1, conf.accessExpiry) * 1000;
|
|
744
|
+
const accessOptions = sessionCookie ? options : { ...options, maxAge: accessMaxAge };
|
|
745
|
+
apiReq.res.cookie(conf.accessCookie, accessToken, accessOptions);
|
|
746
|
+
}
|
|
747
|
+
async tryRefreshAccessToken(apiReq) {
|
|
748
|
+
const conf = this.config;
|
|
749
|
+
if (!conf.refreshSecret || !conf.accessSecret) {
|
|
750
|
+
return null;
|
|
751
|
+
}
|
|
752
|
+
const rawRefresh = apiReq.req.cookies?.[conf.refreshCookie];
|
|
753
|
+
if (typeof rawRefresh !== 'string') {
|
|
754
|
+
return null;
|
|
755
|
+
}
|
|
756
|
+
const refreshToken = rawRefresh.trim();
|
|
757
|
+
if (!refreshToken) {
|
|
758
|
+
return null;
|
|
759
|
+
}
|
|
760
|
+
const verify = this.jwtVerify(refreshToken, conf.refreshSecret);
|
|
761
|
+
if (!verify.success || !verify.data) {
|
|
762
|
+
return null;
|
|
763
|
+
}
|
|
764
|
+
let stored = null;
|
|
765
|
+
try {
|
|
766
|
+
stored = await this.storageAdapter.getToken({ refreshToken });
|
|
767
|
+
}
|
|
768
|
+
catch {
|
|
769
|
+
return null;
|
|
770
|
+
}
|
|
771
|
+
if (!stored) {
|
|
772
|
+
return null;
|
|
773
|
+
}
|
|
774
|
+
const storedUid = String(stored.userId);
|
|
775
|
+
const verifyUid = verify.data.uid === undefined || verify.data.uid === null ? null : String(verify.data.uid);
|
|
776
|
+
if (verifyUid && verifyUid !== storedUid) {
|
|
777
|
+
return null;
|
|
778
|
+
}
|
|
779
|
+
const claims = verify.data;
|
|
780
|
+
const { exp: _exp, iat: _iat, nbf: _nbf, ...payload } = claims;
|
|
781
|
+
void _exp;
|
|
782
|
+
void _iat;
|
|
783
|
+
void _nbf;
|
|
784
|
+
// Ensure we never embed token secrets into refreshed access tokens.
|
|
785
|
+
delete payload.accessToken;
|
|
786
|
+
delete payload.refreshToken;
|
|
787
|
+
delete payload.userId;
|
|
788
|
+
delete payload.expires;
|
|
789
|
+
delete payload.issuedAt;
|
|
790
|
+
delete payload.lastSeenAt;
|
|
791
|
+
delete payload.status;
|
|
792
|
+
const access = this.jwtSign(payload, conf.accessSecret, conf.accessExpiry);
|
|
793
|
+
if (!access.success || !access.token) {
|
|
794
|
+
return null;
|
|
795
|
+
}
|
|
796
|
+
const updated = await this.updateToken({
|
|
797
|
+
refreshToken,
|
|
798
|
+
accessToken: access.token,
|
|
799
|
+
lastSeenAt: new Date()
|
|
800
|
+
});
|
|
801
|
+
if (!updated) {
|
|
802
|
+
return null;
|
|
803
|
+
}
|
|
804
|
+
this.setAccessCookie(apiReq, access.token, stored.sessionCookie ?? false);
|
|
805
|
+
if (apiReq.req.cookies) {
|
|
806
|
+
apiReq.req.cookies[conf.accessCookie] = access.token;
|
|
807
|
+
}
|
|
808
|
+
const verifiedAccess = await this.verifyJWT(access.token);
|
|
809
|
+
if (!verifiedAccess.tokenData) {
|
|
810
|
+
return null;
|
|
811
|
+
}
|
|
812
|
+
const refreshedStored = { ...stored, accessToken: access.token };
|
|
813
|
+
apiReq.authToken = refreshedStored;
|
|
814
|
+
return { token: access.token, tokenData: verifiedAccess.tokenData, stored: refreshedStored };
|
|
705
815
|
}
|
|
706
816
|
async authenticate(apiReq, authType) {
|
|
707
817
|
if (authType === 'none') {
|
|
@@ -711,6 +821,7 @@ export class ApiServer {
|
|
|
711
821
|
let token = null;
|
|
712
822
|
const authHeader = apiReq.req.headers.authorization;
|
|
713
823
|
const requiresAuthToken = this.requiresAuthToken(authType);
|
|
824
|
+
const allowRefresh = requiresAuthToken || (authType === 'maybe' && this.config.refreshMaybe);
|
|
714
825
|
const apiKeyAuth = await this.tryAuthenticateApiKey(apiReq, authType, authHeader);
|
|
715
826
|
if (apiKeyAuth) {
|
|
716
827
|
return apiKeyAuth;
|
|
@@ -719,32 +830,84 @@ export class ApiServer {
|
|
|
719
830
|
token = authHeader.slice(7).trim();
|
|
720
831
|
}
|
|
721
832
|
if (!token) {
|
|
722
|
-
const access = apiReq.req.cookies?.
|
|
833
|
+
const access = apiReq.req.cookies?.[this.config.accessCookie];
|
|
723
834
|
if (access) {
|
|
724
835
|
token = access;
|
|
725
836
|
}
|
|
726
837
|
}
|
|
838
|
+
let tokenData;
|
|
839
|
+
let error;
|
|
840
|
+
let expired = false;
|
|
727
841
|
if (!token || token === null) {
|
|
728
|
-
if (
|
|
729
|
-
|
|
842
|
+
if (authType === 'maybe') {
|
|
843
|
+
if (!this.config.refreshMaybe) {
|
|
844
|
+
return null;
|
|
845
|
+
}
|
|
846
|
+
const refreshed = await this.tryRefreshAccessToken(apiReq);
|
|
847
|
+
if (!refreshed) {
|
|
848
|
+
return null;
|
|
849
|
+
}
|
|
850
|
+
token = refreshed.token;
|
|
851
|
+
tokenData = refreshed.tokenData;
|
|
852
|
+
error = undefined;
|
|
853
|
+
expired = false;
|
|
854
|
+
}
|
|
855
|
+
else if (requiresAuthToken) {
|
|
856
|
+
const refreshed = await this.tryRefreshAccessToken(apiReq);
|
|
857
|
+
if (!refreshed) {
|
|
858
|
+
throw new ApiError({ code: 401, message: 'Authorization token is required (Bearer/cookie)' });
|
|
859
|
+
}
|
|
860
|
+
token = refreshed.token;
|
|
861
|
+
tokenData = refreshed.tokenData;
|
|
862
|
+
error = undefined;
|
|
863
|
+
expired = false;
|
|
730
864
|
}
|
|
731
865
|
}
|
|
732
866
|
if (!token) {
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
867
|
+
throw new ApiError({ code: 401, message: 'Unauthorized Access - requires authentication' });
|
|
868
|
+
}
|
|
869
|
+
if (!tokenData) {
|
|
870
|
+
const verified = await this.verifyJWT(token);
|
|
871
|
+
tokenData = verified.tokenData;
|
|
872
|
+
error = verified.error;
|
|
873
|
+
expired = verified.expired ?? false;
|
|
874
|
+
}
|
|
875
|
+
if (!tokenData && allowRefresh && expired) {
|
|
876
|
+
const refreshed = await this.tryRefreshAccessToken(apiReq);
|
|
877
|
+
if (refreshed) {
|
|
878
|
+
token = refreshed.token;
|
|
879
|
+
tokenData = refreshed.tokenData;
|
|
880
|
+
error = undefined;
|
|
738
881
|
}
|
|
739
882
|
}
|
|
740
|
-
const { tokenData, error } = await this.verifyJWT(token);
|
|
741
883
|
if (!tokenData) {
|
|
742
884
|
throw new ApiError({ code: 401, message: 'Unathorized Access - ' + error });
|
|
743
885
|
}
|
|
744
886
|
const effectiveUserId = this.extractTokenUserId(tokenData);
|
|
745
887
|
apiReq.realUid = this.resolveRealUserId(tokenData, effectiveUserId);
|
|
746
888
|
if (this.shouldValidateStoredToken(authType)) {
|
|
747
|
-
|
|
889
|
+
try {
|
|
890
|
+
await this.assertStoredAccessToken(apiReq, token, tokenData);
|
|
891
|
+
}
|
|
892
|
+
catch (error) {
|
|
893
|
+
if (allowRefresh &&
|
|
894
|
+
error instanceof ApiError &&
|
|
895
|
+
error.code === 401 &&
|
|
896
|
+
error.message === 'Authorization token is no longer valid') {
|
|
897
|
+
const refreshed = await this.tryRefreshAccessToken(apiReq);
|
|
898
|
+
if (!refreshed) {
|
|
899
|
+
throw error;
|
|
900
|
+
}
|
|
901
|
+
token = refreshed.token;
|
|
902
|
+
tokenData = refreshed.tokenData;
|
|
903
|
+
const refreshedUserId = this.extractTokenUserId(tokenData);
|
|
904
|
+
apiReq.realUid = this.resolveRealUserId(tokenData, refreshedUserId);
|
|
905
|
+
await this.assertStoredAccessToken(apiReq, token, tokenData);
|
|
906
|
+
}
|
|
907
|
+
else {
|
|
908
|
+
throw error;
|
|
909
|
+
}
|
|
910
|
+
}
|
|
748
911
|
}
|
|
749
912
|
apiReq.token = token;
|
|
750
913
|
return tokenData;
|
|
@@ -786,6 +949,9 @@ export class ApiServer {
|
|
|
786
949
|
}
|
|
787
950
|
async assertStoredAccessToken(apiReq, token, tokenData) {
|
|
788
951
|
const userId = String(this.extractTokenUserId(tokenData));
|
|
952
|
+
if (apiReq.authToken && apiReq.authToken.accessToken === token && String(apiReq.authToken.userId) === userId) {
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
789
955
|
const stored = await this.storageAdapter.getToken({
|
|
790
956
|
accessToken: token,
|
|
791
957
|
userId
|
|
@@ -825,32 +991,100 @@ export class ApiServer {
|
|
|
825
991
|
}
|
|
826
992
|
return rawReal;
|
|
827
993
|
}
|
|
828
|
-
|
|
994
|
+
useExpress(pathOrHandler, ...handlers) {
|
|
995
|
+
if (typeof pathOrHandler === 'string') {
|
|
996
|
+
this.app.use(pathOrHandler, ...handlers);
|
|
997
|
+
}
|
|
998
|
+
else {
|
|
999
|
+
this.app.use(pathOrHandler, ...handlers);
|
|
1000
|
+
}
|
|
1001
|
+
this.ensureApiNotFoundOrdering();
|
|
1002
|
+
return this;
|
|
1003
|
+
}
|
|
1004
|
+
createApiRequest(req, res) {
|
|
1005
|
+
const apiReq = {
|
|
1006
|
+
server: this,
|
|
1007
|
+
req,
|
|
1008
|
+
res,
|
|
1009
|
+
token: '',
|
|
1010
|
+
tokenData: null,
|
|
1011
|
+
realUid: null,
|
|
1012
|
+
getClientInfo: () => ensureClientInfo(apiReq),
|
|
1013
|
+
getClientIp: () => ensureClientInfo(apiReq).ip,
|
|
1014
|
+
getClientIpChain: () => ensureClientInfo(apiReq).ipchain,
|
|
1015
|
+
getRealUid: () => apiReq.realUid ?? null,
|
|
1016
|
+
isImpersonating: () => {
|
|
1017
|
+
const realUid = apiReq.realUid;
|
|
1018
|
+
const tokenUid = apiReq.tokenData?.uid;
|
|
1019
|
+
if (realUid === null || realUid === undefined) {
|
|
1020
|
+
return false;
|
|
1021
|
+
}
|
|
1022
|
+
if (tokenUid === null || tokenUid === undefined) {
|
|
1023
|
+
return false;
|
|
1024
|
+
}
|
|
1025
|
+
return realUid !== tokenUid;
|
|
1026
|
+
}
|
|
1027
|
+
};
|
|
1028
|
+
return apiReq;
|
|
1029
|
+
}
|
|
1030
|
+
expressAuth(auth) {
|
|
829
1031
|
return async (req, res, next) => {
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
getClientIpChain: () => ensureClientInfo(apiReq).ipchain,
|
|
841
|
-
getRealUid: () => apiReq.realUid ?? null,
|
|
842
|
-
isImpersonating: () => {
|
|
843
|
-
const realUid = apiReq.realUid;
|
|
844
|
-
const tokenUid = apiReq.tokenData?.uid;
|
|
845
|
-
if (realUid === null || realUid === undefined) {
|
|
846
|
-
return false;
|
|
847
|
-
}
|
|
848
|
-
if (tokenUid === null || tokenUid === undefined) {
|
|
849
|
-
return false;
|
|
850
|
-
}
|
|
851
|
-
return realUid !== tokenUid;
|
|
1032
|
+
const apiReq = this.createApiRequest(req, res);
|
|
1033
|
+
req.apiReq = apiReq;
|
|
1034
|
+
res.locals.apiReq = apiReq;
|
|
1035
|
+
this.currReq = apiReq;
|
|
1036
|
+
try {
|
|
1037
|
+
if (this.config.hydrateGetBody) {
|
|
1038
|
+
hydrateGetBody(req);
|
|
1039
|
+
}
|
|
1040
|
+
if (this.config.debug) {
|
|
1041
|
+
this.dumpRequest(apiReq);
|
|
852
1042
|
}
|
|
1043
|
+
apiReq.tokenData = await this.authenticate(apiReq, auth.type);
|
|
1044
|
+
await this.authorize(apiReq, auth.req);
|
|
1045
|
+
next();
|
|
1046
|
+
}
|
|
1047
|
+
catch (error) {
|
|
1048
|
+
next(error);
|
|
1049
|
+
}
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1052
|
+
expressErrorHandler() {
|
|
1053
|
+
return (error, _req, res, next) => {
|
|
1054
|
+
void _req;
|
|
1055
|
+
if (res.headersSent) {
|
|
1056
|
+
next(error);
|
|
1057
|
+
return;
|
|
1058
|
+
}
|
|
1059
|
+
if (error instanceof ApiError || isApiErrorLike(error)) {
|
|
1060
|
+
const apiError = error;
|
|
1061
|
+
const normalizedErrors = apiError.errors && typeof apiError.errors === 'object' && !Array.isArray(apiError.errors)
|
|
1062
|
+
? apiError.errors
|
|
1063
|
+
: {};
|
|
1064
|
+
const errorPayload = {
|
|
1065
|
+
success: false,
|
|
1066
|
+
code: apiError.code,
|
|
1067
|
+
message: apiError.message,
|
|
1068
|
+
data: apiError.data ?? null,
|
|
1069
|
+
errors: normalizedErrors
|
|
1070
|
+
};
|
|
1071
|
+
res.status(apiError.code).json(errorPayload);
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1074
|
+
const errorPayload = {
|
|
1075
|
+
success: false,
|
|
1076
|
+
code: 500,
|
|
1077
|
+
message: this.guessExceptionText(error),
|
|
1078
|
+
data: null,
|
|
1079
|
+
errors: {}
|
|
853
1080
|
};
|
|
1081
|
+
res.status(500).json(errorPayload);
|
|
1082
|
+
};
|
|
1083
|
+
}
|
|
1084
|
+
handle_request(handler, auth) {
|
|
1085
|
+
return async (req, res, next) => {
|
|
1086
|
+
void next;
|
|
1087
|
+
const apiReq = this.createApiRequest(req, res);
|
|
854
1088
|
this.currReq = apiReq;
|
|
855
1089
|
try {
|
|
856
1090
|
if (this.config.hydrateGetBody) {
|
|
@@ -873,7 +1107,7 @@ export class ApiServer {
|
|
|
873
1107
|
throw new ApiError({ code: 500, message: 'Handler result must start with a numeric status code' });
|
|
874
1108
|
}
|
|
875
1109
|
const message = typeof rawMessage === 'string' ? rawMessage : 'Success';
|
|
876
|
-
const responsePayload = { code, message, data };
|
|
1110
|
+
const responsePayload = { success: true, code, message, data, errors: {} };
|
|
877
1111
|
if (this.config.debug) {
|
|
878
1112
|
this.dumpResponse(apiReq, responsePayload, code);
|
|
879
1113
|
}
|
|
@@ -886,6 +1120,7 @@ export class ApiServer {
|
|
|
886
1120
|
? apiError.errors
|
|
887
1121
|
: {};
|
|
888
1122
|
const errorPayload = {
|
|
1123
|
+
success: false,
|
|
889
1124
|
code: apiError.code,
|
|
890
1125
|
message: apiError.message,
|
|
891
1126
|
data: apiError.data ?? null,
|
|
@@ -898,6 +1133,7 @@ export class ApiServer {
|
|
|
898
1133
|
}
|
|
899
1134
|
else {
|
|
900
1135
|
const errorPayload = {
|
|
1136
|
+
success: false,
|
|
901
1137
|
code: 500,
|
|
902
1138
|
message: this.guessExceptionText(error),
|
|
903
1139
|
data: null,
|
|
@@ -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 {
|
|
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():
|
|
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 {};
|