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