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