@araminix/platform 0.0.0-stage → 1.7.0

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 (175) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +282 -2
  3. package/dist/index.d.ts +15 -0
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +15 -0
  6. package/dist/index.js.map +1 -0
  7. package/dist/src/Platform.d.ts +59 -0
  8. package/dist/src/Platform.d.ts.map +1 -0
  9. package/dist/src/Platform.js +415 -0
  10. package/dist/src/Platform.js.map +1 -0
  11. package/dist/src/agent/AgentController.d.ts +41 -0
  12. package/dist/src/agent/AgentController.d.ts.map +1 -0
  13. package/dist/src/agent/AgentController.js +332 -0
  14. package/dist/src/agent/AgentController.js.map +1 -0
  15. package/dist/src/agent/agentWorkerDriver.d.ts +11 -0
  16. package/dist/src/agent/agentWorkerDriver.d.ts.map +1 -0
  17. package/dist/src/agent/agentWorkerDriver.js +20 -0
  18. package/dist/src/agent/agentWorkerDriver.js.map +1 -0
  19. package/dist/src/agent/d.d.ts +39 -0
  20. package/dist/src/agent/d.d.ts.map +1 -0
  21. package/dist/src/agent/d.js +91 -0
  22. package/dist/src/agent/d.js.map +1 -0
  23. package/dist/src/agent/worker.d.ts +2 -0
  24. package/dist/src/agent/worker.d.ts.map +1 -0
  25. package/dist/src/agent/worker.js +72 -0
  26. package/dist/src/agent/worker.js.map +1 -0
  27. package/dist/src/builder/Fragment.d.ts +40 -0
  28. package/dist/src/builder/Fragment.d.ts.map +1 -0
  29. package/dist/src/builder/Fragment.js +87 -0
  30. package/dist/src/builder/Fragment.js.map +1 -0
  31. package/dist/src/builder/Panel.d.ts +30 -0
  32. package/dist/src/builder/Panel.d.ts.map +1 -0
  33. package/dist/src/builder/Panel.js +45 -0
  34. package/dist/src/builder/Panel.js.map +1 -0
  35. package/dist/src/builder/builder.d.ts +31 -0
  36. package/dist/src/builder/builder.d.ts.map +1 -0
  37. package/dist/src/builder/builder.js +43 -0
  38. package/dist/src/builder/builder.js.map +1 -0
  39. package/dist/src/executor/ActivityInstance.d.ts +98 -0
  40. package/dist/src/executor/ActivityInstance.d.ts.map +1 -0
  41. package/dist/src/executor/ActivityInstance.js +389 -0
  42. package/dist/src/executor/ActivityInstance.js.map +1 -0
  43. package/dist/src/executor/AppInstance.d.ts +73 -0
  44. package/dist/src/executor/AppInstance.d.ts.map +1 -0
  45. package/dist/src/executor/AppInstance.js +399 -0
  46. package/dist/src/executor/AppInstance.js.map +1 -0
  47. package/dist/src/executor/Instance.d.ts +43 -0
  48. package/dist/src/executor/Instance.d.ts.map +1 -0
  49. package/dist/src/executor/Instance.js +415 -0
  50. package/dist/src/executor/Instance.js.map +1 -0
  51. package/dist/src/executor/index.d.ts +4 -0
  52. package/dist/src/executor/index.d.ts.map +1 -0
  53. package/dist/src/executor/index.js +3 -0
  54. package/dist/src/executor/index.js.map +1 -0
  55. package/dist/src/executor/proto.d.ts +120 -0
  56. package/dist/src/executor/proto.d.ts.map +1 -0
  57. package/dist/src/executor/proto.js +207 -0
  58. package/dist/src/executor/proto.js.map +1 -0
  59. package/dist/src/executor/worker.d.ts +2 -0
  60. package/dist/src/executor/worker.d.ts.map +1 -0
  61. package/dist/src/executor/worker.js +154 -0
  62. package/dist/src/executor/worker.js.map +1 -0
  63. package/dist/src/executor/workerDriver.d.ts +14 -0
  64. package/dist/src/executor/workerDriver.d.ts.map +1 -0
  65. package/dist/src/executor/workerDriver.js +26 -0
  66. package/dist/src/executor/workerDriver.js.map +1 -0
  67. package/dist/src/lib/actors.d.ts +94 -0
  68. package/dist/src/lib/actors.d.ts.map +1 -0
  69. package/dist/src/lib/actors.js +139 -0
  70. package/dist/src/lib/actors.js.map +1 -0
  71. package/dist/src/lib/d.d.ts +145 -0
  72. package/dist/src/lib/d.d.ts.map +1 -0
  73. package/dist/src/lib/d.js +296 -0
  74. package/dist/src/lib/d.js.map +1 -0
  75. package/dist/src/lib/worker_socket.d.ts +56 -0
  76. package/dist/src/lib/worker_socket.d.ts.map +1 -0
  77. package/dist/src/lib/worker_socket.js +161 -0
  78. package/dist/src/lib/worker_socket.js.map +1 -0
  79. package/dist/src/server/AuthProvider.d.ts +199 -0
  80. package/dist/src/server/AuthProvider.d.ts.map +1 -0
  81. package/dist/src/server/AuthProvider.js +69 -0
  82. package/dist/src/server/AuthProvider.js.map +1 -0
  83. package/dist/src/server/ConnectRequest.d.ts +15 -0
  84. package/dist/src/server/ConnectRequest.d.ts.map +1 -0
  85. package/dist/src/server/ConnectRequest.js +36 -0
  86. package/dist/src/server/ConnectRequest.js.map +1 -0
  87. package/dist/src/server/Endpoint.d.ts +78 -0
  88. package/dist/src/server/Endpoint.d.ts.map +1 -0
  89. package/dist/src/server/Endpoint.js +809 -0
  90. package/dist/src/server/Endpoint.js.map +1 -0
  91. package/dist/src/server/EndpointController.d.ts +15 -0
  92. package/dist/src/server/EndpointController.d.ts.map +1 -0
  93. package/dist/src/server/EndpointController.js +92 -0
  94. package/dist/src/server/EndpointController.js.map +1 -0
  95. package/dist/src/server/index.d.ts +8 -0
  96. package/dist/src/server/index.d.ts.map +1 -0
  97. package/dist/src/server/index.js +8 -0
  98. package/dist/src/server/index.js.map +1 -0
  99. package/dist/src/utils/i18n.d.ts +20 -0
  100. package/dist/src/utils/i18n.d.ts.map +1 -0
  101. package/dist/src/utils/i18n.js +41 -0
  102. package/dist/src/utils/i18n.js.map +1 -0
  103. package/dist/src/utils/index.d.ts +4 -0
  104. package/dist/src/utils/index.d.ts.map +1 -0
  105. package/dist/src/utils/index.js +4 -0
  106. package/dist/src/utils/index.js.map +1 -0
  107. package/dist/src/utils/log_middleware.d.ts +3 -0
  108. package/dist/src/utils/log_middleware.d.ts.map +1 -0
  109. package/dist/src/utils/log_middleware.js +38 -0
  110. package/dist/src/utils/log_middleware.js.map +1 -0
  111. package/dist/src/utils/minioService.d.ts +54 -0
  112. package/dist/src/utils/minioService.d.ts.map +1 -0
  113. package/dist/src/utils/minioService.js +166 -0
  114. package/dist/src/utils/minioService.js.map +1 -0
  115. package/dist/tests/actors.test.d.ts +2 -0
  116. package/dist/tests/actors.test.d.ts.map +1 -0
  117. package/dist/tests/actors.test.js +45 -0
  118. package/dist/tests/actors.test.js.map +1 -0
  119. package/dist/tests/agentWorker.test.d.ts +2 -0
  120. package/dist/tests/agentWorker.test.d.ts.map +1 -0
  121. package/dist/tests/agentWorker.test.js +225 -0
  122. package/dist/tests/agentWorker.test.js.map +1 -0
  123. package/dist/tests/authProvider.test.d.ts +2 -0
  124. package/dist/tests/authProvider.test.d.ts.map +1 -0
  125. package/dist/tests/authProvider.test.js +39 -0
  126. package/dist/tests/authProvider.test.js.map +1 -0
  127. package/dist/tests/builder.test.d.ts +2 -0
  128. package/dist/tests/builder.test.d.ts.map +1 -0
  129. package/dist/tests/builder.test.js +37 -0
  130. package/dist/tests/builder.test.js.map +1 -0
  131. package/dist/tests/connectRequest.test.d.ts +2 -0
  132. package/dist/tests/connectRequest.test.d.ts.map +1 -0
  133. package/dist/tests/connectRequest.test.js +55 -0
  134. package/dist/tests/connectRequest.test.js.map +1 -0
  135. package/dist/tests/endpointController.test.d.ts +2 -0
  136. package/dist/tests/endpointController.test.d.ts.map +1 -0
  137. package/dist/tests/endpointController.test.js +28 -0
  138. package/dist/tests/endpointController.test.js.map +1 -0
  139. package/dist/tests/endpointSecurity.test.d.ts +2 -0
  140. package/dist/tests/endpointSecurity.test.d.ts.map +1 -0
  141. package/dist/tests/endpointSecurity.test.js +1007 -0
  142. package/dist/tests/endpointSecurity.test.js.map +1 -0
  143. package/dist/tests/i18n.test.d.ts +2 -0
  144. package/dist/tests/i18n.test.d.ts.map +1 -0
  145. package/dist/tests/i18n.test.js +25 -0
  146. package/dist/tests/i18n.test.js.map +1 -0
  147. package/dist/tests/logMiddleware.test.d.ts +2 -0
  148. package/dist/tests/logMiddleware.test.d.ts.map +1 -0
  149. package/dist/tests/logMiddleware.test.js +60 -0
  150. package/dist/tests/logMiddleware.test.js.map +1 -0
  151. package/dist/tests/minioService.test.d.ts +2 -0
  152. package/dist/tests/minioService.test.d.ts.map +1 -0
  153. package/dist/tests/minioService.test.js +206 -0
  154. package/dist/tests/minioService.test.js.map +1 -0
  155. package/dist/tests/platform.test.d.ts +2 -0
  156. package/dist/tests/platform.test.d.ts.map +1 -0
  157. package/dist/tests/platform.test.js +288 -0
  158. package/dist/tests/platform.test.js.map +1 -0
  159. package/dist/tests/proto.test.d.ts +2 -0
  160. package/dist/tests/proto.test.d.ts.map +1 -0
  161. package/dist/tests/proto.test.js +37 -0
  162. package/dist/tests/proto.test.js.map +1 -0
  163. package/dist/tests/unhandledRejections.test.d.ts +2 -0
  164. package/dist/tests/unhandledRejections.test.d.ts.map +1 -0
  165. package/dist/tests/unhandledRejections.test.js +204 -0
  166. package/dist/tests/unhandledRejections.test.js.map +1 -0
  167. package/dist/tests/uploadSecurity.test.d.ts +2 -0
  168. package/dist/tests/uploadSecurity.test.d.ts.map +1 -0
  169. package/dist/tests/uploadSecurity.test.js +219 -0
  170. package/dist/tests/uploadSecurity.test.js.map +1 -0
  171. package/dist/tests/workerSocket.test.d.ts +2 -0
  172. package/dist/tests/workerSocket.test.d.ts.map +1 -0
  173. package/dist/tests/workerSocket.test.js +53 -0
  174. package/dist/tests/workerSocket.test.js.map +1 -0
  175. package/package.json +70 -3
@@ -0,0 +1,809 @@
1
+ import { logger, trimSlash, uuid, wrap, WsCloseCodes } from '@araminix/lib';
2
+ import axios from 'axios';
3
+ import cookieParser from 'cookie-parser';
4
+ import expressCore from 'express';
5
+ import { ipKeyGenerator, rateLimit } from 'express-rate-limit';
6
+ import expressWs from 'express-ws';
7
+ import fs from 'fs';
8
+ import multer from 'multer';
9
+ import path from 'path';
10
+ import { logMiddleware, MimeToExtension, MinioService } from '../utils/index.js';
11
+ import { GoogleData } from './AuthProvider.js';
12
+ import EndpointController from './EndpointController.js';
13
+ function validateToken(req, res, next) {
14
+ if (!req.token || !req.info) {
15
+ res.status(401).send({ message: 'Unauthorized' }).end();
16
+ return;
17
+ }
18
+ const isMutation = ['POST', 'PUT', 'PATCH', 'DELETE'].includes(req.method.toUpperCase());
19
+ const isCookieAuth = Boolean(req.signedCookies?.token && !req.header('x-token'));
20
+ if (isMutation && isCookieAuth) {
21
+ const contentType = req.headers['content-type'];
22
+ const hasJson = Boolean(req.is('application/json') || (typeof contentType === 'string' && contentType.toLowerCase().includes('application/json')));
23
+ const hasCustomHeader = Boolean(req.header('x-requested-with') || req.header('x-csrf-token'));
24
+ if (!hasJson && !hasCustomHeader) {
25
+ res.status(403)
26
+ .json({
27
+ error: 'Forbidden: Content-Type: application/json or custom header (e.g. X-Requested-With) required for cookie-authenticated requests',
28
+ message: 'Forbidden',
29
+ })
30
+ .end();
31
+ return;
32
+ }
33
+ }
34
+ next();
35
+ }
36
+ export default class Endpoint {
37
+ authProvider;
38
+ port;
39
+ mount;
40
+ version;
41
+ cookieSecret;
42
+ sessionTtl;
43
+ uploadDir;
44
+ minio;
45
+ maxUploadBytes;
46
+ uploadAllowlist;
47
+ googleClientIds;
48
+ allowedOrigins;
49
+ authRateLimit;
50
+ authRateLimiter;
51
+ express;
52
+ server;
53
+ connections = new Map();
54
+ endpoints = new Map();
55
+ COOKIE_OPTIONS;
56
+ constructor({ authProvider, port = 3000, mount, version, cookieSecret = uuid(), sessionTtl = 3600 * 24, uploadDir, minio = undefined, cookieOptions, maxUploadBytes = 20 * 1024 * 1024, uploadAllowlist, googleClientIds, allowedOrigins, authRateLimit, }) {
57
+ this.authProvider = authProvider;
58
+ this.port = port;
59
+ this.mount = mount ? `/${trimSlash(mount)}` : '';
60
+ this.version = version ?? '0.0.0';
61
+ this.cookieSecret = cookieSecret;
62
+ this.sessionTtl = sessionTtl;
63
+ this.uploadDir = uploadDir;
64
+ this.minio = minio;
65
+ this.maxUploadBytes = maxUploadBytes;
66
+ this.uploadAllowlist = uploadAllowlist;
67
+ this.googleClientIds = googleClientIds;
68
+ this.allowedOrigins = allowedOrigins;
69
+ this.authRateLimit = authRateLimit;
70
+ const rateLimitEnabled = this.authRateLimit !== false;
71
+ if (rateLimitEnabled) {
72
+ const customConfig = typeof this.authRateLimit === 'object' ? this.authRateLimit : {};
73
+ this.authRateLimiter = rateLimit({
74
+ windowMs: customConfig.windowMs ?? 15 * 60 * 1000,
75
+ limit: customConfig.limit ?? customConfig.max ?? 100,
76
+ standardHeaders: true,
77
+ legacyHeaders: false,
78
+ validate: { xForwardedForHeader: false, default: false },
79
+ message: customConfig.message ?? {
80
+ message: 'Too many authentication attempts, please try again later.',
81
+ error: 'Too many requests',
82
+ },
83
+ keyGenerator: customConfig.keyGenerator ??
84
+ ((req) => {
85
+ const rawIp = req.ip || req.socket?.remoteAddress || req.headers['x-forwarded-for'] || '127.0.0.1';
86
+ const ip = ipKeyGenerator(typeof rawIp === 'string' ? rawIp : Array.isArray(rawIp) ? rawIp[0] : '127.0.0.1');
87
+ const account = req.body?.accountName || req.body?.username || req.body?.email || req.body?.account || req.body?.old_token || '';
88
+ return account ? `${ip}:${account}` : ip;
89
+ }),
90
+ ...customConfig,
91
+ });
92
+ }
93
+ else {
94
+ this.authRateLimiter = (_req, _res, next) => next();
95
+ }
96
+ this.COOKIE_OPTIONS = {
97
+ signed: true,
98
+ httpOnly: true,
99
+ secure: true,
100
+ sameSite: 'lax',
101
+ path: '/',
102
+ maxAge: this.sessionTtl * 1000,
103
+ ...cookieOptions,
104
+ };
105
+ const wsOptions = {
106
+ verifyClient: (info, cb) => {
107
+ const origin = info.origin || info.req?.headers?.origin || info.req?.headers?.['sec-websocket-origin'];
108
+ const allowed = this.checkOrigin(origin);
109
+ if (allowed) {
110
+ return cb(true);
111
+ }
112
+ return cb(false, 403, 'Forbidden');
113
+ },
114
+ };
115
+ this.express = expressWs(expressCore(), undefined, { wsOptions }).app;
116
+ this.init();
117
+ }
118
+ checkOrigin(origin) {
119
+ if (!origin) {
120
+ return true;
121
+ }
122
+ if (!this.allowedOrigins || this.allowedOrigins.length === 0) {
123
+ return false;
124
+ }
125
+ if (this.allowedOrigins.includes('*')) {
126
+ return true;
127
+ }
128
+ const normalized = origin.trim().toLowerCase().replace(/\/+$/, '');
129
+ return this.allowedOrigins.some((allowed) => {
130
+ return allowed.trim().toLowerCase().replace(/\/+$/, '') === normalized;
131
+ });
132
+ }
133
+ isOriginAllowed(req) {
134
+ const origin = req.headers?.origin || req.headers?.['sec-websocket-origin'];
135
+ return this.checkOrigin(origin);
136
+ }
137
+ init() {
138
+ this.express.use(cookieParser(this.cookieSecret));
139
+ this.express.use(expressCore.json());
140
+ this.express.use(logMiddleware);
141
+ this.initMiddleware();
142
+ this.initWs();
143
+ this.initAuth();
144
+ if (this.uploadDir) {
145
+ this.initMulter(this.uploadDir);
146
+ }
147
+ if (this.minio) {
148
+ this.initMinio(this.minio);
149
+ }
150
+ this.init2FA();
151
+ this.initMobileRequests();
152
+ return this.express;
153
+ }
154
+ declare(name) {
155
+ if (!this.endpoints.has(name)) {
156
+ const endpointController = new EndpointController();
157
+ this.endpoints.set(name, endpointController);
158
+ }
159
+ }
160
+ acquire(name) {
161
+ logger.trace(`Endpoint - acquire name: ${name}`);
162
+ this.declare(name);
163
+ return this.endpoints.get(name).acquire();
164
+ }
165
+ async handle(token, info, endpointName, arg, ws) {
166
+ const endpoint = this.endpoints.get(endpointName);
167
+ if (!endpoint) {
168
+ ws.close(WsCloseCodes.NotFound);
169
+ return;
170
+ }
171
+ await endpoint.connect(ws, token, info, arg);
172
+ }
173
+ deauth(info, token) {
174
+ this.endpoints.forEach((endpoint) => {
175
+ endpoint.deauthUser(info, token);
176
+ });
177
+ }
178
+ initMiddleware() {
179
+ this.express.use(async (req, _res, next) => {
180
+ const cookieToken = req.signedCookies?.token;
181
+ const headerToken = req.header('x-token');
182
+ req.token = cookieToken ? cookieToken : headerToken ? headerToken : undefined;
183
+ if (req.token) {
184
+ const result = await this.authProvider.getInfo(req.token);
185
+ if (result.isOk()) {
186
+ req.info = result.unwrap();
187
+ }
188
+ }
189
+ return next();
190
+ });
191
+ }
192
+ initWs() {
193
+ /**
194
+ * @deprecated Use /ws/:endpoint/:arg with cookie or x-token header instead.
195
+ */
196
+ this.express.ws(`${this.mount}/ws-token/:token/:endpoint/:arg`, async (ws, req) => {
197
+ try {
198
+ if (!this.isOriginAllowed(req)) {
199
+ ws.close(WsCloseCodes.Denied);
200
+ return;
201
+ }
202
+ const token = String(req.params.token);
203
+ const endpointName = String(req.params.endpoint);
204
+ const arg = String(req.params.arg ?? '');
205
+ const result = await this.authProvider.getInfo(token);
206
+ if (result.isErr()) {
207
+ ws.close(WsCloseCodes.Unauthorized);
208
+ return;
209
+ }
210
+ const clientInfo = result.unwrap();
211
+ await this.handle(token, clientInfo, endpointName, arg, ws);
212
+ }
213
+ catch (err) {
214
+ logger.error('Unhandled rejection in /ws-token/:token/:endpoint/:arg:', err);
215
+ try {
216
+ ws.close(WsCloseCodes.InternalError);
217
+ }
218
+ catch { }
219
+ }
220
+ });
221
+ /**
222
+ * @deprecated Use /ws/:endpoint with cookie or x-token header instead.
223
+ */
224
+ this.express.ws(`${this.mount}/ws-token/:token/:endpoint`, async (ws, req) => {
225
+ try {
226
+ if (!this.isOriginAllowed(req)) {
227
+ ws.close(WsCloseCodes.Denied);
228
+ return;
229
+ }
230
+ const token = String(req.params.token);
231
+ const endpointName = String(req.params.endpoint);
232
+ const arg = '';
233
+ const result = await this.authProvider.getInfo(token);
234
+ if (result.isErr()) {
235
+ ws.close(WsCloseCodes.Unauthorized);
236
+ return;
237
+ }
238
+ const info = result.unwrap();
239
+ await this.handle(token, info, endpointName, arg, ws);
240
+ }
241
+ catch (err) {
242
+ logger.error('Unhandled rejection in /ws-token/:token/:endpoint:', err);
243
+ try {
244
+ ws.close(WsCloseCodes.InternalError);
245
+ }
246
+ catch { }
247
+ }
248
+ });
249
+ this.express.ws(`${this.mount}/ws/:endpoint/:arg`, async (ws, req) => {
250
+ try {
251
+ if (!this.isOriginAllowed(req)) {
252
+ ws.close(WsCloseCodes.Denied);
253
+ return;
254
+ }
255
+ if (!req.info || !req.token) {
256
+ ws.close(WsCloseCodes.Unauthorized);
257
+ return;
258
+ }
259
+ const arg = String(req.params.arg ?? '');
260
+ const endpointName = String(req.params.endpoint);
261
+ await this.handle(req.token, req.info, endpointName, arg, ws);
262
+ }
263
+ catch (err) {
264
+ logger.error('Unhandled rejection in /ws/:endpoint/:arg:', err);
265
+ try {
266
+ ws.close(WsCloseCodes.InternalError);
267
+ }
268
+ catch { }
269
+ }
270
+ });
271
+ this.express.ws(`${this.mount}/ws/:endpoint`, async (ws, req) => {
272
+ try {
273
+ if (!this.isOriginAllowed(req)) {
274
+ ws.close(WsCloseCodes.Denied);
275
+ return;
276
+ }
277
+ if (!req.info || !req.token) {
278
+ ws.close(WsCloseCodes.Unauthorized);
279
+ return;
280
+ }
281
+ const arg = '';
282
+ const endpointName = String(req.params.endpoint);
283
+ await this.handle(req.token, req.info, endpointName, arg, ws);
284
+ }
285
+ catch (err) {
286
+ logger.error('Unhandled rejection in /ws/:endpoint:', err);
287
+ try {
288
+ ws.close(WsCloseCodes.InternalError);
289
+ }
290
+ catch { }
291
+ }
292
+ });
293
+ }
294
+ initAuth() {
295
+ this.express.get(`${this.mount}/health`, (_req, res) => {
296
+ res.status(200).send('200!').end();
297
+ });
298
+ this.express.get(`${this.mount}/version`, (_req, res) => {
299
+ res.status(200).json({ version: this.version }).end();
300
+ });
301
+ this.express.post(`${this.mount}/auth`, this.authRateLimiter, async (req, res) => {
302
+ const result = await this.authProvider.auth(req.body);
303
+ if (result.isErr()) {
304
+ logger.error('API Auth - Login Error: ', result.e);
305
+ res.status(401).send({ error: result.e }).end();
306
+ return;
307
+ }
308
+ const authResponse = result.unwrap();
309
+ if (typeof authResponse === 'object') {
310
+ res.status(200).send(authResponse).end();
311
+ }
312
+ else {
313
+ res.cookie('token', authResponse, this.COOKIE_OPTIONS).status(200).end();
314
+ }
315
+ });
316
+ // TODO (D6): Long-term improvement - have frontend send Google ID token and verify using google-auth-library
317
+ this.express.post(`${this.mount}/gauth`, this.authRateLimiter, async (req, res) => {
318
+ if (!this.googleClientIds || this.googleClientIds.length === 0) {
319
+ logger.error('Google Auth Error: "googleClientIds" is not configured on Endpoint.');
320
+ res.status(503).send({ message: 'Google authentication is not configured on this server.' }).end();
321
+ return;
322
+ }
323
+ const access_token = req.body?.access_token;
324
+ if (!access_token || typeof access_token !== 'string') {
325
+ logger.error('Google Auth Validation Error: "access_token" is required in body.');
326
+ res.status(422).send({ message: '"access_token" is required in body.' }).end();
327
+ return;
328
+ }
329
+ const tokenInfoResult = await wrap(axios.get(`https://oauth2.googleapis.com/tokeninfo?access_token=${encodeURIComponent(access_token)}`, {
330
+ timeout: 10000,
331
+ }));
332
+ if (tokenInfoResult.isErr()) {
333
+ logger.error('Google Auth Token Info Error: ', tokenInfoResult.e);
334
+ res.status(401).send({ error: tokenInfoResult.e }).end();
335
+ return;
336
+ }
337
+ const tokenInfoResponse = tokenInfoResult.unwrap();
338
+ if (tokenInfoResponse.status !== 200 || !tokenInfoResponse.data) {
339
+ logger.error('Google Auth Token Info Status not 200: ', tokenInfoResponse.data);
340
+ res.status(401).send({ error: tokenInfoResponse.data }).end();
341
+ return;
342
+ }
343
+ const tokenData = tokenInfoResponse.data;
344
+ const isAudValid = typeof tokenData.aud === 'string' && this.googleClientIds.includes(tokenData.aud);
345
+ const isAzpValid = typeof tokenData.azp === 'string' && this.googleClientIds.includes(tokenData.azp);
346
+ const isClientAuthorized = isAudValid || isAzpValid;
347
+ const isEmailVerified = tokenData.email_verified === 'true' || tokenData.email_verified === true;
348
+ if (!isClientAuthorized || !isEmailVerified) {
349
+ logger.error('Google Auth Token Verification Failed: token not issued for configured client or email not verified.', {
350
+ aud: tokenData.aud,
351
+ azp: tokenData.azp,
352
+ email_verified: tokenData.email_verified,
353
+ });
354
+ res.status(401).send({ error: 'Unauthorized Google token: client ID mismatch or email not verified.' }).end();
355
+ return;
356
+ }
357
+ const userInfoResult = await wrap(axios.get('https://www.googleapis.com/oauth2/v1/userinfo', {
358
+ headers: { Authorization: `Bearer ${access_token}`, Accept: 'application/json' },
359
+ timeout: 10000,
360
+ }));
361
+ if (userInfoResult.isErr()) {
362
+ logger.error('Google Auth API Error: ', userInfoResult.e);
363
+ res.status(401).send({ error: userInfoResult.e }).end();
364
+ return;
365
+ }
366
+ const authResponse = userInfoResult.unwrap();
367
+ if (authResponse.status !== 200) {
368
+ logger.error('Google Auth Status not 200: ', authResponse.data);
369
+ res.status(401).send({ error: authResponse.data }).end();
370
+ return;
371
+ }
372
+ const googleData = authResponse.data;
373
+ if (googleData?.verified_email === false) {
374
+ logger.error('Google Auth UserInfo Verification Error: email is not verified.', googleData);
375
+ res.status(401).send({ error: 'Google email is not verified.' }).end();
376
+ return;
377
+ }
378
+ if (tokenData.email && googleData?.email && tokenData.email.toLowerCase() !== googleData.email.toLowerCase()) {
379
+ logger.error('Google Auth Verification Error: tokeninfo email does not match userinfo email.', {
380
+ tokenEmail: tokenData.email,
381
+ userEmail: googleData.email,
382
+ });
383
+ res.status(401).send({ error: 'Google token email does not match userinfo.' }).end();
384
+ return;
385
+ }
386
+ if (GoogleData.is(googleData)) {
387
+ const authRes = await this.authProvider.googleAuth(googleData);
388
+ if (authRes.isErr()) {
389
+ logger.error('Google Auth Provider Error: ', authRes.e);
390
+ res.status(401).send({ error: authRes.e }).end();
391
+ return;
392
+ }
393
+ const response = authRes.unwrap();
394
+ if (typeof response === 'object') {
395
+ res.status(200).send(response).end();
396
+ return;
397
+ }
398
+ res.cookie('token', response, this.COOKIE_OPTIONS).status(200).end();
399
+ }
400
+ else {
401
+ logger.error('Google Data Read Error: ', googleData);
402
+ res.status(500).end();
403
+ }
404
+ });
405
+ this.express.delete(`${this.mount}/auth`, async (req, res) => {
406
+ if (req.token && req.info) {
407
+ await this.authProvider.destroy(req.token);
408
+ this.deauth(req.info, req.token);
409
+ }
410
+ res.clearCookie('token', this.COOKIE_OPTIONS).status(200).send({ success: true }).end();
411
+ });
412
+ this.express.put(`${this.mount}/auth/refresh`, this.authRateLimiter, async (req, res) => {
413
+ const result = await this.authProvider.refreshToken(req.body.old_token);
414
+ if (result.isErr()) {
415
+ logger.error('API Auth - Refresh Token Error: ', result.e);
416
+ res.status(401).send({ error: result.e }).end();
417
+ return;
418
+ }
419
+ const token = result.unwrap();
420
+ res.cookie('token', token, this.COOKIE_OPTIONS).status(200).end();
421
+ });
422
+ }
423
+ sanitizeFilename(originalName) {
424
+ const base = path
425
+ .basename(originalName)
426
+ .replace(/[/\\]/g, '')
427
+ .replace(/[\x00-\x1f\x7f-\x9f]/g, '')
428
+ .trim();
429
+ const safe = base === '.' || base === '..' || base === '' ? 'upload' : base;
430
+ return safe.slice(0, 255);
431
+ }
432
+ initMulter(uploadDir) {
433
+ const sanitize = (name) => this.sanitizeFilename(name);
434
+ const storage = multer.diskStorage({
435
+ destination: function (_req, file, cb) {
436
+ const id = uuid();
437
+ const dir = path.join(uploadDir, id);
438
+ fs.mkdirSync(dir, { recursive: true });
439
+ file.id = id;
440
+ cb(null, dir);
441
+ },
442
+ filename: function (_req, file, cb) {
443
+ file.originalname = sanitize(file.originalname || '');
444
+ cb(null, file.originalname);
445
+ },
446
+ });
447
+ const fileFilter = (_req, file, cb) => {
448
+ if (this.uploadAllowlist) {
449
+ const { extensions, mimetypes } = this.uploadAllowlist;
450
+ const safeName = sanitize(file.originalname || '');
451
+ const ext = path.extname(safeName).toLowerCase().replace(/^\./, '');
452
+ const mime = (file.mimetype || '').toLowerCase();
453
+ if (extensions && extensions.length > 0) {
454
+ const normalizedExts = extensions.map((e) => e.toLowerCase().replace(/^\./, ''));
455
+ if (!normalizedExts.includes(ext)) {
456
+ const err = new Error('File extension not allowed');
457
+ err.statusCode = 415;
458
+ cb(err);
459
+ return;
460
+ }
461
+ }
462
+ if (mimetypes && mimetypes.length > 0) {
463
+ const normalizedMimes = mimetypes.map((m) => m.toLowerCase());
464
+ if (!normalizedMimes.includes(mime)) {
465
+ const err = new Error('MIME type not allowed');
466
+ err.statusCode = 415;
467
+ cb(err);
468
+ return;
469
+ }
470
+ }
471
+ }
472
+ cb(null, true);
473
+ };
474
+ const upload = multer({
475
+ storage: storage,
476
+ limits: {
477
+ fileSize: this.maxUploadBytes,
478
+ files: 1,
479
+ },
480
+ fileFilter,
481
+ });
482
+ this.express.post(`${this.mount}/file`, validateToken, (req, res, next) => {
483
+ upload.single('file')(req, res, (err) => {
484
+ if (err) {
485
+ if (err instanceof multer.MulterError) {
486
+ if (err.code === 'LIMIT_FILE_SIZE') {
487
+ res.status(413).json({ error: `File exceeds maximum allowed size of ${this.maxUploadBytes} bytes` });
488
+ return;
489
+ }
490
+ res.status(400).json({ error: err.message });
491
+ return;
492
+ }
493
+ const status = err.statusCode || 400;
494
+ res.status(status).json({ error: err.message || 'Upload error' });
495
+ return;
496
+ }
497
+ if (!req.file) {
498
+ res.status(400).json({ error: 'No file uploaded' });
499
+ return;
500
+ }
501
+ next();
502
+ });
503
+ }, (req, res) => {
504
+ const { originalname, size, mimetype, id } = req.file;
505
+ const fileInfo = {
506
+ name: originalname,
507
+ size,
508
+ mimetype,
509
+ id,
510
+ userId: req.info?.id ?? '',
511
+ };
512
+ const infoPath = path.join(uploadDir, fileInfo.id, 'index.json');
513
+ fs.writeFile(infoPath, JSON.stringify(fileInfo), (err) => {
514
+ if (err) {
515
+ logger.error('Failed to write file metadata:', err);
516
+ res.status(500).json({ error: 'Failed to save file metadata' }).end();
517
+ return;
518
+ }
519
+ res.status(200).json(fileInfo).end();
520
+ });
521
+ });
522
+ this.express.get(`${this.mount}/file/:fileId`, validateToken, (req, res) => {
523
+ const fileId = String(req.params.fileId);
524
+ const dir = path.join(uploadDir, fileId);
525
+ fs.readFile(path.join(dir, 'index.json'), (err, data) => {
526
+ if (err) {
527
+ res.status(404).end();
528
+ }
529
+ else {
530
+ try {
531
+ const info = JSON.parse(data.toString());
532
+ if (!info || typeof info.name !== 'string') {
533
+ res.status(404).end();
534
+ return;
535
+ }
536
+ const safeName = encodeURIComponent(path.basename(info.name));
537
+ if (!safeName) {
538
+ res.status(404).end();
539
+ return;
540
+ }
541
+ res.redirect(302, `${req.baseUrl}${req.path}/${safeName}`);
542
+ }
543
+ catch (parseErr) {
544
+ logger.error('Corrupt index.json for fileId:', fileId, parseErr);
545
+ res.status(404).end();
546
+ }
547
+ }
548
+ });
549
+ });
550
+ this.express.use(`${this.mount}/file/`, validateToken, expressCore.static(this.uploadDir, {
551
+ index: false,
552
+ dotfiles: 'deny',
553
+ setHeaders: (res) => {
554
+ res.setHeader('X-Content-Type-Options', 'nosniff');
555
+ res.setHeader('Content-Disposition', 'attachment');
556
+ res.setHeader('Content-Security-Policy', "default-src 'none'; sandbox");
557
+ },
558
+ }));
559
+ }
560
+ init2FA() {
561
+ this.express.post(`${this.mount}/2fa/request`, validateToken, async (req, res) => {
562
+ try {
563
+ const result = await this.authProvider.request2FA(req.info?.id ?? '');
564
+ if (!result.isOk()) {
565
+ logger.error('API Auth - 2FA Request Error: ', result.e);
566
+ res.status(401).send({ error: result.e }).end();
567
+ return;
568
+ }
569
+ res.status(200).send({ url: result.v }).end();
570
+ }
571
+ catch (error) {
572
+ res.status(500).send({ error }).end();
573
+ }
574
+ });
575
+ /**
576
+ * Enables 2FA for the authenticated user.
577
+ *
578
+ * SECURITY REQUIREMENT:
579
+ * The AuthProvider implementation MUST verify that the `secret` in the request body
580
+ * matches the secret that the server issued in `request2FA` for this user, rather than
581
+ * blindly accepting an arbitrary secret provided by the client.
582
+ */
583
+ this.express.post(`${this.mount}/2fa/enable`, validateToken, async (req, res) => {
584
+ try {
585
+ const { code, secret } = req.body;
586
+ if (!code) {
587
+ res.status(400).send({ message: '2FA code is required' }).end();
588
+ return;
589
+ }
590
+ if (!secret) {
591
+ res.status(400).send({ message: '2FA secret is required' }).end();
592
+ return;
593
+ }
594
+ const result = await this.authProvider.enable2FA(req.info?.id ?? '', secret, code);
595
+ if (!result.isOk()) {
596
+ logger.error('API Auth - 2FA Enable Error: ', result.e);
597
+ res.status(401).send({ error: result.e }).end();
598
+ return;
599
+ }
600
+ res.status(200).send({ success: result.v }).end();
601
+ }
602
+ catch (error) {
603
+ res.status(500).send({ error }).end();
604
+ }
605
+ });
606
+ this.express.post(`${this.mount}/2fa/verify`, this.authRateLimiter, async (req, res) => {
607
+ try {
608
+ const { accountName, code, uuid: uuidCode } = req.body;
609
+ if (!accountName || !code || !uuidCode) {
610
+ res.status(400).send({ message: 'AccountName, code, and uuid are required.' }).end();
611
+ return;
612
+ }
613
+ const result = await this.authProvider.verify2FA(accountName, code, uuidCode);
614
+ if (result.isErr()) {
615
+ logger.error('API Auth - 2FA Verify Error: ', result.e);
616
+ res.status(401).send({ error: result.e }).end();
617
+ return;
618
+ }
619
+ const verifyResponse = result.unwrap();
620
+ if (typeof verifyResponse === 'boolean') {
621
+ res.status(200).send({ success: verifyResponse }).end();
622
+ }
623
+ else {
624
+ res.cookie('token', verifyResponse, this.COOKIE_OPTIONS).status(200).end();
625
+ }
626
+ }
627
+ catch (error) {
628
+ res.status(500).send({ error }).end();
629
+ }
630
+ });
631
+ this.express.delete(`${this.mount}/2fa/disable`, validateToken, async (req, res) => {
632
+ try {
633
+ const rawCode = req.body?.code ?? req.query?.code;
634
+ const code = rawCode !== undefined && rawCode !== null ? String(rawCode) : undefined;
635
+ const result = await this.authProvider.disable2FA(req.info?.id ?? '', code);
636
+ if (result.isErr()) {
637
+ logger.error('API Auth - 2FA Disable Error: ', result.e);
638
+ res.status(401).send({ error: result.e }).end();
639
+ return;
640
+ }
641
+ res.status(200).send({ success: result.unwrap() }).end();
642
+ }
643
+ catch (error) {
644
+ res.status(500).send({ error }).end();
645
+ }
646
+ });
647
+ }
648
+ initMinio(minio) {
649
+ const minioService = new MinioService(minio);
650
+ this.express.post(`${this.mount}/attachments/upload`, validateToken, async (req, res) => {
651
+ try {
652
+ if (!req.info?.id) {
653
+ res.status(401).send({ message: 'Unauthorized' }).end();
654
+ return;
655
+ }
656
+ const { mimetype, fileName } = req.body || {};
657
+ if (!mimetype || typeof mimetype !== 'string' || !fileName || typeof fileName !== 'string') {
658
+ res.status(400).send({ message: 'mimetype and fileName are required strings.' }).end();
659
+ return;
660
+ }
661
+ const decodedFileName = (() => {
662
+ try {
663
+ return decodeURIComponent(fileName);
664
+ }
665
+ catch {
666
+ return fileName;
667
+ }
668
+ })();
669
+ if (fileName.includes('..') ||
670
+ fileName.startsWith('/') ||
671
+ fileName.startsWith('\\') ||
672
+ decodedFileName.includes('..') ||
673
+ decodedFileName.startsWith('/') ||
674
+ decodedFileName.startsWith('\\')) {
675
+ res.status(400).send({ message: 'Invalid fileName: path traversal and leading slash are not allowed.' }).end();
676
+ return;
677
+ }
678
+ let ext = path.extname(fileName).replace(/^\./, '').trim().toLowerCase();
679
+ if (!ext && mimetype) {
680
+ const mimeExt = MimeToExtension[mimetype];
681
+ if (mimeExt && mimeExt !== '*') {
682
+ ext = mimeExt.toLowerCase();
683
+ }
684
+ }
685
+ if (ext && !/^[a-zA-Z0-9]+$/.test(ext)) {
686
+ ext = '';
687
+ }
688
+ const userId = req.info.id;
689
+ const fileId = uuid();
690
+ const key = ext ? `${userId}/${fileId}.${ext}` : `${userId}/${fileId}`;
691
+ const urlResult = await minioService.presignedPutObject(mimetype, key);
692
+ if (urlResult.isErr()) {
693
+ logger.error('Error on Upload Request API: ', urlResult.e);
694
+ res.status(500).send({ error: urlResult.e }).end();
695
+ return;
696
+ }
697
+ const url = urlResult.unwrap();
698
+ res.status(200).json({ url, key, attachmentId: key }).end();
699
+ }
700
+ catch (error) {
701
+ logger.error('Error on Upload Request API: ', error);
702
+ res.status(500).send({ error }).end();
703
+ }
704
+ });
705
+ const handleGetAttachment = async (req, res) => {
706
+ try {
707
+ if (!req.info?.id) {
708
+ res.status(401).send({ message: 'Unauthorized' }).end();
709
+ return;
710
+ }
711
+ let attachmentId = '';
712
+ if (req.params.userId && req.params.fileName) {
713
+ attachmentId = `${req.params.userId}/${req.params.fileName}`;
714
+ }
715
+ else if (req.params.attachmentId) {
716
+ attachmentId = String(req.params.attachmentId);
717
+ }
718
+ if (!attachmentId) {
719
+ res.status(400).send({ message: 'attachmentId is required.' }).end();
720
+ return;
721
+ }
722
+ const decoded = (() => {
723
+ try {
724
+ return decodeURIComponent(attachmentId);
725
+ }
726
+ catch {
727
+ return attachmentId;
728
+ }
729
+ })();
730
+ if (attachmentId.includes('..') ||
731
+ attachmentId.startsWith('/') ||
732
+ attachmentId.startsWith('\\') ||
733
+ decoded.includes('..') ||
734
+ decoded.startsWith('/') ||
735
+ decoded.startsWith('\\')) {
736
+ res.status(400).send({ message: 'Invalid attachmentId: path traversal and leading slash are not allowed.' }).end();
737
+ return;
738
+ }
739
+ const userId = req.info.id;
740
+ let objectKey;
741
+ if (attachmentId.startsWith(`${userId}/`)) {
742
+ objectKey = attachmentId;
743
+ }
744
+ else if (!attachmentId.includes('/')) {
745
+ objectKey = `${userId}/${attachmentId}`;
746
+ }
747
+ else {
748
+ logger.error(`IDOR attempt or unauthorized attachment access denied for user '${userId}': ${attachmentId}`);
749
+ res.status(403).send({ message: 'Forbidden: access to attachment denied.' }).end();
750
+ return;
751
+ }
752
+ if (objectKey === `${userId}/` || objectKey.endsWith('/')) {
753
+ res.status(400).send({ message: 'Invalid attachmentId: file name cannot be empty.' }).end();
754
+ return;
755
+ }
756
+ const fileURLResult = await minioService.presignedGetObject(objectKey);
757
+ if (fileURLResult.isErr()) {
758
+ logger.error('Error on Get File API: ', fileURLResult.e);
759
+ res.status(500).send({ error: fileURLResult.e }).end();
760
+ return;
761
+ }
762
+ const fileURL = fileURLResult.unwrap();
763
+ res.status(200).json({ fileURL }).end();
764
+ }
765
+ catch (error) {
766
+ logger.error('Error on Get File API: ', error);
767
+ res.status(500).send({ error }).end();
768
+ }
769
+ };
770
+ this.express.get(`${this.mount}/attachments/:attachmentId`, validateToken, handleGetAttachment);
771
+ this.express.get(`${this.mount}/attachments/:userId/:fileName`, validateToken, handleGetAttachment);
772
+ }
773
+ initMobileRequests() {
774
+ this.express.post(`${this.mount}/mobile/:module/:action`, validateToken, async (req, res) => {
775
+ try {
776
+ const moduleName = String(req.params.module);
777
+ const action = String(req.params.action);
778
+ if (!moduleName || !action) {
779
+ res.status(400).send({ error: 'Module and action are both required.' }).end();
780
+ return;
781
+ }
782
+ const result = await this.authProvider.mobileRequests({
783
+ module: moduleName,
784
+ action: action,
785
+ userId: req.info?.id ?? '',
786
+ data: req.body,
787
+ });
788
+ if (result.status !== 200) {
789
+ logger.error('API Auth - Mobile Request Error: ', result.error);
790
+ }
791
+ res.status(result.status).send(result).end();
792
+ }
793
+ catch (error) {
794
+ res.status(500).send({ error }).end();
795
+ }
796
+ });
797
+ }
798
+ open() {
799
+ if (!this.server) {
800
+ this.server = this.express.listen(this.port);
801
+ }
802
+ }
803
+ close() {
804
+ if (this.server) {
805
+ this.server.close();
806
+ }
807
+ }
808
+ }
809
+ //# sourceMappingURL=Endpoint.js.map