@technomoron/api-server-base 2.0.0-beta.22 → 2.0.0-beta.23
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/dist/cjs/common/types.cjs +10 -0
- package/dist/cjs/common/types.d.ts +137 -0
- package/dist/cjs/{api-module.cjs → server/src/api-module.cjs} +8 -0
- package/dist/{esm → cjs/server/src}/api-module.d.ts +12 -0
- package/dist/cjs/{api-server-base.cjs → server/src/api-server-base.cjs} +573 -615
- package/dist/{esm → cjs/server/src}/api-server-base.d.ts +97 -87
- package/dist/cjs/{auth-api/auth-module.js → server/src/auth-api/auth-module.cjs} +96 -76
- package/dist/cjs/{auth-api → server/src/auth-api}/auth-module.d.ts +1 -1
- package/dist/cjs/{auth-api/compat-auth-storage.js → server/src/auth-api/compat-auth-storage.cjs} +4 -4
- package/dist/cjs/{auth-api/mem-auth-store.js → server/src/auth-api/mem-auth-store.cjs} +7 -7
- package/dist/cjs/{auth-api/module.js → server/src/auth-api/module.cjs} +1 -1
- package/dist/cjs/server/src/auth-api/schemas.cjs +171 -0
- package/dist/cjs/server/src/auth-api/schemas.d.ts +21 -0
- package/dist/cjs/{auth-api/sql-auth-store.js → server/src/auth-api/sql-auth-store.cjs} +8 -8
- package/dist/cjs/{auth-api/user-id.js → server/src/auth-api/user-id.cjs} +12 -3
- package/dist/{esm → cjs/server/src}/auth-cookie-options.d.ts +5 -3
- package/dist/cjs/server/src/base/client-info.cjs +285 -0
- package/dist/cjs/server/src/base/client-info.d.ts +27 -0
- package/dist/cjs/server/src/base/error-utils.cjs +50 -0
- package/dist/cjs/server/src/base/error-utils.d.ts +16 -0
- package/dist/cjs/server/src/base/request-utils.cjs +27 -0
- package/dist/cjs/server/src/base/request-utils.d.ts +8 -0
- package/dist/cjs/{index.cjs → server/src/index.cjs} +24 -15
- package/dist/{esm → cjs/server/src}/index.d.ts +7 -0
- package/dist/cjs/server/src/limiter/auth-rate-limiter.cjs +35 -0
- package/dist/cjs/server/src/limiter/auth-rate-limiter.d.ts +12 -0
- package/dist/cjs/server/src/limiter/fixed-window.cjs +41 -0
- package/dist/cjs/server/src/limiter/fixed-window.d.ts +11 -0
- package/dist/cjs/{oauth/base.js → server/src/oauth/base.cjs} +1 -0
- package/dist/{esm → cjs/server/src}/oauth/base.d.ts +8 -1
- package/dist/cjs/{oauth/memory.js → server/src/oauth/memory.cjs} +7 -4
- package/dist/{esm → cjs/server/src}/oauth/memory.d.ts +1 -1
- package/dist/cjs/{oauth/models.js → server/src/oauth/models.cjs} +2 -2
- package/dist/cjs/{oauth/sequelize.js → server/src/oauth/sequelize.cjs} +11 -7
- package/dist/{esm → cjs/server/src}/oauth/sequelize.d.ts +1 -1
- package/dist/cjs/{passkey/base.js → server/src/passkey/base.cjs} +1 -0
- package/dist/{esm → cjs/server/src}/passkey/base.d.ts +11 -0
- package/dist/cjs/{passkey/memory.js → server/src/passkey/memory.cjs} +2 -2
- package/dist/cjs/{passkey/models.js → server/src/passkey/models.cjs} +1 -1
- package/dist/cjs/{passkey/sequelize.js → server/src/passkey/sequelize.cjs} +3 -3
- package/dist/cjs/{passkey/service.js → server/src/passkey/service.cjs} +17 -3
- package/dist/{esm → cjs/server/src}/passkey/service.d.ts +1 -1
- package/dist/cjs/{sequelize-utils.js → server/src/sequelize-utils.cjs} +4 -5
- package/dist/cjs/{token/base.js → server/src/token/base.cjs} +4 -0
- package/dist/{esm → cjs/server/src}/token/base.d.ts +7 -0
- package/dist/cjs/{token/memory.js → server/src/token/memory.cjs} +15 -20
- package/dist/cjs/{token/sequelize.js → server/src/token/sequelize.cjs} +25 -11
- package/dist/cjs/server/src/upload/memory.cjs +92 -0
- package/dist/cjs/server/src/upload/memory.d.ts +17 -0
- package/dist/cjs/server/src/upload/tus-module.cjs +270 -0
- package/dist/cjs/server/src/upload/tus-module.d.ts +38 -0
- package/dist/cjs/server/src/upload/types.d.ts +8 -0
- package/dist/cjs/{user/base.js → server/src/user/base.cjs} +1 -0
- package/dist/{esm → cjs/server/src}/user/base.d.ts +9 -0
- package/dist/cjs/{user/memory.js → server/src/user/memory.cjs} +29 -7
- package/dist/cjs/{user/sequelize.js → server/src/user/sequelize.cjs} +33 -8
- package/dist/cjs/server/src/user/types.cjs +2 -0
- package/dist/esm/common/types.d.ts +137 -0
- package/dist/esm/common/types.js +9 -0
- package/dist/{cjs → esm/server/src}/api-module.d.ts +12 -0
- package/dist/esm/{api-module.js → server/src/api-module.js} +8 -0
- package/dist/{cjs → esm/server/src}/api-server-base.d.ts +97 -87
- package/dist/esm/{api-server-base.js → server/src/api-server-base.js} +562 -604
- package/dist/esm/{auth-api → server/src/auth-api}/auth-module.d.ts +1 -1
- package/dist/esm/{auth-api → server/src/auth-api}/auth-module.js +92 -72
- package/dist/esm/{auth-api → server/src/auth-api}/compat-auth-storage.js +3 -3
- package/dist/esm/server/src/auth-api/schemas.d.ts +21 -0
- package/dist/esm/server/src/auth-api/schemas.js +168 -0
- package/dist/esm/{auth-api → server/src/auth-api}/user-id.js +12 -3
- package/dist/{cjs → esm/server/src}/auth-cookie-options.d.ts +5 -3
- package/dist/esm/server/src/base/client-info.d.ts +27 -0
- package/dist/esm/server/src/base/client-info.js +282 -0
- package/dist/esm/server/src/base/error-utils.d.ts +16 -0
- package/dist/esm/server/src/base/error-utils.js +44 -0
- package/dist/esm/server/src/base/request-utils.d.ts +8 -0
- package/dist/esm/server/src/base/request-utils.js +23 -0
- package/dist/{cjs → esm/server/src}/index.d.ts +7 -0
- package/dist/esm/{index.js → server/src/index.js} +4 -0
- package/dist/esm/server/src/limiter/auth-rate-limiter.d.ts +12 -0
- package/dist/esm/server/src/limiter/auth-rate-limiter.js +32 -0
- package/dist/esm/server/src/limiter/fixed-window.d.ts +11 -0
- package/dist/esm/server/src/limiter/fixed-window.js +37 -0
- package/dist/{cjs → esm/server/src}/oauth/base.d.ts +8 -1
- package/dist/esm/server/src/oauth/base.js +3 -0
- package/dist/{cjs → esm/server/src}/oauth/memory.d.ts +1 -1
- package/dist/esm/{oauth → server/src/oauth}/memory.js +5 -2
- package/dist/{cjs → esm/server/src}/oauth/sequelize.d.ts +1 -1
- package/dist/esm/{oauth → server/src/oauth}/sequelize.js +6 -2
- package/dist/{cjs → esm/server/src}/passkey/base.d.ts +11 -0
- package/dist/esm/server/src/passkey/base.js +3 -0
- package/dist/{cjs → esm/server/src}/passkey/service.d.ts +1 -1
- package/dist/esm/{passkey → server/src/passkey}/service.js +17 -3
- package/dist/esm/{sequelize-utils.js → server/src/sequelize-utils.js} +4 -5
- package/dist/{cjs → esm/server/src}/token/base.d.ts +7 -0
- package/dist/esm/{token → server/src/token}/base.js +4 -0
- package/dist/esm/{token → server/src/token}/memory.js +14 -19
- package/dist/esm/{token → server/src/token}/sequelize.js +22 -8
- package/dist/esm/server/src/upload/memory.d.ts +17 -0
- package/dist/esm/server/src/upload/memory.js +86 -0
- package/dist/esm/server/src/upload/tus-module.d.ts +38 -0
- package/dist/esm/server/src/upload/tus-module.js +266 -0
- package/dist/esm/server/src/upload/types.d.ts +8 -0
- package/dist/{cjs → esm/server/src}/user/base.d.ts +9 -0
- package/dist/esm/{user → server/src/user}/base.js +1 -0
- package/dist/esm/{user → server/src/user}/memory.js +27 -5
- package/dist/esm/{user → server/src/user}/sequelize.js +30 -5
- package/dist/esm/server/src/user/types.js +1 -0
- package/docs/swagger/openapi.json +1 -1
- package/package.json +18 -17
- package/README.txt +0 -216
- package/dist/esm/oauth/base.js +0 -2
- package/dist/esm/passkey/base.js +0 -2
- /package/dist/cjs/{auth-api → server/src/auth-api}/compat-auth-storage.d.ts +0 -0
- /package/dist/cjs/{auth-api → server/src/auth-api}/mem-auth-store.d.ts +0 -0
- /package/dist/cjs/{auth-api → server/src/auth-api}/module.d.ts +0 -0
- /package/dist/cjs/{auth-api → server/src/auth-api}/sql-auth-store.d.ts +0 -0
- /package/dist/cjs/{auth-api/storage.js → server/src/auth-api/storage.cjs} +0 -0
- /package/dist/cjs/{auth-api → server/src/auth-api}/storage.d.ts +0 -0
- /package/dist/cjs/{auth-api/types.js → server/src/auth-api/types.cjs} +0 -0
- /package/dist/cjs/{auth-api → server/src/auth-api}/types.d.ts +0 -0
- /package/dist/cjs/{auth-api → server/src/auth-api}/user-id.d.ts +0 -0
- /package/dist/cjs/{auth-cookie-options.js → server/src/auth-cookie-options.cjs} +0 -0
- /package/dist/cjs/{oauth → server/src/oauth}/models.d.ts +0 -0
- /package/dist/cjs/{oauth/types.js → server/src/oauth/types.cjs} +0 -0
- /package/dist/cjs/{oauth → server/src/oauth}/types.d.ts +0 -0
- /package/dist/cjs/{passkey/config.js → server/src/passkey/config.cjs} +0 -0
- /package/dist/cjs/{passkey → server/src/passkey}/config.d.ts +0 -0
- /package/dist/cjs/{passkey → server/src/passkey}/memory.d.ts +0 -0
- /package/dist/cjs/{passkey → server/src/passkey}/models.d.ts +0 -0
- /package/dist/cjs/{passkey → server/src/passkey}/sequelize.d.ts +0 -0
- /package/dist/cjs/{passkey/types.js → server/src/passkey/types.cjs} +0 -0
- /package/dist/cjs/{passkey → server/src/passkey}/types.d.ts +0 -0
- /package/dist/cjs/{sequelize-utils.d.ts → server/src/sequelize-utils.d.ts} +0 -0
- /package/dist/cjs/{token → server/src/token}/memory.d.ts +0 -0
- /package/dist/cjs/{token → server/src/token}/sequelize.d.ts +0 -0
- /package/dist/cjs/{token/types.js → server/src/token/types.cjs} +0 -0
- /package/dist/cjs/{token → server/src/token}/types.d.ts +0 -0
- /package/dist/cjs/{user/types.js → server/src/upload/types.cjs} +0 -0
- /package/dist/cjs/{user → server/src/user}/memory.d.ts +0 -0
- /package/dist/cjs/{user → server/src/user}/sequelize.d.ts +0 -0
- /package/dist/cjs/{user → server/src/user}/types.d.ts +0 -0
- /package/dist/esm/{auth-api → server/src/auth-api}/compat-auth-storage.d.ts +0 -0
- /package/dist/esm/{auth-api → server/src/auth-api}/mem-auth-store.d.ts +0 -0
- /package/dist/esm/{auth-api → server/src/auth-api}/mem-auth-store.js +0 -0
- /package/dist/esm/{auth-api → server/src/auth-api}/module.d.ts +0 -0
- /package/dist/esm/{auth-api → server/src/auth-api}/module.js +0 -0
- /package/dist/esm/{auth-api → server/src/auth-api}/sql-auth-store.d.ts +0 -0
- /package/dist/esm/{auth-api → server/src/auth-api}/sql-auth-store.js +0 -0
- /package/dist/esm/{auth-api → server/src/auth-api}/storage.d.ts +0 -0
- /package/dist/esm/{auth-api → server/src/auth-api}/storage.js +0 -0
- /package/dist/esm/{auth-api → server/src/auth-api}/types.d.ts +0 -0
- /package/dist/esm/{auth-api → server/src/auth-api}/types.js +0 -0
- /package/dist/esm/{auth-api → server/src/auth-api}/user-id.d.ts +0 -0
- /package/dist/esm/{auth-cookie-options.js → server/src/auth-cookie-options.js} +0 -0
- /package/dist/esm/{oauth → server/src/oauth}/models.d.ts +0 -0
- /package/dist/esm/{oauth → server/src/oauth}/models.js +0 -0
- /package/dist/esm/{oauth → server/src/oauth}/types.d.ts +0 -0
- /package/dist/esm/{oauth → server/src/oauth}/types.js +0 -0
- /package/dist/esm/{passkey → server/src/passkey}/config.d.ts +0 -0
- /package/dist/esm/{passkey → server/src/passkey}/config.js +0 -0
- /package/dist/esm/{passkey → server/src/passkey}/memory.d.ts +0 -0
- /package/dist/esm/{passkey → server/src/passkey}/memory.js +0 -0
- /package/dist/esm/{passkey → server/src/passkey}/models.d.ts +0 -0
- /package/dist/esm/{passkey → server/src/passkey}/models.js +0 -0
- /package/dist/esm/{passkey → server/src/passkey}/sequelize.d.ts +0 -0
- /package/dist/esm/{passkey → server/src/passkey}/sequelize.js +0 -0
- /package/dist/esm/{passkey → server/src/passkey}/types.d.ts +0 -0
- /package/dist/esm/{passkey → server/src/passkey}/types.js +0 -0
- /package/dist/esm/{sequelize-utils.d.ts → server/src/sequelize-utils.d.ts} +0 -0
- /package/dist/esm/{token → server/src/token}/memory.d.ts +0 -0
- /package/dist/esm/{token → server/src/token}/sequelize.d.ts +0 -0
- /package/dist/esm/{token → server/src/token}/types.d.ts +0 -0
- /package/dist/esm/{token → server/src/token}/types.js +0 -0
- /package/dist/esm/{user → server/src/upload}/types.js +0 -0
- /package/dist/esm/{user → server/src/user}/memory.d.ts +0 -0
- /package/dist/esm/{user → server/src/user}/sequelize.d.ts +0 -0
- /package/dist/esm/{user → server/src/user}/types.d.ts +0 -0
|
@@ -10,19 +10,53 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
12
|
exports.ApiServer = exports.ApiError = exports.ApiModule = void 0;
|
|
13
|
-
const node_crypto_1 = require("node:crypto");
|
|
14
13
|
const promises_1 = require("node:fs/promises");
|
|
15
14
|
const node_module_1 = require("node:module");
|
|
16
15
|
const node_path_1 = __importDefault(require("node:path"));
|
|
17
|
-
const
|
|
18
|
-
const cors_1 = __importDefault(require("cors"));
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
const
|
|
16
|
+
const cookie_1 = __importDefault(require("@fastify/cookie"));
|
|
17
|
+
const cors_1 = __importDefault(require("@fastify/cors"));
|
|
18
|
+
const multipart_1 = __importDefault(require("@fastify/multipart"));
|
|
19
|
+
const static_1 = __importDefault(require("@fastify/static"));
|
|
20
|
+
const fastify_1 = __importDefault(require("fastify"));
|
|
21
|
+
const module_js_1 = require("./auth-api/module.cjs");
|
|
22
|
+
const storage_js_1 = require("./auth-api/storage.cjs");
|
|
23
|
+
const user_id_js_1 = require("./auth-api/user-id.cjs");
|
|
24
|
+
const auth_cookie_options_js_1 = require("./auth-cookie-options.cjs");
|
|
25
|
+
const client_info_js_1 = require("./base/client-info.cjs");
|
|
26
|
+
const error_utils_js_1 = require("./base/error-utils.cjs");
|
|
27
|
+
const request_utils_js_1 = require("./base/request-utils.cjs");
|
|
28
|
+
const base_js_1 = require("./token/base.cjs");
|
|
29
|
+
class FastifyResponseAdapter {
|
|
30
|
+
constructor(reply) {
|
|
31
|
+
this.reply = reply;
|
|
32
|
+
this.locals = {};
|
|
33
|
+
this.statusCode = 200;
|
|
34
|
+
}
|
|
35
|
+
get headersSent() {
|
|
36
|
+
return this.reply.sent;
|
|
37
|
+
}
|
|
38
|
+
status(code) {
|
|
39
|
+
this.statusCode = code;
|
|
40
|
+
this.reply.code(code);
|
|
41
|
+
return this;
|
|
42
|
+
}
|
|
43
|
+
json(payload) {
|
|
44
|
+
if (!this.reply.sent) {
|
|
45
|
+
this.reply.code(this.statusCode).send(payload);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
send(payload) {
|
|
49
|
+
if (!this.reply.sent) {
|
|
50
|
+
this.reply.code(this.statusCode).send(payload);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
cookie(name, value, options = {}) {
|
|
54
|
+
this.reply.setCookie(name, value, options);
|
|
55
|
+
}
|
|
56
|
+
clearCookie(name, options = {}) {
|
|
57
|
+
this.reply.clearCookie(name, options);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
26
60
|
class JwtHelperStore extends base_js_1.TokenStore {
|
|
27
61
|
async save() {
|
|
28
62
|
throw new Error('Token store is not configured');
|
|
@@ -45,288 +79,9 @@ class JwtHelperStore extends base_js_1.TokenStore {
|
|
|
45
79
|
}
|
|
46
80
|
var api_module_js_1 = require("./api-module.cjs");
|
|
47
81
|
Object.defineProperty(exports, "ApiModule", { enumerable: true, get: function () { return api_module_js_1.ApiModule; } });
|
|
48
|
-
function guess_exception_text(error, defMsg = 'Unknown Error') {
|
|
49
|
-
const msg = [];
|
|
50
|
-
if (typeof error === 'string' && error.trim() !== '') {
|
|
51
|
-
msg.push(error);
|
|
52
|
-
}
|
|
53
|
-
else if (error && typeof error === 'object') {
|
|
54
|
-
const errorDetails = error;
|
|
55
|
-
if (typeof errorDetails.message === 'string' && errorDetails.message.trim() !== '') {
|
|
56
|
-
msg.push(errorDetails.message);
|
|
57
|
-
}
|
|
58
|
-
if (errorDetails.parent &&
|
|
59
|
-
typeof errorDetails.parent.message === 'string' &&
|
|
60
|
-
errorDetails.parent.message.trim() !== '') {
|
|
61
|
-
msg.push(errorDetails.parent.message);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
return msg.length > 0 ? msg.join(' / ') : defMsg;
|
|
65
|
-
}
|
|
66
|
-
function isPlainObject(value) {
|
|
67
|
-
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
68
|
-
}
|
|
69
|
-
function hydrateGetBody(req) {
|
|
70
|
-
if ((req.method ?? '').toUpperCase() !== 'GET') {
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
const query = isPlainObject(req.query) ? req.query : null;
|
|
74
|
-
if (!query || Object.keys(query).length === 0) {
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
const body = isPlainObject(req.body) ? req.body : null;
|
|
78
|
-
if (!body || Object.keys(body).length === 0) {
|
|
79
|
-
req.body = { ...query };
|
|
80
|
-
return;
|
|
81
|
-
}
|
|
82
|
-
// Keep explicit body fields authoritative when both query and body provide the same key.
|
|
83
|
-
req.body = { ...query, ...body };
|
|
84
|
-
}
|
|
85
|
-
function normalizeIpAddress(candidate) {
|
|
86
|
-
let value = candidate.trim();
|
|
87
|
-
if (!value) {
|
|
88
|
-
return null;
|
|
89
|
-
}
|
|
90
|
-
value = value.replace(/^"+|"+$/g, '').replace(/^'+|'+$/g, '');
|
|
91
|
-
if (value.startsWith('::ffff:')) {
|
|
92
|
-
value = value.slice(7);
|
|
93
|
-
}
|
|
94
|
-
if (value.startsWith('[') && value.endsWith(']')) {
|
|
95
|
-
value = value.slice(1, -1);
|
|
96
|
-
}
|
|
97
|
-
const firstColon = value.indexOf(':');
|
|
98
|
-
const lastColon = value.lastIndexOf(':');
|
|
99
|
-
if (firstColon !== -1 && firstColon === lastColon) {
|
|
100
|
-
const maybePort = value.slice(lastColon + 1);
|
|
101
|
-
if (/^\d+$/.test(maybePort)) {
|
|
102
|
-
value = value.slice(0, lastColon);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
value = value.trim();
|
|
106
|
-
return value || null;
|
|
107
|
-
}
|
|
108
|
-
function extractForwardedFor(header) {
|
|
109
|
-
if (!header) {
|
|
110
|
-
return [];
|
|
111
|
-
}
|
|
112
|
-
const values = Array.isArray(header) ? header : [header];
|
|
113
|
-
const ips = [];
|
|
114
|
-
for (const entry of values) {
|
|
115
|
-
for (const part of entry.split(',')) {
|
|
116
|
-
const normalized = normalizeIpAddress(part);
|
|
117
|
-
if (normalized) {
|
|
118
|
-
ips.push(normalized);
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
return ips;
|
|
123
|
-
}
|
|
124
|
-
function extractForwardedHeader(header) {
|
|
125
|
-
if (!header) {
|
|
126
|
-
return [];
|
|
127
|
-
}
|
|
128
|
-
const values = Array.isArray(header) ? header : [header];
|
|
129
|
-
const ips = [];
|
|
130
|
-
for (const entry of values) {
|
|
131
|
-
for (const part of entry.split(',')) {
|
|
132
|
-
const match = part.match(/for=([^;]+)/i);
|
|
133
|
-
if (match) {
|
|
134
|
-
const normalized = normalizeIpAddress(match[1]);
|
|
135
|
-
if (normalized) {
|
|
136
|
-
ips.push(normalized);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
return ips;
|
|
142
|
-
}
|
|
143
|
-
function detectBrowser(userAgent) {
|
|
144
|
-
const browserMatchers = [
|
|
145
|
-
{ label: 'Edge', pattern: /(Edg|Edge|EdgiOS|EdgA)\/([\d.]+)/i, versionGroup: 2 },
|
|
146
|
-
{ label: 'Chrome', pattern: /(Chrome|CriOS)\/([\d.]+)/i, versionGroup: 2 },
|
|
147
|
-
{ label: 'Firefox', pattern: /(Firefox|FxiOS)\/([\d.]+)/i, versionGroup: 2 },
|
|
148
|
-
{ label: 'Safari', pattern: /Version\/([\d.]+).*Safari/i, versionGroup: 1 },
|
|
149
|
-
{ label: 'Opera', pattern: /(OPR|Opera)\/([\d.]+)/i, versionGroup: 2 },
|
|
150
|
-
{ label: 'Brave', pattern: /Brave\/([\d.]+)/i, versionGroup: 1 },
|
|
151
|
-
{ label: 'Vivaldi', pattern: /Vivaldi\/([\d.]+)/i, versionGroup: 1 },
|
|
152
|
-
{ label: 'Electron', pattern: /Electron\/([\d.]+)/i, versionGroup: 1 },
|
|
153
|
-
{ label: 'Node', pattern: /Node\.js\/([\d.]+)/i, versionGroup: 1 },
|
|
154
|
-
{ label: 'IE', pattern: /MSIE ([\d.]+)/i, versionGroup: 1 },
|
|
155
|
-
{ label: 'IE', pattern: /Trident\/.*rv:([\d.]+)/i, versionGroup: 1 }
|
|
156
|
-
];
|
|
157
|
-
for (const matcher of browserMatchers) {
|
|
158
|
-
const m = userAgent.match(matcher.pattern);
|
|
159
|
-
if (m) {
|
|
160
|
-
const version = matcher.versionGroup ? m[matcher.versionGroup] : '';
|
|
161
|
-
return version ? `${matcher.label} ${version}` : matcher.label;
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
return '';
|
|
165
|
-
}
|
|
166
|
-
function detectOs(userAgent) {
|
|
167
|
-
const osMatchers = [
|
|
168
|
-
{
|
|
169
|
-
label: 'Windows',
|
|
170
|
-
pattern: /Windows NT ([\d.]+)/i,
|
|
171
|
-
transform: (match) => `Windows ${match[1]}`
|
|
172
|
-
},
|
|
173
|
-
{
|
|
174
|
-
label: 'iOS',
|
|
175
|
-
pattern: /iPhone OS ([\d_]+)/i,
|
|
176
|
-
transform: (match) => `iOS ${match[1].replace(/_/g, '.')}`
|
|
177
|
-
},
|
|
178
|
-
{
|
|
179
|
-
label: 'iPadOS',
|
|
180
|
-
pattern: /iPad; CPU OS ([\d_]+)/i,
|
|
181
|
-
transform: (match) => `iPadOS ${match[1].replace(/_/g, '.')}`
|
|
182
|
-
},
|
|
183
|
-
{
|
|
184
|
-
label: 'macOS',
|
|
185
|
-
pattern: /Mac OS X ([\d_]+)/i,
|
|
186
|
-
transform: (match) => `macOS ${match[1].replace(/_/g, '.')}`
|
|
187
|
-
},
|
|
188
|
-
{
|
|
189
|
-
label: 'Android',
|
|
190
|
-
pattern: /Android ([\d.]+)/i,
|
|
191
|
-
transform: (match) => `Android ${match[1]}`
|
|
192
|
-
},
|
|
193
|
-
{
|
|
194
|
-
label: 'ChromeOS',
|
|
195
|
-
pattern: /CrOS [^ ]+ ([\d.]+)/i,
|
|
196
|
-
transform: (match) => `ChromeOS ${match[1]}`
|
|
197
|
-
},
|
|
198
|
-
{ label: 'Linux', pattern: /Linux/i },
|
|
199
|
-
{ label: 'Unix', pattern: /X11/i }
|
|
200
|
-
];
|
|
201
|
-
for (const matcher of osMatchers) {
|
|
202
|
-
const m = userAgent.match(matcher.pattern);
|
|
203
|
-
if (m) {
|
|
204
|
-
return matcher.transform ? matcher.transform(m) : matcher.label;
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
return '';
|
|
208
|
-
}
|
|
209
|
-
function detectDevice(userAgent, osLabel) {
|
|
210
|
-
if (/iPhone/i.test(userAgent)) {
|
|
211
|
-
return 'iPhone';
|
|
212
|
-
}
|
|
213
|
-
if (/iPad/i.test(userAgent)) {
|
|
214
|
-
return 'iPad';
|
|
215
|
-
}
|
|
216
|
-
if (/iPod/i.test(userAgent)) {
|
|
217
|
-
return 'iPod';
|
|
218
|
-
}
|
|
219
|
-
if (/Android/i.test(userAgent)) {
|
|
220
|
-
const match = userAgent.match(/;\s*([^;]+)\s+Build/i);
|
|
221
|
-
if (match) {
|
|
222
|
-
return match[1];
|
|
223
|
-
}
|
|
224
|
-
return 'Android Device';
|
|
225
|
-
}
|
|
226
|
-
if (/Macintosh/i.test(userAgent)) {
|
|
227
|
-
return 'Mac';
|
|
228
|
-
}
|
|
229
|
-
if (/Windows/i.test(userAgent)) {
|
|
230
|
-
return 'PC';
|
|
231
|
-
}
|
|
232
|
-
if (/CrOS/i.test(userAgent)) {
|
|
233
|
-
return 'Chromebook';
|
|
234
|
-
}
|
|
235
|
-
return osLabel;
|
|
236
|
-
}
|
|
237
|
-
function parseClientAgent(userAgentHeader) {
|
|
238
|
-
const raw = Array.isArray(userAgentHeader) ? userAgentHeader[0] : userAgentHeader;
|
|
239
|
-
const ua = typeof raw === 'string' ? raw.trim() : '';
|
|
240
|
-
if (!ua) {
|
|
241
|
-
return { ua: '', browser: '', os: '', device: '' };
|
|
242
|
-
}
|
|
243
|
-
const os = detectOs(ua);
|
|
244
|
-
const browser = detectBrowser(ua);
|
|
245
|
-
const device = detectDevice(ua, os);
|
|
246
|
-
return { ua, browser, os, device };
|
|
247
|
-
}
|
|
248
|
-
function isLoopbackAddress(ip) {
|
|
249
|
-
if (ip === '::1' || ip === '0:0:0:0:0:0:0:1') {
|
|
250
|
-
return true;
|
|
251
|
-
}
|
|
252
|
-
if (ip === '0.0.0.0' || ip === '127.0.0.1') {
|
|
253
|
-
return true;
|
|
254
|
-
}
|
|
255
|
-
if (ip.startsWith('127.')) {
|
|
256
|
-
return true;
|
|
257
|
-
}
|
|
258
|
-
return false;
|
|
259
|
-
}
|
|
260
|
-
function collectClientIpChain(req) {
|
|
261
|
-
const seen = new Set();
|
|
262
|
-
const result = [];
|
|
263
|
-
const pushNormalized = (ip) => {
|
|
264
|
-
if (!ip || seen.has(ip)) {
|
|
265
|
-
return;
|
|
266
|
-
}
|
|
267
|
-
seen.add(ip);
|
|
268
|
-
result.push(ip);
|
|
269
|
-
};
|
|
270
|
-
for (const ip of extractForwardedFor(req.headers['x-forwarded-for'])) {
|
|
271
|
-
pushNormalized(ip);
|
|
272
|
-
}
|
|
273
|
-
for (const ip of extractForwardedHeader(req.headers['forwarded'])) {
|
|
274
|
-
pushNormalized(ip);
|
|
275
|
-
}
|
|
276
|
-
const realIp = req.headers['x-real-ip'];
|
|
277
|
-
if (Array.isArray(realIp)) {
|
|
278
|
-
for (const value of realIp) {
|
|
279
|
-
pushNormalized(normalizeIpAddress(value));
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
else if (typeof realIp === 'string') {
|
|
283
|
-
pushNormalized(normalizeIpAddress(realIp));
|
|
284
|
-
}
|
|
285
|
-
if (Array.isArray(req.ips)) {
|
|
286
|
-
for (const ip of req.ips) {
|
|
287
|
-
pushNormalized(normalizeIpAddress(ip));
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
if (typeof req.ip === 'string') {
|
|
291
|
-
pushNormalized(normalizeIpAddress(req.ip));
|
|
292
|
-
}
|
|
293
|
-
const socketAddress = req.socket?.remoteAddress;
|
|
294
|
-
if (typeof socketAddress === 'string') {
|
|
295
|
-
pushNormalized(normalizeIpAddress(socketAddress));
|
|
296
|
-
}
|
|
297
|
-
const connectionAddress = req.connection?.remoteAddress;
|
|
298
|
-
if (typeof connectionAddress === 'string') {
|
|
299
|
-
pushNormalized(normalizeIpAddress(connectionAddress));
|
|
300
|
-
}
|
|
301
|
-
return result;
|
|
302
|
-
}
|
|
303
|
-
function selectClientIp(chain) {
|
|
304
|
-
for (const ip of chain) {
|
|
305
|
-
if (!isLoopbackAddress(ip)) {
|
|
306
|
-
return ip;
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
return chain[0] ?? null;
|
|
310
|
-
}
|
|
311
|
-
function buildClientInfo(req) {
|
|
312
|
-
const agent = parseClientAgent(req.headers['user-agent']);
|
|
313
|
-
const ipchain = collectClientIpChain(req);
|
|
314
|
-
const ip = selectClientIp(ipchain);
|
|
315
|
-
return {
|
|
316
|
-
...agent,
|
|
317
|
-
ip,
|
|
318
|
-
ipchain
|
|
319
|
-
};
|
|
320
|
-
}
|
|
321
|
-
function ensureClientInfo(apiReq) {
|
|
322
|
-
if (!apiReq.clientInfo) {
|
|
323
|
-
apiReq.clientInfo = buildClientInfo(apiReq.req);
|
|
324
|
-
}
|
|
325
|
-
return apiReq.clientInfo;
|
|
326
|
-
}
|
|
327
82
|
class ApiError extends Error {
|
|
328
83
|
constructor({ code, message, data, errors }) {
|
|
329
|
-
const msg =
|
|
84
|
+
const msg = (0, error_utils_js_1.guessExceptionText)(message, '[Unknown error (null/undefined)]');
|
|
330
85
|
super(msg);
|
|
331
86
|
this.message = msg;
|
|
332
87
|
this.code = typeof code === 'number' ? code : 500;
|
|
@@ -335,24 +90,6 @@ class ApiError extends Error {
|
|
|
335
90
|
}
|
|
336
91
|
}
|
|
337
92
|
exports.ApiError = ApiError;
|
|
338
|
-
function isApiErrorLike(candidate) {
|
|
339
|
-
if (!candidate || typeof candidate !== 'object') {
|
|
340
|
-
return false;
|
|
341
|
-
}
|
|
342
|
-
const maybeError = candidate;
|
|
343
|
-
return typeof maybeError.code === 'number' && typeof maybeError.message === 'string';
|
|
344
|
-
}
|
|
345
|
-
function asHttpStatus(error) {
|
|
346
|
-
if (!error || typeof error !== 'object') {
|
|
347
|
-
return null;
|
|
348
|
-
}
|
|
349
|
-
const maybe = error;
|
|
350
|
-
const status = typeof maybe.status === 'number' ? maybe.status : maybe.statusCode;
|
|
351
|
-
if (typeof status === 'number' && status >= 400 && status <= 599) {
|
|
352
|
-
return status;
|
|
353
|
-
}
|
|
354
|
-
return null;
|
|
355
|
-
}
|
|
356
93
|
function fillConfig(config) {
|
|
357
94
|
return {
|
|
358
95
|
apiPort: config.apiPort ?? 3101,
|
|
@@ -389,36 +126,27 @@ function fillConfig(config) {
|
|
|
389
126
|
apiVersion: config.apiVersion ?? '',
|
|
390
127
|
minClientVersion: config.minClientVersion ?? '',
|
|
391
128
|
tokenStore: config.tokenStore,
|
|
392
|
-
|
|
393
|
-
|
|
129
|
+
onStartError: config.onStartError,
|
|
130
|
+
trustProxy: config.trustProxy ?? true
|
|
394
131
|
};
|
|
395
132
|
}
|
|
133
|
+
/** Core Fastify-based API server with module mounting and auth integration hooks. */
|
|
396
134
|
class ApiServer {
|
|
397
|
-
/**
|
|
398
|
-
* @deprecated ApiServer does not track a global "current request". This value is always null.
|
|
399
|
-
* Use the per-request ApiRequest passed to handlers, or `req.apiReq` / `res.locals.apiReq`
|
|
400
|
-
* when mounting raw Express endpoints.
|
|
401
|
-
*/
|
|
402
135
|
get currReq() {
|
|
403
136
|
return null;
|
|
404
137
|
}
|
|
405
138
|
set currReq(_value) {
|
|
406
139
|
if (this.config.devMode && !this.currReqDeprecationWarned) {
|
|
407
140
|
this.currReqDeprecationWarned = true;
|
|
408
|
-
console.warn('[api-server-base] ApiServer.currReq is deprecated and always null. Use
|
|
141
|
+
console.warn('[api-server-base] ApiServer.currReq is deprecated and always null. Use per-request ApiRequest in handlers.');
|
|
409
142
|
}
|
|
410
143
|
void _value;
|
|
411
144
|
}
|
|
412
145
|
constructor(config = {}) {
|
|
413
146
|
this.finalized = false;
|
|
414
|
-
this.serverAuthAdapter = null;
|
|
415
|
-
this.apiNotFoundHandler = null;
|
|
416
147
|
this.apiErrorHandlerInstalled = false;
|
|
417
148
|
this.tokenStoreAdapter = null;
|
|
418
|
-
this.
|
|
419
|
-
this.passkeyServiceAdapter = null;
|
|
420
|
-
this.oauthStoreAdapter = null;
|
|
421
|
-
this.canImpersonateAdapter = null;
|
|
149
|
+
this.compatGlobalErrorHandler = null;
|
|
422
150
|
this.currReqDeprecationWarned = false;
|
|
423
151
|
this.config = fillConfig(config);
|
|
424
152
|
this.apiBasePath = this.normalizeApiBasePath(this.config.apiBasePath);
|
|
@@ -427,100 +155,153 @@ class ApiServer {
|
|
|
427
155
|
this.moduleAdapter = module_js_1.nullAuthModule;
|
|
428
156
|
this.jwtHelper = new JwtHelperStore();
|
|
429
157
|
this.tokenStoreAdapter = this.config.tokenStore ?? null;
|
|
430
|
-
if (this.config.
|
|
431
|
-
const { userStore, tokenStore, passkeyService, oauthStore, canImpersonate } = this.config.authStores;
|
|
432
|
-
this.userStoreAdapter = userStore;
|
|
433
|
-
this.tokenStoreAdapter = tokenStore;
|
|
434
|
-
this.passkeyServiceAdapter = passkeyService ?? null;
|
|
435
|
-
this.oauthStoreAdapter = oauthStore ?? null;
|
|
436
|
-
this.canImpersonateAdapter = canImpersonate ?? null;
|
|
437
|
-
this.storageAdapter = this.getServerAuthAdapter();
|
|
438
|
-
}
|
|
439
|
-
if ((this.config.authApi || this.config.authStores) &&
|
|
440
|
-
(!this.config.accessSecret || !this.config.refreshSecret)) {
|
|
158
|
+
if (this.config.authApi && (!this.config.accessSecret || !this.config.refreshSecret)) {
|
|
441
159
|
console.warn('[api-server-base] Auth is enabled but accessSecret and/or refreshSecret are empty. JWT signing will fail at runtime.');
|
|
442
160
|
}
|
|
443
|
-
this.
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
const
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
161
|
+
this.fastify = (0, fastify_1.default)({ logger: false, ajv: { customOptions: { allErrors: true, allowUnionTypes: true } } });
|
|
162
|
+
this.setupRuntime();
|
|
163
|
+
this.readyPromise = Promise.resolve(this.fastify.ready()).then(() => undefined);
|
|
164
|
+
const appHandler = (req, res) => {
|
|
165
|
+
void this.readyPromise
|
|
166
|
+
.then(() => {
|
|
167
|
+
this.fastify.routing(req, res);
|
|
168
|
+
})
|
|
169
|
+
.catch((error) => {
|
|
170
|
+
const message = this.internalServerErrorMessage(error);
|
|
171
|
+
res.statusCode = 500;
|
|
172
|
+
res.setHeader('content-type', 'application/json');
|
|
173
|
+
res.end(JSON.stringify({ success: false, code: 500, message, data: null, errors: {} }));
|
|
174
|
+
});
|
|
175
|
+
};
|
|
176
|
+
appHandler.listen = (...args) => {
|
|
177
|
+
const server = this.fastify.server;
|
|
178
|
+
void this.readyPromise.then(() => {
|
|
179
|
+
if (!server.listening) {
|
|
180
|
+
server.listen(...args);
|
|
462
181
|
}
|
|
463
|
-
next(err);
|
|
464
182
|
});
|
|
465
|
-
|
|
466
|
-
|
|
183
|
+
return server;
|
|
184
|
+
};
|
|
185
|
+
this.app = appHandler;
|
|
186
|
+
}
|
|
187
|
+
setupRuntime() {
|
|
188
|
+
this.fastify.register(cookie_1.default);
|
|
189
|
+
this.fastify.register(cors_1.default, {
|
|
190
|
+
origin: (origin, callback) => {
|
|
191
|
+
// No Origin header means a non-browser client (curl, server-to-server).
|
|
192
|
+
// CORS is a browser security feature; non-browser clients can bypass it
|
|
193
|
+
// by simply omitting the header, so blocking them here adds no security.
|
|
194
|
+
if (!origin) {
|
|
195
|
+
callback(null, true);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
// NOTE: An empty origins list means "allow all origins" (open/dev mode).
|
|
199
|
+
// Configure a non-empty origins list in production to restrict access.
|
|
200
|
+
if (this.config.origins.length > 0 && !this.config.origins.includes(origin)) {
|
|
201
|
+
callback(new Error('Not allowed by CORS'), false);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
callback(null, true);
|
|
205
|
+
},
|
|
206
|
+
credentials: true
|
|
207
|
+
});
|
|
208
|
+
this.fastify.register(multipart_1.default, {
|
|
209
|
+
limits: { fileSize: this.config.uploadMax }
|
|
210
|
+
});
|
|
211
|
+
this.fastify.addHook('preValidation', async (request) => {
|
|
212
|
+
if (request.method === 'GET' &&
|
|
213
|
+
request.body === undefined &&
|
|
214
|
+
typeof request.headers['content-length'] === 'string' &&
|
|
215
|
+
Number.parseInt(request.headers['content-length'], 10) > 0) {
|
|
216
|
+
const rawBody = await this.readRawBody(request.raw);
|
|
217
|
+
if (rawBody) {
|
|
218
|
+
const contentType = (request.headers['content-type'] ?? '').toString().toLowerCase();
|
|
219
|
+
request.body = this.parseRawBody(rawBody, contentType);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (!request.isMultipart()) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
const files = [];
|
|
226
|
+
const body = {};
|
|
227
|
+
for await (const part of request.parts()) {
|
|
228
|
+
if (part.type === 'file') {
|
|
229
|
+
const buffer = await part.toBuffer();
|
|
230
|
+
files.push({
|
|
231
|
+
fieldname: part.fieldname,
|
|
232
|
+
originalname: part.filename,
|
|
233
|
+
encoding: part.encoding,
|
|
234
|
+
mimetype: part.mimetype,
|
|
235
|
+
size: buffer.length,
|
|
236
|
+
buffer
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
body[part.fieldname] = part.value;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
request.__apiParsedBody = body;
|
|
244
|
+
request.__apiParsedFiles = files;
|
|
245
|
+
});
|
|
467
246
|
this.installStaticDirs();
|
|
468
247
|
this.installPingHandler();
|
|
469
248
|
this.installSwaggerHandler();
|
|
470
|
-
this.app.use(this.apiBasePath, this.apiRouter);
|
|
471
|
-
// addSwaggerUi(this.app);
|
|
472
249
|
this.installApiNotFoundHandler();
|
|
473
250
|
this.installApiErrorHandler();
|
|
474
251
|
}
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
252
|
+
async readRawBody(req, maxBytes = 1048576) {
|
|
253
|
+
const chunks = [];
|
|
254
|
+
let total = 0;
|
|
255
|
+
for await (const chunk of req) {
|
|
256
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));
|
|
257
|
+
total += buf.length;
|
|
258
|
+
if (total > maxBytes) {
|
|
259
|
+
throw new ApiError({ code: 413, message: `Request body exceeds maximum size of ${maxBytes} bytes` });
|
|
260
|
+
}
|
|
261
|
+
chunks.push(buf);
|
|
478
262
|
}
|
|
263
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
479
264
|
}
|
|
480
|
-
|
|
481
|
-
if (
|
|
482
|
-
return
|
|
265
|
+
parseRawBody(raw, contentType) {
|
|
266
|
+
if (!raw) {
|
|
267
|
+
return {};
|
|
483
268
|
}
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
return normalized;
|
|
269
|
+
if (contentType.includes('application/json')) {
|
|
270
|
+
try {
|
|
271
|
+
return JSON.parse(raw);
|
|
272
|
+
}
|
|
273
|
+
catch {
|
|
274
|
+
return {};
|
|
275
|
+
}
|
|
492
276
|
}
|
|
493
|
-
if (
|
|
494
|
-
|
|
277
|
+
if (contentType.includes('application/x-www-form-urlencoded')) {
|
|
278
|
+
const params = new URLSearchParams(raw);
|
|
279
|
+
return Object.fromEntries(params.entries());
|
|
495
280
|
}
|
|
496
|
-
|
|
497
|
-
|
|
281
|
+
return raw;
|
|
282
|
+
}
|
|
283
|
+
assertNotFinalized(action) {
|
|
284
|
+
if (this.finalized) {
|
|
285
|
+
throw new Error(`Cannot call ApiServer.${action}() after finalize()/start()`);
|
|
498
286
|
}
|
|
499
|
-
return null;
|
|
500
287
|
}
|
|
501
288
|
finalize() {
|
|
502
|
-
this.installApiNotFoundHandler();
|
|
503
|
-
this.installApiErrorHandler();
|
|
504
289
|
this.finalized = true;
|
|
505
290
|
return this;
|
|
506
291
|
}
|
|
507
292
|
authStorage(storage) {
|
|
293
|
+
this.assertNotFinalized('authStorage');
|
|
508
294
|
this.storageAdapter = storage;
|
|
509
295
|
return this;
|
|
510
296
|
}
|
|
511
|
-
/**
|
|
512
|
-
* @deprecated Use {@link ApiServer.authStorage} instead.
|
|
513
|
-
*/
|
|
514
297
|
useAuthStorage(storage) {
|
|
515
298
|
return this.authStorage(storage);
|
|
516
299
|
}
|
|
517
300
|
authModule(module) {
|
|
301
|
+
this.assertNotFinalized('authModule');
|
|
518
302
|
this.moduleAdapter = module;
|
|
519
303
|
return this;
|
|
520
304
|
}
|
|
521
|
-
/**
|
|
522
|
-
* @deprecated Use {@link ApiServer.authModule} instead.
|
|
523
|
-
*/
|
|
524
305
|
useAuthModule(module) {
|
|
525
306
|
return this.authModule(module);
|
|
526
307
|
}
|
|
@@ -531,88 +312,39 @@ class ApiServer {
|
|
|
531
312
|
return this.moduleAdapter;
|
|
532
313
|
}
|
|
533
314
|
setTokenStore(store) {
|
|
315
|
+
this.assertNotFinalized('setTokenStore');
|
|
534
316
|
this.tokenStoreAdapter = store;
|
|
535
|
-
// If using direct stores, expose the server-backed auth adapter.
|
|
536
|
-
if (this.userStoreAdapter) {
|
|
537
|
-
this.storageAdapter = this.getServerAuthAdapter();
|
|
538
|
-
}
|
|
539
317
|
return this;
|
|
540
318
|
}
|
|
541
319
|
getTokenStore() {
|
|
542
320
|
return this.tokenStoreAdapter;
|
|
543
321
|
}
|
|
544
|
-
|
|
545
|
-
if (
|
|
546
|
-
throw new Error('User store is not configured');
|
|
547
|
-
}
|
|
548
|
-
return this.userStoreAdapter;
|
|
549
|
-
}
|
|
550
|
-
ensureTokenStore() {
|
|
551
|
-
if (!this.tokenStoreAdapter) {
|
|
552
|
-
throw new Error('Token store is not configured');
|
|
553
|
-
}
|
|
554
|
-
return this.tokenStoreAdapter;
|
|
555
|
-
}
|
|
556
|
-
ensurePasskeyService() {
|
|
557
|
-
if (!this.passkeyServiceAdapter) {
|
|
322
|
+
async listUserCredentials(userId) {
|
|
323
|
+
if (typeof this.storageAdapter.listUserCredentials !== 'function') {
|
|
558
324
|
throw new Error('Passkey service is not configured');
|
|
559
325
|
}
|
|
560
|
-
return this.
|
|
561
|
-
}
|
|
562
|
-
async listUserCredentials(userId) {
|
|
563
|
-
return this.ensurePasskeyService().listUserCredentials(userId);
|
|
326
|
+
return this.storageAdapter.listUserCredentials(userId);
|
|
564
327
|
}
|
|
565
328
|
async deletePasskeyCredential(credentialId) {
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
ensureOAuthStore() {
|
|
569
|
-
if (!this.oauthStoreAdapter) {
|
|
570
|
-
throw new Error('OAuth store is not configured');
|
|
329
|
+
if (typeof this.storageAdapter.deletePasskeyCredential !== 'function') {
|
|
330
|
+
throw new Error('Passkey service is not configured');
|
|
571
331
|
}
|
|
572
|
-
return this.
|
|
573
|
-
}
|
|
574
|
-
getServerAuthAdapter() {
|
|
575
|
-
if (this.serverAuthAdapter) {
|
|
576
|
-
return this.serverAuthAdapter;
|
|
577
|
-
}
|
|
578
|
-
const server = this;
|
|
579
|
-
this.serverAuthAdapter = {
|
|
580
|
-
getUser: (identifier) => server.getUser(identifier),
|
|
581
|
-
getUserPasswordHash: (user) => server.getUserPasswordHash(user),
|
|
582
|
-
getUserId: (user) => server.getUserId(user),
|
|
583
|
-
filterUser: (user) => server.filterUser(user),
|
|
584
|
-
verifyPassword: (password, hash) => server.verifyPassword(password, hash),
|
|
585
|
-
storeToken: (data) => server.storeToken(data),
|
|
586
|
-
getToken: (query, opts) => server.getToken(query, opts),
|
|
587
|
-
deleteToken: (query) => server.deleteToken(query),
|
|
588
|
-
updateToken: (updates) => server.updateToken(updates),
|
|
589
|
-
createPasskeyChallenge: (params) => server.createPasskeyChallenge(params),
|
|
590
|
-
verifyPasskeyResponse: (params) => server.verifyPasskeyResponse(params),
|
|
591
|
-
listUserCredentials: (userId) => server.listUserCredentials(userId),
|
|
592
|
-
deletePasskeyCredential: (credentialId) => server.deletePasskeyCredential(credentialId),
|
|
593
|
-
getClient: (clientId) => server.getClient(clientId),
|
|
594
|
-
verifyClientSecret: (client, clientSecret) => server.verifyClientSecret(client, clientSecret),
|
|
595
|
-
createAuthCode: (request) => server.createAuthCode(request),
|
|
596
|
-
consumeAuthCode: (code, clientId) => server.consumeAuthCode(code, clientId),
|
|
597
|
-
canImpersonate: (params) => server.canImpersonate(params)
|
|
598
|
-
};
|
|
599
|
-
return this.serverAuthAdapter;
|
|
332
|
+
return this.storageAdapter.deletePasskeyCredential(credentialId);
|
|
600
333
|
}
|
|
601
|
-
// AuthAdapter-compatible helpers (used by AuthModule)
|
|
602
334
|
async getUser(identifier) {
|
|
603
|
-
return this.
|
|
335
|
+
return this.storageAdapter.getUser(identifier);
|
|
604
336
|
}
|
|
605
337
|
getUserPasswordHash(user) {
|
|
606
|
-
return this.
|
|
338
|
+
return this.storageAdapter.getUserPasswordHash(user);
|
|
607
339
|
}
|
|
608
340
|
getUserId(user) {
|
|
609
|
-
return this.
|
|
341
|
+
return this.storageAdapter.getUserId(user);
|
|
610
342
|
}
|
|
611
343
|
filterUser(user) {
|
|
612
|
-
return this.
|
|
344
|
+
return this.storageAdapter.filterUser(user);
|
|
613
345
|
}
|
|
614
346
|
async verifyPassword(password, hash) {
|
|
615
|
-
return this.
|
|
347
|
+
return this.storageAdapter.verifyPassword(password, hash);
|
|
616
348
|
}
|
|
617
349
|
async storeToken(data) {
|
|
618
350
|
if (this.tokenStoreAdapter) {
|
|
@@ -655,47 +387,55 @@ class ApiServer {
|
|
|
655
387
|
return 0;
|
|
656
388
|
}
|
|
657
389
|
async createPasskeyChallenge(params) {
|
|
658
|
-
|
|
390
|
+
if (typeof this.storageAdapter.createPasskeyChallenge !== 'function') {
|
|
391
|
+
throw new Error('Passkey service is not configured');
|
|
392
|
+
}
|
|
393
|
+
return this.storageAdapter.createPasskeyChallenge(params);
|
|
659
394
|
}
|
|
660
395
|
async verifyPasskeyResponse(params) {
|
|
661
|
-
|
|
396
|
+
if (typeof this.storageAdapter.verifyPasskeyResponse !== 'function') {
|
|
397
|
+
throw new Error('Passkey service is not configured');
|
|
398
|
+
}
|
|
399
|
+
return this.storageAdapter.verifyPasskeyResponse(params);
|
|
662
400
|
}
|
|
663
401
|
async getClient(clientId) {
|
|
664
|
-
|
|
402
|
+
if (typeof this.storageAdapter.getClient !== 'function') {
|
|
403
|
+
return null;
|
|
404
|
+
}
|
|
405
|
+
return this.storageAdapter.getClient(clientId);
|
|
665
406
|
}
|
|
666
407
|
async verifyClientSecret(client, clientSecret) {
|
|
667
|
-
|
|
408
|
+
if (typeof this.storageAdapter.verifyClientSecret !== 'function') {
|
|
409
|
+
throw new Error('OAuth store is not configured');
|
|
410
|
+
}
|
|
411
|
+
return this.storageAdapter.verifyClientSecret(client, clientSecret);
|
|
668
412
|
}
|
|
669
413
|
async createAuthCode(request) {
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
return
|
|
674
|
-
code,
|
|
675
|
-
clientId: request.clientId,
|
|
676
|
-
userId: request.userId,
|
|
677
|
-
redirectUri: request.redirectUri,
|
|
678
|
-
scope: request.scope ?? [],
|
|
679
|
-
codeChallenge: request.codeChallenge,
|
|
680
|
-
codeChallengeMethod: request.codeChallengeMethod,
|
|
681
|
-
expiresAt,
|
|
682
|
-
metadata: request.metadata
|
|
683
|
-
};
|
|
414
|
+
if (typeof this.storageAdapter.createAuthCode !== 'function') {
|
|
415
|
+
throw new Error('OAuth store is not configured');
|
|
416
|
+
}
|
|
417
|
+
return this.storageAdapter.createAuthCode(request);
|
|
684
418
|
}
|
|
685
419
|
async consumeAuthCode(code, clientId) {
|
|
686
|
-
|
|
420
|
+
if (typeof this.storageAdapter.consumeAuthCode !== 'function') {
|
|
421
|
+
return null;
|
|
422
|
+
}
|
|
423
|
+
const consumed = await this.storageAdapter.consumeAuthCode(code, clientId);
|
|
687
424
|
if (!consumed || consumed.clientId !== clientId) {
|
|
688
425
|
return null;
|
|
689
426
|
}
|
|
690
427
|
return consumed;
|
|
691
428
|
}
|
|
692
429
|
async canImpersonate(params) {
|
|
693
|
-
if (this.
|
|
694
|
-
return !!(await this.
|
|
430
|
+
if (typeof this.storageAdapter.canImpersonate === 'function') {
|
|
431
|
+
return !!(await this.storageAdapter.canImpersonate(params));
|
|
695
432
|
}
|
|
696
433
|
return params.realUserId === params.effectiveUserId;
|
|
697
434
|
}
|
|
698
435
|
jwtSign(payload, secret, expiresInSeconds, options) {
|
|
436
|
+
if (!secret) {
|
|
437
|
+
return { success: false, error: 'JWT secret is not configured' };
|
|
438
|
+
}
|
|
699
439
|
return (this.tokenStoreAdapter ?? this.jwtHelper).jwtSign(payload, secret, expiresInSeconds, options);
|
|
700
440
|
}
|
|
701
441
|
jwtVerify(token, secret, options) {
|
|
@@ -706,6 +446,8 @@ class ApiServer {
|
|
|
706
446
|
}
|
|
707
447
|
async getApiKey(token) {
|
|
708
448
|
void token;
|
|
449
|
+
console.warn('[api-server-base] getApiKey() is not implemented. ' +
|
|
450
|
+
'Override getApiKey() in your ApiServer subclass to support API key authentication.');
|
|
709
451
|
return null;
|
|
710
452
|
}
|
|
711
453
|
async authenticateUser(params) {
|
|
@@ -730,55 +472,34 @@ class ApiServer {
|
|
|
730
472
|
return false;
|
|
731
473
|
}
|
|
732
474
|
guessExceptionText(error, defMsg = 'Unknown Error') {
|
|
733
|
-
return
|
|
475
|
+
return (0, error_utils_js_1.guessExceptionText)(error, defMsg);
|
|
734
476
|
}
|
|
735
477
|
async authorize(apiReq, requiredClass) {
|
|
736
478
|
void apiReq;
|
|
737
|
-
|
|
479
|
+
if (requiredClass && requiredClass !== 'any') {
|
|
480
|
+
console.warn(`[api-server-base] Route requires auth class "${requiredClass}" but the base authorize() is not overridden. ` +
|
|
481
|
+
'Override authorize() in your ApiServer subclass to enforce role/class requirements.');
|
|
482
|
+
}
|
|
738
483
|
}
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
},
|
|
755
|
-
credentials: true
|
|
756
|
-
};
|
|
757
|
-
this.app.use((0, cors_1.default)(corsOptions));
|
|
758
|
-
// Provide a consistent and non-500 response when requests are blocked by the CORS allowlist.
|
|
759
|
-
this.app.use((err, req, res, next) => {
|
|
760
|
-
const message = err instanceof Error ? err.message : '';
|
|
761
|
-
if (message.includes('Not allowed by CORS')) {
|
|
762
|
-
const isApiRequest = Boolean(req.originalUrl?.startsWith(this.apiBasePath));
|
|
763
|
-
if (isApiRequest) {
|
|
764
|
-
res.status(403).json({
|
|
765
|
-
success: false,
|
|
766
|
-
code: 403,
|
|
767
|
-
message: 'Origin not allowed by CORS',
|
|
768
|
-
data: null,
|
|
769
|
-
errors: {}
|
|
770
|
-
});
|
|
771
|
-
return;
|
|
772
|
-
}
|
|
773
|
-
res.status(403).send('Origin not allowed by CORS');
|
|
774
|
-
return;
|
|
775
|
-
}
|
|
776
|
-
next(err);
|
|
777
|
-
});
|
|
484
|
+
/**
|
|
485
|
+
* Authenticate and authorise an incoming Fastify request outside of the
|
|
486
|
+
* standard `defineRoutes` pipeline (e.g. TUS upload routes that need full
|
|
487
|
+
* control over their own response format).
|
|
488
|
+
*
|
|
489
|
+
* Throws `ApiError` on auth failure — callers should catch it and respond
|
|
490
|
+
* with the appropriate HTTP status code.
|
|
491
|
+
*/
|
|
492
|
+
async resolveRequest(request, reply, auth) {
|
|
493
|
+
const req = this.toExtendedReq(request);
|
|
494
|
+
const res = new FastifyResponseAdapter(reply);
|
|
495
|
+
const apiReq = this.createApiRequest(req, res);
|
|
496
|
+
apiReq.tokenData = await this.authenticate(apiReq, auth.type);
|
|
497
|
+
await this.authorize(apiReq, auth.req ?? 'any');
|
|
498
|
+
return apiReq;
|
|
778
499
|
}
|
|
779
500
|
installStaticDirs() {
|
|
780
501
|
const staticDirs = this.config.staticDirs;
|
|
781
|
-
if (!staticDirs || !isPlainObject(staticDirs)) {
|
|
502
|
+
if (!staticDirs || !(0, request_utils_js_1.isPlainObject)(staticDirs)) {
|
|
782
503
|
return;
|
|
783
504
|
}
|
|
784
505
|
for (const [mountRaw, dirRaw] of Object.entries(staticDirs)) {
|
|
@@ -788,12 +509,15 @@ class ApiServer {
|
|
|
788
509
|
continue;
|
|
789
510
|
}
|
|
790
511
|
const resolvedMount = mount.startsWith('/') ? mount : `/${mount}`;
|
|
791
|
-
this.
|
|
512
|
+
void this.fastify.register(static_1.default, {
|
|
513
|
+
root: dir,
|
|
514
|
+
prefix: resolvedMount.endsWith('/') ? resolvedMount : `${resolvedMount}/`,
|
|
515
|
+
decorateReply: false
|
|
516
|
+
});
|
|
792
517
|
}
|
|
793
518
|
}
|
|
794
519
|
installPingHandler() {
|
|
795
|
-
|
|
796
|
-
this.app.get(path, (_req, res) => {
|
|
520
|
+
this.fastify.get(`${this.apiBasePath}/v1/ping`, async () => {
|
|
797
521
|
const payload = {
|
|
798
522
|
success: true,
|
|
799
523
|
status: 'ok',
|
|
@@ -803,7 +527,7 @@ class ApiServer {
|
|
|
803
527
|
startedAt: this.startedAt,
|
|
804
528
|
timestamp: new Date().toISOString()
|
|
805
529
|
};
|
|
806
|
-
|
|
530
|
+
return { success: true, code: 200, message: 'Success', data: payload, errors: {} };
|
|
807
531
|
});
|
|
808
532
|
}
|
|
809
533
|
async loadSwaggerSpec() {
|
|
@@ -811,10 +535,11 @@ class ApiServer {
|
|
|
811
535
|
if (typeof __dirname === 'string') {
|
|
812
536
|
candidates.push(node_path_1.default.resolve(__dirname, '../../docs/swagger/openapi.json'));
|
|
813
537
|
}
|
|
538
|
+
let packageRoot;
|
|
814
539
|
try {
|
|
815
540
|
const require = (0, node_module_1.createRequire)(node_path_1.default.join(process.cwd(), 'package.json'));
|
|
816
541
|
const entry = require.resolve('@technomoron/api-server-base');
|
|
817
|
-
|
|
542
|
+
packageRoot = node_path_1.default.resolve(node_path_1.default.dirname(entry), '..', '..');
|
|
818
543
|
candidates.push(node_path_1.default.join(packageRoot, 'docs/swagger/openapi.json'));
|
|
819
544
|
}
|
|
820
545
|
catch {
|
|
@@ -829,7 +554,13 @@ class ApiServer {
|
|
|
829
554
|
}
|
|
830
555
|
try {
|
|
831
556
|
const raw = await (0, promises_1.readFile)(candidate, 'utf8');
|
|
832
|
-
|
|
557
|
+
const spec = JSON.parse(raw);
|
|
558
|
+
// Inject version from package.json at runtime
|
|
559
|
+
const version = await this.readPackageVersion(node_path_1.default.dirname(candidate));
|
|
560
|
+
if (version && spec.info && typeof spec.info === 'object') {
|
|
561
|
+
spec.info.version = version;
|
|
562
|
+
}
|
|
563
|
+
return spec;
|
|
833
564
|
}
|
|
834
565
|
catch {
|
|
835
566
|
return null;
|
|
@@ -837,6 +568,27 @@ class ApiServer {
|
|
|
837
568
|
}
|
|
838
569
|
return null;
|
|
839
570
|
}
|
|
571
|
+
async readPackageVersion(specDir) {
|
|
572
|
+
// Walk up from the spec directory looking for package.json
|
|
573
|
+
const candidates = [
|
|
574
|
+
node_path_1.default.resolve(specDir, '../../package.json'),
|
|
575
|
+
node_path_1.default.resolve(specDir, '../package.json'),
|
|
576
|
+
node_path_1.default.resolve(process.cwd(), 'package.json')
|
|
577
|
+
];
|
|
578
|
+
for (const candidate of candidates) {
|
|
579
|
+
try {
|
|
580
|
+
const raw = await (0, promises_1.readFile)(candidate, 'utf8');
|
|
581
|
+
const pkg = JSON.parse(raw);
|
|
582
|
+
if (typeof pkg.version === 'string') {
|
|
583
|
+
return pkg.version;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
catch {
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
return null;
|
|
591
|
+
}
|
|
840
592
|
installSwaggerHandler() {
|
|
841
593
|
const rawPath = typeof this.config.swaggerPath === 'string' ? this.config.swaggerPath.trim() : '';
|
|
842
594
|
const enabled = Boolean(this.config.swaggerEnabled) || rawPath.length > 0;
|
|
@@ -845,31 +597,35 @@ class ApiServer {
|
|
|
845
597
|
}
|
|
846
598
|
const base = this.apiBasePath === '/' ? '' : this.apiBasePath;
|
|
847
599
|
const resolved = rawPath.length > 0 ? rawPath : `${base}/swagger`;
|
|
848
|
-
const
|
|
600
|
+
const routePath = resolved.startsWith('/') ? resolved : `/${resolved}`;
|
|
849
601
|
let specPromise;
|
|
850
|
-
this.
|
|
602
|
+
this.fastify.get(routePath, async (_request, reply) => {
|
|
851
603
|
if (!specPromise) {
|
|
852
604
|
specPromise = this.loadSwaggerSpec();
|
|
853
605
|
}
|
|
854
606
|
const spec = await specPromise;
|
|
855
607
|
if (!spec) {
|
|
856
|
-
|
|
608
|
+
// Clear cached failure so next request retries
|
|
609
|
+
specPromise = undefined;
|
|
610
|
+
}
|
|
611
|
+
if (!spec) {
|
|
612
|
+
reply.code(500);
|
|
613
|
+
return {
|
|
857
614
|
success: false,
|
|
858
615
|
code: 500,
|
|
859
616
|
message: 'Swagger spec is unavailable',
|
|
860
617
|
data: null,
|
|
861
618
|
errors: {}
|
|
862
|
-
}
|
|
863
|
-
return;
|
|
619
|
+
};
|
|
864
620
|
}
|
|
865
|
-
|
|
621
|
+
return spec;
|
|
866
622
|
});
|
|
867
623
|
}
|
|
868
|
-
normalizeApiBasePath(
|
|
869
|
-
if (!
|
|
624
|
+
normalizeApiBasePath(routePath) {
|
|
625
|
+
if (!routePath || typeof routePath !== 'string') {
|
|
870
626
|
return '/api';
|
|
871
627
|
}
|
|
872
|
-
const trimmed =
|
|
628
|
+
const trimmed = routePath.trim();
|
|
873
629
|
if (!trimmed) {
|
|
874
630
|
return '/api';
|
|
875
631
|
}
|
|
@@ -880,47 +636,121 @@ class ApiServer {
|
|
|
880
636
|
return withLeadingSlash.replace(/\/+$/, '') || '/api';
|
|
881
637
|
}
|
|
882
638
|
installApiNotFoundHandler() {
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
639
|
+
this.fastify.setNotFoundHandler((request, reply) => {
|
|
640
|
+
const target = request.raw.url ?? request.url;
|
|
641
|
+
if (!target.startsWith(this.apiBasePath)) {
|
|
642
|
+
reply.code(404).send('Not Found');
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
const method = request.method.toUpperCase();
|
|
646
|
+
reply.code(404).send({
|
|
888
647
|
success: false,
|
|
889
648
|
code: 404,
|
|
890
|
-
message:
|
|
649
|
+
message: `No such endpoint: ${method} ${target}`,
|
|
891
650
|
data: null,
|
|
892
651
|
errors: {}
|
|
893
|
-
};
|
|
894
|
-
|
|
895
|
-
};
|
|
896
|
-
this.app.use(this.apiBasePath, this.apiNotFoundHandler);
|
|
652
|
+
});
|
|
653
|
+
});
|
|
897
654
|
}
|
|
898
655
|
installApiErrorHandler() {
|
|
899
656
|
if (this.apiErrorHandlerInstalled) {
|
|
900
657
|
return;
|
|
901
658
|
}
|
|
902
659
|
this.apiErrorHandlerInstalled = true;
|
|
903
|
-
this.
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
660
|
+
this.fastify.setErrorHandler((error, request, reply) => {
|
|
661
|
+
const message = error instanceof Error ? error.message : '';
|
|
662
|
+
if (message.includes('Not allowed by CORS')) {
|
|
663
|
+
const target = request.raw.url ?? request.url;
|
|
664
|
+
if (target.startsWith(this.apiBasePath)) {
|
|
665
|
+
reply.code(403).send({
|
|
666
|
+
success: false,
|
|
667
|
+
code: 403,
|
|
668
|
+
message: 'Origin not allowed by CORS',
|
|
669
|
+
data: null,
|
|
670
|
+
errors: {}
|
|
671
|
+
});
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
reply.code(403).send('Origin not allowed by CORS');
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
const validation = error.validation;
|
|
678
|
+
if (Array.isArray(validation)) {
|
|
679
|
+
const fieldErrors = {};
|
|
680
|
+
for (const v of validation) {
|
|
681
|
+
const field = v.params?.missingProperty ?? (v.instancePath?.replace(/^\//, '') || 'body');
|
|
682
|
+
fieldErrors[field] = v.message ?? 'Invalid value';
|
|
683
|
+
}
|
|
684
|
+
reply.code(400).send({
|
|
685
|
+
success: false,
|
|
686
|
+
code: 400,
|
|
687
|
+
message: 'Validation failed',
|
|
688
|
+
data: null,
|
|
689
|
+
errors: fieldErrors
|
|
690
|
+
});
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
if (error instanceof ApiError || (0, error_utils_js_1.isApiErrorLike)(error)) {
|
|
694
|
+
const apiError = error;
|
|
695
|
+
reply.code(apiError.code).send({
|
|
696
|
+
success: false,
|
|
697
|
+
code: apiError.code,
|
|
698
|
+
message: apiError.message,
|
|
699
|
+
data: apiError.data ?? null,
|
|
700
|
+
errors: apiError.errors && typeof apiError.errors === 'object' && !Array.isArray(apiError.errors)
|
|
701
|
+
? apiError.errors
|
|
702
|
+
: {}
|
|
703
|
+
});
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
const status = (0, error_utils_js_1.asHttpStatus)(error);
|
|
707
|
+
if (status) {
|
|
708
|
+
if (status >= 500) {
|
|
709
|
+
this.logUnhandledError(`Unhandled Fastify error mapped to HTTP ${status}`, error);
|
|
710
|
+
}
|
|
711
|
+
if (status === 413) {
|
|
712
|
+
reply.code(413).send({
|
|
713
|
+
success: false,
|
|
714
|
+
code: 413,
|
|
715
|
+
message: `Upload exceeds maximum size of ${this.config.uploadMax} bytes`,
|
|
716
|
+
data: null,
|
|
717
|
+
errors: {}
|
|
718
|
+
});
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
reply.code(status).send({
|
|
722
|
+
success: false,
|
|
723
|
+
code: status,
|
|
724
|
+
message: status === 400 ? 'Invalid request payload' : this.internalServerErrorMessage(error),
|
|
725
|
+
data: null,
|
|
726
|
+
errors: {}
|
|
727
|
+
});
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
this.logUnhandledError('Unhandled Fastify error fallback to HTTP 500', error);
|
|
731
|
+
reply.code(500).send({
|
|
732
|
+
success: false,
|
|
733
|
+
code: 500,
|
|
734
|
+
message: this.internalServerErrorMessage(error),
|
|
735
|
+
data: null,
|
|
736
|
+
errors: {}
|
|
737
|
+
});
|
|
738
|
+
});
|
|
909
739
|
}
|
|
910
740
|
start() {
|
|
911
741
|
if (!this.finalized) {
|
|
912
742
|
console.warn('[api-server-base] ApiServer.start() called without finalize(); auto-finalizing. Call server.finalize() before start() to silence this warning.');
|
|
913
743
|
this.finalize();
|
|
914
744
|
}
|
|
915
|
-
this.
|
|
745
|
+
void this.fastify
|
|
916
746
|
.listen({
|
|
917
747
|
port: this.config.apiPort,
|
|
918
748
|
host: this.config.apiHost
|
|
919
749
|
})
|
|
920
|
-
.
|
|
750
|
+
.then(() => {
|
|
921
751
|
console.log(`Server is running on http://${this.config.apiHost}:${this.config.apiPort}`);
|
|
922
752
|
})
|
|
923
|
-
.
|
|
753
|
+
.catch((error) => {
|
|
924
754
|
let message;
|
|
925
755
|
if (error.code === 'EADDRINUSE') {
|
|
926
756
|
message = `Port ${this.config.apiPort} is already in use.`;
|
|
@@ -940,13 +770,17 @@ class ApiServer {
|
|
|
940
770
|
this.config.onStartError(err);
|
|
941
771
|
return;
|
|
942
772
|
}
|
|
943
|
-
|
|
773
|
+
this.logUnhandledError('Server startup failed', err);
|
|
774
|
+
process.exitCode = 1;
|
|
944
775
|
});
|
|
945
776
|
return this;
|
|
946
777
|
}
|
|
947
778
|
internalServerErrorMessage(error) {
|
|
948
779
|
return this.config.debug ? this.guessExceptionText(error) : 'Internal server error';
|
|
949
780
|
}
|
|
781
|
+
logUnhandledError(context, error) {
|
|
782
|
+
console.error(`[ApiServer] ${context}`, error);
|
|
783
|
+
}
|
|
950
784
|
async verifyJWT(token) {
|
|
951
785
|
if (!this.config.accessSecret) {
|
|
952
786
|
return { tokenData: undefined, error: 'JWT authentication disabled; no jwtSecret set', expired: false };
|
|
@@ -955,7 +789,7 @@ class ApiServer {
|
|
|
955
789
|
if (!result.success) {
|
|
956
790
|
return { tokenData: undefined, error: result.error, expired: result.expired };
|
|
957
791
|
}
|
|
958
|
-
if (
|
|
792
|
+
if (result.data.uid == null) {
|
|
959
793
|
return { tokenData: undefined, error: 'Missing/bad userid in token', expired: false };
|
|
960
794
|
}
|
|
961
795
|
return { tokenData: result.data, error: undefined, expired: false };
|
|
@@ -998,8 +832,9 @@ class ApiServer {
|
|
|
998
832
|
return null;
|
|
999
833
|
}
|
|
1000
834
|
const storedUid = String(stored.userId);
|
|
1001
|
-
const verifyUid = verify.data.uid
|
|
1002
|
-
|
|
835
|
+
const verifyUid = verify.data.uid != null ? String(verify.data.uid) : null;
|
|
836
|
+
// A missing uid claim is treated as a binding failure, not a skip.
|
|
837
|
+
if (!verifyUid || verifyUid !== storedUid) {
|
|
1003
838
|
return null;
|
|
1004
839
|
}
|
|
1005
840
|
const claims = verify.data;
|
|
@@ -1007,7 +842,6 @@ class ApiServer {
|
|
|
1007
842
|
void _exp;
|
|
1008
843
|
void _iat;
|
|
1009
844
|
void _nbf;
|
|
1010
|
-
// Ensure we never embed token secrets into refreshed access tokens.
|
|
1011
845
|
delete payload.accessToken;
|
|
1012
846
|
delete payload.refreshToken;
|
|
1013
847
|
delete payload.userId;
|
|
@@ -1046,7 +880,8 @@ class ApiServer {
|
|
|
1046
880
|
}
|
|
1047
881
|
let token = null;
|
|
1048
882
|
const authHeader = apiReq.req.headers.authorization;
|
|
1049
|
-
const
|
|
883
|
+
const headerValue = Array.isArray(authHeader) ? authHeader[0] : authHeader;
|
|
884
|
+
const bearerToken = headerValue?.startsWith('Bearer ') ? headerValue.slice(7).trim() || null : null;
|
|
1050
885
|
const paramToken = bearerToken ? null : this.resolveTokenFromRequest(apiReq.req);
|
|
1051
886
|
const requiresAuthToken = this.requiresAuthToken(authType);
|
|
1052
887
|
const allowRefresh = requiresAuthToken || (authType === 'maybe' && this.config.refreshMaybe);
|
|
@@ -1173,7 +1008,6 @@ class ApiServer {
|
|
|
1173
1008
|
apiReq.token = secret;
|
|
1174
1009
|
apiReq.apiKey = key;
|
|
1175
1010
|
apiReq.authMethod = 'apikey';
|
|
1176
|
-
// uid is the real identity; euid (if set) is the effective/impersonated identity.
|
|
1177
1011
|
const resolvedRuid = this.normalizeAuthIdentifier(key.uid);
|
|
1178
1012
|
const resolvedEuid = key.euid !== undefined ? this.normalizeAuthIdentifier(key.euid) : null;
|
|
1179
1013
|
const effectiveUid = resolvedEuid ?? resolvedRuid;
|
|
@@ -1271,21 +1105,25 @@ class ApiServer {
|
|
|
1271
1105
|
}
|
|
1272
1106
|
return rawReal;
|
|
1273
1107
|
}
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
}
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1108
|
+
toExtendedReq(request) {
|
|
1109
|
+
const parsedBody = request.__apiParsedBody;
|
|
1110
|
+
const parsedFiles = request.__apiParsedFiles;
|
|
1111
|
+
const headers = request.headers;
|
|
1112
|
+
return {
|
|
1113
|
+
method: request.method,
|
|
1114
|
+
url: request.url,
|
|
1115
|
+
originalUrl: request.raw.url,
|
|
1116
|
+
headers,
|
|
1117
|
+
query: (request.query ?? {}),
|
|
1118
|
+
body: parsedBody ?? request.body,
|
|
1119
|
+
params: (request.params ?? {}),
|
|
1120
|
+
cookies: (request.cookies ?? {}),
|
|
1121
|
+
ip: request.ip,
|
|
1122
|
+
ips: request.ips,
|
|
1123
|
+
socket: request.socket,
|
|
1124
|
+
protocol: request.protocol,
|
|
1125
|
+
files: parsedFiles
|
|
1126
|
+
};
|
|
1289
1127
|
}
|
|
1290
1128
|
createApiRequest(req, res) {
|
|
1291
1129
|
const apiReq = {
|
|
@@ -1296,9 +1134,9 @@ class ApiServer {
|
|
|
1296
1134
|
tokenData: null,
|
|
1297
1135
|
authMethod: null,
|
|
1298
1136
|
realUid: null,
|
|
1299
|
-
getClientInfo: () => ensureClientInfo(apiReq),
|
|
1300
|
-
getClientIp: () => ensureClientInfo(apiReq).ip,
|
|
1301
|
-
getClientIpChain: () => ensureClientInfo(apiReq).ipchain,
|
|
1137
|
+
getClientInfo: () => (0, client_info_js_1.ensureClientInfo)(apiReq, this.config.trustProxy),
|
|
1138
|
+
getClientIp: () => (0, client_info_js_1.ensureClientInfo)(apiReq, this.config.trustProxy).ip,
|
|
1139
|
+
getClientIpChain: () => (0, client_info_js_1.ensureClientInfo)(apiReq, this.config.trustProxy).ipchain,
|
|
1302
1140
|
getRealUid: () => apiReq.realUid ?? null,
|
|
1303
1141
|
isImpersonating: () => {
|
|
1304
1142
|
const realUid = apiReq.realUid;
|
|
@@ -1309,11 +1147,101 @@ class ApiServer {
|
|
|
1309
1147
|
if (tokenUid === null || tokenUid === undefined) {
|
|
1310
1148
|
return false;
|
|
1311
1149
|
}
|
|
1312
|
-
|
|
1150
|
+
// Normalise both sides to strings before comparing so that
|
|
1151
|
+
// numeric 42 and string "42" are treated as the same identity.
|
|
1152
|
+
return String(realUid) !== String(tokenUid);
|
|
1313
1153
|
}
|
|
1314
1154
|
};
|
|
1315
1155
|
return apiReq;
|
|
1316
1156
|
}
|
|
1157
|
+
useExpress(pathOrHandler, ...handlers) {
|
|
1158
|
+
this.assertNotFinalized('useExpress');
|
|
1159
|
+
if (typeof pathOrHandler !== 'string') {
|
|
1160
|
+
if (pathOrHandler.length === 4) {
|
|
1161
|
+
this.compatGlobalErrorHandler = pathOrHandler;
|
|
1162
|
+
}
|
|
1163
|
+
return this;
|
|
1164
|
+
}
|
|
1165
|
+
const path = pathOrHandler;
|
|
1166
|
+
const stack = handlers;
|
|
1167
|
+
this.fastify.all(path, async (request, reply) => {
|
|
1168
|
+
const req = this.toExtendedReq(request);
|
|
1169
|
+
const res = new FastifyResponseAdapter(reply);
|
|
1170
|
+
try {
|
|
1171
|
+
await this.runCompatHandlers(stack, req, res);
|
|
1172
|
+
}
|
|
1173
|
+
catch (error) {
|
|
1174
|
+
await this.runCompatErrorHandlers(error, stack, req, res);
|
|
1175
|
+
}
|
|
1176
|
+
});
|
|
1177
|
+
return this;
|
|
1178
|
+
}
|
|
1179
|
+
async runCompatHandlers(stack, req, res) {
|
|
1180
|
+
const handlers = stack.filter((fn) => fn.length !== 4);
|
|
1181
|
+
for (const fn of handlers) {
|
|
1182
|
+
await new Promise((resolve, reject) => {
|
|
1183
|
+
let nextCalled = false;
|
|
1184
|
+
const next = (error) => {
|
|
1185
|
+
nextCalled = true;
|
|
1186
|
+
if (error) {
|
|
1187
|
+
reject(error);
|
|
1188
|
+
return;
|
|
1189
|
+
}
|
|
1190
|
+
resolve();
|
|
1191
|
+
};
|
|
1192
|
+
try {
|
|
1193
|
+
const out = fn(req, res, next);
|
|
1194
|
+
Promise.resolve(out)
|
|
1195
|
+
.then(() => {
|
|
1196
|
+
if (!nextCalled) {
|
|
1197
|
+
resolve();
|
|
1198
|
+
}
|
|
1199
|
+
})
|
|
1200
|
+
.catch(reject);
|
|
1201
|
+
}
|
|
1202
|
+
catch (error) {
|
|
1203
|
+
reject(error);
|
|
1204
|
+
}
|
|
1205
|
+
});
|
|
1206
|
+
if (res.headersSent) {
|
|
1207
|
+
return;
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
async runCompatErrorHandlers(error, stack, req, res) {
|
|
1212
|
+
const errorHandlers = stack.filter((fn) => fn.length === 4);
|
|
1213
|
+
if (this.compatGlobalErrorHandler) {
|
|
1214
|
+
errorHandlers.push(this.compatGlobalErrorHandler);
|
|
1215
|
+
}
|
|
1216
|
+
if (errorHandlers.length === 0) {
|
|
1217
|
+
throw error;
|
|
1218
|
+
}
|
|
1219
|
+
let currentError = error;
|
|
1220
|
+
for (const handler of errorHandlers) {
|
|
1221
|
+
await new Promise((resolve, reject) => {
|
|
1222
|
+
const next = (nextError) => {
|
|
1223
|
+
if (nextError) {
|
|
1224
|
+
currentError = nextError;
|
|
1225
|
+
}
|
|
1226
|
+
resolve();
|
|
1227
|
+
};
|
|
1228
|
+
try {
|
|
1229
|
+
Promise.resolve(handler(currentError, req, res, next))
|
|
1230
|
+
.then(() => resolve())
|
|
1231
|
+
.catch(reject);
|
|
1232
|
+
}
|
|
1233
|
+
catch (caught) {
|
|
1234
|
+
reject(caught);
|
|
1235
|
+
}
|
|
1236
|
+
});
|
|
1237
|
+
if (res.headersSent) {
|
|
1238
|
+
return;
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
if (!res.headersSent) {
|
|
1242
|
+
throw currentError;
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1317
1245
|
expressAuth(auth) {
|
|
1318
1246
|
return async (req, res, next) => {
|
|
1319
1247
|
const apiReq = this.createApiRequest(req, res);
|
|
@@ -1321,7 +1249,7 @@ class ApiServer {
|
|
|
1321
1249
|
res.locals.apiReq = apiReq;
|
|
1322
1250
|
try {
|
|
1323
1251
|
if (this.config.hydrateGetBody) {
|
|
1324
|
-
hydrateGetBody(req);
|
|
1252
|
+
(0, request_utils_js_1.hydrateGetBody)(req);
|
|
1325
1253
|
}
|
|
1326
1254
|
if (this.config.debug) {
|
|
1327
1255
|
this.dumpRequest(apiReq);
|
|
@@ -1342,7 +1270,7 @@ class ApiServer {
|
|
|
1342
1270
|
next(error);
|
|
1343
1271
|
return;
|
|
1344
1272
|
}
|
|
1345
|
-
if (error instanceof ApiError || isApiErrorLike(error)) {
|
|
1273
|
+
if (error instanceof ApiError || (0, error_utils_js_1.isApiErrorLike)(error)) {
|
|
1346
1274
|
const apiError = error;
|
|
1347
1275
|
const normalizedErrors = apiError.errors && typeof apiError.errors === 'object' && !Array.isArray(apiError.errors)
|
|
1348
1276
|
? apiError.errors
|
|
@@ -1357,8 +1285,11 @@ class ApiServer {
|
|
|
1357
1285
|
res.status(apiError.code).json(errorPayload);
|
|
1358
1286
|
return;
|
|
1359
1287
|
}
|
|
1360
|
-
const status = asHttpStatus(error);
|
|
1288
|
+
const status = (0, error_utils_js_1.asHttpStatus)(error);
|
|
1361
1289
|
if (status) {
|
|
1290
|
+
if (status >= 500) {
|
|
1291
|
+
this.logUnhandledError(`Unhandled compat middleware error mapped to HTTP ${status}`, error);
|
|
1292
|
+
}
|
|
1362
1293
|
res.status(status).json({
|
|
1363
1294
|
success: false,
|
|
1364
1295
|
code: status,
|
|
@@ -1368,23 +1299,24 @@ class ApiServer {
|
|
|
1368
1299
|
});
|
|
1369
1300
|
return;
|
|
1370
1301
|
}
|
|
1371
|
-
|
|
1302
|
+
this.logUnhandledError('Unhandled compat middleware error fallback to HTTP 500', error);
|
|
1303
|
+
res.status(500).json({
|
|
1372
1304
|
success: false,
|
|
1373
1305
|
code: 500,
|
|
1374
1306
|
message: this.internalServerErrorMessage(error),
|
|
1375
1307
|
data: null,
|
|
1376
1308
|
errors: {}
|
|
1377
|
-
};
|
|
1378
|
-
res.status(500).json(errorPayload);
|
|
1309
|
+
});
|
|
1379
1310
|
};
|
|
1380
1311
|
}
|
|
1381
|
-
|
|
1382
|
-
return async (
|
|
1383
|
-
|
|
1312
|
+
handleRequest(handler, auth) {
|
|
1313
|
+
return async (request, reply) => {
|
|
1314
|
+
const req = this.toExtendedReq(request);
|
|
1315
|
+
const res = new FastifyResponseAdapter(reply);
|
|
1384
1316
|
const apiReq = this.createApiRequest(req, res);
|
|
1385
1317
|
try {
|
|
1386
1318
|
if (this.config.hydrateGetBody) {
|
|
1387
|
-
hydrateGetBody(apiReq.req);
|
|
1319
|
+
(0, request_utils_js_1.hydrateGetBody)(apiReq.req);
|
|
1388
1320
|
}
|
|
1389
1321
|
if (this.config.debug) {
|
|
1390
1322
|
this.dumpRequest(apiReq);
|
|
@@ -1403,14 +1335,14 @@ class ApiServer {
|
|
|
1403
1335
|
throw new ApiError({ code: 500, message: 'Handler result must start with a numeric status code' });
|
|
1404
1336
|
}
|
|
1405
1337
|
const message = typeof rawMessage === 'string' ? rawMessage : 'Success';
|
|
1406
|
-
const responsePayload = { success:
|
|
1338
|
+
const responsePayload = { success: code < 400, code, message, data, errors: {} };
|
|
1407
1339
|
if (this.config.debug) {
|
|
1408
1340
|
this.dumpResponse(apiReq, responsePayload, code);
|
|
1409
1341
|
}
|
|
1410
|
-
|
|
1342
|
+
reply.code(code).send(responsePayload);
|
|
1411
1343
|
}
|
|
1412
1344
|
catch (error) {
|
|
1413
|
-
if (error instanceof ApiError || isApiErrorLike(error)) {
|
|
1345
|
+
if (error instanceof ApiError || (0, error_utils_js_1.isApiErrorLike)(error)) {
|
|
1414
1346
|
const apiError = error;
|
|
1415
1347
|
const normalizedErrors = apiError.errors && typeof apiError.errors === 'object' && !Array.isArray(apiError.errors)
|
|
1416
1348
|
? apiError.errors
|
|
@@ -1425,27 +1357,53 @@ class ApiServer {
|
|
|
1425
1357
|
if (this.config.debug) {
|
|
1426
1358
|
this.dumpResponse(apiReq, errorPayload, apiError.code);
|
|
1427
1359
|
}
|
|
1428
|
-
|
|
1360
|
+
reply.code(apiError.code).send(errorPayload);
|
|
1429
1361
|
}
|
|
1430
1362
|
else {
|
|
1363
|
+
const status = (0, error_utils_js_1.asHttpStatus)(error) ?? 500;
|
|
1364
|
+
if (status >= 500) {
|
|
1365
|
+
this.logUnhandledError('Unhandled API route error fallback to HTTP 500', error);
|
|
1366
|
+
}
|
|
1367
|
+
const uploadTooLarge = error &&
|
|
1368
|
+
typeof error === 'object' &&
|
|
1369
|
+
('code' in error ? error.code === 'FST_REQ_FILE_TOO_LARGE' : false);
|
|
1431
1370
|
const errorPayload = {
|
|
1432
1371
|
success: false,
|
|
1433
|
-
code:
|
|
1434
|
-
message:
|
|
1372
|
+
code: uploadTooLarge ? 413 : status,
|
|
1373
|
+
message: uploadTooLarge
|
|
1374
|
+
? `Upload exceeds maximum size of ${this.config.uploadMax} bytes`
|
|
1375
|
+
: this.internalServerErrorMessage(error),
|
|
1435
1376
|
data: null,
|
|
1436
1377
|
errors: {}
|
|
1437
1378
|
};
|
|
1438
1379
|
if (this.config.debug) {
|
|
1439
|
-
this.dumpResponse(apiReq, errorPayload,
|
|
1380
|
+
this.dumpResponse(apiReq, errorPayload, uploadTooLarge ? 413 : status);
|
|
1440
1381
|
}
|
|
1441
|
-
|
|
1382
|
+
reply.code(uploadTooLarge ? 413 : status).send(errorPayload);
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
};
|
|
1386
|
+
}
|
|
1387
|
+
// Test/compat shim for direct unit-level request wrapping without Fastify runtime.
|
|
1388
|
+
handle_request(handler, auth) {
|
|
1389
|
+
return async (req, res, next) => {
|
|
1390
|
+
const apiReq = this.createApiRequest(req, res);
|
|
1391
|
+
try {
|
|
1392
|
+
if (this.config.hydrateGetBody) {
|
|
1393
|
+
(0, request_utils_js_1.hydrateGetBody)(apiReq.req);
|
|
1442
1394
|
}
|
|
1395
|
+
apiReq.tokenData = await this.authenticate(apiReq, auth.type);
|
|
1396
|
+
await this.authorize(apiReq, auth.req);
|
|
1397
|
+
await handler(apiReq);
|
|
1398
|
+
next();
|
|
1399
|
+
}
|
|
1400
|
+
catch (error) {
|
|
1401
|
+
next(error);
|
|
1443
1402
|
}
|
|
1444
1403
|
};
|
|
1445
1404
|
}
|
|
1446
1405
|
api(module) {
|
|
1447
1406
|
this.assertNotFinalized('api');
|
|
1448
|
-
const router = express_1.default.Router();
|
|
1449
1407
|
module.server = this;
|
|
1450
1408
|
const moduleType = module.moduleType;
|
|
1451
1409
|
if (moduleType === 'auth') {
|
|
@@ -1456,36 +1414,33 @@ class ApiServer {
|
|
|
1456
1414
|
const name = module.constructor?.name ? String(module.constructor.name) : 'ApiModule';
|
|
1457
1415
|
throw new Error(`${name}.checkConfig() returned false`);
|
|
1458
1416
|
}
|
|
1459
|
-
|
|
1417
|
+
module.onMount();
|
|
1460
1418
|
const ns = module.namespace;
|
|
1461
|
-
const mountPath = `${
|
|
1419
|
+
const mountPath = `${this.apiBasePath}${ns}`;
|
|
1462
1420
|
module.mountpath = mountPath;
|
|
1463
|
-
module.defineRoutes()
|
|
1464
|
-
const handler = this.
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
break;
|
|
1469
|
-
case 'post':
|
|
1470
|
-
router.post(r.path, handler);
|
|
1471
|
-
break;
|
|
1472
|
-
case 'put':
|
|
1473
|
-
router.put(r.path, handler);
|
|
1474
|
-
break;
|
|
1475
|
-
case 'patch':
|
|
1476
|
-
router.patch(r.path, handler);
|
|
1477
|
-
break;
|
|
1478
|
-
case 'delete':
|
|
1479
|
-
router.delete(r.path, handler);
|
|
1480
|
-
break;
|
|
1481
|
-
}
|
|
1421
|
+
for (const route of module.defineRoutes()) {
|
|
1422
|
+
const handler = this.handleRequest(route.handler, route.auth);
|
|
1423
|
+
const method = route.method.toUpperCase();
|
|
1424
|
+
const fullPath = this.joinRoutePath(this.apiBasePath, ns, route.path);
|
|
1425
|
+
this.fastify.route({ method, url: fullPath, handler, ...(route.schema ? { schema: route.schema } : {}) });
|
|
1482
1426
|
if (this.config.debug) {
|
|
1483
|
-
console.log(`Adding ${
|
|
1427
|
+
console.log(`Adding ${fullPath} (${method})`);
|
|
1484
1428
|
}
|
|
1485
|
-
}
|
|
1486
|
-
this.apiRouter.use(ns, router);
|
|
1429
|
+
}
|
|
1487
1430
|
return this;
|
|
1488
1431
|
}
|
|
1432
|
+
joinRoutePath(base, namespace, routePath) {
|
|
1433
|
+
const parts = [base, namespace, routePath]
|
|
1434
|
+
.filter((value) => typeof value === 'string' && value.length > 0)
|
|
1435
|
+
.map((value, index) => {
|
|
1436
|
+
if (index === 0) {
|
|
1437
|
+
return value.startsWith('/') ? value : `/${value}`;
|
|
1438
|
+
}
|
|
1439
|
+
return value.replace(/^\/+/, '').replace(/\/+$/, '');
|
|
1440
|
+
});
|
|
1441
|
+
const joined = parts.join('/').replace(/\/+/g, '/');
|
|
1442
|
+
return joined.startsWith('/') ? joined : `/${joined}`;
|
|
1443
|
+
}
|
|
1489
1444
|
dumpRequest(apiReq) {
|
|
1490
1445
|
const req = apiReq.req;
|
|
1491
1446
|
const tokenParam = this.config.tokenParam.trim();
|
|
@@ -1493,10 +1448,7 @@ class ApiServer {
|
|
|
1493
1448
|
const url = req.originalUrl || req.url;
|
|
1494
1449
|
console.log('URL:', url);
|
|
1495
1450
|
console.log('Method:', req.method);
|
|
1496
|
-
if (tokenParam &&
|
|
1497
|
-
req.query &&
|
|
1498
|
-
typeof req.query === 'object' &&
|
|
1499
|
-
tokenParam in req.query) {
|
|
1451
|
+
if (tokenParam && req.query && tokenParam in req.query) {
|
|
1500
1452
|
const maskedQuery = { ...req.query, [tokenParam]: '[REDACTED]' };
|
|
1501
1453
|
console.log('Query Params:', maskedQuery);
|
|
1502
1454
|
}
|
|
@@ -1528,6 +1480,12 @@ class ApiServer {
|
|
|
1528
1480
|
if (headers.authorization) {
|
|
1529
1481
|
headers.authorization = '[REDACTED]';
|
|
1530
1482
|
}
|
|
1483
|
+
if (headers['x-api-key']) {
|
|
1484
|
+
headers['x-api-key'] = '[REDACTED]';
|
|
1485
|
+
}
|
|
1486
|
+
if (headers.cookie) {
|
|
1487
|
+
headers.cookie = '[REDACTED]';
|
|
1488
|
+
}
|
|
1531
1489
|
console.log('Headers:', headers);
|
|
1532
1490
|
console.log('------------------------');
|
|
1533
1491
|
}
|