@ztimson/momentum 0.53.2 → 1.0.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 (88) hide show
  1. package/README.md +96 -41
  2. package/bin/build-models.mjs +28 -37
  3. package/dist/actions.d.ts +67 -52
  4. package/dist/actions.d.ts.map +1 -1
  5. package/dist/ai.d.ts +43 -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 +34 -101
  16. package/dist/auth.d.ts.map +1 -1
  17. package/dist/call.d.ts +61 -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 +46 -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 +58 -76
  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 +14 -3
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +4473 -0
  40. package/dist/index.mjs +3469 -1752
  41. package/dist/logger.d.ts +78 -48
  42. package/dist/logger.d.ts.map +1 -1
  43. package/dist/momentum.d.ts +71 -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 +124 -0
  48. package/dist/notifications.d.ts +39 -0
  49. package/dist/notifications.d.ts.map +1 -0
  50. package/dist/pdf.d.ts +32 -14
  51. package/dist/pdf.d.ts.map +1 -1
  52. package/dist/permissions.d.ts +19 -0
  53. package/dist/permissions.d.ts.map +1 -0
  54. package/dist/products.d.ts +47 -0
  55. package/dist/products.d.ts.map +1 -0
  56. package/dist/routes.d.ts +40 -0
  57. package/dist/routes.d.ts.map +1 -0
  58. package/dist/schemas.d.ts +79 -0
  59. package/dist/schemas.d.ts.map +1 -0
  60. package/dist/settings.d.ts +30 -14
  61. package/dist/settings.d.ts.map +1 -1
  62. package/dist/sms.d.ts +14 -0
  63. package/dist/sms.d.ts.map +1 -0
  64. package/dist/sockets.d.ts +22 -12
  65. package/dist/sockets.d.ts.map +1 -1
  66. package/dist/static.d.ts +4 -2
  67. package/dist/static.d.ts.map +1 -1
  68. package/dist/storage.d.ts +103 -24
  69. package/dist/storage.d.ts.map +1 -1
  70. package/dist/templates.d.ts +23 -0
  71. package/dist/templates.d.ts.map +1 -0
  72. package/dist/tokens.d.ts +50 -0
  73. package/dist/tokens.d.ts.map +1 -0
  74. package/dist/totp.d.ts +45 -0
  75. package/dist/totp.d.ts.map +1 -0
  76. package/dist/transactions.d.ts +153 -0
  77. package/dist/transactions.d.ts.map +1 -0
  78. package/dist/users.d.ts +63 -25
  79. package/dist/users.d.ts.map +1 -1
  80. package/dist/webRtc.d.ts +39 -0
  81. package/dist/webRtc.d.ts.map +1 -0
  82. package/package.json +23 -13
  83. package/dist/index.cjs +0 -2756
  84. package/dist/momentum.worker.js +0 -16
  85. package/dist/payments.d.ts +0 -184
  86. package/dist/payments.d.ts.map +0 -1
  87. package/dist/phone.d.ts +0 -19
  88. package/dist/phone.d.ts.map +0 -1
package/dist/index.js ADDED
@@ -0,0 +1,4473 @@
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("login:d", (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["DELETE"] = 0] = "DELETE";
1774
+ ActionType2[ActionType2["POST"] = 1] = "POST";
1775
+ ActionType2[ActionType2["PATCH"] = 2] = "PATCH";
1776
+ ActionType2[ActionType2["PUT"] = 3] = "PUT";
1777
+ ActionType2[ActionType2["GET"] = 4] = "GET";
1778
+ ActionType2[ActionType2["EVENT"] = 6] = "EVENT";
1779
+ ActionType2[ActionType2["CRON"] = 5] = "CRON";
1780
+ ActionType2[ActionType2["AI"] = 7] = "AI";
1781
+ return ActionType2;
1782
+ })(ActionType || {});
1783
+ class Actions extends AssetController {
1784
+ constructor(momentum) {
1785
+ super(momentum, { module: "actions", key: "_id" });
1786
+ this.momentum = momentum;
1787
+ }
1788
+ /**
1789
+ * Manually trigger an actions execution
1790
+ * @param {string} id Action ID
1791
+ * @param {HttpRequestOptions} opts Additional arguments
1792
+ * @return {Promise<ActionResult>} All action output including console logs & return
1793
+ */
1794
+ debug(id, opts = {}) {
1795
+ return this.momentum.api.request({ url: `api/actions/debug/${id}`, method: "POST", ...opts });
1796
+ }
1797
+ /**
1798
+ * Run an HTTP action
1799
+ * @param {string} path HTTP path excluding `/api/actions/run`
1800
+ * @param {HttpRequestOptions} opts HTTP options
1801
+ * @return {Promise<T>} HTTP response
1802
+ */
1803
+ run(path, opts = {}) {
1804
+ return this.momentum.api.request({ ...opts, url: `api/actions/run/${path}` });
1805
+ }
1806
+ }
1807
+ class Ai extends PathEventEmitter {
1808
+ constructor(momentum) {
1809
+ super("ai");
1810
+ this.momentum = momentum;
1811
+ }
1812
+ /** Cancel current AI requests */
1813
+ abort() {
1814
+ return this.momentum.api.request({ url: "/api/ai/abort", method: "POST" });
1815
+ }
1816
+ /**
1817
+ * Ask the AI assistant a question
1818
+ * @param {string} question Users question
1819
+ * @param {context: string, stream: Function} options context - Any hidden context information. stream - Receive response in chunks
1820
+ * @return {Promise<string>} AI's response
1821
+ */
1822
+ async ask(question, options = {}) {
1823
+ if (!question) throw new Error("Cannot ask AI, missing question");
1824
+ const files = (await Promise.all((options?.files || []).map((f) => new Promise((res) => {
1825
+ const reader = new FileReader();
1826
+ reader.onload = () => res([f.name, reader.result]);
1827
+ reader.readAsDataURL(f);
1828
+ })))).reduce((acc, f) => ({ ...acc, [f[0]]: f[1] }), {});
1829
+ const q = { question: question.trim(), context: options.context, files };
1830
+ if (options.stream && this.momentum.socket?.connected) {
1831
+ let unsub = () => {
1832
+ };
1833
+ return new Promise((res) => {
1834
+ let buffer = "";
1835
+ unsub = this.momentum.socket?.on("llm", (event, chunk) => {
1836
+ options.stream?.(chunk);
1837
+ if (chunk.text) buffer += chunk.text;
1838
+ if (chunk.done) res(buffer);
1839
+ });
1840
+ this.momentum.socket?.send("llm", q);
1841
+ }).finally(() => unsub());
1842
+ } else {
1843
+ return this.momentum.api.request({ url: `api/ai`, method: "POST", body: q }).then((resp) => {
1844
+ this.emit(PES`:c`, resp);
1845
+ return resp;
1846
+ });
1847
+ }
1848
+ }
1849
+ /**
1850
+ * Clear AI assistant memory & context
1851
+ * @return {Promise<void>} Resolves once complete
1852
+ */
1853
+ clear() {
1854
+ return this.momentum.api.request({ url: "api/ai", method: "DELETE" }).then(() => this.emit(PES`:d`, this.momentum.api.token));
1855
+ }
1856
+ /**
1857
+ * Current chat history
1858
+ * @return {Promise<AiMessage[]>}
1859
+ */
1860
+ history() {
1861
+ return this.momentum.api.request({ url: "api/ai" }).then((resp) => {
1862
+ this.emit(PES`:r`, resp);
1863
+ return resp;
1864
+ });
1865
+ }
1866
+ /**
1867
+ * Get model info
1868
+ * @return {AiInfo>} Model Info
1869
+ */
1870
+ info() {
1871
+ return this.momentum.api.request({ url: "api/ai/info" });
1872
+ }
1873
+ }
1874
+ let dialogCount = 0;
1875
+ function createDialog(html, opts = {}) {
1876
+ opts = {
1877
+ backdrop: true,
1878
+ css: void 0,
1879
+ closeBtn: true,
1880
+ position: "center",
1881
+ ...opts
1882
+ };
1883
+ const counter = dialogCount++;
1884
+ if (!document.querySelector("style.momentum-prompt")) {
1885
+ const style2 = document.createElement("style");
1886
+ style2.className = "momentum-prompt";
1887
+ style2.innerHTML = `
1888
+ .momentum-backdrop {
1889
+ position: fixed;
1890
+ inset: 0;
1891
+ backdrop-filter: blur(10px);
1892
+ background: rgba(0, 0, 0, 0.5);
1893
+ animation: fadeIn 0.5s ease-out forwards;
1894
+ z-index: 9998;
1895
+ &.closing { animation: fadeOut 0.5s ease-out forwards; }
1896
+ }
1897
+
1898
+ .momentum-prompt {
1899
+ box-sizing: border-box;
1900
+ font-family: sans-serif;
1901
+ position: fixed;
1902
+ padding: 1rem;
1903
+ width: 100%;
1904
+ max-width: 350px;
1905
+ background: #fff;
1906
+ color: #4a5568;
1907
+ border: 1px solid #fff3;
1908
+ border-radius: 24px;
1909
+ box-shadow: 0 20px 40px #0003;
1910
+ overflow: hidden;
1911
+ z-index: 9999;
1912
+
1913
+ &.center {
1914
+ translate(-50%, -50%);
1915
+ left: 50%;
1916
+ top: 50%;
1917
+ animation: slideInCenter 0.5s ease-out forwards;
1918
+ &.closing { animation: slideOutCenter 0.5s ease-out forwards; }
1919
+ }
1920
+
1921
+ &.corner {
1922
+ right: 1rem;
1923
+ bottom: 1rem;
1924
+ animation: slideIn 0.5s ease-out forwards;
1925
+ &.closing { animation: slideOut 0.5s ease-out forwards; }
1926
+ }
1927
+
1928
+ .close {
1929
+ position: absolute;
1930
+ top: 8px;
1931
+ right: 8px;
1932
+ width: 32px;
1933
+ height: 32px;
1934
+ padding: 0;
1935
+ padding-top: 4px;
1936
+ border: none;
1937
+ background: transparent;
1938
+ border-radius: 50%;
1939
+ cursor: pointer;
1940
+ display: flex;
1941
+ align-items: center;
1942
+ justify-content: center;
1943
+ transition: all 0.5s ease;
1944
+ z-index: 1;
1945
+ &:hover {
1946
+ background: #71809633;
1947
+ transform: scale(1.1);
1948
+ }
1949
+ }
1950
+
1951
+ .btn {
1952
+ flex: 1;
1953
+ padding: 14px 20px;
1954
+ border: 1px solid #71809633;
1955
+ border-radius: 12px;
1956
+ background: #7180961a;
1957
+ color: #718096;
1958
+ font: 600 16px sans-serif;
1959
+ cursor: pointer;
1960
+ transition: all 0.2s ease;
1961
+ position: relative;
1962
+ overflow: hidden;
1963
+ &:hover {transform: translateY(-1px);}
1964
+ &.btn-primary {
1965
+ background: var(--theme-primary);
1966
+ color: var(--theme-primary-contrast);
1967
+ box-shadow: 0 4px 15px #667eea66;
1968
+ }
1969
+ }
1970
+
1971
+ .expand-toggle {
1972
+ display: flex;
1973
+ align-items: center;
1974
+ justify-content: center;
1975
+ gap: 6px;
1976
+ width: 100%;
1977
+ padding: 12px;
1978
+ margin-top: 16px;
1979
+ font-size: 13px;
1980
+ background: none;
1981
+ border: none;
1982
+ border-radius: 8px;
1983
+ color: #718096;
1984
+ cursor: pointer;
1985
+ transition: all 0.2s ease;
1986
+ &:hover { background-color: #7180961a; }
1987
+ }
1988
+
1989
+ .expand-area {transition: all 0.4s ease;}
1990
+
1991
+ .hide {
1992
+ margin: 0;
1993
+ padding: 0;
1994
+ border: 0;
1995
+ height: 0;
1996
+ overflow: hidden;
1997
+ }
1998
+ }
1999
+ @keyframes slideInCenter {
2000
+ from { transform: translate(-50%, calc(100vh + 2rem)) }
2001
+ to { transform: translate(-50%, -50%) }
2002
+ }
2003
+ @keyframes slideOutCenter {
2004
+ from { transform: translate(-50%, -50%) }
2005
+ to { transform: translate(-50%, calc(100vh + 2rem)) }
2006
+ }
2007
+ @keyframes slideIn {
2008
+ from { transform: translateY(calc(100% + 2rem)) }
2009
+ to { transform: translateY(0) }
2010
+ }
2011
+ @keyframes slideOut {
2012
+ from { transform: translateY(0) }
2013
+ to { transform: translateY(calc(100% + 2rem)) }
2014
+ }
2015
+ @keyframes fadeIn {
2016
+ from { opacity: 0 }
2017
+ to { opacity: 1 }
2018
+ }
2019
+ @keyframes fadeOut {
2020
+ from { opacity: 1 }
2021
+ to { opacity: 0 }
2022
+ }`;
2023
+ document.head.append(style2);
2024
+ }
2025
+ let style;
2026
+ if (opts.css) {
2027
+ style = document.createElement("style");
2028
+ style.className = `momentum-prompt-${counter}`;
2029
+ style.innerHTML = opts.css;
2030
+ document.head.append(style);
2031
+ }
2032
+ let backdrop;
2033
+ if (opts.backdrop) {
2034
+ backdrop = document.createElement("div");
2035
+ backdrop.className = `momentum-backdrop`;
2036
+ document.body.append(backdrop);
2037
+ }
2038
+ const dialog = document.createElement("div");
2039
+ dialog.className = "momentum-prompt";
2040
+ dialog.classList.add(opts.position);
2041
+ dialog.innerHTML = html;
2042
+ document.body.append(dialog);
2043
+ let htmlOverflow, bodyOverflow;
2044
+ if (opts.backdrop && document.documentElement.style.overflow != "hidden") {
2045
+ htmlOverflow = document.documentElement.style.overflow;
2046
+ document.documentElement.style.overflow = "hidden";
2047
+ bodyOverflow = document.body.style.overflow;
2048
+ document.body.style.overflow = "hidden";
2049
+ }
2050
+ let resolve;
2051
+ const done = new Promise((r) => resolve = r);
2052
+ const close = (result) => {
2053
+ resolve(result);
2054
+ dialog.classList.add("closing");
2055
+ if (backdrop) backdrop.classList.add("closing");
2056
+ setTimeout(() => {
2057
+ if (htmlOverflow) document.documentElement.style.overflow = htmlOverflow;
2058
+ if (bodyOverflow) document.body.style.overflow = bodyOverflow;
2059
+ dialog.remove();
2060
+ if (backdrop) backdrop.remove();
2061
+ if (style) style.remove();
2062
+ }, 1e3);
2063
+ };
2064
+ if (opts.closeBtn) {
2065
+ const btn = document.createElement("button");
2066
+ btn.className = "close";
2067
+ 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>';
2068
+ btn.onclick = close;
2069
+ dialog.prepend(btn);
2070
+ }
2071
+ return { close, dialog, result: done };
2072
+ }
2073
+ class Analytics extends AssetController {
2074
+ constructor(momentum) {
2075
+ super(momentum, { module: "analytics", key: "_id" });
2076
+ this.momentum = momentum;
2077
+ if (this.momentum.client.isHeadless) {
2078
+ this.performance = Promise.resolve(this._performance);
2079
+ } else {
2080
+ this.performance = Promise.all([
2081
+ this.collectNavigationMetrics(),
2082
+ this.collectLCPMetrics()
2083
+ ]).then(() => this._performance);
2084
+ }
2085
+ if (!["anonymous", "force", "prompt", "manual", "false"].includes(this.momentum.opts.analytics.toString())) {
2086
+ if (typeof this.opts.analytics === "string") this.id = this.opts.analytics;
2087
+ if (this._ready) {
2088
+ this._ready();
2089
+ this._ready = null;
2090
+ }
2091
+ return;
2092
+ }
2093
+ if (this.momentum.opts.analytics !== false) {
2094
+ if (this.momentum.client.storage) {
2095
+ this.id = JSONAttemptParse(sessionStorage.getItem(`${this.momentum.url.host}:analytics`) || "null");
2096
+ if (this.id) this.new = false;
2097
+ }
2098
+ this.momentum.auth.on("login:d", () => {
2099
+ if (this.momentum.client.storage) sessionStorage.removeItem(`${this.momentum.url.host}:analytics`);
2100
+ this._id = "";
2101
+ this._consent = void 0;
2102
+ this._new = true;
2103
+ });
2104
+ this.momentum.auth.on("login:c", async () => {
2105
+ if (this.consent) await this.init();
2106
+ });
2107
+ if (this.momentum.opts.analytics == "force") this.consent = true;
2108
+ else if (this.momentum.opts.analytics == "anonymous") {
2109
+ this.consent = false;
2110
+ this.init();
2111
+ } else if (this.momentum.client.storage) {
2112
+ const stored = localStorage.getItem(`${this.momentum.url.host}:consent`);
2113
+ if (stored == "true") this.consent = true;
2114
+ else if (stored && !this.canPromptAgain(stored)) {
2115
+ this.consent = false;
2116
+ this.init();
2117
+ } else {
2118
+ if (stored) localStorage.removeItem(`${this.momentum.url.host}:consent`);
2119
+ this.init();
2120
+ }
2121
+ } else {
2122
+ this.init();
2123
+ }
2124
+ if (this.consent == null && !this.momentum.client.isHeadless && this.momentum.opts.analytics == "prompt")
2125
+ setTimeout(() => this.consentPrompt(), 1e3);
2126
+ }
2127
+ }
2128
+ lastPage;
2129
+ _ready;
2130
+ ready = new Promise((res) => this._ready = res);
2131
+ _performance = { fcp: 0, lcp: 0, ttfb: 0, dom: 0, total: 0 };
2132
+ performance;
2133
+ _consented;
2134
+ consented = new Promise((res) => this._consented = res);
2135
+ _consent;
2136
+ get consent() {
2137
+ return this._consent;
2138
+ }
2139
+ set consent(value) {
2140
+ if (this._consent === value || value == null) return;
2141
+ this._consent = value;
2142
+ if (this.momentum.client.storage) {
2143
+ if (value) localStorage.setItem(`${this.momentum.url.host}:consent`, "true");
2144
+ else if (this.canPromptAgain()) localStorage.setItem(`${this.momentum.url.host}:consent`, (/* @__PURE__ */ new Date()).toISOString());
2145
+ }
2146
+ if (this._consented) {
2147
+ this._consented(this._consent);
2148
+ this._consented = null;
2149
+ }
2150
+ this.emit(`${this.id}:x`, value);
2151
+ if (value) this.init();
2152
+ }
2153
+ _id;
2154
+ get id() {
2155
+ return this._id;
2156
+ }
2157
+ set id(id) {
2158
+ this._id = id;
2159
+ if (this.momentum.client.storage) sessionStorage.setItem(`${this.momentum.url.host}:analytics`, id);
2160
+ if (this.momentum.opts.credentialsStrategy == "auto" && !this.momentum.api.sameOrigin || this.momentum.opts.credentialsStrategy == "header")
2161
+ this.momentum.api.headers["X-Analytics"] = id;
2162
+ }
2163
+ _new = true;
2164
+ set new(val) {
2165
+ this._new = val;
2166
+ }
2167
+ get new() {
2168
+ return this._new;
2169
+ }
2170
+ canPromptAgain(date = localStorage.getItem(`${this.momentum.url.host}:consent`)) {
2171
+ if (!date) return true;
2172
+ if (date == "true") return false;
2173
+ if (typeof date != "object") date = new Date(date);
2174
+ return Date.now() - date.getTime() >= 6e4 * 60 * 24 * 30;
2175
+ }
2176
+ collectNavigationMetrics() {
2177
+ return new Promise((res) => {
2178
+ window.addEventListener("load", () => {
2179
+ const nav = performance.getEntriesByType("navigation")[0];
2180
+ if (nav) {
2181
+ this._performance.ttfb = Math.round(nav.responseStart - nav.requestStart);
2182
+ this._performance.dom = Math.round(nav.domContentLoadedEventEnd - nav.startTime);
2183
+ this._performance.total = Math.round(nav.loadEventEnd - nav.startTime);
2184
+ } else {
2185
+ this._performance.dom = Date.now() - performance.timeOrigin;
2186
+ }
2187
+ try {
2188
+ const paints = performance.getEntriesByType("paint");
2189
+ const fcp = paints.find((e) => e.name === "first-contentful-paint");
2190
+ if (fcp) this._performance.fcp = Math.round(fcp.startTime);
2191
+ } catch {
2192
+ }
2193
+ res();
2194
+ });
2195
+ });
2196
+ }
2197
+ collectLCPMetrics() {
2198
+ return new Promise((res) => {
2199
+ let resolved = false;
2200
+ try {
2201
+ const lcpObserver = new PerformanceObserver((entryList) => {
2202
+ const lcp = entryList.getEntries().pop();
2203
+ if (lcp) this._performance.lcp = Math.round(lcp.startTime);
2204
+ lcpObserver.disconnect();
2205
+ if (!resolved) {
2206
+ resolved = true;
2207
+ res();
2208
+ }
2209
+ });
2210
+ lcpObserver.observe({ type: "largest-contentful-paint", buffered: true });
2211
+ setTimeout(() => {
2212
+ if (!resolved) {
2213
+ lcpObserver.disconnect();
2214
+ res();
2215
+ }
2216
+ }, 3e3);
2217
+ } catch (_) {
2218
+ res();
2219
+ }
2220
+ });
2221
+ }
2222
+ async init() {
2223
+ if (!this.momentum.opts.analytics) return;
2224
+ const performance2 = await this.performance;
2225
+ this.id = await this.momentum.api.request({
2226
+ url: `api/analytics`,
2227
+ body: [
2228
+ navigator?.language,
2229
+ (/* @__PURE__ */ new Date()).getTimezoneOffset(),
2230
+ this.consent ? 1 : 0,
2231
+ navigator?.hardwareConcurrency ?? null,
2232
+ navigator?.deviceMemory ?? null,
2233
+ this.momentum.client.isHeadless ? null : window.innerWidth,
2234
+ this.momentum.client.isHeadless ? null : window.innerHeight,
2235
+ await this.momentum.client.networkSpeed(),
2236
+ performance2.fcp,
2237
+ performance2.lcp,
2238
+ performance2.ttfb,
2239
+ performance2.dom,
2240
+ performance2.total
2241
+ ]
2242
+ });
2243
+ if (this._ready) {
2244
+ this._ready();
2245
+ this._ready = null;
2246
+ }
2247
+ if ("serviceWorker" in navigator && navigator.serviceWorker.controller)
2248
+ navigator.serviceWorker.controller.postMessage({ analyticsId: this.id });
2249
+ this.page();
2250
+ window.addEventListener("beforeunload", () => this.page("CLOSED"));
2251
+ return this.id;
2252
+ }
2253
+ async consentPrompt(content) {
2254
+ const settings = await this.momentum.settings.map();
2255
+ const { close, dialog, result } = createDialog(`
2256
+ <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>
2257
+ <div style="display: flex; gap: 10px">
2258
+ <button class="agree btn btn-primary" >Agree</button>
2259
+ <button class="decline btn">Essential Only</button>
2260
+ </div>`, { backdrop: false, position: "corner" });
2261
+ dialog.addEventListener("click", (e) => {
2262
+ let t = e.target, depth = 0;
2263
+ while (!!t && t.tagName != "BUTTON" && ++depth < 5) t = t.parentElement;
2264
+ if (!t || t.tagName != "BUTTON") return;
2265
+ if (t.matches(".agree")) close(true);
2266
+ else if (t.matches(".decline")) close(false);
2267
+ });
2268
+ return result.then((consent) => {
2269
+ this.consent = !!consent;
2270
+ return this.consent;
2271
+ });
2272
+ }
2273
+ async speedTest(format = false, bytes) {
2274
+ let bps = 0;
2275
+ if (!bytes) {
2276
+ bps = await this.speedTest(false, 1024 * 1024);
2277
+ if (bps > 1024 * 1024 * 10) bps = await this.speedTest(false, 1024 * 1024 * 20);
2278
+ } else {
2279
+ const start = performance.now();
2280
+ await this.momentum.api.request({ url: `api/tools/dummy-file?size=${bytes}` });
2281
+ const duration = (performance.now() - start) / 1e3;
2282
+ bps = Math.ceil(bytes / duration);
2283
+ }
2284
+ return format ? formatBytes(bps, 2) : bps;
2285
+ }
2286
+ ipTrace(ip) {
2287
+ return this.momentum.api.request({ url: `api/analytics/trace/${ip}` });
2288
+ }
2289
+ async metric(name, value, strategy = "add") {
2290
+ if (!this.momentum.opts.analytics) return;
2291
+ await this.ready;
2292
+ return this.momentum.api.request({
2293
+ url: "api/analytics/metric",
2294
+ body: {
2295
+ name: Array.isArray(name) ? name[0] : name,
2296
+ category: Array.isArray(name) ? name[1] : null,
2297
+ value,
2298
+ strategy
2299
+ }
2300
+ }).then(() => this.emit(`${this.id}:u`));
2301
+ }
2302
+ async page(page = location.href, meta = {}) {
2303
+ if (!this.momentum.opts.analytics || this.lastPage == page) return;
2304
+ await this.ready;
2305
+ return this.momentum.api.request({
2306
+ url: "api/analytics/page",
2307
+ body: { page, timestamp: /* @__PURE__ */ new Date(), meta }
2308
+ }).then(() => {
2309
+ this.lastPage = page;
2310
+ this.emit(`${this.id}:u`);
2311
+ });
2312
+ }
2313
+ }
2314
+ class Api extends Http {
2315
+ constructor(momentum) {
2316
+ super({ ...momentum.opts.http, url: momentum.url.toString() });
2317
+ this.momentum = momentum;
2318
+ this.storageKey = `${this.momentum.url.host}:token`;
2319
+ if (this.momentum.opts.persist && typeof localStorage != "undefined") {
2320
+ const token = localStorage.getItem(this.storageKey);
2321
+ if (token) this.token = token;
2322
+ }
2323
+ }
2324
+ emitter = new PathEventEmitter("api");
2325
+ pending = {};
2326
+ storageKey;
2327
+ get sameOrigin() {
2328
+ if (typeof window == "undefined" || !window?.location) return false;
2329
+ return window.location.host == this.momentum.url.host;
2330
+ }
2331
+ _token = null;
2332
+ /** Current API token */
2333
+ get token() {
2334
+ return this._token;
2335
+ }
2336
+ set token(token) {
2337
+ if (token == this._token) return;
2338
+ this._token = token;
2339
+ if (this.momentum.opts.credentialsStrategy == "auto" && !this.sameOrigin || this.momentum.opts.credentialsStrategy == "header") {
2340
+ this.headers["Authorization"] = this._token ? `Bearer ${this._token}` : null;
2341
+ if (this.momentum.opts.persist && typeof localStorage != "undefined") {
2342
+ if (this._token) localStorage.setItem(this.storageKey, this._token);
2343
+ else localStorage.removeItem(this.storageKey);
2344
+ }
2345
+ }
2346
+ this.emit(`token:${token ? "c" : "d"}`, this._token);
2347
+ }
2348
+ // Path emitter functions
2349
+ emit = this.emitter.emit.bind(this.emitter);
2350
+ off = this.emitter.off.bind(this.emitter);
2351
+ on = this.emitter.on.bind(this.emitter);
2352
+ once = this.emitter.once.bind(this.emitter);
2353
+ relayEvents = this.emitter.relayEvents.bind(this.emitter);
2354
+ /**
2355
+ * Fetch current Momentum status
2356
+ * @return {Promise<Health>}
2357
+ */
2358
+ healthcheck() {
2359
+ return this.request({ url: "api/tools/healthcheck" });
2360
+ }
2361
+ /**
2362
+ * Create API request
2363
+ * @param {HttpRequestOptions} options Request options
2364
+ * @return {Promise} Response
2365
+ */
2366
+ request(options) {
2367
+ const method = options.method == "GET" ? "r" : options.method == "POST" ? "c" : options.method == "DELETE" ? "d" : "u";
2368
+ if (options.url) options.url = encodeURI(options.url.replaceAll("#", "%23"));
2369
+ const key = options.url + method + (options.headers ? JSONSanitize(options.headers) : "") + (options.body ? JSONSanitize(options.body) : "");
2370
+ if (this.pending[key] != null) return this.pending[key];
2371
+ if (this.momentum.client?.captchaScore != null) {
2372
+ if (!options.headers) options.headers = {};
2373
+ options.headers["X-Captcha"] = (+this.momentum.client.captchaScore.toFixed(2)).toString();
2374
+ }
2375
+ this.pending[key] = super.request({ ...options, credentials: "include" }).then((response) => {
2376
+ this.emit(`response:${method}`, { request: options, response });
2377
+ return options.decode === false ? response : response.data;
2378
+ }).catch((err) => {
2379
+ const e = err?.data || err;
2380
+ this.emit(`error:${method}`, { request: options, error: e });
2381
+ throw e;
2382
+ }).finally(() => delete this.pending[key]);
2383
+ this.emit(PES`request:${method}`, { request: options, response: this.pending[key] });
2384
+ return this.pending[key];
2385
+ }
2386
+ }
2387
+ class Audit extends PathEventEmitter {
2388
+ constructor(momentum) {
2389
+ super("audit");
2390
+ this.momentum = momentum;
2391
+ }
2392
+ async available(module2) {
2393
+ return this.momentum.api.request({ url: `api/audit/${module2 || ""}` });
2394
+ }
2395
+ async history(module2, pk) {
2396
+ if (!module2 || !pk) throw new Error("Module and primary key required to fetch audit history");
2397
+ return this.momentum.api.request({ url: `api/audit/${module2}/${pk || ""}` });
2398
+ }
2399
+ async rollback(id) {
2400
+ if (!id) throw new Error("Audit ID required to rollback");
2401
+ return this.momentum.api.request({ url: `api/audit/${id}`, method: "PATCH" });
2402
+ }
2403
+ }
2404
+ class Totp {
2405
+ constructor(momentum) {
2406
+ this.momentum = momentum;
2407
+ }
2408
+ /**
2409
+ * Set default 2FA method
2410
+ * @param {string} username User to set 2FA method on
2411
+ * @param {string} method Default method, must be either: app, call, email, sms
2412
+ * @returns {Promise<void>} Returns once complete
2413
+ */
2414
+ default(username, method) {
2415
+ return this.momentum.api.request({ url: `/api/auth/totp/${username}`, method: "PUT", body: { method } });
2416
+ }
2417
+ /**
2418
+ * Disable 2FA for user
2419
+ * @param {string} username User to disable 2FA for
2420
+ * @param password Confirm user password (Not required as admin)
2421
+ * @return {Promise<void>} Resolves once complete
2422
+ */
2423
+ disable(username, password) {
2424
+ return this.momentum.api.request({ url: `/api/auth/totp/${username}`, method: `DELETE`, body: { password } });
2425
+ }
2426
+ /**
2427
+ * Enable 2FA for user
2428
+ * @param {string} username User to reset
2429
+ * @return {Promise<void>} Resolves once complete
2430
+ */
2431
+ enable = this.reset;
2432
+ /**
2433
+ * Reset users 2FA
2434
+ * @param {string} username User to reset
2435
+ * @param password Confirm user password (Not required as admin)
2436
+ * @return {Promise<void>} Resolves once complete
2437
+ */
2438
+ reset(username, password) {
2439
+ return this.momentum.api.request({ url: `/api/auth/totp/${username}`, method: "PATCH", body: { password } });
2440
+ }
2441
+ /**
2442
+ * Fetch TOTP secret to set up authenticator
2443
+ * @param {string} username
2444
+ * @param password Confirm user password (Not required as admin)
2445
+ * @returns {Promise<{secret: string, qr: string}>}
2446
+ */
2447
+ secret(username, password) {
2448
+ return this.momentum.api.request({ url: `/api/auth/totp/${username}`, method: "POST", body: { password } });
2449
+ }
2450
+ }
2451
+ class Auth extends PathEventEmitter {
2452
+ constructor(momentum) {
2453
+ super("auth");
2454
+ this.momentum = momentum;
2455
+ this.totp = new Totp(momentum);
2456
+ this.momentum.api.addInterceptor((resp, next) => {
2457
+ const blacklist = ["api/auth/login", "api/auth/password", "api/auth/totp"];
2458
+ if (resp.status == 401 && !blacklist.find((url) => resp.url.includes(url)))
2459
+ this.emit(PES`login:x`, this.momentum.api.token);
2460
+ next();
2461
+ });
2462
+ this.on("login:x", async () => {
2463
+ if (this.momentum.opts.expiredStrategy != "ignore") await this.momentum.auth.logout();
2464
+ if (this.momentum.opts.expiredStrategy == "reload") window.location.reload();
2465
+ });
2466
+ }
2467
+ /** Manage user 2FA */
2468
+ totp;
2469
+ session;
2470
+ _user;
2471
+ /** Get current user, undefined if not yet initialized */
2472
+ get user() {
2473
+ return this._user;
2474
+ }
2475
+ /** Update user info without changing the session */
2476
+ set user(user) {
2477
+ if (!isEqual(this.user, user)) {
2478
+ this._user = user ? user : null;
2479
+ this.emit(PES`login:${user ? "c" : "d"}`, this._user);
2480
+ }
2481
+ }
2482
+ async handleLogin(reload = false) {
2483
+ const urlParams = new URLSearchParams(window.location.search);
2484
+ const token = urlParams.get("token");
2485
+ let s;
2486
+ if (!token) {
2487
+ s = await this.readSession();
2488
+ } else {
2489
+ urlParams.delete("token");
2490
+ history.replaceState(null, "", `${window.location.pathname}?${urlParams.toString()}`);
2491
+ s = await this.readSession(token, true);
2492
+ }
2493
+ if (!s?.user) await this.loginRedirect();
2494
+ if (reload && this.user) location.reload();
2495
+ return this.session;
2496
+ }
2497
+ /**
2498
+ * Check if origin is recognized & whitelisted
2499
+ * @param {string} host Origin to check
2500
+ * @return {Promise<void>} Resolves in known, 401 code otherwise
2501
+ */
2502
+ knownHost(host = location.origin) {
2503
+ if (host.startsWith("/")) return Promise.resolve();
2504
+ return this.momentum.api.request({ url: `api/auth/known-host?host=${encodeURI(new URL(host).origin)}` });
2505
+ }
2506
+ /**
2507
+ * Login a user & return the account
2508
+ * @param {string} id username
2509
+ * @param {string} password user's password
2510
+ * @param {{totp: string, totpMethod: TotpMethods, expire: null | number | Date}} opts 2FA code, 2FA code push method, and expiry options (null to never expire)
2511
+ * @return {Promise<{reset?: string, token?: string} | null>} User account on success
2512
+ */
2513
+ login(id, password, opts) {
2514
+ if (!id) throw new Error("Cannot login, missing ID or password");
2515
+ if (opts?.totpMethod && !["call", "email", "sms"].includes(opts.totpMethod))
2516
+ throw new Error(`Invalid 2FA method: ${opts.totpMethod}
2517
+ Must be either: call, email or sms`);
2518
+ return this.momentum.api.request({
2519
+ url: "api/auth/login",
2520
+ headers: password ? { Authorization: void 0 } : void 0,
2521
+ method: "POST",
2522
+ body: clean({
2523
+ username: id.trim(),
2524
+ password: password?.trim(),
2525
+ totp: opts?.totp ? opts.totp.toString() : void 0,
2526
+ totpMethod: opts?.totpMethod,
2527
+ expire: opts?.expire
2528
+ }, true)
2529
+ }).then(async (resp) => {
2530
+ if (resp?.token) await this.readSession(resp.token, true);
2531
+ return resp;
2532
+ });
2533
+ }
2534
+ /**
2535
+ * Login via Momentum single sign on
2536
+ * @param {string} host Host origin attempting to login
2537
+ * @return {Promise<string>} Token on success
2538
+ */
2539
+ loginRedirect(host = location.origin) {
2540
+ return new Promise((res, rej) => {
2541
+ let listener, win;
2542
+ window.addEventListener("message", listener = async (event) => {
2543
+ const data = event?.data || {};
2544
+ if (event.origin != origin || data.sender != origin) return;
2545
+ if (!data.token) return rej("Unknown response from login");
2546
+ window.removeEventListener("message", listener);
2547
+ await this.readSession(data.token, true);
2548
+ win.close();
2549
+ res(data.token);
2550
+ });
2551
+ win = window.open(`${this.momentum.opts.loginUrl}?redirect=postmessage&host=${encodeURI(host)}`, "_blank");
2552
+ if (!win) {
2553
+ window.removeEventListener("message", listener);
2554
+ return rej("Unable to open login");
2555
+ }
2556
+ });
2557
+ }
2558
+ /**
2559
+ * Logout current user
2560
+ */
2561
+ async logout() {
2562
+ await this.momentum.api.request({ url: "api/auth/logout", method: "DELETE" });
2563
+ this.momentum.api.token = this.session = this.user = null;
2564
+ await this.readSession();
2565
+ }
2566
+ /**
2567
+ * Create a new user with login
2568
+ * @param {Partial<User> & {password: string}} user User data with password
2569
+ * @return {Promise<User>} Registered user data
2570
+ */
2571
+ async register(user) {
2572
+ if (!user._id || !user.password) throw new Error("Cannot register user, missing username or password");
2573
+ const u = await this.momentum.api.request({ url: "api/auth/register", body: { ...user } });
2574
+ this.momentum.users.emit(PES`:c`, u);
2575
+ return u;
2576
+ }
2577
+ reset(emailOrPass, token) {
2578
+ if (!emailOrPass) throw new Error("Cannot reset password, missing email or token");
2579
+ return this.momentum.api.request({
2580
+ url: "api/auth/reset",
2581
+ headers: { "Authorization": token ? `Bearer ${token}` : void 0 },
2582
+ method: "PATCH",
2583
+ body: {
2584
+ email: token ? void 0 : emailOrPass,
2585
+ password: token ? emailOrPass : void 0
2586
+ }
2587
+ }).then(() => this.emit(PES`:u`, token ? { token } : { email: emailOrPass }));
2588
+ }
2589
+ /**
2590
+ * Get session information
2591
+ * @param {string} token Token to fetch session info for
2592
+ * @param {boolean} set Set as current active session
2593
+ * @return {Promise<{token: string, user: User, permissions: string[], custom: any} | null>} Session information
2594
+ */
2595
+ async readSession(token, set = false) {
2596
+ const t = token || this.momentum.api.token;
2597
+ const session = await this.momentum.api.request({
2598
+ url: "api/auth/session",
2599
+ headers: { "Authorization": t ? `Bearer ${t}` : void 0 }
2600
+ });
2601
+ if (!token || set) {
2602
+ this.momentum.api.token = session?.token || null;
2603
+ 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}` : ""}`;
2604
+ this.session = session;
2605
+ this.user = session?.user || null;
2606
+ }
2607
+ this.emit(PES`:r`, session);
2608
+ return session;
2609
+ }
2610
+ /** Unlock an account that has been locked from too many login attempts */
2611
+ async unlock(username) {
2612
+ return this.momentum.api.request({ url: "api/auth/unlock", method: "PATCH", body: { username } }).then(() => this.emit(PES`:x`, username));
2613
+ }
2614
+ /**
2615
+ * Update password for user
2616
+ * @param {string} username User to reset
2617
+ * @param {string} password New user password
2618
+ * @param {string} oldPassword Old password for validation
2619
+ * @return {Promise<void>} Resolves once complete
2620
+ */
2621
+ async updatePassword(username, password, oldPassword) {
2622
+ if (!username || !password) throw new Error("Cannot update password, missing username or password");
2623
+ return this.momentum.api.request({
2624
+ url: "api/auth/password",
2625
+ method: "PATCH",
2626
+ body: oldPassword ? { username, password, oldPassword } : { username, password }
2627
+ }).then((resp) => {
2628
+ this.emit(PES`:u`, resp?.token);
2629
+ if (resp?.token) this.momentum.api.token = resp.token;
2630
+ });
2631
+ }
2632
+ }
2633
+ class Call extends PathEventEmitter {
2634
+ constructor(momentum) {
2635
+ super();
2636
+ this.momentum = momentum;
2637
+ }
2638
+ /**
2639
+ * Place a call to number
2640
+ * @param {Call} message Text that will be converted to speech
2641
+ * @return {Promise<any>} Twilio call instance
2642
+ */
2643
+ create(message) {
2644
+ return this.momentum.api.request({ url: "api/call", body: message }).then((resp) => this.emit(PES`call/${message.to}`, message));
2645
+ }
2646
+ /**
2647
+ * Get available numbers and voice
2648
+ */
2649
+ info() {
2650
+ return this.momentum.api.request({ url: "api/call" });
2651
+ }
2652
+ }
2653
+ class Captcha {
2654
+ start = Date.now();
2655
+ events = { mouse: [], keys: [], clicks: [] };
2656
+ lastMouse = null;
2657
+ velocities = [];
2658
+ _score = 0.5;
2659
+ set score(v) {
2660
+ this._score = v;
2661
+ }
2662
+ get score() {
2663
+ return this._score;
2664
+ }
2665
+ get verdict() {
2666
+ if (this.score == 0.5) return "unknown";
2667
+ if (this.score > 0.5) return "human";
2668
+ return "bot";
2669
+ }
2670
+ constructor() {
2671
+ if (typeof window === "undefined" || !window.innerWidth || !this.validUserAgent()) {
2672
+ this.score = 0;
2673
+ return;
2674
+ }
2675
+ document.addEventListener("mousemove", (e) => {
2676
+ const now = Date.now();
2677
+ const curr = { x: e.clientX, y: e.clientY, t: now };
2678
+ this.events.mouse.push(curr);
2679
+ if (this.lastMouse) {
2680
+ const dt = now - this.lastMouse.t;
2681
+ const dx = curr.x - this.lastMouse.x;
2682
+ const dy = curr.y - this.lastMouse.y;
2683
+ const dist2 = Math.sqrt(dx * dx + dy * dy);
2684
+ if (dt > 0) this.velocities.push(dist2 / dt);
2685
+ }
2686
+ this.lastMouse = curr;
2687
+ if (this.events.mouse.length > 50) this.events.mouse.shift();
2688
+ if (this.velocities.length > 30) this.velocities.shift();
2689
+ });
2690
+ document.addEventListener("keydown", (e) => {
2691
+ this.events.keys.push(Date.now());
2692
+ if (this.events.keys.length > 20) this.events.keys.shift();
2693
+ });
2694
+ document.addEventListener("click", (e) => {
2695
+ this.events.clicks.push({ x: e.clientX, y: e.clientY, t: Date.now() });
2696
+ if (this.events.clicks.length > 15) this.events.clicks.shift();
2697
+ });
2698
+ setInterval(() => this.update(), 1e3);
2699
+ }
2700
+ validUserAgent() {
2701
+ if (!navigator?.userAgent) return false;
2702
+ const ua = navigator.userAgent.toLowerCase();
2703
+ if (["bot", "crawler", "spider", "scraper", "headless", "phantom", "selenium", "webdriver", "puppeteer", "playwright", "cypress", "automation"].some((pattern) => ua.includes(pattern))) return false;
2704
+ if (!["chrome", "firefox", "safari", "edge", "opera"].some((browser) => ua.includes(browser))) return false;
2705
+ if (!["windows", "mac", "linux", "android", "ios"].some((os) => ua.includes(os))) return false;
2706
+ return ua.length > 20 && ua.length < 1e3;
2707
+ }
2708
+ update() {
2709
+ const totalEvents = this.events.mouse.length + this.events.keys.length + this.events.clicks.length;
2710
+ const timeBonus = Math.min((Date.now() - this.start) / 2e4, 0.15);
2711
+ const activityBonus = totalEvents > 10 ? 0.15 : totalEvents > 3 ? 0.1 : 0;
2712
+ let mouseScore = 0.6;
2713
+ if (this.events.mouse.length > 5) {
2714
+ const recent = this.events.mouse.slice(-15);
2715
+ let changes = 0;
2716
+ for (let i = 2; i < recent.length; i++) {
2717
+ const a1 = Math.atan2(recent[i - 1].y - recent[i - 2].y, recent[i - 1].x - recent[i - 2].x);
2718
+ const a2 = Math.atan2(recent[i].y - recent[i - 1].y, recent[i].x - recent[i - 1].x);
2719
+ if (Math.abs(a1 - a2) > 0.5) changes++;
2720
+ }
2721
+ mouseScore = 0.6 + Math.min(changes / recent.length, 0.3);
2722
+ }
2723
+ let velocityScore = 0.7;
2724
+ if (this.velocities.length > 5) {
2725
+ const avg = this.velocities.reduce((a, b) => a + b, 0) / this.velocities.length;
2726
+ if (avg > 0.01) velocityScore = 0.8;
2727
+ }
2728
+ let typingScore = 0.7;
2729
+ if (this.events.keys.length > 2) {
2730
+ const intervals = [];
2731
+ for (let i = 1; i < this.events.keys.length; i++) {
2732
+ intervals.push(this.events.keys[i] - this.events.keys[i - 1]);
2733
+ }
2734
+ const avgInterval = intervals.reduce((a, b) => a + b, 0) / intervals.length;
2735
+ if (avgInterval > 50 && avgInterval < 1e3) typingScore = 0.85;
2736
+ }
2737
+ const newScore = 0.6 + mouseScore * 0.25 + velocityScore * 0.2 + typingScore * 0.15 + timeBonus + activityBonus;
2738
+ this.score = this.score * 0.8 + newScore * 0.2;
2739
+ this.score = Math.max(0.3, Math.min(0.95, this.score));
2740
+ }
2741
+ }
2742
+ class Client extends PathEventEmitter {
2743
+ constructor(momentum) {
2744
+ super();
2745
+ this.momentum = momentum;
2746
+ this.momentum.on("analytics/speed", (event, bps) => {
2747
+ if (this.storage) sessionStorage.setItem(`${this.momentum.url.host}:speed`, bps.toString());
2748
+ this.momentum.client.speed = bps;
2749
+ });
2750
+ if (!this.isHeadless) {
2751
+ window.addEventListener("beforeinstallprompt", (event) => {
2752
+ event.preventDefault();
2753
+ this.nativePwaPrompt = event;
2754
+ this.emit("install:r");
2755
+ });
2756
+ if (this.storage && sessionStorage.getItem(`${this.momentum.url.host}:speed`)) {
2757
+ this.speed = Promise.resolve(+sessionStorage.getItem(`${this.momentum.url.host}:speed`));
2758
+ } else {
2759
+ this.speed = new Promise((res) => {
2760
+ window.addEventListener("load", async () => res(await this.momentum.analytics.speedTest()));
2761
+ });
2762
+ }
2763
+ }
2764
+ }
2765
+ captcha = new Captcha();
2766
+ installTimeout;
2767
+ nativePwaPrompt;
2768
+ pendingInstall = null;
2769
+ speed;
2770
+ updating = false;
2771
+ get actorType() {
2772
+ return this.captcha.verdict;
2773
+ }
2774
+ get captchaScore() {
2775
+ return this.captcha.score;
2776
+ }
2777
+ /** Check for updates */
2778
+ updateReady = new Promise((res) => {
2779
+ if (!navigator.serviceWorker) return res(false);
2780
+ navigator.serviceWorker.getRegistration().then((sw) => {
2781
+ if (!sw) return res(false);
2782
+ if (sw.waiting) return res(true);
2783
+ const t = setTimeout(() => res(false), 1e4);
2784
+ sw.addEventListener("updatefound", () => {
2785
+ clearTimeout(t);
2786
+ res(true);
2787
+ });
2788
+ sw.update();
2789
+ }).catch(() => res(false));
2790
+ });
2791
+ /** Check if PWA can be installed */
2792
+ get canInstall() {
2793
+ return !!this.nativePwaPrompt;
2794
+ }
2795
+ /** Running inside a container */
2796
+ get isApp() {
2797
+ return this.isElectron || this.isIframe || this.isPwa;
2798
+ }
2799
+ /** Running inside an electron app */
2800
+ get isElectron() {
2801
+ return !!window.electronEnvironment;
2802
+ }
2803
+ /** Headless client without access to DOM */
2804
+ get isHeadless() {
2805
+ return typeof window == "undefined";
2806
+ }
2807
+ /** Running inside an iframe */
2808
+ get isIframe() {
2809
+ return parent?.location != location;
2810
+ }
2811
+ /** Running on a mobile device */
2812
+ get isMobile() {
2813
+ return ["android", "ios"].includes(this.platform);
2814
+ }
2815
+ _isPwa;
2816
+ /** Running as a PWA */
2817
+ get isPwa() {
2818
+ if (this._isPwa == null)
2819
+ this._isPwa = matchMedia("(display-mode: standalone)").matches || navigator?.standalone || document.referrer.includes("android-app://");
2820
+ return this._isPwa;
2821
+ }
2822
+ _platform;
2823
+ /** Get the current platform type */
2824
+ get platform() {
2825
+ if (!this._platform) {
2826
+ const userAgent = navigator.userAgent || navigator.vendor;
2827
+ if (/windows/i.test(userAgent)) this._platform = "windows";
2828
+ else if (/android/i.test(userAgent)) this._platform = "android";
2829
+ else if (/iPad|iPhone|iPod/.test(userAgent)) this._platform = "ios";
2830
+ else if (/macintosh|mac os x/i.test(userAgent)) this._platform = "mac";
2831
+ else if (/linux/i.test(userAgent)) this._platform = "linux";
2832
+ else this._platform = "unknown";
2833
+ }
2834
+ return this._platform;
2835
+ }
2836
+ get storage() {
2837
+ return typeof localStorage != "undefined";
2838
+ }
2839
+ /**
2840
+ * Inject the client theme settings, meta tags & PWA prompt
2841
+ * @param opts Injection options: reload - use cached settings or reload; pwa - disabled auto pwa prompt
2842
+ * @return {Promise<void>} Resolves on completion
2843
+ */
2844
+ async inject(opts = {}) {
2845
+ opts = { install: true, reload: void 0, ...opts };
2846
+ let settings;
2847
+ if (opts.reload == null) {
2848
+ this.inject({ ...opts, reload: true });
2849
+ await this.momentum.settings.cache.loading;
2850
+ settings = this.momentum.settings.cache.entries().reduce((acc, [k, v]) => ({ ...acc, [k]: v.value }), {});
2851
+ } else settings = await this.momentum.settings.map(opts.reload);
2852
+ const meta = (key, name, value) => {
2853
+ const exists = document.querySelector(`meta[${key}="${name}"]`);
2854
+ if (value === void 0 || exists) return exists;
2855
+ const meta2 = document.createElement("meta");
2856
+ meta2.setAttribute(key, name);
2857
+ meta2.content = value;
2858
+ document.head.append(meta2);
2859
+ };
2860
+ document.querySelectorAll(".momentum-logo").forEach((el) => {
2861
+ const size = el.src ? /[?&]size="?(.+)(?:"|&|$)/.exec(el.src) : null;
2862
+ el.src = `${this.momentum.url.toString()}favicon.png${size ? `?size=${size[1]}` : ""}`;
2863
+ });
2864
+ document.querySelectorAll(`[class^="momentum-setting_"]`).forEach((el) => {
2865
+ const keys = [...el.classList].find((c) => c.startsWith("momentum-setting_"))?.match(/momentum-setting_(.*)(?:\:(.+))?/);
2866
+ if (el.tagName == "TITLE") return el.innerText = el.innerText.includes("|") ? `${el.innerText.split("|")[0].trim()} | ${settings["title"]}` : settings["title"];
2867
+ else if (keys[1] == "contact" && el.tagName == "A") el.href = `mailto:${settings.contact}?subject=${settings.title} - ${document.title.split("|")[0]}`;
2868
+ el.innerText = keys[2] ? dotNotation(settings[keys[1]], keys[2]) : settings[keys[1]];
2869
+ });
2870
+ meta("name", "description", settings["description"]);
2871
+ meta("property", "og:title", settings["title"]);
2872
+ meta("property", "og:description", settings["description"]);
2873
+ meta("property", "og:image", `${settings["public_url"]}/banner.png?size=1200x630`);
2874
+ meta("property", "og:logo", `${settings["public_url"]}/favicon.png?size=180`);
2875
+ meta("property", "og:url", settings["public_url"]);
2876
+ meta("property", "og:site_name", "Momentum");
2877
+ meta("property", "og:type", "website");
2878
+ meta("name", "twitter:card", "summary_large_image");
2879
+ if (settings.modules?.["pwa"]) {
2880
+ meta("name", "mobile-web-app-capable", "yes");
2881
+ meta("name", "apple-mobile-web-app-status-bar-style", "default");
2882
+ meta("name", "apple-mobile-web-app-title", settings["title"]);
2883
+ meta("name", "apple-touch-icon", `${settings["public_url"]}/favicon.png`);
2884
+ meta("name", "apple-touch-startup-image", `${settings["public_url"]}/favicon.png`);
2885
+ if (!document.querySelector('link[rel="manifest"]')) {
2886
+ const link = document.createElement("link");
2887
+ link.setAttribute("rel", "manifest");
2888
+ link.setAttribute("href", this.momentum.url.toString() + "manifest.json");
2889
+ document.head.append(link);
2890
+ }
2891
+ if (opts.install !== false && !this.installTimeout && !this.isApp && !this.isHeadless) {
2892
+ this.installTimeout = setTimeout(async () => {
2893
+ this.installTimeout = null;
2894
+ const dismissed = localStorage.getItem(`momentum:install`);
2895
+ if (!dismissed || dismissed != "true" && Date.now() - new Date(dismissed).getTime() > 6e4 * 60 * 24 * this.momentum.opts.installPrompt.retry)
2896
+ this.install();
2897
+ }, 6e4 * this.momentum.opts.installPrompt.delay);
2898
+ }
2899
+ }
2900
+ meta("name", "theme-color", settings["theme"]?.background);
2901
+ if (!document.body.classList.contains("theme-manual")) {
2902
+ document.body.classList.add(settings["theme"]?.darkMode ? "theme-dark" : "theme-light");
2903
+ document.body.classList.remove(!settings["theme"]?.darkMode ? "theme-dark" : "theme-light");
2904
+ }
2905
+ const style = document.querySelector("style.momentum-theme") || document.createElement("style");
2906
+ style.classList.add("momentum-theme");
2907
+ style.innerHTML = `
2908
+ :root {
2909
+ --theme-backdrop: ${settings["theme"]?.background} !important;
2910
+ --theme-backdrop-alt: ${settings["theme"]?.darkMode ? "rgba(0, 0, 0, 0.1)" : "rgba(0, 0, 0, 0.05)"} !important;
2911
+ --theme-primary: ${settings["theme"]?.primary} !important;
2912
+ --theme-accent: ${settings["theme"]?.accent} !important;
2913
+ --theme-backdrop-contrast: ${contrast(settings["theme"]?.background)} !important;
2914
+ --theme-primary-contrast: ${contrast(settings["theme"]?.primary)} !important;
2915
+ --theme-accent-contrast: ${contrast(settings["theme"]?.accent)} !important;
2916
+ --theme-font: ${settings["theme"]?.font} !important;
2917
+ }
2918
+ `;
2919
+ if (!style.parentElement) document.head.append(style);
2920
+ this.emit(PES`client/inject:c`, this.platform);
2921
+ return settings;
2922
+ }
2923
+ async networkSpeed(format) {
2924
+ if (typeof navigator != "undefined" && !navigator.onLine) return 0;
2925
+ const speed = await this.speed;
2926
+ return speed == -1 ? -1 : !format ? speed : formatBytes(speed, 2);
2927
+ }
2928
+ /**
2929
+ * Create an installation prompt popup
2930
+ * @param platform Platform specific prompt, leave blank to auto-detect
2931
+ */
2932
+ async install(platform = this.platform) {
2933
+ if (this.pendingInstall) this.pendingInstall();
2934
+ const settings = await this.momentum.settings.map();
2935
+ if (!settings.pwa) throw new Error("Universal application isn't enabled");
2936
+ this.emit("install:u");
2937
+ const nativePrompt = platform == this.platform && this.nativePwaPrompt;
2938
+ const features = [
2939
+ { 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"/>' },
2940
+ { 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"/>' },
2941
+ { 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"/>' }
2942
+ ].map((f) => `<div class="feature"><svg viewBox="0 0 24 24">${f.icon}</svg><span>${f.label}</span></div>`).join("\n");
2943
+ const aInstructions = [
2944
+ { 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"/>' },
2945
+ { 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"/>' }
2946
+ ].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");
2947
+ const iInstructions = [
2948
+ { 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"/>' },
2949
+ { 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"/>' }
2950
+ ].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");
2951
+ const downloads = [
2952
+ { 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>' },
2953
+ { 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>' },
2954
+ { 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>' }
2955
+ ].map((d) => `<button class="${d.label.toLowerCase()} download-btn btn">${d.icon}<div>${d.label}</div></button>`).join("\n");
2956
+ const { close, dialog, result } = createDialog(`
2957
+ <div style="margin: 1.5rem 0; text-align: center"><img src="${this.momentum.url}favicon.png?size=75" alt="logo" style="display: inline-block" /><h2 style="margin: 0.5rem 0; font-weight: 500; font-size: 2rem;">Install ${settings.title}</h2><p style="margin: 0; color: #4a5568">Get the full experience with our app!</p></div>
2958
+ <div style="margin: 2rem 1rem">
2959
+ ${nativePrompt || platform != "android" && platform != "ios" ? `<div class="features">${features}</div>` : ""}
2960
+ ${!nativePrompt && platform == "android" ? `<div class="android">${aInstructions}</div>` : ""}
2961
+ ${!nativePrompt && platform == "ios" ? `<div class="ios">${iInstructions}</div>` : ""}
2962
+ </div>
2963
+ <div style="margin-top: 1.5rem; display: flex; gap: 10px">
2964
+ ${nativePrompt ? `<button class="install btn btn-primary">Install</button>` : ""}
2965
+ ${!nativePrompt && platform != "android" && platform != "ios" ? `<button class="download btn btn-primary">Download</button>` : ""}
2966
+ <button class="dismiss btn btn-secondary">Not Now</button>
2967
+ </div>
2968
+ <button class="expand-toggle">Download Options ▼</button>
2969
+ <div class="downloads expand-area hide">${downloads}</div>
2970
+ `, { css: `
2971
+ .downloads {
2972
+ display: flex;
2973
+ height: 95px;
2974
+ margin-top: 0.75rem;
2975
+ gap: 10px;
2976
+ }
2977
+
2978
+ .download-btn {
2979
+ display: flex;
2980
+ flex-direction: column;
2981
+ align-items: center;
2982
+ justify-content: space-between;
2983
+ height: 85px
2984
+ }
2985
+
2986
+ .feature, .instruction {
2987
+ display: flex;
2988
+ align-items: center;
2989
+ font-size: 14px;
2990
+ margin-bottom: 0.5rem
2991
+ }
2992
+
2993
+ .instruction {
2994
+ margin-bottom: 1rem
2995
+ }
2996
+
2997
+ .feature svg {
2998
+ width: 20px;
2999
+ height: 20px;
3000
+ margin-right: 12px;
3001
+ fill: var(--theme-primary)
3002
+ }` });
3003
+ this.pendingInstall = close;
3004
+ let downloaded = false;
3005
+ const download = (platform2) => {
3006
+ downloaded = true;
3007
+ open(`${this.momentum.url}api/download/${platform2}?analytics=${this.momentum.analytics.id}`, "_blank");
3008
+ };
3009
+ dialog.addEventListener("click", (e) => {
3010
+ let t = e.target, depth = 0;
3011
+ while (!!t && t.tagName != "BUTTON" && ++depth < 5) t = t.parentElement;
3012
+ if (!t || t.tagName != "BUTTON") return;
3013
+ else if (t.matches(".dismiss")) close("dismiss");
3014
+ else if (t.matches(".expand-toggle")) dialog.querySelector(".downloads").classList.toggle("hide");
3015
+ else if (t.matches(".install")) {
3016
+ this.nativePwaPrompt.prompt();
3017
+ close(true);
3018
+ } else if (t.matches(".download")) {
3019
+ download(platform);
3020
+ close(true);
3021
+ } else if (t.matches(".windows")) download("windows");
3022
+ else if (t.matches(".macos")) download("mac");
3023
+ else if (t.matches(".linux")) download("linux");
3024
+ });
3025
+ return result.then((resp) => {
3026
+ this.pendingInstall = null;
3027
+ const success = downloaded || resp && resp != "dismiss";
3028
+ if (resp == "dismiss") localStorage.setItem("momentum:install", (/* @__PURE__ */ new Date()).toISOString());
3029
+ else if (resp) localStorage.setItem("momentum:install", "true");
3030
+ else localStorage.removeItem("momentum:install");
3031
+ this.emit(`install:${success ? "c" : "d"}`);
3032
+ return success;
3033
+ });
3034
+ }
3035
+ /** Update service worker & other tabs */
3036
+ update() {
3037
+ if (this.updating) return;
3038
+ this.updating = true;
3039
+ if (navigator.serviceWorker.controller) {
3040
+ navigator.serviceWorker.addEventListener("message", (event) => {
3041
+ if (event.data?.update) location.reload();
3042
+ });
3043
+ navigator.serviceWorker.controller.postMessage({ update: true });
3044
+ }
3045
+ }
3046
+ }
3047
+ class Data extends PathEventEmitter {
3048
+ constructor(momentum) {
3049
+ super();
3050
+ this.momentum = momentum;
3051
+ }
3052
+ subscribers = {};
3053
+ /**
3054
+ * Create new document in collection
3055
+ * @param {string} collection Target collection
3056
+ * @param {Document<T>} document New document
3057
+ * @return {Promise<Document<T>>} New saved document
3058
+ */
3059
+ create(collection, document2) {
3060
+ if (!collection || !document2) throw new Error("Cannot create document, missing collection or document");
3061
+ return this.momentum.api.request({
3062
+ url: `api/` + PES`data/${collection}`,
3063
+ method: "POST",
3064
+ body: document2
3065
+ }).then((resp) => {
3066
+ this.emit(PES`data/${collection}:c`, resp);
3067
+ return resp;
3068
+ });
3069
+ }
3070
+ /**
3071
+ * Delete document from collection
3072
+ * @param {string} collection Target collection
3073
+ * @param {number} id Document ID
3074
+ * @return {Promise<void>} Returns once complete
3075
+ */
3076
+ async delete(collection, id) {
3077
+ if (!collection || !id) throw new Error("Cannot delete document, missing collection or ID");
3078
+ const count = await this.momentum.api.request({
3079
+ url: `api/` + PES`data/${collection}/${id}`,
3080
+ method: "DELETE"
3081
+ });
3082
+ if (count) this.emit(PES`data/${collection}/${id}:d`, id);
3083
+ return count;
3084
+ }
3085
+ read(collection, id) {
3086
+ if (!collection) throw new Error("Cannot read documents, missing collection");
3087
+ return this.momentum.api.request({ url: `api/` + PES`data/${collection}/${id}` }).then((resp) => {
3088
+ this.emit(PES`data/${collection}/${id}:r`, collection, resp);
3089
+ return resp;
3090
+ });
3091
+ }
3092
+ /**
3093
+ * Update document in collection
3094
+ * @param {string} collection Target collection
3095
+ * @param {Document<T>} document Document to update
3096
+ * @param {boolean} append Append or replace existing document
3097
+ * @return {Promise<Document<T>>} New saved document
3098
+ */
3099
+ update(collection, document2, append) {
3100
+ if (!collection || !document2) throw new Error("Cannot update document, missing collection or document");
3101
+ return this.momentum.api.request({
3102
+ url: `api/` + PES`data/${collection}/${document2._id}`,
3103
+ method: append ? "PATCH" : "PUT",
3104
+ body: document2
3105
+ }).then((resp) => {
3106
+ this.emit(PES`data/${collection}/${document2._id}:u`, resp);
3107
+ return resp;
3108
+ });
3109
+ }
3110
+ /**
3111
+ * Create raw MongoDB query
3112
+ * @param {string} collection Target collection name
3113
+ * @param {RawQuery} query Raw query
3114
+ * @return {Promise<any>} Query response
3115
+ */
3116
+ raw(collection, query) {
3117
+ if (!collection || !query) throw new Error("Cannot execute raw query, missing collection or query");
3118
+ const mode = query.operand.startsWith("find") ? "r" : query.operand == "insertOne" ? "c" : query.operand.startsWith("delete") ? "d" : "u";
3119
+ return this.momentum.api.request({ url: `api/` + PES`data/${collection}` + "?raw", body: query }).then((resp) => {
3120
+ this.emit(PES`data/${collection}:${mode}`, resp);
3121
+ return resp;
3122
+ });
3123
+ }
3124
+ /**
3125
+ * Subscribe to live updates with callback
3126
+ * @param path Path to data
3127
+ * @param {(value: T[]) => any | null} callback Received changes
3128
+ * @param opts Reload data immediately
3129
+ * @return {() => void} Function to unsubscribe
3130
+ */
3131
+ sync(path, callback, opts = {}) {
3132
+ const e = PES`data/${path}`;
3133
+ const cache = new Cache("_id");
3134
+ const unsubscribe = this.on(e, (event, payload) => {
3135
+ if (event.read && Array.isArray(payload)) cache.addAll(payload);
3136
+ else if (event.create || event.update || event.read) cache.add(payload);
3137
+ else if (event.delete) cache.delete(+event.name);
3138
+ callback(cache.all());
3139
+ });
3140
+ if (opts.reload == void 0 || opts.reload) this.read(path);
3141
+ if (!this.subscribers.length) this.momentum.socket?.subscribe(e);
3142
+ const key = Object.keys(this.subscribers).length.toString();
3143
+ this.subscribers[key] = () => {
3144
+ unsubscribe();
3145
+ delete this.subscribers[key];
3146
+ if (!this.subscribers.length) this.momentum.socket?.unsubscribe(e);
3147
+ };
3148
+ return this.subscribers[key];
3149
+ }
3150
+ }
3151
+ class Discounts extends AssetController {
3152
+ constructor(momentum) {
3153
+ super(momentum, { module: "discounts", key: "_id" });
3154
+ this.momentum = momentum;
3155
+ }
3156
+ }
3157
+ class Email extends PathEventEmitter {
3158
+ constructor(momentum) {
3159
+ super();
3160
+ this.momentum = momentum;
3161
+ }
3162
+ /**
3163
+ * Send an Email
3164
+ * @param {Mail} email Email information
3165
+ * @return {Promise<any>} SMTP Response
3166
+ */
3167
+ create(email) {
3168
+ return this.momentum.api.request({ url: "api/email", body: email }).then((response) => {
3169
+ this.emit(PES`email/${email.to || email.bcc?.[0]}:c`, email);
3170
+ return response;
3171
+ });
3172
+ }
3173
+ }
3174
+ class Forms extends TreeAssetController {
3175
+ constructor(momentum) {
3176
+ super(momentum, { module: "forms", key: "path" });
3177
+ this.momentum = momentum;
3178
+ }
3179
+ }
3180
+ class Groups extends AssetController {
3181
+ constructor(momentum) {
3182
+ super(momentum, { module: "groups", key: "_id" });
3183
+ this.momentum = momentum;
3184
+ }
3185
+ }
3186
+ class Logger extends PathEventEmitter {
3187
+ constructor(momentum) {
3188
+ super("logs");
3189
+ this.momentum = momentum;
3190
+ this.channel = this.momentum.opts.app;
3191
+ this.logLevel = this.momentum.opts.logLevel;
3192
+ if (["local", "server"].includes(this.channel.toLowerCase())) throw new Error(`${this.channel} is reserved`);
3193
+ if (typeof window != "undefined") {
3194
+ window.addEventListener("unhandledrejection", async (event) => {
3195
+ let log = event.reason?.stack || event.reason;
3196
+ if (event.reason?.url) log = `${event.reason.method} ${event.reason.url} -> ${event.reason.code}
3197
+
3198
+ ${log}`;
3199
+ if (event.reason?.code == null || event.reason?.code >= 500) {
3200
+ if (LOG_LEVEL[this.logLevel] >= 0) this.error(log);
3201
+ else {
3202
+ this.logs.push(this.buildLog(LOG_LEVEL.ERROR, log));
3203
+ this.emit(`local/error:c`, this.logs);
3204
+ }
3205
+ } else {
3206
+ if (LOG_LEVEL[this.logLevel] >= 1) this.warn(log);
3207
+ else {
3208
+ this.logs.push(this.buildLog(LOG_LEVEL.WARN, log));
3209
+ this.emit(`local/warn:c`, this.logs);
3210
+ }
3211
+ }
3212
+ });
3213
+ }
3214
+ console.error = (...args) => {
3215
+ this.console.error(...args);
3216
+ if (LOG_LEVEL[this.logLevel] >= 0) this.error(args);
3217
+ else {
3218
+ this.logs.push(this.buildLog(LOG_LEVEL.ERROR, args));
3219
+ this.emit(`local/error:c`, this.logs);
3220
+ }
3221
+ };
3222
+ console.warn = (...args) => {
3223
+ this.console.warn(...args);
3224
+ if (LOG_LEVEL[this.logLevel] >= 1) this.warn(args);
3225
+ else {
3226
+ this.logs.push(this.buildLog(LOG_LEVEL.WARN, args));
3227
+ this.emit(`local/warn:c`, this.logs);
3228
+ }
3229
+ };
3230
+ console.info = (...args) => {
3231
+ this.console.info(...args);
3232
+ if (LOG_LEVEL[this.logLevel] >= 2) this.info(args);
3233
+ else {
3234
+ this.logs.push(this.buildLog(LOG_LEVEL.INFO, args));
3235
+ this.emit(`local/info:c`, this.logs);
3236
+ }
3237
+ };
3238
+ console.log = (...args) => {
3239
+ this.console.log(...args);
3240
+ if (LOG_LEVEL[this.logLevel] >= 3) this.log(args);
3241
+ else {
3242
+ this.logs.push(this.buildLog(LOG_LEVEL.LOG, args));
3243
+ this.emit(`local/log:c`, this.logs);
3244
+ }
3245
+ };
3246
+ console.debug = (...args) => {
3247
+ this.console.debug(...args);
3248
+ if (LOG_LEVEL[this.logLevel] >= 4) this.debug(args);
3249
+ else {
3250
+ this.logs.push(this.buildLog(LOG_LEVEL.DEBUG, args));
3251
+ this.emit(`local/debug:c`, this.logs);
3252
+ }
3253
+ };
3254
+ }
3255
+ static cache = {};
3256
+ channel;
3257
+ logLevel;
3258
+ console = {
3259
+ debug: console.debug,
3260
+ log: console.log,
3261
+ info: console.info,
3262
+ warn: console.warn,
3263
+ error: console.error
3264
+ };
3265
+ logs = [];
3266
+ buildLog(level, log) {
3267
+ return {
3268
+ time: /* @__PURE__ */ new Date(),
3269
+ level,
3270
+ log: makeArray(log),
3271
+ ctx: this.momentum.analytics.id
3272
+ };
3273
+ }
3274
+ create(log, channel = this.channel) {
3275
+ if (channel.toLowerCase() == "server") throw new Error('"Server" namespace is reserved');
3276
+ return this.momentum.api.request({ url: `api/logs/${channel}`, body: log }).catch(() => {
3277
+ });
3278
+ }
3279
+ /**
3280
+ * Get available channels
3281
+ * @return {Promise<string[]>} List of channel names
3282
+ */
3283
+ channels() {
3284
+ return this.momentum.api.request({ url: "api/logs/channels" });
3285
+ }
3286
+ /**
3287
+ * Clear logs in channel
3288
+ * @param {string} channel Channel to clear
3289
+ * @return {Promise<number>} Reruns once complete
3290
+ */
3291
+ delete(channel) {
3292
+ if (channel.toLowerCase() == "local") {
3293
+ const length = this.logs.length;
3294
+ this.logs = [];
3295
+ this.emit(`local:d`, this.logs);
3296
+ return Promise.resolve(length);
3297
+ }
3298
+ return this.momentum.api.request({ url: `api/logs/${channel}`, method: "DELETE" });
3299
+ }
3300
+ /**
3301
+ * Read logs from channel
3302
+ * @param {string} channel Channel name
3303
+ * @return {Promise<Log[]>} Logs in channel
3304
+ */
3305
+ read(channel) {
3306
+ if (channel.toLowerCase() == "local") return Promise.resolve(this.logs);
3307
+ return this.momentum.api.request({ url: `api/logs/${channel}` });
3308
+ }
3309
+ sync(channel, callback, opts = {}) {
3310
+ if (!Logger.cache[channel]) Logger.cache[channel] = { subs: 1, logs: [] };
3311
+ else Logger.cache[channel].subs++;
3312
+ if (callback) callback(PE`logs/${channel}:r`, Logger.cache[channel].logs);
3313
+ if (opts.reload !== false) this.read(channel).then((resp) => {
3314
+ Logger.cache[channel].logs = resp;
3315
+ if (callback) callback(PE`logs/${channel}:r`, resp);
3316
+ });
3317
+ const unsubscribe = callback ? this.on(channel, (event, payload) => {
3318
+ if (event.delete) Logger.cache[channel].logs = [];
3319
+ else if (event.create || event.update) Logger.cache[channel].logs.push(payload);
3320
+ else if (event.read && Array.isArray(payload)) Logger.cache[channel].logs = payload;
3321
+ callback(event, Logger.cache[channel].logs);
3322
+ }) : null;
3323
+ if (Logger.cache[channel].subs) this.momentum.socket?.subscribe(`logs/${channel}`);
3324
+ Logger.cache[channel].unsubscribe = () => {
3325
+ if (unsubscribe) unsubscribe();
3326
+ Logger.cache[channel].subs--;
3327
+ if (!Logger.cache[channel].subs) {
3328
+ this.momentum.socket?.unsubscribe(`logs/${channel}`);
3329
+ delete Logger.cache[channel];
3330
+ }
3331
+ };
3332
+ return Logger.cache[channel].unsubscribe;
3333
+ }
3334
+ // Console =========================================================================================================
3335
+ /**
3336
+ * Create debug log
3337
+ * @param log What you want to log
3338
+ * @param {string} channel Channel to publish log to
3339
+ * @return {Promise<void>} Log saved
3340
+ */
3341
+ debug(log, channel = this.channel) {
3342
+ if (typeof log == "string" || Array.isArray(log)) log = this.buildLog(LOG_LEVEL.DEBUG, log);
3343
+ this.logs.push(log);
3344
+ this.emit(`local/debug:c`, this.logs);
3345
+ this.create(log, channel);
3346
+ }
3347
+ /**
3348
+ * Create regular log
3349
+ * @param log What you want to log
3350
+ * @param {string} channel Channel to publish log to
3351
+ * @return {Promise<void>} Log saved
3352
+ */
3353
+ log(log, channel = this.channel) {
3354
+ if (typeof log == "string" || Array.isArray(log)) log = this.buildLog(LOG_LEVEL.LOG, log);
3355
+ this.logs.push(log);
3356
+ this.emit(`local/log:c`, this.logs);
3357
+ this.create(log, channel);
3358
+ }
3359
+ /**
3360
+ * Create info log
3361
+ * @param log What you want to log
3362
+ * @param {string} channel Channel to publish log to
3363
+ * @return {Promise<void>} Log saved
3364
+ */
3365
+ info(log, channel = this.channel) {
3366
+ if (typeof log == "string" || Array.isArray(log)) log = this.buildLog(LOG_LEVEL.INFO, log);
3367
+ this.logs.push(log);
3368
+ this.emit(`local/info:c`, this.logs);
3369
+ this.create(log, channel);
3370
+ }
3371
+ /**
3372
+ * Create warning log
3373
+ * @param log What you want to log
3374
+ * @param {string} channel Channel to publish log to
3375
+ * @return {Promise<void>} Log saved
3376
+ */
3377
+ warn(log, channel = this.channel) {
3378
+ if (typeof log == "string" || Array.isArray(log)) log = this.buildLog(LOG_LEVEL.WARN, log);
3379
+ this.logs.push(log);
3380
+ this.emit(`local/warn:c`, this.logs);
3381
+ this.create(log, channel);
3382
+ }
3383
+ /**
3384
+ * Create error log
3385
+ * @param log What you want to log
3386
+ * @param {string} channel Channel to publish log to
3387
+ * @return {Promise<void>} Log saved
3388
+ */
3389
+ error(log, channel = this.channel) {
3390
+ if (typeof log == "string" || Array.isArray(log)) log = this.buildLog(LOG_LEVEL.ERROR, log);
3391
+ this.logs.push(log);
3392
+ this.emit(`local/error:c`, this.logs);
3393
+ this.create(log, channel);
3394
+ }
3395
+ }
3396
+ class Notifications extends PathEventEmitter {
3397
+ constructor(momentum) {
3398
+ super();
3399
+ this.momentum = momentum;
3400
+ this.subscription.then((resp) => this.enabled = !!resp);
3401
+ }
3402
+ _enabled = false;
3403
+ /** Are notifications enabled */
3404
+ get enabled() {
3405
+ return this._enabled;
3406
+ }
3407
+ set enabled(enabled) {
3408
+ this._enabled = enabled;
3409
+ this.emit(PES`notifications:${enabled ? "u" : "d"}`, enabled);
3410
+ }
3411
+ /** Get Push Subscription info */
3412
+ get subscription() {
3413
+ if (!navigator.serviceWorker?.controller) return Promise.resolve(null);
3414
+ return navigator.serviceWorker.ready.then((sw) => sw?.pushManager?.getSubscription());
3415
+ }
3416
+ /**
3417
+ * Send a notification
3418
+ * @param {Notification} notification Notification that will be sent to user
3419
+ * @return {Promise<void>} Returns once complete
3420
+ */
3421
+ create(notification) {
3422
+ return this.momentum.api.request({ url: "api/notifications", body: notification }).then((response) => {
3423
+ this.emit(PES`notifications/${notification.to}:c`, notification);
3424
+ return response;
3425
+ });
3426
+ }
3427
+ /**
3428
+ * Disable device notifications
3429
+ * @return {Promise<void>} Resolves on success
3430
+ */
3431
+ async disable() {
3432
+ const subscription = await this.subscription;
3433
+ subscription?.unsubscribe();
3434
+ return this.momentum.api.request({ url: "api/notifications", method: "DELETE", body: {
3435
+ p256dh: subscription?.toJSON().keys?.["p256dh"]
3436
+ } }).then(() => this.enabled = false);
3437
+ }
3438
+ /**
3439
+ * Enable device notifications
3440
+ * @return {Promise<null>} Resolves on success
3441
+ */
3442
+ async enable() {
3443
+ const granted = await Notification.requestPermission();
3444
+ if (!granted) return null;
3445
+ const sw = await navigator.serviceWorker.ready;
3446
+ const token = (await this.momentum.settings.read("push_public_key"))?.value;
3447
+ const subscription = (await sw.pushManager.subscribe({
3448
+ userVisibleOnly: true,
3449
+ applicationServerKey: token
3450
+ })).toJSON();
3451
+ return this.momentum.api.request({ url: "api/notifications", method: "PATCH", body: subscription }).then(() => this.enabled = true);
3452
+ }
3453
+ }
3454
+ class Blacklist extends AssetController {
3455
+ constructor(momentum) {
3456
+ super(momentum, { module: "blacklist", path: "routes/blacklist", key: "_id" });
3457
+ this.momentum = momentum;
3458
+ }
3459
+ async update(ignore) {
3460
+ throw new Error("Not supported");
3461
+ }
3462
+ }
3463
+ class RateLimiter extends AssetController {
3464
+ constructor(momentum) {
3465
+ super(momentum, { module: "rate_limit", path: "routes/limit", key: "_id" });
3466
+ this.momentum = momentum;
3467
+ }
3468
+ }
3469
+ class Routes extends PathEventEmitter {
3470
+ constructor(momentum) {
3471
+ super();
3472
+ this.momentum = momentum;
3473
+ this.blacklist = new Blacklist(momentum);
3474
+ this.rateLimiter = new RateLimiter(momentum);
3475
+ this.relayEvents(this.blacklist);
3476
+ this.relayEvents(this.rateLimiter);
3477
+ }
3478
+ blacklist;
3479
+ rateLimiter;
3480
+ }
3481
+ class Templates extends AssetController {
3482
+ constructor(momentum) {
3483
+ super(momentum, { module: "templates", key: "_id" });
3484
+ this.momentum = momentum;
3485
+ }
3486
+ render(id, data) {
3487
+ return this.momentum.api.request({ url: `/api/templates/render/${id}`, method: "POST", body: data });
3488
+ }
3489
+ renderHtml(html, data) {
3490
+ return this.momentum.api.request({ url: `/api/templates/render`, method: "POST", body: { html, data } });
3491
+ }
3492
+ }
3493
+ class Transactions extends AssetController {
3494
+ constructor(momentum) {
3495
+ super(momentum, { module: "transactions", key: "_id" });
3496
+ this.momentum = momentum;
3497
+ }
3498
+ /** Stripe object */
3499
+ stripe;
3500
+ /**
3501
+ * Initialize stripe API
3502
+ * @return {Promise<any>} Returns stripe API
3503
+ * @private
3504
+ */
3505
+ async initStripe() {
3506
+ await new Promise((res) => {
3507
+ if (this.stripe) return res(window["Stripe"]);
3508
+ const stripeScript = document.createElement("script");
3509
+ stripeScript.src = "https://js.stripe.com/v3/";
3510
+ stripeScript.setAttribute("crossorigin", "anonymous");
3511
+ stripeScript.onload = () => res();
3512
+ document.head.appendChild(stripeScript);
3513
+ }).then(() => this.stripe = window["Stripe"]);
3514
+ const token = await this.momentum.settings.read("stripe_token");
3515
+ return this.stripe(token.value);
3516
+ }
3517
+ /**
3518
+ * Create a stripe client secret to complete Payment
3519
+ * @param {string} id Payment ID
3520
+ * @return {Promise<string>} Client secret
3521
+ * @private
3522
+ */
3523
+ getToken(id) {
3524
+ return this.momentum.api.request({ url: `api/transactions/init/${id || ""}`, method: "POST" }).then((resp) => resp.token);
3525
+ }
3526
+ /**
3527
+ * Process transaction programmatically
3528
+ * @param {string} id Transaction ID
3529
+ * @param {Card} card Credit card information
3530
+ * @return {Promise<any>} Stripe confirmation
3531
+ */
3532
+ async checkout(id, card) {
3533
+ const [client, secret] = await Promise.all([this.initStripe(), this.getToken(id)]);
3534
+ return client.confirmPayment({
3535
+ clientSecret: secret,
3536
+ confirmParams: {
3537
+ payment_method: {
3538
+ card: { ...card, details: void 0 },
3539
+ billing_details: card.details || {}
3540
+ }
3541
+ }
3542
+ });
3543
+ }
3544
+ /**
3545
+ * Process transaction using Stripe form
3546
+ * @param {string} id Transaction ID
3547
+ * @param {string} element DOM element to inject form into
3548
+ * @return {Promise<() => Promise<any>>} Callback to submit form
3549
+ */
3550
+ async checkoutForm(id, element) {
3551
+ const [client, secret] = await Promise.all([this.initStripe(), this.getToken(id)]);
3552
+ const form = client.elements({ clientSecret: secret });
3553
+ form.create("payment").mount(element);
3554
+ return (redirect = location.href) => client.confirmPayment({
3555
+ elements: form,
3556
+ redirect: "if_required",
3557
+ confirmParams: { return_url: redirect + "?payment_status=success" }
3558
+ });
3559
+ }
3560
+ /**
3561
+ * Complete transaction via Momentum's checkout page
3562
+ * @param {string} id Transaction ID
3563
+ * @param {string} host Host origin attempting to login
3564
+ * @return {Promise<string>} Translation ID on success
3565
+ */
3566
+ checkoutRedirect(id, host = location.origin) {
3567
+ return new Promise(async (res, rej) => {
3568
+ let origin2 = new URL(this.momentum.opts.checkoutUrl).origin, listener, win;
3569
+ window.addEventListener("message", listener = (event) => {
3570
+ const data = event?.data || {};
3571
+ if (event.origin != origin2 || data.sender != origin2) return;
3572
+ if (!data.response) return rej("Unknown response from payment page");
3573
+ window.removeEventListener("message", listener);
3574
+ win.close();
3575
+ res(data.response);
3576
+ });
3577
+ win = window.open(this.checkoutUrl(id) + `&host=${host}`, "_blank");
3578
+ if (!win) {
3579
+ window.removeEventListener("message", listener);
3580
+ return rej("Unable to open checkout page");
3581
+ }
3582
+ });
3583
+ }
3584
+ /**
3585
+ * Get URL to checkout page for transaction
3586
+ * @param {string} id Transaction ID
3587
+ * @returns {string} URL
3588
+ */
3589
+ checkoutUrl(id) {
3590
+ return encodeURI(`${this.momentum.opts.checkoutUrl}?token=${id}`);
3591
+ }
3592
+ async delete(key) {
3593
+ throw new Error("Not supported");
3594
+ }
3595
+ /**
3596
+ * Send recipient transaction receipt or invoice
3597
+ * @param {string} id Translation ID
3598
+ * @return {Promise<void>} Returns once complete
3599
+ */
3600
+ notify(id) {
3601
+ return this.momentum.api.request({ url: `api/transactions/notify/${id || ""}`, method: "POST" });
3602
+ }
3603
+ /**
3604
+ * Refund a transaction
3605
+ * @param {string} key Asset primary key
3606
+ * @return {Promise<Transaction>} Returns on success
3607
+ */
3608
+ async refund(key) {
3609
+ const transaction = await this.momentum.api.request({ url: `api/${this.opts.module}/${key || ""}`, method: "DELETE" });
3610
+ this.emit(`${key}:u`, transaction);
3611
+ return transaction;
3612
+ }
3613
+ }
3614
+ class Pdf extends PathEventEmitter {
3615
+ constructor(momentum) {
3616
+ super();
3617
+ this.momentum = momentum;
3618
+ }
3619
+ createPdf(body, options) {
3620
+ return this.momentum.api.request({ url: `api/` + PES`pdf`, body: { ...body, options } }).then(async (resp) => {
3621
+ if (options?.downloadAs) {
3622
+ let filename = options?.downloadAs || timestampFilename();
3623
+ if (!filename.endsWith(".pdf")) filename += ".pdf";
3624
+ downloadFile(resp, filename);
3625
+ }
3626
+ this.emit(PES`pdf:c`, resp);
3627
+ return resp;
3628
+ });
3629
+ }
3630
+ /**
3631
+ * Create a PDF from raw HTML code
3632
+ * @param {string} html HTML code
3633
+ * @param {PdfOptions} options PDF rendering options
3634
+ * @return {Promise<Blob>}
3635
+ */
3636
+ fromHtml(html, options = {}) {
3637
+ if (!html) throw new Error("Cannot create PDF, missing HTML");
3638
+ return this.createPdf({ html }, options);
3639
+ }
3640
+ /**
3641
+ * Create a PDF from a server template
3642
+ * @param {RenderTemplate} template Template path
3643
+ * @param {PdfOptions} options PDF rendering options
3644
+ * @return {Promise<Blob>}
3645
+ */
3646
+ fromTemplate(template, options = {}) {
3647
+ if (!template) throw new Error("Cannot create PDF, missing template");
3648
+ return this.createPdf(template, options);
3649
+ }
3650
+ /**
3651
+ * Create PDF from a public URL
3652
+ * @param {string} url HTTP(s) URL
3653
+ * @param {PdfOptions} options PDF rendering option
3654
+ * @return {Promise<Blob>}
3655
+ */
3656
+ fromUrl(url, options = {}) {
3657
+ if (!url) throw new Error("Cannot create PDF, missing URL");
3658
+ return this.createPdf({ url }, options);
3659
+ }
3660
+ async toHtml(file) {
3661
+ if (!file) file = (await fileBrowser({ accept: "application/pdf", multiple: false }))[0];
3662
+ if (!file) return Promise.resolve("");
3663
+ return uploadWithProgress({
3664
+ url: `${this.momentum.url.toString()}api/pdf/html`,
3665
+ headers: this.momentum.api.headers,
3666
+ files: [file]
3667
+ });
3668
+ }
3669
+ }
3670
+ class Products extends PathEventEmitter {
3671
+ constructor(momentum) {
3672
+ super();
3673
+ this.momentum = momentum;
3674
+ this.controller = new AssetController(momentum, { module: "products", key: "_id" });
3675
+ this.relayEvents(this.controller);
3676
+ }
3677
+ controller;
3678
+ get cache() {
3679
+ return this.controller.cache;
3680
+ }
3681
+ /**
3682
+ * Fetch all products
3683
+ * @param {boolean} reload Force reload instead of using cache
3684
+ * @return {Promise<Product[]>} List of products
3685
+ */
3686
+ all(reload) {
3687
+ return this.controller.all(reload);
3688
+ }
3689
+ /**
3690
+ * Fetch specific product information
3691
+ * @param {number} id Product ID
3692
+ * @param {boolean} reload Force reload instead of using cache
3693
+ * @return {Promise<Product>} Found product
3694
+ */
3695
+ read(id, reload) {
3696
+ return this.controller.read(id.toString(), reload);
3697
+ }
3698
+ /**
3699
+ * Subscribe to live updates with callback
3700
+ * @param {(value: Product[]) => any | null} callback Received changes
3701
+ * @param opts Scope to single product
3702
+ * @return {() => void} Function to unsubscribe
3703
+ */
3704
+ sync(callback, opts = {}) {
3705
+ return this.controller.sync(callback, { path: opts.product ? opts.product.toString() : void 0 });
3706
+ }
3707
+ }
3708
+ class Schemas extends TreeAssetController {
3709
+ constructor(momentum) {
3710
+ super(momentum, { module: "schemas", key: "path" });
3711
+ this.momentum = momentum;
3712
+ }
3713
+ }
3714
+ class Sms extends PathEventEmitter {
3715
+ constructor(momentum) {
3716
+ super();
3717
+ this.momentum = momentum;
3718
+ }
3719
+ /**
3720
+ * Send an sms to a number
3721
+ * @param {Message} message Text that will be sent
3722
+ * @return {Promise<any>} Twilio message instance
3723
+ */
3724
+ create(message) {
3725
+ return this.momentum.api.request({ url: "api/sms", body: message }).then((resp) => this.emit(PES`sms/${message.to}`, message));
3726
+ }
3727
+ }
3728
+ class Socket extends PathEventEmitter {
3729
+ constructor(momentum) {
3730
+ super();
3731
+ this.momentum = momentum;
3732
+ this.momentum.auth.on("login:cd", (event, user) => {
3733
+ if (user !== void 0 && this.momentum.api.token != this.token) this.connect();
3734
+ });
3735
+ }
3736
+ static pollingSpeed = 15e3;
3737
+ connection;
3738
+ connecting;
3739
+ connectingResolver;
3740
+ events = {};
3741
+ reconnect = true;
3742
+ pending = [];
3743
+ token;
3744
+ reconnectTimeout;
3745
+ connected = false;
3746
+ scheduleReconnect() {
3747
+ if (!this.reconnect) return;
3748
+ clearTimeout(this.reconnectTimeout);
3749
+ this.reconnectTimeout = setTimeout(() => {
3750
+ this.connect();
3751
+ }, Socket.pollingSpeed);
3752
+ }
3753
+ close(reconnect) {
3754
+ console.debug("Disconnected from Momentum");
3755
+ this.connected = false;
3756
+ this.connection?.close();
3757
+ this.connection = void 0;
3758
+ this.reconnect = !!reconnect;
3759
+ if (this.reconnect) {
3760
+ this.scheduleReconnect();
3761
+ } else {
3762
+ this.events = {};
3763
+ clearTimeout(this.reconnectTimeout);
3764
+ }
3765
+ }
3766
+ connect() {
3767
+ if (!this.connecting) {
3768
+ this.connecting = new Promise((res) => this.connectingResolver = res);
3769
+ }
3770
+ if (typeof navigator != "undefined" && !navigator.onLine || this.connected && this.token == this.momentum.api.token) return this.connecting;
3771
+ if (this.connected) this.close();
3772
+ clearTimeout(this.reconnectTimeout);
3773
+ if (navigator.onLine) {
3774
+ const url = this.momentum.url.toString().replace("http", "ws");
3775
+ this.connection = new WebSocket(url + (this.momentum.api.token ? `?token=${this.momentum.api.token}&url=${location?.href || ""}` : ""));
3776
+ this.connection.onopen = () => {
3777
+ console.debug("Connected to Momentum");
3778
+ this.connected = true;
3779
+ this.token = this.momentum.api.token;
3780
+ clearTimeout(this.reconnectTimeout);
3781
+ Object.keys(this.events).forEach((e) => this.send("subscribe", e));
3782
+ this.pending = this.pending.filter((p) => {
3783
+ this.send(p.event, p.payload);
3784
+ return false;
3785
+ });
3786
+ this.connectingResolver?.();
3787
+ this.connecting = void 0;
3788
+ this.connectingResolver = void 0;
3789
+ };
3790
+ this.connection.onclose = () => {
3791
+ if (!this.reconnect) return;
3792
+ this.close(true);
3793
+ };
3794
+ this.connection.onmessage = (message) => {
3795
+ const data = JSONAttemptParse(message.data);
3796
+ if (data.connected === false) this.close(true);
3797
+ else this.emit(data.event, data.payload);
3798
+ };
3799
+ this.scheduleReconnect();
3800
+ }
3801
+ return this.connecting;
3802
+ }
3803
+ async ping() {
3804
+ const start = Date.now();
3805
+ if (!this.connected) await this.connect();
3806
+ return new Promise((res) => {
3807
+ this.once("pong", () => res(Date.now() - start));
3808
+ this.send("ping");
3809
+ });
3810
+ }
3811
+ send(channel, payload = null) {
3812
+ if (!this.connection || this.connection.readyState !== WebSocket.OPEN) {
3813
+ this.pending.push({ event: channel, payload });
3814
+ } else {
3815
+ this.connection.send(JSON.stringify({ event: channel, payload }));
3816
+ }
3817
+ }
3818
+ subscribe(event) {
3819
+ const e = new PathEvent(event).toString();
3820
+ if (!this.events[e]) this.events[e] = 1;
3821
+ else this.events[e]++;
3822
+ if (this.events[e] == 1) {
3823
+ return new Promise((res) => {
3824
+ this.once("subscribe", (event2, payload) => {
3825
+ if (payload.event == e) res(payload.success);
3826
+ });
3827
+ this.send("subscribe", e.toString());
3828
+ });
3829
+ } else return Promise.resolve(true);
3830
+ }
3831
+ async subscriptions() {
3832
+ if (!this.connected) await this.connect();
3833
+ return new Promise((res) => {
3834
+ this.once("subscriptions", (event, payload) => res(payload));
3835
+ this.send("subscriptions");
3836
+ });
3837
+ }
3838
+ unsubscribe(event) {
3839
+ const e = new PathEvent(event).toString();
3840
+ this.events[e]--;
3841
+ if (this.events[e] <= 0) {
3842
+ delete this.events[e];
3843
+ return new Promise((res) => {
3844
+ this.once("unsubscribe", (event2, payload) => {
3845
+ if (payload.event == e) res(payload.success);
3846
+ });
3847
+ this.send("unsubscribe", e.toString());
3848
+ });
3849
+ } else return Promise.resolve(true);
3850
+ }
3851
+ }
3852
+ class Storage extends PathEventEmitter {
3853
+ constructor(momentum, module2 = "storage") {
3854
+ super();
3855
+ this.momentum = momentum;
3856
+ this.module = module2;
3857
+ this.on(this.module, (event, payload) => {
3858
+ if (Array.isArray(payload)) {
3859
+ this.cache.addAll(payload);
3860
+ } else if ((event.create || event.update || event.read) && payload != null) {
3861
+ this.cache.add(payload);
3862
+ this.cache.values().forEach((meta) => {
3863
+ if (meta?.children && event.path == meta.path) meta.children = [...meta.children.filter((c) => c.path != event.name), payload.name];
3864
+ });
3865
+ } else if (event.delete || payload == null) {
3866
+ if (!event.path) return this.cache.clear();
3867
+ else this.cache.delete(event.path);
3868
+ }
3869
+ });
3870
+ }
3871
+ subscribers = {};
3872
+ cache = new Cache("path");
3873
+ /**
3874
+ * List all files
3875
+ * @param {string} path Target directory
3876
+ * @return {Promise<DirMeta>} Directory metadata
3877
+ */
3878
+ all(path) {
3879
+ if (!path) path = "/";
3880
+ return this.momentum.api.request({ url: `api/${this.module}/${path}?all=true` }).then((resp) => {
3881
+ this.emit(PES`${this.module}/${path}:r`, resp);
3882
+ return resp;
3883
+ });
3884
+ }
3885
+ /**
3886
+ * Copy one location to another
3887
+ * @param {string} source Source location
3888
+ * @param {string} destination Destination of copy
3889
+ * @return {Promise<DirMeta | FileMeta>} Metadata of copy
3890
+ */
3891
+ copy(source, destination) {
3892
+ if (!source) throw new Error("Cannot copy file or folder, missing source");
3893
+ if (!destination) {
3894
+ const path = source.split("/"), fullName = path.pop(), split = fullName.split(".");
3895
+ destination = split.length == 1 || split.length == 2 && !split[0] ? `${fullName} (COPY)` : `${split.splice(0, split.length - 1).join(".")} (COPY).${split.at(-1)}`;
3896
+ destination = [...path.slice(0, -1), destination].join("/");
3897
+ if (!destination.startsWith("/")) destination = "/" + destination;
3898
+ }
3899
+ return this.momentum.api.request({ url: `api/${this.module}/${destination}`, body: { from: source } }).then((response) => {
3900
+ this.emit(PES`${this.module}/${destination}:c`, response);
3901
+ return response;
3902
+ });
3903
+ }
3904
+ /**
3905
+ * Delete location
3906
+ * @param {string} path Target module
3907
+ * @return {Promise<void>} Returns once complete
3908
+ */
3909
+ async delete(path) {
3910
+ if (!path) throw new Error("Cannot delete file or folder, missing module");
3911
+ const count = await this.momentum.api.request({ url: `api/${this.module}/${path}`, method: "DELETE" });
3912
+ if (count) this.emit(PES`${this.module}/${path}:d`, path);
3913
+ return count;
3914
+ }
3915
+ /**
3916
+ * Download module
3917
+ * @param {string} path Path to target
3918
+ * @param {{downloadAs?: string}} opts Download filename
3919
+ * @return {PromiseProgress<Blob>} File as blob with progress tracking
3920
+ */
3921
+ download(path, opts = {}) {
3922
+ if (!path) throw new Error("Cannot download file, missing module");
3923
+ return this.momentum.api.request({ url: `api/${this.module}/${path}?download=true`, decode: false }).then(async (response) => {
3924
+ const blob = await response.blob();
3925
+ const name = opts.downloadAs || path.split("/").pop();
3926
+ downloadFile(blob, name);
3927
+ return response;
3928
+ });
3929
+ }
3930
+ /**
3931
+ * Retrieve file/folder metadata
3932
+ * @param {string} path Path to target
3933
+ * @return {Promise<DirMeta | FileMeta>} Metadata
3934
+ */
3935
+ meta(path) {
3936
+ if (!path) path = "/";
3937
+ return this.momentum.api.request({ url: `api/${this.module}/${path}?meta=true` }).then((resp) => {
3938
+ this.emit(PES`${this.module}/${path}:r`, resp);
3939
+ return resp;
3940
+ });
3941
+ }
3942
+ /**
3943
+ * Create a new directory
3944
+ * @param {string} path Directory module
3945
+ * @return {Promise<DirMeta>} New directory metadata
3946
+ */
3947
+ mkdir(path) {
3948
+ if (!path) throw new Error("Cannot make directory, missing module");
3949
+ return this.momentum.api.request({ url: `api/${this.module}/${path}`, body: { directory: true } }).then((resp) => {
3950
+ this.emit(PES`${this.module}/${path}:c`, resp);
3951
+ return resp;
3952
+ });
3953
+ }
3954
+ /**
3955
+ * Move a file from one location to another
3956
+ * @param {string} source Target that will be moved
3957
+ * @param {string} destination Destination location
3958
+ * @return {Promise<DirMeta | FileMeta>} New file/folder metadata
3959
+ */
3960
+ move(source, destination) {
3961
+ if (!source || !destination) throw new Error("Cannot move file or folder, missing source or destination");
3962
+ if (source == destination) return this.meta(destination);
3963
+ return this.momentum.api.request({ url: `api/${this.module}/${source}`, method: "PATCH", body: { move: destination } }).then((response) => {
3964
+ this.emit(PES`${this.module}/${source}:u`, response);
3965
+ return response;
3966
+ });
3967
+ }
3968
+ open(path, target = "_blank") {
3969
+ if (!path) throw new Error("Cannot download file, missing module");
3970
+ 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}` : "");
3971
+ if (!target) return link;
3972
+ return window.open(link, target);
3973
+ }
3974
+ /**
3975
+ * Read file as text via extraction or OCR
3976
+ * @param {string} path Path to target
3977
+ * @return {Promise<string>} Extracted text
3978
+ */
3979
+ text(path) {
3980
+ if (!path) throw new Error("Missing path");
3981
+ return this.momentum.api.request({ url: `api/${this.module}/${path}?text=true` });
3982
+ }
3983
+ /**
3984
+ * Upload file(s) to server, Uses file browser if no files provided
3985
+ * @param path Path files will be uploaded to
3986
+ * @param {File | File[] | null} files Files to upload or null to open file browser
3987
+ * @param {string | {accept?: string, rename?: string, multiple?: boolean}} opts file browser options & rename file on upload
3988
+ * @return {PromiseProgress<FileMeta[]>} Returns once complete with progress tracking
3989
+ */
3990
+ upload(path = "/", files, opts) {
3991
+ return new PromiseProgress(async (res, rej, prog) => {
3992
+ if (!files) files = await fileBrowser(typeof opts == "object" ? opts : void 0);
3993
+ let f = makeArray(files);
3994
+ if (!files || !f.length) return [];
3995
+ if (f.length == 1 && opts?.rename) f = [new File([f[0]], opts.rename, { type: f[0].type })];
3996
+ return uploadWithProgress({
3997
+ url: `${this.momentum.url.toString()}api/${this.module}/${path}`,
3998
+ headers: this.momentum.api.headers,
3999
+ files: f
4000
+ }).onProgress((p) => {
4001
+ prog(p);
4002
+ }).then((resp) => {
4003
+ this.emit(PES`${this.module}/${path}:c`, resp);
4004
+ res(resp);
4005
+ }).catch((err) => rej(err));
4006
+ });
4007
+ }
4008
+ /**
4009
+ * Subscribe to live updates with callback
4010
+ * @param path Path to data
4011
+ * @param {(value: DirMeta | FileMeta) => any} callback Received changes
4012
+ * @param opts Reload data immediately
4013
+ * @return {() => void} Function to unsubscribe
4014
+ */
4015
+ sync(path, callback, opts = {}) {
4016
+ const e = PE`${this.module}/${path}`;
4017
+ const unsubscribe = callback ? this.on(e.toString(), (event) => callback(event, this.cache.get(path))) : null;
4018
+ if (!opts.reload) this.meta(path);
4019
+ if (!this.subscribers.length) this.momentum.socket?.subscribe(e.toString());
4020
+ const key = Object.keys(this.subscribers).length.toString();
4021
+ this.subscribers[key] = () => {
4022
+ if (unsubscribe) unsubscribe();
4023
+ delete this.subscribers[key];
4024
+ if (!this.subscribers.length) this.momentum.socket?.unsubscribe(e.toString());
4025
+ };
4026
+ return this.subscribers[key];
4027
+ }
4028
+ }
4029
+ class Tokens extends PathEventEmitter {
4030
+ constructor(momentum) {
4031
+ super();
4032
+ this.momentum = momentum;
4033
+ this.controller = new AssetController(momentum, { module: "tokens", key: "_id" });
4034
+ this.relayEvents(this.controller);
4035
+ }
4036
+ controller;
4037
+ get cache() {
4038
+ return this.controller.cache;
4039
+ }
4040
+ /**
4041
+ * Fetch all tokens for user
4042
+ * @param {string} username User to search
4043
+ * @param reload Force reload instead of using cache
4044
+ * @return {Promise<Token[]>} List of tokens
4045
+ */
4046
+ all(username, reload) {
4047
+ return this.controller.read(username, reload);
4048
+ }
4049
+ /**
4050
+ * Create a new user token
4051
+ * @param {{name: string, owner: string, expire: null | Date}} token Token settings
4052
+ * @return {Promise<Token>} Crated token
4053
+ */
4054
+ create(token) {
4055
+ return this.controller.create(token);
4056
+ }
4057
+ /**
4058
+ * Delete an existing token
4059
+ * @param {string} id Token ID
4060
+ * @return {Promise<void>} Resolves once complete
4061
+ */
4062
+ delete(id) {
4063
+ return this.controller.delete(id);
4064
+ }
4065
+ /**
4066
+ * Subscribe to token changes for user
4067
+ * @param {(value: Product[]) => any | null} callback Received changes
4068
+ * @param opts Scope to single user
4069
+ * @return {() => void} Function to unsubscribe
4070
+ */
4071
+ sync(callback, opts = {}) {
4072
+ return this.controller.sync(callback, { path: opts.username });
4073
+ }
4074
+ }
4075
+ class Users extends AssetController {
4076
+ constructor(momentum, socket) {
4077
+ super(momentum, { module: "users", key: "_id" });
4078
+ this.momentum = momentum;
4079
+ this.socket = socket;
4080
+ }
4081
+ afterRead(user) {
4082
+ if (user == null) return null;
4083
+ user.image = this.profileImage(user._id);
4084
+ return user;
4085
+ }
4086
+ beforeWrite(user) {
4087
+ delete user.image;
4088
+ return user;
4089
+ }
4090
+ profileImage(username, token = true) {
4091
+ let url = `${this.momentum.url.toString()}api/users/${username}/image`;
4092
+ if (token && !this.momentum.api.sameOrigin && this.momentum.api.token) url += `?token=${this.momentum.api.token}`;
4093
+ return url;
4094
+ }
4095
+ /**
4096
+ * Upload new profile image for user
4097
+ * @param {string} username Target user
4098
+ * @param {File} file File from form input
4099
+ * @return {PromiseProgress<void>} Returns once upload is complete, provides progress callback
4100
+ */
4101
+ uploadImage(username, file) {
4102
+ return uploadWithProgress({
4103
+ url: this.profileImage(username, false),
4104
+ files: [file],
4105
+ headers: this.momentum.api.headers
4106
+ });
4107
+ }
4108
+ }
4109
+ class Settings extends AssetController {
4110
+ constructor(momentum) {
4111
+ super(momentum, {
4112
+ module: "settings",
4113
+ key: "_id",
4114
+ storage: momentum.opts?.persist ? { storage: momentum.database, key: "_settings" } : void 0
4115
+ });
4116
+ this.momentum = momentum;
4117
+ this.momentum.api.on("token", () => {
4118
+ this.cache.clear();
4119
+ this.all(true);
4120
+ });
4121
+ }
4122
+ /**
4123
+ * Get all schemas organized into a map
4124
+ * @param {boolean} reload Reload instead of using cache
4125
+ * @return {Promise<TreeNode<Schema>[]>} Schemas as nested tree
4126
+ */
4127
+ async map(reload) {
4128
+ const rows = await this.all(reload);
4129
+ return rows.reduce((acc, row) => ({ ...acc, [row._id]: row.value }), {});
4130
+ }
4131
+ /**
4132
+ * Subscribe to live updates with optional callback
4133
+ * @param {(schema: Schema[]) => any} callback Receives latest data
4134
+ * @param opts path - scope events, map - return as map or array
4135
+ * @return {() => void}
4136
+ */
4137
+ sync(callback, opts = {}) {
4138
+ return super.sync(callback ? (event, value) => callback(event, opts.map ? value.reduce((acc, row) => ({ ...acc, [row._id]: row.value }), {}) : value) : void 0, opts);
4139
+ }
4140
+ }
4141
+ class Static extends Storage {
4142
+ constructor(momentum) {
4143
+ super(momentum, "static");
4144
+ this.momentum = momentum;
4145
+ }
4146
+ }
4147
+ const version = "1.0.0";
4148
+ class WebRtc extends PathEventEmitter {
4149
+ constructor(momentum) {
4150
+ super("webrtc");
4151
+ this.momentum = momentum;
4152
+ }
4153
+ get ice() {
4154
+ const host = this.momentum.url.hostname.replace("localhost", "127.0.0.1");
4155
+ const port = this.momentum.settings.cache.get("webrtc_port")?.value;
4156
+ const key = this.momentum.settings.cache.get("webrtc_key")?.value;
4157
+ return [
4158
+ { urls: [`stun:${host}:${port}`] },
4159
+ { urls: [`turn:${host}:${port}`], username: "momentum", credential: key }
4160
+ ];
4161
+ }
4162
+ async answer(offer, stream) {
4163
+ const rtc = new RTCPeerConnection({ iceServers: this.ice });
4164
+ stream.getTracks().forEach((track) => rtc.addTrack(track, stream));
4165
+ await rtc.setRemoteDescription(new RTCSessionDescription(offer));
4166
+ const answer = await rtc.createAnswer();
4167
+ await rtc.setLocalDescription(answer);
4168
+ await new Promise((res) => {
4169
+ if (rtc.iceGatheringState === "complete") return res();
4170
+ rtc.onicegatheringstatechange = () => {
4171
+ if (rtc.iceGatheringState === "complete") res();
4172
+ };
4173
+ });
4174
+ return rtc;
4175
+ }
4176
+ async offer(stream) {
4177
+ const rtc = new RTCPeerConnection({ iceServers: this.ice });
4178
+ stream.getTracks().forEach((track) => rtc.addTrack(track, stream));
4179
+ await rtc.setLocalDescription(await rtc.createOffer());
4180
+ await new Promise((res) => {
4181
+ if (rtc.iceGatheringState === "complete") return res();
4182
+ rtc.onicegatheringstatechange = () => {
4183
+ if (rtc.iceGatheringState === "complete") res();
4184
+ };
4185
+ });
4186
+ return rtc;
4187
+ }
4188
+ async connect(id = randomStringBuilder(16, true, true), audio = true, video = true) {
4189
+ const session = {
4190
+ id,
4191
+ uid: randomStringBuilder(8, true, true),
4192
+ peers: {},
4193
+ stream: await navigator.mediaDevices.getUserMedia({ audio, video }),
4194
+ open: true,
4195
+ disconnect: () => {
4196
+ session.open = false;
4197
+ Object.values(session.peers).forEach((p) => p.connection.close());
4198
+ session.stream.getTracks().forEach((track) => track.stop());
4199
+ this.momentum.socket?.send(`webrtc/${id}/${session.uid}:d`, {
4200
+ uid: session.uid,
4201
+ username: this.momentum.auth.user?._id || "Anonymous",
4202
+ disconnect: true
4203
+ });
4204
+ }
4205
+ };
4206
+ this.momentum.socket?.subscribe(`webrtc/${id}`);
4207
+ this.momentum.socket?.on(`webrtc/${id}`, async (event, payload) => {
4208
+ if (event.name != session.uid) {
4209
+ if (payload.connected) {
4210
+ session.peers[event.name] = {
4211
+ uid: event.name,
4212
+ username: payload.username,
4213
+ connection: await this.offer(session.stream)
4214
+ };
4215
+ this.momentum.socket?.send(`webrtc/${id}/${event.name}`, {
4216
+ uid: session.uid,
4217
+ username: this.momentum.auth.user?._id || "Anonymous",
4218
+ offer: session.peers[event.name].connection.localDescription
4219
+ });
4220
+ } else if (payload.disconnect) {
4221
+ session.peers[event.name].connection.close();
4222
+ delete session.peers[event.name];
4223
+ this.emit(id, session);
4224
+ }
4225
+ } else {
4226
+ if (payload.offer) {
4227
+ session.peers[event.name] = {
4228
+ uid: payload.uid,
4229
+ username: payload.username,
4230
+ connection: await this.answer(payload.offer, session.stream)
4231
+ };
4232
+ session.peers[event.name].stream = session.peers[event.name].connection.getRemoteStreams()[0];
4233
+ if (!session.peers[event.name].stream) session.peers[event.name].connection.ontrack = (e) => {
4234
+ session.peers[event.name].stream = e.streams[0];
4235
+ this.emit(id, session);
4236
+ };
4237
+ this.emit(id, session);
4238
+ this.momentum.socket?.send(`webrtc/${id}/${payload.uid}`, {
4239
+ uid: session.uid,
4240
+ username: this.momentum.auth.user?._id || "Anonymous",
4241
+ answer: session.peers[event.name].connection.localDescription
4242
+ });
4243
+ } else if (payload.answer) {
4244
+ session.peers[payload.uid].connection.setRemoteDescription(new RTCSessionDescription(payload.answer));
4245
+ session.peers[payload.uid].stream = session.peers[payload.uid].connection.getRemoteStreams()[0];
4246
+ if (!session.peers[payload.uid].stream) session.peers[payload.uid].connection.ontrack = (e) => {
4247
+ session.peers[payload.uid].stream = e.streams[0];
4248
+ this.emit(id, session);
4249
+ };
4250
+ this.emit(id, session);
4251
+ }
4252
+ }
4253
+ });
4254
+ this.momentum.socket?.send(`webrtc/${id}/${session.uid}`, {
4255
+ uid: session.uid,
4256
+ username: this.momentum.auth.user?._id || "Anonymous",
4257
+ connected: true
4258
+ });
4259
+ return session;
4260
+ }
4261
+ }
4262
+ class Permissions {
4263
+ constructor(momentum) {
4264
+ this.momentum = momentum;
4265
+ }
4266
+ /** Get all permissions */
4267
+ all = () => this.momentum.auth.session?.permissions || [];
4268
+ /** Filter list of permissions to ones the user has */
4269
+ filter = (...events) => PathEvent.filter(this.all(), ...events);
4270
+ /** Does user have any permissions (or) */
4271
+ has = (...events) => PathEvent.has(this.all(), ...events);
4272
+ /** Does user have all permissions (and) */
4273
+ hasAll = (...events) => PathEvent.hasAll(this.all(), ...events);
4274
+ /** Raise error if user has no permissions (or) */
4275
+ hasFatal = (...events) => PathEvent.hasFatal(this.all(), ...events);
4276
+ /** Raise error if user missing any permissions (and) */
4277
+ hasAllFatal = (...events) => PathEvent.hasAllFatal(this.all(), ...events);
4278
+ }
4279
+ class Momentum extends PathEventEmitter {
4280
+ static pathEvent = PathEvent;
4281
+ static version = version;
4282
+ pathEvent = Momentum.pathEvent;
4283
+ version = Momentum.version;
4284
+ opts;
4285
+ url;
4286
+ api;
4287
+ actions;
4288
+ ai;
4289
+ analytics;
4290
+ audit;
4291
+ auth;
4292
+ call;
4293
+ client;
4294
+ data;
4295
+ database;
4296
+ discounts;
4297
+ email;
4298
+ forms;
4299
+ groups;
4300
+ logger;
4301
+ notifications;
4302
+ pdf;
4303
+ permissions;
4304
+ products;
4305
+ routes;
4306
+ schemas;
4307
+ settings;
4308
+ sms;
4309
+ socket;
4310
+ static;
4311
+ storage;
4312
+ templates;
4313
+ tokens;
4314
+ transactions;
4315
+ users;
4316
+ webRtc;
4317
+ get banner() {
4318
+ return `
4319
+
4320
+ ######## ##
4321
+ ### ### Momentum v${Momentum.version}
4322
+ ## ### #### ## App: ${this.opts.app}
4323
+ ## ## ## ## ## Analytics: ${typeof this.opts.analytics == "string" ? this.opts.analytics.toUpperCase() : "DISABLED"}
4324
+ ## ### ### ## Logging: ${this.opts.logLevel != "NONE" ? this.opts.logLevel.toUpperCase() : "DISABLED"}
4325
+ ## ## ## ## ## Offline: ${this.opts.offline ? "ENABLED" : "DISABLED"}
4326
+ ## #### ### ## Sockets: ${this.opts.socket ? "ENABLED" : "DISABLED"}
4327
+ ### ### Worker: ${this.opts.worker !== false ? "ENABLED" : "DISABLED"}
4328
+ ## ########
4329
+
4330
+ `;
4331
+ }
4332
+ constructor(url = location.origin, opts = {}) {
4333
+ super();
4334
+ this.url = new URL(url);
4335
+ this.opts = {
4336
+ app: "Momentum App",
4337
+ analytics: "prompt",
4338
+ banner: true,
4339
+ checkoutUrl: `${this.url.toString()}ui/checkout`,
4340
+ credentialsStrategy: "auto",
4341
+ expiredStrategy: "logout",
4342
+ http: {},
4343
+ installPrompt: { delay: 2, retry: 15, ...opts.installPrompt || {} },
4344
+ logLevel: "NONE",
4345
+ loginUrl: `${this.url.toString()}ui/login`,
4346
+ offline: false,
4347
+ persist: true,
4348
+ socket: true,
4349
+ worker: `/momentum.worker.mjs`,
4350
+ ...opts
4351
+ };
4352
+ if (this.opts.banner) console.log(this.banner);
4353
+ if (typeof window != "undefined" && typeof window["indexedDB"] != "undefined")
4354
+ this.database = new Database(this.url.host);
4355
+ this.api = new Api(this);
4356
+ this.auth = new Auth(this);
4357
+ if (this.opts.socket) {
4358
+ this.socket = new Socket(this);
4359
+ this.socket.on("*", (event, data) => {
4360
+ let service = this[event.module];
4361
+ if (event.module == "logs") service = this.logger;
4362
+ if (service?.emit) service.emit(event, data);
4363
+ });
4364
+ }
4365
+ this.settings = new Settings(this);
4366
+ this.client = new Client(this);
4367
+ this.actions = new Actions(this);
4368
+ this.analytics = new Analytics(this);
4369
+ this.ai = new Ai(this);
4370
+ this.audit = new Audit(this);
4371
+ this.call = new Call(this);
4372
+ this.data = new Data(this);
4373
+ this.discounts = new Discounts(this);
4374
+ this.email = new Email(this);
4375
+ this.forms = new Forms(this);
4376
+ this.groups = new Groups(this);
4377
+ this.logger = new Logger(this);
4378
+ this.notifications = new Notifications(this);
4379
+ this.pdf = new Pdf(this);
4380
+ this.permissions = new Permissions(this);
4381
+ this.products = new Products(this);
4382
+ this.routes = new Routes(this);
4383
+ this.schemas = new Schemas(this);
4384
+ this.sms = new Sms(this);
4385
+ this.static = new Static(this);
4386
+ this.storage = new Storage(this);
4387
+ this.templates = new Templates(this);
4388
+ this.tokens = new Tokens(this);
4389
+ this.transactions = new Transactions(this);
4390
+ this.users = new Users(this);
4391
+ this.webRtc = new WebRtc(this);
4392
+ this.relayEvents(this.actions);
4393
+ this.relayEvents(this.ai);
4394
+ this.relayEvents(this.analytics);
4395
+ this.relayEvents(this.api);
4396
+ this.relayEvents(this.audit);
4397
+ this.relayEvents(this.auth);
4398
+ this.relayEvents(this.call);
4399
+ this.relayEvents(this.client);
4400
+ this.relayEvents(this.data);
4401
+ this.relayEvents(this.discounts);
4402
+ this.relayEvents(this.email);
4403
+ this.relayEvents(this.forms);
4404
+ this.relayEvents(this.groups);
4405
+ this.relayEvents(this.logger);
4406
+ this.relayEvents(this.notifications);
4407
+ this.relayEvents(this.pdf);
4408
+ this.relayEvents(this.products);
4409
+ this.relayEvents(this.routes);
4410
+ this.relayEvents(this.schemas);
4411
+ this.relayEvents(this.settings);
4412
+ this.relayEvents(this.sms);
4413
+ this.relayEvents(this.static);
4414
+ this.relayEvents(this.storage);
4415
+ this.relayEvents(this.templates);
4416
+ this.relayEvents(this.tokens);
4417
+ this.relayEvents(this.transactions);
4418
+ this.relayEvents(this.users);
4419
+ this.relayEvents(this.webRtc);
4420
+ this.users.on(`*`, () => {
4421
+ if (!this.auth.user) return;
4422
+ const cached = this.users.cache.get(this.auth.user._id);
4423
+ if (cached) this.auth.user = cached;
4424
+ });
4425
+ if (this.opts.worker !== false && "serviceWorker" in navigator) {
4426
+ navigator.serviceWorker.register(this.opts.worker, { type: "module" }).catch(() => console.warn("Unable to load momentum worker, some features may be limited."));
4427
+ }
4428
+ }
4429
+ }
4430
+ exports2.ActionType = ActionType;
4431
+ exports2.Actions = Actions;
4432
+ exports2.Ai = Ai;
4433
+ exports2.Analytics = Analytics;
4434
+ exports2.Api = Api;
4435
+ exports2.Audit = Audit;
4436
+ exports2.Auth = Auth;
4437
+ exports2.Blacklist = Blacklist;
4438
+ exports2.Call = Call;
4439
+ exports2.Captcha = Captcha;
4440
+ exports2.Client = Client;
4441
+ exports2.Data = Data;
4442
+ exports2.Discounts = Discounts;
4443
+ exports2.Email = Email;
4444
+ exports2.Forms = Forms;
4445
+ exports2.Groups = Groups;
4446
+ exports2.Logger = Logger;
4447
+ exports2.Momentum = Momentum;
4448
+ exports2.Notifications = Notifications;
4449
+ exports2.PE = PE;
4450
+ exports2.PES = PES;
4451
+ exports2.PathEvent = PathEvent;
4452
+ exports2.PathEventEmitter = PathEventEmitter;
4453
+ exports2.Pdf = Pdf;
4454
+ exports2.Permissions = Permissions;
4455
+ exports2.Products = Products;
4456
+ exports2.RateLimiter = RateLimiter;
4457
+ exports2.Routes = Routes;
4458
+ exports2.Schemas = Schemas;
4459
+ exports2.Settings = Settings;
4460
+ exports2.Sms = Sms;
4461
+ exports2.Socket = Socket;
4462
+ exports2.Static = Static;
4463
+ exports2.Storage = Storage;
4464
+ exports2.Templates = Templates;
4465
+ exports2.Tokens = Tokens;
4466
+ exports2.Totp = Totp;
4467
+ exports2.Transactions = Transactions;
4468
+ exports2.Users = Users;
4469
+ exports2.WebRtc = WebRtc;
4470
+ exports2.treeBuilder = treeBuilder;
4471
+ exports2.validEvent = validEvent;
4472
+ Object.defineProperty(exports2, Symbol.toStringTag, { value: "Module" });
4473
+ }));