@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.
Files changed (177) hide show
  1. package/dist/cjs/common/types.cjs +10 -0
  2. package/dist/cjs/common/types.d.ts +137 -0
  3. package/dist/cjs/{api-module.cjs → server/src/api-module.cjs} +8 -0
  4. package/dist/{esm → cjs/server/src}/api-module.d.ts +15 -0
  5. package/dist/cjs/{api-server-base.cjs → server/src/api-server-base.cjs} +669 -627
  6. package/dist/{esm → cjs/server/src}/api-server-base.d.ts +105 -78
  7. package/dist/cjs/{auth-api/auth-module.js → server/src/auth-api/auth-module.cjs} +96 -76
  8. package/dist/cjs/{auth-api → server/src/auth-api}/auth-module.d.ts +1 -1
  9. package/dist/cjs/{auth-api/compat-auth-storage.js → server/src/auth-api/compat-auth-storage.cjs} +4 -4
  10. package/dist/cjs/{auth-api/mem-auth-store.js → server/src/auth-api/mem-auth-store.cjs} +7 -7
  11. package/dist/cjs/{auth-api/module.js → server/src/auth-api/module.cjs} +1 -1
  12. package/dist/cjs/server/src/auth-api/schemas.cjs +171 -0
  13. package/dist/cjs/server/src/auth-api/schemas.d.ts +21 -0
  14. package/dist/cjs/{auth-api/sql-auth-store.js → server/src/auth-api/sql-auth-store.cjs} +8 -8
  15. package/dist/cjs/{auth-api/user-id.js → server/src/auth-api/user-id.cjs} +12 -3
  16. package/dist/{esm → cjs/server/src}/auth-cookie-options.d.ts +5 -3
  17. package/dist/cjs/server/src/base/client-info.cjs +285 -0
  18. package/dist/cjs/server/src/base/client-info.d.ts +27 -0
  19. package/dist/cjs/server/src/base/error-utils.cjs +50 -0
  20. package/dist/cjs/server/src/base/error-utils.d.ts +16 -0
  21. package/dist/cjs/server/src/base/request-utils.cjs +27 -0
  22. package/dist/cjs/server/src/base/request-utils.d.ts +8 -0
  23. package/dist/cjs/{index.cjs → server/src/index.cjs} +24 -15
  24. package/dist/{esm → cjs/server/src}/index.d.ts +7 -0
  25. package/dist/cjs/server/src/limiter/auth-rate-limiter.cjs +35 -0
  26. package/dist/cjs/server/src/limiter/auth-rate-limiter.d.ts +12 -0
  27. package/dist/cjs/server/src/limiter/fixed-window.cjs +41 -0
  28. package/dist/cjs/server/src/limiter/fixed-window.d.ts +11 -0
  29. package/dist/cjs/{oauth/base.js → server/src/oauth/base.cjs} +1 -0
  30. package/dist/cjs/{oauth → server/src/oauth}/base.d.ts +8 -1
  31. package/dist/cjs/{oauth/memory.js → server/src/oauth/memory.cjs} +7 -4
  32. package/dist/{esm → cjs/server/src}/oauth/memory.d.ts +1 -1
  33. package/dist/cjs/{oauth/models.js → server/src/oauth/models.cjs} +2 -2
  34. package/dist/cjs/{oauth/sequelize.js → server/src/oauth/sequelize.cjs} +11 -7
  35. package/dist/{esm → cjs/server/src}/oauth/sequelize.d.ts +1 -1
  36. package/dist/cjs/{passkey/base.js → server/src/passkey/base.cjs} +1 -0
  37. package/dist/{esm → cjs/server/src}/passkey/base.d.ts +11 -0
  38. package/dist/cjs/{passkey/memory.js → server/src/passkey/memory.cjs} +2 -2
  39. package/dist/cjs/{passkey/models.js → server/src/passkey/models.cjs} +1 -1
  40. package/dist/cjs/{passkey/sequelize.js → server/src/passkey/sequelize.cjs} +3 -3
  41. package/dist/cjs/{passkey/service.js → server/src/passkey/service.cjs} +17 -3
  42. package/dist/{esm → cjs/server/src}/passkey/service.d.ts +1 -1
  43. package/dist/cjs/{sequelize-utils.js → server/src/sequelize-utils.cjs} +4 -5
  44. package/dist/cjs/{token/base.js → server/src/token/base.cjs} +4 -0
  45. package/dist/{esm → cjs/server/src}/token/base.d.ts +7 -0
  46. package/dist/cjs/{token/memory.js → server/src/token/memory.cjs} +15 -20
  47. package/dist/cjs/{token/sequelize.js → server/src/token/sequelize.cjs} +25 -11
  48. package/dist/cjs/server/src/upload/memory.cjs +92 -0
  49. package/dist/cjs/server/src/upload/memory.d.ts +17 -0
  50. package/dist/cjs/server/src/upload/tus-module.cjs +270 -0
  51. package/dist/cjs/server/src/upload/tus-module.d.ts +38 -0
  52. package/dist/cjs/server/src/upload/types.d.ts +8 -0
  53. package/dist/cjs/{user/base.js → server/src/user/base.cjs} +1 -0
  54. package/dist/cjs/{user → server/src/user}/base.d.ts +9 -0
  55. package/dist/cjs/{user/memory.js → server/src/user/memory.cjs} +29 -7
  56. package/dist/cjs/{user/sequelize.js → server/src/user/sequelize.cjs} +33 -8
  57. package/dist/cjs/server/src/user/types.cjs +2 -0
  58. package/dist/esm/common/types.d.ts +137 -0
  59. package/dist/esm/common/types.js +9 -0
  60. package/dist/{cjs → esm/server/src}/api-module.d.ts +15 -0
  61. package/dist/esm/{api-module.js → server/src/api-module.js} +8 -0
  62. package/dist/{cjs → esm/server/src}/api-server-base.d.ts +105 -78
  63. package/dist/esm/{api-server-base.js → server/src/api-server-base.js} +658 -616
  64. package/dist/esm/{auth-api → server/src/auth-api}/auth-module.d.ts +1 -1
  65. package/dist/esm/{auth-api → server/src/auth-api}/auth-module.js +92 -72
  66. package/dist/esm/{auth-api → server/src/auth-api}/compat-auth-storage.js +3 -3
  67. package/dist/esm/server/src/auth-api/schemas.d.ts +21 -0
  68. package/dist/esm/server/src/auth-api/schemas.js +168 -0
  69. package/dist/esm/{auth-api → server/src/auth-api}/user-id.js +12 -3
  70. package/dist/{cjs → esm/server/src}/auth-cookie-options.d.ts +5 -3
  71. package/dist/esm/server/src/base/client-info.d.ts +27 -0
  72. package/dist/esm/server/src/base/client-info.js +282 -0
  73. package/dist/esm/server/src/base/error-utils.d.ts +16 -0
  74. package/dist/esm/server/src/base/error-utils.js +44 -0
  75. package/dist/esm/server/src/base/request-utils.d.ts +8 -0
  76. package/dist/esm/server/src/base/request-utils.js +23 -0
  77. package/dist/{cjs → esm/server/src}/index.d.ts +7 -0
  78. package/dist/esm/{index.js → server/src/index.js} +4 -0
  79. package/dist/esm/server/src/limiter/auth-rate-limiter.d.ts +12 -0
  80. package/dist/esm/server/src/limiter/auth-rate-limiter.js +32 -0
  81. package/dist/esm/server/src/limiter/fixed-window.d.ts +11 -0
  82. package/dist/esm/server/src/limiter/fixed-window.js +37 -0
  83. package/dist/esm/{oauth → server/src/oauth}/base.d.ts +8 -1
  84. package/dist/esm/server/src/oauth/base.js +3 -0
  85. package/dist/{cjs → esm/server/src}/oauth/memory.d.ts +1 -1
  86. package/dist/esm/{oauth → server/src/oauth}/memory.js +5 -2
  87. package/dist/{cjs → esm/server/src}/oauth/sequelize.d.ts +1 -1
  88. package/dist/esm/{oauth → server/src/oauth}/sequelize.js +6 -2
  89. package/dist/{cjs → esm/server/src}/passkey/base.d.ts +11 -0
  90. package/dist/esm/server/src/passkey/base.js +3 -0
  91. package/dist/{cjs → esm/server/src}/passkey/service.d.ts +1 -1
  92. package/dist/esm/{passkey → server/src/passkey}/service.js +17 -3
  93. package/dist/esm/{sequelize-utils.js → server/src/sequelize-utils.js} +4 -5
  94. package/dist/{cjs → esm/server/src}/token/base.d.ts +7 -0
  95. package/dist/esm/{token → server/src/token}/base.js +4 -0
  96. package/dist/esm/{token → server/src/token}/memory.js +14 -19
  97. package/dist/esm/{token → server/src/token}/sequelize.js +22 -8
  98. package/dist/esm/server/src/upload/memory.d.ts +17 -0
  99. package/dist/esm/server/src/upload/memory.js +86 -0
  100. package/dist/esm/server/src/upload/tus-module.d.ts +38 -0
  101. package/dist/esm/server/src/upload/tus-module.js +266 -0
  102. package/dist/esm/server/src/upload/types.d.ts +8 -0
  103. package/dist/esm/{user → server/src/user}/base.d.ts +9 -0
  104. package/dist/esm/{user → server/src/user}/base.js +1 -0
  105. package/dist/esm/{user → server/src/user}/memory.js +27 -5
  106. package/dist/esm/{user → server/src/user}/sequelize.js +30 -5
  107. package/dist/esm/server/src/user/types.js +1 -0
  108. package/docs/swagger/openapi.json +411 -125
  109. package/package.json +129 -134
  110. package/README.txt +0 -213
  111. package/dist/esm/oauth/base.js +0 -2
  112. package/dist/esm/passkey/base.js +0 -2
  113. /package/dist/cjs/{auth-api → server/src/auth-api}/compat-auth-storage.d.ts +0 -0
  114. /package/dist/cjs/{auth-api → server/src/auth-api}/mem-auth-store.d.ts +0 -0
  115. /package/dist/cjs/{auth-api → server/src/auth-api}/module.d.ts +0 -0
  116. /package/dist/cjs/{auth-api → server/src/auth-api}/sql-auth-store.d.ts +0 -0
  117. /package/dist/cjs/{auth-api/storage.js → server/src/auth-api/storage.cjs} +0 -0
  118. /package/dist/cjs/{auth-api → server/src/auth-api}/storage.d.ts +0 -0
  119. /package/dist/cjs/{auth-api/types.js → server/src/auth-api/types.cjs} +0 -0
  120. /package/dist/cjs/{auth-api → server/src/auth-api}/types.d.ts +0 -0
  121. /package/dist/cjs/{auth-api → server/src/auth-api}/user-id.d.ts +0 -0
  122. /package/dist/cjs/{auth-cookie-options.js → server/src/auth-cookie-options.cjs} +0 -0
  123. /package/dist/cjs/{oauth → server/src/oauth}/models.d.ts +0 -0
  124. /package/dist/cjs/{oauth/types.js → server/src/oauth/types.cjs} +0 -0
  125. /package/dist/cjs/{oauth → server/src/oauth}/types.d.ts +0 -0
  126. /package/dist/cjs/{passkey/config.js → server/src/passkey/config.cjs} +0 -0
  127. /package/dist/cjs/{passkey → server/src/passkey}/config.d.ts +0 -0
  128. /package/dist/cjs/{passkey → server/src/passkey}/memory.d.ts +0 -0
  129. /package/dist/cjs/{passkey → server/src/passkey}/models.d.ts +0 -0
  130. /package/dist/cjs/{passkey → server/src/passkey}/sequelize.d.ts +0 -0
  131. /package/dist/cjs/{passkey/types.js → server/src/passkey/types.cjs} +0 -0
  132. /package/dist/cjs/{passkey → server/src/passkey}/types.d.ts +0 -0
  133. /package/dist/cjs/{sequelize-utils.d.ts → server/src/sequelize-utils.d.ts} +0 -0
  134. /package/dist/cjs/{token → server/src/token}/memory.d.ts +0 -0
  135. /package/dist/cjs/{token → server/src/token}/sequelize.d.ts +0 -0
  136. /package/dist/cjs/{token/types.js → server/src/token/types.cjs} +0 -0
  137. /package/dist/cjs/{token → server/src/token}/types.d.ts +0 -0
  138. /package/dist/cjs/{user/types.js → server/src/upload/types.cjs} +0 -0
  139. /package/dist/cjs/{user → server/src/user}/memory.d.ts +0 -0
  140. /package/dist/cjs/{user → server/src/user}/sequelize.d.ts +0 -0
  141. /package/dist/cjs/{user → server/src/user}/types.d.ts +0 -0
  142. /package/dist/esm/{auth-api → server/src/auth-api}/compat-auth-storage.d.ts +0 -0
  143. /package/dist/esm/{auth-api → server/src/auth-api}/mem-auth-store.d.ts +0 -0
  144. /package/dist/esm/{auth-api → server/src/auth-api}/mem-auth-store.js +0 -0
  145. /package/dist/esm/{auth-api → server/src/auth-api}/module.d.ts +0 -0
  146. /package/dist/esm/{auth-api → server/src/auth-api}/module.js +0 -0
  147. /package/dist/esm/{auth-api → server/src/auth-api}/sql-auth-store.d.ts +0 -0
  148. /package/dist/esm/{auth-api → server/src/auth-api}/sql-auth-store.js +0 -0
  149. /package/dist/esm/{auth-api → server/src/auth-api}/storage.d.ts +0 -0
  150. /package/dist/esm/{auth-api → server/src/auth-api}/storage.js +0 -0
  151. /package/dist/esm/{auth-api → server/src/auth-api}/types.d.ts +0 -0
  152. /package/dist/esm/{auth-api → server/src/auth-api}/types.js +0 -0
  153. /package/dist/esm/{auth-api → server/src/auth-api}/user-id.d.ts +0 -0
  154. /package/dist/esm/{auth-cookie-options.js → server/src/auth-cookie-options.js} +0 -0
  155. /package/dist/esm/{oauth → server/src/oauth}/models.d.ts +0 -0
  156. /package/dist/esm/{oauth → server/src/oauth}/models.js +0 -0
  157. /package/dist/esm/{oauth → server/src/oauth}/types.d.ts +0 -0
  158. /package/dist/esm/{oauth → server/src/oauth}/types.js +0 -0
  159. /package/dist/esm/{passkey → server/src/passkey}/config.d.ts +0 -0
  160. /package/dist/esm/{passkey → server/src/passkey}/config.js +0 -0
  161. /package/dist/esm/{passkey → server/src/passkey}/memory.d.ts +0 -0
  162. /package/dist/esm/{passkey → server/src/passkey}/memory.js +0 -0
  163. /package/dist/esm/{passkey → server/src/passkey}/models.d.ts +0 -0
  164. /package/dist/esm/{passkey → server/src/passkey}/models.js +0 -0
  165. /package/dist/esm/{passkey → server/src/passkey}/sequelize.d.ts +0 -0
  166. /package/dist/esm/{passkey → server/src/passkey}/sequelize.js +0 -0
  167. /package/dist/esm/{passkey → server/src/passkey}/types.d.ts +0 -0
  168. /package/dist/esm/{passkey → server/src/passkey}/types.js +0 -0
  169. /package/dist/esm/{sequelize-utils.d.ts → server/src/sequelize-utils.d.ts} +0 -0
  170. /package/dist/esm/{token → server/src/token}/memory.d.ts +0 -0
  171. /package/dist/esm/{token → server/src/token}/sequelize.d.ts +0 -0
  172. /package/dist/esm/{token → server/src/token}/types.d.ts +0 -0
  173. /package/dist/esm/{token → server/src/token}/types.js +0 -0
  174. /package/dist/esm/{user → server/src/upload}/types.js +0 -0
  175. /package/dist/esm/{user → server/src/user}/memory.d.ts +0 -0
  176. /package/dist/esm/{user → server/src/user}/sequelize.d.ts +0 -0
  177. /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 cookie_parser_1 = __importDefault(require("cookie-parser"));
18
- const cors_1 = __importDefault(require("cors"));
19
- const express_1 = __importDefault(require("express"));
20
- const multer_1 = __importDefault(require("multer"));
21
- const module_js_1 = require("./auth-api/module.js");
22
- const storage_js_1 = require("./auth-api/storage.js");
23
- const user_id_js_1 = require("./auth-api/user-id.js");
24
- const auth_cookie_options_js_1 = require("./auth-cookie-options.js");
25
- const base_js_1 = require("./token/base.js");
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 = guess_exception_text(message, '[Unknown error (null/undefined)]');
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
- authStores: config.authStores,
389
- onStartError: config.onStartError
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 req.apiReq or res.locals.apiReq.');
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.userStoreAdapter = null;
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.authStores) {
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.app = (0, express_1.default)();
440
- // Mount API modules and any custom endpoints under `apiBasePath` on this router so we can keep
441
- // the API 404 handler ordered last without relying on Express internals.
442
- this.apiRouter = express_1.default.Router();
443
- if (config.uploadPath) {
444
- const upload = (0, multer_1.default)({ dest: config.uploadPath, limits: { fileSize: this.config.uploadMax } });
445
- this.app.use(upload.any());
446
- // Multer errors happen before ApiModule route wrappers; format the common "too large" failure.
447
- this.app.use((err, _req, res, next) => {
448
- const code = err && typeof err === 'object' ? err.code : undefined;
449
- if (code === 'LIMIT_FILE_SIZE') {
450
- res.status(413).json({
451
- success: false,
452
- code: 413,
453
- message: `Upload exceeds maximum size of ${this.config.uploadMax} bytes`,
454
- data: null,
455
- errors: {}
456
- });
457
- return;
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
- this.middlewares();
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
- assertNotFinalized(action) {
472
- if (this.finalized) {
473
- throw new Error(`Cannot call ApiServer.${action}() after finalize()/start()`);
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
- toApiRouterPath(candidate) {
477
- if (typeof candidate !== 'string') {
478
- return null;
265
+ parseRawBody(raw, contentType) {
266
+ if (!raw) {
267
+ return {};
479
268
  }
480
- const trimmed = candidate.trim();
481
- if (!trimmed) {
482
- return null;
483
- }
484
- const normalized = trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
485
- const base = this.apiBasePath;
486
- if (base === '/') {
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 (normalized === base) {
490
- return '/';
277
+ if (contentType.includes('application/x-www-form-urlencoded')) {
278
+ const params = new URLSearchParams(raw);
279
+ return Object.fromEntries(params.entries());
491
280
  }
492
- if (normalized.startsWith(`${base}/`)) {
493
- return normalized.slice(base.length) || '/';
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
- ensureUserStore() {
541
- if (!this.userStoreAdapter) {
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.passkeyServiceAdapter;
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
- return this.ensurePasskeyService().deleteCredential(credentialId);
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.oauthStoreAdapter;
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.userStoreAdapter ? this.userStoreAdapter.findUser(identifier) : null;
335
+ return this.storageAdapter.getUser(identifier);
600
336
  }
601
337
  getUserPasswordHash(user) {
602
- return this.ensureUserStore().getPasswordHash(user) ?? '';
338
+ return this.storageAdapter.getUserPasswordHash(user);
603
339
  }
604
340
  getUserId(user) {
605
- return this.ensureUserStore().getUserId(user);
341
+ return this.storageAdapter.getUserId(user);
606
342
  }
607
343
  filterUser(user) {
608
- return this.ensureUserStore().toPublic(user);
344
+ return this.storageAdapter.filterUser(user);
609
345
  }
610
346
  async verifyPassword(password, hash) {
611
- return this.ensureUserStore().verifyPassword(password, hash);
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
- return this.ensurePasskeyService().createChallenge(params);
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
- return this.ensurePasskeyService().verifyResponse(params);
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
- return this.oauthStoreAdapter ? this.oauthStoreAdapter.getClient(clientId) : null;
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
- return this.ensureOAuthStore().verifyClientSecret(client.clientId, 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);
664
412
  }
665
413
  async createAuthCode(request) {
666
- const expiresAt = new Date(Date.now() + (request.expiresInSeconds ?? 300) * 1000);
667
- const code = request.code ?? (0, node_crypto_1.randomUUID)();
668
- await this.ensureOAuthStore().createAuthCode({ ...request, code, expiresAt });
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
- const consumed = await this.ensureOAuthStore().consumeAuthCode(code);
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.canImpersonateAdapter) {
690
- return !!(await this.canImpersonateAdapter(params));
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 guess_exception_text(error, defMsg);
475
+ return (0, error_utils_js_1.guessExceptionText)(error, defMsg);
730
476
  }
731
477
  async authorize(apiReq, requiredClass) {
732
478
  void apiReq;
733
- void requiredClass;
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
- middlewares() {
736
- this.app.use(express_1.default.json());
737
- this.app.use((0, cookie_parser_1.default)());
738
- const corsOptions = {
739
- origin: (origin, callback) => {
740
- if (!origin) {
741
- return callback(null, true);
742
- }
743
- if (this.config.origins && this.config.origins.length > 0) {
744
- if (this.config.origins.includes(origin)) {
745
- return callback(null, true);
746
- }
747
- return callback(new Error(`${origin} Not allowed by CORS`));
748
- }
749
- return callback(null, true);
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.app.use(resolvedMount, express_1.default.static(dir));
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
- const path = `${this.apiBasePath}/v1/ping`;
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
- res.status(200).json({ success: true, code: 200, message: 'Success', data: payload, errors: {} });
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
- const packageRoot = node_path_1.default.resolve(node_path_1.default.dirname(entry), '..', '..');
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
- return JSON.parse(raw);
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 path = resolved.startsWith('/') ? resolved : `/${resolved}`;
600
+ const routePath = resolved.startsWith('/') ? resolved : `/${resolved}`;
845
601
  let specPromise;
846
- this.app.get(path, async (_req, res) => {
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
- res.status(500).json({
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
- res.status(200).json(spec);
621
+ return spec;
862
622
  });
863
623
  }
864
- normalizeApiBasePath(path) {
865
- if (!path || typeof path !== 'string') {
624
+ normalizeApiBasePath(routePath) {
625
+ if (!routePath || typeof routePath !== 'string') {
866
626
  return '/api';
867
627
  }
868
- const trimmed = path.trim();
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
- if (this.apiNotFoundHandler) {
880
- return;
881
- }
882
- this.apiNotFoundHandler = (req, res) => {
883
- const payload = {
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: this.describeMissingEndpoint(req),
649
+ message: `No such endpoint: ${method} ${target}`,
887
650
  data: null,
888
651
  errors: {}
889
- };
890
- res.status(404).json(payload);
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.app.use(this.apiBasePath, this.expressErrorHandler());
900
- }
901
- describeMissingEndpoint(req) {
902
- const method = typeof req.method === 'string' ? req.method.toUpperCase() : 'GET';
903
- const target = req.originalUrl || req.url || this.apiBasePath;
904
- return `No such endpoint: ${method} ${target}`;
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.app
745
+ void this.fastify
912
746
  .listen({
913
747
  port: this.config.apiPort,
914
748
  host: this.config.apiHost
915
749
  })
916
- .on('listening', () => {
750
+ .then(() => {
917
751
  console.log(`Server is running on http://${this.config.apiHost}:${this.config.apiPort}`);
918
752
  })
919
- .on('error', (error) => {
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
- throw err;
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 (!result.data.uid) {
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 === undefined || verify.data.uid === null ? null : String(verify.data.uid);
998
- if (verifyUid && verifyUid !== storedUid) {
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
- const apiKeyAuth = await this.tryAuthenticateApiKey(apiReq, authType, authHeader);
1048
- if (apiKeyAuth) {
1049
- return apiKeyAuth;
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 (authHeader?.startsWith('Bearer ')) {
1052
- token = authHeader.slice(7).trim();
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, authHeader) {
1138
- if (!authHeader?.startsWith('Bearer ')) {
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 = authHeader.slice(7).trim();
1145
- if (!keyToken.startsWith('apikey-')) {
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
- throw new ApiError({ code: 401, message: 'Invalid API Key' });
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
- // Treat API keys as authenticated identities, consistent with JWT-based flows.
1159
- const resolvedUid = this.normalizeAuthIdentifier(key.uid);
1160
- if (resolvedUid !== null) {
1161
- apiReq.realUid = resolvedUid;
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: key.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
- useExpress(pathOrHandler, ...handlers) {
1219
- this.assertNotFinalized('useExpress');
1220
- if (typeof pathOrHandler === 'string') {
1221
- const apiPath = this.toApiRouterPath(pathOrHandler);
1222
- if (apiPath) {
1223
- this.apiRouter.use(apiPath, ...handlers);
1224
- }
1225
- else {
1226
- this.app.use(pathOrHandler, ...handlers);
1227
- }
1228
- }
1229
- else {
1230
- this.app.use(pathOrHandler, ...handlers);
1231
- }
1232
- return this;
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
- return realUid !== tokenUid;
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
- const errorPayload = {
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
- handle_request(handler, auth) {
1325
- return async (req, res, next) => {
1326
- void next;
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: true, code, message, data, errors: {} };
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
- res.status(code).json(responsePayload);
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
- res.status(apiError.code).json(errorPayload);
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: 500,
1377
- message: this.internalServerErrorMessage(error),
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, 500);
1380
+ this.dumpResponse(apiReq, errorPayload, uploadTooLarge ? 413 : status);
1383
1381
  }
1384
- res.status(500).json(errorPayload);
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
- const base = this.apiBasePath;
1417
+ module.onMount();
1403
1418
  const ns = module.namespace;
1404
- const mountPath = `${base}${ns}`;
1419
+ const mountPath = `${this.apiBasePath}${ns}`;
1405
1420
  module.mountpath = mountPath;
1406
- module.defineRoutes().forEach((r) => {
1407
- const handler = this.handle_request(r.handler, r.auth);
1408
- switch (r.method) {
1409
- case 'get':
1410
- router.get(r.path, handler);
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 ${mountPath}${r.path} (${r.method.toUpperCase()})`);
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
- console.log('Query Params:', req.query || {});
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 url = apiReq.req.originalUrl || apiReq.req.url;
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);