@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.
- package/README.txt +25 -2
- package/dist/cjs/api-server-base.cjs +269 -39
- 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 +193 -45
- 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 +270 -40
- package/dist/esm/auth-api/auth-module.d.ts +11 -2
- package/dist/esm/auth-api/auth-module.js +194 -46
- 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 +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
|
|
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`
|
|
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.
|
|
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
|
-
//
|
|
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
|
|
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?.
|
|
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 (
|
|
737
|
-
|
|
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
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
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 {
|
|
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:
|
|
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:
|
|
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():
|
|
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 {
|
|
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 {};
|