@technomoron/apicore-server 1.0.0-beta.1

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.
Files changed (171) hide show
  1. package/LICENSE +21 -0
  2. package/dist/cjs/api-module.cjs +34 -0
  3. package/dist/cjs/api-module.d.ts +45 -0
  4. package/dist/cjs/apicore-server.cjs +1561 -0
  5. package/dist/cjs/apicore-server.d.ts +288 -0
  6. package/dist/cjs/auth-api/auth-module.cjs +1248 -0
  7. package/dist/cjs/auth-api/auth-module.d.ts +116 -0
  8. package/dist/cjs/auth-api/compat-auth-storage.cjs +128 -0
  9. package/dist/cjs/auth-api/compat-auth-storage.d.ts +57 -0
  10. package/dist/cjs/auth-api/mem-auth-store.cjs +121 -0
  11. package/dist/cjs/auth-api/mem-auth-store.d.ts +68 -0
  12. package/dist/cjs/auth-api/module.cjs +25 -0
  13. package/dist/cjs/auth-api/module.d.ts +20 -0
  14. package/dist/cjs/auth-api/schemas.cjs +171 -0
  15. package/dist/cjs/auth-api/schemas.d.ts +21 -0
  16. package/dist/cjs/auth-api/sql-auth-store.cjs +179 -0
  17. package/dist/cjs/auth-api/sql-auth-store.d.ts +87 -0
  18. package/dist/cjs/auth-api/storage.cjs +102 -0
  19. package/dist/cjs/auth-api/storage.d.ts +38 -0
  20. package/dist/cjs/auth-api/types.cjs +2 -0
  21. package/dist/cjs/auth-api/types.d.ts +34 -0
  22. package/dist/cjs/auth-api/user-id.cjs +47 -0
  23. package/dist/cjs/auth-api/user-id.d.ts +5 -0
  24. package/dist/cjs/auth-cookie-options.cjs +66 -0
  25. package/dist/cjs/auth-cookie-options.d.ts +13 -0
  26. package/dist/cjs/base/client-info.cjs +285 -0
  27. package/dist/cjs/base/client-info.d.ts +27 -0
  28. package/dist/cjs/base/error-utils.cjs +50 -0
  29. package/dist/cjs/base/error-utils.d.ts +16 -0
  30. package/dist/cjs/base/request-utils.cjs +27 -0
  31. package/dist/cjs/base/request-utils.d.ts +8 -0
  32. package/dist/cjs/index.cjs +51 -0
  33. package/dist/cjs/index.d.ts +34 -0
  34. package/dist/cjs/limiter/auth-rate-limiter.cjs +35 -0
  35. package/dist/cjs/limiter/auth-rate-limiter.d.ts +12 -0
  36. package/dist/cjs/limiter/fixed-window.cjs +41 -0
  37. package/dist/cjs/limiter/fixed-window.d.ts +11 -0
  38. package/dist/cjs/oauth/base.cjs +7 -0
  39. package/dist/cjs/oauth/base.d.ts +17 -0
  40. package/dist/cjs/oauth/memory.cjs +135 -0
  41. package/dist/cjs/oauth/memory.d.ts +22 -0
  42. package/dist/cjs/oauth/models.cjs +47 -0
  43. package/dist/cjs/oauth/models.d.ts +50 -0
  44. package/dist/cjs/oauth/sequelize.cjs +159 -0
  45. package/dist/cjs/oauth/sequelize.d.ts +30 -0
  46. package/dist/cjs/oauth/types.cjs +3 -0
  47. package/dist/cjs/oauth/types.d.ts +51 -0
  48. package/dist/cjs/passkey/base.cjs +7 -0
  49. package/dist/cjs/passkey/base.d.ts +28 -0
  50. package/dist/cjs/passkey/config.cjs +26 -0
  51. package/dist/cjs/passkey/config.d.ts +2 -0
  52. package/dist/cjs/passkey/memory.cjs +123 -0
  53. package/dist/cjs/passkey/memory.d.ts +34 -0
  54. package/dist/cjs/passkey/models.cjs +142 -0
  55. package/dist/cjs/passkey/models.d.ts +34 -0
  56. package/dist/cjs/passkey/sequelize.cjs +126 -0
  57. package/dist/cjs/passkey/sequelize.d.ts +42 -0
  58. package/dist/cjs/passkey/service.cjs +413 -0
  59. package/dist/cjs/passkey/service.d.ts +21 -0
  60. package/dist/cjs/passkey/types.cjs +2 -0
  61. package/dist/cjs/passkey/types.d.ts +84 -0
  62. package/dist/cjs/sequelize-utils.cjs +56 -0
  63. package/dist/cjs/sequelize-utils.d.ts +8 -0
  64. package/dist/cjs/token/base.cjs +120 -0
  65. package/dist/cjs/token/base.d.ts +46 -0
  66. package/dist/cjs/token/memory.cjs +234 -0
  67. package/dist/cjs/token/memory.d.ts +29 -0
  68. package/dist/cjs/token/sequelize.cjs +400 -0
  69. package/dist/cjs/token/sequelize.d.ts +58 -0
  70. package/dist/cjs/token/types.cjs +2 -0
  71. package/dist/cjs/token/types.d.ts +34 -0
  72. package/dist/cjs/upload/memory.cjs +92 -0
  73. package/dist/cjs/upload/memory.d.ts +17 -0
  74. package/dist/cjs/upload/tus-module.cjs +270 -0
  75. package/dist/cjs/upload/tus-module.d.ts +38 -0
  76. package/dist/cjs/upload/types.cjs +2 -0
  77. package/dist/cjs/upload/types.d.ts +28 -0
  78. package/dist/cjs/user/base.cjs +53 -0
  79. package/dist/cjs/user/base.d.ts +36 -0
  80. package/dist/cjs/user/memory.cjs +194 -0
  81. package/dist/cjs/user/memory.d.ts +37 -0
  82. package/dist/cjs/user/sequelize.cjs +194 -0
  83. package/dist/cjs/user/sequelize.d.ts +46 -0
  84. package/dist/cjs/user/types.cjs +2 -0
  85. package/dist/cjs/user/types.d.ts +11 -0
  86. package/dist/esm/api-module.d.ts +45 -0
  87. package/dist/esm/api-module.js +30 -0
  88. package/dist/esm/apicore-server.d.ts +288 -0
  89. package/dist/esm/apicore-server.js +1552 -0
  90. package/dist/esm/auth-api/auth-module.d.ts +116 -0
  91. package/dist/esm/auth-api/auth-module.js +1246 -0
  92. package/dist/esm/auth-api/compat-auth-storage.d.ts +57 -0
  93. package/dist/esm/auth-api/compat-auth-storage.js +124 -0
  94. package/dist/esm/auth-api/mem-auth-store.d.ts +68 -0
  95. package/dist/esm/auth-api/mem-auth-store.js +117 -0
  96. package/dist/esm/auth-api/module.d.ts +20 -0
  97. package/dist/esm/auth-api/module.js +21 -0
  98. package/dist/esm/auth-api/schemas.d.ts +21 -0
  99. package/dist/esm/auth-api/schemas.js +168 -0
  100. package/dist/esm/auth-api/sql-auth-store.d.ts +87 -0
  101. package/dist/esm/auth-api/sql-auth-store.js +175 -0
  102. package/dist/esm/auth-api/storage.d.ts +38 -0
  103. package/dist/esm/auth-api/storage.js +98 -0
  104. package/dist/esm/auth-api/types.d.ts +34 -0
  105. package/dist/esm/auth-api/types.js +1 -0
  106. package/dist/esm/auth-api/user-id.d.ts +5 -0
  107. package/dist/esm/auth-api/user-id.js +41 -0
  108. package/dist/esm/auth-cookie-options.d.ts +13 -0
  109. package/dist/esm/auth-cookie-options.js +63 -0
  110. package/dist/esm/base/client-info.d.ts +27 -0
  111. package/dist/esm/base/client-info.js +282 -0
  112. package/dist/esm/base/error-utils.d.ts +16 -0
  113. package/dist/esm/base/error-utils.js +44 -0
  114. package/dist/esm/base/request-utils.d.ts +8 -0
  115. package/dist/esm/base/request-utils.js +23 -0
  116. package/dist/esm/index.d.ts +34 -0
  117. package/dist/esm/index.js +21 -0
  118. package/dist/esm/limiter/auth-rate-limiter.d.ts +12 -0
  119. package/dist/esm/limiter/auth-rate-limiter.js +32 -0
  120. package/dist/esm/limiter/fixed-window.d.ts +11 -0
  121. package/dist/esm/limiter/fixed-window.js +37 -0
  122. package/dist/esm/oauth/base.d.ts +17 -0
  123. package/dist/esm/oauth/base.js +3 -0
  124. package/dist/esm/oauth/memory.d.ts +22 -0
  125. package/dist/esm/oauth/memory.js +128 -0
  126. package/dist/esm/oauth/models.d.ts +50 -0
  127. package/dist/esm/oauth/models.js +38 -0
  128. package/dist/esm/oauth/sequelize.d.ts +30 -0
  129. package/dist/esm/oauth/sequelize.js +148 -0
  130. package/dist/esm/oauth/types.d.ts +51 -0
  131. package/dist/esm/oauth/types.js +2 -0
  132. package/dist/esm/passkey/base.d.ts +28 -0
  133. package/dist/esm/passkey/base.js +3 -0
  134. package/dist/esm/passkey/config.d.ts +2 -0
  135. package/dist/esm/passkey/config.js +23 -0
  136. package/dist/esm/passkey/memory.d.ts +34 -0
  137. package/dist/esm/passkey/memory.js +119 -0
  138. package/dist/esm/passkey/models.d.ts +34 -0
  139. package/dist/esm/passkey/models.js +135 -0
  140. package/dist/esm/passkey/sequelize.d.ts +42 -0
  141. package/dist/esm/passkey/sequelize.js +122 -0
  142. package/dist/esm/passkey/service.d.ts +21 -0
  143. package/dist/esm/passkey/service.js +376 -0
  144. package/dist/esm/passkey/types.d.ts +84 -0
  145. package/dist/esm/passkey/types.js +1 -0
  146. package/dist/esm/sequelize-utils.d.ts +8 -0
  147. package/dist/esm/sequelize-utils.js +47 -0
  148. package/dist/esm/token/base.d.ts +46 -0
  149. package/dist/esm/token/base.js +113 -0
  150. package/dist/esm/token/memory.d.ts +29 -0
  151. package/dist/esm/token/memory.js +230 -0
  152. package/dist/esm/token/sequelize.d.ts +58 -0
  153. package/dist/esm/token/sequelize.js +396 -0
  154. package/dist/esm/token/types.d.ts +34 -0
  155. package/dist/esm/token/types.js +1 -0
  156. package/dist/esm/upload/memory.d.ts +17 -0
  157. package/dist/esm/upload/memory.js +86 -0
  158. package/dist/esm/upload/tus-module.d.ts +38 -0
  159. package/dist/esm/upload/tus-module.js +266 -0
  160. package/dist/esm/upload/types.d.ts +28 -0
  161. package/dist/esm/upload/types.js +1 -0
  162. package/dist/esm/user/base.d.ts +36 -0
  163. package/dist/esm/user/base.js +46 -0
  164. package/dist/esm/user/memory.d.ts +37 -0
  165. package/dist/esm/user/memory.js +190 -0
  166. package/dist/esm/user/sequelize.d.ts +46 -0
  167. package/dist/esm/user/sequelize.js +188 -0
  168. package/dist/esm/user/types.d.ts +11 -0
  169. package/dist/esm/user/types.js +1 -0
  170. package/docs/swagger/openapi.json +2162 -0
  171. package/package.json +131 -0
@@ -0,0 +1,1561 @@
1
+ "use strict";
2
+ /**
3
+ * Copyright (c) 2025 Bjørn Erik Jacobsen / Technomoron
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+ var __importDefault = (this && this.__importDefault) || function (mod) {
9
+ return (mod && mod.__esModule) ? mod : { "default": mod };
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.ApiServer = exports.ApiError = exports.ApiModule = void 0;
13
+ const promises_1 = require("node:fs/promises");
14
+ const node_module_1 = require("node:module");
15
+ const node_path_1 = __importDefault(require("node:path"));
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
+ }
60
+ class JwtHelperStore extends base_js_1.TokenStore {
61
+ async save() {
62
+ throw new Error('Token store is not configured');
63
+ }
64
+ async get() {
65
+ throw new Error('Token store is not configured');
66
+ }
67
+ async delete() {
68
+ throw new Error('Token store is not configured');
69
+ }
70
+ async update() {
71
+ throw new Error('Token store is not configured');
72
+ }
73
+ async list() {
74
+ return [];
75
+ }
76
+ async close() {
77
+ return;
78
+ }
79
+ }
80
+ var api_module_js_1 = require("./api-module.cjs");
81
+ Object.defineProperty(exports, "ApiModule", { enumerable: true, get: function () { return api_module_js_1.ApiModule; } });
82
+ class ApiError extends Error {
83
+ constructor({ code, message, data, errors }) {
84
+ const msg = (0, error_utils_js_1.guessExceptionText)(message, '[Unknown error (null/undefined)]');
85
+ super(msg);
86
+ this.message = msg;
87
+ this.code = typeof code === 'number' ? code : 500;
88
+ this.data = data !== undefined ? data : null;
89
+ this.errors = errors !== undefined ? errors : {};
90
+ }
91
+ }
92
+ exports.ApiError = ApiError;
93
+ function fillConfig(config) {
94
+ return {
95
+ apiPort: config.apiPort ?? 3101,
96
+ apiHost: config.apiHost ?? 'localhost',
97
+ uploadPath: config.uploadPath ?? '',
98
+ uploadMax: config.uploadMax ?? 30 * 1024 * 1024,
99
+ staticDirs: config.staticDirs,
100
+ origins: config.origins ?? [],
101
+ debug: config.debug ?? false,
102
+ apiBasePath: config.apiBasePath ?? '/api',
103
+ swaggerEnabled: config.swaggerEnabled ?? false,
104
+ swaggerPath: config.swaggerPath ?? '',
105
+ accessSecret: config.accessSecret ?? '',
106
+ refreshSecret: config.refreshSecret ?? '',
107
+ cookieDomain: config.cookieDomain ?? '',
108
+ cookiePath: config.cookiePath ?? '/',
109
+ cookieSameSite: config.cookieSameSite ?? 'lax',
110
+ cookieSecure: config.cookieSecure ?? 'auto',
111
+ cookieHttpOnly: config.cookieHttpOnly ?? true,
112
+ apiKeyPrefix: config.apiKeyPrefix ?? '',
113
+ apiKeyEnabled: config.apiKeyEnabled ?? false,
114
+ tokenParam: config.tokenParam ?? '',
115
+ tokenParamLocation: config.tokenParamLocation ?? 'body',
116
+ accessCookie: config.accessCookie ?? 'dat',
117
+ refreshCookie: config.refreshCookie ?? 'drt',
118
+ accessExpiry: config.accessExpiry ?? 60 * 15,
119
+ refreshExpiry: config.refreshExpiry ?? 30 * 24 * 60 * 60,
120
+ sessionRefreshExpiry: config.sessionRefreshExpiry ?? 24 * 60 * 60,
121
+ authApi: config.authApi ?? false,
122
+ devMode: config.devMode ?? false,
123
+ hydrateGetBody: config.hydrateGetBody ?? true,
124
+ validateTokens: config.validateTokens ?? false,
125
+ refreshMaybe: config.refreshMaybe ?? false,
126
+ apiVersion: config.apiVersion ?? '',
127
+ minClientVersion: config.minClientVersion ?? '',
128
+ tokenStore: config.tokenStore,
129
+ onStartError: config.onStartError,
130
+ trustProxy: config.trustProxy ?? true
131
+ };
132
+ }
133
+ /** Core Fastify-based API server with module mounting and auth integration hooks. */
134
+ class ApiServer {
135
+ get currReq() {
136
+ return null;
137
+ }
138
+ set currReq(_value) {
139
+ if (this.config.devMode && !this.currReqDeprecationWarned) {
140
+ this.currReqDeprecationWarned = true;
141
+ console.warn('[apicore] ApiServer.currReq is deprecated and always null. Use per-request ApiRequest in handlers.');
142
+ }
143
+ void _value;
144
+ }
145
+ constructor(config = {}) {
146
+ this.finalized = false;
147
+ this.apiErrorHandlerInstalled = false;
148
+ this.tokenStoreAdapter = null;
149
+ this.compatGlobalErrorHandler = null;
150
+ this.currReqDeprecationWarned = false;
151
+ this.config = fillConfig(config);
152
+ this.apiBasePath = this.normalizeApiBasePath(this.config.apiBasePath);
153
+ this.startedAt = Date.now();
154
+ this.storageAdapter = storage_js_1.nullAuthAdapter;
155
+ this.moduleAdapter = module_js_1.nullAuthModule;
156
+ this.jwtHelper = new JwtHelperStore();
157
+ this.tokenStoreAdapter = this.config.tokenStore ?? null;
158
+ if (this.config.authApi && (!this.config.accessSecret || !this.config.refreshSecret)) {
159
+ console.warn('[apicore] Auth is enabled but accessSecret and/or refreshSecret are empty. JWT signing will fail at runtime.');
160
+ }
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);
181
+ }
182
+ });
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
+ });
246
+ this.installStaticDirs();
247
+ this.installPingHandler();
248
+ this.installSwaggerHandler();
249
+ this.installApiNotFoundHandler();
250
+ this.installApiErrorHandler();
251
+ }
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);
262
+ }
263
+ return Buffer.concat(chunks).toString('utf8');
264
+ }
265
+ parseRawBody(raw, contentType) {
266
+ if (!raw) {
267
+ return {};
268
+ }
269
+ if (contentType.includes('application/json')) {
270
+ try {
271
+ return JSON.parse(raw);
272
+ }
273
+ catch {
274
+ return {};
275
+ }
276
+ }
277
+ if (contentType.includes('application/x-www-form-urlencoded')) {
278
+ const params = new URLSearchParams(raw);
279
+ return Object.fromEntries(params.entries());
280
+ }
281
+ return raw;
282
+ }
283
+ assertNotFinalized(action) {
284
+ if (this.finalized) {
285
+ throw new Error(`Cannot call ApiServer.${action}() after finalize()/start()`);
286
+ }
287
+ }
288
+ finalize() {
289
+ this.finalized = true;
290
+ return this;
291
+ }
292
+ authStorage(storage) {
293
+ this.assertNotFinalized('authStorage');
294
+ this.storageAdapter = storage;
295
+ return this;
296
+ }
297
+ useAuthStorage(storage) {
298
+ return this.authStorage(storage);
299
+ }
300
+ authModule(module) {
301
+ this.assertNotFinalized('authModule');
302
+ this.moduleAdapter = module;
303
+ return this;
304
+ }
305
+ useAuthModule(module) {
306
+ return this.authModule(module);
307
+ }
308
+ getAuthStorage() {
309
+ return this.storageAdapter;
310
+ }
311
+ getAuthModule() {
312
+ return this.moduleAdapter;
313
+ }
314
+ setTokenStore(store) {
315
+ this.assertNotFinalized('setTokenStore');
316
+ this.tokenStoreAdapter = store;
317
+ return this;
318
+ }
319
+ getTokenStore() {
320
+ return this.tokenStoreAdapter;
321
+ }
322
+ async listUserCredentials(userId) {
323
+ if (typeof this.storageAdapter.listUserCredentials !== 'function') {
324
+ throw new Error('Passkey service is not configured');
325
+ }
326
+ return this.storageAdapter.listUserCredentials(userId);
327
+ }
328
+ async deletePasskeyCredential(credentialId) {
329
+ if (typeof this.storageAdapter.deletePasskeyCredential !== 'function') {
330
+ throw new Error('Passkey service is not configured');
331
+ }
332
+ return this.storageAdapter.deletePasskeyCredential(credentialId);
333
+ }
334
+ async getUser(identifier) {
335
+ return this.storageAdapter.getUser(identifier);
336
+ }
337
+ getUserPasswordHash(user) {
338
+ return this.storageAdapter.getUserPasswordHash(user);
339
+ }
340
+ getUserId(user) {
341
+ return this.storageAdapter.getUserId(user);
342
+ }
343
+ filterUser(user) {
344
+ return this.storageAdapter.filterUser(user);
345
+ }
346
+ async verifyPassword(password, hash) {
347
+ return this.storageAdapter.verifyPassword(password, hash);
348
+ }
349
+ async storeToken(data) {
350
+ if (this.tokenStoreAdapter) {
351
+ return this.tokenStoreAdapter.save(data);
352
+ }
353
+ const storage = this.storageAdapter;
354
+ if (typeof storage.storeToken === 'function') {
355
+ return storage.storeToken(data);
356
+ }
357
+ throw new Error('Token store is not configured');
358
+ }
359
+ async getToken(query, opts) {
360
+ const normalized = {
361
+ ...query,
362
+ userId: (0, user_id_js_1.toOptionalStringId)(query.userId),
363
+ ruid: (0, user_id_js_1.toOptionalStringId)(query.ruid)
364
+ };
365
+ if (this.tokenStoreAdapter) {
366
+ return this.tokenStoreAdapter.get(normalized, opts);
367
+ }
368
+ const storage = this.storageAdapter;
369
+ if (typeof storage.getToken === 'function') {
370
+ return storage.getToken(normalized, opts);
371
+ }
372
+ return null;
373
+ }
374
+ async deleteToken(query) {
375
+ const normalized = {
376
+ ...query,
377
+ userId: (0, user_id_js_1.toOptionalStringId)(query.userId),
378
+ ruid: (0, user_id_js_1.toOptionalStringId)(query.ruid)
379
+ };
380
+ if (this.tokenStoreAdapter) {
381
+ return this.tokenStoreAdapter.delete(normalized);
382
+ }
383
+ const storage = this.storageAdapter;
384
+ if (typeof storage.deleteToken === 'function') {
385
+ return storage.deleteToken(normalized);
386
+ }
387
+ return 0;
388
+ }
389
+ async createPasskeyChallenge(params) {
390
+ if (typeof this.storageAdapter.createPasskeyChallenge !== 'function') {
391
+ throw new Error('Passkey service is not configured');
392
+ }
393
+ return this.storageAdapter.createPasskeyChallenge(params);
394
+ }
395
+ async verifyPasskeyResponse(params) {
396
+ if (typeof this.storageAdapter.verifyPasskeyResponse !== 'function') {
397
+ throw new Error('Passkey service is not configured');
398
+ }
399
+ return this.storageAdapter.verifyPasskeyResponse(params);
400
+ }
401
+ async getClient(clientId) {
402
+ if (typeof this.storageAdapter.getClient !== 'function') {
403
+ return null;
404
+ }
405
+ return this.storageAdapter.getClient(clientId);
406
+ }
407
+ async verifyClientSecret(client, clientSecret) {
408
+ if (typeof this.storageAdapter.verifyClientSecret !== 'function') {
409
+ throw new Error('OAuth store is not configured');
410
+ }
411
+ return this.storageAdapter.verifyClientSecret(client, clientSecret);
412
+ }
413
+ async createAuthCode(request) {
414
+ if (typeof this.storageAdapter.createAuthCode !== 'function') {
415
+ throw new Error('OAuth store is not configured');
416
+ }
417
+ return this.storageAdapter.createAuthCode(request);
418
+ }
419
+ async consumeAuthCode(code, clientId) {
420
+ if (typeof this.storageAdapter.consumeAuthCode !== 'function') {
421
+ return null;
422
+ }
423
+ const consumed = await this.storageAdapter.consumeAuthCode(code, clientId);
424
+ if (!consumed || consumed.clientId !== clientId) {
425
+ return null;
426
+ }
427
+ return consumed;
428
+ }
429
+ async canImpersonate(params) {
430
+ if (typeof this.storageAdapter.canImpersonate === 'function') {
431
+ return !!(await this.storageAdapter.canImpersonate(params));
432
+ }
433
+ return params.realUserId === params.effectiveUserId;
434
+ }
435
+ jwtSign(payload, secret, expiresInSeconds, options) {
436
+ if (!secret) {
437
+ return { success: false, error: 'JWT secret is not configured' };
438
+ }
439
+ return (this.tokenStoreAdapter ?? this.jwtHelper).jwtSign(payload, secret, expiresInSeconds, options);
440
+ }
441
+ jwtVerify(token, secret, options) {
442
+ return (this.tokenStoreAdapter ?? this.jwtHelper).jwtVerify(token, secret, options);
443
+ }
444
+ jwtDecode(token, options) {
445
+ return (this.tokenStoreAdapter ?? this.jwtHelper).jwtDecode(token, options);
446
+ }
447
+ async getApiKey(token) {
448
+ void token;
449
+ console.warn('[apicore] getApiKey() is not implemented. ' +
450
+ 'Override getApiKey() in your ApiServer subclass to support API key authentication.');
451
+ return null;
452
+ }
453
+ async authenticateUser(params) {
454
+ if (!params?.login || !params?.password) {
455
+ return false;
456
+ }
457
+ const user = await this.storageAdapter.getUser(params.login);
458
+ if (!user) {
459
+ return false;
460
+ }
461
+ const hash = this.storageAdapter.getUserPasswordHash(user);
462
+ return this.storageAdapter.verifyPassword(params.password, hash);
463
+ }
464
+ async updateToken(updates) {
465
+ if (this.tokenStoreAdapter) {
466
+ return this.tokenStoreAdapter.update(updates);
467
+ }
468
+ const storage = this.storageAdapter;
469
+ if (typeof storage.updateToken === 'function') {
470
+ return storage.updateToken(updates);
471
+ }
472
+ return false;
473
+ }
474
+ guessExceptionText(error, defMsg = 'Unknown Error') {
475
+ return (0, error_utils_js_1.guessExceptionText)(error, defMsg);
476
+ }
477
+ async authorize(apiReq, requiredClass) {
478
+ void apiReq;
479
+ if (requiredClass && requiredClass !== 'any') {
480
+ console.warn(`[apicore] 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
+ }
483
+ }
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;
499
+ }
500
+ installStaticDirs() {
501
+ const staticDirs = this.config.staticDirs;
502
+ if (!staticDirs || !(0, request_utils_js_1.isPlainObject)(staticDirs)) {
503
+ return;
504
+ }
505
+ for (const [mountRaw, dirRaw] of Object.entries(staticDirs)) {
506
+ const mount = typeof mountRaw === 'string' ? mountRaw.trim() : '';
507
+ const dir = typeof dirRaw === 'string' ? dirRaw.trim() : '';
508
+ if (!mount || !dir) {
509
+ continue;
510
+ }
511
+ const resolvedMount = mount.startsWith('/') ? mount : `/${mount}`;
512
+ void this.fastify.register(static_1.default, {
513
+ root: dir,
514
+ prefix: resolvedMount.endsWith('/') ? resolvedMount : `${resolvedMount}/`,
515
+ decorateReply: false
516
+ });
517
+ }
518
+ }
519
+ installPingHandler() {
520
+ this.fastify.get(`${this.apiBasePath}/v1/ping`, async () => {
521
+ const payload = {
522
+ success: true,
523
+ status: 'ok',
524
+ apiVersion: this.config.apiVersion ?? '',
525
+ minClientVersion: this.config.minClientVersion ?? '',
526
+ uptimeSec: process.uptime(),
527
+ startedAt: this.startedAt,
528
+ timestamp: new Date().toISOString()
529
+ };
530
+ return { success: true, code: 200, message: 'Success', data: payload, errors: {} };
531
+ });
532
+ }
533
+ async loadSwaggerSpec() {
534
+ const candidates = [node_path_1.default.resolve(process.cwd(), 'docs/swagger/openapi.json')];
535
+ if (typeof __dirname === 'string') {
536
+ candidates.push(node_path_1.default.resolve(__dirname, '../../docs/swagger/openapi.json'));
537
+ }
538
+ let packageRoot;
539
+ try {
540
+ const require = (0, node_module_1.createRequire)(node_path_1.default.join(process.cwd(), 'package.json'));
541
+ const entry = require.resolve('@technomoron/apicore-server');
542
+ packageRoot = node_path_1.default.resolve(node_path_1.default.dirname(entry), '..', '..');
543
+ candidates.push(node_path_1.default.join(packageRoot, 'docs/swagger/openapi.json'));
544
+ }
545
+ catch {
546
+ // Ignore resolution failures; fall back to any existing candidate.
547
+ }
548
+ for (const candidate of candidates) {
549
+ try {
550
+ await (0, promises_1.access)(candidate);
551
+ }
552
+ catch {
553
+ continue;
554
+ }
555
+ try {
556
+ const raw = await (0, promises_1.readFile)(candidate, 'utf8');
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;
564
+ }
565
+ catch {
566
+ return null;
567
+ }
568
+ }
569
+ return null;
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
+ }
592
+ installSwaggerHandler() {
593
+ const rawPath = typeof this.config.swaggerPath === 'string' ? this.config.swaggerPath.trim() : '';
594
+ const enabled = Boolean(this.config.swaggerEnabled) || rawPath.length > 0;
595
+ if (!enabled) {
596
+ return;
597
+ }
598
+ const base = this.apiBasePath === '/' ? '' : this.apiBasePath;
599
+ const resolved = rawPath.length > 0 ? rawPath : `${base}/swagger`;
600
+ const routePath = resolved.startsWith('/') ? resolved : `/${resolved}`;
601
+ let specPromise;
602
+ this.fastify.get(routePath, async (_request, reply) => {
603
+ if (!specPromise) {
604
+ specPromise = this.loadSwaggerSpec();
605
+ }
606
+ const spec = await specPromise;
607
+ if (!spec) {
608
+ // Clear cached failure so next request retries
609
+ specPromise = undefined;
610
+ }
611
+ if (!spec) {
612
+ reply.code(500);
613
+ return {
614
+ success: false,
615
+ code: 500,
616
+ message: 'Swagger spec is unavailable',
617
+ data: null,
618
+ errors: {}
619
+ };
620
+ }
621
+ return spec;
622
+ });
623
+ }
624
+ normalizeApiBasePath(routePath) {
625
+ if (!routePath || typeof routePath !== 'string') {
626
+ return '/api';
627
+ }
628
+ const trimmed = routePath.trim();
629
+ if (!trimmed) {
630
+ return '/api';
631
+ }
632
+ const withLeadingSlash = trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
633
+ if (withLeadingSlash.length === 1) {
634
+ return withLeadingSlash;
635
+ }
636
+ return withLeadingSlash.replace(/\/+$/, '') || '/api';
637
+ }
638
+ installApiNotFoundHandler() {
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({
647
+ success: false,
648
+ code: 404,
649
+ message: `No such endpoint: ${method} ${target}`,
650
+ data: null,
651
+ errors: {}
652
+ });
653
+ });
654
+ }
655
+ installApiErrorHandler() {
656
+ if (this.apiErrorHandlerInstalled) {
657
+ return;
658
+ }
659
+ this.apiErrorHandlerInstalled = true;
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
+ });
739
+ }
740
+ start() {
741
+ if (!this.finalized) {
742
+ console.warn('[apicore] ApiServer.start() called without finalize(); auto-finalizing. Call server.finalize() before start() to silence this warning.');
743
+ this.finalize();
744
+ }
745
+ void this.fastify
746
+ .listen({
747
+ port: this.config.apiPort,
748
+ host: this.config.apiHost
749
+ })
750
+ .then(() => {
751
+ console.log(`Server is running on http://${this.config.apiHost}:${this.config.apiPort}`);
752
+ })
753
+ .catch((error) => {
754
+ let message;
755
+ if (error.code === 'EADDRINUSE') {
756
+ message = `Port ${this.config.apiPort} is already in use.`;
757
+ }
758
+ else if (error.code === 'EACCES') {
759
+ message = `Insufficient permissions to bind to port ${this.config.apiPort}. Try using a port number >= 1024 or running with elevated privileges.`;
760
+ }
761
+ else if (error.code === 'EADDRNOTAVAIL') {
762
+ message = `Address ${this.config.apiHost} is not available on this machine.`;
763
+ }
764
+ else {
765
+ message = `Failed to start server: ${error.message}`;
766
+ }
767
+ const err = new Error(message);
768
+ err.cause = error;
769
+ if (typeof this.config.onStartError === 'function') {
770
+ this.config.onStartError(err);
771
+ return;
772
+ }
773
+ this.logUnhandledError('Server startup failed', err);
774
+ process.exitCode = 1;
775
+ });
776
+ return this;
777
+ }
778
+ internalServerErrorMessage(error) {
779
+ return this.config.debug ? this.guessExceptionText(error) : 'Internal server error';
780
+ }
781
+ logUnhandledError(context, error) {
782
+ console.error(`[ApiServer] ${context}`, error);
783
+ }
784
+ async verifyJWT(token) {
785
+ if (!this.config.accessSecret) {
786
+ return { tokenData: undefined, error: 'JWT authentication disabled; no jwtSecret set', expired: false };
787
+ }
788
+ const result = this.jwtVerify(token, this.config.accessSecret);
789
+ if (!result.success) {
790
+ return { tokenData: undefined, error: result.error, expired: result.expired };
791
+ }
792
+ if (result.data.uid == null) {
793
+ return { tokenData: undefined, error: 'Missing/bad userid in token', expired: false };
794
+ }
795
+ return { tokenData: result.data, error: undefined, expired: false };
796
+ }
797
+ jwtCookieOptions(apiReq) {
798
+ return (0, auth_cookie_options_js_1.buildAuthCookieOptions)(this.config, apiReq.req);
799
+ }
800
+ setAccessCookie(apiReq, accessToken, sessionCookie) {
801
+ const conf = this.config;
802
+ const options = this.jwtCookieOptions(apiReq);
803
+ const accessMaxAge = Math.max(1, conf.accessExpiry) * 1000;
804
+ const accessOptions = sessionCookie ? options : { ...options, maxAge: accessMaxAge };
805
+ apiReq.res.cookie(conf.accessCookie, accessToken, accessOptions);
806
+ }
807
+ async tryRefreshAccessToken(apiReq) {
808
+ const conf = this.config;
809
+ if (!conf.refreshSecret || !conf.accessSecret) {
810
+ return null;
811
+ }
812
+ const rawRefresh = apiReq.req.cookies?.[conf.refreshCookie];
813
+ if (typeof rawRefresh !== 'string') {
814
+ return null;
815
+ }
816
+ const refreshToken = rawRefresh.trim();
817
+ if (!refreshToken) {
818
+ return null;
819
+ }
820
+ const verify = this.jwtVerify(refreshToken, conf.refreshSecret);
821
+ if (!verify.success || !verify.data) {
822
+ return null;
823
+ }
824
+ let stored = null;
825
+ try {
826
+ stored = await this.storageAdapter.getToken({ refreshToken });
827
+ }
828
+ catch {
829
+ return null;
830
+ }
831
+ if (!stored) {
832
+ return null;
833
+ }
834
+ const storedUid = String(stored.userId);
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) {
838
+ return null;
839
+ }
840
+ const claims = verify.data;
841
+ const { exp: _exp, iat: _iat, nbf: _nbf, ...payload } = claims;
842
+ void _exp;
843
+ void _iat;
844
+ void _nbf;
845
+ delete payload.accessToken;
846
+ delete payload.refreshToken;
847
+ delete payload.userId;
848
+ delete payload.expires;
849
+ delete payload.issuedAt;
850
+ delete payload.lastSeenAt;
851
+ delete payload.status;
852
+ const access = this.jwtSign(payload, conf.accessSecret, conf.accessExpiry);
853
+ if (!access.success || !access.token) {
854
+ return null;
855
+ }
856
+ const updated = await this.updateToken({
857
+ refreshToken,
858
+ accessToken: access.token,
859
+ lastSeenAt: new Date()
860
+ });
861
+ if (!updated) {
862
+ return null;
863
+ }
864
+ this.setAccessCookie(apiReq, access.token, stored.sessionCookie ?? false);
865
+ if (apiReq.req.cookies) {
866
+ apiReq.req.cookies[conf.accessCookie] = access.token;
867
+ }
868
+ const verifiedAccess = await this.verifyJWT(access.token);
869
+ if (!verifiedAccess.tokenData) {
870
+ return null;
871
+ }
872
+ const refreshedStored = { ...stored, accessToken: access.token };
873
+ apiReq.authToken = refreshedStored;
874
+ return { token: access.token, tokenData: verifiedAccess.tokenData, stored: refreshedStored };
875
+ }
876
+ async authenticate(apiReq, authType) {
877
+ if (authType === 'none') {
878
+ apiReq.realUid = null;
879
+ return null;
880
+ }
881
+ let token = null;
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);
886
+ const requiresAuthToken = this.requiresAuthToken(authType);
887
+ const allowRefresh = requiresAuthToken || (authType === 'maybe' && this.config.refreshMaybe);
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';
897
+ }
898
+ else if (paramToken) {
899
+ apiReq.authMethod = 'param';
900
+ }
901
+ if (!token) {
902
+ const access = apiReq.req.cookies?.[this.config.accessCookie];
903
+ if (access) {
904
+ token = access;
905
+ apiReq.authMethod = 'cookie';
906
+ }
907
+ }
908
+ let tokenData;
909
+ let error;
910
+ let expired = false;
911
+ if (!token) {
912
+ if (authType === 'maybe') {
913
+ if (!this.config.refreshMaybe) {
914
+ return null;
915
+ }
916
+ const refreshed = await this.tryRefreshAccessToken(apiReq);
917
+ if (!refreshed) {
918
+ return null;
919
+ }
920
+ token = refreshed.token;
921
+ tokenData = refreshed.tokenData;
922
+ error = undefined;
923
+ expired = false;
924
+ }
925
+ else if (requiresAuthToken) {
926
+ const refreshed = await this.tryRefreshAccessToken(apiReq);
927
+ if (!refreshed) {
928
+ throw new ApiError({ code: 401, message: 'Authorization token is required (Bearer/cookie)' });
929
+ }
930
+ token = refreshed.token;
931
+ tokenData = refreshed.tokenData;
932
+ error = undefined;
933
+ expired = false;
934
+ }
935
+ }
936
+ if (!token) {
937
+ throw new ApiError({ code: 401, message: 'Unauthorized Access - requires authentication' });
938
+ }
939
+ if (!tokenData) {
940
+ const verified = await this.verifyJWT(token);
941
+ tokenData = verified.tokenData;
942
+ error = verified.error;
943
+ expired = verified.expired ?? false;
944
+ }
945
+ if (!tokenData && allowRefresh && expired) {
946
+ const refreshed = await this.tryRefreshAccessToken(apiReq);
947
+ if (refreshed) {
948
+ token = refreshed.token;
949
+ tokenData = refreshed.tokenData;
950
+ error = undefined;
951
+ }
952
+ }
953
+ if (!tokenData) {
954
+ throw new ApiError({ code: 401, message: 'Unauthorized Access - ' + error });
955
+ }
956
+ const effectiveUserId = this.extractTokenUserId(tokenData);
957
+ apiReq.realUid = this.resolveRealUserId(tokenData, effectiveUserId);
958
+ if (this.shouldValidateStoredToken(authType)) {
959
+ try {
960
+ await this.assertStoredAccessToken(apiReq, token, tokenData);
961
+ }
962
+ catch (error) {
963
+ if (allowRefresh &&
964
+ error instanceof ApiError &&
965
+ error.code === 401 &&
966
+ error.message === 'Authorization token is no longer valid') {
967
+ const refreshed = await this.tryRefreshAccessToken(apiReq);
968
+ if (!refreshed) {
969
+ throw error;
970
+ }
971
+ token = refreshed.token;
972
+ tokenData = refreshed.tokenData;
973
+ const refreshedUserId = this.extractTokenUserId(tokenData);
974
+ apiReq.realUid = this.resolveRealUserId(tokenData, refreshedUserId);
975
+ await this.assertStoredAccessToken(apiReq, token, tokenData);
976
+ }
977
+ else {
978
+ throw error;
979
+ }
980
+ }
981
+ }
982
+ apiReq.token = token;
983
+ return tokenData;
984
+ }
985
+ async tryAuthenticateApiKey(apiReq, authType, tokenCandidate) {
986
+ if (!tokenCandidate) {
987
+ if (authType === 'apikey') {
988
+ throw new ApiError({ code: 401, message: 'Authorization header is missing or invalid' });
989
+ }
990
+ return null;
991
+ }
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) {
996
+ if (authType === 'apikey') {
997
+ throw new ApiError({ code: 401, message: 'Invalid API Key' });
998
+ }
999
+ return null;
1000
+ }
1001
+ const key = await this.getApiKey(secret);
1002
+ if (!key) {
1003
+ if (authType === 'apikey') {
1004
+ throw new ApiError({ code: 401, message: 'Invalid API Key' });
1005
+ }
1006
+ return null;
1007
+ }
1008
+ apiReq.token = secret;
1009
+ apiReq.apiKey = key;
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;
1016
+ }
1017
+ return {
1018
+ uid: effectiveUid,
1019
+ ...(resolvedEuid !== null ? { ruid: resolvedRuid !== null ? String(resolvedRuid) : undefined } : {}),
1020
+ domain: '',
1021
+ fingerprint: '',
1022
+ iat: 0,
1023
+ exp: 0
1024
+ };
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
+ }
1061
+ requiresAuthToken(authType) {
1062
+ return authType === 'yes' || authType === 'strict';
1063
+ }
1064
+ shouldValidateStoredToken(authType) {
1065
+ return this.config.validateTokens || authType === 'strict';
1066
+ }
1067
+ async assertStoredAccessToken(apiReq, token, tokenData) {
1068
+ const userId = String(this.extractTokenUserId(tokenData));
1069
+ if (apiReq.authToken && apiReq.authToken.accessToken === token && String(apiReq.authToken.userId) === userId) {
1070
+ return;
1071
+ }
1072
+ const stored = await this.storageAdapter.getToken({
1073
+ accessToken: token,
1074
+ userId
1075
+ });
1076
+ if (!stored) {
1077
+ throw new ApiError({ code: 401, message: 'Authorization token is no longer valid' });
1078
+ }
1079
+ apiReq.authToken = stored;
1080
+ }
1081
+ normalizeAuthIdentifier(candidate) {
1082
+ if (typeof candidate === 'number' && Number.isFinite(candidate)) {
1083
+ return candidate;
1084
+ }
1085
+ if (typeof candidate === 'string') {
1086
+ const trimmed = candidate.trim();
1087
+ if (trimmed.length > 0) {
1088
+ return trimmed;
1089
+ }
1090
+ }
1091
+ return null;
1092
+ }
1093
+ extractTokenUserId(tokenData) {
1094
+ const normalized = this.normalizeAuthIdentifier(tokenData.uid);
1095
+ if (normalized === null) {
1096
+ throw new ApiError({ code: 401, message: 'Authorization token is malformed' });
1097
+ }
1098
+ return normalized;
1099
+ }
1100
+ resolveRealUserId(tokenData, effectiveUserId) {
1101
+ const withReal = tokenData;
1102
+ const rawReal = this.normalizeAuthIdentifier(withReal.ruid);
1103
+ if (rawReal === null) {
1104
+ return effectiveUserId;
1105
+ }
1106
+ return rawReal;
1107
+ }
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
+ };
1127
+ }
1128
+ createApiRequest(req, res) {
1129
+ const apiReq = {
1130
+ server: this,
1131
+ req,
1132
+ res,
1133
+ token: '',
1134
+ tokenData: null,
1135
+ authMethod: null,
1136
+ realUid: null,
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,
1140
+ getRealUid: () => apiReq.realUid ?? null,
1141
+ isImpersonating: () => {
1142
+ const realUid = apiReq.realUid;
1143
+ const tokenUid = apiReq.tokenData?.uid;
1144
+ if (realUid === null || realUid === undefined) {
1145
+ return false;
1146
+ }
1147
+ if (tokenUid === null || tokenUid === undefined) {
1148
+ return false;
1149
+ }
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);
1153
+ }
1154
+ };
1155
+ return apiReq;
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
+ }
1245
+ expressAuth(auth) {
1246
+ return async (req, res, next) => {
1247
+ const apiReq = this.createApiRequest(req, res);
1248
+ req.apiReq = apiReq;
1249
+ res.locals.apiReq = apiReq;
1250
+ try {
1251
+ if (this.config.hydrateGetBody) {
1252
+ (0, request_utils_js_1.hydrateGetBody)(req);
1253
+ }
1254
+ if (this.config.debug) {
1255
+ this.dumpRequest(apiReq);
1256
+ }
1257
+ apiReq.tokenData = await this.authenticate(apiReq, auth.type);
1258
+ await this.authorize(apiReq, auth.req);
1259
+ next();
1260
+ }
1261
+ catch (error) {
1262
+ next(error);
1263
+ }
1264
+ };
1265
+ }
1266
+ expressErrorHandler() {
1267
+ return (error, _req, res, next) => {
1268
+ void _req;
1269
+ if (res.headersSent) {
1270
+ next(error);
1271
+ return;
1272
+ }
1273
+ if (error instanceof ApiError || (0, error_utils_js_1.isApiErrorLike)(error)) {
1274
+ const apiError = error;
1275
+ const normalizedErrors = apiError.errors && typeof apiError.errors === 'object' && !Array.isArray(apiError.errors)
1276
+ ? apiError.errors
1277
+ : {};
1278
+ const errorPayload = {
1279
+ success: false,
1280
+ code: apiError.code,
1281
+ message: apiError.message,
1282
+ data: apiError.data ?? null,
1283
+ errors: normalizedErrors
1284
+ };
1285
+ res.status(apiError.code).json(errorPayload);
1286
+ return;
1287
+ }
1288
+ const status = (0, error_utils_js_1.asHttpStatus)(error);
1289
+ if (status) {
1290
+ if (status >= 500) {
1291
+ this.logUnhandledError(`Unhandled compat middleware error mapped to HTTP ${status}`, error);
1292
+ }
1293
+ res.status(status).json({
1294
+ success: false,
1295
+ code: status,
1296
+ message: status === 400 ? 'Invalid request payload' : this.internalServerErrorMessage(error),
1297
+ data: null,
1298
+ errors: {}
1299
+ });
1300
+ return;
1301
+ }
1302
+ this.logUnhandledError('Unhandled compat middleware error fallback to HTTP 500', error);
1303
+ res.status(500).json({
1304
+ success: false,
1305
+ code: 500,
1306
+ message: this.internalServerErrorMessage(error),
1307
+ data: null,
1308
+ errors: {}
1309
+ });
1310
+ };
1311
+ }
1312
+ handleRequest(handler, auth) {
1313
+ return async (request, reply) => {
1314
+ const req = this.toExtendedReq(request);
1315
+ const res = new FastifyResponseAdapter(reply);
1316
+ const apiReq = this.createApiRequest(req, res);
1317
+ try {
1318
+ if (this.config.hydrateGetBody) {
1319
+ (0, request_utils_js_1.hydrateGetBody)(apiReq.req);
1320
+ }
1321
+ if (this.config.debug) {
1322
+ this.dumpRequest(apiReq);
1323
+ }
1324
+ apiReq.tokenData = await this.authenticate(apiReq, auth.type);
1325
+ await this.authorize(apiReq, auth.req);
1326
+ const handlerResult = await handler(apiReq);
1327
+ if (!Array.isArray(handlerResult)) {
1328
+ throw new ApiError({
1329
+ code: 500,
1330
+ message: 'Handler result must be an array: [status, data?, message?]'
1331
+ });
1332
+ }
1333
+ const [code, data = null, rawMessage = 'Success'] = handlerResult;
1334
+ if (typeof code !== 'number' || Number.isNaN(code)) {
1335
+ throw new ApiError({ code: 500, message: 'Handler result must start with a numeric status code' });
1336
+ }
1337
+ const message = typeof rawMessage === 'string' ? rawMessage : 'Success';
1338
+ const responsePayload = { success: code < 400, code, message, data, errors: {} };
1339
+ if (this.config.debug) {
1340
+ this.dumpResponse(apiReq, responsePayload, code);
1341
+ }
1342
+ reply.code(code).send(responsePayload);
1343
+ }
1344
+ catch (error) {
1345
+ if (error instanceof ApiError || (0, error_utils_js_1.isApiErrorLike)(error)) {
1346
+ const apiError = error;
1347
+ const normalizedErrors = apiError.errors && typeof apiError.errors === 'object' && !Array.isArray(apiError.errors)
1348
+ ? apiError.errors
1349
+ : {};
1350
+ const errorPayload = {
1351
+ success: false,
1352
+ code: apiError.code,
1353
+ message: apiError.message,
1354
+ data: apiError.data ?? null,
1355
+ errors: normalizedErrors
1356
+ };
1357
+ if (this.config.debug) {
1358
+ this.dumpResponse(apiReq, errorPayload, apiError.code);
1359
+ }
1360
+ reply.code(apiError.code).send(errorPayload);
1361
+ }
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);
1370
+ const errorPayload = {
1371
+ success: false,
1372
+ code: uploadTooLarge ? 413 : status,
1373
+ message: uploadTooLarge
1374
+ ? `Upload exceeds maximum size of ${this.config.uploadMax} bytes`
1375
+ : this.internalServerErrorMessage(error),
1376
+ data: null,
1377
+ errors: {}
1378
+ };
1379
+ if (this.config.debug) {
1380
+ this.dumpResponse(apiReq, errorPayload, uploadTooLarge ? 413 : status);
1381
+ }
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);
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
+ }
1405
+ api(module) {
1406
+ this.assertNotFinalized('api');
1407
+ module.server = this;
1408
+ const moduleType = module.moduleType;
1409
+ if (moduleType === 'auth') {
1410
+ this.authModule(module);
1411
+ }
1412
+ const configOk = module.checkConfig();
1413
+ if (configOk === false) {
1414
+ const name = module.constructor?.name ? String(module.constructor.name) : 'ApiModule';
1415
+ throw new Error(`${name}.checkConfig() returned false`);
1416
+ }
1417
+ module.onMount();
1418
+ const ns = module.namespace;
1419
+ const mountPath = `${this.apiBasePath}${ns}`;
1420
+ module.mountpath = mountPath;
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 } : {}) });
1426
+ if (this.config.debug) {
1427
+ console.log(`Adding ${fullPath} (${method})`);
1428
+ }
1429
+ }
1430
+ return this;
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
+ }
1444
+ dumpRequest(apiReq) {
1445
+ const req = apiReq.req;
1446
+ const tokenParam = this.config.tokenParam.trim();
1447
+ console.log('--- Incoming Request! ---');
1448
+ const url = req.originalUrl || req.url;
1449
+ console.log('URL:', url);
1450
+ console.log('Method:', req.method);
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
+ }
1458
+ const sensitiveBodyKeys = ['password', 'client_secret', 'clientSecret', 'secret'];
1459
+ if (tokenParam) {
1460
+ sensitiveBodyKeys.push(tokenParam);
1461
+ }
1462
+ const body = req.body && typeof req.body === 'object' ? { ...req.body } : req.body;
1463
+ if (body && typeof body === 'object') {
1464
+ for (const key of sensitiveBodyKeys) {
1465
+ if (key in body) {
1466
+ body[key] = '[REDACTED]';
1467
+ }
1468
+ }
1469
+ }
1470
+ console.log('Body Params:', body || {});
1471
+ const cookies = req.cookies ? { ...req.cookies } : {};
1472
+ const sensitiveCookieKeys = [this.config.accessCookie, this.config.refreshCookie];
1473
+ for (const key of sensitiveCookieKeys) {
1474
+ if (key in cookies) {
1475
+ cookies[key] = '[REDACTED]';
1476
+ }
1477
+ }
1478
+ console.log('Cookies:', cookies);
1479
+ const headers = { ...req.headers };
1480
+ if (headers.authorization) {
1481
+ headers.authorization = '[REDACTED]';
1482
+ }
1483
+ if (headers['x-api-key']) {
1484
+ headers['x-api-key'] = '[REDACTED]';
1485
+ }
1486
+ if (headers.cookie) {
1487
+ headers.cookie = '[REDACTED]';
1488
+ }
1489
+ console.log('Headers:', headers);
1490
+ console.log('------------------------');
1491
+ }
1492
+ formatDebugValue(value, maxLength = 50, seen = new WeakSet()) {
1493
+ if (value === null || value === undefined) {
1494
+ return value;
1495
+ }
1496
+ if (typeof value === 'string') {
1497
+ return value.length <= maxLength
1498
+ ? value
1499
+ : `${value.slice(0, maxLength)}… [truncated ${value.length - maxLength} chars]`;
1500
+ }
1501
+ if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') {
1502
+ return value;
1503
+ }
1504
+ if (typeof value === 'symbol') {
1505
+ return value.toString();
1506
+ }
1507
+ if (value instanceof Date) {
1508
+ return value.toISOString();
1509
+ }
1510
+ if (typeof Buffer !== 'undefined' && Buffer.isBuffer(value)) {
1511
+ return `<Buffer length=${value.length}>`;
1512
+ }
1513
+ if (typeof value === 'function') {
1514
+ return `[Function ${value.name || 'anonymous'}]`;
1515
+ }
1516
+ if (typeof value === 'object') {
1517
+ const obj = value;
1518
+ if (seen.has(obj)) {
1519
+ return '[Circular]';
1520
+ }
1521
+ seen.add(obj);
1522
+ if (Array.isArray(value)) {
1523
+ const arr = value.map((item) => this.formatDebugValue(item, maxLength, seen));
1524
+ seen.delete(obj);
1525
+ return arr;
1526
+ }
1527
+ const recordValue = value;
1528
+ const entries = Object.entries(recordValue).reduce((acc, [key, val]) => {
1529
+ acc[key] = this.formatDebugValue(val, maxLength, seen);
1530
+ return acc;
1531
+ }, {});
1532
+ seen.delete(obj);
1533
+ return entries;
1534
+ }
1535
+ return value;
1536
+ }
1537
+ dumpResponse(apiReq, payload, status) {
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
+ }
1553
+ console.log('--- Outgoing Response! ---');
1554
+ console.log('URL:', url);
1555
+ console.log('Status:', status);
1556
+ console.log('Payload:', this.formatDebugValue(payload));
1557
+ console.log('--------------------------');
1558
+ }
1559
+ }
1560
+ exports.ApiServer = ApiServer;
1561
+ exports.default = ApiServer;