@technomoron/api-server-base 2.0.0-beta.21 → 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 +15 -0
- package/dist/cjs/{api-server-base.cjs → server/src/api-server-base.cjs} +669 -627
- package/dist/{esm → cjs/server/src}/api-server-base.d.ts +105 -78
- 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/cjs/{oauth → 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/cjs/{user → 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 +15 -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 +105 -78
- package/dist/esm/{api-server-base.js → server/src/api-server-base.js} +658 -616
- 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/esm/{oauth → 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/esm/{user → 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 +411 -125
- package/package.json +129 -134
- package/README.txt +0 -213
- 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,
|
|
@@ -372,6 +109,10 @@ function fillConfig(config) {
|
|
|
372
109
|
cookieSameSite: config.cookieSameSite ?? 'lax',
|
|
373
110
|
cookieSecure: config.cookieSecure ?? 'auto',
|
|
374
111
|
cookieHttpOnly: config.cookieHttpOnly ?? true,
|
|
112
|
+
apiKeyPrefix: config.apiKeyPrefix ?? '',
|
|
113
|
+
apiKeyEnabled: config.apiKeyEnabled ?? false,
|
|
114
|
+
tokenParam: config.tokenParam ?? '',
|
|
115
|
+
tokenParamLocation: config.tokenParamLocation ?? 'body',
|
|
375
116
|
accessCookie: config.accessCookie ?? 'dat',
|
|
376
117
|
refreshCookie: config.refreshCookie ?? 'drt',
|
|
377
118
|
accessExpiry: config.accessExpiry ?? 60 * 15,
|
|
@@ -385,36 +126,27 @@ function fillConfig(config) {
|
|
|
385
126
|
apiVersion: config.apiVersion ?? '',
|
|
386
127
|
minClientVersion: config.minClientVersion ?? '',
|
|
387
128
|
tokenStore: config.tokenStore,
|
|
388
|
-
|
|
389
|
-
|
|
129
|
+
onStartError: config.onStartError,
|
|
130
|
+
trustProxy: config.trustProxy ?? true
|
|
390
131
|
};
|
|
391
132
|
}
|
|
133
|
+
/** Core Fastify-based API server with module mounting and auth integration hooks. */
|
|
392
134
|
class ApiServer {
|
|
393
|
-
/**
|
|
394
|
-
* @deprecated ApiServer does not track a global "current request". This value is always null.
|
|
395
|
-
* Use the per-request ApiRequest passed to handlers, or `req.apiReq` / `res.locals.apiReq`
|
|
396
|
-
* when mounting raw Express endpoints.
|
|
397
|
-
*/
|
|
398
135
|
get currReq() {
|
|
399
136
|
return null;
|
|
400
137
|
}
|
|
401
138
|
set currReq(_value) {
|
|
402
139
|
if (this.config.devMode && !this.currReqDeprecationWarned) {
|
|
403
140
|
this.currReqDeprecationWarned = true;
|
|
404
|
-
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.');
|
|
405
142
|
}
|
|
406
143
|
void _value;
|
|
407
144
|
}
|
|
408
145
|
constructor(config = {}) {
|
|
409
146
|
this.finalized = false;
|
|
410
|
-
this.serverAuthAdapter = null;
|
|
411
|
-
this.apiNotFoundHandler = null;
|
|
412
147
|
this.apiErrorHandlerInstalled = false;
|
|
413
148
|
this.tokenStoreAdapter = null;
|
|
414
|
-
this.
|
|
415
|
-
this.passkeyServiceAdapter = null;
|
|
416
|
-
this.oauthStoreAdapter = null;
|
|
417
|
-
this.canImpersonateAdapter = null;
|
|
149
|
+
this.compatGlobalErrorHandler = null;
|
|
418
150
|
this.currReqDeprecationWarned = false;
|
|
419
151
|
this.config = fillConfig(config);
|
|
420
152
|
this.apiBasePath = this.normalizeApiBasePath(this.config.apiBasePath);
|
|
@@ -423,100 +155,153 @@ class ApiServer {
|
|
|
423
155
|
this.moduleAdapter = module_js_1.nullAuthModule;
|
|
424
156
|
this.jwtHelper = new JwtHelperStore();
|
|
425
157
|
this.tokenStoreAdapter = this.config.tokenStore ?? null;
|
|
426
|
-
if (this.config.
|
|
427
|
-
const { userStore, tokenStore, passkeyService, oauthStore, canImpersonate } = this.config.authStores;
|
|
428
|
-
this.userStoreAdapter = userStore;
|
|
429
|
-
this.tokenStoreAdapter = tokenStore;
|
|
430
|
-
this.passkeyServiceAdapter = passkeyService ?? null;
|
|
431
|
-
this.oauthStoreAdapter = oauthStore ?? null;
|
|
432
|
-
this.canImpersonateAdapter = canImpersonate ?? null;
|
|
433
|
-
this.storageAdapter = this.getServerAuthAdapter();
|
|
434
|
-
}
|
|
435
|
-
if ((this.config.authApi || this.config.authStores) &&
|
|
436
|
-
(!this.config.accessSecret || !this.config.refreshSecret)) {
|
|
158
|
+
if (this.config.authApi && (!this.config.accessSecret || !this.config.refreshSecret)) {
|
|
437
159
|
console.warn('[api-server-base] Auth is enabled but accessSecret and/or refreshSecret are empty. JWT signing will fail at runtime.');
|
|
438
160
|
}
|
|
439
|
-
this.
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
const
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
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);
|
|
458
181
|
}
|
|
459
|
-
next(err);
|
|
460
182
|
});
|
|
461
|
-
|
|
462
|
-
|
|
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
|
+
});
|
|
463
246
|
this.installStaticDirs();
|
|
464
247
|
this.installPingHandler();
|
|
465
248
|
this.installSwaggerHandler();
|
|
466
|
-
this.app.use(this.apiBasePath, this.apiRouter);
|
|
467
|
-
// addSwaggerUi(this.app);
|
|
468
249
|
this.installApiNotFoundHandler();
|
|
469
250
|
this.installApiErrorHandler();
|
|
470
251
|
}
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
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);
|
|
474
262
|
}
|
|
263
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
475
264
|
}
|
|
476
|
-
|
|
477
|
-
if (
|
|
478
|
-
return
|
|
265
|
+
parseRawBody(raw, contentType) {
|
|
266
|
+
if (!raw) {
|
|
267
|
+
return {};
|
|
479
268
|
}
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
return normalized;
|
|
269
|
+
if (contentType.includes('application/json')) {
|
|
270
|
+
try {
|
|
271
|
+
return JSON.parse(raw);
|
|
272
|
+
}
|
|
273
|
+
catch {
|
|
274
|
+
return {};
|
|
275
|
+
}
|
|
488
276
|
}
|
|
489
|
-
if (
|
|
490
|
-
|
|
277
|
+
if (contentType.includes('application/x-www-form-urlencoded')) {
|
|
278
|
+
const params = new URLSearchParams(raw);
|
|
279
|
+
return Object.fromEntries(params.entries());
|
|
491
280
|
}
|
|
492
|
-
|
|
493
|
-
|
|
281
|
+
return raw;
|
|
282
|
+
}
|
|
283
|
+
assertNotFinalized(action) {
|
|
284
|
+
if (this.finalized) {
|
|
285
|
+
throw new Error(`Cannot call ApiServer.${action}() after finalize()/start()`);
|
|
494
286
|
}
|
|
495
|
-
return null;
|
|
496
287
|
}
|
|
497
288
|
finalize() {
|
|
498
|
-
this.installApiNotFoundHandler();
|
|
499
|
-
this.installApiErrorHandler();
|
|
500
289
|
this.finalized = true;
|
|
501
290
|
return this;
|
|
502
291
|
}
|
|
503
292
|
authStorage(storage) {
|
|
293
|
+
this.assertNotFinalized('authStorage');
|
|
504
294
|
this.storageAdapter = storage;
|
|
505
295
|
return this;
|
|
506
296
|
}
|
|
507
|
-
/**
|
|
508
|
-
* @deprecated Use {@link ApiServer.authStorage} instead.
|
|
509
|
-
*/
|
|
510
297
|
useAuthStorage(storage) {
|
|
511
298
|
return this.authStorage(storage);
|
|
512
299
|
}
|
|
513
300
|
authModule(module) {
|
|
301
|
+
this.assertNotFinalized('authModule');
|
|
514
302
|
this.moduleAdapter = module;
|
|
515
303
|
return this;
|
|
516
304
|
}
|
|
517
|
-
/**
|
|
518
|
-
* @deprecated Use {@link ApiServer.authModule} instead.
|
|
519
|
-
*/
|
|
520
305
|
useAuthModule(module) {
|
|
521
306
|
return this.authModule(module);
|
|
522
307
|
}
|
|
@@ -527,88 +312,39 @@ class ApiServer {
|
|
|
527
312
|
return this.moduleAdapter;
|
|
528
313
|
}
|
|
529
314
|
setTokenStore(store) {
|
|
315
|
+
this.assertNotFinalized('setTokenStore');
|
|
530
316
|
this.tokenStoreAdapter = store;
|
|
531
|
-
// If using direct stores, expose the server-backed auth adapter.
|
|
532
|
-
if (this.userStoreAdapter) {
|
|
533
|
-
this.storageAdapter = this.getServerAuthAdapter();
|
|
534
|
-
}
|
|
535
317
|
return this;
|
|
536
318
|
}
|
|
537
319
|
getTokenStore() {
|
|
538
320
|
return this.tokenStoreAdapter;
|
|
539
321
|
}
|
|
540
|
-
|
|
541
|
-
if (
|
|
542
|
-
throw new Error('User store is not configured');
|
|
543
|
-
}
|
|
544
|
-
return this.userStoreAdapter;
|
|
545
|
-
}
|
|
546
|
-
ensureTokenStore() {
|
|
547
|
-
if (!this.tokenStoreAdapter) {
|
|
548
|
-
throw new Error('Token store is not configured');
|
|
549
|
-
}
|
|
550
|
-
return this.tokenStoreAdapter;
|
|
551
|
-
}
|
|
552
|
-
ensurePasskeyService() {
|
|
553
|
-
if (!this.passkeyServiceAdapter) {
|
|
322
|
+
async listUserCredentials(userId) {
|
|
323
|
+
if (typeof this.storageAdapter.listUserCredentials !== 'function') {
|
|
554
324
|
throw new Error('Passkey service is not configured');
|
|
555
325
|
}
|
|
556
|
-
return this.
|
|
557
|
-
}
|
|
558
|
-
async listUserCredentials(userId) {
|
|
559
|
-
return this.ensurePasskeyService().listUserCredentials(userId);
|
|
326
|
+
return this.storageAdapter.listUserCredentials(userId);
|
|
560
327
|
}
|
|
561
328
|
async deletePasskeyCredential(credentialId) {
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
ensureOAuthStore() {
|
|
565
|
-
if (!this.oauthStoreAdapter) {
|
|
566
|
-
throw new Error('OAuth store is not configured');
|
|
329
|
+
if (typeof this.storageAdapter.deletePasskeyCredential !== 'function') {
|
|
330
|
+
throw new Error('Passkey service is not configured');
|
|
567
331
|
}
|
|
568
|
-
return this.
|
|
569
|
-
}
|
|
570
|
-
getServerAuthAdapter() {
|
|
571
|
-
if (this.serverAuthAdapter) {
|
|
572
|
-
return this.serverAuthAdapter;
|
|
573
|
-
}
|
|
574
|
-
const server = this;
|
|
575
|
-
this.serverAuthAdapter = {
|
|
576
|
-
getUser: (identifier) => server.getUser(identifier),
|
|
577
|
-
getUserPasswordHash: (user) => server.getUserPasswordHash(user),
|
|
578
|
-
getUserId: (user) => server.getUserId(user),
|
|
579
|
-
filterUser: (user) => server.filterUser(user),
|
|
580
|
-
verifyPassword: (password, hash) => server.verifyPassword(password, hash),
|
|
581
|
-
storeToken: (data) => server.storeToken(data),
|
|
582
|
-
getToken: (query, opts) => server.getToken(query, opts),
|
|
583
|
-
deleteToken: (query) => server.deleteToken(query),
|
|
584
|
-
updateToken: (updates) => server.updateToken(updates),
|
|
585
|
-
createPasskeyChallenge: (params) => server.createPasskeyChallenge(params),
|
|
586
|
-
verifyPasskeyResponse: (params) => server.verifyPasskeyResponse(params),
|
|
587
|
-
listUserCredentials: (userId) => server.listUserCredentials(userId),
|
|
588
|
-
deletePasskeyCredential: (credentialId) => server.deletePasskeyCredential(credentialId),
|
|
589
|
-
getClient: (clientId) => server.getClient(clientId),
|
|
590
|
-
verifyClientSecret: (client, clientSecret) => server.verifyClientSecret(client, clientSecret),
|
|
591
|
-
createAuthCode: (request) => server.createAuthCode(request),
|
|
592
|
-
consumeAuthCode: (code, clientId) => server.consumeAuthCode(code, clientId),
|
|
593
|
-
canImpersonate: (params) => server.canImpersonate(params)
|
|
594
|
-
};
|
|
595
|
-
return this.serverAuthAdapter;
|
|
332
|
+
return this.storageAdapter.deletePasskeyCredential(credentialId);
|
|
596
333
|
}
|
|
597
|
-
// AuthAdapter-compatible helpers (used by AuthModule)
|
|
598
334
|
async getUser(identifier) {
|
|
599
|
-
return this.
|
|
335
|
+
return this.storageAdapter.getUser(identifier);
|
|
600
336
|
}
|
|
601
337
|
getUserPasswordHash(user) {
|
|
602
|
-
return this.
|
|
338
|
+
return this.storageAdapter.getUserPasswordHash(user);
|
|
603
339
|
}
|
|
604
340
|
getUserId(user) {
|
|
605
|
-
return this.
|
|
341
|
+
return this.storageAdapter.getUserId(user);
|
|
606
342
|
}
|
|
607
343
|
filterUser(user) {
|
|
608
|
-
return this.
|
|
344
|
+
return this.storageAdapter.filterUser(user);
|
|
609
345
|
}
|
|
610
346
|
async verifyPassword(password, hash) {
|
|
611
|
-
return this.
|
|
347
|
+
return this.storageAdapter.verifyPassword(password, hash);
|
|
612
348
|
}
|
|
613
349
|
async storeToken(data) {
|
|
614
350
|
if (this.tokenStoreAdapter) {
|
|
@@ -651,47 +387,55 @@ class ApiServer {
|
|
|
651
387
|
return 0;
|
|
652
388
|
}
|
|
653
389
|
async createPasskeyChallenge(params) {
|
|
654
|
-
|
|
390
|
+
if (typeof this.storageAdapter.createPasskeyChallenge !== 'function') {
|
|
391
|
+
throw new Error('Passkey service is not configured');
|
|
392
|
+
}
|
|
393
|
+
return this.storageAdapter.createPasskeyChallenge(params);
|
|
655
394
|
}
|
|
656
395
|
async verifyPasskeyResponse(params) {
|
|
657
|
-
|
|
396
|
+
if (typeof this.storageAdapter.verifyPasskeyResponse !== 'function') {
|
|
397
|
+
throw new Error('Passkey service is not configured');
|
|
398
|
+
}
|
|
399
|
+
return this.storageAdapter.verifyPasskeyResponse(params);
|
|
658
400
|
}
|
|
659
401
|
async getClient(clientId) {
|
|
660
|
-
|
|
402
|
+
if (typeof this.storageAdapter.getClient !== 'function') {
|
|
403
|
+
return null;
|
|
404
|
+
}
|
|
405
|
+
return this.storageAdapter.getClient(clientId);
|
|
661
406
|
}
|
|
662
407
|
async verifyClientSecret(client, clientSecret) {
|
|
663
|
-
|
|
408
|
+
if (typeof this.storageAdapter.verifyClientSecret !== 'function') {
|
|
409
|
+
throw new Error('OAuth store is not configured');
|
|
410
|
+
}
|
|
411
|
+
return this.storageAdapter.verifyClientSecret(client, clientSecret);
|
|
664
412
|
}
|
|
665
413
|
async createAuthCode(request) {
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
return
|
|
670
|
-
code,
|
|
671
|
-
clientId: request.clientId,
|
|
672
|
-
userId: request.userId,
|
|
673
|
-
redirectUri: request.redirectUri,
|
|
674
|
-
scope: request.scope ?? [],
|
|
675
|
-
codeChallenge: request.codeChallenge,
|
|
676
|
-
codeChallengeMethod: request.codeChallengeMethod,
|
|
677
|
-
expiresAt,
|
|
678
|
-
metadata: request.metadata
|
|
679
|
-
};
|
|
414
|
+
if (typeof this.storageAdapter.createAuthCode !== 'function') {
|
|
415
|
+
throw new Error('OAuth store is not configured');
|
|
416
|
+
}
|
|
417
|
+
return this.storageAdapter.createAuthCode(request);
|
|
680
418
|
}
|
|
681
419
|
async consumeAuthCode(code, clientId) {
|
|
682
|
-
|
|
420
|
+
if (typeof this.storageAdapter.consumeAuthCode !== 'function') {
|
|
421
|
+
return null;
|
|
422
|
+
}
|
|
423
|
+
const consumed = await this.storageAdapter.consumeAuthCode(code, clientId);
|
|
683
424
|
if (!consumed || consumed.clientId !== clientId) {
|
|
684
425
|
return null;
|
|
685
426
|
}
|
|
686
427
|
return consumed;
|
|
687
428
|
}
|
|
688
429
|
async canImpersonate(params) {
|
|
689
|
-
if (this.
|
|
690
|
-
return !!(await this.
|
|
430
|
+
if (typeof this.storageAdapter.canImpersonate === 'function') {
|
|
431
|
+
return !!(await this.storageAdapter.canImpersonate(params));
|
|
691
432
|
}
|
|
692
433
|
return params.realUserId === params.effectiveUserId;
|
|
693
434
|
}
|
|
694
435
|
jwtSign(payload, secret, expiresInSeconds, options) {
|
|
436
|
+
if (!secret) {
|
|
437
|
+
return { success: false, error: 'JWT secret is not configured' };
|
|
438
|
+
}
|
|
695
439
|
return (this.tokenStoreAdapter ?? this.jwtHelper).jwtSign(payload, secret, expiresInSeconds, options);
|
|
696
440
|
}
|
|
697
441
|
jwtVerify(token, secret, options) {
|
|
@@ -702,6 +446,8 @@ class ApiServer {
|
|
|
702
446
|
}
|
|
703
447
|
async getApiKey(token) {
|
|
704
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.');
|
|
705
451
|
return null;
|
|
706
452
|
}
|
|
707
453
|
async authenticateUser(params) {
|
|
@@ -726,55 +472,34 @@ class ApiServer {
|
|
|
726
472
|
return false;
|
|
727
473
|
}
|
|
728
474
|
guessExceptionText(error, defMsg = 'Unknown Error') {
|
|
729
|
-
return
|
|
475
|
+
return (0, error_utils_js_1.guessExceptionText)(error, defMsg);
|
|
730
476
|
}
|
|
731
477
|
async authorize(apiReq, requiredClass) {
|
|
732
478
|
void apiReq;
|
|
733
|
-
|
|
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
|
+
}
|
|
734
483
|
}
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
},
|
|
751
|
-
credentials: true
|
|
752
|
-
};
|
|
753
|
-
this.app.use((0, cors_1.default)(corsOptions));
|
|
754
|
-
// Provide a consistent and non-500 response when requests are blocked by the CORS allowlist.
|
|
755
|
-
this.app.use((err, req, res, next) => {
|
|
756
|
-
const message = err instanceof Error ? err.message : '';
|
|
757
|
-
if (message.includes('Not allowed by CORS')) {
|
|
758
|
-
const isApiRequest = Boolean(req.originalUrl?.startsWith(this.apiBasePath));
|
|
759
|
-
if (isApiRequest) {
|
|
760
|
-
res.status(403).json({
|
|
761
|
-
success: false,
|
|
762
|
-
code: 403,
|
|
763
|
-
message: 'Origin not allowed by CORS',
|
|
764
|
-
data: null,
|
|
765
|
-
errors: {}
|
|
766
|
-
});
|
|
767
|
-
return;
|
|
768
|
-
}
|
|
769
|
-
res.status(403).send('Origin not allowed by CORS');
|
|
770
|
-
return;
|
|
771
|
-
}
|
|
772
|
-
next(err);
|
|
773
|
-
});
|
|
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;
|
|
774
499
|
}
|
|
775
500
|
installStaticDirs() {
|
|
776
501
|
const staticDirs = this.config.staticDirs;
|
|
777
|
-
if (!staticDirs || !isPlainObject(staticDirs)) {
|
|
502
|
+
if (!staticDirs || !(0, request_utils_js_1.isPlainObject)(staticDirs)) {
|
|
778
503
|
return;
|
|
779
504
|
}
|
|
780
505
|
for (const [mountRaw, dirRaw] of Object.entries(staticDirs)) {
|
|
@@ -784,12 +509,15 @@ class ApiServer {
|
|
|
784
509
|
continue;
|
|
785
510
|
}
|
|
786
511
|
const resolvedMount = mount.startsWith('/') ? mount : `/${mount}`;
|
|
787
|
-
this.
|
|
512
|
+
void this.fastify.register(static_1.default, {
|
|
513
|
+
root: dir,
|
|
514
|
+
prefix: resolvedMount.endsWith('/') ? resolvedMount : `${resolvedMount}/`,
|
|
515
|
+
decorateReply: false
|
|
516
|
+
});
|
|
788
517
|
}
|
|
789
518
|
}
|
|
790
519
|
installPingHandler() {
|
|
791
|
-
|
|
792
|
-
this.app.get(path, (_req, res) => {
|
|
520
|
+
this.fastify.get(`${this.apiBasePath}/v1/ping`, async () => {
|
|
793
521
|
const payload = {
|
|
794
522
|
success: true,
|
|
795
523
|
status: 'ok',
|
|
@@ -799,7 +527,7 @@ class ApiServer {
|
|
|
799
527
|
startedAt: this.startedAt,
|
|
800
528
|
timestamp: new Date().toISOString()
|
|
801
529
|
};
|
|
802
|
-
|
|
530
|
+
return { success: true, code: 200, message: 'Success', data: payload, errors: {} };
|
|
803
531
|
});
|
|
804
532
|
}
|
|
805
533
|
async loadSwaggerSpec() {
|
|
@@ -807,10 +535,11 @@ class ApiServer {
|
|
|
807
535
|
if (typeof __dirname === 'string') {
|
|
808
536
|
candidates.push(node_path_1.default.resolve(__dirname, '../../docs/swagger/openapi.json'));
|
|
809
537
|
}
|
|
538
|
+
let packageRoot;
|
|
810
539
|
try {
|
|
811
540
|
const require = (0, node_module_1.createRequire)(node_path_1.default.join(process.cwd(), 'package.json'));
|
|
812
541
|
const entry = require.resolve('@technomoron/api-server-base');
|
|
813
|
-
|
|
542
|
+
packageRoot = node_path_1.default.resolve(node_path_1.default.dirname(entry), '..', '..');
|
|
814
543
|
candidates.push(node_path_1.default.join(packageRoot, 'docs/swagger/openapi.json'));
|
|
815
544
|
}
|
|
816
545
|
catch {
|
|
@@ -825,7 +554,13 @@ class ApiServer {
|
|
|
825
554
|
}
|
|
826
555
|
try {
|
|
827
556
|
const raw = await (0, promises_1.readFile)(candidate, 'utf8');
|
|
828
|
-
|
|
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;
|
|
829
564
|
}
|
|
830
565
|
catch {
|
|
831
566
|
return null;
|
|
@@ -833,6 +568,27 @@ class ApiServer {
|
|
|
833
568
|
}
|
|
834
569
|
return null;
|
|
835
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
|
+
}
|
|
836
592
|
installSwaggerHandler() {
|
|
837
593
|
const rawPath = typeof this.config.swaggerPath === 'string' ? this.config.swaggerPath.trim() : '';
|
|
838
594
|
const enabled = Boolean(this.config.swaggerEnabled) || rawPath.length > 0;
|
|
@@ -841,31 +597,35 @@ class ApiServer {
|
|
|
841
597
|
}
|
|
842
598
|
const base = this.apiBasePath === '/' ? '' : this.apiBasePath;
|
|
843
599
|
const resolved = rawPath.length > 0 ? rawPath : `${base}/swagger`;
|
|
844
|
-
const
|
|
600
|
+
const routePath = resolved.startsWith('/') ? resolved : `/${resolved}`;
|
|
845
601
|
let specPromise;
|
|
846
|
-
this.
|
|
602
|
+
this.fastify.get(routePath, async (_request, reply) => {
|
|
847
603
|
if (!specPromise) {
|
|
848
604
|
specPromise = this.loadSwaggerSpec();
|
|
849
605
|
}
|
|
850
606
|
const spec = await specPromise;
|
|
851
607
|
if (!spec) {
|
|
852
|
-
|
|
608
|
+
// Clear cached failure so next request retries
|
|
609
|
+
specPromise = undefined;
|
|
610
|
+
}
|
|
611
|
+
if (!spec) {
|
|
612
|
+
reply.code(500);
|
|
613
|
+
return {
|
|
853
614
|
success: false,
|
|
854
615
|
code: 500,
|
|
855
616
|
message: 'Swagger spec is unavailable',
|
|
856
617
|
data: null,
|
|
857
618
|
errors: {}
|
|
858
|
-
}
|
|
859
|
-
return;
|
|
619
|
+
};
|
|
860
620
|
}
|
|
861
|
-
|
|
621
|
+
return spec;
|
|
862
622
|
});
|
|
863
623
|
}
|
|
864
|
-
normalizeApiBasePath(
|
|
865
|
-
if (!
|
|
624
|
+
normalizeApiBasePath(routePath) {
|
|
625
|
+
if (!routePath || typeof routePath !== 'string') {
|
|
866
626
|
return '/api';
|
|
867
627
|
}
|
|
868
|
-
const trimmed =
|
|
628
|
+
const trimmed = routePath.trim();
|
|
869
629
|
if (!trimmed) {
|
|
870
630
|
return '/api';
|
|
871
631
|
}
|
|
@@ -876,47 +636,121 @@ class ApiServer {
|
|
|
876
636
|
return withLeadingSlash.replace(/\/+$/, '') || '/api';
|
|
877
637
|
}
|
|
878
638
|
installApiNotFoundHandler() {
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
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({
|
|
884
647
|
success: false,
|
|
885
648
|
code: 404,
|
|
886
|
-
message:
|
|
649
|
+
message: `No such endpoint: ${method} ${target}`,
|
|
887
650
|
data: null,
|
|
888
651
|
errors: {}
|
|
889
|
-
};
|
|
890
|
-
|
|
891
|
-
};
|
|
892
|
-
this.app.use(this.apiBasePath, this.apiNotFoundHandler);
|
|
652
|
+
});
|
|
653
|
+
});
|
|
893
654
|
}
|
|
894
655
|
installApiErrorHandler() {
|
|
895
656
|
if (this.apiErrorHandlerInstalled) {
|
|
896
657
|
return;
|
|
897
658
|
}
|
|
898
659
|
this.apiErrorHandlerInstalled = true;
|
|
899
|
-
this.
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
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
|
+
});
|
|
905
739
|
}
|
|
906
740
|
start() {
|
|
907
741
|
if (!this.finalized) {
|
|
908
742
|
console.warn('[api-server-base] ApiServer.start() called without finalize(); auto-finalizing. Call server.finalize() before start() to silence this warning.');
|
|
909
743
|
this.finalize();
|
|
910
744
|
}
|
|
911
|
-
this.
|
|
745
|
+
void this.fastify
|
|
912
746
|
.listen({
|
|
913
747
|
port: this.config.apiPort,
|
|
914
748
|
host: this.config.apiHost
|
|
915
749
|
})
|
|
916
|
-
.
|
|
750
|
+
.then(() => {
|
|
917
751
|
console.log(`Server is running on http://${this.config.apiHost}:${this.config.apiPort}`);
|
|
918
752
|
})
|
|
919
|
-
.
|
|
753
|
+
.catch((error) => {
|
|
920
754
|
let message;
|
|
921
755
|
if (error.code === 'EADDRINUSE') {
|
|
922
756
|
message = `Port ${this.config.apiPort} is already in use.`;
|
|
@@ -936,13 +770,17 @@ class ApiServer {
|
|
|
936
770
|
this.config.onStartError(err);
|
|
937
771
|
return;
|
|
938
772
|
}
|
|
939
|
-
|
|
773
|
+
this.logUnhandledError('Server startup failed', err);
|
|
774
|
+
process.exitCode = 1;
|
|
940
775
|
});
|
|
941
776
|
return this;
|
|
942
777
|
}
|
|
943
778
|
internalServerErrorMessage(error) {
|
|
944
779
|
return this.config.debug ? this.guessExceptionText(error) : 'Internal server error';
|
|
945
780
|
}
|
|
781
|
+
logUnhandledError(context, error) {
|
|
782
|
+
console.error(`[ApiServer] ${context}`, error);
|
|
783
|
+
}
|
|
946
784
|
async verifyJWT(token) {
|
|
947
785
|
if (!this.config.accessSecret) {
|
|
948
786
|
return { tokenData: undefined, error: 'JWT authentication disabled; no jwtSecret set', expired: false };
|
|
@@ -951,7 +789,7 @@ class ApiServer {
|
|
|
951
789
|
if (!result.success) {
|
|
952
790
|
return { tokenData: undefined, error: result.error, expired: result.expired };
|
|
953
791
|
}
|
|
954
|
-
if (
|
|
792
|
+
if (result.data.uid == null) {
|
|
955
793
|
return { tokenData: undefined, error: 'Missing/bad userid in token', expired: false };
|
|
956
794
|
}
|
|
957
795
|
return { tokenData: result.data, error: undefined, expired: false };
|
|
@@ -994,8 +832,9 @@ class ApiServer {
|
|
|
994
832
|
return null;
|
|
995
833
|
}
|
|
996
834
|
const storedUid = String(stored.userId);
|
|
997
|
-
const verifyUid = verify.data.uid
|
|
998
|
-
|
|
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) {
|
|
999
838
|
return null;
|
|
1000
839
|
}
|
|
1001
840
|
const claims = verify.data;
|
|
@@ -1003,7 +842,6 @@ class ApiServer {
|
|
|
1003
842
|
void _exp;
|
|
1004
843
|
void _iat;
|
|
1005
844
|
void _nbf;
|
|
1006
|
-
// Ensure we never embed token secrets into refreshed access tokens.
|
|
1007
845
|
delete payload.accessToken;
|
|
1008
846
|
delete payload.refreshToken;
|
|
1009
847
|
delete payload.userId;
|
|
@@ -1042,19 +880,29 @@ class ApiServer {
|
|
|
1042
880
|
}
|
|
1043
881
|
let token = null;
|
|
1044
882
|
const authHeader = apiReq.req.headers.authorization;
|
|
883
|
+
const headerValue = Array.isArray(authHeader) ? authHeader[0] : authHeader;
|
|
884
|
+
const bearerToken = headerValue?.startsWith('Bearer ') ? headerValue.slice(7).trim() || null : null;
|
|
885
|
+
const paramToken = bearerToken ? null : this.resolveTokenFromRequest(apiReq.req);
|
|
1045
886
|
const requiresAuthToken = this.requiresAuthToken(authType);
|
|
1046
887
|
const allowRefresh = requiresAuthToken || (authType === 'maybe' && this.config.refreshMaybe);
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
888
|
+
if (this.config.apiKeyEnabled || authType === 'apikey') {
|
|
889
|
+
const apiKeyAuth = await this.tryAuthenticateApiKey(apiReq, authType, bearerToken ?? paramToken);
|
|
890
|
+
if (apiKeyAuth) {
|
|
891
|
+
return apiKeyAuth;
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
token = bearerToken ?? paramToken;
|
|
895
|
+
if (bearerToken) {
|
|
896
|
+
apiReq.authMethod = 'bearer';
|
|
1050
897
|
}
|
|
1051
|
-
if (
|
|
1052
|
-
|
|
898
|
+
else if (paramToken) {
|
|
899
|
+
apiReq.authMethod = 'param';
|
|
1053
900
|
}
|
|
1054
901
|
if (!token) {
|
|
1055
902
|
const access = apiReq.req.cookies?.[this.config.accessCookie];
|
|
1056
903
|
if (access) {
|
|
1057
904
|
token = access;
|
|
905
|
+
apiReq.authMethod = 'cookie';
|
|
1058
906
|
}
|
|
1059
907
|
}
|
|
1060
908
|
let tokenData;
|
|
@@ -1134,40 +982,82 @@ class ApiServer {
|
|
|
1134
982
|
apiReq.token = token;
|
|
1135
983
|
return tokenData;
|
|
1136
984
|
}
|
|
1137
|
-
async tryAuthenticateApiKey(apiReq, authType,
|
|
1138
|
-
if (!
|
|
985
|
+
async tryAuthenticateApiKey(apiReq, authType, tokenCandidate) {
|
|
986
|
+
if (!tokenCandidate) {
|
|
1139
987
|
if (authType === 'apikey') {
|
|
1140
988
|
throw new ApiError({ code: 401, message: 'Authorization header is missing or invalid' });
|
|
1141
989
|
}
|
|
1142
990
|
return null;
|
|
1143
991
|
}
|
|
1144
|
-
const keyToken =
|
|
1145
|
-
|
|
992
|
+
const keyToken = tokenCandidate;
|
|
993
|
+
const prefix = this.config.apiKeyPrefix;
|
|
994
|
+
const secret = prefix === '' ? keyToken : keyToken.startsWith(prefix) ? keyToken.slice(prefix.length) : null;
|
|
995
|
+
if (!secret) {
|
|
1146
996
|
if (authType === 'apikey') {
|
|
1147
997
|
throw new ApiError({ code: 401, message: 'Invalid API Key' });
|
|
1148
998
|
}
|
|
1149
999
|
return null;
|
|
1150
1000
|
}
|
|
1151
|
-
const secret = keyToken.replace(/^apikey-/, '');
|
|
1152
1001
|
const key = await this.getApiKey(secret);
|
|
1153
1002
|
if (!key) {
|
|
1154
|
-
|
|
1003
|
+
if (authType === 'apikey') {
|
|
1004
|
+
throw new ApiError({ code: 401, message: 'Invalid API Key' });
|
|
1005
|
+
}
|
|
1006
|
+
return null;
|
|
1155
1007
|
}
|
|
1156
1008
|
apiReq.token = secret;
|
|
1157
1009
|
apiReq.apiKey = key;
|
|
1158
|
-
|
|
1159
|
-
const
|
|
1160
|
-
|
|
1161
|
-
|
|
1010
|
+
apiReq.authMethod = 'apikey';
|
|
1011
|
+
const resolvedRuid = this.normalizeAuthIdentifier(key.uid);
|
|
1012
|
+
const resolvedEuid = key.euid !== undefined ? this.normalizeAuthIdentifier(key.euid) : null;
|
|
1013
|
+
const effectiveUid = resolvedEuid ?? resolvedRuid;
|
|
1014
|
+
if (resolvedRuid !== null) {
|
|
1015
|
+
apiReq.realUid = resolvedRuid;
|
|
1162
1016
|
}
|
|
1163
1017
|
return {
|
|
1164
|
-
uid:
|
|
1018
|
+
uid: effectiveUid,
|
|
1019
|
+
...(resolvedEuid !== null ? { ruid: resolvedRuid !== null ? String(resolvedRuid) : undefined } : {}),
|
|
1165
1020
|
domain: '',
|
|
1166
1021
|
fingerprint: '',
|
|
1167
1022
|
iat: 0,
|
|
1168
1023
|
exp: 0
|
|
1169
1024
|
};
|
|
1170
1025
|
}
|
|
1026
|
+
resolveTokenFromRequest(req) {
|
|
1027
|
+
const paramName = this.config.tokenParam.trim();
|
|
1028
|
+
if (!paramName) {
|
|
1029
|
+
return null;
|
|
1030
|
+
}
|
|
1031
|
+
const location = this.config.tokenParamLocation;
|
|
1032
|
+
if (location === 'body') {
|
|
1033
|
+
return this.readNamedValue(req.body, paramName);
|
|
1034
|
+
}
|
|
1035
|
+
if (location === 'query') {
|
|
1036
|
+
return this.readNamedValue(req.query, paramName);
|
|
1037
|
+
}
|
|
1038
|
+
return this.readNamedValue(req.body, paramName) ?? this.readNamedValue(req.query, paramName);
|
|
1039
|
+
}
|
|
1040
|
+
readNamedValue(container, key) {
|
|
1041
|
+
if (!container || typeof container !== 'object' || Array.isArray(container)) {
|
|
1042
|
+
return null;
|
|
1043
|
+
}
|
|
1044
|
+
const raw = container[key];
|
|
1045
|
+
if (typeof raw === 'string') {
|
|
1046
|
+
const token = raw.trim();
|
|
1047
|
+
return token.length > 0 ? token : null;
|
|
1048
|
+
}
|
|
1049
|
+
if (Array.isArray(raw)) {
|
|
1050
|
+
for (const entry of raw) {
|
|
1051
|
+
if (typeof entry === 'string') {
|
|
1052
|
+
const token = entry.trim();
|
|
1053
|
+
if (token.length > 0) {
|
|
1054
|
+
return token;
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
return null;
|
|
1060
|
+
}
|
|
1171
1061
|
requiresAuthToken(authType) {
|
|
1172
1062
|
return authType === 'yes' || authType === 'strict';
|
|
1173
1063
|
}
|
|
@@ -1215,21 +1105,25 @@ class ApiServer {
|
|
|
1215
1105
|
}
|
|
1216
1106
|
return rawReal;
|
|
1217
1107
|
}
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
}
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
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
|
+
};
|
|
1233
1127
|
}
|
|
1234
1128
|
createApiRequest(req, res) {
|
|
1235
1129
|
const apiReq = {
|
|
@@ -1238,10 +1132,11 @@ class ApiServer {
|
|
|
1238
1132
|
res,
|
|
1239
1133
|
token: '',
|
|
1240
1134
|
tokenData: null,
|
|
1135
|
+
authMethod: null,
|
|
1241
1136
|
realUid: null,
|
|
1242
|
-
getClientInfo: () => ensureClientInfo(apiReq),
|
|
1243
|
-
getClientIp: () => ensureClientInfo(apiReq).ip,
|
|
1244
|
-
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,
|
|
1245
1140
|
getRealUid: () => apiReq.realUid ?? null,
|
|
1246
1141
|
isImpersonating: () => {
|
|
1247
1142
|
const realUid = apiReq.realUid;
|
|
@@ -1252,11 +1147,101 @@ class ApiServer {
|
|
|
1252
1147
|
if (tokenUid === null || tokenUid === undefined) {
|
|
1253
1148
|
return false;
|
|
1254
1149
|
}
|
|
1255
|
-
|
|
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);
|
|
1256
1153
|
}
|
|
1257
1154
|
};
|
|
1258
1155
|
return apiReq;
|
|
1259
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
|
+
}
|
|
1260
1245
|
expressAuth(auth) {
|
|
1261
1246
|
return async (req, res, next) => {
|
|
1262
1247
|
const apiReq = this.createApiRequest(req, res);
|
|
@@ -1264,7 +1249,7 @@ class ApiServer {
|
|
|
1264
1249
|
res.locals.apiReq = apiReq;
|
|
1265
1250
|
try {
|
|
1266
1251
|
if (this.config.hydrateGetBody) {
|
|
1267
|
-
hydrateGetBody(req);
|
|
1252
|
+
(0, request_utils_js_1.hydrateGetBody)(req);
|
|
1268
1253
|
}
|
|
1269
1254
|
if (this.config.debug) {
|
|
1270
1255
|
this.dumpRequest(apiReq);
|
|
@@ -1285,7 +1270,7 @@ class ApiServer {
|
|
|
1285
1270
|
next(error);
|
|
1286
1271
|
return;
|
|
1287
1272
|
}
|
|
1288
|
-
if (error instanceof ApiError || isApiErrorLike(error)) {
|
|
1273
|
+
if (error instanceof ApiError || (0, error_utils_js_1.isApiErrorLike)(error)) {
|
|
1289
1274
|
const apiError = error;
|
|
1290
1275
|
const normalizedErrors = apiError.errors && typeof apiError.errors === 'object' && !Array.isArray(apiError.errors)
|
|
1291
1276
|
? apiError.errors
|
|
@@ -1300,8 +1285,11 @@ class ApiServer {
|
|
|
1300
1285
|
res.status(apiError.code).json(errorPayload);
|
|
1301
1286
|
return;
|
|
1302
1287
|
}
|
|
1303
|
-
const status = asHttpStatus(error);
|
|
1288
|
+
const status = (0, error_utils_js_1.asHttpStatus)(error);
|
|
1304
1289
|
if (status) {
|
|
1290
|
+
if (status >= 500) {
|
|
1291
|
+
this.logUnhandledError(`Unhandled compat middleware error mapped to HTTP ${status}`, error);
|
|
1292
|
+
}
|
|
1305
1293
|
res.status(status).json({
|
|
1306
1294
|
success: false,
|
|
1307
1295
|
code: status,
|
|
@@ -1311,23 +1299,24 @@ class ApiServer {
|
|
|
1311
1299
|
});
|
|
1312
1300
|
return;
|
|
1313
1301
|
}
|
|
1314
|
-
|
|
1302
|
+
this.logUnhandledError('Unhandled compat middleware error fallback to HTTP 500', error);
|
|
1303
|
+
res.status(500).json({
|
|
1315
1304
|
success: false,
|
|
1316
1305
|
code: 500,
|
|
1317
1306
|
message: this.internalServerErrorMessage(error),
|
|
1318
1307
|
data: null,
|
|
1319
1308
|
errors: {}
|
|
1320
|
-
};
|
|
1321
|
-
res.status(500).json(errorPayload);
|
|
1309
|
+
});
|
|
1322
1310
|
};
|
|
1323
1311
|
}
|
|
1324
|
-
|
|
1325
|
-
return async (
|
|
1326
|
-
|
|
1312
|
+
handleRequest(handler, auth) {
|
|
1313
|
+
return async (request, reply) => {
|
|
1314
|
+
const req = this.toExtendedReq(request);
|
|
1315
|
+
const res = new FastifyResponseAdapter(reply);
|
|
1327
1316
|
const apiReq = this.createApiRequest(req, res);
|
|
1328
1317
|
try {
|
|
1329
1318
|
if (this.config.hydrateGetBody) {
|
|
1330
|
-
hydrateGetBody(apiReq.req);
|
|
1319
|
+
(0, request_utils_js_1.hydrateGetBody)(apiReq.req);
|
|
1331
1320
|
}
|
|
1332
1321
|
if (this.config.debug) {
|
|
1333
1322
|
this.dumpRequest(apiReq);
|
|
@@ -1346,14 +1335,14 @@ class ApiServer {
|
|
|
1346
1335
|
throw new ApiError({ code: 500, message: 'Handler result must start with a numeric status code' });
|
|
1347
1336
|
}
|
|
1348
1337
|
const message = typeof rawMessage === 'string' ? rawMessage : 'Success';
|
|
1349
|
-
const responsePayload = { success:
|
|
1338
|
+
const responsePayload = { success: code < 400, code, message, data, errors: {} };
|
|
1350
1339
|
if (this.config.debug) {
|
|
1351
1340
|
this.dumpResponse(apiReq, responsePayload, code);
|
|
1352
1341
|
}
|
|
1353
|
-
|
|
1342
|
+
reply.code(code).send(responsePayload);
|
|
1354
1343
|
}
|
|
1355
1344
|
catch (error) {
|
|
1356
|
-
if (error instanceof ApiError || isApiErrorLike(error)) {
|
|
1345
|
+
if (error instanceof ApiError || (0, error_utils_js_1.isApiErrorLike)(error)) {
|
|
1357
1346
|
const apiError = error;
|
|
1358
1347
|
const normalizedErrors = apiError.errors && typeof apiError.errors === 'object' && !Array.isArray(apiError.errors)
|
|
1359
1348
|
? apiError.errors
|
|
@@ -1368,27 +1357,53 @@ class ApiServer {
|
|
|
1368
1357
|
if (this.config.debug) {
|
|
1369
1358
|
this.dumpResponse(apiReq, errorPayload, apiError.code);
|
|
1370
1359
|
}
|
|
1371
|
-
|
|
1360
|
+
reply.code(apiError.code).send(errorPayload);
|
|
1372
1361
|
}
|
|
1373
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);
|
|
1374
1370
|
const errorPayload = {
|
|
1375
1371
|
success: false,
|
|
1376
|
-
code:
|
|
1377
|
-
message:
|
|
1372
|
+
code: uploadTooLarge ? 413 : status,
|
|
1373
|
+
message: uploadTooLarge
|
|
1374
|
+
? `Upload exceeds maximum size of ${this.config.uploadMax} bytes`
|
|
1375
|
+
: this.internalServerErrorMessage(error),
|
|
1378
1376
|
data: null,
|
|
1379
1377
|
errors: {}
|
|
1380
1378
|
};
|
|
1381
1379
|
if (this.config.debug) {
|
|
1382
|
-
this.dumpResponse(apiReq, errorPayload,
|
|
1380
|
+
this.dumpResponse(apiReq, errorPayload, uploadTooLarge ? 413 : status);
|
|
1383
1381
|
}
|
|
1384
|
-
|
|
1382
|
+
reply.code(uploadTooLarge ? 413 : status).send(errorPayload);
|
|
1385
1383
|
}
|
|
1386
1384
|
}
|
|
1387
1385
|
};
|
|
1388
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);
|
|
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);
|
|
1402
|
+
}
|
|
1403
|
+
};
|
|
1404
|
+
}
|
|
1389
1405
|
api(module) {
|
|
1390
1406
|
this.assertNotFinalized('api');
|
|
1391
|
-
const router = express_1.default.Router();
|
|
1392
1407
|
module.server = this;
|
|
1393
1408
|
const moduleType = module.moduleType;
|
|
1394
1409
|
if (moduleType === 'auth') {
|
|
@@ -1399,44 +1414,51 @@ class ApiServer {
|
|
|
1399
1414
|
const name = module.constructor?.name ? String(module.constructor.name) : 'ApiModule';
|
|
1400
1415
|
throw new Error(`${name}.checkConfig() returned false`);
|
|
1401
1416
|
}
|
|
1402
|
-
|
|
1417
|
+
module.onMount();
|
|
1403
1418
|
const ns = module.namespace;
|
|
1404
|
-
const mountPath = `${
|
|
1419
|
+
const mountPath = `${this.apiBasePath}${ns}`;
|
|
1405
1420
|
module.mountpath = mountPath;
|
|
1406
|
-
module.defineRoutes()
|
|
1407
|
-
const handler = this.
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
break;
|
|
1412
|
-
case 'post':
|
|
1413
|
-
router.post(r.path, handler);
|
|
1414
|
-
break;
|
|
1415
|
-
case 'put':
|
|
1416
|
-
router.put(r.path, handler);
|
|
1417
|
-
break;
|
|
1418
|
-
case 'patch':
|
|
1419
|
-
router.patch(r.path, handler);
|
|
1420
|
-
break;
|
|
1421
|
-
case 'delete':
|
|
1422
|
-
router.delete(r.path, handler);
|
|
1423
|
-
break;
|
|
1424
|
-
}
|
|
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 } : {}) });
|
|
1425
1426
|
if (this.config.debug) {
|
|
1426
|
-
console.log(`Adding ${
|
|
1427
|
+
console.log(`Adding ${fullPath} (${method})`);
|
|
1427
1428
|
}
|
|
1428
|
-
}
|
|
1429
|
-
this.apiRouter.use(ns, router);
|
|
1429
|
+
}
|
|
1430
1430
|
return this;
|
|
1431
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
|
+
}
|
|
1432
1444
|
dumpRequest(apiReq) {
|
|
1433
1445
|
const req = apiReq.req;
|
|
1446
|
+
const tokenParam = this.config.tokenParam.trim();
|
|
1434
1447
|
console.log('--- Incoming Request! ---');
|
|
1435
1448
|
const url = req.originalUrl || req.url;
|
|
1436
1449
|
console.log('URL:', url);
|
|
1437
1450
|
console.log('Method:', req.method);
|
|
1438
|
-
|
|
1451
|
+
if (tokenParam && req.query && tokenParam in req.query) {
|
|
1452
|
+
const maskedQuery = { ...req.query, [tokenParam]: '[REDACTED]' };
|
|
1453
|
+
console.log('Query Params:', maskedQuery);
|
|
1454
|
+
}
|
|
1455
|
+
else {
|
|
1456
|
+
console.log('Query Params:', req.query || {});
|
|
1457
|
+
}
|
|
1439
1458
|
const sensitiveBodyKeys = ['password', 'client_secret', 'clientSecret', 'secret'];
|
|
1459
|
+
if (tokenParam) {
|
|
1460
|
+
sensitiveBodyKeys.push(tokenParam);
|
|
1461
|
+
}
|
|
1440
1462
|
const body = req.body && typeof req.body === 'object' ? { ...req.body } : req.body;
|
|
1441
1463
|
if (body && typeof body === 'object') {
|
|
1442
1464
|
for (const key of sensitiveBodyKeys) {
|
|
@@ -1458,6 +1480,12 @@ class ApiServer {
|
|
|
1458
1480
|
if (headers.authorization) {
|
|
1459
1481
|
headers.authorization = '[REDACTED]';
|
|
1460
1482
|
}
|
|
1483
|
+
if (headers['x-api-key']) {
|
|
1484
|
+
headers['x-api-key'] = '[REDACTED]';
|
|
1485
|
+
}
|
|
1486
|
+
if (headers.cookie) {
|
|
1487
|
+
headers.cookie = '[REDACTED]';
|
|
1488
|
+
}
|
|
1461
1489
|
console.log('Headers:', headers);
|
|
1462
1490
|
console.log('------------------------');
|
|
1463
1491
|
}
|
|
@@ -1507,7 +1535,21 @@ class ApiServer {
|
|
|
1507
1535
|
return value;
|
|
1508
1536
|
}
|
|
1509
1537
|
dumpResponse(apiReq, payload, status) {
|
|
1510
|
-
const
|
|
1538
|
+
const rawUrl = apiReq.req.originalUrl || apiReq.req.url;
|
|
1539
|
+
const tokenParam = this.config.tokenParam.trim();
|
|
1540
|
+
let url = rawUrl;
|
|
1541
|
+
if (tokenParam && rawUrl) {
|
|
1542
|
+
try {
|
|
1543
|
+
const parsed = new URL(rawUrl, 'http://x');
|
|
1544
|
+
if (parsed.searchParams.has(tokenParam)) {
|
|
1545
|
+
parsed.searchParams.set(tokenParam, '[REDACTED]');
|
|
1546
|
+
url = parsed.pathname + '?' + parsed.searchParams.toString();
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
catch {
|
|
1550
|
+
// Leave url unmodified if parsing fails
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1511
1553
|
console.log('--- Outgoing Response! ---');
|
|
1512
1554
|
console.log('URL:', url);
|
|
1513
1555
|
console.log('Status:', status);
|