@ztimson/momentum 0.53.1 → 0.58.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 (86) hide show
  1. package/README.md +200 -148
  2. package/bin/build-models.mjs +28 -37
  3. package/dist/actions.d.ts +32 -43
  4. package/dist/actions.d.ts.map +1 -1
  5. package/dist/ai.d.ts +38 -17
  6. package/dist/ai.d.ts.map +1 -1
  7. package/dist/analytics.d.ts +169 -36
  8. package/dist/analytics.d.ts.map +1 -1
  9. package/dist/api.d.ts +22 -22
  10. package/dist/api.d.ts.map +1 -1
  11. package/dist/asset-controller.d.ts +92 -0
  12. package/dist/asset-controller.d.ts.map +1 -0
  13. package/dist/audit.d.ts +22 -0
  14. package/dist/audit.d.ts.map +1 -0
  15. package/dist/auth.d.ts +39 -92
  16. package/dist/auth.d.ts.map +1 -1
  17. package/dist/call.d.ts +39 -0
  18. package/dist/call.d.ts.map +1 -0
  19. package/dist/captcha.d.ts +14 -0
  20. package/dist/captcha.d.ts.map +1 -0
  21. package/dist/client.d.ts +44 -33
  22. package/dist/client.d.ts.map +1 -1
  23. package/dist/core.d.ts +50 -21
  24. package/dist/core.d.ts.map +1 -1
  25. package/dist/data.d.ts +59 -77
  26. package/dist/data.d.ts.map +1 -1
  27. package/dist/dialog.d.ts +11 -0
  28. package/dist/dialog.d.ts.map +1 -0
  29. package/dist/discounts.d.ts +8 -12
  30. package/dist/discounts.d.ts.map +1 -1
  31. package/dist/email.d.ts +19 -19
  32. package/dist/email.d.ts.map +1 -1
  33. package/dist/forms.d.ts +6 -13
  34. package/dist/forms.d.ts.map +1 -1
  35. package/dist/groups.d.ts +22 -16
  36. package/dist/groups.d.ts.map +1 -1
  37. package/dist/index.d.ts +13 -3
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +4446 -0
  40. package/dist/index.mjs +3422 -1731
  41. package/dist/logger.d.ts +78 -48
  42. package/dist/logger.d.ts.map +1 -1
  43. package/dist/momentum.d.ts +69 -15
  44. package/dist/momentum.d.ts.map +1 -1
  45. package/dist/momentum.worker.d.mts +2 -0
  46. package/dist/momentum.worker.d.mts.map +1 -0
  47. package/dist/momentum.worker.mjs +103 -0
  48. package/dist/notifications.d.ts +39 -0
  49. package/dist/notifications.d.ts.map +1 -0
  50. package/dist/pdf.d.ts +31 -14
  51. package/dist/pdf.d.ts.map +1 -1
  52. package/dist/products.d.ts +47 -0
  53. package/dist/products.d.ts.map +1 -0
  54. package/dist/routes.d.ts +40 -0
  55. package/dist/routes.d.ts.map +1 -0
  56. package/dist/schemas.d.ts +79 -0
  57. package/dist/schemas.d.ts.map +1 -0
  58. package/dist/settings.d.ts +30 -14
  59. package/dist/settings.d.ts.map +1 -1
  60. package/dist/sms.d.ts +14 -0
  61. package/dist/sms.d.ts.map +1 -0
  62. package/dist/sockets.d.ts +21 -10
  63. package/dist/sockets.d.ts.map +1 -1
  64. package/dist/static.d.ts +4 -2
  65. package/dist/static.d.ts.map +1 -1
  66. package/dist/storage.d.ts +103 -24
  67. package/dist/storage.d.ts.map +1 -1
  68. package/dist/templates.d.ts +23 -0
  69. package/dist/templates.d.ts.map +1 -0
  70. package/dist/tokens.d.ts +50 -0
  71. package/dist/tokens.d.ts.map +1 -0
  72. package/dist/totp.d.ts +45 -0
  73. package/dist/totp.d.ts.map +1 -0
  74. package/dist/transactions.d.ts +153 -0
  75. package/dist/transactions.d.ts.map +1 -0
  76. package/dist/users.d.ts +63 -25
  77. package/dist/users.d.ts.map +1 -1
  78. package/dist/webRtc.d.ts +39 -0
  79. package/dist/webRtc.d.ts.map +1 -0
  80. package/package.json +53 -43
  81. package/dist/index.cjs +0 -2755
  82. package/dist/momentum.worker.js +0 -16
  83. package/dist/payments.d.ts +0 -184
  84. package/dist/payments.d.ts.map +0 -1
  85. package/dist/phone.d.ts +0 -19
  86. package/dist/phone.d.ts.map +0 -1
package/dist/index.js ADDED
@@ -0,0 +1,4446 @@
1
+ (function(global, factory) {
2
+ typeof exports === "object" && typeof module !== "undefined" ? factory(exports) : typeof define === "function" && define.amd ? define(["exports"], factory) : (global = typeof globalThis !== "undefined" ? globalThis : global || self, factory(global.momentum = {}));
3
+ })(this, (function(exports2) {
4
+ "use strict";
5
+ function JSONAttemptParse(json, fallback) {
6
+ try {
7
+ return JSON.parse(json);
8
+ } catch {
9
+ return fallback ?? json;
10
+ }
11
+ }
12
+ function JSONSanitize(obj, space) {
13
+ const cache = [];
14
+ return JSON.stringify(obj, (key, value) => {
15
+ if (typeof value === "object" && value !== null) {
16
+ if (cache.includes(value)) return "[Circular]";
17
+ cache.push(value);
18
+ }
19
+ return value;
20
+ }, space);
21
+ }
22
+ function clean(obj, undefinedOnly = false) {
23
+ if (obj == null) throw new Error("Cannot clean a NULL value");
24
+ if (Array.isArray(obj)) {
25
+ obj = obj.filter((o) => undefinedOnly ? o !== void 0 : o != null);
26
+ } else {
27
+ Object.entries(obj).forEach(([key, value]) => {
28
+ if (undefinedOnly && value === void 0 || !undefinedOnly && value == null) delete obj[key];
29
+ });
30
+ }
31
+ return obj;
32
+ }
33
+ function deepCopy(value) {
34
+ if (value == null) return value;
35
+ const t = typeof value;
36
+ if (t === "string" || t === "number" || t === "boolean" || t === "function") return value;
37
+ try {
38
+ return structuredClone(value);
39
+ } catch {
40
+ return JSON.parse(JSONSanitize(value));
41
+ }
42
+ }
43
+ function dotNotation(obj, prop, set) {
44
+ if (obj == null || !prop) return void 0;
45
+ return prop.split(/[.[\]]/g).filter((prop2) => prop2.length).reduce((obj2, prop2, i, arr) => {
46
+ if (prop2[0] == '"' || prop2[0] == "'") prop2 = prop2.slice(1, -1);
47
+ if (!obj2?.hasOwnProperty(prop2)) {
48
+ return void 0;
49
+ }
50
+ return obj2[prop2];
51
+ }, obj);
52
+ }
53
+ function includes(target, values, allowMissing = false) {
54
+ if (target == void 0) return allowMissing;
55
+ if (Array.isArray(values)) return values.findIndex((e, i) => !includes(target[i], values[i], allowMissing)) == -1;
56
+ const type = typeof values;
57
+ if (type != typeof target) return false;
58
+ if (type == "object") {
59
+ return Object.keys(values).find((key) => !includes(target[key], values[key], allowMissing)) == null;
60
+ }
61
+ if (type == "function") return target.toString() == values.toString();
62
+ return target == values;
63
+ }
64
+ function isEqual(a, b, seen = /* @__PURE__ */ new WeakMap()) {
65
+ if (a === b) return true;
66
+ if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime();
67
+ if (a instanceof RegExp && b instanceof RegExp) return a.source === b.source && a.flags === b.flags;
68
+ if (typeof a !== "object" || a === null || typeof b !== "object" || b === null) {
69
+ if (Number.isNaN(a) && Number.isNaN(b)) return true;
70
+ if (typeof a === "function" && typeof b === "function") return a.toString() === b.toString();
71
+ return false;
72
+ }
73
+ if (seen.has(a)) return seen.get(a) === b;
74
+ seen.set(a, b);
75
+ const isArrayA = Array.isArray(a);
76
+ const isArrayB = Array.isArray(b);
77
+ if (isArrayA && isArrayB) {
78
+ if (a.length !== b.length) return false;
79
+ for (let i = 0; i < a.length; i++) {
80
+ if (!isEqual(a[i], b[i], seen)) return false;
81
+ }
82
+ return true;
83
+ }
84
+ if (isArrayA !== isArrayB) return false;
85
+ const keysA = Object.keys(a);
86
+ const keysB = Object.keys(b);
87
+ if (keysA.length !== keysB.length) return false;
88
+ for (const key of keysA) {
89
+ if (!Object.prototype.hasOwnProperty.call(b, key) || !isEqual(a[key], b[key], seen)) return false;
90
+ }
91
+ return true;
92
+ }
93
+ class ASet extends Array {
94
+ /** Number of elements in set */
95
+ get size() {
96
+ return this.length;
97
+ }
98
+ /**
99
+ * Array to create set from, duplicate values will be removed
100
+ * @param {T[]} elements Elements which will be added to set
101
+ */
102
+ constructor(elements = []) {
103
+ super();
104
+ if (!!elements?.["forEach"])
105
+ elements.forEach((el) => this.add(el));
106
+ }
107
+ /**
108
+ * Add elements to set if unique
109
+ * @param items
110
+ */
111
+ add(...items) {
112
+ items.filter((el) => !this.has(el)).forEach((el) => this.push(el));
113
+ return this;
114
+ }
115
+ /**
116
+ * Remove all elements
117
+ */
118
+ clear() {
119
+ this.splice(0, this.length);
120
+ return this;
121
+ }
122
+ /**
123
+ * Delete elements from set
124
+ * @param items Elements that will be deleted
125
+ */
126
+ delete(...items) {
127
+ items.forEach((el) => {
128
+ const index = this.indexOf(el);
129
+ if (index != -1) this.splice(index, 1);
130
+ });
131
+ return this;
132
+ }
133
+ /**
134
+ * Create list of elements this set has which the comparison set does not
135
+ * @param {ASet<T>} set Set to compare against
136
+ * @return {ASet<T>} Different elements
137
+ */
138
+ difference(set) {
139
+ return new ASet(this.filter((el) => !set.has(el)));
140
+ }
141
+ /**
142
+ * Check if set includes element
143
+ * @param {T} el Element to look for
144
+ * @return {boolean} True if element was found, false otherwise
145
+ */
146
+ has(el) {
147
+ return this.indexOf(el) != -1;
148
+ }
149
+ /**
150
+ * Find index number of element, or -1 if it doesn't exist. Matches by equality not reference
151
+ *
152
+ * @param {T} search Element to find
153
+ * @param {number} fromIndex Starting index position
154
+ * @return {number} Element index number or -1 if missing
155
+ */
156
+ indexOf(search2, fromIndex) {
157
+ return super.findIndex((el) => isEqual(el, search2), fromIndex);
158
+ }
159
+ /**
160
+ * Create list of elements this set has in common with the comparison set
161
+ * @param {ASet<T>} set Set to compare against
162
+ * @return {boolean} Set of common elements
163
+ */
164
+ intersection(set) {
165
+ return new ASet(this.filter((el) => set.has(el)));
166
+ }
167
+ /**
168
+ * Check if this set has no elements in common with the comparison set
169
+ * @param {ASet<T>} set Set to compare against
170
+ * @return {boolean} True if nothing in common, false otherwise
171
+ */
172
+ isDisjointFrom(set) {
173
+ return this.intersection(set).size == 0;
174
+ }
175
+ /**
176
+ * Check if all elements in this set are included in the comparison set
177
+ * @param {ASet<T>} set Set to compare against
178
+ * @return {boolean} True if all elements are included, false otherwise
179
+ */
180
+ isSubsetOf(set) {
181
+ return this.findIndex((el) => !set.has(el)) == -1;
182
+ }
183
+ /**
184
+ * Check if all elements from comparison set are included in this set
185
+ * @param {ASet<T>} set Set to compare against
186
+ * @return {boolean} True if all elements are included, false otherwise
187
+ */
188
+ isSuperset(set) {
189
+ return set.findIndex((el) => !this.has(el)) == -1;
190
+ }
191
+ /**
192
+ * Create list of elements that are only in one set but not both (XOR)
193
+ * @param {ASet<T>} set Set to compare against
194
+ * @return {ASet<T>} New set of unique elements
195
+ */
196
+ symmetricDifference(set) {
197
+ return new ASet([...this.difference(set), ...set.difference(this)]);
198
+ }
199
+ /**
200
+ * Create joined list of elements included in this & the comparison set
201
+ * @param {ASet<T>} set Set join
202
+ * @return {ASet<T>} New set of both previous sets combined
203
+ */
204
+ union(set) {
205
+ return new ASet([...this, ...set]);
206
+ }
207
+ }
208
+ function findByProp(prop, value) {
209
+ return (v) => isEqual(dotNotation(v, prop), value);
210
+ }
211
+ function makeArray(value) {
212
+ return Array.isArray(value) ? value : [value];
213
+ }
214
+ class Cache {
215
+ /**
216
+ * Create new cache
217
+ * @param {keyof T} key Default property to use as primary key
218
+ * @param options
219
+ */
220
+ constructor(key, options = {}) {
221
+ this.key = key;
222
+ this.options = options;
223
+ if (this.options.persistentStorage != null) {
224
+ if (typeof this.options.persistentStorage == "string")
225
+ this.options.persistentStorage = { storage: localStorage, key: this.options.persistentStorage };
226
+ if (this.options.persistentStorage?.storage?.database != void 0) {
227
+ (async () => {
228
+ const persists = this.options.persistentStorage;
229
+ const table = await persists.storage.createTable({ name: persists.key, key: this.key });
230
+ const rows = await table.getAll();
231
+ for (const row of rows) this.store.set(this.getKey(row), row);
232
+ this._loading();
233
+ })();
234
+ } else if (this.options.persistentStorage?.storage?.getItem != void 0) {
235
+ const { storage, key: key2 } = this.options.persistentStorage;
236
+ const stored = storage.getItem(key2);
237
+ if (stored != null) {
238
+ try {
239
+ const obj = JSON.parse(stored);
240
+ for (const k of Object.keys(obj)) this.store.set(k, obj[k]);
241
+ } catch {
242
+ }
243
+ }
244
+ this._loading();
245
+ }
246
+ } else {
247
+ this._loading();
248
+ }
249
+ return new Proxy(this, {
250
+ get: (target, prop) => {
251
+ if (prop in target) return target[prop];
252
+ return this.get(prop, true);
253
+ },
254
+ set: (target, prop, value) => {
255
+ if (prop in target) target[prop] = value;
256
+ else this.set(prop, value);
257
+ return true;
258
+ }
259
+ });
260
+ }
261
+ _loading;
262
+ store = /* @__PURE__ */ new Map();
263
+ timers = /* @__PURE__ */ new Map();
264
+ lruOrder = [];
265
+ /** Whether cache is complete */
266
+ complete = false;
267
+ /** Await initial loading */
268
+ loading = new Promise((r) => this._loading = r);
269
+ getKey(value) {
270
+ if (!this.key) throw new Error("No key defined");
271
+ if (value[this.key] === void 0) throw new Error(`${this.key.toString()} Doesn't exist on ${JSON.stringify(value, null, 2)}`);
272
+ return value[this.key];
273
+ }
274
+ /** Save item to storage */
275
+ save(key) {
276
+ const persists = this.options.persistentStorage;
277
+ if (!!persists?.storage) {
278
+ if (persists.storage?.database != void 0) {
279
+ persists.storage.createTable({ name: persists.key, key: this.key }).then((table) => {
280
+ if (key !== void 0) {
281
+ const value = this.get(key, true);
282
+ if (value != null) table.set(value, key);
283
+ else table.delete(key);
284
+ } else {
285
+ table.clear();
286
+ this.all(true).forEach((row) => table.add(row));
287
+ }
288
+ });
289
+ } else if (persists.storage?.setItem != void 0) {
290
+ const obj = {};
291
+ for (const [k, v] of this.store.entries()) obj[k] = v;
292
+ persists.storage.setItem(persists.key, JSONSanitize(obj));
293
+ }
294
+ }
295
+ }
296
+ clearTimer(key) {
297
+ const t = this.timers.get(key);
298
+ if (t) {
299
+ clearTimeout(t);
300
+ this.timers.delete(key);
301
+ }
302
+ }
303
+ touchLRU(key) {
304
+ if (!this.options.sizeLimit || this.options.sizeLimit <= 0) return;
305
+ const idx = this.lruOrder.indexOf(key);
306
+ if (idx >= 0) this.lruOrder.splice(idx, 1);
307
+ this.lruOrder.push(key);
308
+ while (this.lruOrder.length > (this.options.sizeLimit || 0)) {
309
+ const lru = this.lruOrder.shift();
310
+ if (lru !== void 0) this.delete(lru);
311
+ }
312
+ }
313
+ /**
314
+ * Get all cached items
315
+ * @return {T[]} Array of items
316
+ */
317
+ all(expired) {
318
+ const out = [];
319
+ for (const v of this.store.values()) {
320
+ const val = v;
321
+ if (expired || !val?._expired) out.push(deepCopy(val));
322
+ }
323
+ return out;
324
+ }
325
+ /**
326
+ * Add a new item to the cache. Like set, but finds key automatically
327
+ */
328
+ add(value, ttl = this.ttl) {
329
+ const key = this.getKey(value);
330
+ this.set(key, value, ttl);
331
+ return this;
332
+ }
333
+ /**
334
+ * Add several rows to the cache
335
+ */
336
+ addAll(rows, complete = true) {
337
+ this.clear();
338
+ rows.forEach((r) => this.add(r));
339
+ this.complete = complete;
340
+ return this;
341
+ }
342
+ /** Remove all keys */
343
+ clear() {
344
+ this.complete = false;
345
+ for (const [k, t] of this.timers) clearTimeout(t);
346
+ this.timers.clear();
347
+ this.lruOrder = [];
348
+ this.store.clear();
349
+ this.save();
350
+ return this;
351
+ }
352
+ /** Delete a cached item */
353
+ delete(key) {
354
+ this.clearTimer(key);
355
+ const idx = this.lruOrder.indexOf(key);
356
+ if (idx >= 0) this.lruOrder.splice(idx, 1);
357
+ this.store.delete(key);
358
+ this.save(key);
359
+ return this;
360
+ }
361
+ /** Return entries as array */
362
+ entries(expired) {
363
+ const out = [];
364
+ for (const [k, v] of this.store.entries()) {
365
+ const val = v;
366
+ if (expired || !val?._expired) out.push([k, deepCopy(val)]);
367
+ }
368
+ return out;
369
+ }
370
+ /** Manually expire a cached item */
371
+ expire(key) {
372
+ this.complete = false;
373
+ if (this.options.expiryPolicy == "keep") {
374
+ const v = this.store.get(key);
375
+ if (v) {
376
+ v._expired = true;
377
+ this.store.set(key, v);
378
+ this.save(key);
379
+ }
380
+ } else this.delete(key);
381
+ return this;
382
+ }
383
+ /** Find first matching item */
384
+ find(filter, expired) {
385
+ for (const v of this.store.values()) {
386
+ const row = v;
387
+ if ((expired || !row._expired) && includes(row, filter)) return deepCopy(row);
388
+ }
389
+ return void 0;
390
+ }
391
+ /** Get cached item by key */
392
+ get(key, expired) {
393
+ const raw = this.store.get(key);
394
+ if (raw == null) return null;
395
+ this.touchLRU(key);
396
+ const isExpired = raw?._expired;
397
+ if (expired || !isExpired) return deepCopy(raw);
398
+ return null;
399
+ }
400
+ /** Return list of keys */
401
+ keys(expired) {
402
+ const out = [];
403
+ for (const [k, v] of this.store.entries()) {
404
+ const val = v;
405
+ if (expired || !val?._expired) out.push(k);
406
+ }
407
+ return out;
408
+ }
409
+ /** Return map of key → item */
410
+ map(expired) {
411
+ const copy = {};
412
+ for (const [k, v] of this.store.entries()) {
413
+ const val = v;
414
+ if (expired || !val?._expired) copy[k] = deepCopy(val);
415
+ }
416
+ return copy;
417
+ }
418
+ /** Add item manually specifying the key */
419
+ set(key, value, ttl = this.options.ttl) {
420
+ if (this.options.expiryPolicy == "keep") delete value._expired;
421
+ this.clearTimer(key);
422
+ this.store.set(key, value);
423
+ this.touchLRU(key);
424
+ this.save(key);
425
+ if (ttl) {
426
+ const t = setTimeout(() => {
427
+ this.expire(key);
428
+ this.save(key);
429
+ }, ttl * 1e3);
430
+ this.timers.set(key, t);
431
+ }
432
+ return this;
433
+ }
434
+ /** Get all cached items */
435
+ values = this.all;
436
+ }
437
+ function contrast(background) {
438
+ const exploded = background?.match(background.length >= 6 ? /[0-9a-fA-F]{2}/g : /[0-9a-fA-F]/g);
439
+ if (!exploded || exploded?.length < 3) return "black";
440
+ const [r, g, b] = exploded.map((hex) => parseInt(hex.length == 1 ? `${hex}${hex}` : hex, 16));
441
+ const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
442
+ return luminance > 0.5 ? "black" : "white";
443
+ }
444
+ const LETTER_LIST = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
445
+ const NUMBER_LIST = "0123456789";
446
+ const SYMBOL_LIST = "~`!@#$%^&*()_-+={[}]|\\:;\"'<,>.?/";
447
+ function formatBytes(bytes, decimals = 2) {
448
+ if (bytes === 0) return "0 Bytes";
449
+ const k = 1024;
450
+ const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
451
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
452
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(decimals)) + " " + sizes[i];
453
+ }
454
+ function randomStringBuilder(length, letters = false, numbers = false, symbols = false) {
455
+ if (!letters && !numbers && !symbols) throw new Error("Must enable at least one: letters, numbers, symbols");
456
+ return Array(length).fill(null).map(() => {
457
+ let c;
458
+ do {
459
+ const type = ~~(Math.random() * 3);
460
+ if (letters && type == 0) {
461
+ c = LETTER_LIST[~~(Math.random() * LETTER_LIST.length)];
462
+ } else if (numbers && type == 1) {
463
+ c = NUMBER_LIST[~~(Math.random() * NUMBER_LIST.length)];
464
+ } else if (symbols && type == 2) {
465
+ c = SYMBOL_LIST[~~(Math.random() * SYMBOL_LIST.length)];
466
+ }
467
+ } while (!c);
468
+ return c;
469
+ }).join("");
470
+ }
471
+ function dayOfWeek(d) {
472
+ return ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"][d];
473
+ }
474
+ function dayOfYear(date) {
475
+ const start = new Date(Date.UTC(date.getUTCFullYear(), 0, 1));
476
+ return Math.ceil((date.getTime() - start.getTime()) / (1e3 * 60 * 60 * 24));
477
+ }
478
+ function formatDate(format = "YYYY-MM-DD H:mm", date = /* @__PURE__ */ new Date(), tz = "local") {
479
+ if (typeof date === "number" || typeof date === "string") date = new Date(date);
480
+ if (isNaN(date.getTime())) throw new Error("Invalid date input");
481
+ const numericTz = typeof tz === "number";
482
+ const localTz = tz === "local" || !numericTz && tz.toLowerCase?.() === "local";
483
+ const tzName = localTz ? Intl.DateTimeFormat().resolvedOptions().timeZone : numericTz ? "UTC" : tz;
484
+ if (!numericTz && tzName !== "UTC") {
485
+ try {
486
+ new Intl.DateTimeFormat("en-US", { timeZone: tzName }).format();
487
+ } catch {
488
+ throw new Error(`Invalid timezone: ${tzName}`);
489
+ }
490
+ }
491
+ let zonedDate = new Date(date);
492
+ let get;
493
+ const partsMap = {};
494
+ if (!numericTz && tzName !== "UTC") {
495
+ const parts = new Intl.DateTimeFormat("en-US", {
496
+ timeZone: tzName,
497
+ year: "numeric",
498
+ month: "2-digit",
499
+ day: "2-digit",
500
+ weekday: "long",
501
+ hour: "2-digit",
502
+ minute: "2-digit",
503
+ second: "2-digit",
504
+ hour12: false
505
+ }).formatToParts(date);
506
+ parts.forEach((p) => {
507
+ partsMap[p.type] = p.value;
508
+ });
509
+ const monthValue = parseInt(partsMap.month) - 1;
510
+ const dayOfWeekValue = (/* @__PURE__ */ new Date(`${partsMap.year}-${partsMap.month}-${partsMap.day}`)).getDay();
511
+ const hourValue = parseInt(partsMap.hour);
512
+ get = (fn2) => {
513
+ switch (fn2) {
514
+ case "FullYear":
515
+ return parseInt(partsMap.year);
516
+ case "Month":
517
+ return monthValue;
518
+ case "Date":
519
+ return parseInt(partsMap.day);
520
+ case "Day":
521
+ return dayOfWeekValue;
522
+ case "Hours":
523
+ return hourValue;
524
+ case "Minutes":
525
+ return parseInt(partsMap.minute);
526
+ case "Seconds":
527
+ return parseInt(partsMap.second);
528
+ default:
529
+ return 0;
530
+ }
531
+ };
532
+ } else {
533
+ const offset = numericTz ? tz : 0;
534
+ zonedDate = new Date(date.getTime() + offset * 60 * 60 * 1e3);
535
+ get = (fn2) => zonedDate[`getUTC${fn2}`]();
536
+ }
537
+ function numSuffix2(n) {
538
+ const s = ["th", "st", "nd", "rd"];
539
+ const v = n % 100;
540
+ return n + (s[(v - 20) % 10] || s[v] || s[0]);
541
+ }
542
+ function getTZOffset() {
543
+ if (numericTz) {
544
+ const total = tz * 60;
545
+ const hours = Math.floor(Math.abs(total) / 60);
546
+ const mins = Math.abs(total) % 60;
547
+ return `${tz >= 0 ? "+" : "-"}${String(hours).padStart(2, "0")}:${String(mins).padStart(2, "0")}`;
548
+ }
549
+ try {
550
+ const offset = new Intl.DateTimeFormat("en-US", { timeZone: tzName, timeZoneName: "longOffset", hour: "2-digit", minute: "2-digit" }).formatToParts(date).find((p) => p.type === "timeZoneName")?.value.match(/([+-]\d{2}:\d{2})/)?.[1];
551
+ if (offset) return offset;
552
+ } catch {
553
+ }
554
+ return "+00:00";
555
+ }
556
+ function getTZAbbr() {
557
+ if (numericTz && tz === 0) return "UTC";
558
+ try {
559
+ return new Intl.DateTimeFormat("en-US", { timeZone: tzName, timeZoneName: "short" }).formatToParts(date).find((p) => p.type === "timeZoneName")?.value || "";
560
+ } catch {
561
+ return tzName;
562
+ }
563
+ }
564
+ const tokens = {
565
+ YYYY: get("FullYear").toString(),
566
+ YY: get("FullYear").toString().slice(2),
567
+ MMMM: month(get("Month")),
568
+ MMM: month(get("Month")).slice(0, 3),
569
+ MM: (get("Month") + 1).toString().padStart(2, "0"),
570
+ M: (get("Month") + 1).toString(),
571
+ DDD: dayOfYear(zonedDate).toString(),
572
+ DD: get("Date").toString().padStart(2, "0"),
573
+ Do: numSuffix2(get("Date")),
574
+ D: get("Date").toString(),
575
+ dddd: dayOfWeek(get("Day")),
576
+ ddd: dayOfWeek(get("Day")).slice(0, 3),
577
+ HH: get("Hours").toString().padStart(2, "0"),
578
+ H: get("Hours").toString(),
579
+ hh: (get("Hours") % 12 || 12).toString().padStart(2, "0"),
580
+ h: (get("Hours") % 12 || 12).toString(),
581
+ mm: get("Minutes").toString().padStart(2, "0"),
582
+ m: get("Minutes").toString(),
583
+ ss: get("Seconds").toString().padStart(2, "0"),
584
+ s: get("Seconds").toString(),
585
+ SSS: zonedDate[`getUTC${"Milliseconds"}`]().toString().padStart(3, "0"),
586
+ A: get("Hours") >= 12 ? "PM" : "AM",
587
+ a: get("Hours") >= 12 ? "pm" : "am",
588
+ ZZ: getTZOffset().replace(":", ""),
589
+ Z: getTZOffset(),
590
+ z: getTZAbbr()
591
+ };
592
+ return format.replace(/YYYY|YY|MMMM|MMM|MM|M|DDD|DD|Do|D|dddd|ddd|HH|H|hh|h|mm|m|ss|s|SSS|A|a|ZZ|Z|z/g, (token) => tokens[token]);
593
+ }
594
+ function month(m) {
595
+ return ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"][m];
596
+ }
597
+ function sleep(ms) {
598
+ return new Promise((res) => setTimeout(res, ms));
599
+ }
600
+ async function sleepWhile(fn2, checkInterval = 100) {
601
+ while (await fn2()) await sleep(checkInterval);
602
+ }
603
+ class AsyncLock {
604
+ p = Promise.resolve();
605
+ run(fn2) {
606
+ const res = this.p.then(fn2, fn2);
607
+ this.p = res.then(() => {
608
+ }, () => {
609
+ });
610
+ return res;
611
+ }
612
+ }
613
+ class Database {
614
+ constructor(database, tables, version2) {
615
+ this.database = database;
616
+ this.version = version2;
617
+ this.connection = new Promise((resolve, reject) => {
618
+ let req;
619
+ try {
620
+ req = indexedDB.open(this.database, this.version);
621
+ } catch (err) {
622
+ return reject(err);
623
+ }
624
+ this.tables = !tables ? [] : tables.map((t) => {
625
+ t = typeof t == "object" ? t : { name: t };
626
+ return { ...t, name: t.name.toString() };
627
+ });
628
+ req.onerror = () => reject(req.error);
629
+ req.onsuccess = () => {
630
+ let db;
631
+ try {
632
+ db = req.result;
633
+ } catch (err) {
634
+ return reject(err);
635
+ }
636
+ const existing = Array.from(db.objectStoreNames);
637
+ if (!tables) {
638
+ this.tables = existing.map((t) => {
639
+ try {
640
+ const tx = db.transaction(t, "readonly");
641
+ const store = tx.objectStore(t);
642
+ return { name: t, key: store.keyPath };
643
+ } catch {
644
+ return { name: t };
645
+ }
646
+ });
647
+ }
648
+ const desired = new ASet((tables || []).map((t) => typeof t == "string" ? t : t.name));
649
+ if (tables && desired.symmetricDifference(new ASet(existing)).length) {
650
+ db.close();
651
+ Object.assign(this, new Database(this.database, this.tables, db.version + 1));
652
+ this.connection.then(resolve);
653
+ } else {
654
+ this.version = db.version;
655
+ resolve(db);
656
+ }
657
+ this.upgrading = false;
658
+ };
659
+ req.onupgradeneeded = () => {
660
+ this.upgrading = true;
661
+ let db;
662
+ try {
663
+ db = req.result;
664
+ } catch {
665
+ return;
666
+ }
667
+ try {
668
+ const existingTables = new ASet(Array.from(db.objectStoreNames));
669
+ if (tables) {
670
+ const desired = new ASet((tables || []).map((t) => typeof t == "string" ? t : t.name));
671
+ existingTables.difference(desired).forEach((name) => db.deleteObjectStore(name));
672
+ desired.difference(existingTables).forEach((name) => {
673
+ const t = this.tables.find(findByProp("name", name));
674
+ db.createObjectStore(name, {
675
+ keyPath: t?.key,
676
+ autoIncrement: t?.autoIncrement || !t?.key
677
+ });
678
+ });
679
+ }
680
+ } catch {
681
+ }
682
+ };
683
+ });
684
+ }
685
+ schemaLock = new AsyncLock();
686
+ upgrading = false;
687
+ connection;
688
+ tables;
689
+ get ready() {
690
+ return !this.upgrading;
691
+ }
692
+ waitForUpgrade = () => sleepWhile(() => this.upgrading);
693
+ async createTable(table) {
694
+ return this.schemaLock.run(async () => {
695
+ if (typeof table == "string") table = { name: table };
696
+ const conn = await this.connection;
697
+ if (!this.includes(table.name)) {
698
+ const newDb = new Database(this.database, [...this.tables, table], (this.version ?? 0) + 1);
699
+ conn.close();
700
+ this.connection = newDb.connection;
701
+ await this.connection;
702
+ Object.assign(this, newDb);
703
+ }
704
+ return this.table(table.name);
705
+ });
706
+ }
707
+ async deleteTable(table) {
708
+ return this.schemaLock.run(async () => {
709
+ if (typeof table == "string") table = { name: table };
710
+ if (!this.includes(table.name)) return;
711
+ const conn = await this.connection;
712
+ const newDb = new Database(this.database, this.tables.filter((t) => t.name != table.name), (this.version ?? 0) + 1);
713
+ conn.close();
714
+ this.connection = newDb.connection;
715
+ await this.connection;
716
+ Object.assign(this, newDb);
717
+ });
718
+ }
719
+ includes(name) {
720
+ return !!this.tables.find((t) => t.name == (typeof name == "object" ? name.name : name.toString()));
721
+ }
722
+ table(name) {
723
+ return new Table(this, name.toString());
724
+ }
725
+ }
726
+ class Table {
727
+ constructor(database, name, key = "id") {
728
+ this.database = database;
729
+ this.name = name;
730
+ this.key = key;
731
+ this.database.connection.then(() => {
732
+ const exists = !!this.database.tables.find(findByProp("name", this.name));
733
+ if (!exists) this.database.createTable(this.name);
734
+ });
735
+ }
736
+ async tx(table, fn2, readonly = false) {
737
+ await this.database.waitForUpgrade();
738
+ const db = await this.database.connection;
739
+ const tx = db.transaction(table, readonly ? "readonly" : "readwrite");
740
+ return new Promise((resolve, reject) => {
741
+ const request = fn2(tx.objectStore(table));
742
+ request.onsuccess = () => resolve(request.result);
743
+ request.onerror = () => reject(request.error);
744
+ });
745
+ }
746
+ add(value, key) {
747
+ return this.tx(this.name, (store) => store.add(value, key));
748
+ }
749
+ all = this.getAll;
750
+ clear() {
751
+ return this.tx(this.name, (store) => store.clear());
752
+ }
753
+ count() {
754
+ return this.tx(this.name, (store) => store.count(), true);
755
+ }
756
+ create = this.add;
757
+ delete(key) {
758
+ return this.tx(this.name, (store) => store.delete(key));
759
+ }
760
+ get(key) {
761
+ return this.tx(this.name, (store) => store.get(key), true);
762
+ }
763
+ getAll() {
764
+ return this.tx(this.name, (store) => store.getAll(), true);
765
+ }
766
+ getAllKeys() {
767
+ return this.tx(this.name, (store) => store.getAllKeys(), true);
768
+ }
769
+ put(value, key) {
770
+ return this.tx(this.name, (store) => {
771
+ if (store.keyPath) return store.put(value);
772
+ return store.put(value, key);
773
+ });
774
+ }
775
+ read(key) {
776
+ return key ? this.get(key) : this.getAll();
777
+ }
778
+ set(value, key) {
779
+ if (key) value[this.key] = key;
780
+ if (!value[this.key]) return this.add(value);
781
+ return this.put(value);
782
+ }
783
+ update = this.set;
784
+ }
785
+ class PromiseProgress extends Promise {
786
+ listeners = [];
787
+ _progress = 0;
788
+ get progress() {
789
+ return this._progress;
790
+ }
791
+ set progress(p) {
792
+ if (p == this._progress) return;
793
+ this._progress = p;
794
+ this.listeners.forEach((l) => l(p));
795
+ }
796
+ constructor(executor) {
797
+ super((resolve, reject) => executor(
798
+ (value) => resolve(value),
799
+ (reason) => reject(reason),
800
+ (progress) => this.progress = progress
801
+ ));
802
+ }
803
+ static from(promise) {
804
+ if (promise instanceof PromiseProgress) return promise;
805
+ return new PromiseProgress((res, rej) => promise.then((...args) => res(...args)).catch((...args) => rej(...args)));
806
+ }
807
+ from(promise) {
808
+ const newPromise = PromiseProgress.from(promise);
809
+ this.onProgress((p) => newPromise.progress = p);
810
+ return newPromise;
811
+ }
812
+ onProgress(callback) {
813
+ this.listeners.push(callback);
814
+ return this;
815
+ }
816
+ then(res, rej) {
817
+ const resp = super.then(res, rej);
818
+ return this.from(resp);
819
+ }
820
+ catch(rej) {
821
+ return this.from(super.catch(rej));
822
+ }
823
+ finally(res) {
824
+ return this.from(super.finally(res));
825
+ }
826
+ }
827
+ function downloadFile(blob, name) {
828
+ if (!(blob instanceof Blob)) blob = new Blob(makeArray(blob));
829
+ const url = URL.createObjectURL(blob);
830
+ downloadUrl(url, name);
831
+ URL.revokeObjectURL(url);
832
+ }
833
+ function downloadUrl(href, name) {
834
+ const a = document.createElement("a");
835
+ a.href = href;
836
+ a.download = name || href.split("/").pop();
837
+ document.body.appendChild(a);
838
+ a.click();
839
+ document.body.removeChild(a);
840
+ }
841
+ function fileBrowser(options = {}) {
842
+ return new Promise((res) => {
843
+ const input = document.createElement("input");
844
+ input.type = "file";
845
+ input.accept = options.accept || "*";
846
+ input.style.display = "none";
847
+ input.multiple = !!options.multiple;
848
+ input.onblur = input.onchange = async () => {
849
+ res(Array.from(input.files));
850
+ input.remove();
851
+ };
852
+ document.body.appendChild(input);
853
+ input.click();
854
+ });
855
+ }
856
+ function timestampFilename(name, date = /* @__PURE__ */ new Date()) {
857
+ if (typeof date == "number" || typeof date == "string") date = new Date(date);
858
+ const timestamp = formatDate("YYYY-MM-DD_HH-mm", date);
859
+ return timestamp;
860
+ }
861
+ function uploadWithProgress(options) {
862
+ return new PromiseProgress((res, rej, prog) => {
863
+ const xhr = new XMLHttpRequest();
864
+ const formData2 = new FormData();
865
+ options.files.forEach((f) => formData2.append("file", f));
866
+ xhr.withCredentials = !!options.withCredentials;
867
+ xhr.upload.addEventListener("progress", (event) => event.lengthComputable ? prog(event.loaded / event.total) : null);
868
+ xhr.addEventListener("loadend", () => res(JSONAttemptParse(xhr.responseText)));
869
+ xhr.addEventListener("error", () => rej(JSONAttemptParse(xhr.responseText)));
870
+ xhr.addEventListener("timeout", () => rej({ error: "Request timed out" }));
871
+ xhr.open("POST", options.url);
872
+ Object.entries(options.headers || {}).forEach(([key, value]) => xhr.setRequestHeader(key, value));
873
+ xhr.send(formData2);
874
+ });
875
+ }
876
+ class HttpResponse extends Response {
877
+ data;
878
+ ok;
879
+ redirected;
880
+ type;
881
+ url;
882
+ constructor(resp, stream) {
883
+ const body = [204, 205, 304].includes(resp.status) ? null : stream;
884
+ super(body, {
885
+ headers: resp.headers,
886
+ status: resp.status,
887
+ statusText: resp.statusText
888
+ });
889
+ this.ok = resp.ok;
890
+ this.redirected = resp.redirected;
891
+ this.type = resp.type;
892
+ this.url = resp.url;
893
+ }
894
+ }
895
+ class Http {
896
+ static interceptors = {};
897
+ static headers = {};
898
+ interceptors = {};
899
+ headers = {};
900
+ url;
901
+ constructor(defaults = {}) {
902
+ this.url = defaults.url ?? null;
903
+ this.headers = defaults.headers || {};
904
+ if (defaults.interceptors) {
905
+ defaults.interceptors.forEach((i) => Http.addInterceptor(i));
906
+ }
907
+ }
908
+ static addInterceptor(fn2) {
909
+ const key = Object.keys(Http.interceptors).length.toString();
910
+ Http.interceptors[key] = fn2;
911
+ return () => {
912
+ Http.interceptors[key] = null;
913
+ };
914
+ }
915
+ addInterceptor(fn2) {
916
+ const key = Object.keys(this.interceptors).length.toString();
917
+ this.interceptors[key] = fn2;
918
+ return () => {
919
+ this.interceptors[key] = null;
920
+ };
921
+ }
922
+ request(opts = {}) {
923
+ if (!this.url && !opts.url) throw new Error("URL needs to be set");
924
+ let url = opts.url?.startsWith("http") ? opts.url : (this.url || "") + (opts.url || "");
925
+ url = url.replaceAll(/([^:]\/)\/+/g, "$1");
926
+ if (opts.fragment) url.includes("#") ? url.replace(/#.*([?\n])/g, (match, arg1) => `#${opts.fragment}${arg1}`) : `${url}#${opts.fragment}`;
927
+ if (opts.query) {
928
+ const q = Array.isArray(opts.query) ? opts.query : Object.keys(opts.query).map((k) => ({ key: k, value: opts.query[k] }));
929
+ url += (url.includes("?") ? "&" : "?") + q.map((q2) => `${q2.key}=${q2.value}`).join("&");
930
+ }
931
+ const headers = clean({
932
+ "Content-Type": !opts.body ? void 0 : opts.body instanceof FormData ? "multipart/form-data" : "application/json",
933
+ ...Http.headers,
934
+ ...this.headers,
935
+ ...opts.headers
936
+ });
937
+ if (typeof opts.body == "object" && opts.body != null && headers["Content-Type"] == "application/json")
938
+ opts.body = JSON.stringify(opts.body);
939
+ return new PromiseProgress((res, rej, prog) => {
940
+ try {
941
+ fetch(url, {
942
+ headers,
943
+ method: opts.method || (opts.body ? "POST" : "GET"),
944
+ body: opts.body
945
+ }).then(async (resp) => {
946
+ for (let fn2 of [...Object.values(Http.interceptors), ...Object.values(this.interceptors)]) {
947
+ await new Promise((res2) => fn2(resp, () => res2()));
948
+ }
949
+ const contentLength = resp.headers.get("Content-Length");
950
+ const total = contentLength ? parseInt(contentLength, 10) : 0;
951
+ let loaded = 0;
952
+ const reader = resp.body?.getReader();
953
+ const stream = new ReadableStream({
954
+ start(controller) {
955
+ function push() {
956
+ reader?.read().then((event) => {
957
+ if (event.done) return controller.close();
958
+ loaded += event.value.byteLength;
959
+ prog(loaded / total);
960
+ controller.enqueue(event.value);
961
+ push();
962
+ }).catch((error) => controller.error(error));
963
+ }
964
+ push();
965
+ }
966
+ });
967
+ resp = new HttpResponse(resp, stream);
968
+ if (opts.decode !== false) {
969
+ const content = resp.headers.get("Content-Type")?.toLowerCase();
970
+ if (content?.includes("form")) resp.data = await resp.formData();
971
+ else if (content?.includes("json")) resp.data = await resp.json();
972
+ else if (content?.includes("text")) resp.data = await resp.text();
973
+ else if (content?.includes("application")) resp.data = await resp.blob();
974
+ }
975
+ if (resp.ok) res(resp);
976
+ else rej(resp);
977
+ }).catch((err) => rej(err));
978
+ } catch (err) {
979
+ rej(err);
980
+ }
981
+ });
982
+ }
983
+ }
984
+ var LOG_LEVEL = /* @__PURE__ */ ((LOG_LEVEL2) => {
985
+ LOG_LEVEL2[LOG_LEVEL2["ERROR"] = 0] = "ERROR";
986
+ LOG_LEVEL2[LOG_LEVEL2["WARN"] = 1] = "WARN";
987
+ LOG_LEVEL2[LOG_LEVEL2["INFO"] = 2] = "INFO";
988
+ LOG_LEVEL2[LOG_LEVEL2["LOG"] = 3] = "LOG";
989
+ LOG_LEVEL2[LOG_LEVEL2["DEBUG"] = 4] = "DEBUG";
990
+ return LOG_LEVEL2;
991
+ })(LOG_LEVEL || {});
992
+ function PE(str, ...args) {
993
+ const combined = [];
994
+ for (let i = 0; i < str.length || i < args.length; i++) {
995
+ if (str[i]) combined.push(str[i]);
996
+ if (args[i]) combined.push(args[i]);
997
+ }
998
+ return new PathEvent(combined.join("/"));
999
+ }
1000
+ function PES(str, ...args) {
1001
+ let combined = [];
1002
+ for (let i = 0; i < str.length || i < args.length; i++) {
1003
+ if (str[i]) combined.push(str[i]);
1004
+ if (args[i]) combined.push(args[i]);
1005
+ }
1006
+ const [paths, methods] = combined.join("/").split(":");
1007
+ return PathEvent.toString(paths, methods?.split(""));
1008
+ }
1009
+ class PathError extends Error {
1010
+ }
1011
+ class PathEvent {
1012
+ /** First directory in path */
1013
+ module;
1014
+ /** Entire path, including the module & name */
1015
+ fullPath;
1016
+ /** Parent directory, excludes module & name */
1017
+ dir;
1018
+ /** Path including the name, excluding the module */
1019
+ path;
1020
+ /** Last segment of path */
1021
+ name;
1022
+ /** List of methods */
1023
+ methods;
1024
+ /** Whether this path contains glob patterns */
1025
+ hasGlob;
1026
+ /** Internal cache for PathEvent instances to avoid redundant parsing */
1027
+ static pathEventCache = /* @__PURE__ */ new Map();
1028
+ /** Cache for compiled permissions (path + required permissions → result) */
1029
+ static permissionCache = /* @__PURE__ */ new Map();
1030
+ /** Max size for permission cache before LRU eviction */
1031
+ static MAX_PERMISSION_CACHE_SIZE = 1e3;
1032
+ /** All/Wildcard specified */
1033
+ get all() {
1034
+ return this.methods.has("*");
1035
+ }
1036
+ set all(v) {
1037
+ v ? this.methods = new ASet(["*"]) : this.methods.delete("*");
1038
+ }
1039
+ /** None specified */
1040
+ get none() {
1041
+ return this.methods.has("n");
1042
+ }
1043
+ set none(v) {
1044
+ v ? this.methods = new ASet(["n"]) : this.methods.delete("n");
1045
+ }
1046
+ /** Create method specified */
1047
+ get create() {
1048
+ return !this.methods.has("n") && (this.methods.has("*") || this.methods.has("c"));
1049
+ }
1050
+ set create(v) {
1051
+ v ? this.methods.delete("n").delete("*").add("c") : this.methods.delete("c");
1052
+ }
1053
+ /** Execute method specified */
1054
+ get execute() {
1055
+ return !this.methods.has("n") && (this.methods.has("*") || this.methods.has("x"));
1056
+ }
1057
+ set execute(v) {
1058
+ v ? this.methods.delete("n").delete("*").add("x") : this.methods.delete("x");
1059
+ }
1060
+ /** Read method specified */
1061
+ get read() {
1062
+ return !this.methods.has("n") && (this.methods.has("*") || this.methods.has("r"));
1063
+ }
1064
+ set read(v) {
1065
+ v ? this.methods.delete("n").delete("*").add("r") : this.methods.delete("r");
1066
+ }
1067
+ /** Update method specified */
1068
+ get update() {
1069
+ return !this.methods.has("n") && (this.methods.has("*") || this.methods.has("u"));
1070
+ }
1071
+ set update(v) {
1072
+ v ? this.methods.delete("n").delete("*").add("u") : this.methods.delete("u");
1073
+ }
1074
+ /** Delete method specified */
1075
+ get delete() {
1076
+ return !this.methods.has("n") && (this.methods.has("*") || this.methods.has("d"));
1077
+ }
1078
+ set delete(v) {
1079
+ v ? this.methods.delete("n").delete("*").add("d") : this.methods.delete("d");
1080
+ }
1081
+ constructor(e) {
1082
+ if (typeof e == "object") {
1083
+ Object.assign(this, e);
1084
+ return;
1085
+ }
1086
+ if (PathEvent.pathEventCache.has(e)) {
1087
+ Object.assign(this, PathEvent.pathEventCache.get(e));
1088
+ return;
1089
+ }
1090
+ let [p, method] = e.replaceAll(/(^|\/)\*+\/?$/g, "").split(":");
1091
+ if (!method) method = "*";
1092
+ if (p === "" || p === void 0 || p === "*") {
1093
+ this.module = "";
1094
+ this.path = "";
1095
+ this.dir = "";
1096
+ this.fullPath = "**";
1097
+ this.name = "";
1098
+ this.methods = new ASet(p === "*" ? ["*"] : method.split(""));
1099
+ this.hasGlob = true;
1100
+ PathEvent.pathEventCache.set(e, this);
1101
+ return;
1102
+ }
1103
+ let temp = p.split("/").filter((p2) => !!p2);
1104
+ this.module = temp.splice(0, 1)[0] || "";
1105
+ this.path = temp.join("/");
1106
+ this.dir = temp.length > 2 ? temp.slice(0, -1).join("/") : "";
1107
+ this.fullPath = `${this.module}${this.module && this.path ? "/" : ""}${this.path}`;
1108
+ this.name = temp.pop() || "";
1109
+ this.hasGlob = this.fullPath.includes("*");
1110
+ this.methods = new ASet(method.split(""));
1111
+ PathEvent.pathEventCache.set(e, this);
1112
+ }
1113
+ /**
1114
+ * Check if a filter pattern matches a target path
1115
+ * @private
1116
+ */
1117
+ static matches(pattern, target) {
1118
+ const methodsMatch = pattern.all || target.all || pattern.methods.intersection(target.methods).length > 0;
1119
+ if (!methodsMatch) return false;
1120
+ if (!pattern.hasGlob && !target.hasGlob) {
1121
+ const last = pattern.fullPath[target.fullPath.length];
1122
+ return pattern.fullPath.startsWith(target.fullPath) && (last == null || last == "/");
1123
+ }
1124
+ if (pattern.hasGlob) return this.pathMatchesGlob(target.fullPath, pattern.fullPath);
1125
+ return this.pathMatchesGlob(pattern.fullPath, target.fullPath);
1126
+ }
1127
+ /**
1128
+ * Check if a path matches a glob pattern
1129
+ * @private
1130
+ */
1131
+ static pathMatchesGlob(path, pattern) {
1132
+ if (pattern === path) return true;
1133
+ const pathParts = path.split("/").filter((p) => !!p);
1134
+ const patternParts = pattern.split("/").filter((p) => !!p);
1135
+ let pathIdx = 0;
1136
+ let patternIdx = 0;
1137
+ while (patternIdx < patternParts.length && pathIdx < pathParts.length) {
1138
+ const patternPart = patternParts[patternIdx];
1139
+ if (patternPart === "**") {
1140
+ if (patternIdx === patternParts.length - 1) return true;
1141
+ while (pathIdx < pathParts.length) {
1142
+ if (PathEvent.pathMatchesGlob(pathParts.slice(pathIdx).join("/"), patternParts.slice(patternIdx + 1).join("/"))) return true;
1143
+ pathIdx++;
1144
+ }
1145
+ return false;
1146
+ } else if (patternPart === "*") {
1147
+ pathIdx++;
1148
+ patternIdx++;
1149
+ } else {
1150
+ if (patternPart !== pathParts[pathIdx]) return false;
1151
+ pathIdx++;
1152
+ patternIdx++;
1153
+ }
1154
+ }
1155
+ if (patternIdx < patternParts.length) return patternParts.slice(patternIdx).every((p) => p === "**");
1156
+ return pathIdx === pathParts.length;
1157
+ }
1158
+ /**
1159
+ * Score a path for specificity ranking (lower = more specific = higher priority)
1160
+ * @private
1161
+ */
1162
+ static scoreSpecificity(path) {
1163
+ if (path === "**" || path === "") return Number.MAX_SAFE_INTEGER;
1164
+ const segments = path.split("/").filter((p) => !!p);
1165
+ let score = -segments.length;
1166
+ segments.forEach((seg) => {
1167
+ if (seg === "**") score += 0.5;
1168
+ else if (seg === "*") score += 0.25;
1169
+ });
1170
+ return score;
1171
+ }
1172
+ /** Clear the cache of all PathEvents */
1173
+ static clearCache() {
1174
+ PathEvent.pathEventCache.clear();
1175
+ }
1176
+ /** Clear the permission cache */
1177
+ static clearPermissionCache() {
1178
+ PathEvent.permissionCache.clear();
1179
+ }
1180
+ /**
1181
+ * Combine multiple events into one parsed object. Longest path takes precedent, but all subsequent methods are
1182
+ * combined until a "none" is reached
1183
+ *
1184
+ * @param {string | PathEvent} paths Events as strings or pre-parsed
1185
+ * @return {PathEvent} Final combined permission
1186
+ */
1187
+ static combine(...paths) {
1188
+ const parsed = paths.map((p) => p instanceof PathEvent ? p : new PathEvent(p));
1189
+ const sorted = parsed.toSorted((p1, p2) => {
1190
+ const score1 = PathEvent.scoreSpecificity(p1.fullPath);
1191
+ const score2 = PathEvent.scoreSpecificity(p2.fullPath);
1192
+ return score1 - score2;
1193
+ });
1194
+ let result = null;
1195
+ for (const p of sorted) {
1196
+ if (!result) {
1197
+ result = p;
1198
+ } else {
1199
+ if (result.fullPath.startsWith(p.fullPath)) {
1200
+ if (p.none) break;
1201
+ result.methods = new ASet([...result.methods, ...p.methods]);
1202
+ }
1203
+ }
1204
+ }
1205
+ return result || new PathEvent("");
1206
+ }
1207
+ /**
1208
+ * Filter a set of paths based on the target
1209
+ *
1210
+ * @param {string | PathEvent | (string | PathEvent)[]} target Array of events that will filtered
1211
+ * @param filter {...PathEvent} Must contain one of
1212
+ * @return {PathEvent[]} Filtered results
1213
+ */
1214
+ static filter(target, ...filter) {
1215
+ const parsedTarget = makeArray(target).map((pe) => pe instanceof PathEvent ? pe : new PathEvent(pe));
1216
+ const parsedFilter = makeArray(filter).map((pe) => pe instanceof PathEvent ? pe : new PathEvent(pe));
1217
+ return parsedTarget.filter((t) => !!parsedFilter.find((r) => PathEvent.matches(r, t)));
1218
+ }
1219
+ /**
1220
+ * Squash 2 sets of paths & return true if any overlap is found
1221
+ *
1222
+ * @param {string | PathEvent | (string | PathEvent)[]} target Array of Events as strings or pre-parsed
1223
+ * @param has Target must have at least one of these path
1224
+ * @return {boolean} Whether there is any overlap
1225
+ */
1226
+ static has(target, ...has) {
1227
+ const parsedTarget = makeArray(target).map((pe) => pe instanceof PathEvent ? pe : new PathEvent(pe));
1228
+ const parsedRequired = makeArray(has).map((pe) => pe instanceof PathEvent ? pe : new PathEvent(pe));
1229
+ return !!parsedRequired.find((r) => !!parsedTarget.find((t) => PathEvent.matches(r, t)));
1230
+ }
1231
+ /**
1232
+ * Squash 2 sets of paths & return true if the target has all paths
1233
+ *
1234
+ * @param {string | PathEvent | (string | PathEvent)[]} target Array of Events as strings or pre-parsed
1235
+ * @param has Target must have all these paths
1236
+ * @return {boolean} Whether all are present
1237
+ */
1238
+ static hasAll(target, ...has) {
1239
+ return has.filter((h) => PathEvent.has(target, h)).length == has.length;
1240
+ }
1241
+ /**
1242
+ * Same as `has` but raises an error if there is no overlap
1243
+ *
1244
+ * @param {string | PathEvent | (string | PathEvent)[]} target Array of Events as strings or pre-parsed
1245
+ * @param has Target must have at least one of these path
1246
+ */
1247
+ static hasFatal(target, ...has) {
1248
+ if (!PathEvent.has(target, ...has)) throw new PathError(`Requires one of: ${makeArray(has).join(", ")}`);
1249
+ }
1250
+ /**
1251
+ * Same as `hasAll` but raises an error if the target is missing any paths
1252
+ *
1253
+ * @param {string | PathEvent | (string | PathEvent)[]} target Array of Events as strings or pre-parsed
1254
+ * @param has Target must have all these paths
1255
+ */
1256
+ static hasAllFatal(target, ...has) {
1257
+ if (!PathEvent.hasAll(target, ...has)) throw new PathError(`Requires all: ${makeArray(has).join(", ")}`);
1258
+ }
1259
+ /**
1260
+ * Create event string from its components
1261
+ *
1262
+ * @param {string | string[]} path Event path
1263
+ * @param {Method} methods Event method
1264
+ * @return {string} String representation of Event
1265
+ */
1266
+ static toString(path, methods) {
1267
+ let p = makeArray(path).filter((p2) => !!p2).join("/");
1268
+ p = p?.trim().replaceAll(/\/{2,}/g, "/").replaceAll(/(^\/|\/$)/g, "");
1269
+ if (methods?.length) p += `:${makeArray(methods).map((m) => m.toLowerCase()).join("")}`;
1270
+ return p;
1271
+ }
1272
+ /**
1273
+ * Squash 2 sets of paths & return true if any overlap is found
1274
+ *
1275
+ * @param has Target must have at least one of these path
1276
+ * @return {boolean} Whether there is any overlap
1277
+ */
1278
+ has(...has) {
1279
+ return PathEvent.has(this, ...has);
1280
+ }
1281
+ /**
1282
+ * Squash 2 sets of paths & return true if the target has all paths
1283
+ *
1284
+ * @param has Target must have all these paths
1285
+ * @return {boolean} Whether all are present
1286
+ */
1287
+ hasAll(...has) {
1288
+ return PathEvent.hasAll(this, ...has);
1289
+ }
1290
+ /**
1291
+ * Same as `has` but raises an error if there is no overlap
1292
+ *
1293
+ * @param has Target must have at least one of these path
1294
+ */
1295
+ hasFatal(...has) {
1296
+ return PathEvent.hasFatal(this, ...has);
1297
+ }
1298
+ /**
1299
+ * Same as `hasAll` but raises an error if the target is missing any paths
1300
+ *
1301
+ * @param has Target must have all these paths
1302
+ */
1303
+ hasAllFatal(...has) {
1304
+ return PathEvent.hasAllFatal(this, ...has);
1305
+ }
1306
+ /**
1307
+ * Filter a set of paths based on this event
1308
+ *
1309
+ * @param {string | PathEvent | (string | PathEvent)[]} target Array of events that will filtered
1310
+ * @return {PathEvent[]} Filtered results
1311
+ */
1312
+ filter(target) {
1313
+ return PathEvent.filter(target, this);
1314
+ }
1315
+ /**
1316
+ * Create event string from its components
1317
+ *
1318
+ * @return {string} String representation of Event
1319
+ */
1320
+ toString() {
1321
+ return PathEvent.toString(this.fullPath, this.methods);
1322
+ }
1323
+ }
1324
+ class PathEventEmitter {
1325
+ constructor(prefix = "") {
1326
+ this.prefix = prefix;
1327
+ }
1328
+ listeners = [];
1329
+ emit(event, ...args) {
1330
+ const parsed = event instanceof PathEvent ? event : new PathEvent(`${this.prefix}/${event}`);
1331
+ this.listeners.filter((l) => PathEvent.has(l[0], parsed)).forEach((l) => l[1](parsed, ...args));
1332
+ }
1333
+ off(listener) {
1334
+ this.listeners = this.listeners.filter((l) => l[1] != listener);
1335
+ }
1336
+ on(event, listener) {
1337
+ makeArray(event).forEach((e) => {
1338
+ let fullEvent;
1339
+ if (typeof e === "string") {
1340
+ if (e[0] === ":" && this.prefix) {
1341
+ fullEvent = `${this.prefix}${e}`;
1342
+ } else if (this.prefix) {
1343
+ fullEvent = `${this.prefix}/${e}`;
1344
+ } else {
1345
+ fullEvent = e;
1346
+ }
1347
+ } else {
1348
+ fullEvent = e instanceof PathEvent ? PathEvent.toString(e.fullPath, e.methods) : e;
1349
+ }
1350
+ this.listeners.push([
1351
+ new PathEvent(fullEvent),
1352
+ listener
1353
+ ]);
1354
+ });
1355
+ return () => this.off(listener);
1356
+ }
1357
+ once(event, listener) {
1358
+ return new Promise((res) => {
1359
+ const unsubscribe = this.on(event, (event2, ...args) => {
1360
+ res(args.length < 2 ? args[0] : args);
1361
+ if (listener) listener(event2, ...args);
1362
+ unsubscribe();
1363
+ });
1364
+ });
1365
+ }
1366
+ relayEvents(emitter) {
1367
+ emitter.on("**", (event, ...args) => this.emit(event, ...args));
1368
+ }
1369
+ }
1370
+ var dist = {};
1371
+ var persist = {};
1372
+ var hasRequiredPersist;
1373
+ function requirePersist() {
1374
+ if (hasRequiredPersist) return persist;
1375
+ hasRequiredPersist = 1;
1376
+ Object.defineProperty(persist, "__esModule", { value: true });
1377
+ persist.persist = persist.Persist = void 0;
1378
+ class Persist {
1379
+ key;
1380
+ options;
1381
+ /** Backend service to store data, must implement `Storage` interface */
1382
+ storage;
1383
+ /** Listeners which should be notified on changes */
1384
+ watches = {};
1385
+ /** Private value field */
1386
+ _value;
1387
+ /** Current value or default if undefined */
1388
+ get value() {
1389
+ return this._value !== void 0 ? this._value : this.options?.default;
1390
+ }
1391
+ /** Set value with proxy object wrapper to sync future changes */
1392
+ set value(v) {
1393
+ if (v == null || typeof v != "object")
1394
+ this._value = v;
1395
+ else
1396
+ this._value = new Proxy(v, {
1397
+ get: (target, p) => {
1398
+ const f = typeof target[p] == "function";
1399
+ if (!f)
1400
+ return target[p];
1401
+ return (...args) => {
1402
+ const value = target[p](...args);
1403
+ this.save();
1404
+ return value;
1405
+ };
1406
+ },
1407
+ set: (target, p, newValue) => {
1408
+ target[p] = newValue;
1409
+ this.save();
1410
+ return true;
1411
+ }
1412
+ });
1413
+ this.save();
1414
+ }
1415
+ /**
1416
+ * @param {string} key Primary key value will be stored under
1417
+ * @param {PersistOptions<T>} options Configure using {@link PersistOptions}
1418
+ */
1419
+ constructor(key, options = {}) {
1420
+ this.key = key;
1421
+ this.options = options;
1422
+ this.storage = options.storage || localStorage;
1423
+ this.load();
1424
+ }
1425
+ /** Notify listeners of change */
1426
+ notify(value) {
1427
+ Object.values(this.watches).forEach((watch) => watch(value));
1428
+ }
1429
+ /** Delete value from storage */
1430
+ clear() {
1431
+ this.storage.removeItem(this.key);
1432
+ }
1433
+ /** Save current value to storage */
1434
+ save() {
1435
+ if (this._value === void 0)
1436
+ this.clear();
1437
+ else
1438
+ this.storage.setItem(this.key, JSON.stringify(this._value));
1439
+ this.notify(this.value);
1440
+ }
1441
+ /** Load value from storage */
1442
+ load() {
1443
+ if (this.storage[this.key] != void 0) {
1444
+ let value = JSON.parse(this.storage.getItem(this.key));
1445
+ if (value != null && typeof value == "object" && this.options.type)
1446
+ value.__proto__ = this.options.type.prototype;
1447
+ this.value = value;
1448
+ } else
1449
+ this.value = this.options.default || void 0;
1450
+ }
1451
+ /**
1452
+ * Callback function which is run when there are changes
1453
+ *
1454
+ * @param {(value: T) => any} fn Callback will run on each change; it's passed the next value & it's return is ignored
1455
+ * @returns {() => void} Function which will unsubscribe the watch/callback when called
1456
+ */
1457
+ watch(fn2) {
1458
+ const index = Object.keys(this.watches).length;
1459
+ this.watches[index] = fn2;
1460
+ return () => {
1461
+ delete this.watches[index];
1462
+ };
1463
+ }
1464
+ /**
1465
+ * Return value as JSON string
1466
+ *
1467
+ * @returns {string} Stringified object as JSON
1468
+ */
1469
+ toString() {
1470
+ return JSON.stringify(this.value);
1471
+ }
1472
+ /**
1473
+ * Return current value
1474
+ *
1475
+ * @returns {T} Current value
1476
+ */
1477
+ valueOf() {
1478
+ return this.value;
1479
+ }
1480
+ }
1481
+ persist.Persist = Persist;
1482
+ function persist$1(options) {
1483
+ return (target, prop) => {
1484
+ const key = options?.key || `${target.constructor.name}.${prop.toString()}`;
1485
+ const wrapper = new Persist(key, options);
1486
+ Object.defineProperty(target, prop, {
1487
+ get: function() {
1488
+ return wrapper.value;
1489
+ },
1490
+ set: function(v) {
1491
+ wrapper.value = v;
1492
+ }
1493
+ });
1494
+ };
1495
+ }
1496
+ persist.persist = persist$1;
1497
+ return persist;
1498
+ }
1499
+ var memoryStorage = {};
1500
+ var hasRequiredMemoryStorage;
1501
+ function requireMemoryStorage() {
1502
+ if (hasRequiredMemoryStorage) return memoryStorage;
1503
+ hasRequiredMemoryStorage = 1;
1504
+ Object.defineProperty(memoryStorage, "__esModule", { value: true });
1505
+ memoryStorage.MemoryStorage = void 0;
1506
+ class MemoryStorage {
1507
+ get length() {
1508
+ return Object.keys(this).length;
1509
+ }
1510
+ clear() {
1511
+ Object.keys(this).forEach((k) => this.removeItem(k));
1512
+ }
1513
+ getItem(key) {
1514
+ return this[key];
1515
+ }
1516
+ key(index) {
1517
+ return Object.keys(this)[index];
1518
+ }
1519
+ removeItem(key) {
1520
+ delete this[key];
1521
+ }
1522
+ setItem(key, value) {
1523
+ this[key] = value;
1524
+ }
1525
+ }
1526
+ memoryStorage.MemoryStorage = MemoryStorage;
1527
+ return memoryStorage;
1528
+ }
1529
+ var hasRequiredDist;
1530
+ function requireDist() {
1531
+ if (hasRequiredDist) return dist;
1532
+ hasRequiredDist = 1;
1533
+ (function(exports$1) {
1534
+ var __createBinding = dist && dist.__createBinding || (Object.create ? (function(o, m, k, k2) {
1535
+ if (k2 === void 0) k2 = k;
1536
+ var desc = Object.getOwnPropertyDescriptor(m, k);
1537
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
1538
+ desc = { enumerable: true, get: function() {
1539
+ return m[k];
1540
+ } };
1541
+ }
1542
+ Object.defineProperty(o, k2, desc);
1543
+ }) : (function(o, m, k, k2) {
1544
+ if (k2 === void 0) k2 = k;
1545
+ o[k2] = m[k];
1546
+ }));
1547
+ var __exportStar = dist && dist.__exportStar || function(m, exports$12) {
1548
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports$12, p)) __createBinding(exports$12, m, p);
1549
+ };
1550
+ Object.defineProperty(exports$1, "__esModule", { value: true });
1551
+ __exportStar(requirePersist(), exports$1);
1552
+ __exportStar(requireMemoryStorage(), exports$1);
1553
+ })(dist);
1554
+ return dist;
1555
+ }
1556
+ requireDist();
1557
+ function treeBuilder(schemas) {
1558
+ const tree = [];
1559
+ function findOrCreateNode(tree2, segment, fullPath) {
1560
+ let node = tree2.find((n) => n.name === segment);
1561
+ if (!node) {
1562
+ node = {
1563
+ name: segment,
1564
+ path: fullPath,
1565
+ description: null,
1566
+ children: []
1567
+ };
1568
+ tree2.push(node);
1569
+ }
1570
+ return node;
1571
+ }
1572
+ for (const s of schemas) {
1573
+ const segments = s.path.split("/");
1574
+ let currentLevel = tree;
1575
+ let currentPath = "";
1576
+ segments.forEach((segment, index) => {
1577
+ const isLast = index === segments.length - 1;
1578
+ currentPath += currentPath ? `/${segment}` : segment;
1579
+ let node = findOrCreateNode(currentLevel, segment, currentPath);
1580
+ if (isLast) Object.assign(node, s, { autoIncrement: void 0, exists: true });
1581
+ currentLevel = node.children;
1582
+ });
1583
+ }
1584
+ function sortTree(tree2) {
1585
+ tree2.sort((a, b) => a.name.localeCompare(b.name));
1586
+ for (const node of tree2) {
1587
+ if (node.children && node.children.length > 0) {
1588
+ sortTree(node.children);
1589
+ }
1590
+ }
1591
+ }
1592
+ sortTree(tree);
1593
+ return tree;
1594
+ }
1595
+ function validEvent(event) {
1596
+ return !![
1597
+ "*",
1598
+ "actions",
1599
+ "ai",
1600
+ "analytics",
1601
+ "auth",
1602
+ "call",
1603
+ "data",
1604
+ "discounts",
1605
+ "email",
1606
+ "forms",
1607
+ "groups",
1608
+ "payments",
1609
+ "pdf",
1610
+ "schema",
1611
+ "settings",
1612
+ "sms",
1613
+ "static",
1614
+ "storage",
1615
+ "token",
1616
+ "tools",
1617
+ "users"
1618
+ ].find((e) => event.startsWith(e));
1619
+ }
1620
+ class AssetController extends PathEventEmitter {
1621
+ constructor(momentum, opts) {
1622
+ super();
1623
+ this.momentum = momentum;
1624
+ this.opts = { storage: null, key: "_id", ...opts };
1625
+ if (!this.opts.storage && this.momentum.opts?.offline)
1626
+ this.opts.storage = { storage: momentum.database, key: "_" + this.opts.module };
1627
+ this.cache = new Cache("_id", { persistentStorage: this.opts.storage });
1628
+ this.momentum.auth.on("logout", (e) => this.cache.clear());
1629
+ this.on(this.opts.module, (event, payload) => {
1630
+ if (Array.isArray(payload)) {
1631
+ this.cache.addAll(payload);
1632
+ } else if ((event.create || event.update || event.read) && payload != null) {
1633
+ this.cache.add(payload);
1634
+ } else if (event.delete || payload == null) {
1635
+ if (!event.path) return this.cache.clear();
1636
+ const id = this.opts.key == "_id" ? event.name : this.cache.find({ [this.opts.key]: event.path })?._id;
1637
+ if (id != null) this.cache.delete(id);
1638
+ }
1639
+ });
1640
+ }
1641
+ subscribers = {};
1642
+ cache;
1643
+ opts;
1644
+ /**
1645
+ * Process raw data before caching & returning
1646
+ * @hidden
1647
+ */
1648
+ afterRead(value) {
1649
+ return value;
1650
+ }
1651
+ /**
1652
+ * Process raw data before sending
1653
+ * @hidden
1654
+ */
1655
+ beforeWrite(value) {
1656
+ return value;
1657
+ }
1658
+ /**
1659
+ * Fetch all assets as array
1660
+ * @param {boolean} reload Force reload instead of using cache
1661
+ * @return {Promise<T[]>} All assets as array
1662
+ */
1663
+ async all(reload) {
1664
+ await this.cache.loading;
1665
+ if (!reload && this.cache.complete) return this.cache.all();
1666
+ let resp = await this.momentum.api.request({ url: `api/${this.opts.path || this.opts.module}` });
1667
+ resp = resp.map((v) => this.afterRead(v));
1668
+ this.emit(PES`${this.opts.module}:r`, resp);
1669
+ return resp;
1670
+ }
1671
+ /**
1672
+ * Create new asset
1673
+ * @param {T} value Asset that will be created
1674
+ * @return {Promise<T>} Saved result
1675
+ */
1676
+ async create(value) {
1677
+ await this.cache.loading;
1678
+ let resp = await this.momentum.api.request({
1679
+ url: `api/${this.opts.path || this.opts.module}`,
1680
+ method: "POST",
1681
+ body: this.beforeWrite(value)
1682
+ });
1683
+ resp = this.afterRead(resp);
1684
+ this.emit(PES`${this.opts.module}/${resp[this.opts.key]}:c`, resp);
1685
+ return resp;
1686
+ }
1687
+ /**
1688
+ * Delete asset
1689
+ * @param {string} key Asset primary key
1690
+ * @return {Promise<number>} Returns on success
1691
+ */
1692
+ async delete(key) {
1693
+ await this.cache.loading;
1694
+ const count = await this.momentum.api.request({ url: `api/${this.opts.path || this.opts.module}/${key || ""}`, method: "DELETE" });
1695
+ if (count) this.emit(PES`${this.opts.module}/${key}:d`, key);
1696
+ return count;
1697
+ }
1698
+ /**
1699
+ * Read single asset
1700
+ * @param {string} key Asset primary key
1701
+ * @param {boolean} reload Force reload instead of using cache
1702
+ * @return {Promise<T>} Discovered asset
1703
+ */
1704
+ async read(key, reload) {
1705
+ await this.cache.loading;
1706
+ const cached = !reload ? this.cache.find({ [this.opts.key]: key }) : null;
1707
+ let resp = cached || await this.momentum.api.request({ url: `api/${this.opts.path || this.opts.module}/${key || ""}` });
1708
+ resp = this.afterRead(resp);
1709
+ this.emit(PES`${this.opts.module}/${key}:r`, resp);
1710
+ return resp;
1711
+ }
1712
+ /**
1713
+ * Update exiting asset
1714
+ * @param {T} value Asset that will be updated
1715
+ * @return {Promise<T>} Saved result
1716
+ */
1717
+ async update(value) {
1718
+ await this.cache.loading;
1719
+ let resp = await this.momentum.api.request({
1720
+ url: `api/${this.opts.path || this.opts.module}/${value[this.opts.key] || ""}`,
1721
+ method: "PATCH",
1722
+ body: this.beforeWrite(value)
1723
+ });
1724
+ resp = this.afterRead(resp);
1725
+ this.emit(PES`${this.opts.module}/${value[this.opts.key]}:u`, resp);
1726
+ return resp;
1727
+ }
1728
+ /**
1729
+ * Subscribe to live updates with callback
1730
+ * @param {(value: T[]) => any | null} callback Received changes
1731
+ * @param opts Additional options: path - scope to events within path, reload - Re-fetch even if cached
1732
+ * @return {() => void} Function to unsubscribe
1733
+ */
1734
+ sync(callback, opts = {}) {
1735
+ const e = PE`${this.opts.module}/${opts.path || ""}`;
1736
+ const unsubscribe = callback ? this.on(e.toString(), (event) => callback(event, this.cache.all())) : null;
1737
+ if (callback) callback(e, this.cache.all());
1738
+ if (opts.reload !== false || !this.cache.complete) this.all(true);
1739
+ if (!this.subscribers.length) this.momentum.socket?.subscribe(e.toString());
1740
+ const key = Object.keys(this.subscribers).length.toString();
1741
+ this.subscribers[key] = () => {
1742
+ if (unsubscribe) unsubscribe();
1743
+ delete this.subscribers[key];
1744
+ if (!this.subscribers.length) this.momentum.socket?.unsubscribe(e.toString());
1745
+ };
1746
+ return this.subscribers[key];
1747
+ }
1748
+ }
1749
+ class TreeAssetController extends AssetController {
1750
+ /**
1751
+ * Get all schemas organized into a map
1752
+ * @param {boolean} reload Reload instead of using cache
1753
+ * @return {Promise<TreeNode<Schema>[]>} Schemas as a map
1754
+ */
1755
+ async map(reload) {
1756
+ const rows = await this.all(reload);
1757
+ return treeBuilder(rows);
1758
+ }
1759
+ /**
1760
+ * Subscribe to live updates with optional callback
1761
+ * @param {(schema: Schema[]) => any} callback Receives latest data
1762
+ * @param opts path - scope events, tree - return as a map or array
1763
+ * @return {() => void}
1764
+ */
1765
+ sync(callback, opts = {}) {
1766
+ return super.sync(
1767
+ callback ? (event, value) => callback(event, opts.map ? treeBuilder(value) : value) : void 0,
1768
+ { reload: true, ...opts }
1769
+ );
1770
+ }
1771
+ }
1772
+ var ActionType = /* @__PURE__ */ ((ActionType2) => {
1773
+ ActionType2[ActionType2["CRON"] = 0] = "CRON";
1774
+ ActionType2[ActionType2["EVENT"] = 1] = "EVENT";
1775
+ ActionType2[ActionType2["DELETE"] = 2] = "DELETE";
1776
+ ActionType2[ActionType2["GET"] = 3] = "GET";
1777
+ ActionType2[ActionType2["PATCH"] = 4] = "PATCH";
1778
+ ActionType2[ActionType2["POST"] = 5] = "POST";
1779
+ ActionType2[ActionType2["PUT"] = 6] = "PUT";
1780
+ return ActionType2;
1781
+ })(ActionType || {});
1782
+ class Actions extends AssetController {
1783
+ constructor(momentum) {
1784
+ super(momentum, { module: "actions", key: "_id" });
1785
+ this.momentum = momentum;
1786
+ }
1787
+ /**
1788
+ * Manually trigger an actions execution
1789
+ * @param {string} id Action ID
1790
+ * @param {HttpRequestOptions} opts Additional arguments
1791
+ * @return {Promise<ActionResult>} All action output including console logs & return
1792
+ */
1793
+ debug(id, opts = {}) {
1794
+ return this.momentum.api.request({ url: `api/actions/debug/${id}`, method: "POST", ...opts });
1795
+ }
1796
+ /**
1797
+ * Run an HTTP action
1798
+ * @param {string} path HTTP path excluding `/api/actions/run`
1799
+ * @param {HttpRequestOptions} opts HTTP options
1800
+ * @return {Promise<T>} HTTP response
1801
+ */
1802
+ run(path, opts = {}) {
1803
+ return this.momentum.api.request({ ...opts, url: `api/actions/run/${path}` });
1804
+ }
1805
+ }
1806
+ class Ai extends PathEventEmitter {
1807
+ constructor(momentum) {
1808
+ super();
1809
+ this.momentum = momentum;
1810
+ }
1811
+ /** Cancel current AI requests */
1812
+ abort() {
1813
+ return this.momentum.api.request({ url: "/api/ai/abort", method: "POST" });
1814
+ }
1815
+ /**
1816
+ * Ask the AI assistant a question
1817
+ * @param {string} question Users question
1818
+ * @param {context: string, stream: Function} options context - Any hidden context information. stream - Receive response in chunks
1819
+ * @return {Promise<string>} AI's response
1820
+ */
1821
+ async ask(question, options = {}) {
1822
+ if (!question) throw new Error("Cannot ask AI, missing question");
1823
+ const files = (await Promise.all((options?.files || []).map((f) => new Promise((res) => {
1824
+ const reader = new FileReader();
1825
+ reader.onload = () => res([f.name, reader.result]);
1826
+ reader.readAsDataURL(f);
1827
+ })))).reduce((acc, f) => ({ ...acc, [f[0]]: f[1] }), {});
1828
+ const q = { question: question.trim(), context: options.context, files };
1829
+ if (options.stream && this.momentum.socket?.connected) {
1830
+ let unsub = () => {
1831
+ };
1832
+ return new Promise((res) => {
1833
+ let buffer = "";
1834
+ unsub = this.momentum.socket?.on("llm", (event, chunk) => {
1835
+ options.stream?.(chunk);
1836
+ if (chunk.text) buffer += chunk.text;
1837
+ if (chunk.done) res(buffer);
1838
+ });
1839
+ this.momentum.socket?.send("llm", q);
1840
+ }).finally(() => unsub());
1841
+ } else {
1842
+ return this.momentum.api.request({ url: `api/ai`, method: "POST", body: q }).then((response) => {
1843
+ this.emit(PES`ai:c`, { question, context: options.context, response });
1844
+ return response;
1845
+ });
1846
+ }
1847
+ }
1848
+ /**
1849
+ * Clear AI assistant memory & context
1850
+ * @return {Promise<void>} Resolves once complete
1851
+ */
1852
+ clear() {
1853
+ return this.momentum.api.request({ url: "api/ai", method: "DELETE" }).then(() => this.emit(PES`ai:d`, this.momentum.api.token));
1854
+ }
1855
+ /**
1856
+ * Current chat history
1857
+ * @return {Promise<AiMessage[]>}
1858
+ */
1859
+ history() {
1860
+ return this.momentum.api.request({ url: "api/ai" }).then((resp) => {
1861
+ this.emit(PES`ai:r`, resp);
1862
+ return resp;
1863
+ });
1864
+ }
1865
+ /**
1866
+ * Get model info
1867
+ * @return {AiInfo>} Model Info
1868
+ */
1869
+ info() {
1870
+ return this.momentum.api.request({ url: "api/ai/info" });
1871
+ }
1872
+ }
1873
+ let dialogCount = 0;
1874
+ function createDialog(html, opts = {}) {
1875
+ opts = {
1876
+ backdrop: true,
1877
+ css: void 0,
1878
+ closeBtn: true,
1879
+ position: "center",
1880
+ ...opts
1881
+ };
1882
+ const counter = dialogCount++;
1883
+ if (!document.querySelector("style.momentum-prompt")) {
1884
+ const style2 = document.createElement("style");
1885
+ style2.className = "momentum-prompt";
1886
+ style2.innerHTML = `
1887
+ .momentum-backdrop {
1888
+ position: fixed;
1889
+ inset: 0;
1890
+ backdrop-filter: blur(10px);
1891
+ background: rgba(0, 0, 0, 0.5);
1892
+ animation: fadeIn 0.5s ease-out forwards;
1893
+ z-index: 9998;
1894
+ &.closing { animation: fadeOut 0.5s ease-out forwards; }
1895
+ }
1896
+
1897
+ .momentum-prompt {
1898
+ box-sizing: border-box;
1899
+ font-family: sans-serif;
1900
+ position: fixed;
1901
+ padding: 1rem;
1902
+ width: 100%;
1903
+ max-width: 350px;
1904
+ background: #fff;
1905
+ color: #4a5568;
1906
+ border: 1px solid #fff3;
1907
+ border-radius: 24px;
1908
+ box-shadow: 0 20px 40px #0003;
1909
+ overflow: hidden;
1910
+ z-index: 9999;
1911
+
1912
+ &.center {
1913
+ translate(-50%, -50%);
1914
+ left: 50%;
1915
+ top: 50%;
1916
+ animation: slideInCenter 0.5s ease-out forwards;
1917
+ &.closing { animation: slideOutCenter 0.5s ease-out forwards; }
1918
+ }
1919
+
1920
+ &.corner {
1921
+ right: 1rem;
1922
+ bottom: 1rem;
1923
+ animation: slideIn 0.5s ease-out forwards;
1924
+ &.closing { animation: slideOut 0.5s ease-out forwards; }
1925
+ }
1926
+
1927
+ .close {
1928
+ position: absolute;
1929
+ top: 8px;
1930
+ right: 8px;
1931
+ width: 32px;
1932
+ height: 32px;
1933
+ padding: 0;
1934
+ padding-top: 4px;
1935
+ border: none;
1936
+ background: transparent;
1937
+ border-radius: 50%;
1938
+ cursor: pointer;
1939
+ display: flex;
1940
+ align-items: center;
1941
+ justify-content: center;
1942
+ transition: all 0.5s ease;
1943
+ z-index: 1;
1944
+ &:hover {
1945
+ background: #71809633;
1946
+ transform: scale(1.1);
1947
+ }
1948
+ }
1949
+
1950
+ .btn {
1951
+ flex: 1;
1952
+ padding: 14px 20px;
1953
+ border: 1px solid #71809633;
1954
+ border-radius: 12px;
1955
+ background: #7180961a;
1956
+ color: #718096;
1957
+ font: 600 16px sans-serif;
1958
+ cursor: pointer;
1959
+ transition: all 0.2s ease;
1960
+ position: relative;
1961
+ overflow: hidden;
1962
+ &:hover {transform: translateY(-1px);}
1963
+ &.btn-primary {
1964
+ background: var(--theme-primary);
1965
+ color: var(--theme-primary-contrast);
1966
+ box-shadow: 0 4px 15px #667eea66;
1967
+ }
1968
+ }
1969
+
1970
+ .expand-toggle {
1971
+ display: flex;
1972
+ align-items: center;
1973
+ justify-content: center;
1974
+ gap: 6px;
1975
+ width: 100%;
1976
+ padding: 12px;
1977
+ margin-top: 16px;
1978
+ font-size: 13px;
1979
+ background: none;
1980
+ border: none;
1981
+ border-radius: 8px;
1982
+ color: #718096;
1983
+ cursor: pointer;
1984
+ transition: all 0.2s ease;
1985
+ &:hover { background-color: #7180961a; }
1986
+ }
1987
+
1988
+ .expand-area {transition: all 0.4s ease;}
1989
+
1990
+ .hide {
1991
+ margin: 0;
1992
+ padding: 0;
1993
+ border: 0;
1994
+ height: 0;
1995
+ overflow: hidden;
1996
+ }
1997
+ }
1998
+ @keyframes slideInCenter {
1999
+ from { transform: translate(-50%, calc(100vh + 2rem)) }
2000
+ to { transform: translate(-50%, -50%) }
2001
+ }
2002
+ @keyframes slideOutCenter {
2003
+ from { transform: translate(-50%, -50%) }
2004
+ to { transform: translate(-50%, calc(100vh + 2rem)) }
2005
+ }
2006
+ @keyframes slideIn {
2007
+ from { transform: translateY(calc(100% + 2rem)) }
2008
+ to { transform: translateY(0) }
2009
+ }
2010
+ @keyframes slideOut {
2011
+ from { transform: translateY(0) }
2012
+ to { transform: translateY(calc(100% + 2rem)) }
2013
+ }
2014
+ @keyframes fadeIn {
2015
+ from { opacity: 0 }
2016
+ to { opacity: 1 }
2017
+ }
2018
+ @keyframes fadeOut {
2019
+ from { opacity: 1 }
2020
+ to { opacity: 0 }
2021
+ }`;
2022
+ document.head.append(style2);
2023
+ }
2024
+ let style;
2025
+ if (opts.css) {
2026
+ style = document.createElement("style");
2027
+ style.className = `momentum-prompt-${counter}`;
2028
+ style.innerHTML = opts.css;
2029
+ document.head.append(style);
2030
+ }
2031
+ let backdrop;
2032
+ if (opts.backdrop) {
2033
+ backdrop = document.createElement("div");
2034
+ backdrop.className = `momentum-backdrop`;
2035
+ document.body.append(backdrop);
2036
+ }
2037
+ const dialog = document.createElement("div");
2038
+ dialog.className = "momentum-prompt";
2039
+ dialog.classList.add(opts.position);
2040
+ dialog.innerHTML = html;
2041
+ document.body.append(dialog);
2042
+ let htmlOverflow, bodyOverflow;
2043
+ if (opts.backdrop && document.documentElement.style.overflow != "hidden") {
2044
+ htmlOverflow = document.documentElement.style.overflow;
2045
+ document.documentElement.style.overflow = "hidden";
2046
+ bodyOverflow = document.body.style.overflow;
2047
+ document.body.style.overflow = "hidden";
2048
+ }
2049
+ let resolve;
2050
+ const done = new Promise((r) => resolve = r);
2051
+ const close = (result) => {
2052
+ resolve(result);
2053
+ dialog.classList.add("closing");
2054
+ if (backdrop) backdrop.classList.add("closing");
2055
+ setTimeout(() => {
2056
+ if (htmlOverflow) document.documentElement.style.overflow = htmlOverflow;
2057
+ if (bodyOverflow) document.body.style.overflow = bodyOverflow;
2058
+ dialog.remove();
2059
+ if (backdrop) backdrop.remove();
2060
+ if (style) style.remove();
2061
+ }, 1e3);
2062
+ };
2063
+ if (opts.closeBtn) {
2064
+ const btn = document.createElement("button");
2065
+ btn.className = "close";
2066
+ btn.innerHTML = '<div><svg height="18" viewBox="0 0 24 24"><path stroke="#718096" stroke-width="2" d="M5 5 L19 19 M19 5 L5 19" stroke-linecap="round" /></svg></div>';
2067
+ btn.onclick = close;
2068
+ dialog.prepend(btn);
2069
+ }
2070
+ return { close, dialog, result: done };
2071
+ }
2072
+ class Analytics extends AssetController {
2073
+ constructor(momentum) {
2074
+ super(momentum, { module: "analytics", key: "_id" });
2075
+ this.momentum = momentum;
2076
+ if (this.momentum.client.isHeadless) {
2077
+ this.performance = Promise.resolve(this._performance);
2078
+ } else {
2079
+ this.performance = Promise.all([
2080
+ this.collectNavigationMetrics(),
2081
+ this.collectLCPMetrics()
2082
+ ]).then(() => this._performance);
2083
+ }
2084
+ if (this.momentum.opts.analytics !== false) {
2085
+ if (this.momentum.client.storage) {
2086
+ this.id = JSONAttemptParse(sessionStorage.getItem(`${this.momentum.url.host}:analytics`) || "null");
2087
+ if (this.id) this.new = false;
2088
+ }
2089
+ this.momentum.auth.on("logout", () => {
2090
+ if (this.momentum.client.storage) sessionStorage.removeItem(`${this.momentum.url.host}:analytics`);
2091
+ this._id = "";
2092
+ this._consent = void 0;
2093
+ this._new = true;
2094
+ });
2095
+ this.momentum.auth.on("login", async () => {
2096
+ if (this.consent) await this.init();
2097
+ });
2098
+ if (this.momentum.opts.analytics == "force") this.consent = true;
2099
+ else if (this.momentum.opts.analytics == "anonymous") {
2100
+ this.consent = false;
2101
+ this.init();
2102
+ } else if (this.momentum.client.storage) {
2103
+ const stored = localStorage.getItem(`${this.momentum.url.host}:consent`);
2104
+ if (stored == "true") this.consent = true;
2105
+ else if (stored && !this.canPromptAgain(stored)) {
2106
+ this.consent = false;
2107
+ this.init();
2108
+ } else {
2109
+ if (stored) localStorage.removeItem(`${this.momentum.url.host}:consent`);
2110
+ this.init();
2111
+ }
2112
+ } else {
2113
+ this.init();
2114
+ }
2115
+ if (this.consent == null && !this.momentum.client.isHeadless && this.momentum.opts.analytics == "prompt")
2116
+ setTimeout(() => this.consentPrompt(), 1e3);
2117
+ }
2118
+ }
2119
+ lastPage;
2120
+ _ready;
2121
+ ready = new Promise((res) => this._ready = res);
2122
+ _performance = { fcp: 0, lcp: 0, ttfb: 0, dom: 0, total: 0 };
2123
+ performance;
2124
+ _consented;
2125
+ consented = new Promise((res) => this._consented = res);
2126
+ _consent;
2127
+ get consent() {
2128
+ return this._consent;
2129
+ }
2130
+ set consent(value) {
2131
+ if (this._consent === value || value == null) return;
2132
+ this._consent = value;
2133
+ if (this.momentum.client.storage) {
2134
+ if (value) localStorage.setItem(`${this.momentum.url.host}:consent`, "true");
2135
+ else if (this.canPromptAgain()) localStorage.setItem(`${this.momentum.url.host}:consent`, (/* @__PURE__ */ new Date()).toISOString());
2136
+ }
2137
+ if (this._consented) {
2138
+ this._consented(this._consent);
2139
+ this._consented = null;
2140
+ }
2141
+ this.emit("consent", value);
2142
+ if (value) this.init();
2143
+ }
2144
+ _id;
2145
+ get id() {
2146
+ return this._id;
2147
+ }
2148
+ set id(id) {
2149
+ this._id = id;
2150
+ if (this.momentum.client.storage) sessionStorage.setItem(`${this.momentum.url.host}:analytics`, id);
2151
+ if (this.momentum.opts.credentialsStrategy == "auto" && !this.momentum.api.sameOrigin || this.momentum.opts.credentialsStrategy == "header")
2152
+ this.momentum.api.headers["X-Analytics"] = id;
2153
+ }
2154
+ _new = true;
2155
+ set new(val) {
2156
+ this._new = val;
2157
+ }
2158
+ get new() {
2159
+ return this._new;
2160
+ }
2161
+ canPromptAgain(date = localStorage.getItem(`${this.momentum.url.host}:consent`)) {
2162
+ if (!date) return true;
2163
+ if (date == "true") return false;
2164
+ if (typeof date != "object") date = new Date(date);
2165
+ return Date.now() - date.getTime() >= 6e4 * 60 * 24 * 30;
2166
+ }
2167
+ collectNavigationMetrics() {
2168
+ return new Promise((res) => {
2169
+ window.addEventListener("load", () => {
2170
+ const nav = performance.getEntriesByType("navigation")[0];
2171
+ if (nav) {
2172
+ this._performance.ttfb = Math.round(nav.responseStart - nav.requestStart);
2173
+ this._performance.dom = Math.round(nav.domContentLoadedEventEnd - nav.startTime);
2174
+ this._performance.total = Math.round(nav.loadEventEnd - nav.startTime);
2175
+ } else {
2176
+ this._performance.dom = Date.now() - performance.timeOrigin;
2177
+ }
2178
+ try {
2179
+ const paints = performance.getEntriesByType("paint");
2180
+ const fcp = paints.find((e) => e.name === "first-contentful-paint");
2181
+ if (fcp) this._performance.fcp = Math.round(fcp.startTime);
2182
+ } catch {
2183
+ }
2184
+ res();
2185
+ });
2186
+ });
2187
+ }
2188
+ collectLCPMetrics() {
2189
+ return new Promise((res) => {
2190
+ let resolved = false;
2191
+ try {
2192
+ const lcpObserver = new PerformanceObserver((entryList) => {
2193
+ const lcp = entryList.getEntries().pop();
2194
+ if (lcp) this._performance.lcp = Math.round(lcp.startTime);
2195
+ lcpObserver.disconnect();
2196
+ if (!resolved) {
2197
+ resolved = true;
2198
+ res();
2199
+ }
2200
+ });
2201
+ lcpObserver.observe({ type: "largest-contentful-paint", buffered: true });
2202
+ setTimeout(() => {
2203
+ if (!resolved) {
2204
+ lcpObserver.disconnect();
2205
+ res();
2206
+ }
2207
+ }, 3e3);
2208
+ } catch (_) {
2209
+ res();
2210
+ }
2211
+ });
2212
+ }
2213
+ async init() {
2214
+ if (!this.momentum.opts.analytics) return;
2215
+ const performance2 = await this.performance;
2216
+ this.id = await this.momentum.api.request({
2217
+ url: `api/analytics`,
2218
+ body: [
2219
+ navigator?.language,
2220
+ (/* @__PURE__ */ new Date()).getTimezoneOffset(),
2221
+ this.consent ? 1 : 0,
2222
+ navigator?.hardwareConcurrency ?? null,
2223
+ navigator?.deviceMemory ?? null,
2224
+ this.momentum.client.isHeadless ? null : window.innerWidth,
2225
+ this.momentum.client.isHeadless ? null : window.innerHeight,
2226
+ await this.momentum.client.networkSpeed(),
2227
+ performance2.fcp,
2228
+ performance2.lcp,
2229
+ performance2.ttfb,
2230
+ performance2.dom,
2231
+ performance2.total
2232
+ ]
2233
+ });
2234
+ if (this._ready) {
2235
+ this._ready();
2236
+ this._ready = null;
2237
+ }
2238
+ this.page();
2239
+ window.addEventListener("beforeunload", () => this.page("CLOSED"));
2240
+ return this.id;
2241
+ }
2242
+ async consentPrompt(content) {
2243
+ const settings = await this.momentum.settings.map();
2244
+ const { close, dialog, result } = createDialog(`
2245
+ <div style="margin: 1rem; text-align: center">${content || `<p style="margin: 0;">This website uses cookies to improve your experience. ${settings.privacy_policy_url ? `For more information, please read our <a href="${this.momentum.url}${settings.privacy_policy_url}" target="_blank">Privacy Policy</a>.` : ""}</p>`}</div>
2246
+ <div style="display: flex; gap: 10px">
2247
+ <button class="agree btn btn-primary" >Agree</button>
2248
+ <button class="decline btn">Essential Only</button>
2249
+ </div>`, { backdrop: false, position: "corner" });
2250
+ dialog.addEventListener("click", (e) => {
2251
+ let t = e.target, depth = 0;
2252
+ while (!!t && t.tagName != "BUTTON" && ++depth < 5) t = t.parentElement;
2253
+ if (!t || t.tagName != "BUTTON") return;
2254
+ if (t.matches(".agree")) close(true);
2255
+ else if (t.matches(".decline")) close(false);
2256
+ });
2257
+ return result.then((consent) => {
2258
+ this.consent = !!consent;
2259
+ return this.consent;
2260
+ });
2261
+ }
2262
+ async speedTest(format = false, bytes) {
2263
+ let bps = 0;
2264
+ if (!bytes) {
2265
+ bps = await this.speedTest(false, 1024 * 1024);
2266
+ if (bps > 1024 * 1024 * 10) bps = await this.speedTest(false, 1024 * 1024 * 20);
2267
+ } else {
2268
+ const start = performance.now();
2269
+ await this.momentum.api.request({ url: `api/tools/dummy-file?size=${bytes}` });
2270
+ const duration = (performance.now() - start) / 1e3;
2271
+ bps = Math.ceil(bytes / duration);
2272
+ }
2273
+ return format ? formatBytes(bps, 2) : bps;
2274
+ }
2275
+ ipTrace(ip) {
2276
+ return this.momentum.api.request({ url: `api/analytics/trace/${ip}` });
2277
+ }
2278
+ async metric(name, value, strategy = "add") {
2279
+ if (!this.momentum.opts.analytics) return;
2280
+ await this.ready;
2281
+ return this.momentum.api.request({
2282
+ url: "api/analytics/metric",
2283
+ body: {
2284
+ name: Array.isArray(name) ? name[0] : name,
2285
+ category: Array.isArray(name) ? name[1] : null,
2286
+ value,
2287
+ strategy
2288
+ }
2289
+ });
2290
+ }
2291
+ async page(page = location.href, meta = {}) {
2292
+ if (!this.momentum.opts.analytics || this.lastPage == page) return;
2293
+ await this.ready;
2294
+ return this.momentum.api.request({
2295
+ url: "api/analytics/page",
2296
+ body: { page, timestamp: /* @__PURE__ */ new Date(), meta }
2297
+ }).then(() => this.lastPage = page);
2298
+ }
2299
+ }
2300
+ class Api extends Http {
2301
+ constructor(momentum) {
2302
+ super({ ...momentum.opts.http, url: momentum.url.toString() });
2303
+ this.momentum = momentum;
2304
+ this.storageKey = `${this.momentum.url.host}:token`;
2305
+ if (this.momentum.opts.persist && typeof localStorage != "undefined") {
2306
+ const token = localStorage.getItem(this.storageKey);
2307
+ if (token) this.token = token;
2308
+ }
2309
+ }
2310
+ emitter = new PathEventEmitter("api");
2311
+ pending = {};
2312
+ storageKey;
2313
+ get sameOrigin() {
2314
+ if (typeof window == "undefined" || !window?.location) return false;
2315
+ return window.location.host == this.momentum.url.host;
2316
+ }
2317
+ _token = null;
2318
+ /** Current API token */
2319
+ get token() {
2320
+ return this._token;
2321
+ }
2322
+ set token(token) {
2323
+ if (token == this._token) return;
2324
+ this._token = token;
2325
+ if (this.momentum.opts.credentialsStrategy == "auto" && !this.sameOrigin || this.momentum.opts.credentialsStrategy == "header") {
2326
+ this.headers["Authorization"] = this._token ? `Bearer ${this._token}` : null;
2327
+ if (this.momentum.opts.persist && typeof localStorage != "undefined") {
2328
+ if (this._token) localStorage.setItem(this.storageKey, this._token);
2329
+ else localStorage.removeItem(this.storageKey);
2330
+ }
2331
+ }
2332
+ this.emit(`token:${token ? "u" : "d"}`, this._token);
2333
+ }
2334
+ // Path emitter functions
2335
+ emit = this.emitter.emit.bind(this.emitter);
2336
+ off = this.emitter.off.bind(this.emitter);
2337
+ on = this.emitter.on.bind(this.emitter);
2338
+ once = this.emitter.once.bind(this.emitter);
2339
+ relayEvents = this.emitter.relayEvents.bind(this.emitter);
2340
+ /**
2341
+ * Fetch current Momentum status
2342
+ * @return {Promise<Health>}
2343
+ */
2344
+ healthcheck() {
2345
+ return this.request({ url: "api/tools/healthcheck" }).then((resp) => {
2346
+ this.emit(`healthcheck:r`, resp);
2347
+ return resp;
2348
+ });
2349
+ }
2350
+ /**
2351
+ * Create API request
2352
+ * @param {HttpRequestOptions} options Request options
2353
+ * @return {Promise} Response
2354
+ */
2355
+ request(options) {
2356
+ const method = options.method == "GET" ? "r" : options.method == "POST" ? "c" : options.method == "DELETE" ? "d" : "u";
2357
+ if (options.url) options.url = encodeURI(options.url.replaceAll("#", "%23"));
2358
+ const key = options.url + method + (options.headers ? JSONSanitize(options.headers) : "") + (options.body ? JSONSanitize(options.body) : "");
2359
+ if (this.pending[key] != null) return this.pending[key];
2360
+ if (this.momentum.client?.captchaScore != null) {
2361
+ if (!options.headers) options.headers = {};
2362
+ options.headers["X-Captcha"] = (+this.momentum.client.captchaScore.toFixed(2)).toString();
2363
+ }
2364
+ this.pending[key] = super.request({ ...options, credentials: "include" }).then((response) => {
2365
+ this.emit(`response:${method}`, { request: options, response });
2366
+ return options.decode === false ? response : response.data;
2367
+ }).catch((err) => {
2368
+ const e = err?.data || err;
2369
+ this.emit(`error:${method}`, { request: options, error: e });
2370
+ throw e;
2371
+ }).finally(() => delete this.pending[key]);
2372
+ this.emit(PES`api/request:${method}`, { request: options, response: this.pending[key] });
2373
+ return this.pending[key];
2374
+ }
2375
+ }
2376
+ class Audit extends PathEventEmitter {
2377
+ constructor(momentum) {
2378
+ super("audit");
2379
+ this.momentum = momentum;
2380
+ }
2381
+ async available(module2) {
2382
+ return this.momentum.api.request({ url: `api/audit/${module2 || ""}` });
2383
+ }
2384
+ async history(module2, pk) {
2385
+ if (!module2 || !pk) throw new Error("Module and primary key required to fetch audit history");
2386
+ return this.momentum.api.request({ url: `api/audit/${module2}/${pk || ""}` });
2387
+ }
2388
+ async rollback(id) {
2389
+ if (!id) throw new Error("Audit ID required to rollback");
2390
+ return this.momentum.api.request({ url: `api/audit/${id}`, method: "PATCH" });
2391
+ }
2392
+ }
2393
+ class Totp {
2394
+ constructor(momentum) {
2395
+ this.momentum = momentum;
2396
+ }
2397
+ /**
2398
+ * Set default 2FA method
2399
+ * @param {string} username User to set 2FA method on
2400
+ * @param {string} method Default method, must be either: app, call, email, sms
2401
+ * @returns {Promise<void>} Returns once complete
2402
+ */
2403
+ default(username, method) {
2404
+ return this.momentum.api.request({ url: `/api/auth/totp/${username}`, method: "PUT", body: { method } });
2405
+ }
2406
+ /**
2407
+ * Disable 2FA for user
2408
+ * @param {string} username User to disable 2FA for
2409
+ * @param password Confirm user password (Not required as admin)
2410
+ * @return {Promise<void>} Resolves once complete
2411
+ */
2412
+ disable(username, password) {
2413
+ return this.momentum.api.request({ url: `/api/auth/totp/${username}`, method: `DELETE`, body: { password } });
2414
+ }
2415
+ /**
2416
+ * Enable 2FA for user
2417
+ * @param {string} username User to reset
2418
+ * @return {Promise<void>} Resolves once complete
2419
+ */
2420
+ enable = this.reset;
2421
+ /**
2422
+ * Reset users 2FA
2423
+ * @param {string} username User to reset
2424
+ * @param password Confirm user password (Not required as admin)
2425
+ * @return {Promise<void>} Resolves once complete
2426
+ */
2427
+ reset(username, password) {
2428
+ return this.momentum.api.request({ url: `/api/auth/totp/${username}`, method: "PATCH", body: { password } });
2429
+ }
2430
+ /**
2431
+ * Fetch TOTP secret to set up authenticator
2432
+ * @param {string} username
2433
+ * @param password Confirm user password (Not required as admin)
2434
+ * @returns {Promise<{secret: string, qr: string}>}
2435
+ */
2436
+ secret(username, password) {
2437
+ return this.momentum.api.request({ url: `/api/auth/totp/${username}`, method: "POST", body: { password } });
2438
+ }
2439
+ }
2440
+ class Auth extends PathEventEmitter {
2441
+ constructor(momentum) {
2442
+ super("auth");
2443
+ this.momentum = momentum;
2444
+ this.totp = new Totp(momentum);
2445
+ this.momentum.api.addInterceptor((resp, next) => {
2446
+ const blacklist = ["api/auth/login", "api/auth/password", "api/auth/totp"];
2447
+ if (resp.status == 401 && !blacklist.find((url) => resp.url.includes(url)))
2448
+ this.emit(PES`expired:d`, this.momentum.api.token);
2449
+ next();
2450
+ });
2451
+ this.on("expired", async () => {
2452
+ if (this.momentum.opts.expiredStrategy != "ignore") await this.momentum.auth.logout();
2453
+ if (this.momentum.opts.expiredStrategy == "reload") window.location.reload();
2454
+ });
2455
+ }
2456
+ /** Manage user 2FA */
2457
+ totp;
2458
+ session;
2459
+ _permissions = [];
2460
+ /** Get current user permissions */
2461
+ get permissions() {
2462
+ return this._permissions;
2463
+ }
2464
+ set permissions(perms) {
2465
+ this._permissions = perms;
2466
+ this.emit(PES`permissions:u`, this._permissions);
2467
+ }
2468
+ _user;
2469
+ /** Get current user, undefined if not yet initialized */
2470
+ get user() {
2471
+ return this._user;
2472
+ }
2473
+ /** Update user info without changing the session */
2474
+ set user(user) {
2475
+ if (!isEqual(this.user, user)) {
2476
+ this._user = user ? user : null;
2477
+ this.emit(PES`user:u`, this._user);
2478
+ }
2479
+ }
2480
+ /** Filter list of permissions to ones the user has */
2481
+ filter = (...events) => PathEvent.filter(this.permissions, ...events);
2482
+ /** Does user have any permissions (or) */
2483
+ has = (...events) => PathEvent.has(this.permissions, ...events);
2484
+ /** Does user have all permissions (and) */
2485
+ hasAll = (...events) => PathEvent.hasAll(this.permissions, ...events);
2486
+ /** Raise error if user has no permissions (or) */
2487
+ hasFatal = (...events) => PathEvent.hasFatal(this.permissions, ...events);
2488
+ /** Raise error if user missing any permissions (and) */
2489
+ hasAllFatal = (...events) => PathEvent.hasAllFatal(this.permissions, ...events);
2490
+ async handleLogin(reload = false) {
2491
+ const urlParams = new URLSearchParams(window.location.search);
2492
+ const token = urlParams.get("token");
2493
+ let s;
2494
+ if (!token) {
2495
+ s = await this.readSession();
2496
+ } else {
2497
+ urlParams.delete("token");
2498
+ history.replaceState(null, "", `${window.location.pathname}?${urlParams.toString()}`);
2499
+ s = await this.readSession(token, true);
2500
+ }
2501
+ if (!s?.user) await this.loginRedirect();
2502
+ if (reload && this.user) location.reload();
2503
+ return this.session;
2504
+ }
2505
+ /**
2506
+ * Check if origin is recognized & whitelisted
2507
+ * @param {string} host Origin to check
2508
+ * @return {Promise<void>} Resolves in known, 401 code otherwise
2509
+ */
2510
+ knownHost(host = location.origin) {
2511
+ if (host.startsWith("/")) return Promise.resolve();
2512
+ return this.momentum.api.request({ url: `api/auth/known-host?host=${encodeURI(new URL(host).origin)}` });
2513
+ }
2514
+ /**
2515
+ * Login a user & return the account
2516
+ * @param {string} id username
2517
+ * @param {string} password user's password
2518
+ * @param {{totp: string, totpMethod: TotpMethods, expire: null | number | Date}} opts 2FA code, 2FA code push method, and expiry options (null to never expire)
2519
+ * @return {Promise<{reset?: string, token?: string} | null>} User account on success
2520
+ */
2521
+ login(id, password, opts) {
2522
+ if (!id) throw new Error("Cannot login, missing ID or password");
2523
+ if (opts?.totpMethod && !["call", "email", "sms"].includes(opts.totpMethod))
2524
+ throw new Error(`Invalid 2FA method: ${opts.totpMethod}
2525
+ Must be either: call, email or sms`);
2526
+ return this.momentum.api.request({
2527
+ url: "api/auth/login",
2528
+ headers: password ? { Authorization: void 0 } : void 0,
2529
+ method: "POST",
2530
+ body: clean({
2531
+ username: id.trim(),
2532
+ password: password?.trim(),
2533
+ totp: opts?.totp ? opts.totp.toString() : void 0,
2534
+ totpMethod: opts?.totpMethod,
2535
+ expire: opts?.expire
2536
+ }, true)
2537
+ }).then(async (resp) => {
2538
+ if (resp?.token) await this.readSession(resp.token, true);
2539
+ return resp;
2540
+ });
2541
+ }
2542
+ /**
2543
+ * Login via Momentum single sign on
2544
+ * @param {string} host Host origin attempting to login
2545
+ * @return {Promise<string>} Token on success
2546
+ */
2547
+ loginRedirect(host = location.origin) {
2548
+ return new Promise((res, rej) => {
2549
+ let listener, win;
2550
+ window.addEventListener("message", listener = async (event) => {
2551
+ const data = event?.data || {};
2552
+ if (event.origin != origin || data.sender != origin) return;
2553
+ if (!data.token) return rej("Unknown response from login");
2554
+ window.removeEventListener("message", listener);
2555
+ await this.readSession(data.token, true);
2556
+ win.close();
2557
+ res(data.token);
2558
+ });
2559
+ win = window.open(`${this.momentum.opts.loginUrl}?redirect=postmessage&host=${encodeURI(host)}`, "_blank");
2560
+ if (!win) {
2561
+ window.removeEventListener("message", listener);
2562
+ return rej("Unable to open login");
2563
+ }
2564
+ });
2565
+ }
2566
+ /**
2567
+ * Logout current user
2568
+ */
2569
+ async logout() {
2570
+ await this.momentum.api.request({ url: "api/auth/logout", method: "DELETE" });
2571
+ this.momentum.api.token = null;
2572
+ await this.readSession();
2573
+ this.emit(PES`logout:d`, null);
2574
+ }
2575
+ /**
2576
+ * Create a new user with login
2577
+ * @param {Partial<User> & {password: string}} user User data with password
2578
+ * @return {Promise<User>} Registered user data
2579
+ */
2580
+ async register(user) {
2581
+ if (!user._id || !user.password) throw new Error("Cannot register user, missing username or password");
2582
+ const u = await this.momentum.api.request({ url: "api/auth/register", body: { ...user } });
2583
+ if (u?.image?.startsWith("/")) u.image = `${this.momentum.url.toString().slice(0, -1)}${u.image}${!this.momentum.api.sameOrigin && this.momentum.api.token ? `?token=${this.momentum.api.token}` : ""}`;
2584
+ this.user = u;
2585
+ this.permissions = [];
2586
+ this.emit(PES`register:u`, u);
2587
+ return u;
2588
+ }
2589
+ reset(emailOrPass, token) {
2590
+ if (!emailOrPass) throw new Error("Cannot reset password, missing email or token");
2591
+ return this.momentum.api.request({
2592
+ url: "api/auth/reset",
2593
+ headers: { "Authorization": token ? `Bearer ${token}` : void 0 },
2594
+ method: "PATCH",
2595
+ body: {
2596
+ email: token ? void 0 : emailOrPass,
2597
+ password: token ? emailOrPass : void 0
2598
+ }
2599
+ }).then(() => {
2600
+ this.emit(PES`reset:${token ? "u" : "c"}`, token || emailOrPass);
2601
+ });
2602
+ }
2603
+ /**
2604
+ * Get session information
2605
+ * @param {string} token Token to fetch session info for
2606
+ * @param {boolean} set Set as current active session
2607
+ * @return {Promise<{token: string, user: User, permissions: string[], custom: any} | null>} Session information
2608
+ */
2609
+ async readSession(token, set = false) {
2610
+ const t = token || this.momentum.api.token;
2611
+ const session = await this.momentum.api.request({
2612
+ url: "api/auth/session",
2613
+ headers: { "Authorization": t ? `Bearer ${t}` : void 0 }
2614
+ });
2615
+ if (!token || set) {
2616
+ this.momentum.api.token = session?.token || null;
2617
+ if (session?.user) session.user.image = `${this.momentum.url.toString().slice(0, -1)}${session.user.image}${!this.momentum.api.sameOrigin && this.momentum.api.token ? `?token=${this.momentum.api.token}` : ""}`;
2618
+ this.session = session;
2619
+ this.permissions = session?.permissions || [];
2620
+ this.user = session?.user || null;
2621
+ if (session?.user) this.emit(PES`login:c`, session.user);
2622
+ }
2623
+ this.emit(PES`session:r`, session);
2624
+ return session;
2625
+ }
2626
+ /** Unlock an account that has been locked from too many login attempts */
2627
+ async unlock(username) {
2628
+ return this.momentum.api.request({ url: "api/auth/unlock", method: "PATCH", body: { username } });
2629
+ }
2630
+ /**
2631
+ * Update password for user
2632
+ * @param {string} username User to reset
2633
+ * @param {string} password New user password
2634
+ * @param {string} oldPassword Old password for validation
2635
+ * @return {Promise<void>} Resolves once complete
2636
+ */
2637
+ async updatePassword(username, password, oldPassword) {
2638
+ if (!username || !password) throw new Error("Cannot update password, missing username or password");
2639
+ return this.momentum.api.request({
2640
+ url: "api/auth/password",
2641
+ method: "PATCH",
2642
+ body: { username, password, oldPassword }
2643
+ }).then((resp) => {
2644
+ this.emit(PES`reset:u`, resp?.token);
2645
+ if (resp?.token) this.momentum.api.token = resp.token;
2646
+ });
2647
+ }
2648
+ }
2649
+ class Call extends PathEventEmitter {
2650
+ constructor(momentum) {
2651
+ super();
2652
+ this.momentum = momentum;
2653
+ }
2654
+ /**
2655
+ * Place a call to number
2656
+ * @param {Call} message Text that will be converted to speech
2657
+ * @return {Promise<any>} Twilio call instance
2658
+ */
2659
+ create(message) {
2660
+ return this.momentum.api.request({ url: "api/call", body: message }).then((resp) => this.emit(PES`call/${message.to}`, message));
2661
+ }
2662
+ }
2663
+ class Captcha {
2664
+ start = Date.now();
2665
+ events = { mouse: [], keys: [], clicks: [] };
2666
+ lastMouse = null;
2667
+ velocities = [];
2668
+ _score = 0.5;
2669
+ set score(v) {
2670
+ this._score = v;
2671
+ }
2672
+ get score() {
2673
+ return this._score;
2674
+ }
2675
+ get verdict() {
2676
+ if (this.score == 0.5) return "unknown";
2677
+ if (this.score > 0.5) return "human";
2678
+ return "bot";
2679
+ }
2680
+ constructor() {
2681
+ if (typeof window === "undefined" || !window.innerWidth || !this.validUserAgent()) {
2682
+ this.score = 0;
2683
+ return;
2684
+ }
2685
+ document.addEventListener("mousemove", (e) => {
2686
+ const now = Date.now();
2687
+ const curr = { x: e.clientX, y: e.clientY, t: now };
2688
+ this.events.mouse.push(curr);
2689
+ if (this.lastMouse) {
2690
+ const dt = now - this.lastMouse.t;
2691
+ const dx = curr.x - this.lastMouse.x;
2692
+ const dy = curr.y - this.lastMouse.y;
2693
+ const dist2 = Math.sqrt(dx * dx + dy * dy);
2694
+ if (dt > 0) this.velocities.push(dist2 / dt);
2695
+ }
2696
+ this.lastMouse = curr;
2697
+ if (this.events.mouse.length > 50) this.events.mouse.shift();
2698
+ if (this.velocities.length > 30) this.velocities.shift();
2699
+ });
2700
+ document.addEventListener("keydown", (e) => {
2701
+ this.events.keys.push(Date.now());
2702
+ if (this.events.keys.length > 20) this.events.keys.shift();
2703
+ });
2704
+ document.addEventListener("click", (e) => {
2705
+ this.events.clicks.push({ x: e.clientX, y: e.clientY, t: Date.now() });
2706
+ if (this.events.clicks.length > 15) this.events.clicks.shift();
2707
+ });
2708
+ setInterval(() => this.update(), 1e3);
2709
+ }
2710
+ validUserAgent() {
2711
+ if (!navigator?.userAgent) return false;
2712
+ const ua = navigator.userAgent.toLowerCase();
2713
+ if (["bot", "crawler", "spider", "scraper", "headless", "phantom", "selenium", "webdriver", "puppeteer", "playwright", "cypress", "automation"].some((pattern) => ua.includes(pattern))) return false;
2714
+ if (!["chrome", "firefox", "safari", "edge", "opera"].some((browser) => ua.includes(browser))) return false;
2715
+ if (!["windows", "mac", "linux", "android", "ios"].some((os) => ua.includes(os))) return false;
2716
+ return ua.length > 20 && ua.length < 1e3;
2717
+ }
2718
+ update() {
2719
+ const totalEvents = this.events.mouse.length + this.events.keys.length + this.events.clicks.length;
2720
+ const timeBonus = Math.min((Date.now() - this.start) / 2e4, 0.15);
2721
+ const activityBonus = totalEvents > 10 ? 0.15 : totalEvents > 3 ? 0.1 : 0;
2722
+ let mouseScore = 0.6;
2723
+ if (this.events.mouse.length > 5) {
2724
+ const recent = this.events.mouse.slice(-15);
2725
+ let changes = 0;
2726
+ for (let i = 2; i < recent.length; i++) {
2727
+ const a1 = Math.atan2(recent[i - 1].y - recent[i - 2].y, recent[i - 1].x - recent[i - 2].x);
2728
+ const a2 = Math.atan2(recent[i].y - recent[i - 1].y, recent[i].x - recent[i - 1].x);
2729
+ if (Math.abs(a1 - a2) > 0.5) changes++;
2730
+ }
2731
+ mouseScore = 0.6 + Math.min(changes / recent.length, 0.3);
2732
+ }
2733
+ let velocityScore = 0.7;
2734
+ if (this.velocities.length > 5) {
2735
+ const avg = this.velocities.reduce((a, b) => a + b, 0) / this.velocities.length;
2736
+ if (avg > 0.01) velocityScore = 0.8;
2737
+ }
2738
+ let typingScore = 0.7;
2739
+ if (this.events.keys.length > 2) {
2740
+ const intervals = [];
2741
+ for (let i = 1; i < this.events.keys.length; i++) {
2742
+ intervals.push(this.events.keys[i] - this.events.keys[i - 1]);
2743
+ }
2744
+ const avgInterval = intervals.reduce((a, b) => a + b, 0) / intervals.length;
2745
+ if (avgInterval > 50 && avgInterval < 1e3) typingScore = 0.85;
2746
+ }
2747
+ const newScore = 0.6 + mouseScore * 0.25 + velocityScore * 0.2 + typingScore * 0.15 + timeBonus + activityBonus;
2748
+ this.score = this.score * 0.8 + newScore * 0.2;
2749
+ this.score = Math.max(0.3, Math.min(0.95, this.score));
2750
+ }
2751
+ }
2752
+ class Client extends PathEventEmitter {
2753
+ constructor(momentum) {
2754
+ super();
2755
+ this.momentum = momentum;
2756
+ this.momentum.on("analytics/speed", (event, bps) => {
2757
+ if (this.storage) sessionStorage.setItem(`${this.momentum.url.host}:speed`, bps.toString());
2758
+ this.momentum.client.speed = bps;
2759
+ });
2760
+ if (!this.isHeadless) {
2761
+ window.addEventListener("beforeinstallprompt", (event) => {
2762
+ event.preventDefault();
2763
+ this.nativePwaPrompt = event;
2764
+ this.emit("install:r");
2765
+ });
2766
+ if (this.storage && sessionStorage.getItem(`${this.momentum.url.host}:speed`)) {
2767
+ this.speed = Promise.resolve(+sessionStorage.getItem(`${this.momentum.url.host}:speed`));
2768
+ } else {
2769
+ this.speed = new Promise((res) => {
2770
+ window.addEventListener("load", async () => res(await this.momentum.analytics.speedTest()));
2771
+ });
2772
+ }
2773
+ }
2774
+ }
2775
+ captcha = new Captcha();
2776
+ nativePwaPrompt;
2777
+ speed;
2778
+ updating = false;
2779
+ get actorType() {
2780
+ return this.captcha.verdict;
2781
+ }
2782
+ get captchaScore() {
2783
+ return this.captcha.score;
2784
+ }
2785
+ /** Check for updates */
2786
+ updateReady = new Promise((res) => {
2787
+ if (!navigator.serviceWorker) return res(false);
2788
+ navigator.serviceWorker.getRegistration().then((sw) => {
2789
+ if (!sw) return res(false);
2790
+ if (sw.waiting) return res(true);
2791
+ const t = setTimeout(() => res(false), 1e4);
2792
+ sw.addEventListener("updatefound", () => {
2793
+ clearTimeout(t);
2794
+ res(true);
2795
+ });
2796
+ sw.update();
2797
+ }).catch(() => res(false));
2798
+ });
2799
+ /** Check if PWA can be installed */
2800
+ get canInstall() {
2801
+ return !!this.nativePwaPrompt;
2802
+ }
2803
+ /** Running inside a container */
2804
+ get isApp() {
2805
+ return this.isElectron || this.isIframe || this.isPwa;
2806
+ }
2807
+ /** Running inside an electron app */
2808
+ get isElectron() {
2809
+ return !!window.electronEnvironment;
2810
+ }
2811
+ /** Headless client without access to DOM */
2812
+ get isHeadless() {
2813
+ return typeof window == "undefined";
2814
+ }
2815
+ /** Running inside an iframe */
2816
+ get isIframe() {
2817
+ return parent?.location != location;
2818
+ }
2819
+ /** Running on a mobile device */
2820
+ get isMobile() {
2821
+ return ["android", "ios"].includes(this.platform);
2822
+ }
2823
+ _isPwa;
2824
+ /** Running as a PWA */
2825
+ get isPwa() {
2826
+ if (this._isPwa == null)
2827
+ this._isPwa = matchMedia("(display-mode: standalone)").matches || navigator?.standalone || document.referrer.includes("android-app://");
2828
+ return this._isPwa;
2829
+ }
2830
+ _platform;
2831
+ /** Get the current platform type */
2832
+ get platform() {
2833
+ if (!this._platform) {
2834
+ const userAgent = navigator.userAgent || navigator.vendor;
2835
+ if (/windows/i.test(userAgent)) this._platform = "windows";
2836
+ else if (/android/i.test(userAgent)) this._platform = "android";
2837
+ else if (/iPad|iPhone|iPod/.test(userAgent)) this._platform = "ios";
2838
+ else if (/macintosh|mac os x/i.test(userAgent)) this._platform = "mac";
2839
+ else if (/linux/i.test(userAgent)) this._platform = "linux";
2840
+ else this._platform = "unknown";
2841
+ }
2842
+ return this._platform;
2843
+ }
2844
+ get storage() {
2845
+ return typeof localStorage != "undefined";
2846
+ }
2847
+ /**
2848
+ * Inject the client theme settings, meta tags & PWA prompt
2849
+ * @param opts Injection options: reload - use cached settings or reload; pwa - disabled auto pwa prompt
2850
+ * @return {Promise<void>} Resolves on completion
2851
+ */
2852
+ async inject(opts = {}) {
2853
+ opts = { install: true, reload: void 0, ...opts };
2854
+ let settings;
2855
+ if (opts.reload == null) {
2856
+ this.inject({ ...opts, reload: true });
2857
+ await this.momentum.settings.cache.loading;
2858
+ settings = this.momentum.settings.cache.entries().reduce((acc, [k, v]) => ({ ...acc, [k]: v.value }), {});
2859
+ } else settings = await this.momentum.settings.map(opts.reload);
2860
+ const meta = (key, name, value) => {
2861
+ const exists = document.querySelector(`meta[${key}="${name}"]`);
2862
+ if (value === void 0 || exists) return exists;
2863
+ const meta2 = document.createElement("meta");
2864
+ meta2.setAttribute(key, name);
2865
+ meta2.content = value;
2866
+ document.head.append(meta2);
2867
+ };
2868
+ document.querySelectorAll(".momentum-logo").forEach((el) => {
2869
+ const size = el.src ? /[?&]size="?(.+)(?:"|&|$)/.exec(el.src) : null;
2870
+ el.src = `${this.momentum.url.toString()}favicon.png${size ? `?size=${size[1]}` : ""}`;
2871
+ });
2872
+ document.querySelectorAll(`[class^="momentum-setting_"]`).forEach((el) => {
2873
+ const keys = [...el.classList].find((c) => c.startsWith("momentum-setting_"))?.match(/momentum-setting_(.*)(?:\:(.+))?/);
2874
+ if (el.tagName == "TITLE") return el.innerText = el.innerText.includes("|") ? `${el.innerText.split("|")[0].trim()} | ${settings["title"]}` : settings["title"];
2875
+ else if (keys[1] == "contact" && el.tagName == "A") el.href = `mailto:${settings.contact}?subject=${settings.title} - ${document.title.split("|")[0]}`;
2876
+ el.innerText = keys[2] ? dotNotation(settings[keys[1]], keys[2]) : settings[keys[1]];
2877
+ });
2878
+ meta("name", "description", settings["description"]);
2879
+ meta("property", "og:title", settings["title"]);
2880
+ meta("property", "og:description", settings["description"]);
2881
+ meta("property", "og:image", `${settings["public_url"]}/banner.png?size=1200x630`);
2882
+ meta("property", "og:logo", `${settings["public_url"]}/favicon.png?size=180`);
2883
+ meta("property", "og:url", settings["public_url"]);
2884
+ meta("property", "og:site_name", "Momentum");
2885
+ meta("property", "og:type", "website");
2886
+ meta("name", "twitter:card", "summary_large_image");
2887
+ if (settings.modules?.["pwa"]) {
2888
+ meta("name", "mobile-web-app-capable", "yes");
2889
+ meta("name", "apple-mobile-web-app-status-bar-style", "default");
2890
+ meta("name", "apple-mobile-web-app-title", settings["title"]);
2891
+ meta("name", "apple-touch-icon", `${settings["public_url"]}/favicon.png`);
2892
+ meta("name", "apple-touch-startup-image", `${settings["public_url"]}/favicon.png`);
2893
+ if (!document.querySelector('link[rel="manifest"]')) {
2894
+ const link = document.createElement("link");
2895
+ link.setAttribute("rel", "manifest");
2896
+ link.setAttribute("href", this.momentum.url.toString() + "manifest.json");
2897
+ document.head.append(link);
2898
+ }
2899
+ if (opts.install !== false && !this.isApp && !this.isHeadless) setTimeout(async () => {
2900
+ await this.momentum.analytics.consented;
2901
+ const dismissed = localStorage.getItem(`momentum:install`);
2902
+ if (!dismissed || dismissed != "true" && Date.now() - new Date(dismissed).getTime() > 6e4 * 60 * 24 * this.momentum.opts.installPrompt.retry)
2903
+ this.install();
2904
+ }, 6e4 * this.momentum.opts.installPrompt.delay);
2905
+ }
2906
+ meta("name", "theme-color", settings["theme"]?.background);
2907
+ if (!document.body.classList.contains("theme-manual")) {
2908
+ document.body.classList.add(settings["theme"]?.darkMode ? "theme-dark" : "theme-light");
2909
+ document.body.classList.remove(!settings["theme"]?.darkMode ? "theme-dark" : "theme-light");
2910
+ }
2911
+ const style = document.querySelector("style.momentum-theme") || document.createElement("style");
2912
+ style.classList.add("momentum-theme");
2913
+ style.innerHTML = `
2914
+ :root {
2915
+ --theme-backdrop: ${settings["theme"]?.background} !important;
2916
+ --theme-primary: ${settings["theme"]?.primary} !important;
2917
+ --theme-accent: ${settings["theme"]?.accent} !important;
2918
+ --theme-backdrop-contrast: ${contrast(settings["theme"]?.background)} !important;
2919
+ --theme-primary-contrast: ${contrast(settings["theme"]?.primary)} !important;
2920
+ --theme-accent-contrast: ${contrast(settings["theme"]?.accent)} !important;
2921
+ --theme-font: ${settings["theme"].font} !important;
2922
+ }
2923
+ `;
2924
+ if (!style.parentElement) document.head.append(style);
2925
+ this.emit(PES`client/inject:c`, this.platform);
2926
+ return settings;
2927
+ }
2928
+ async networkSpeed(format) {
2929
+ if (typeof navigator != "undefined" && !navigator.onLine) return 0;
2930
+ const speed = await this.speed;
2931
+ return speed == -1 ? -1 : !format ? speed : formatBytes(speed, 2);
2932
+ }
2933
+ /**
2934
+ * Create an installation prompt popup
2935
+ * @param platform Platform specific prompt, leave blank to auto-detect
2936
+ */
2937
+ async install(platform = this.platform) {
2938
+ const settings = await this.momentum.settings.map();
2939
+ if (!settings.pwa) throw new Error("Universal application isn't enabled");
2940
+ this.emit("install:u");
2941
+ const nativePrompt = platform == this.platform && this.nativePwaPrompt;
2942
+ const features = [
2943
+ { label: "Works Offline", icon: '<path d="M13 9h-2V7h2m0 10h-2v-6h2m-1-9A10 10 0 0 0 2 12a10 10 0 0 0 10 10 10 10 0 0 0 10-10A10 10 0 0 0 12 2"/>' },
2944
+ { label: "Fast & Responsive", icon: '<path d="M12 15.5A3.5 3.5 0 0 1 8.5 12 3.5 3.5 0 0 1 12 8.5a3.5 3.5 0 0 1 3.5 3.5 3.5 3.5 0 0 1-3.5 3.5m7.43-2.53c.04-.32.07-.64.07-.97s-.03-.66-.07-1l2.11-1.63c.19-.15.24-.42.12-.64l-2-3.46c-.12-.22-.39-.31-.61-.22l-2.49 1c-.52-.39-1.06-.73-1.69-.98l-.37-2.65A.506.506 0 0 0 14 2h-4c-.25 0-.46.18-.5.42l-.37 2.65c-.63.25-1.17.59-1.69.98l-2.49-1c-.22-.09-.49 0-.61.22l-2 3.46c-.12.22-.07.49.12.64L4.57 11c-.04.34-.07.67-.07 1s.03.65.07.97l-2.11 1.66c-.19.15-.24.42-.12.64l2 3.46c.12.22.39.3.61.22l2.49-1.01c.52.4 1.06.74 1.69.99l.37 2.65c.04.24.25.42.5.42h4c.25 0 .46-.18.5-.42l.37-2.65c.63-.25 1.17-.59 1.69-.99l2.49 1.01c.22.08.49 0 .61-.22l2-3.46c.12-.22.07-.49-.12-.64z"/>' },
2945
+ { label: "Private & Secure", icon: '<path d="M12 1 3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5zm0 6c1.4 0 2.8 1.6 2.8 3v1.5c.6 0 1.2.9 1.2 1.5v3c0 1.4-.6 2-1.2 2H9.2c-.6 0-1.2-.6-1.2-2v-3c0-.6.6-1.5 1.2-1.5V10c0-1.4 1.4-3 2.8-3m0 1.2c-.8 0-1.5.5-1.5 1.8v1.5h3V10c0-1.3-.7-1.8-1.5-1.8"/>' }
2946
+ ].map((f) => `<div class="feature"><svg viewBox="0 0 24 24">${f.icon}</svg><span>${f.label}</span></div>`).join("\n");
2947
+ const aInstructions = [
2948
+ { label: "1) Open the dropdown menu", icon: '<circle cx="12" cy="5" r="2"/><circle cx="12" cy="12" r="2"/><circle cx="12" cy="19" r="2"/>' },
2949
+ { label: '2) Press "Add to Home Screen"', icon: '<rect x="6" y="2" width="12" height="20" rx="2" ry="2"/><path fill="#fff" d="M7.5 5h9v14h-9z"/><path d="M6.5 8v8" stroke="#fff" stroke-linecap="square" stroke-width="3"/><path d="M6.5 8.5h5v5M11 9l-6 6" stroke-linecap="square" stroke-width="2" fill="transparent" stroke="#000"/>' }
2950
+ ].map((i) => `<div class="instruction"><svg width="40" viewBox="0 0 24 24">${i.icon}</svg><span style="margin-left: 0.7rem">${i.label}</span></div>`).join("\n");
2951
+ const iInstructions = [
2952
+ { label: '1) Press the "Share" button', icon: '<rect x="2" y="10" width="20" height="18" rx="4" ry="4"/><path d="M9 10h6" stroke="#fff" stroke-linecap="square" stroke-width="3"/><path d="M12 19V1M7 6l5-5 5 5" stroke-linecap="round" stroke-linejoin="round"/>' },
2953
+ { label: '2) Press "Add to Home Screen"', icon: '<rect x="2" y="2" width="20" height="20" rx="4" ry="4"/><path d="M12 7v10m-5-5h10" stroke-linecap="round"/>' }
2954
+ ].map((i) => `<div class="instruction"><svg width="35" viewBox="0 0 24 30" fill="transparent" stroke="#0B76FC" stroke-width="2">${i.icon}</svg><span style="margin-left: 1rem">${i.label}</span></div>`).join("\n");
2955
+ const downloads = [
2956
+ { label: "Windows", icon: '<svg height="30" viewBox="0 0 24 24"><path d="M3 12V6.75l6-1.32v6.48zm17-9v8.75l-10 .15V5.21zM3 13l6 .09v6.81l-6-1.15zm17 .25V22l-10-1.91V13.1z"/></svg>' },
2957
+ { label: "macOS", icon: '<svg height="30" viewBox="0 0 100 100"><path d="M86 35c-7-10-21-10-28-7s-10 2-17-1c-13-5-27 6-29 18-4 20 5 37 12 44s10 9 19 6c6-2 10-5 17-1 5 3 11 4 18-4s9-11 11-18c-19-8-14-32-3-37m-37-8c0-11 9-21 19-22 0 14-11 22-19 22"/></svg>' },
2958
+ { label: "Linux", icon: '<svg height="30" viewBox="0 0 640 640"><path d="M316.9 187.3c1 .5 1.8 1.7 3 1.7 1.1 0 2.8-.4 2.9-1.5.2-1.4-1.9-2.3-3.2-2.9-1.7-.7-3.9-1-5.5-.1-.4.2-.8.7-.6 1.1.3 1.3 2.3 1.1 3.4 1.7M295 189c1.2 0 2-1.2 3-1.7 1.1-.6 3.1-.4 3.5-1.6.2-.4-.2-.9-.6-1.1-1.6-.9-3.8-.6-5.5.1-1.3.6-3.4 1.5-3.2 2.9.1 1 1.8 1.5 2.8 1.4m221 278.8c-3.6-4-5.3-11.6-7.2-19.7-1.8-8.1-3.9-16.8-10.5-22.4-1.3-1.1-2.6-2.1-4-2.9-1.3-.8-2.7-1.5-4.1-2 9.2-27.3 5.6-54.5-3.7-79.1-11.4-30.1-31.3-56.4-46.5-74.4-17.1-21.5-33.7-41.9-33.4-72 .5-45.9 5.1-131.2-75.8-131.3-102.4-.2-76.8 103.4-77.9 135.2-1.7 23.4-6.4 41.8-22.5 64.7-18.9 22.5-45.5 58.8-58.1 96.7-6 17.9-8.8 36.1-6.2 53.3-6.5 5.8-11.4 14.7-16.6 20.2-4.2 4.3-10.3 5.9-17 8.3s-14 6-18.5 14.5c-2.1 3.9-2.8 8.1-2.8 12.4 0 3.9.6 7.9 1.2 11.8 1.2 8.1 2.5 15.7.8 20.8-5.2 14.4-5.9 24.4-2.2 31.7 3.8 7.3 11.4 10.5 20.1 12.3 17.3 3.6 40.8 2.7 59.3 12.5 19.8 10.4 39.9 14.1 55.9 10.4 11.6-2.6 21.1-9.6 25.9-20.2 12.5-.1 26.3-5.4 48.3-6.6 14.9-1.2 33.6 5.3 55.1 4.1.6 2.3 1.4 4.6 2.5 6.7v.1c8.3 16.7 23.8 24.3 40.3 23 16.6-1.3 34.1-11 48.3-27.9 13.6-16.4 36-23.2 50.9-32.2 7.4-4.5 13.4-10.1 13.9-18.3.4-8.2-4.4-17.3-15.5-29.7M319.8 151.3c9.8-22.2 34.2-21.8 44-.4 6.5 14.2 3.6 30.9-4.3 40.4-1.6-.8-5.9-2.6-12.6-4.9 1.1-1.2 3.1-2.7 3.9-4.6 4.8-11.8-.2-27-9.1-27.3-7.3-.5-13.9 10.8-11.8 23-4.1-2-9.4-3.5-13-4.4-1-6.9-.3-14.6 2.9-21.8m-40.7-11.5c10.1 0 20.8 14.2 19.1 33.5-3.5 1-7.1 2.5-10.2 4.6 1.2-8.9-3.3-20.1-9.6-19.6-8.4.7-9.8 21.2-1.8 28.1 1 .8 1.9-.2-5.9 5.5-15.6-14.6-10.5-52.1 8.4-52.1m-13.6 60.7c6.2-4.6 13.6-10 14.1-10.5 4.7-4.4 13.5-14.2 27.9-14.2 7.1 0 15.6 2.3 25.9 8.9 6.3 4.1 11.3 4.4 22.6 9.3 8.4 3.5 13.7 9.7 10.5 18.2-2.6 7.1-11 14.4-22.7 18.1-11.1 3.6-19.8 16-38.2 14.9-3.9-.2-7-1-9.6-2.1-8-3.5-12.2-10.4-20-15-8.6-4.8-13.2-10.4-14.7-15.3q-2.1-7.35 4.2-12.3m3.3 334c-2.7 35.1-43.9 34.4-75.3 18-29.9-15.8-68.6-6.5-76.5-21.9-2.4-4.7-2.4-12.7 2.6-26.4v-.2c2.4-7.6.6-16-.6-23.9-1.2-7.8-1.8-15 .9-20 3.5-6.7 8.5-9.1 14.8-11.3 10.3-3.7 11.8-3.4 19.6-9.9 5.5-5.7 9.5-12.9 14.3-18 5.1-5.5 10-8.1 17.7-6.9 8.1 1.2 15.1 6.8 21.9 16l19.6 35.6c9.5 19.9 43.1 48.4 41 68.9m-1.4-25.9c-4.1-6.6-9.6-13.6-14.4-19.6 7.1 0 14.2-2.2 16.7-8.9 2.3-6.2 0-14.9-7.4-24.9-13.5-18.2-38.3-32.5-38.3-32.5-13.5-8.4-21.1-18.7-24.6-29.9s-3-23.3-.3-35.2c5.2-22.9 18.6-45.2 27.2-59.2 2.3-1.7.8 3.2-8.7 20.8-8.5 16.1-24.4 53.3-2.6 82.4.6-20.7 5.5-41.8 13.8-61.5 12-27.4 37.3-74.9 39.3-112.7 1.1.8 4.6 3.2 6.2 4.1 4.6 2.7 8.1 6.7 12.6 10.3 12.4 10 28.5 9.2 42.4 1.2 6.2-3.5 11.2-7.5 15.9-9 9.9-3.1 17.8-8.6 22.3-15 7.7 30.4 25.7 74.3 37.2 95.7 6.1 11.4 18.3 35.5 23.6 64.6 3.3-.1 7 .4 10.9 1.4 13.8-35.7-11.7-74.2-23.3-84.9-4.7-4.6-4.9-6.6-2.6-6.5 12.6 11.2 29.2 33.7 35.2 59 2.8 11.6 3.3 23.7.4 35.7 16.4 6.8 35.9 17.9 30.7 34.8-2.2-.1-3.2 0-4.2 0 3.2-10.1-3.9-17.6-22.8-26.1-19.6-8.6-36-8.6-38.3 12.5-12.1 4.2-18.3 14.7-21.4 27.3-2.8 11.2-3.6 24.7-4.4 39.9-.5 7.7-3.6 18-6.8 29-32.1 22.9-76.7 32.9-114.3 7.2m257.4-11.5c-.9 16.8-41.2 19.9-63.2 46.5-13.2 15.7-29.4 24.4-43.6 25.5s-26.5-4.8-33.7-19.3c-4.7-11.1-2.4-23.1 1.1-36.3 3.7-14.2 9.2-28.8 9.9-40.6.8-15.2 1.7-28.5 4.2-38.7 2.6-10.3 6.6-17.2 13.7-21.1.3-.2.7-.3 1-.5.8 13.2 7.3 26.6 18.8 29.5 12.6 3.3 30.7-7.5 38.4-16.3 9-.3 15.7-.9 22.6 5.1 9.9 8.5 7.1 30.3 17.1 41.6 10.6 11.6 14 19.5 13.7 24.6M269.4 212.7c2 1.9 4.7 4.5 8 7.1 6.6 5.2 15.8 10.6 27.3 10.6 11.6 0 22.5-5.9 31.8-10.8 4.9-2.6 10.9-7 14.8-10.4s5.9-6.3 3.1-6.6-2.6 2.6-6 5.1c-4.4 3.2-9.7 7.4-13.9 9.8-7.4 4.2-19.5 10.2-29.9 10.2s-18.7-4.8-24.9-9.7c-3.1-2.5-5.7-5-7.7-6.9-1.5-1.4-1.9-4.6-4.3-4.9-1.4-.1-1.8 3.7 1.7 6.5"/></svg>' }
2959
+ ].map((d) => `<button class="${d.label.toLowerCase()} download-btn btn">${d.icon}<div>${d.label}</div></button>`).join("\n");
2960
+ const { close, dialog, result } = createDialog(`
2961
+ <div style="margin: 1.5rem 0; text-align: center"><img src="${this.momentum.url}favicon.png?size=75" alt="logo" /><h2 style="margin: 0.5rem 0">Install ${settings.title}</h2><p style="margin: 0; color: #4a5568">Get the full experience with our app!</p></div>
2962
+ <div style="margin: 2rem 1rem">
2963
+ ${nativePrompt || platform != "android" && platform != "ios" ? `<div class="features">${features}</div>` : ""}
2964
+ ${!nativePrompt && platform == "android" ? `<div class="android">${aInstructions}</div>` : ""}
2965
+ ${!nativePrompt && platform == "ios" ? `<div class="ios">${iInstructions}</div>` : ""}
2966
+ </div>
2967
+ <div style="margin-top: 1.5rem; display: flex; gap: 10px">
2968
+ ${nativePrompt ? `<button class="install btn btn-primary">Install</button>` : ""}
2969
+ ${!nativePrompt && platform != "android" && platform != "ios" ? `<button class="download btn btn-primary">Download</button>` : ""}
2970
+ <button class="dismiss btn btn-secondary">Not Now</button>
2971
+ </div>
2972
+ <button class="expand-toggle">Download Options ▼</button>
2973
+ <div class="downloads expand-area hide">${downloads}</div>
2974
+ `, { css: `
2975
+ .downloads {
2976
+ display: flex;
2977
+ height: 95px;
2978
+ margin-top: 0.75rem;
2979
+ gap: 10px;
2980
+ }
2981
+
2982
+ .download-btn {
2983
+ display: flex;
2984
+ flex-direction: column;
2985
+ align-items: center;
2986
+ justify-content: space-between;
2987
+ height: 85px
2988
+ }
2989
+
2990
+ .feature, .instruction {
2991
+ display: flex;
2992
+ align-items: center;
2993
+ font-size: 14px;
2994
+ margin-bottom: 0.5rem
2995
+ }
2996
+
2997
+ .instruction {
2998
+ margin-bottom: 1rem
2999
+ }
3000
+
3001
+ .feature svg {
3002
+ width: 20px;
3003
+ height: 20px;
3004
+ margin-right: 12px;
3005
+ fill: var(--theme-primary)
3006
+ }` });
3007
+ let downloaded = false;
3008
+ const download = (platform2) => {
3009
+ downloaded = true;
3010
+ open(`${this.momentum.url}api/download/${platform2}?analytics=${this.momentum.analytics.id}`, "_blank");
3011
+ };
3012
+ dialog.addEventListener("click", (e) => {
3013
+ let t = e.target, depth = 0;
3014
+ while (!!t && t.tagName != "BUTTON" && ++depth < 5) t = t.parentElement;
3015
+ if (!t || t.tagName != "BUTTON") return;
3016
+ else if (t.matches(".dismiss")) close("dismiss");
3017
+ else if (t.matches(".expand-toggle")) dialog.querySelector(".downloads").classList.toggle("hide");
3018
+ else if (t.matches(".install")) {
3019
+ this.nativePwaPrompt.prompt();
3020
+ close(true);
3021
+ } else if (t.matches(".download")) {
3022
+ download(platform);
3023
+ close(true);
3024
+ } else if (t.matches(".windows")) download("windows");
3025
+ else if (t.matches(".macos")) download("mac");
3026
+ else if (t.matches(".linux")) download("linux");
3027
+ });
3028
+ return result.then((resp) => {
3029
+ const success = downloaded || resp && resp != "dismiss";
3030
+ if (resp == "dismiss") localStorage.setItem("momentum:install", (/* @__PURE__ */ new Date()).toISOString());
3031
+ else if (resp) localStorage.setItem("momentum:install", "true");
3032
+ else localStorage.removeItem("momentum:install");
3033
+ this.emit(`install:${success ? "c" : "d"}`);
3034
+ return success;
3035
+ });
3036
+ }
3037
+ /** Update service worker & other tabs */
3038
+ update() {
3039
+ if (this.updating) return;
3040
+ this.updating = true;
3041
+ if (navigator.serviceWorker.controller) {
3042
+ navigator.serviceWorker.addEventListener("message", (event) => {
3043
+ if (event.data?.update) location.reload();
3044
+ });
3045
+ navigator.serviceWorker.controller.postMessage({ update: true });
3046
+ }
3047
+ }
3048
+ }
3049
+ class Data extends PathEventEmitter {
3050
+ constructor(momentum) {
3051
+ super();
3052
+ this.momentum = momentum;
3053
+ }
3054
+ subscribers = {};
3055
+ /**
3056
+ * Create new document in collection
3057
+ * @param {string} collection Target collection
3058
+ * @param {Document<T>} document New document
3059
+ * @return {Promise<Document<T>>} New saved document
3060
+ */
3061
+ create(collection, document2) {
3062
+ if (!collection || !document2) throw new Error("Cannot create document, missing collection or document");
3063
+ return this.momentum.api.request({
3064
+ url: `api/` + PES`data/${collection}`,
3065
+ method: "POST",
3066
+ body: document2
3067
+ }).then((resp) => {
3068
+ this.emit(PES`data/${collection}:c`, resp);
3069
+ return resp;
3070
+ });
3071
+ }
3072
+ /**
3073
+ * Delete document from collection
3074
+ * @param {string} collection Target collection
3075
+ * @param {number} id Document ID
3076
+ * @return {Promise<void>} Returns once complete
3077
+ */
3078
+ async delete(collection, id) {
3079
+ if (!collection || !id) throw new Error("Cannot delete document, missing collection or ID");
3080
+ const count = await this.momentum.api.request({
3081
+ url: `api/` + PES`data/${collection}/${id}`,
3082
+ method: "DELETE"
3083
+ });
3084
+ if (count) this.emit(PES`data/${collection}/${id}:d`, id);
3085
+ return count;
3086
+ }
3087
+ read(collection, id) {
3088
+ if (!collection) throw new Error("Cannot read documents, missing collection");
3089
+ return this.momentum.api.request({ url: `api/` + PES`data/${collection}/${id}` }).then((resp) => {
3090
+ this.emit(PES`data/${collection}/${id}:r`, collection, resp);
3091
+ return resp;
3092
+ });
3093
+ }
3094
+ /**
3095
+ * Update document in collection
3096
+ * @param {string} collection Target collection
3097
+ * @param {Document<T>} document Document to update
3098
+ * @param {boolean} append Append or replace existing document
3099
+ * @return {Promise<Document<T>>} New saved document
3100
+ */
3101
+ update(collection, document2, append) {
3102
+ if (!collection || !document2) throw new Error("Cannot update document, missing collection or document");
3103
+ return this.momentum.api.request({
3104
+ url: `api/` + PES`data/${collection}/${document2._id}`,
3105
+ method: append ? "PATCH" : "PUT",
3106
+ body: document2
3107
+ }).then((resp) => {
3108
+ this.emit(PES`data/${collection}/${document2._id}:u`, resp);
3109
+ return resp;
3110
+ });
3111
+ }
3112
+ /**
3113
+ * Create raw MongoDB query
3114
+ * @param {string} collection Target collection name
3115
+ * @param {RawQuery} query Raw query
3116
+ * @return {Promise<any>} Query response
3117
+ */
3118
+ raw(collection, query) {
3119
+ if (!collection || !query) throw new Error("Cannot execute raw query, missing collection or query");
3120
+ const mode = query.operand.startsWith("find") ? "r" : query.operand == "insertOne" ? "c" : query.operand.startsWith("delete") ? "d" : "u";
3121
+ return this.momentum.api.request({ url: `api/` + PES`data/${collection}` + "?raw", body: query }).then((resp) => {
3122
+ this.emit(PES`data/${collection}:${mode}`, resp);
3123
+ return resp;
3124
+ });
3125
+ }
3126
+ /**
3127
+ * Subscribe to live updates with callback
3128
+ * @param path Path to data
3129
+ * @param {(value: T[]) => any | null} callback Received changes
3130
+ * @param opts Reload data immediately
3131
+ * @return {() => void} Function to unsubscribe
3132
+ */
3133
+ sync(path, callback, opts = {}) {
3134
+ const e = PES`data/${path}`;
3135
+ const cache = new Cache("_id");
3136
+ const unsubscribe = this.on(e, (event, payload) => {
3137
+ if (event.read && Array.isArray(payload)) cache.addAll(payload);
3138
+ else if (event.create || event.update || event.read) cache.add(payload);
3139
+ else if (event.delete) cache.delete(+event.name);
3140
+ callback(cache.all());
3141
+ });
3142
+ if (opts.reload == void 0 || opts.reload) this.read(path);
3143
+ if (!this.subscribers.length) this.momentum.socket?.subscribe(e);
3144
+ const key = Object.keys(this.subscribers).length.toString();
3145
+ this.subscribers[key] = () => {
3146
+ unsubscribe();
3147
+ delete this.subscribers[key];
3148
+ if (!this.subscribers.length) this.momentum.socket?.unsubscribe(e);
3149
+ };
3150
+ return this.subscribers[key];
3151
+ }
3152
+ }
3153
+ class Discounts extends AssetController {
3154
+ constructor(momentum) {
3155
+ super(momentum, { module: "discounts", key: "_id" });
3156
+ this.momentum = momentum;
3157
+ }
3158
+ }
3159
+ class Email extends PathEventEmitter {
3160
+ constructor(momentum) {
3161
+ super();
3162
+ this.momentum = momentum;
3163
+ }
3164
+ /**
3165
+ * Send an Email
3166
+ * @param {Mail} email Email information
3167
+ * @return {Promise<any>} SMTP Response
3168
+ */
3169
+ create(email) {
3170
+ return this.momentum.api.request({ url: "api/email", body: email }).then((response) => {
3171
+ this.emit(PES`email/${email.to || email.bcc?.[0]}:c`, email);
3172
+ return response;
3173
+ });
3174
+ }
3175
+ }
3176
+ class Forms extends TreeAssetController {
3177
+ constructor(momentum) {
3178
+ super(momentum, { module: "forms", key: "path" });
3179
+ this.momentum = momentum;
3180
+ }
3181
+ }
3182
+ class Groups extends AssetController {
3183
+ constructor(momentum) {
3184
+ super(momentum, { module: "groups", key: "_id" });
3185
+ this.momentum = momentum;
3186
+ }
3187
+ }
3188
+ class Logger extends PathEventEmitter {
3189
+ constructor(momentum) {
3190
+ super("logs");
3191
+ this.momentum = momentum;
3192
+ this.channel = this.momentum.opts.app;
3193
+ this.logLevel = this.momentum.opts.logLevel;
3194
+ if (["local", "server"].includes(this.channel.toLowerCase())) throw new Error(`${this.channel} is reserved`);
3195
+ if (typeof window != "undefined") {
3196
+ window.addEventListener("unhandledrejection", async (event) => {
3197
+ let log = event.reason?.stack || event.reason;
3198
+ if (event.reason?.url) log = `${event.reason.method} ${event.reason.url} -> ${event.reason.code}
3199
+
3200
+ ${log}`;
3201
+ if (event.reason?.code == null || event.reason?.code >= 500) {
3202
+ if (LOG_LEVEL[this.logLevel] >= 0) this.error(log);
3203
+ else {
3204
+ this.logs.push(this.buildLog(LOG_LEVEL.ERROR, log));
3205
+ this.emit(`local/error:c`, this.logs);
3206
+ }
3207
+ } else {
3208
+ if (LOG_LEVEL[this.logLevel] >= 1) this.warn(log);
3209
+ else {
3210
+ this.logs.push(this.buildLog(LOG_LEVEL.WARN, log));
3211
+ this.emit(`local/warn:c`, this.logs);
3212
+ }
3213
+ }
3214
+ });
3215
+ }
3216
+ console.error = (...args) => {
3217
+ this.console.error(...args);
3218
+ if (LOG_LEVEL[this.logLevel] >= 0) this.error(args);
3219
+ else {
3220
+ this.logs.push(this.buildLog(LOG_LEVEL.ERROR, args));
3221
+ this.emit(`local/error:c`, this.logs);
3222
+ }
3223
+ };
3224
+ console.warn = (...args) => {
3225
+ this.console.warn(...args);
3226
+ if (LOG_LEVEL[this.logLevel] >= 1) this.warn(args);
3227
+ else {
3228
+ this.logs.push(this.buildLog(LOG_LEVEL.WARN, args));
3229
+ this.emit(`local/warn:c`, this.logs);
3230
+ }
3231
+ };
3232
+ console.info = (...args) => {
3233
+ this.console.info(...args);
3234
+ if (LOG_LEVEL[this.logLevel] >= 2) this.info(args);
3235
+ else {
3236
+ this.logs.push(this.buildLog(LOG_LEVEL.INFO, args));
3237
+ this.emit(`local/info:c`, this.logs);
3238
+ }
3239
+ };
3240
+ console.log = (...args) => {
3241
+ this.console.log(...args);
3242
+ if (LOG_LEVEL[this.logLevel] >= 3) this.log(args);
3243
+ else {
3244
+ this.logs.push(this.buildLog(LOG_LEVEL.LOG, args));
3245
+ this.emit(`local/log:c`, this.logs);
3246
+ }
3247
+ };
3248
+ console.debug = (...args) => {
3249
+ this.console.debug(...args);
3250
+ if (LOG_LEVEL[this.logLevel] >= 4) this.debug(args);
3251
+ else {
3252
+ this.logs.push(this.buildLog(LOG_LEVEL.DEBUG, args));
3253
+ this.emit(`local/debug:c`, this.logs);
3254
+ }
3255
+ };
3256
+ }
3257
+ static cache = {};
3258
+ channel;
3259
+ logLevel;
3260
+ console = {
3261
+ debug: console.debug,
3262
+ log: console.log,
3263
+ info: console.info,
3264
+ warn: console.warn,
3265
+ error: console.error
3266
+ };
3267
+ logs = [];
3268
+ buildLog(level, log) {
3269
+ return {
3270
+ time: /* @__PURE__ */ new Date(),
3271
+ level,
3272
+ log: makeArray(log),
3273
+ ctx: this.momentum.analytics.id
3274
+ };
3275
+ }
3276
+ create(log, channel = this.channel) {
3277
+ if (channel.toLowerCase() == "server") throw new Error('"Server" namespace is reserved');
3278
+ return this.momentum.api.request({ url: `api/logs/${channel}`, body: log }).catch(() => {
3279
+ });
3280
+ }
3281
+ /**
3282
+ * Get available channels
3283
+ * @return {Promise<string[]>} List of channel names
3284
+ */
3285
+ channels() {
3286
+ return this.momentum.api.request({ url: "api/logs/channels" });
3287
+ }
3288
+ /**
3289
+ * Clear logs in channel
3290
+ * @param {string} channel Channel to clear
3291
+ * @return {Promise<number>} Reruns once complete
3292
+ */
3293
+ delete(channel) {
3294
+ if (channel.toLowerCase() == "local") {
3295
+ const length = this.logs.length;
3296
+ this.logs = [];
3297
+ this.emit(`local:d`, this.logs);
3298
+ return Promise.resolve(length);
3299
+ }
3300
+ return this.momentum.api.request({ url: `api/logs/${channel}`, method: "DELETE" });
3301
+ }
3302
+ /**
3303
+ * Read logs from channel
3304
+ * @param {string} channel Channel name
3305
+ * @return {Promise<Log[]>} Logs in channel
3306
+ */
3307
+ read(channel) {
3308
+ if (channel.toLowerCase() == "local") return Promise.resolve(this.logs);
3309
+ return this.momentum.api.request({ url: `api/logs/${channel}` });
3310
+ }
3311
+ sync(channel, callback, opts = {}) {
3312
+ if (!Logger.cache[channel]) Logger.cache[channel] = { subs: 1, logs: [] };
3313
+ else Logger.cache[channel].subs++;
3314
+ if (callback) callback(PE`logs/${channel}:r`, Logger.cache[channel].logs);
3315
+ if (opts.reload !== false) this.read(channel).then((resp) => {
3316
+ Logger.cache[channel].logs = resp;
3317
+ if (callback) callback(PE`logs/${channel}:r`, resp);
3318
+ });
3319
+ const unsubscribe = callback ? this.on(channel, (event, payload) => {
3320
+ if (event.delete) Logger.cache[channel].logs = [];
3321
+ else if (event.create || event.update) Logger.cache[channel].logs.push(payload);
3322
+ else if (event.read && Array.isArray(payload)) Logger.cache[channel].logs = payload;
3323
+ callback(event, Logger.cache[channel].logs);
3324
+ }) : null;
3325
+ if (Logger.cache[channel].subs) this.momentum.socket?.subscribe(`logs/${channel}`);
3326
+ Logger.cache[channel].unsubscribe = () => {
3327
+ if (unsubscribe) unsubscribe();
3328
+ Logger.cache[channel].subs--;
3329
+ if (!Logger.cache[channel].subs) {
3330
+ this.momentum.socket?.unsubscribe(`logs/${channel}`);
3331
+ delete Logger.cache[channel];
3332
+ }
3333
+ };
3334
+ return Logger.cache[channel].unsubscribe;
3335
+ }
3336
+ // Console =========================================================================================================
3337
+ /**
3338
+ * Create debug log
3339
+ * @param log What you want to log
3340
+ * @param {string} channel Channel to publish log to
3341
+ * @return {Promise<void>} Log saved
3342
+ */
3343
+ debug(log, channel = this.channel) {
3344
+ if (typeof log == "string" || Array.isArray(log)) log = this.buildLog(LOG_LEVEL.DEBUG, log);
3345
+ this.logs.push(log);
3346
+ this.emit(`local/debug:c`, this.logs);
3347
+ this.create(log, channel);
3348
+ }
3349
+ /**
3350
+ * Create regular log
3351
+ * @param log What you want to log
3352
+ * @param {string} channel Channel to publish log to
3353
+ * @return {Promise<void>} Log saved
3354
+ */
3355
+ log(log, channel = this.channel) {
3356
+ if (typeof log == "string" || Array.isArray(log)) log = this.buildLog(LOG_LEVEL.LOG, log);
3357
+ this.logs.push(log);
3358
+ this.emit(`local/log:c`, this.logs);
3359
+ this.create(log, channel);
3360
+ }
3361
+ /**
3362
+ * Create info log
3363
+ * @param log What you want to log
3364
+ * @param {string} channel Channel to publish log to
3365
+ * @return {Promise<void>} Log saved
3366
+ */
3367
+ info(log, channel = this.channel) {
3368
+ if (typeof log == "string" || Array.isArray(log)) log = this.buildLog(LOG_LEVEL.INFO, log);
3369
+ this.logs.push(log);
3370
+ this.emit(`local/info:c`, this.logs);
3371
+ this.create(log, channel);
3372
+ }
3373
+ /**
3374
+ * Create warning log
3375
+ * @param log What you want to log
3376
+ * @param {string} channel Channel to publish log to
3377
+ * @return {Promise<void>} Log saved
3378
+ */
3379
+ warn(log, channel = this.channel) {
3380
+ if (typeof log == "string" || Array.isArray(log)) log = this.buildLog(LOG_LEVEL.WARN, log);
3381
+ this.logs.push(log);
3382
+ this.emit(`local/warn:c`, this.logs);
3383
+ this.create(log, channel);
3384
+ }
3385
+ /**
3386
+ * Create error log
3387
+ * @param log What you want to log
3388
+ * @param {string} channel Channel to publish log to
3389
+ * @return {Promise<void>} Log saved
3390
+ */
3391
+ error(log, channel = this.channel) {
3392
+ if (typeof log == "string" || Array.isArray(log)) log = this.buildLog(LOG_LEVEL.ERROR, log);
3393
+ this.logs.push(log);
3394
+ this.emit(`local/error:c`, this.logs);
3395
+ this.create(log, channel);
3396
+ }
3397
+ }
3398
+ class Notifications extends PathEventEmitter {
3399
+ constructor(momentum) {
3400
+ super();
3401
+ this.momentum = momentum;
3402
+ this.subscription.then((resp) => this.enabled = !!resp);
3403
+ }
3404
+ _enabled = false;
3405
+ /** Are notifications enabled */
3406
+ get enabled() {
3407
+ return this._enabled;
3408
+ }
3409
+ set enabled(enabled) {
3410
+ this._enabled = enabled;
3411
+ this.emit(PES`notifications:${enabled ? "u" : "d"}`, enabled);
3412
+ }
3413
+ /** Get Push Subscription info */
3414
+ get subscription() {
3415
+ if (!navigator.serviceWorker?.controller) return Promise.resolve(null);
3416
+ return navigator.serviceWorker.ready.then((sw) => sw?.pushManager?.getSubscription());
3417
+ }
3418
+ /**
3419
+ * Send a notification
3420
+ * @param {Notification} notification Notification that will be sent to user
3421
+ * @return {Promise<void>} Returns once complete
3422
+ */
3423
+ create(notification) {
3424
+ return this.momentum.api.request({ url: "api/notifications", body: notification }).then((response) => {
3425
+ this.emit(PES`notifications/${notification.to}:c`, notification);
3426
+ return response;
3427
+ });
3428
+ }
3429
+ /**
3430
+ * Disable device notifications
3431
+ * @return {Promise<void>} Resolves on success
3432
+ */
3433
+ async disable() {
3434
+ const subscription = await this.subscription;
3435
+ subscription?.unsubscribe();
3436
+ return this.momentum.api.request({ url: "api/notifications", method: "DELETE", body: {
3437
+ p256dh: subscription?.toJSON().keys?.["p256dh"]
3438
+ } }).then(() => this.enabled = false);
3439
+ }
3440
+ /**
3441
+ * Enable device notifications
3442
+ * @return {Promise<null>} Resolves on success
3443
+ */
3444
+ async enable() {
3445
+ const granted = await Notification.requestPermission();
3446
+ if (!granted) return null;
3447
+ const sw = await navigator.serviceWorker.ready;
3448
+ const token = (await this.momentum.settings.read("push_public_key"))?.value;
3449
+ const subscription = (await sw.pushManager.subscribe({
3450
+ userVisibleOnly: true,
3451
+ applicationServerKey: token
3452
+ })).toJSON();
3453
+ return this.momentum.api.request({ url: "api/notifications", method: "PATCH", body: subscription }).then(() => this.enabled = true);
3454
+ }
3455
+ }
3456
+ class Blacklist extends AssetController {
3457
+ constructor(momentum) {
3458
+ super(momentum, { module: "blacklist", path: "routes/blacklist", key: "_id" });
3459
+ this.momentum = momentum;
3460
+ }
3461
+ async update(ignore) {
3462
+ throw new Error("Not supported");
3463
+ }
3464
+ }
3465
+ class RateLimiter extends AssetController {
3466
+ constructor(momentum) {
3467
+ super(momentum, { module: "rate_limit", path: "routes/limit", key: "_id" });
3468
+ this.momentum = momentum;
3469
+ }
3470
+ }
3471
+ class Routes extends PathEventEmitter {
3472
+ constructor(momentum) {
3473
+ super();
3474
+ this.momentum = momentum;
3475
+ this.blacklist = new Blacklist(momentum);
3476
+ this.rateLimiter = new RateLimiter(momentum);
3477
+ this.relayEvents(this.blacklist);
3478
+ this.relayEvents(this.rateLimiter);
3479
+ }
3480
+ blacklist;
3481
+ rateLimiter;
3482
+ }
3483
+ class Templates extends AssetController {
3484
+ constructor(momentum) {
3485
+ super(momentum, { module: "templates", key: "_id" });
3486
+ this.momentum = momentum;
3487
+ }
3488
+ render(id, data) {
3489
+ return this.momentum.api.request({ url: `/api/templates/render/${id}`, method: "POST", body: data });
3490
+ }
3491
+ renderHtml(html, data) {
3492
+ return this.momentum.api.request({ url: `/api/templates/render`, method: "POST", body: { html, data } });
3493
+ }
3494
+ }
3495
+ class Transactions extends AssetController {
3496
+ constructor(momentum) {
3497
+ super(momentum, { module: "transactions", key: "_id" });
3498
+ this.momentum = momentum;
3499
+ }
3500
+ /** Stripe object */
3501
+ stripe;
3502
+ /**
3503
+ * Initialize stripe API
3504
+ * @return {Promise<any>} Returns stripe API
3505
+ * @private
3506
+ */
3507
+ async initStripe() {
3508
+ await new Promise((res) => {
3509
+ if (this.stripe) return res(window["Stripe"]);
3510
+ const stripeScript = document.createElement("script");
3511
+ stripeScript.src = "https://js.stripe.com/v3/";
3512
+ stripeScript.setAttribute("crossorigin", "anonymous");
3513
+ stripeScript.onload = () => res();
3514
+ document.head.appendChild(stripeScript);
3515
+ }).then(() => this.stripe = window["Stripe"]);
3516
+ const token = await this.momentum.settings.read("stripe_token");
3517
+ return this.stripe(token.value);
3518
+ }
3519
+ /**
3520
+ * Create a stripe client secret to complete Payment
3521
+ * @param {string} id Payment ID
3522
+ * @return {Promise<string>} Client secret
3523
+ * @private
3524
+ */
3525
+ getToken(id) {
3526
+ return this.momentum.api.request({ url: `api/transactions/init/${id || ""}`, method: "POST" }).then((resp) => resp.token);
3527
+ }
3528
+ /**
3529
+ * Process transaction programmatically
3530
+ * @param {string} id Transaction ID
3531
+ * @param {Card} card Credit card information
3532
+ * @return {Promise<any>} Stripe confirmation
3533
+ */
3534
+ async checkout(id, card) {
3535
+ const [client, secret] = await Promise.all([this.initStripe(), this.getToken(id)]);
3536
+ return client.confirmPayment({
3537
+ clientSecret: secret,
3538
+ confirmParams: {
3539
+ payment_method: {
3540
+ card: { ...card, details: void 0 },
3541
+ billing_details: card.details || {}
3542
+ }
3543
+ }
3544
+ });
3545
+ }
3546
+ /**
3547
+ * Process transaction using Stripe form
3548
+ * @param {string} id Transaction ID
3549
+ * @param {string} element DOM element to inject form into
3550
+ * @return {Promise<() => Promise<any>>} Callback to submit form
3551
+ */
3552
+ async checkoutForm(id, element) {
3553
+ const [client, secret] = await Promise.all([this.initStripe(), this.getToken(id)]);
3554
+ const form = client.elements({ clientSecret: secret });
3555
+ form.create("payment").mount(element);
3556
+ return (redirect = location.href) => client.confirmPayment({
3557
+ elements: form,
3558
+ redirect: "if_required",
3559
+ confirmParams: { return_url: redirect + "?payment_status=success" }
3560
+ });
3561
+ }
3562
+ /**
3563
+ * Complete transaction via Momentum's checkout page
3564
+ * @param {string} id Transaction ID
3565
+ * @param {string} host Host origin attempting to login
3566
+ * @return {Promise<string>} Translation ID on success
3567
+ */
3568
+ checkoutRedirect(id, host = location.origin) {
3569
+ return new Promise(async (res, rej) => {
3570
+ let origin2 = new URL(this.momentum.opts.checkoutUrl).origin, listener, win;
3571
+ window.addEventListener("message", listener = (event) => {
3572
+ const data = event?.data || {};
3573
+ if (event.origin != origin2 || data.sender != origin2) return;
3574
+ if (!data.response) return rej("Unknown response from payment page");
3575
+ window.removeEventListener("message", listener);
3576
+ win.close();
3577
+ res(data.response);
3578
+ });
3579
+ win = window.open(this.checkoutUrl(id) + `&host=${host}`, "_blank");
3580
+ if (!win) {
3581
+ window.removeEventListener("message", listener);
3582
+ return rej("Unable to open checkout page");
3583
+ }
3584
+ });
3585
+ }
3586
+ /**
3587
+ * Get URL to checkout page for transaction
3588
+ * @param {string} id Transaction ID
3589
+ * @returns {string} URL
3590
+ */
3591
+ checkoutUrl(id) {
3592
+ return encodeURI(`${this.momentum.opts.checkoutUrl}?token=${id}`);
3593
+ }
3594
+ async delete(key) {
3595
+ throw new Error("Not supported");
3596
+ }
3597
+ /**
3598
+ * Send recipient transaction receipt or invoice
3599
+ * @param {string} id Translation ID
3600
+ * @return {Promise<void>} Returns once complete
3601
+ */
3602
+ notify(id) {
3603
+ return this.momentum.api.request({ url: `api/transactions/notify/${id || ""}`, method: "POST" });
3604
+ }
3605
+ /**
3606
+ * Refund a transaction
3607
+ * @param {string} key Asset primary key
3608
+ * @return {Promise<Transaction>} Returns on success
3609
+ */
3610
+ async refund(key) {
3611
+ const transaction = await this.momentum.api.request({ url: `api/${this.opts.module}/${key || ""}`, method: "DELETE" });
3612
+ this.emit(`${key}:u`, transaction);
3613
+ return transaction;
3614
+ }
3615
+ }
3616
+ class Pdf extends PathEventEmitter {
3617
+ constructor(momentum) {
3618
+ super();
3619
+ this.momentum = momentum;
3620
+ }
3621
+ createPdf(body, options) {
3622
+ return this.momentum.api.request({ url: `api/` + PES`pdf`, body: { ...body, options } }).then(async (resp) => {
3623
+ if (options?.downloadAs) {
3624
+ let filename = options?.downloadAs || timestampFilename();
3625
+ if (!filename.endsWith(".pdf")) filename += ".pdf";
3626
+ downloadFile(resp, filename);
3627
+ }
3628
+ this.emit(PES`pdf:c`, resp);
3629
+ return resp;
3630
+ });
3631
+ }
3632
+ /**
3633
+ * Create a PDF from raw HTML code
3634
+ * @param {string} html HTML code
3635
+ * @param {PdfOptions} options PDF rendering options
3636
+ * @return {Promise<Blob>}
3637
+ */
3638
+ fromHtml(html, options = {}) {
3639
+ if (!html) throw new Error("Cannot create PDF, missing HTML");
3640
+ return this.createPdf({ html }, options);
3641
+ }
3642
+ /**
3643
+ * Create a PDF from a server template
3644
+ * @param {RenderTemplate} template Template path
3645
+ * @param {PdfOptions} options PDF rendering options
3646
+ * @return {Promise<Blob>}
3647
+ */
3648
+ fromTemplate(template, options = {}) {
3649
+ if (!template) throw new Error("Cannot create PDF, missing template");
3650
+ return this.createPdf(template, options);
3651
+ }
3652
+ /**
3653
+ * Create PDF from a public URL
3654
+ * @param {string} url HTTP(s) URL
3655
+ * @param {PdfOptions} options PDF rendering option
3656
+ * @return {Promise<Blob>}
3657
+ */
3658
+ fromUrl(url, options = {}) {
3659
+ if (!url) throw new Error("Cannot create PDF, missing URL");
3660
+ return this.createPdf({ url }, options);
3661
+ }
3662
+ }
3663
+ class Products extends PathEventEmitter {
3664
+ constructor(momentum) {
3665
+ super();
3666
+ this.momentum = momentum;
3667
+ this.controller = new AssetController(momentum, { module: "products", key: "_id" });
3668
+ this.relayEvents(this.controller);
3669
+ }
3670
+ controller;
3671
+ get cache() {
3672
+ return this.controller.cache;
3673
+ }
3674
+ /**
3675
+ * Fetch all products
3676
+ * @param {boolean} reload Force reload instead of using cache
3677
+ * @return {Promise<Product[]>} List of products
3678
+ */
3679
+ all(reload) {
3680
+ return this.controller.all(reload);
3681
+ }
3682
+ /**
3683
+ * Fetch specific product information
3684
+ * @param {number} id Product ID
3685
+ * @param {boolean} reload Force reload instead of using cache
3686
+ * @return {Promise<Product>} Found product
3687
+ */
3688
+ read(id, reload) {
3689
+ return this.controller.read(id.toString(), reload);
3690
+ }
3691
+ /**
3692
+ * Subscribe to live updates with callback
3693
+ * @param {(value: Product[]) => any | null} callback Received changes
3694
+ * @param opts Scope to single product
3695
+ * @return {() => void} Function to unsubscribe
3696
+ */
3697
+ sync(callback, opts = {}) {
3698
+ return this.controller.sync(callback, { path: opts.product ? opts.product.toString() : void 0 });
3699
+ }
3700
+ }
3701
+ class Schemas extends TreeAssetController {
3702
+ constructor(momentum) {
3703
+ super(momentum, { module: "schemas", key: "path" });
3704
+ this.momentum = momentum;
3705
+ }
3706
+ }
3707
+ class Sms extends PathEventEmitter {
3708
+ constructor(momentum) {
3709
+ super();
3710
+ this.momentum = momentum;
3711
+ }
3712
+ /**
3713
+ * Send an sms to a number
3714
+ * @param {Message} message Text that will be sent
3715
+ * @return {Promise<any>} Twilio message instance
3716
+ */
3717
+ create(message) {
3718
+ return this.momentum.api.request({ url: "api/sms", body: message }).then((resp) => this.emit(PES`sms/${message.to}`, message));
3719
+ }
3720
+ }
3721
+ class Socket extends PathEventEmitter {
3722
+ constructor(momentum) {
3723
+ super();
3724
+ this.momentum = momentum;
3725
+ this.momentum.auth.on("user", (event, user) => {
3726
+ if (user !== void 0 && this.momentum.api.token != this.token) this.connect();
3727
+ });
3728
+ }
3729
+ static pollingSpeed = 15e3;
3730
+ connection;
3731
+ connecting;
3732
+ connectingResolver;
3733
+ events = {};
3734
+ reconnect = true;
3735
+ pending = [];
3736
+ token;
3737
+ reconnectTimeout;
3738
+ connected = false;
3739
+ scheduleReconnect() {
3740
+ if (!this.reconnect) return;
3741
+ clearTimeout(this.reconnectTimeout);
3742
+ this.reconnectTimeout = setTimeout(() => {
3743
+ this.connect();
3744
+ }, Socket.pollingSpeed);
3745
+ }
3746
+ close(reconnect) {
3747
+ console.debug("Disconnected from Momentum");
3748
+ this.connected = false;
3749
+ this.connection?.close();
3750
+ this.connection = void 0;
3751
+ this.reconnect = !!reconnect;
3752
+ if (this.reconnect) {
3753
+ this.scheduleReconnect();
3754
+ } else {
3755
+ this.events = {};
3756
+ clearTimeout(this.reconnectTimeout);
3757
+ }
3758
+ }
3759
+ connect() {
3760
+ if (!this.connecting) {
3761
+ this.connecting = new Promise((res) => this.connectingResolver = res);
3762
+ }
3763
+ if (typeof navigator != "undefined" && !navigator.onLine || this.connected && this.token == this.momentum.api.token) return this.connecting;
3764
+ if (this.connected) this.close();
3765
+ clearTimeout(this.reconnectTimeout);
3766
+ if (navigator.onLine) {
3767
+ const url = this.momentum.url.toString().replace("http", "ws");
3768
+ this.connection = new WebSocket(url + (this.momentum.api.token ? `?token=${this.momentum.api.token}&url=${location?.href || ""}` : ""));
3769
+ this.connection.onopen = () => {
3770
+ console.debug("Connected to Momentum");
3771
+ this.connected = true;
3772
+ this.token = this.momentum.api.token;
3773
+ clearTimeout(this.reconnectTimeout);
3774
+ Object.keys(this.events).forEach((e) => this.send("subscribe", e));
3775
+ this.pending = this.pending.filter((p) => {
3776
+ this.send(p.event, p.payload);
3777
+ return false;
3778
+ });
3779
+ this.connectingResolver?.();
3780
+ this.connecting = void 0;
3781
+ this.connectingResolver = void 0;
3782
+ };
3783
+ this.connection.onclose = () => {
3784
+ if (!this.reconnect) return;
3785
+ this.close(true);
3786
+ };
3787
+ this.connection.onmessage = (message) => {
3788
+ const data = JSONAttemptParse(message.data);
3789
+ if (data.connected === false) this.close(true);
3790
+ else this.emit(data.event, data.payload);
3791
+ };
3792
+ this.scheduleReconnect();
3793
+ }
3794
+ return this.connecting;
3795
+ }
3796
+ async ping() {
3797
+ const start = Date.now();
3798
+ if (!this.connected) await this.connect();
3799
+ return new Promise((res) => {
3800
+ this.once("pong", () => res(Date.now() - start));
3801
+ this.send("ping");
3802
+ });
3803
+ }
3804
+ send(channel, payload = null) {
3805
+ if (!this.connection || this.connection.readyState !== WebSocket.OPEN) {
3806
+ this.pending.push({ event: channel, payload });
3807
+ } else {
3808
+ this.connection.send(JSON.stringify({ event: channel, payload }));
3809
+ }
3810
+ }
3811
+ subscribe(event) {
3812
+ const e = new PathEvent(event).toString();
3813
+ if (!this.events[e]) this.events[e] = 1;
3814
+ else this.events[e]++;
3815
+ if (this.events[e] == 1) {
3816
+ return new Promise((res) => {
3817
+ this.once("subscribe", (event2, payload) => {
3818
+ if (payload.event == e) res(payload.success);
3819
+ });
3820
+ this.send("subscribe", e.toString());
3821
+ });
3822
+ } else return Promise.resolve(true);
3823
+ }
3824
+ async subscriptions() {
3825
+ if (!this.connected) await this.connect();
3826
+ return new Promise((res) => {
3827
+ this.once("subscriptions", (event, payload) => res(payload));
3828
+ this.send("subscriptions");
3829
+ });
3830
+ }
3831
+ unsubscribe(event) {
3832
+ const e = new PathEvent(event).toString();
3833
+ this.events[e]--;
3834
+ if (this.events[e] <= 0) {
3835
+ delete this.events[e];
3836
+ return new Promise((res) => {
3837
+ this.once("unsubscribe", (event2, payload) => {
3838
+ if (payload.event == e) res(payload.success);
3839
+ });
3840
+ this.send("unsubscribe", e.toString());
3841
+ });
3842
+ } else return Promise.resolve(true);
3843
+ }
3844
+ }
3845
+ class Storage extends PathEventEmitter {
3846
+ constructor(momentum, module2 = "storage") {
3847
+ super();
3848
+ this.momentum = momentum;
3849
+ this.module = module2;
3850
+ this.on(this.module, (event, payload) => {
3851
+ if (Array.isArray(payload)) {
3852
+ this.cache.addAll(payload);
3853
+ } else if ((event.create || event.update || event.read) && payload != null) {
3854
+ this.cache.add(payload);
3855
+ this.cache.values().forEach((meta) => {
3856
+ if (meta?.children && event.path == meta.path) meta.children = [...meta.children.filter((c) => c.path != event.name), payload.name];
3857
+ });
3858
+ } else if (event.delete || payload == null) {
3859
+ if (!event.path) return this.cache.clear();
3860
+ else this.cache.delete(event.path);
3861
+ }
3862
+ });
3863
+ }
3864
+ subscribers = {};
3865
+ cache = new Cache("path");
3866
+ /**
3867
+ * List all files
3868
+ * @param {string} path Target directory
3869
+ * @return {Promise<DirMeta>} Directory metadata
3870
+ */
3871
+ all(path) {
3872
+ if (!path) path = "/";
3873
+ return this.momentum.api.request({ url: `api/${this.module}/${path}?all=true` }).then((resp) => {
3874
+ this.emit(PES`${this.module}/${path}:r`, resp);
3875
+ return resp;
3876
+ });
3877
+ }
3878
+ /**
3879
+ * Copy one location to another
3880
+ * @param {string} source Source location
3881
+ * @param {string} destination Destination of copy
3882
+ * @return {Promise<DirMeta | FileMeta>} Metadata of copy
3883
+ */
3884
+ copy(source, destination) {
3885
+ if (!source) throw new Error("Cannot copy file or folder, missing source");
3886
+ if (!destination) {
3887
+ const path = source.split("/"), fullName = path.pop(), split = fullName.split(".");
3888
+ destination = split.length == 1 || split.length == 2 && !split[0] ? `${fullName} (COPY)` : `${split.splice(0, split.length - 1).join(".")} (COPY).${split.at(-1)}`;
3889
+ destination = [...path.slice(0, -1), destination].join("/");
3890
+ if (!destination.startsWith("/")) destination = "/" + destination;
3891
+ }
3892
+ return this.momentum.api.request({ url: `api/${this.module}/${destination}`, body: { from: source } }).then((response) => {
3893
+ this.emit(PES`${this.module}/${destination}:c`, response);
3894
+ return response;
3895
+ });
3896
+ }
3897
+ /**
3898
+ * Delete location
3899
+ * @param {string} path Target module
3900
+ * @return {Promise<void>} Returns once complete
3901
+ */
3902
+ async delete(path) {
3903
+ if (!path) throw new Error("Cannot delete file or folder, missing module");
3904
+ const count = await this.momentum.api.request({ url: `api/${this.module}/${path}`, method: "DELETE" });
3905
+ if (count) this.emit(PES`${this.module}/${path}:d`, path);
3906
+ return count;
3907
+ }
3908
+ /**
3909
+ * Download module
3910
+ * @param {string} path Path to target
3911
+ * @param {{downloadAs?: string}} opts Download filename
3912
+ * @return {PromiseProgress<Blob>} File as blob with progress tracking
3913
+ */
3914
+ download(path, opts = {}) {
3915
+ if (!path) throw new Error("Cannot download file, missing module");
3916
+ return this.momentum.api.request({ url: `api/${this.module}/${path}?download=true`, decode: false }).then(async (response) => {
3917
+ const blob = await response.blob();
3918
+ const name = opts.downloadAs || path.split("/").pop();
3919
+ downloadFile(blob, name);
3920
+ return response;
3921
+ });
3922
+ }
3923
+ /**
3924
+ * Retrieve file/folder metadata
3925
+ * @param {string} path Path to target
3926
+ * @return {Promise<DirMeta | FileMeta>} Metadata
3927
+ */
3928
+ meta(path) {
3929
+ if (!path) path = "/";
3930
+ return this.momentum.api.request({ url: `api/${this.module}/${path}?meta=true` }).then((resp) => {
3931
+ this.emit(PES`${this.module}/${path}:r`, resp);
3932
+ return resp;
3933
+ });
3934
+ }
3935
+ /**
3936
+ * Create a new directory
3937
+ * @param {string} path Directory module
3938
+ * @return {Promise<DirMeta>} New directory metadata
3939
+ */
3940
+ mkdir(path) {
3941
+ if (!path) throw new Error("Cannot make directory, missing module");
3942
+ return this.momentum.api.request({ url: `api/${this.module}/${path}`, body: { directory: true } }).then((resp) => {
3943
+ this.emit(PES`${this.module}/${path}:c`, resp);
3944
+ return resp;
3945
+ });
3946
+ }
3947
+ /**
3948
+ * Move a file from one location to another
3949
+ * @param {string} source Target that will be moved
3950
+ * @param {string} destination Destination location
3951
+ * @return {Promise<DirMeta | FileMeta>} New file/folder metadata
3952
+ */
3953
+ move(source, destination) {
3954
+ if (!source || !destination) throw new Error("Cannot move file or folder, missing source or destination");
3955
+ if (source == destination) return this.meta(destination);
3956
+ return this.momentum.api.request({ url: `api/${this.module}/${source}`, method: "PATCH", body: { move: destination } }).then((response) => {
3957
+ this.emit(PES`${this.module}/${source}:u`, response);
3958
+ return response;
3959
+ });
3960
+ }
3961
+ open(path, target = "_blank") {
3962
+ if (!path) throw new Error("Cannot download file, missing module");
3963
+ const link = `${this.momentum.url.toString()}api/${this.module}/${path}`.replaceAll(/([^:])\/+/g, "$1/") + (!this.momentum.api.sameOrigin && this.momentum.api.token ? `?token=${this.momentum.api.token}` : "");
3964
+ if (!target) return link;
3965
+ return window.open(link, target);
3966
+ }
3967
+ /**
3968
+ * Read file as text via extraction or OCR
3969
+ * @param {string} path Path to target
3970
+ * @return {Promise<string>} Extracted text
3971
+ */
3972
+ text(path) {
3973
+ if (!path) throw new Error("Missing path");
3974
+ return this.momentum.api.request({ url: `api/${this.module}/${path}?text=true` });
3975
+ }
3976
+ /**
3977
+ * Upload file(s) to server, Uses file browser if no files provided
3978
+ * @param path Path files will be uploaded to
3979
+ * @param {File | File[] | null} files Files to upload or null to open file browser
3980
+ * @param {string | {accept?: string, rename?: string, multiple?: boolean}} opts file browser options & rename file on upload
3981
+ * @return {PromiseProgress<FileMeta[]>} Returns once complete with progress tracking
3982
+ */
3983
+ upload(path = "/", files, opts) {
3984
+ return new PromiseProgress(async (res, rej, prog) => {
3985
+ if (!files) files = await fileBrowser(typeof opts == "object" ? opts : void 0);
3986
+ let f = makeArray(files);
3987
+ if (!files || !f.length) return [];
3988
+ if (f.length == 1 && opts?.rename) f = [new File([f[0]], opts.rename, { type: f[0].type })];
3989
+ return uploadWithProgress({
3990
+ url: `${this.momentum.url.toString()}api/${this.module}/${path}`,
3991
+ files: f,
3992
+ headers: this.momentum.api.headers
3993
+ }).onProgress((p) => {
3994
+ prog(p);
3995
+ }).then((resp) => {
3996
+ this.emit(PES`${this.module}/${path}:c`, resp);
3997
+ res(resp);
3998
+ }).catch((err) => rej(err));
3999
+ });
4000
+ }
4001
+ /**
4002
+ * Subscribe to live updates with callback
4003
+ * @param path Path to data
4004
+ * @param {(value: DirMeta | FileMeta) => any} callback Received changes
4005
+ * @param opts Reload data immediately
4006
+ * @return {() => void} Function to unsubscribe
4007
+ */
4008
+ sync(path, callback, opts = {}) {
4009
+ const e = PE`${this.module}/${path}`;
4010
+ const unsubscribe = callback ? this.on(e.toString(), (event) => callback(event, this.cache.get(path))) : null;
4011
+ if (!opts.reload) this.meta(path);
4012
+ if (!this.subscribers.length) this.momentum.socket?.subscribe(e.toString());
4013
+ const key = Object.keys(this.subscribers).length.toString();
4014
+ this.subscribers[key] = () => {
4015
+ if (unsubscribe) unsubscribe();
4016
+ delete this.subscribers[key];
4017
+ if (!this.subscribers.length) this.momentum.socket?.unsubscribe(e.toString());
4018
+ };
4019
+ return this.subscribers[key];
4020
+ }
4021
+ }
4022
+ class Tokens extends PathEventEmitter {
4023
+ constructor(momentum) {
4024
+ super();
4025
+ this.momentum = momentum;
4026
+ this.controller = new AssetController(momentum, { module: "tokens", key: "_id" });
4027
+ this.relayEvents(this.controller);
4028
+ }
4029
+ controller;
4030
+ get cache() {
4031
+ return this.controller.cache;
4032
+ }
4033
+ /**
4034
+ * Fetch all tokens for user
4035
+ * @param {string} username User to search
4036
+ * @param reload Force reload instead of using cache
4037
+ * @return {Promise<Token[]>} List of tokens
4038
+ */
4039
+ all(username, reload) {
4040
+ return this.controller.read(username, reload);
4041
+ }
4042
+ /**
4043
+ * Create a new user token
4044
+ * @param {{name: string, owner: string, expire: null | Date}} token Token settings
4045
+ * @return {Promise<Token>} Crated token
4046
+ */
4047
+ create(token) {
4048
+ return this.controller.create(token);
4049
+ }
4050
+ /**
4051
+ * Delete an existing token
4052
+ * @param {string} id Token ID
4053
+ * @return {Promise<void>} Resolves once complete
4054
+ */
4055
+ delete(id) {
4056
+ return this.controller.delete(id);
4057
+ }
4058
+ /**
4059
+ * Subscribe to token changes for user
4060
+ * @param {(value: Product[]) => any | null} callback Received changes
4061
+ * @param opts Scope to single user
4062
+ * @return {() => void} Function to unsubscribe
4063
+ */
4064
+ sync(callback, opts = {}) {
4065
+ return this.controller.sync(callback, { path: opts.username });
4066
+ }
4067
+ }
4068
+ class Users extends AssetController {
4069
+ constructor(momentum, socket) {
4070
+ super(momentum, { module: "users", key: "_id" });
4071
+ this.momentum = momentum;
4072
+ this.socket = socket;
4073
+ }
4074
+ afterRead(user) {
4075
+ if (user == null) return null;
4076
+ user.image = this.profileImage(user._id);
4077
+ return user;
4078
+ }
4079
+ beforeWrite(user) {
4080
+ delete user.image;
4081
+ return user;
4082
+ }
4083
+ profileImage(username, token = true) {
4084
+ let url = `${this.momentum.url.toString()}api/users/${username}/image`;
4085
+ if (token && !this.momentum.api.sameOrigin && this.momentum.api.token) url += `?token=${this.momentum.api.token}`;
4086
+ return url;
4087
+ }
4088
+ /**
4089
+ * Upload new profile image for user
4090
+ * @param {string} username Target user
4091
+ * @param {File} file File from form input
4092
+ * @return {PromiseProgress<void>} Returns once upload is complete, provides progress callback
4093
+ */
4094
+ uploadImage(username, file) {
4095
+ return uploadWithProgress({
4096
+ url: this.profileImage(username, false),
4097
+ files: [file],
4098
+ headers: this.momentum.api.headers
4099
+ });
4100
+ }
4101
+ }
4102
+ class Settings extends AssetController {
4103
+ constructor(momentum) {
4104
+ super(momentum, {
4105
+ module: "settings",
4106
+ key: "_id",
4107
+ storage: momentum.opts?.persist ? { storage: momentum.database, key: "_settings" } : void 0
4108
+ });
4109
+ this.momentum = momentum;
4110
+ this.momentum.api.on("token", () => {
4111
+ this.cache.clear();
4112
+ this.all(true);
4113
+ });
4114
+ }
4115
+ /**
4116
+ * Get all schemas organized into a map
4117
+ * @param {boolean} reload Reload instead of using cache
4118
+ * @return {Promise<TreeNode<Schema>[]>} Schemas as nested tree
4119
+ */
4120
+ async map(reload) {
4121
+ const rows = await this.all(reload);
4122
+ return rows.reduce((acc, row) => ({ ...acc, [row._id]: row.value }), {});
4123
+ }
4124
+ /**
4125
+ * Subscribe to live updates with optional callback
4126
+ * @param {(schema: Schema[]) => any} callback Receives latest data
4127
+ * @param opts path - scope events, map - return as map or array
4128
+ * @return {() => void}
4129
+ */
4130
+ sync(callback, opts = {}) {
4131
+ return super.sync(callback ? (event, value) => callback(event, opts.map ? value.reduce((acc, row) => ({ ...acc, [row._id]: row.value }), {}) : value) : void 0, opts);
4132
+ }
4133
+ }
4134
+ class Static extends Storage {
4135
+ constructor(momentum) {
4136
+ super(momentum, "static");
4137
+ this.momentum = momentum;
4138
+ }
4139
+ }
4140
+ const version = "0.57.0";
4141
+ class WebRtc extends PathEventEmitter {
4142
+ constructor(momentum) {
4143
+ super("webrtc");
4144
+ this.momentum = momentum;
4145
+ }
4146
+ get ice() {
4147
+ const host = this.momentum.url.hostname.replace("localhost", "127.0.0.1");
4148
+ const port = this.momentum.settings.cache.get("webrtc_port")?.value;
4149
+ const key = this.momentum.settings.cache.get("webrtc_key")?.value;
4150
+ return [
4151
+ { urls: [`stun:${host}:${port}`] },
4152
+ { urls: [`turn:${host}:${port}`], username: "momentum", credential: key }
4153
+ ];
4154
+ }
4155
+ async answer(offer, stream) {
4156
+ const rtc = new RTCPeerConnection({ iceServers: this.ice });
4157
+ stream.getTracks().forEach((track) => rtc.addTrack(track, stream));
4158
+ await rtc.setRemoteDescription(new RTCSessionDescription(offer));
4159
+ const answer = await rtc.createAnswer();
4160
+ await rtc.setLocalDescription(answer);
4161
+ await new Promise((res) => {
4162
+ if (rtc.iceGatheringState === "complete") return res();
4163
+ rtc.onicegatheringstatechange = () => {
4164
+ if (rtc.iceGatheringState === "complete") res();
4165
+ };
4166
+ });
4167
+ return rtc;
4168
+ }
4169
+ async offer(stream) {
4170
+ const rtc = new RTCPeerConnection({ iceServers: this.ice });
4171
+ stream.getTracks().forEach((track) => rtc.addTrack(track, stream));
4172
+ await rtc.setLocalDescription(await rtc.createOffer());
4173
+ await new Promise((res) => {
4174
+ if (rtc.iceGatheringState === "complete") return res();
4175
+ rtc.onicegatheringstatechange = () => {
4176
+ if (rtc.iceGatheringState === "complete") res();
4177
+ };
4178
+ });
4179
+ return rtc;
4180
+ }
4181
+ async connect(id = randomStringBuilder(16, true, true), audio = true, video = true) {
4182
+ const session = {
4183
+ id,
4184
+ uid: randomStringBuilder(8, true, true),
4185
+ peers: {},
4186
+ stream: await navigator.mediaDevices.getUserMedia({ audio, video }),
4187
+ open: true,
4188
+ disconnect: () => {
4189
+ session.open = false;
4190
+ Object.values(session.peers).forEach((p) => p.connection.close());
4191
+ session.stream.getTracks().forEach((track) => track.stop());
4192
+ this.momentum.socket?.send(`webrtc/${id}/${session.uid}:d`, {
4193
+ uid: session.uid,
4194
+ username: this.momentum.auth.user?._id || "Anonymous",
4195
+ disconnect: true
4196
+ });
4197
+ }
4198
+ };
4199
+ this.momentum.socket?.subscribe(`webrtc/${id}`);
4200
+ this.momentum.socket?.on(`webrtc/${id}`, async (event, payload) => {
4201
+ if (event.name != session.uid) {
4202
+ if (payload.connected) {
4203
+ session.peers[event.name] = {
4204
+ uid: event.name,
4205
+ username: payload.username,
4206
+ connection: await this.offer(session.stream)
4207
+ };
4208
+ this.momentum.socket?.send(`webrtc/${id}/${event.name}`, {
4209
+ uid: session.uid,
4210
+ username: this.momentum.auth.user?._id || "Anonymous",
4211
+ offer: session.peers[event.name].connection.localDescription
4212
+ });
4213
+ } else if (payload.disconnect) {
4214
+ session.peers[event.name].connection.close();
4215
+ delete session.peers[event.name];
4216
+ this.emit(id, session);
4217
+ }
4218
+ } else {
4219
+ if (payload.offer) {
4220
+ session.peers[event.name] = {
4221
+ uid: payload.uid,
4222
+ username: payload.username,
4223
+ connection: await this.answer(payload.offer, session.stream)
4224
+ };
4225
+ session.peers[event.name].stream = session.peers[event.name].connection.getRemoteStreams()[0];
4226
+ if (!session.peers[event.name].stream) session.peers[event.name].connection.ontrack = (e) => {
4227
+ session.peers[event.name].stream = e.streams[0];
4228
+ this.emit(id, session);
4229
+ };
4230
+ this.emit(id, session);
4231
+ this.momentum.socket?.send(`webrtc/${id}/${payload.uid}`, {
4232
+ uid: session.uid,
4233
+ username: this.momentum.auth.user?._id || "Anonymous",
4234
+ answer: session.peers[event.name].connection.localDescription
4235
+ });
4236
+ } else if (payload.answer) {
4237
+ session.peers[payload.uid].connection.setRemoteDescription(new RTCSessionDescription(payload.answer));
4238
+ session.peers[payload.uid].stream = session.peers[payload.uid].connection.getRemoteStreams()[0];
4239
+ if (!session.peers[payload.uid].stream) session.peers[payload.uid].connection.ontrack = (e) => {
4240
+ session.peers[payload.uid].stream = e.streams[0];
4241
+ this.emit(id, session);
4242
+ };
4243
+ this.emit(id, session);
4244
+ }
4245
+ }
4246
+ });
4247
+ this.momentum.socket?.send(`webrtc/${id}/${session.uid}`, {
4248
+ uid: session.uid,
4249
+ username: this.momentum.auth.user?._id || "Anonymous",
4250
+ connected: true
4251
+ });
4252
+ return session;
4253
+ }
4254
+ }
4255
+ class Momentum extends PathEventEmitter {
4256
+ static pathEvent = PathEvent;
4257
+ static version = version;
4258
+ pathEvent = Momentum.pathEvent;
4259
+ version = Momentum.version;
4260
+ opts;
4261
+ url;
4262
+ api;
4263
+ actions;
4264
+ ai;
4265
+ analytics;
4266
+ audit;
4267
+ auth;
4268
+ call;
4269
+ client;
4270
+ data;
4271
+ database;
4272
+ discounts;
4273
+ email;
4274
+ forms;
4275
+ groups;
4276
+ logger;
4277
+ notifications;
4278
+ pdf;
4279
+ products;
4280
+ routes;
4281
+ schemas;
4282
+ settings;
4283
+ sms;
4284
+ socket;
4285
+ static;
4286
+ storage;
4287
+ templates;
4288
+ tokens;
4289
+ transactions;
4290
+ users;
4291
+ webRtc;
4292
+ get banner() {
4293
+ return `
4294
+
4295
+ ######## ##
4296
+ ### ### Momentum v${Momentum.version}
4297
+ ## ### #### ## App: ${this.opts.app}
4298
+ ## ## ## ## ## Analytics: ${typeof this.opts.analytics == "string" ? this.opts.analytics.toUpperCase() : "DISABLED"}
4299
+ ## ### ### ## Logging: ${this.opts.logLevel != "NONE" ? this.opts.logLevel.toUpperCase() : "DISABLED"}
4300
+ ## ## ## ## ## Offline: ${this.opts.offline ? "ENABLED" : "DISABLED"}
4301
+ ## #### ### ## Sockets: ${this.opts.socket ? "ENABLED" : "DISABLED"}
4302
+ ### ### Worker: ${this.opts.worker !== false ? "ENABLED" : "DISABLED"}
4303
+ ## ########
4304
+
4305
+ `;
4306
+ }
4307
+ constructor(url = location.origin, opts = {}) {
4308
+ super();
4309
+ this.url = new URL(url);
4310
+ this.opts = {
4311
+ app: "Momentum App",
4312
+ analytics: "prompt",
4313
+ banner: true,
4314
+ checkoutUrl: `${this.url.toString()}ui/checkout`,
4315
+ credentialsStrategy: "auto",
4316
+ expiredStrategy: "logout",
4317
+ http: {},
4318
+ installPrompt: { delay: 2, retry: 15, ...opts.installPrompt || {} },
4319
+ logLevel: "NONE",
4320
+ loginUrl: `${this.url.toString()}ui/login`,
4321
+ offline: false,
4322
+ persist: true,
4323
+ socket: true,
4324
+ worker: `/momentum.worker.mjs`,
4325
+ ...opts
4326
+ };
4327
+ if (this.opts.banner) console.log(this.banner);
4328
+ if (typeof window != "undefined" && typeof window["indexedDB"] != "undefined")
4329
+ this.database = new Database(this.url.host);
4330
+ this.api = new Api(this);
4331
+ this.auth = new Auth(this);
4332
+ if (this.opts.socket) {
4333
+ this.socket = new Socket(this);
4334
+ this.socket.on("*", (event, data) => {
4335
+ let service = this[event.module];
4336
+ if (event.module == "logs") service = this.logger;
4337
+ if (service?.emit) service.emit(event, data);
4338
+ });
4339
+ }
4340
+ this.settings = new Settings(this);
4341
+ this.client = new Client(this);
4342
+ this.actions = new Actions(this);
4343
+ this.analytics = new Analytics(this);
4344
+ this.ai = new Ai(this);
4345
+ this.audit = new Audit(this);
4346
+ this.call = new Call(this);
4347
+ this.data = new Data(this);
4348
+ this.discounts = new Discounts(this);
4349
+ this.email = new Email(this);
4350
+ this.forms = new Forms(this);
4351
+ this.groups = new Groups(this);
4352
+ this.logger = new Logger(this);
4353
+ this.notifications = new Notifications(this);
4354
+ this.pdf = new Pdf(this);
4355
+ this.products = new Products(this);
4356
+ this.routes = new Routes(this);
4357
+ this.schemas = new Schemas(this);
4358
+ this.sms = new Sms(this);
4359
+ this.static = new Static(this);
4360
+ this.storage = new Storage(this);
4361
+ this.templates = new Templates(this);
4362
+ this.tokens = new Tokens(this);
4363
+ this.transactions = new Transactions(this);
4364
+ this.users = new Users(this);
4365
+ this.webRtc = new WebRtc(this);
4366
+ this.relayEvents(this.actions);
4367
+ this.relayEvents(this.ai);
4368
+ this.relayEvents(this.analytics);
4369
+ this.relayEvents(this.api);
4370
+ this.relayEvents(this.audit);
4371
+ this.relayEvents(this.auth);
4372
+ this.relayEvents(this.call);
4373
+ this.relayEvents(this.client);
4374
+ this.relayEvents(this.data);
4375
+ this.relayEvents(this.discounts);
4376
+ this.relayEvents(this.email);
4377
+ this.relayEvents(this.forms);
4378
+ this.relayEvents(this.groups);
4379
+ this.relayEvents(this.logger);
4380
+ this.relayEvents(this.notifications);
4381
+ this.relayEvents(this.pdf);
4382
+ this.relayEvents(this.products);
4383
+ this.relayEvents(this.routes);
4384
+ this.relayEvents(this.schemas);
4385
+ this.relayEvents(this.settings);
4386
+ this.relayEvents(this.sms);
4387
+ this.relayEvents(this.static);
4388
+ this.relayEvents(this.storage);
4389
+ this.relayEvents(this.templates);
4390
+ this.relayEvents(this.tokens);
4391
+ this.relayEvents(this.transactions);
4392
+ this.relayEvents(this.users);
4393
+ this.relayEvents(this.webRtc);
4394
+ this.users.on(`*`, () => {
4395
+ if (!this.auth.user) return;
4396
+ const cached = this.users.cache.get(this.auth.user._id);
4397
+ if (cached) this.auth.user = cached;
4398
+ });
4399
+ if (this.opts.worker !== false && "serviceWorker" in navigator) {
4400
+ navigator.serviceWorker.register(this.opts.worker, { type: "module" }).catch(() => console.warn("Unable to load momentum worker, some features may be limited."));
4401
+ }
4402
+ }
4403
+ }
4404
+ exports2.ActionType = ActionType;
4405
+ exports2.Actions = Actions;
4406
+ exports2.Ai = Ai;
4407
+ exports2.Analytics = Analytics;
4408
+ exports2.Api = Api;
4409
+ exports2.Audit = Audit;
4410
+ exports2.Auth = Auth;
4411
+ exports2.Blacklist = Blacklist;
4412
+ exports2.Call = Call;
4413
+ exports2.Captcha = Captcha;
4414
+ exports2.Client = Client;
4415
+ exports2.Data = Data;
4416
+ exports2.Discounts = Discounts;
4417
+ exports2.Email = Email;
4418
+ exports2.Forms = Forms;
4419
+ exports2.Groups = Groups;
4420
+ exports2.Logger = Logger;
4421
+ exports2.Momentum = Momentum;
4422
+ exports2.Notifications = Notifications;
4423
+ exports2.PE = PE;
4424
+ exports2.PES = PES;
4425
+ exports2.PathEvent = PathEvent;
4426
+ exports2.PathEventEmitter = PathEventEmitter;
4427
+ exports2.Pdf = Pdf;
4428
+ exports2.Products = Products;
4429
+ exports2.RateLimiter = RateLimiter;
4430
+ exports2.Routes = Routes;
4431
+ exports2.Schemas = Schemas;
4432
+ exports2.Settings = Settings;
4433
+ exports2.Sms = Sms;
4434
+ exports2.Socket = Socket;
4435
+ exports2.Static = Static;
4436
+ exports2.Storage = Storage;
4437
+ exports2.Templates = Templates;
4438
+ exports2.Tokens = Tokens;
4439
+ exports2.Totp = Totp;
4440
+ exports2.Transactions = Transactions;
4441
+ exports2.Users = Users;
4442
+ exports2.WebRtc = WebRtc;
4443
+ exports2.treeBuilder = treeBuilder;
4444
+ exports2.validEvent = validEvent;
4445
+ Object.defineProperty(exports2, Symbol.toStringTag, { value: "Module" });
4446
+ }));