@flowerforce/flowerbase 1.6.2 → 1.6.3-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +49 -87
  2. package/dist/auth/controller.d.ts.map +1 -1
  3. package/dist/auth/controller.js +15 -3
  4. package/dist/auth/providers/anon-user/controller.d.ts.map +1 -1
  5. package/dist/auth/providers/anon-user/controller.js +5 -1
  6. package/dist/auth/providers/custom-function/schema.d.ts +1 -0
  7. package/dist/auth/providers/custom-function/schema.d.ts.map +1 -1
  8. package/dist/auth/providers/custom-function/schema.js +1 -0
  9. package/dist/auth/utils.d.ts +7 -0
  10. package/dist/auth/utils.d.ts.map +1 -1
  11. package/dist/auth/utils.js +6 -0
  12. package/dist/constants.d.ts +12 -0
  13. package/dist/constants.d.ts.map +1 -1
  14. package/dist/constants.js +26 -5
  15. package/dist/features/endpoints/utils.d.ts.map +1 -1
  16. package/dist/features/endpoints/utils.js +18 -0
  17. package/dist/features/functions/controller.d.ts.map +1 -1
  18. package/dist/features/functions/controller.js +10 -2
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +11 -1
  21. package/dist/monitoring/plugin.d.ts +7 -0
  22. package/dist/monitoring/plugin.d.ts.map +1 -0
  23. package/dist/monitoring/plugin.js +1319 -0
  24. package/dist/utils/initializer/exposeRoutes.d.ts.map +1 -1
  25. package/dist/utils/initializer/exposeRoutes.js +10 -2
  26. package/dist/utils/initializer/registerPlugins.d.ts.map +1 -1
  27. package/dist/utils/initializer/registerPlugins.js +10 -1
  28. package/package.json +5 -3
  29. package/src/auth/controller.ts +15 -1
  30. package/src/auth/providers/anon-user/controller.ts +5 -0
  31. package/src/auth/providers/custom-function/schema.ts +23 -24
  32. package/src/auth/utils.ts +6 -0
  33. package/src/constants.ts +22 -0
  34. package/src/features/endpoints/utils.ts +18 -0
  35. package/src/features/functions/controller.ts +10 -2
  36. package/src/index.ts +10 -1
  37. package/src/monitoring/plugin.ts +1501 -0
  38. package/src/monitoring/ui.css +1049 -0
  39. package/src/monitoring/ui.html +293 -0
  40. package/src/monitoring/ui.js +1931 -0
  41. package/src/utils/initializer/exposeRoutes.ts +10 -2
  42. package/src/utils/initializer/registerPlugins.ts +13 -2
@@ -0,0 +1,1319 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ const fastify_plugin_1 = __importDefault(require("fastify-plugin"));
16
+ const websocket_1 = __importDefault(require("@fastify/websocket"));
17
+ require("@fastify/websocket");
18
+ const node_fs_1 = __importDefault(require("node:fs"));
19
+ const node_path_1 = __importDefault(require("node:path"));
20
+ const mongodb_1 = require("mongodb");
21
+ const constants_1 = require("../constants");
22
+ const services_1 = require("../services");
23
+ const state_1 = require("../state");
24
+ const handleUserRegistration_1 = __importDefault(require("../shared/handleUserRegistration"));
25
+ const handleUserRegistration_model_1 = require("../shared/models/handleUserRegistration.model");
26
+ const crypto_1 = require("../utils/crypto");
27
+ const context_1 = require("../utils/context");
28
+ const utils_1 = require("../services/mongodb-atlas/utils");
29
+ const utils_2 = require("../utils/roles/machines/utils");
30
+ const DAY_MS = 24 * 60 * 60 * 1000;
31
+ const MAX_DEPTH = 15;
32
+ const MAX_ARRAY = 50;
33
+ const MAX_STRING = 500;
34
+ const COLLECTION_PAGE_SIZE = 50;
35
+ const MONIT_REALM = 'Flowerbase Monitor';
36
+ const isTestEnv = () => process.env.NODE_ENV === 'test' || process.env.JEST_WORKER_ID !== undefined;
37
+ const isPromiseLike = (value) => !!value && typeof value === 'object' && typeof value.then === 'function';
38
+ const isPlainObject = (value) => !!value && typeof value === 'object' && !Array.isArray(value);
39
+ const safeStringify = (value) => {
40
+ const seen = new WeakSet();
41
+ try {
42
+ return JSON.stringify(value, (key, val) => {
43
+ if (typeof val === 'bigint')
44
+ return val.toString();
45
+ if (val && typeof val === 'object') {
46
+ if (seen.has(val))
47
+ return '[Circular]';
48
+ seen.add(val);
49
+ }
50
+ return val;
51
+ });
52
+ }
53
+ catch (_a) {
54
+ return String(value);
55
+ }
56
+ };
57
+ const SENSITIVE_KEYS = [
58
+ /pass(word)?/i,
59
+ /secret/i,
60
+ /token/i,
61
+ /authorization/i,
62
+ /cookie/i,
63
+ /api[-_]?key/i,
64
+ /access[-_]?key/i,
65
+ /refresh[-_]?token/i,
66
+ /signature/i,
67
+ /private/i
68
+ ];
69
+ const redactString = (value) => {
70
+ let result = value;
71
+ result = result.replace(/(Bearer|Basic)\s+[A-Za-z0-9._+\\/=-]+/gi, '$1 [redacted]');
72
+ result = result.replace(/(password|pass|pwd|secret|token|api[_-]?key|access[_-]?key|refresh[_-]?token)\s*[=:]\s*[^\\s,;]+/gi, '$1=[redacted]');
73
+ if (result.length > MAX_STRING) {
74
+ result = result.slice(0, MAX_STRING) + '...[truncated]';
75
+ }
76
+ return result;
77
+ };
78
+ const stripSensitiveFields = (value) => {
79
+ const out = {};
80
+ Object.keys(value).forEach((key) => {
81
+ if (SENSITIVE_KEYS.some((re) => re.test(key)))
82
+ return;
83
+ out[key] = value[key];
84
+ });
85
+ return out;
86
+ };
87
+ const isErrorLike = (value) => {
88
+ if (!value || typeof value !== 'object')
89
+ return false;
90
+ if (value instanceof Error)
91
+ return true;
92
+ const record = value;
93
+ return (typeof record.message === 'string' ||
94
+ typeof record.stack === 'string' ||
95
+ typeof record.name === 'string');
96
+ };
97
+ const sanitizeErrorLike = (value, depth) => {
98
+ const out = {};
99
+ const names = new Set(Object.getOwnPropertyNames(value));
100
+ ['name', 'message', 'stack', 'code', 'statusCode', 'cause'].forEach((key) => names.add(key));
101
+ names.forEach((key) => {
102
+ if (SENSITIVE_KEYS.some((re) => re.test(key))) {
103
+ out[key] = '[redacted]';
104
+ return;
105
+ }
106
+ const raw = value[key];
107
+ if (raw === value) {
108
+ out[key] = '[Circular]';
109
+ return;
110
+ }
111
+ if ((key === 'message' || key === 'stack') && typeof raw === 'string') {
112
+ out[key] = constants_1.DEFAULT_CONFIG.MONIT_REDACT_ERROR_DETAILS ? redactString(raw) : raw;
113
+ return;
114
+ }
115
+ if (typeof raw === 'string') {
116
+ out[key] = redactString(raw);
117
+ return;
118
+ }
119
+ if (raw !== undefined) {
120
+ out[key] = sanitize(raw, depth + 1);
121
+ }
122
+ });
123
+ if (value instanceof Error) {
124
+ if (!out.name)
125
+ out.name = value.name;
126
+ if (!out.message) {
127
+ out.message = constants_1.DEFAULT_CONFIG.MONIT_REDACT_ERROR_DETAILS ? redactString(value.message) : value.message;
128
+ }
129
+ if (!out.stack && value.stack) {
130
+ out.stack = constants_1.DEFAULT_CONFIG.MONIT_REDACT_ERROR_DETAILS ? redactString(value.stack) : value.stack;
131
+ }
132
+ }
133
+ return out;
134
+ };
135
+ const getErrorDetails = (error) => {
136
+ if (isErrorLike(error)) {
137
+ const sanitized = sanitizeErrorLike(error, 0);
138
+ const message = typeof sanitized.message === 'string' && sanitized.message
139
+ ? sanitized.message
140
+ : typeof sanitized.name === 'string' && sanitized.name
141
+ ? sanitized.name
142
+ : safeStringify(error);
143
+ const stack = typeof sanitized.stack === 'string' ? sanitized.stack : undefined;
144
+ return { message, stack };
145
+ }
146
+ if (typeof error === 'string')
147
+ return { message: error };
148
+ return { message: safeStringify(error) };
149
+ };
150
+ const sanitize = (value, depth = 0) => {
151
+ if (depth > MAX_DEPTH)
152
+ return '[max-depth]';
153
+ if (value === null || value === undefined)
154
+ return value;
155
+ if (value instanceof Date)
156
+ return value.toISOString();
157
+ if (Buffer.isBuffer(value))
158
+ return `[buffer ${value.length} bytes]`;
159
+ if (isErrorLike(value)) {
160
+ return sanitizeErrorLike(value, depth);
161
+ }
162
+ if (typeof value === 'object') {
163
+ const maybeObjectId = value;
164
+ if ((maybeObjectId === null || maybeObjectId === void 0 ? void 0 : maybeObjectId._bsontype) === 'ObjectId' && typeof maybeObjectId.toString === 'function') {
165
+ return maybeObjectId.toString();
166
+ }
167
+ }
168
+ if (typeof value === 'string')
169
+ return redactString(value);
170
+ if (typeof value === 'number' || typeof value === 'boolean')
171
+ return value;
172
+ if (typeof value === 'bigint')
173
+ return value.toString();
174
+ if (Array.isArray(value)) {
175
+ const items = value.slice(0, MAX_ARRAY).map((item) => sanitize(item, depth + 1));
176
+ if (value.length > MAX_ARRAY) {
177
+ items.push(`[+${value.length - MAX_ARRAY} items]`);
178
+ }
179
+ return items;
180
+ }
181
+ if (typeof value === 'object') {
182
+ const obj = value;
183
+ const out = {};
184
+ Object.keys(obj).forEach((key) => {
185
+ if (SENSITIVE_KEYS.some((re) => re.test(key))) {
186
+ out[key] = '[redacted]';
187
+ return;
188
+ }
189
+ out[key] = sanitize(obj[key], depth + 1);
190
+ });
191
+ return out;
192
+ }
193
+ return value;
194
+ };
195
+ const pickHeaders = (headers) => {
196
+ const keys = ['user-agent', 'content-type', 'x-forwarded-for', 'host', 'origin', 'referer'];
197
+ const picked = {};
198
+ keys.forEach((key) => {
199
+ const value = headers[key];
200
+ if (value)
201
+ picked[key] = value;
202
+ });
203
+ return sanitize(picked);
204
+ };
205
+ const createEventStore = (maxAgeMs, maxEvents) => {
206
+ const events = [];
207
+ const trim = () => {
208
+ const cutoff = Date.now() - maxAgeMs;
209
+ while (events.length && events[0].ts < cutoff) {
210
+ events.shift();
211
+ }
212
+ if (events.length > maxEvents) {
213
+ events.splice(0, events.length - maxEvents);
214
+ }
215
+ };
216
+ return {
217
+ add(event) {
218
+ events.push(event);
219
+ trim();
220
+ },
221
+ list(query) {
222
+ trim();
223
+ let result = events.slice();
224
+ if (query === null || query === void 0 ? void 0 : query.type) {
225
+ result = result.filter((event) => event.type === query.type);
226
+ }
227
+ if (query === null || query === void 0 ? void 0 : query.q) {
228
+ const q = query.q.toLowerCase();
229
+ result = result.filter((event) => safeStringify(event).toLowerCase().includes(q));
230
+ }
231
+ if ((query === null || query === void 0 ? void 0 : query.limit) && query.limit > 0) {
232
+ result = result.slice(-query.limit);
233
+ }
234
+ return result;
235
+ },
236
+ clear() {
237
+ events.length = 0;
238
+ }
239
+ };
240
+ };
241
+ const classifyRequest = (url) => {
242
+ if (url.includes('/auth') || url.includes('/login') || url.includes('/register') || url.includes('/logout')) {
243
+ return 'auth';
244
+ }
245
+ if (url.includes('/functions'))
246
+ return 'function';
247
+ if (url.includes('/endpoint/'))
248
+ return 'http_endpoint';
249
+ if (url.includes('/triggers'))
250
+ return 'trigger';
251
+ if (url.includes('/api/'))
252
+ return 'api';
253
+ return 'http';
254
+ };
255
+ const createEventId = () => `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
256
+ const buildRulesMeta = (meta) => {
257
+ var _a, _b, _c;
258
+ if (!meta || meta.serviceName !== 'mongodb-atlas' || !meta.collection || !meta.rules) {
259
+ return undefined;
260
+ }
261
+ const collectionRules = meta.rules[meta.collection];
262
+ if (!collectionRules) {
263
+ return { collection: meta.collection, roles: [], filters: [] };
264
+ }
265
+ const filters = (_a = collectionRules.filters) !== null && _a !== void 0 ? _a : [];
266
+ const roles = (_b = collectionRules.roles) !== null && _b !== void 0 ? _b : [];
267
+ const user = ((_c = meta.user) !== null && _c !== void 0 ? _c : {});
268
+ const matchedFilters = (0, utils_1.getValidRule)({ filters, user });
269
+ const matchedFilterNames = matchedFilters.map((filter) => filter.name);
270
+ const matchedFilterQueries = matchedFilters.map((filter) => filter.query);
271
+ const roleCandidates = roles
272
+ .filter((role) => (0, utils_2.checkApplyWhen)(role.apply_when, user, null))
273
+ .map((role) => role.name);
274
+ return {
275
+ collection: meta.collection,
276
+ roles: roles.map((role) => role.name),
277
+ roleCandidates,
278
+ filters: filters.map((filter) => filter.name),
279
+ matchedFilters: matchedFilterNames,
280
+ matchedFilterQueries,
281
+ runAsSystem: !!meta.runAsSystem
282
+ };
283
+ };
284
+ const buildCollectionRulesSnapshot = (rules, collection, user, runAsSystem) => {
285
+ const collectionRules = rules === null || rules === void 0 ? void 0 : rules[collection];
286
+ return collectionRules !== null && collectionRules !== void 0 ? collectionRules : null;
287
+ };
288
+ const resolveAssetCandidates = (filename, prefix) => {
289
+ const rootDir = process.cwd();
290
+ const cleanPrefix = prefix.replace(/^\/+/, '').replace(/\/+$/, '');
291
+ const candidates = [];
292
+ const addCandidate = (candidate) => {
293
+ if (!candidates.includes(candidate))
294
+ candidates.push(candidate);
295
+ };
296
+ if (cleanPrefix) {
297
+ addCandidate(node_path_1.default.join(rootDir, cleanPrefix, filename));
298
+ }
299
+ addCandidate(node_path_1.default.join(rootDir, 'monitoring', filename));
300
+ // Fallbacks: try package-local assets (works in dev and when bundled).
301
+ addCandidate(node_path_1.default.join(__dirname, filename));
302
+ addCandidate(node_path_1.default.join(__dirname, '..', '..', 'src', 'monitoring', filename));
303
+ return candidates;
304
+ };
305
+ const resolveUserContext = (app, userId, userPayload) => __awaiter(void 0, void 0, void 0, function* () {
306
+ var _a, _b;
307
+ if (userPayload && typeof userPayload === 'object') {
308
+ return stripSensitiveFields(userPayload);
309
+ }
310
+ if (!userId)
311
+ return undefined;
312
+ const normalizedUserId = userId.trim();
313
+ const db = app.mongo.client.db(constants_1.DB_NAME);
314
+ const authCollection = (_a = constants_1.AUTH_CONFIG.authCollection) !== null && _a !== void 0 ? _a : 'auth_users';
315
+ const userCollection = constants_1.AUTH_CONFIG.userCollection;
316
+ const userIdField = (_b = constants_1.AUTH_CONFIG.user_id_field) !== null && _b !== void 0 ? _b : 'id';
317
+ const isObjectId = mongodb_1.ObjectId.isValid(normalizedUserId);
318
+ const authSelector = isObjectId
319
+ ? { _id: new mongodb_1.ObjectId(normalizedUserId) }
320
+ : { id: normalizedUserId };
321
+ const authUser = yield db.collection(authCollection).findOne(authSelector);
322
+ let customUser = null;
323
+ if (userCollection) {
324
+ const customSelector = { [userIdField]: normalizedUserId };
325
+ customUser = yield db.collection(userCollection).findOne(customSelector);
326
+ if (!customUser && isObjectId) {
327
+ customUser = yield db.collection(userCollection).findOne({ _id: new mongodb_1.ObjectId(normalizedUserId) });
328
+ }
329
+ }
330
+ const id = authUser && typeof authUser._id !== 'undefined'
331
+ ? String(authUser._id)
332
+ : (customUser && typeof customUser[userIdField] !== 'undefined'
333
+ ? String(customUser[userIdField])
334
+ : normalizedUserId);
335
+ const user_data = Object.assign(Object.assign({}, (customUser ? stripSensitiveFields(customUser) : {})), { id, _id: id, email: authUser && typeof authUser.email === 'string'
336
+ ? authUser.email
337
+ : undefined });
338
+ const user = {
339
+ id,
340
+ user_data,
341
+ data: user_data,
342
+ custom_data: user_data
343
+ };
344
+ if (isObjectId) {
345
+ user._id = new mongodb_1.ObjectId(id);
346
+ }
347
+ return user;
348
+ });
349
+ const getUserInfo = (resolvedUser) => {
350
+ var _a;
351
+ if (!resolvedUser || typeof resolvedUser !== 'object')
352
+ return undefined;
353
+ const record = resolvedUser;
354
+ const id = typeof record.id === 'string' ? record.id : undefined;
355
+ const email = typeof record.email === 'string'
356
+ ? record.email
357
+ : (typeof ((_a = record.user_data) === null || _a === void 0 ? void 0 : _a.email) === 'string' ? record.user_data.email : undefined);
358
+ if (!id && !email)
359
+ return undefined;
360
+ return { id, email };
361
+ };
362
+ const readAsset = (filename, prefix) => {
363
+ const candidates = resolveAssetCandidates(filename, prefix);
364
+ for (const candidate of candidates) {
365
+ try {
366
+ if (node_fs_1.default.existsSync(candidate)) {
367
+ return node_fs_1.default.readFileSync(candidate, 'utf8');
368
+ }
369
+ }
370
+ catch (_a) {
371
+ // ignore and try next
372
+ }
373
+ }
374
+ return '';
375
+ };
376
+ const wrapServicesForMonitoring = (addEvent) => {
377
+ const wrapped = services_1.services;
378
+ if (wrapped.__monitWrapped)
379
+ return;
380
+ wrapped.__monitWrapped = true;
381
+ const serviceTypeMap = {
382
+ api: 'api',
383
+ aws: 'aws',
384
+ auth: 'auth',
385
+ 'mongodb-atlas': 'mongo'
386
+ };
387
+ const initMethodMap = {
388
+ aws: new Set(['lambda', 's3']),
389
+ 'mongodb-atlas': new Set(['db', 'collection', 'limit', 'skip', 'toArray'])
390
+ };
391
+ const cache = new WeakMap();
392
+ const wrapValue = (value, path, serviceName, meta) => {
393
+ if (!value || (typeof value !== 'object' && typeof value !== 'function'))
394
+ return value;
395
+ if (isPromiseLike(value))
396
+ return value;
397
+ if (cache.has(value)) {
398
+ return cache.get(value);
399
+ }
400
+ const handler = {
401
+ get(target, prop, receiver) {
402
+ const propValue = Reflect.get(target, prop, receiver);
403
+ if (typeof prop === 'symbol')
404
+ return propValue;
405
+ if (prop === 'constructor' || prop === 'toJSON')
406
+ return propValue;
407
+ if (typeof propValue === 'function') {
408
+ const propName = String(prop);
409
+ const fnPath = `${path}.${propName}`;
410
+ const wrappedFn = (...args) => {
411
+ var _a, _b;
412
+ let nextMeta = meta;
413
+ if (serviceName === 'mongodb-atlas') {
414
+ if (propName === 'db' && typeof args[0] === 'string') {
415
+ nextMeta = Object.assign(Object.assign({}, (meta !== null && meta !== void 0 ? meta : { serviceName })), { dbName: args[0], serviceName });
416
+ }
417
+ if (propName === 'collection' && typeof args[0] === 'string') {
418
+ nextMeta = Object.assign(Object.assign({}, (meta !== null && meta !== void 0 ? meta : { serviceName })), { collection: args[0], serviceName });
419
+ }
420
+ }
421
+ const shouldLog = !((_a = initMethodMap[serviceName]) === null || _a === void 0 ? void 0 : _a.has(propName));
422
+ if (shouldLog) {
423
+ const ruleInfo = buildRulesMeta(nextMeta !== null && nextMeta !== void 0 ? nextMeta : meta);
424
+ addEvent({
425
+ id: createEventId(),
426
+ ts: Date.now(),
427
+ type: (_b = serviceTypeMap[serviceName]) !== null && _b !== void 0 ? _b : 'service',
428
+ source: `service:${serviceName}`,
429
+ message: fnPath,
430
+ data: sanitize({ args, rules: ruleInfo })
431
+ });
432
+ }
433
+ let result;
434
+ try {
435
+ result = propValue.apply(target, args);
436
+ }
437
+ catch (error) {
438
+ addEvent({
439
+ id: createEventId(),
440
+ ts: Date.now(),
441
+ type: 'error',
442
+ source: `service:${serviceName}`,
443
+ message: `error ${fnPath}`,
444
+ data: sanitize({ error })
445
+ });
446
+ throw error;
447
+ }
448
+ if (isPromiseLike(result)) {
449
+ return result.catch((error) => {
450
+ addEvent({
451
+ id: createEventId(),
452
+ ts: Date.now(),
453
+ type: 'error',
454
+ source: `service:${serviceName}`,
455
+ message: `error ${fnPath}`,
456
+ data: sanitize({ error })
457
+ });
458
+ throw error;
459
+ });
460
+ }
461
+ return wrapValue(result, fnPath, serviceName, nextMeta);
462
+ };
463
+ return wrappedFn;
464
+ }
465
+ return wrapValue(propValue, `${path}.${String(prop)}`, serviceName, meta);
466
+ }
467
+ };
468
+ const proxied = new Proxy(value, handler);
469
+ cache.set(value, proxied);
470
+ return proxied;
471
+ };
472
+ Object.keys(services_1.services).forEach((serviceName) => {
473
+ const original = wrapped[serviceName];
474
+ wrapped[serviceName] = (app, options) => {
475
+ const instance = original(app, options);
476
+ const meta = {
477
+ serviceName,
478
+ rules: options.rules,
479
+ user: options.user,
480
+ runAsSystem: Boolean(options.run_as_system)
481
+ };
482
+ return wrapValue(instance, serviceName, serviceName, meta);
483
+ };
484
+ });
485
+ };
486
+ const createMonitoringPlugin = (0, fastify_plugin_1.default)((app_1, ...args_1) => __awaiter(void 0, [app_1, ...args_1], void 0, function* (app, opts = {}) {
487
+ if (isTestEnv())
488
+ return;
489
+ const enabled = constants_1.DEFAULT_CONFIG.MONIT_ENABLED;
490
+ if (!enabled)
491
+ return;
492
+ const rawPrefix = typeof opts.basePath === 'string' ? opts.basePath : '/monit';
493
+ const normalizedPrefix = rawPrefix.startsWith('/') ? rawPrefix : `/${rawPrefix}`;
494
+ const prefix = normalizedPrefix.endsWith('/')
495
+ ? normalizedPrefix.slice(0, -1)
496
+ : normalizedPrefix;
497
+ const maxAgeMs = Math.max(1, constants_1.DEFAULT_CONFIG.MONIT_CACHE_HOURS) * 60 * 60 * 1000;
498
+ const maxEvents = Math.max(1000, constants_1.DEFAULT_CONFIG.MONIT_MAX_EVENTS);
499
+ const allowedIps = constants_1.DEFAULT_CONFIG.MONIT_ALLOWED_IPS;
500
+ const rateLimitWindowMs = Math.max(0, constants_1.DEFAULT_CONFIG.MONIT_RATE_LIMIT_WINDOW_MS);
501
+ const rateLimitMax = Math.max(0, constants_1.DEFAULT_CONFIG.MONIT_RATE_LIMIT_MAX);
502
+ const allowInvoke = constants_1.DEFAULT_CONFIG.MONIT_ALLOW_INVOKE;
503
+ const allowEdit = constants_1.DEFAULT_CONFIG.MONIT_ALLOW_EDIT;
504
+ const eventStore = createEventStore(maxAgeMs || DAY_MS, maxEvents);
505
+ const functionHistory = [];
506
+ const maxHistory = 30;
507
+ const collectionHistory = [];
508
+ const maxCollectionHistory = 30;
509
+ const statsState = {
510
+ lastCpu: process.cpuUsage(),
511
+ lastHr: process.hrtime.bigint(),
512
+ maxRssMb: 0,
513
+ maxCpu: 0
514
+ };
515
+ const clients = new Set();
516
+ const addFunctionHistory = (entry) => {
517
+ functionHistory.unshift(entry);
518
+ if (functionHistory.length > maxHistory) {
519
+ functionHistory.splice(maxHistory);
520
+ }
521
+ };
522
+ const addCollectionHistory = (entry) => {
523
+ collectionHistory.unshift(entry);
524
+ if (collectionHistory.length > maxCollectionHistory) {
525
+ collectionHistory.splice(maxCollectionHistory);
526
+ }
527
+ };
528
+ const rateBucket = new Map();
529
+ const isRateLimited = (key) => {
530
+ if (rateLimitMax <= 0 || rateLimitWindowMs <= 0)
531
+ return false;
532
+ const now = Date.now();
533
+ const current = rateBucket.get(key);
534
+ if (!current || now > current.resetAt) {
535
+ rateBucket.set(key, { count: 1, resetAt: now + rateLimitWindowMs });
536
+ return false;
537
+ }
538
+ current.count += 1;
539
+ if (current.count > rateLimitMax) {
540
+ return true;
541
+ }
542
+ return false;
543
+ };
544
+ const round1 = (value) => Math.round(value * 10) / 10;
545
+ const getStats = () => {
546
+ const mem = process.memoryUsage();
547
+ const rssMb = mem.rss / (1024 * 1024);
548
+ const now = process.hrtime.bigint();
549
+ const currentCpu = process.cpuUsage();
550
+ const deltaCpu = {
551
+ user: currentCpu.user - statsState.lastCpu.user,
552
+ system: currentCpu.system - statsState.lastCpu.system
553
+ };
554
+ const deltaTimeMicros = Number(now - statsState.lastHr) / 1000;
555
+ const cpuPercent = deltaTimeMicros > 0
556
+ ? ((deltaCpu.user + deltaCpu.system) / deltaTimeMicros) * 100
557
+ : 0;
558
+ statsState.lastCpu = currentCpu;
559
+ statsState.lastHr = now;
560
+ statsState.maxRssMb = Math.max(statsState.maxRssMb, rssMb);
561
+ statsState.maxCpu = Math.max(statsState.maxCpu, cpuPercent);
562
+ return {
563
+ ramMb: round1(rssMb),
564
+ cpuPercent: round1(cpuPercent),
565
+ topRamMb: round1(statsState.maxRssMb),
566
+ topCpuPercent: round1(statsState.maxCpu),
567
+ uptimeSec: Math.round(process.uptime())
568
+ };
569
+ };
570
+ const addEvent = (event) => {
571
+ const sanitizedEvent = Object.assign(Object.assign({}, event), { data: sanitize(event.data), message: redactString(event.message) });
572
+ eventStore.add(sanitizedEvent);
573
+ const payload = JSON.stringify({ type: 'event', event: sanitizedEvent });
574
+ clients.forEach((client) => {
575
+ if (client.readyState !== 1)
576
+ return;
577
+ try {
578
+ client.send(payload);
579
+ }
580
+ catch (_a) {
581
+ clients.delete(client);
582
+ }
583
+ });
584
+ };
585
+ wrapServicesForMonitoring(addEvent);
586
+ if (constants_1.DEFAULT_CONFIG.MONIT_CAPTURE_CONSOLE) {
587
+ const original = {
588
+ log: console.log,
589
+ error: console.error,
590
+ warn: console.warn
591
+ };
592
+ console.log = (...args) => {
593
+ addEvent({
594
+ id: createEventId(),
595
+ ts: Date.now(),
596
+ type: 'log',
597
+ source: 'console',
598
+ message: args.map((item) => (typeof item === 'string' ? item : safeStringify(item))).join(' '),
599
+ data: sanitize(args)
600
+ });
601
+ original.log(...args);
602
+ };
603
+ console.error = (...args) => {
604
+ addEvent({
605
+ id: createEventId(),
606
+ ts: Date.now(),
607
+ type: 'error',
608
+ source: 'console',
609
+ message: args.map((item) => (typeof item === 'string' ? item : safeStringify(item))).join(' '),
610
+ data: sanitize(args)
611
+ });
612
+ original.error(...args);
613
+ };
614
+ console.warn = (...args) => {
615
+ addEvent({
616
+ id: createEventId(),
617
+ ts: Date.now(),
618
+ type: 'warn',
619
+ source: 'console',
620
+ message: args.map((item) => (typeof item === 'string' ? item : safeStringify(item))).join(' '),
621
+ data: sanitize(args)
622
+ });
623
+ original.warn(...args);
624
+ };
625
+ }
626
+ const hasCredentials = () => constants_1.DEFAULT_CONFIG.MONIT_USER && constants_1.DEFAULT_CONFIG.MONIT_PASSWORD;
627
+ const isAuthorized = (req) => {
628
+ if (!hasCredentials())
629
+ return false;
630
+ const header = req.headers.authorization;
631
+ if (!header || !header.startsWith('Basic '))
632
+ return false;
633
+ const encoded = header.slice('Basic '.length);
634
+ const decoded = Buffer.from(encoded, 'base64').toString('utf8');
635
+ const [user, pass] = decoded.split(':');
636
+ return user === constants_1.DEFAULT_CONFIG.MONIT_USER && pass === constants_1.DEFAULT_CONFIG.MONIT_PASSWORD;
637
+ };
638
+ const isMonitRoute = (url) => {
639
+ const path = url.split('?')[0];
640
+ return path === prefix || path.startsWith(`${prefix}/`);
641
+ };
642
+ const shouldSkipLog = (req) => isMonitRoute(req.url);
643
+ app.addHook('onRequest', (req, reply, done) => {
644
+ if (isMonitRoute(req.url)) {
645
+ const audit = (status, reason) => {
646
+ addEvent({
647
+ id: createEventId(),
648
+ ts: Date.now(),
649
+ type: 'auth',
650
+ source: 'monit',
651
+ message: `monit ${status}`,
652
+ data: sanitize({
653
+ status,
654
+ reason,
655
+ ip: req.ip,
656
+ method: req.method,
657
+ path: req.url
658
+ })
659
+ });
660
+ };
661
+ const allowAllIps = allowedIps.includes('0.0.0.0') || allowedIps.includes('*');
662
+ if (allowedIps.length && !allowAllIps && !allowedIps.includes(req.ip)) {
663
+ audit('deny', 'ip');
664
+ reply.code(403).send({ message: 'Forbidden' });
665
+ return;
666
+ }
667
+ if (isRateLimited(req.ip)) {
668
+ audit('deny', 'rate_limit');
669
+ reply.code(429).send({ message: 'Too Many Requests' });
670
+ return;
671
+ }
672
+ if (!hasCredentials()) {
673
+ audit('deny', 'missing_credentials');
674
+ reply.code(503).send({ message: 'Monitoring credentials not configured' });
675
+ return;
676
+ }
677
+ if (!isAuthorized(req)) {
678
+ audit('deny', 'basic_auth');
679
+ reply
680
+ .code(401)
681
+ .header('WWW-Authenticate', `Basic realm="${MONIT_REALM}"`)
682
+ .send({ message: 'Unauthorized' });
683
+ return;
684
+ }
685
+ }
686
+ ;
687
+ req.__monitStart = Date.now();
688
+ done();
689
+ });
690
+ app.addHook('onResponse', (req, reply, done) => {
691
+ var _a, _b, _c;
692
+ if (shouldSkipLog(req)) {
693
+ done();
694
+ return;
695
+ }
696
+ const start = (_a = req.__monitStart) !== null && _a !== void 0 ? _a : Date.now();
697
+ const duration = Date.now() - start;
698
+ const url = (_b = req.url) !== null && _b !== void 0 ? _b : '';
699
+ const type = classifyRequest(url);
700
+ const data = {
701
+ method: req.method,
702
+ url,
703
+ statusCode: reply.statusCode,
704
+ durationMs: duration,
705
+ ip: req.ip,
706
+ query: sanitize(req.query),
707
+ headers: pickHeaders(req.headers)
708
+ };
709
+ if (type === 'function') {
710
+ const body = req.body;
711
+ if (body === null || body === void 0 ? void 0 : body.name) {
712
+ data.function = body.name;
713
+ data.arguments = sanitize(body.arguments);
714
+ }
715
+ }
716
+ if (type === 'auth') {
717
+ const body = req.body;
718
+ if ((body === null || body === void 0 ? void 0 : body.email) || (body === null || body === void 0 ? void 0 : body.username)) {
719
+ data.user = (_c = body.email) !== null && _c !== void 0 ? _c : body.username;
720
+ }
721
+ }
722
+ addEvent({
723
+ id: createEventId(),
724
+ ts: Date.now(),
725
+ type,
726
+ source: 'http',
727
+ message: `${req.method} ${url} -> ${reply.statusCode} (${duration}ms)`,
728
+ data
729
+ });
730
+ done();
731
+ });
732
+ app.addHook('onError', (req, reply, error, done) => {
733
+ var _a;
734
+ if (!shouldSkipLog(req)) {
735
+ addEvent({
736
+ id: createEventId(),
737
+ ts: Date.now(),
738
+ type: 'error',
739
+ source: 'http',
740
+ message: `${req.method} ${req.url} -> error`,
741
+ data: sanitize({ error: (_a = error === null || error === void 0 ? void 0 : error.message) !== null && _a !== void 0 ? _a : error })
742
+ });
743
+ }
744
+ done();
745
+ });
746
+ yield app.register(websocket_1.default);
747
+ const sendUi = (_req, reply) => __awaiter(void 0, void 0, void 0, function* () {
748
+ const raw = readAsset('ui.html', prefix);
749
+ if (!raw) {
750
+ const tried = resolveAssetCandidates('ui.html', prefix).join(', ');
751
+ reply.code(404).send(`ui.html not found. Tried: ${tried}`);
752
+ return;
753
+ }
754
+ const html = raw.replace(/__MONIT_BASE__/g, prefix);
755
+ reply.header('Cache-Control', 'no-store');
756
+ reply.type('text/html').send(html);
757
+ });
758
+ app.get(prefix, sendUi);
759
+ app.get(`${prefix}/`, sendUi);
760
+ app.get(`${prefix}/ui.css`, (_req, reply) => __awaiter(void 0, void 0, void 0, function* () {
761
+ const css = readAsset('ui.css', prefix);
762
+ if (!css) {
763
+ const tried = resolveAssetCandidates('ui.css', prefix).join(', ');
764
+ reply.code(404).send(`ui.css not found. Tried: ${tried}`);
765
+ return;
766
+ }
767
+ reply.header('Cache-Control', 'no-store');
768
+ reply.type('text/css').send(css);
769
+ }));
770
+ app.get(`${prefix}/ui.js`, (_req, reply) => __awaiter(void 0, void 0, void 0, function* () {
771
+ const raw = readAsset('ui.js', prefix);
772
+ if (!raw) {
773
+ const tried = resolveAssetCandidates('ui.js', prefix).join(', ');
774
+ reply.code(404).send(`ui.js not found. Tried: ${tried}`);
775
+ return;
776
+ }
777
+ const js = raw.replace(/__MONIT_BASE__/g, prefix);
778
+ reply.header('Cache-Control', 'no-store');
779
+ reply.type('application/javascript').send(js);
780
+ }));
781
+ app.get(`${prefix}/ws`, { websocket: true }, (connection) => {
782
+ var _a;
783
+ const socket = (_a = connection
784
+ .socket) !== null && _a !== void 0 ? _a : connection;
785
+ clients.add(socket);
786
+ socket.send(JSON.stringify({ type: 'init', events: eventStore.list({ limit: maxEvents }) }));
787
+ addEvent({
788
+ id: createEventId(),
789
+ ts: Date.now(),
790
+ type: 'log',
791
+ source: 'monit',
792
+ message: 'websocket connected'
793
+ });
794
+ socket.on('close', () => {
795
+ clients.delete(socket);
796
+ addEvent({
797
+ id: createEventId(),
798
+ ts: Date.now(),
799
+ type: 'log',
800
+ source: 'monit',
801
+ message: 'websocket disconnected'
802
+ });
803
+ });
804
+ });
805
+ app.get(`${prefix}/api/events`, (req) => __awaiter(void 0, void 0, void 0, function* () {
806
+ const query = req.query;
807
+ const limit = query.limit ? Number(query.limit) : undefined;
808
+ return {
809
+ items: eventStore.list({
810
+ q: query.q,
811
+ type: query.type,
812
+ limit
813
+ })
814
+ };
815
+ }));
816
+ app.get(`${prefix}/api/stats`, () => __awaiter(void 0, void 0, void 0, function* () { return getStats(); }));
817
+ app.get(`${prefix}/api/functions`, () => __awaiter(void 0, void 0, void 0, function* () {
818
+ const functionsList = state_1.StateManager.select('functions');
819
+ const items = Object.keys(functionsList || {}).map((name) => {
820
+ var _a, _b;
821
+ return ({
822
+ name,
823
+ private: !!((_a = functionsList[name]) === null || _a === void 0 ? void 0 : _a.private),
824
+ run_as_system: !!((_b = functionsList[name]) === null || _b === void 0 ? void 0 : _b.run_as_system)
825
+ });
826
+ });
827
+ return { items };
828
+ }));
829
+ app.get(`${prefix}/api/functions/:name`, (req, reply) => __awaiter(void 0, void 0, void 0, function* () {
830
+ var _a;
831
+ if (!allowEdit) {
832
+ reply.code(403);
833
+ return { error: 'Function code access disabled' };
834
+ }
835
+ const params = req.params;
836
+ const name = params.name;
837
+ const functionsList = state_1.StateManager.select('functions');
838
+ const currentFunction = functionsList === null || functionsList === void 0 ? void 0 : functionsList[name];
839
+ if (!currentFunction) {
840
+ reply.code(404);
841
+ return { error: `Function "${name}" not found` };
842
+ }
843
+ return {
844
+ name,
845
+ code: (_a = currentFunction.code) !== null && _a !== void 0 ? _a : '',
846
+ private: !!currentFunction.private,
847
+ run_as_system: !!currentFunction.run_as_system,
848
+ disable_arg_logs: !!currentFunction.disable_arg_logs
849
+ };
850
+ }));
851
+ app.get(`${prefix}/api/functions/history`, () => __awaiter(void 0, void 0, void 0, function* () {
852
+ return ({
853
+ items: functionHistory.slice(0, maxHistory)
854
+ });
855
+ }));
856
+ app.post(`${prefix}/api/functions/invoke`, (req, reply) => __awaiter(void 0, void 0, void 0, function* () {
857
+ var _a, _b;
858
+ if (!allowInvoke) {
859
+ reply.code(403);
860
+ return { error: 'Function invocation disabled' };
861
+ }
862
+ const body = req.body;
863
+ const name = body === null || body === void 0 ? void 0 : body.name;
864
+ const args = Array.isArray(body === null || body === void 0 ? void 0 : body.arguments) ? body.arguments : [];
865
+ if (!name) {
866
+ reply.code(400);
867
+ return { error: 'Missing function name' };
868
+ }
869
+ const functionsList = state_1.StateManager.select('functions');
870
+ const rules = state_1.StateManager.select('rules');
871
+ const appRef = state_1.StateManager.select('app');
872
+ const services = state_1.StateManager.select('services');
873
+ const currentFunction = functionsList === null || functionsList === void 0 ? void 0 : functionsList[name];
874
+ if (!currentFunction) {
875
+ reply.code(404);
876
+ return { error: `Function "${name}" not found` };
877
+ }
878
+ if (!allowEdit && typeof (body === null || body === void 0 ? void 0 : body.code) === 'string' && body.code.trim()) {
879
+ reply.code(403);
880
+ return { error: 'Function override disabled' };
881
+ }
882
+ const overrideCode = typeof (body === null || body === void 0 ? void 0 : body.code) === 'string' && body.code.trim()
883
+ ? body.code
884
+ : undefined;
885
+ const effectiveRunAsSystem = (body === null || body === void 0 ? void 0 : body.runAsSystem) !== false;
886
+ const effectiveFunction = overrideCode
887
+ ? Object.assign(Object.assign({}, currentFunction), { code: overrideCode, run_as_system: effectiveRunAsSystem }) : Object.assign(Object.assign({}, currentFunction), { run_as_system: effectiveRunAsSystem });
888
+ const resolvedUser = yield resolveUserContext(app, body === null || body === void 0 ? void 0 : body.userId, body === null || body === void 0 ? void 0 : body.user);
889
+ const safeArgs = (Array.isArray(args) ? sanitize(args) : sanitize([args]));
890
+ const resolvedUserRecord = resolvedUser;
891
+ const userInfo = resolvedUserRecord
892
+ ? {
893
+ id: typeof resolvedUserRecord.id === 'string' ? resolvedUserRecord.id : undefined,
894
+ email: typeof resolvedUserRecord.email === 'string'
895
+ ? resolvedUserRecord.email
896
+ : (typeof ((_a = resolvedUserRecord.user_data) === null || _a === void 0 ? void 0 : _a.email) === 'string'
897
+ ? (_b = resolvedUserRecord.user_data) === null || _b === void 0 ? void 0 : _b.email
898
+ : undefined)
899
+ }
900
+ : undefined;
901
+ addFunctionHistory({
902
+ ts: Date.now(),
903
+ name,
904
+ args: safeArgs,
905
+ runAsSystem: effectiveRunAsSystem,
906
+ user: userInfo
907
+ });
908
+ addEvent({
909
+ id: createEventId(),
910
+ ts: Date.now(),
911
+ type: 'function',
912
+ source: 'monit',
913
+ message: `invoke ${name}`,
914
+ data: sanitize({
915
+ args,
916
+ user: userInfo,
917
+ runAsSystem: effectiveRunAsSystem,
918
+ override: Boolean(overrideCode)
919
+ })
920
+ });
921
+ try {
922
+ const result = yield (0, context_1.GenerateContext)({
923
+ args,
924
+ app: appRef,
925
+ rules,
926
+ user: resolvedUser !== null && resolvedUser !== void 0 ? resolvedUser : { id: 'monitor', role: 'system' },
927
+ currentFunction: effectiveFunction,
928
+ functionsList,
929
+ services,
930
+ runAsSystem: effectiveRunAsSystem
931
+ });
932
+ return { result: sanitize(result) };
933
+ }
934
+ catch (error) {
935
+ addEvent({
936
+ id: createEventId(),
937
+ ts: Date.now(),
938
+ type: 'error',
939
+ source: 'monit',
940
+ message: `invoke ${name} failed`,
941
+ data: sanitize({ error })
942
+ });
943
+ reply.code(500);
944
+ const details = getErrorDetails(error);
945
+ return { error: details.message, stack: details.stack };
946
+ }
947
+ }));
948
+ app.get(`${prefix}/api/users`, (req) => __awaiter(void 0, void 0, void 0, function* () {
949
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j;
950
+ const query = req.query;
951
+ const scope = (_a = query.scope) !== null && _a !== void 0 ? _a : 'all';
952
+ const rawSearch = typeof query.q === 'string' ? query.q.trim() : '';
953
+ const hasSearch = rawSearch.length > 0;
954
+ const escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
955
+ const searchRegex = hasSearch ? new RegExp(escapeRegex(rawSearch), 'i') : null;
956
+ const searchObjectId = hasSearch && mongodb_1.ObjectId.isValid(rawSearch) ? new mongodb_1.ObjectId(rawSearch) : null;
957
+ const parsedAuthLimit = Number((_c = (_b = query.authLimit) !== null && _b !== void 0 ? _b : query.limit) !== null && _c !== void 0 ? _c : 100);
958
+ const parsedCustomLimit = Number((_e = (_d = query.customLimit) !== null && _d !== void 0 ? _d : query.limit) !== null && _e !== void 0 ? _e : 25);
959
+ const parsedPage = Number((_g = (_f = query.customPage) !== null && _f !== void 0 ? _f : query.page) !== null && _g !== void 0 ? _g : 1);
960
+ const resolvedAuthLimit = Math.min(Number.isFinite(parsedAuthLimit) && parsedAuthLimit > 0 ? parsedAuthLimit : 100, 500);
961
+ const resolvedCustomLimit = Math.min(Number.isFinite(parsedCustomLimit) && parsedCustomLimit > 0 ? parsedCustomLimit : 25, 500);
962
+ const resolvedCustomPage = Math.max(Number.isFinite(parsedPage) && parsedPage > 0 ? parsedPage : 1, 1);
963
+ const db = app.mongo.client.db(constants_1.DB_NAME);
964
+ const authCollection = (_h = constants_1.AUTH_CONFIG.authCollection) !== null && _h !== void 0 ? _h : 'auth_users';
965
+ const userCollection = constants_1.AUTH_CONFIG.userCollection;
966
+ const response = {
967
+ meta: {
968
+ userIdField: constants_1.AUTH_CONFIG.user_id_field,
969
+ authCollection,
970
+ customCollection: userCollection
971
+ }
972
+ };
973
+ if (scope === 'all' || scope === 'auth') {
974
+ const authFilter = hasSearch
975
+ ? {
976
+ $or: [
977
+ ...(searchObjectId ? [{ _id: searchObjectId }] : []),
978
+ { email: searchRegex },
979
+ { status: searchRegex }
980
+ ]
981
+ }
982
+ : {};
983
+ const authItems = yield db
984
+ .collection(authCollection)
985
+ .find(authFilter)
986
+ .sort({ createdAt: -1, _id: -1 })
987
+ .limit(resolvedAuthLimit)
988
+ .toArray();
989
+ response.auth = {
990
+ collection: authCollection,
991
+ items: authItems.map((doc) => sanitize(doc))
992
+ };
993
+ }
994
+ if ((scope === 'all' || scope === 'custom') && userCollection) {
995
+ const userIdField = (_j = constants_1.AUTH_CONFIG.user_id_field) !== null && _j !== void 0 ? _j : 'id';
996
+ const customFilter = hasSearch
997
+ ? {
998
+ $or: [
999
+ ...(searchObjectId ? [{ _id: searchObjectId }] : []),
1000
+ { [userIdField]: searchRegex },
1001
+ { email: searchRegex },
1002
+ { name: searchRegex },
1003
+ { username: searchRegex }
1004
+ ]
1005
+ }
1006
+ : {};
1007
+ const total = yield db.collection(userCollection).countDocuments(customFilter);
1008
+ const totalPages = Math.max(1, Math.ceil(total / Math.max(resolvedCustomLimit, 1)));
1009
+ const page = Math.min(resolvedCustomPage, totalPages);
1010
+ const skip = Math.max(0, (page - 1) * resolvedCustomLimit);
1011
+ const customItems = yield db
1012
+ .collection(userCollection)
1013
+ .find(customFilter)
1014
+ .sort({ createdAt: -1, _id: -1 })
1015
+ .skip(skip)
1016
+ .limit(resolvedCustomLimit)
1017
+ .toArray();
1018
+ response.custom = {
1019
+ collection: userCollection,
1020
+ items: customItems.map((doc) => sanitize(doc)),
1021
+ pagination: {
1022
+ page,
1023
+ pages: totalPages,
1024
+ total,
1025
+ pageSize: resolvedCustomLimit
1026
+ }
1027
+ };
1028
+ }
1029
+ return response;
1030
+ }));
1031
+ app.post(`${prefix}/api/users`, (req, reply) => __awaiter(void 0, void 0, void 0, function* () {
1032
+ var _a, _b, _c;
1033
+ const body = req.body;
1034
+ const email = (_a = body === null || body === void 0 ? void 0 : body.email) === null || _a === void 0 ? void 0 : _a.toLowerCase();
1035
+ const password = body === null || body === void 0 ? void 0 : body.password;
1036
+ if (!email || !password) {
1037
+ reply.code(400);
1038
+ return { error: 'Missing email or password' };
1039
+ }
1040
+ const result = yield (0, handleUserRegistration_1.default)(app, {
1041
+ run_as_system: true,
1042
+ provider: handleUserRegistration_model_1.PROVIDER.LOCAL_USERPASS
1043
+ })({ email, password });
1044
+ const userId = (_b = result === null || result === void 0 ? void 0 : result.insertedId) === null || _b === void 0 ? void 0 : _b.toString();
1045
+ if (userId && constants_1.AUTH_CONFIG.userCollection && constants_1.AUTH_CONFIG.user_id_field) {
1046
+ const db = app.mongo.client.db(constants_1.DB_NAME);
1047
+ const customData = (_c = body === null || body === void 0 ? void 0 : body.customData) !== null && _c !== void 0 ? _c : {};
1048
+ yield db.collection(constants_1.AUTH_CONFIG.userCollection).updateOne({ [constants_1.AUTH_CONFIG.user_id_field]: userId }, {
1049
+ $set: Object.assign(Object.assign({}, customData), { [constants_1.AUTH_CONFIG.user_id_field]: userId })
1050
+ }, { upsert: true });
1051
+ }
1052
+ addEvent({
1053
+ id: createEventId(),
1054
+ ts: Date.now(),
1055
+ type: 'auth',
1056
+ source: 'monit',
1057
+ message: 'user created',
1058
+ data: sanitize({ email, userId })
1059
+ });
1060
+ reply.code(201);
1061
+ return { userId };
1062
+ }));
1063
+ app.patch(`${prefix}/api/users/:id/password`, (req, reply) => __awaiter(void 0, void 0, void 0, function* () {
1064
+ var _a;
1065
+ const params = req.params;
1066
+ const body = req.body;
1067
+ const password = body === null || body === void 0 ? void 0 : body.password;
1068
+ if (!password) {
1069
+ reply.code(400);
1070
+ return { error: 'Missing password' };
1071
+ }
1072
+ const db = app.mongo.client.db(constants_1.DB_NAME);
1073
+ const authCollection = (_a = constants_1.AUTH_CONFIG.authCollection) !== null && _a !== void 0 ? _a : 'auth_users';
1074
+ const selector = {};
1075
+ if (params.id && mongodb_1.ObjectId.isValid(params.id)) {
1076
+ selector._id = new mongodb_1.ObjectId(params.id);
1077
+ }
1078
+ else if (body === null || body === void 0 ? void 0 : body.email) {
1079
+ selector.email = body.email.toLowerCase();
1080
+ }
1081
+ else {
1082
+ reply.code(400);
1083
+ return { error: 'Invalid user identifier' };
1084
+ }
1085
+ const hashedPassword = yield (0, crypto_1.hashPassword)(password);
1086
+ const result = yield db.collection(authCollection).updateOne(selector, {
1087
+ $set: { password: hashedPassword }
1088
+ });
1089
+ if (!result.matchedCount) {
1090
+ reply.code(404);
1091
+ return { error: 'User not found' };
1092
+ }
1093
+ addEvent({
1094
+ id: createEventId(),
1095
+ ts: Date.now(),
1096
+ type: 'auth',
1097
+ source: 'monit',
1098
+ message: 'password updated',
1099
+ data: sanitize({ selector })
1100
+ });
1101
+ return { status: 'ok' };
1102
+ }));
1103
+ app.patch(`${prefix}/api/users/:id/status`, (req, reply) => __awaiter(void 0, void 0, void 0, function* () {
1104
+ var _a, _b;
1105
+ const params = req.params;
1106
+ const body = req.body;
1107
+ const db = app.mongo.client.db(constants_1.DB_NAME);
1108
+ const authCollection = (_a = constants_1.AUTH_CONFIG.authCollection) !== null && _a !== void 0 ? _a : 'auth_users';
1109
+ const selector = {};
1110
+ if (params.id && mongodb_1.ObjectId.isValid(params.id)) {
1111
+ selector._id = new mongodb_1.ObjectId(params.id);
1112
+ }
1113
+ else if (body === null || body === void 0 ? void 0 : body.email) {
1114
+ selector.email = body.email.toLowerCase();
1115
+ }
1116
+ else {
1117
+ reply.code(400);
1118
+ return { error: 'Invalid user identifier' };
1119
+ }
1120
+ const status = typeof (body === null || body === void 0 ? void 0 : body.disabled) === 'boolean'
1121
+ ? (body.disabled ? 'disabled' : 'confirmed')
1122
+ : ((_b = body === null || body === void 0 ? void 0 : body.status) !== null && _b !== void 0 ? _b : 'disabled');
1123
+ const result = yield db.collection(authCollection).updateOne(selector, {
1124
+ $set: { status }
1125
+ });
1126
+ if (!result.matchedCount) {
1127
+ reply.code(404);
1128
+ return { error: 'User not found' };
1129
+ }
1130
+ addEvent({
1131
+ id: createEventId(),
1132
+ ts: Date.now(),
1133
+ type: 'auth',
1134
+ source: 'monit',
1135
+ message: `user status ${status}`,
1136
+ data: sanitize({ selector, status })
1137
+ });
1138
+ return { status: 'ok' };
1139
+ }));
1140
+ app.get(`${prefix}/api/collections`, () => __awaiter(void 0, void 0, void 0, function* () {
1141
+ const db = app.mongo.client.db(constants_1.DB_NAME);
1142
+ const collections = yield db.listCollections().toArray();
1143
+ const items = collections
1144
+ .filter((entry) => !entry.name.startsWith('system.'))
1145
+ .map((entry) => ({
1146
+ name: entry.name,
1147
+ type: entry.type
1148
+ }));
1149
+ return { items };
1150
+ }));
1151
+ app.get(`${prefix}/api/collections/:name/rules`, (req) => __awaiter(void 0, void 0, void 0, function* () {
1152
+ const params = req.params;
1153
+ const query = req.query;
1154
+ const rules = state_1.StateManager.select('rules');
1155
+ const runAsSystem = (query === null || query === void 0 ? void 0 : query.runAsSystem) === 'true';
1156
+ const resolvedUser = yield resolveUserContext(app, query === null || query === void 0 ? void 0 : query.userId);
1157
+ return buildCollectionRulesSnapshot(rules, params.name, resolvedUser, runAsSystem);
1158
+ }));
1159
+ app.get(`${prefix}/api/collections/history`, () => __awaiter(void 0, void 0, void 0, function* () {
1160
+ return ({
1161
+ items: collectionHistory.slice(0, maxCollectionHistory)
1162
+ });
1163
+ }));
1164
+ app.post(`${prefix}/api/collections/query`, (req, reply) => __awaiter(void 0, void 0, void 0, function* () {
1165
+ var _a, _b;
1166
+ const body = req.body;
1167
+ const collection = body === null || body === void 0 ? void 0 : body.collection;
1168
+ if (!collection) {
1169
+ reply.code(400);
1170
+ return { error: 'Missing collection name' };
1171
+ }
1172
+ const rawQuery = (_a = body === null || body === void 0 ? void 0 : body.query) !== null && _a !== void 0 ? _a : {};
1173
+ if (Array.isArray(rawQuery) || typeof rawQuery !== 'object' || rawQuery === null) {
1174
+ reply.code(400);
1175
+ return { error: 'Query must be an object' };
1176
+ }
1177
+ const sort = body === null || body === void 0 ? void 0 : body.sort;
1178
+ if (sort !== undefined && !isPlainObject(sort)) {
1179
+ reply.code(400);
1180
+ return { error: 'Sort must be an object' };
1181
+ }
1182
+ const page = Math.max(1, Math.floor(Number((_b = body === null || body === void 0 ? void 0 : body.page) !== null && _b !== void 0 ? _b : 1) || 1));
1183
+ const skip = (page - 1) * COLLECTION_PAGE_SIZE;
1184
+ const rules = state_1.StateManager.select('rules');
1185
+ const services = state_1.StateManager.select('services');
1186
+ const resolvedUser = yield resolveUserContext(app, body === null || body === void 0 ? void 0 : body.userId);
1187
+ const runAsSystem = (body === null || body === void 0 ? void 0 : body.runAsSystem) !== false;
1188
+ const recordHistory = (body === null || body === void 0 ? void 0 : body.recordHistory) !== false;
1189
+ try {
1190
+ const mongoService = services['mongodb-atlas'](app, {
1191
+ rules,
1192
+ user: resolvedUser !== null && resolvedUser !== void 0 ? resolvedUser : {},
1193
+ run_as_system: runAsSystem
1194
+ });
1195
+ const options = {};
1196
+ if (isPlainObject(sort))
1197
+ options.sort = sort;
1198
+ const cursor = mongoService
1199
+ .db(constants_1.DB_NAME)
1200
+ .collection(collection)
1201
+ .find(rawQuery, undefined, Object.keys(options).length ? options : undefined)
1202
+ .skip(skip)
1203
+ .limit(COLLECTION_PAGE_SIZE + 1);
1204
+ const countPromise = mongoService
1205
+ .db(constants_1.DB_NAME)
1206
+ .collection(collection)
1207
+ .count(rawQuery);
1208
+ const [items, total] = yield Promise.all([cursor.toArray(), countPromise]);
1209
+ const hasMore = page * COLLECTION_PAGE_SIZE < total;
1210
+ const pageItems = items.length > COLLECTION_PAGE_SIZE
1211
+ ? items.slice(0, COLLECTION_PAGE_SIZE)
1212
+ : items;
1213
+ if (recordHistory) {
1214
+ addCollectionHistory({
1215
+ ts: Date.now(),
1216
+ collection,
1217
+ mode: 'query',
1218
+ query: sanitize(rawQuery),
1219
+ sort: sort ? sanitize(sort) : undefined,
1220
+ runAsSystem,
1221
+ user: getUserInfo(resolvedUser),
1222
+ page
1223
+ });
1224
+ }
1225
+ return {
1226
+ items: sanitize(pageItems),
1227
+ count: pageItems.length,
1228
+ total,
1229
+ page,
1230
+ pageSize: COLLECTION_PAGE_SIZE,
1231
+ hasMore
1232
+ };
1233
+ }
1234
+ catch (error) {
1235
+ const details = getErrorDetails(error);
1236
+ reply.code(500);
1237
+ return { error: details.message, stack: details.stack };
1238
+ }
1239
+ }));
1240
+ app.post(`${prefix}/api/collections/aggregate`, (req, reply) => __awaiter(void 0, void 0, void 0, function* () {
1241
+ var _a, _b, _c, _d;
1242
+ const body = req.body;
1243
+ const collection = body === null || body === void 0 ? void 0 : body.collection;
1244
+ if (!collection) {
1245
+ reply.code(400);
1246
+ return { error: 'Missing collection name' };
1247
+ }
1248
+ const rawPipeline = (_a = body === null || body === void 0 ? void 0 : body.pipeline) !== null && _a !== void 0 ? _a : [];
1249
+ if (!Array.isArray(rawPipeline)) {
1250
+ reply.code(400);
1251
+ return { error: 'Aggregate pipeline must be an array' };
1252
+ }
1253
+ const sort = body === null || body === void 0 ? void 0 : body.sort;
1254
+ if (sort !== undefined && !isPlainObject(sort)) {
1255
+ reply.code(400);
1256
+ return { error: 'Sort must be an object' };
1257
+ }
1258
+ const page = Math.max(1, Math.floor(Number((_b = body === null || body === void 0 ? void 0 : body.page) !== null && _b !== void 0 ? _b : 1) || 1));
1259
+ const skip = (page - 1) * COLLECTION_PAGE_SIZE;
1260
+ const rules = state_1.StateManager.select('rules');
1261
+ const services = state_1.StateManager.select('services');
1262
+ const resolvedUser = yield resolveUserContext(app, body === null || body === void 0 ? void 0 : body.userId);
1263
+ const runAsSystem = (body === null || body === void 0 ? void 0 : body.runAsSystem) !== false;
1264
+ const recordHistory = (body === null || body === void 0 ? void 0 : body.recordHistory) !== false;
1265
+ try {
1266
+ const pipeline = [...rawPipeline];
1267
+ if (sort)
1268
+ pipeline.push({ $sort: sort });
1269
+ if (skip > 0)
1270
+ pipeline.push({ $skip: skip });
1271
+ pipeline.push({ $limit: COLLECTION_PAGE_SIZE + 1 });
1272
+ const mongoService = services['mongodb-atlas'](app, {
1273
+ rules,
1274
+ user: resolvedUser !== null && resolvedUser !== void 0 ? resolvedUser : {},
1275
+ run_as_system: runAsSystem
1276
+ });
1277
+ const cursor = mongoService
1278
+ .db(constants_1.DB_NAME)
1279
+ .collection(collection)
1280
+ .aggregate(pipeline, undefined, true);
1281
+ const countCursor = mongoService
1282
+ .db(constants_1.DB_NAME)
1283
+ .collection(collection)
1284
+ .aggregate([...rawPipeline, { $count: 'total' }], undefined, true);
1285
+ const [items, totalResult] = yield Promise.all([cursor.toArray(), countCursor.toArray()]);
1286
+ const total = (_d = (_c = totalResult === null || totalResult === void 0 ? void 0 : totalResult[0]) === null || _c === void 0 ? void 0 : _c.total) !== null && _d !== void 0 ? _d : 0;
1287
+ const hasMore = page * COLLECTION_PAGE_SIZE < total;
1288
+ const pageItems = items.length > COLLECTION_PAGE_SIZE
1289
+ ? items.slice(0, COLLECTION_PAGE_SIZE)
1290
+ : items;
1291
+ if (recordHistory) {
1292
+ addCollectionHistory({
1293
+ ts: Date.now(),
1294
+ collection,
1295
+ mode: 'aggregate',
1296
+ pipeline: sanitize(rawPipeline),
1297
+ sort: sort ? sanitize(sort) : undefined,
1298
+ runAsSystem,
1299
+ user: getUserInfo(resolvedUser),
1300
+ page
1301
+ });
1302
+ }
1303
+ return {
1304
+ items: sanitize(pageItems),
1305
+ count: pageItems.length,
1306
+ total,
1307
+ page,
1308
+ pageSize: COLLECTION_PAGE_SIZE,
1309
+ hasMore
1310
+ };
1311
+ }
1312
+ catch (error) {
1313
+ const details = getErrorDetails(error);
1314
+ reply.code(500);
1315
+ return { error: details.message, stack: details.stack };
1316
+ }
1317
+ }));
1318
+ }), { name: 'monitoring' });
1319
+ exports.default = createMonitoringPlugin;