mosquito-transport-js 0.2.9 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mosquito-transport-js",
3
- "version": "0.2.9",
3
+ "version": "0.3.1",
4
4
  "description": "Javascript web sdk for mosquito-transport (https://github.com/deflexable/mosquito-transport)",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -32,17 +32,17 @@
32
32
  "bson": "^6.8.0",
33
33
  "buffer": "^6.0.3",
34
34
  "crypto-js": "^4.2.0",
35
- "guard-object": "^1.1.0",
35
+ "guard-object": "^1.1.3",
36
+ "limit-task": "1.0.0",
36
37
  "json-buffer": "^3.0.1",
37
38
  "lodash.clonedeep": "^4.5.0",
38
39
  "lodash.get": "^4.4.2",
39
40
  "lodash.set": "^4.3.2",
40
41
  "lodash.unset": "^4.5.2",
41
- "set-large-timeout": "^1.0.1",
42
42
  "simplify-error": "^1.0.1",
43
43
  "socket.io-client": "^4.6.2",
44
44
  "subscription-listener": "^1.1.2",
45
- "tweetnacl": "^1.0.3"
45
+ "tweetnacl-functional": "^1.0.4"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@babel/core": "^7.24.6",
@@ -0,0 +1,109 @@
1
+ import LimitTask from "limit-task";
2
+ import naclPkg from 'tweetnacl-functional';
3
+
4
+ function e2e_baseCode() {
5
+ const serializeE2E = (data, serverPublicKey) => {
6
+ const pair = box.keyPair(),
7
+ nonce = randomBytes(box.nonceLength);
8
+
9
+ return {
10
+ data: box(data, nonce, serverPublicKey, pair.secretKey),
11
+ pair,
12
+ nonce
13
+ };
14
+ };
15
+
16
+ const deserializeE2E = (data, nonce, serverPublicKey, clientPrivateKey) => {
17
+ const result = box.open(
18
+ data,
19
+ nonce,
20
+ serverPublicKey,
21
+ clientPrivateKey
22
+ );
23
+
24
+ if (!result) throw 'Decrypting e2e message failed';
25
+ return result;
26
+ };
27
+
28
+ self.onmessage = function (event) {
29
+ const { data } = event;
30
+
31
+ try {
32
+ let response;
33
+ if (data.type === 'encrypt') {
34
+ response = serializeE2E(...data.params);
35
+ } else if (data.type === 'decrypt') {
36
+ response = deserializeE2E(...data.params);
37
+ }
38
+ self.postMessage([response, data.session]);
39
+ } catch (error) {
40
+ self.postMessage([undefined, undefined, { error }]);
41
+ }
42
+ };
43
+ }
44
+
45
+ const workerCode = `
46
+ ${naclPkg.NACL.toString()}
47
+ const naclPkg = {};
48
+
49
+ NACL(naclPkg);
50
+
51
+ const { box, randomBytes } = naclPkg;
52
+
53
+ ${e2e_baseCode.toString()}
54
+ e2e_baseCode();
55
+ `;
56
+
57
+ const spawnWorker = () => {
58
+ const resolution = {};
59
+ let ite = 0;
60
+ const blob = new Blob([workerCode], { type: 'application/javascript' });
61
+ const worker = new Worker(URL.createObjectURL(blob));
62
+
63
+ // Receive the result from the worker
64
+ worker.onmessage = function (event) {
65
+ const [response, session, error] = event.data;
66
+ if (error) {
67
+ resolution[session][1](error.error);
68
+ } else {
69
+ resolution[session][0](response);
70
+ }
71
+ delete resolution[session];
72
+ };
73
+
74
+ const queue = LimitTask(3);
75
+
76
+ return (type, params) => queue(() =>
77
+ new Promise((resolve, reject) => {
78
+ const session = `${++ite}`;
79
+
80
+ resolution[session] = [resolve, reject];
81
+ worker.postMessage({ type, params, session });
82
+ })
83
+ );
84
+ };
85
+
86
+ let currentTask = -1;
87
+ const e2e_engines = [];
88
+
89
+ const MAX_WORKERS = 7;
90
+
91
+ const addTask = (type, params) => {
92
+ ++currentTask;
93
+ if (currentTask >= e2e_engines.length) {
94
+ if (e2e_engines.length < MAX_WORKERS) {
95
+ // spawn new engine
96
+ e2e_engines.push(spawnWorker());
97
+ } else currentTask = 0;
98
+ }
99
+ return e2e_engines[currentTask](type, params);
100
+ }
101
+
102
+ export default {
103
+ encrypt: (bufferData, serverPublicKey) => {
104
+ return addTask('encrypt', [bufferData, serverPublicKey]);
105
+ },
106
+ decrypt: (data, nonce, serverPublicKey, clientPrivateKey) => {
107
+ return addTask('decrypt', [data, nonce, serverPublicKey, clientPrivateKey]);
108
+ }
109
+ };
@@ -2,8 +2,9 @@ import { Buffer } from "buffer";
2
2
  import { ServerReachableListener } from "./listeners";
3
3
  import aes_pkg from 'crypto-js/aes.js';
4
4
  import Utf8Encoder from 'crypto-js/enc-utf8.js';
5
- import naclPkg from 'tweetnacl';
5
+ import naclPkg from 'tweetnacl-functional';
6
6
  import getLodash from "lodash.get";
7
+ import e2e_worker from "./e2e_worker";
7
8
 
8
9
  const { encrypt, decrypt } = aes_pkg;
9
10
  const { box, randomBytes } = naclPkg;
@@ -36,6 +37,10 @@ export const niceTry = (promise) => new Promise(async resolve => {
36
37
  } catch (e) { resolve(); }
37
38
  });
38
39
 
40
+ export const normalizeRoute = (route = '') => route.split('').map((v, i, a) =>
41
+ ((!i && v === '/') || (i === a.length - 1 && v === '/') || (i && a[i - 1] === '/' && v === '/')) ? '' : v
42
+ ).join('');
43
+
39
44
  export const shuffleArray = (n) => {
40
45
  const array = [...n];
41
46
  let currentIndex = array.length, randomIndex;
@@ -99,6 +104,20 @@ export const decryptString = (txt, password, iv) => {
99
104
  };
100
105
 
101
106
  export const serializeE2E = (data, auth_token, serverPublicKey) => {
107
+ const inputData = new Uint8Array(Buffer.from(JSON.stringify([data, auth_token]), 'utf8'));
108
+
109
+ if (inputData.byteLength > 10240) {
110
+ // dispatch to background thread
111
+ const { data, pair, nonce } = e2e_worker.encrypt(inputData, serverPublicKey);
112
+ const pubBase64 = Buffer.from(pair.publicKey).toString('base64'),
113
+ nonceBase64 = Buffer.from(nonce).toString('base64');
114
+
115
+ return [
116
+ `${pubBase64}.${nonceBase64}.${Buffer.from(data).toString('base64')}`,
117
+ [pair.secretKey, pair.publicKey]
118
+ ];
119
+ }
120
+
102
121
  const pair = box.keyPair(),
103
122
  nonce = randomBytes(box.nonceLength),
104
123
  pubBase64 = Buffer.from(pair.publicKey).toString('base64'),
@@ -107,12 +126,9 @@ export const serializeE2E = (data, auth_token, serverPublicKey) => {
107
126
  return [
108
127
  `${pubBase64}.${nonceBase64}.${Buffer.from(
109
128
  box(
110
- Buffer.from(JSON.stringify([
111
- data,
112
- auth_token
113
- ]), 'utf8'),
129
+ inputData,
114
130
  nonce,
115
- Buffer.from(serverPublicKey, 'base64'),
131
+ serverPublicKey,
116
132
  pair.secretKey
117
133
  )
118
134
  ).toString('base64')}`,
@@ -120,14 +136,16 @@ export const serializeE2E = (data, auth_token, serverPublicKey) => {
120
136
  ];
121
137
  };
122
138
 
123
- export const deserializeE2E = (data, serverPublicKey, clientPrivateKey) => {
124
- const [binaryNonce, binaryData] = data.split('.'),
125
- baseArray = box.open(
126
- Buffer.from(binaryData, 'base64'),
127
- Buffer.from(binaryNonce, 'base64'),
128
- Buffer.from(serverPublicKey, 'base64'),
129
- clientPrivateKey
130
- );
139
+ export const deserializeE2E = (data = '', serverPublicKey, clientPrivateKey) => {
140
+ const [binaryNonce, binaryData] = data.split('.').map(v => new Uint8Array(Buffer.from(v, 'base64')));
141
+ let baseArray;
142
+
143
+ if (binaryData.byteLength > 10240) {
144
+ // dispatch to background thread
145
+ baseArray = e2e_worker.decrypt(binaryData, binaryNonce, serverPublicKey, clientPrivateKey);
146
+ } else {
147
+ baseArray = box.open(binaryData, binaryNonce, serverPublicKey, clientPrivateKey);
148
+ }
131
149
 
132
150
  if (!baseArray) throw 'Decrypting e2e message failed';
133
151
  return JSON.parse(Buffer.from(baseArray).toString('utf8'))[0];
package/src/index.d.ts CHANGED
@@ -380,7 +380,7 @@ interface MTAuth {
380
380
  getAuthToken: () => Promise<string>;
381
381
  getRefreshToken: () => Promise<string>;
382
382
  getRefreshTokenData: () => Promise<RefreshTokenData>;
383
- parseToken: () => string;
383
+ parseToken: (token: string) => AuthData | RefreshTokenData;
384
384
  listenAuth: (callback: (auth: TokenEventData) => void) => () => void;
385
385
  getAuth: () => Promise<TokenEventData>;
386
386
  signOut: () => Promise<void>;
package/src/index.js CHANGED
@@ -5,7 +5,7 @@ import { MTAuth } from "./products/auth";
5
5
  import { MTCollection, batchWrite, trySendPendingWrite } from "./products/database";
6
6
  import { MTStorage } from "./products/storage";
7
7
  import { ServerReachableListener, TokenRefreshListener } from "./helpers/listeners";
8
- import { initTokenRefresher, listenToken, listenTokenReady, triggerAuthToken } from "./products/auth/accessor";
8
+ import { initTokenRefresher, listenToken, listenTokenReady, parseToken, triggerAuthToken } from "./products/auth/accessor";
9
9
  import { TIMESTAMP, DOCUMENT_EXTRACTION, FIND_GEO_JSON, GEO_JSON } from "./products/database/types";
10
10
  import { mfetch } from "./products/http_callable";
11
11
  import { io } from "socket.io-client";
@@ -24,11 +24,22 @@ const {
24
24
  _listenUserVerification
25
25
  } = EngineApi;
26
26
 
27
+ // https://socket.io/docs/v3/emit-cheatsheet/#reserved-events
28
+ const reservedEventName = [
29
+ 'connect',
30
+ 'connect_error',
31
+ 'disconnect',
32
+ 'disconnecting',
33
+ 'newListener',
34
+ 'removeListener'
35
+ ];
36
+
27
37
  export class MosquitoTransport {
28
38
  constructor(config) {
29
39
  validateMTConfig(config, this);
30
40
  this.config = {
31
41
  ...config,
42
+ serverE2E_PublicKey: config.serverE2E_PublicKey && new Uint8Array(Buffer.from(config.serverE2E_PublicKey, 'base64')),
32
43
  castBSON: config.castBSON === undefined || config.castBSON,
33
44
  maxRetries: config.maxRetries || 3,
34
45
  uglify: config.enableE2E_Encryption
@@ -166,7 +177,12 @@ export class MosquitoTransport {
166
177
  tokenListener,
167
178
  clientPrivateKey;
168
179
 
169
- const listenerCallback = (callback) => function () {
180
+ const listenerCallback = (route, callback) => function () {
181
+ if (reservedEventName.includes(route)) {
182
+ callback?.(...[...arguments]);
183
+ return;
184
+ }
185
+
170
186
  const [args, emitable] = [...arguments];
171
187
  let res;
172
188
 
@@ -307,7 +323,7 @@ export class MosquitoTransport {
307
323
  if (restrictedRoute.includes(route))
308
324
  throw `${route} is a restricted socket path, avoid using any of ${restrictedRoute}`;
309
325
  const ref = ++socketListenerIte,
310
- listener = listenerCallback(callback);
326
+ listener = listenerCallback(route, callback);
311
327
 
312
328
  socketListenerList.push([ref, 'on', route, listener]);
313
329
  if (socket) socket.on(route, listener);
@@ -321,7 +337,7 @@ export class MosquitoTransport {
321
337
  if (restrictedRoute.includes(route))
322
338
  throw `${route} is a restricted socket path, avoid using any of ${restrictedRoute}`;
323
339
  const ref = ++socketListenerIte,
324
- listener = listenerCallback(callback);
340
+ listener = listenerCallback(route, callback);
325
341
 
326
342
  socketListenerList.push([ref, 'once', route, listener]);
327
343
  if (socket) socket.once(route, listener);
@@ -1,7 +1,7 @@
1
1
  import { niceHash, shuffleArray, sortArrayByObjectKey } from "../../helpers/peripherals";
2
2
  import { awaitStore, updateCacheStore } from "../../helpers/utils";
3
3
  import { CacheStore, Scoped } from "../../helpers/variables";
4
- import { CompareBson, confirmFilterDoc, defaultBSON, downcastBSON, validateCollectionName, validateFilter } from "./validator";
4
+ import { assignExtractionFind, CompareBson, confirmFilterDoc, defaultBSON, downcastBSON, validateCollectionName, validateFilter } from "./validator";
5
5
  import getLodash from 'lodash.get';
6
6
  import setLodash from 'lodash.set';
7
7
  import unsetLodash from 'lodash.unset';
@@ -48,7 +48,7 @@ export const insertRecord = async (builder, config, accessId, value) => {
48
48
 
49
49
  await awaitStore();
50
50
  const { projectUrl, dbUrl, dbName, path, command } = builder;
51
- const entityId = generateRecordID({}, config);
51
+ const entityId = await generateRecordID({}, config);
52
52
  const colData = getLodash(CacheStore.DatabaseStore, [projectUrl, dbUrl, dbName, path, 'data', entityId], { config, command, listing: [] });
53
53
  const trackedData = getLodash(CacheStore.DatabaseStore, [projectUrl, dbUrl, dbName, path, 'record', accessId]);
54
54
 
@@ -106,7 +106,7 @@ export const getRecord = async (builder, config, accessId) => {
106
106
  await awaitStore();
107
107
  const { projectUrl, dbUrl, dbName, path, command } = builder;
108
108
  const { find, findOne, sort, direction, limit, random } = command;
109
- const entityId = generateRecordID({}, config);
109
+ const entityId = await generateRecordID({}, config);
110
110
  const colData = getLodash(CacheStore.DatabaseStore, [projectUrl, dbUrl, dbName, path, 'data', entityId]);
111
111
  const colRecord = getLodash(CacheStore.DatabaseStore, [projectUrl, dbUrl, dbName, path, 'record', accessId]);
112
112
 
@@ -193,16 +193,18 @@ const recurseNonAtomicWrite = (obj, i, type) => {
193
193
  if (k === '_id') throw `avoid providing "_id" for ${type}() operation as _id only reference a single document`;
194
194
  if (k === '_foreign_doc') throw '"_foreign_doc" is readonly';
195
195
  }
196
- if (k.includes('$') || k.includes('.'))
197
- throw `invalid property "${k}", ${type}() operation fields must not contain .$`;
198
- if (Validator.OBJECT(v)) recurseNonAtomicWrite(obj, i + 1, type);
196
+ if (k.includes('$') || k.includes('.')) {
197
+ if (!(k === '$timestamp' && v === 'now'))
198
+ throw `invalid property "${k}", ${type}() operation fields must not contain .$`;
199
+ }
200
+ if (Validator.OBJECT(v)) recurseNonAtomicWrite(v, i + 1, type);
199
201
  });
200
202
  };
201
203
 
202
204
  const recurseAtomicWrite = (obj, i, type) => {
203
205
  if (!Validator.OBJECT(obj)) throw `expected a document but got ${obj}`;
204
- Object.entries(obj).forEach(([k]) => {
205
- if (!(k in AtomicWriter)) throw `Unknown update operator: ${k}`;
206
+ Object.entries(obj).forEach(([k, v]) => {
207
+ if (!i && !(k in AtomicWriter)) throw `Unknown update operator: ${k}`;
206
208
  if (i === 1) {
207
209
  if ((k === '_id' || k.startsWith('_id.')))
208
210
  throw `avoid providing "_id" for ${type}() operation as _id only reference a single document`;
@@ -211,7 +213,7 @@ const recurseAtomicWrite = (obj, i, type) => {
211
213
  throw '"_foreign_doc" is readonly';
212
214
  }
213
215
  if (k.includes('.$')) throw `unsupported operation at "${k}"`;
214
- if (!i) recurseAtomicWrite(obj, i + 1, type);
216
+ if (!i || Validator.OBJECT(v)) recurseAtomicWrite(v, i + 1, type);
215
217
  });
216
218
  };
217
219
 
@@ -299,7 +301,7 @@ export const syncCache = (builder, result) => {
299
301
  result.value.map(({ scope, value, find, path }) =>
300
302
  ({ type: scope, value, find, path })
301
303
  )
302
- : { ...result, path: builder.path }
304
+ : [{ ...result, path: builder.path }]
303
305
  ).forEach(({ value: writeObj, find, type, path }) => {
304
306
  WriteValidator[type]({ find, value: writeObj });
305
307
  validateCollectionName(path);
@@ -600,22 +602,6 @@ const getFindFields = (find) => {
600
602
  return result.filter((v, i, a) => a.findIndex(b => b === v) === i);
601
603
  };
602
604
 
603
- const assignExtractionFind = (data, find) => {
604
- if (!find) return find;
605
-
606
- if (niceGuard({ $dynamicValue: GuardSignal.NON_EMPTY_STRING }, find)) {
607
- return getLodash(data, find.$dynamicValue) || null;
608
- } else if (Validator.OBJECT(find)) {
609
- return Object.fromEntries(
610
- Object.entries(find).map(([k, v]) =>
611
- Validator.JSON(v) ? [k, assignExtractionFind(data, v)] : [k, v]
612
- )
613
- );
614
- } else if (Array.isArray(find)) {
615
- return find.map(v => assignExtractionFind(data, v));
616
- } else return find;
617
- };
618
-
619
605
  const deserializeWriteValue = (value) => {
620
606
  if (!value) return value;
621
607
 
@@ -133,9 +133,9 @@ const listenDocument = (callback, onError, builder, config) => {
133
133
  const { projectUrl, wsPrefix, serverE2E_PublicKey, baseUrl, dbUrl, dbName, accessKey, path, disableCache, command, uglify, castBSON } = builder;
134
134
  const { find, findOne, sort, direction, limit } = command;
135
135
  const { disableAuth } = config || {};
136
- const accessId = generateRecordID(builder, config);
137
136
  const shouldCache = !disableCache;
138
137
  const processId = `${++Scoped.AnyProcessIte}`;
138
+ let accessId;
139
139
 
140
140
  validateListenFindConfig(config);
141
141
  validateFilter(findOne || find);
@@ -159,9 +159,13 @@ const listenDocument = (callback, onError, builder, config) => {
159
159
  };
160
160
 
161
161
  if (shouldCache) {
162
- cacheListener = listenQueryEntry(snapshot => {
163
- if (!Scoped.IS_CONNECTED[projectUrl]) dispatchSnapshot(snapshot);
164
- }, { accessId, builder, config, processId });
162
+ accessId = generateRecordID(builder, config).then(hash => {
163
+ if (hasCancelled) return hash;
164
+ cacheListener = listenQueryEntry(snapshot => {
165
+ if (!Scoped.IS_CONNECTED[projectUrl]) dispatchSnapshot(snapshot);
166
+ }, { accessId: hash, builder, config, processId });
167
+ return hash;
168
+ });
165
169
 
166
170
  awaitStore().then(() => {
167
171
  if (hasCancelled) return;
@@ -210,13 +214,15 @@ const listenDocument = (callback, onError, builder, config) => {
210
214
  socket.on('mSnapshot', async ([err, snapshot]) => {
211
215
  hasRespond = true;
212
216
  if (err) {
213
- onError?.(simplifyCaughtError(err).simpleError);
217
+ if (typeof onError === 'function') {
218
+ onError(simplifyCaughtError(err).simpleError);
219
+ } else console.error('unhandled listen for:', { path, find }, ' error:', err);
214
220
  } else {
215
221
  if (uglify) snapshot = deserializeE2E(snapshot, serverE2E_PublicKey, privateKey);
216
222
  snapshot = deserializeBSON(snapshot)._;
217
223
  dispatchSnapshot(snapshot);
218
224
 
219
- if (shouldCache) insertRecord(builder, config, accessId, snapshot);
225
+ if (shouldCache) insertRecord(builder, config, await accessId, snapshot);
220
226
  }
221
227
  });
222
228
 
@@ -284,7 +290,7 @@ const initOnDisconnectionTask = (builder, value, type) => {
284
290
  socket = io(`${wsPrefix}://${baseUrl}`, {
285
291
  transports: ['websocket', 'polling', 'flashsocket'],
286
292
  auth: uglify ? {
287
- e2e: serializeE2E(authObj, mtoken, serverE2E_PublicKey)[0],
293
+ e2e: serializeE2E({ accessKey, _body: authObj }, mtoken, serverE2E_PublicKey)[0],
288
294
  _m_internal: true
289
295
  } : {
290
296
  ...mtoken ? { mtoken } : {},
@@ -334,7 +340,7 @@ const countCollection = async (builder, config) => {
334
340
  const { projectUrl, serverE2E_PublicKey, dbUrl, dbName, accessKey, maxRetries = 7, uglify, path, disableCache, command = {} } = builder;
335
341
  const { find } = command;
336
342
  const { disableAuth } = config || {};
337
- const accessId = generateRecordID({ ...builder, countDoc: true }, config);
343
+ const accessId = await generateRecordID({ ...builder, countDoc: true }, config);
338
344
 
339
345
  await awaitStore();
340
346
  if (config !== undefined)
@@ -424,7 +430,7 @@ const findObject = async (builder, config) => {
424
430
  const { find, findOne, sort, direction, limit, random } = command;
425
431
  const { retrieval = RETRIEVAL.DEFAULT, episode = 0, disableAuth, disableMinimizer } = config || {};
426
432
  const enableMinimizer = !disableMinimizer;
427
- const accessId = generateRecordID(builder, config);
433
+ const accessId = await generateRecordID(builder, config);
428
434
  const processAccessId = `${accessId}${projectUrl}${dbUrl}${dbName}${retrieval}`;
429
435
  const getRecordData = () => getRecord(builder, config, accessId);
430
436
  const shouldCache = (retrieval !== RETRIEVAL.DEFAULT || !disableCache) &&
@@ -6,10 +6,15 @@ import { Binary, BSONRegExp, BSONSymbol, Code, DBRef, Decimal128, Double, Int32,
6
6
  import { bboxPolygon, booleanIntersects, booleanWithin, circle, distance, polygon } from "@turf/turf";
7
7
 
8
8
  const DirectionList = [1, -1, 'asc', 'desc', 'ascending', 'descending'];
9
- const FilterFootPrint = t => validateFilter(t);
9
+ const FilterFootPrint = t => {
10
+ validateFilter(t);
11
+ return true;
12
+ };
10
13
  const ReturnAndExcludeFootprint = t => t === undefined ||
11
14
  !(Array.isArray(t) ? t : [t]).filter(v => !Validator.TRIMMED_NON_EMPTY_STRING(v)).length;
12
15
 
16
+ const ConfigFind = t => t && FilterFootPrint(assignExtractionFind({}, t));
17
+
13
18
  const FindConfig = {
14
19
  extraction: t => t === undefined ||
15
20
  (Array.isArray(t) ? t : [t]).filter(m =>
@@ -18,8 +23,8 @@ const FindConfig = {
18
23
  sort: (t, p) => t === undefined || (Validator.TRIMMED_NON_EMPTY_STRING(t) && p.find),
19
24
  direction: (t, p) => t === undefined || (p.sort && p.find && DirectionList.includes(t)),
20
25
  limit: (t, p) => t === undefined || (Validator.POSITIVE_INTEGER(t) && p.find),
21
- find: (t, p) => (t === undefined && p.findOne) || (!p.findOne && FilterFootPrint(t)),
22
- findOne: (t, p) => (t === undefined && p.find) || (!p.find && FilterFootPrint(t)),
26
+ find: (t, p) => (t === undefined && p.findOne) || (!p.findOne && ConfigFind(t)),
27
+ findOne: (t, p) => (t === undefined && p.find) || (!p.find && ConfigFind(t)),
23
28
  returnOnly: ReturnAndExcludeFootprint,
24
29
  excludeFields: ReturnAndExcludeFootprint
25
30
  }).validate(m)
@@ -41,7 +46,8 @@ export const validateListenFindConfig = (config) => config === undefined ||
41
46
  extraction: FindConfig.extraction,
42
47
  returnOnly: FindConfig.returnOnly,
43
48
  excludeFields: FindConfig.excludeFields,
44
- disableAuth: FindConfig.disableAuth
49
+ disableAuth: FindConfig.disableAuth,
50
+ episode: t => [undefined, 0, 1].includes(t)
45
51
  }).validate(config);
46
52
 
47
53
  export const validateFindObject = command =>
@@ -55,6 +61,22 @@ export const validateFindObject = command =>
55
61
  random: (t, p) => t === undefined || (!p.sort && t === true),
56
62
  }).validate({ ...command });
57
63
 
64
+ export const assignExtractionFind = (data, find) => {
65
+ if (!find) return find;
66
+
67
+ if (niceGuard({ $dynamicValue: GuardSignal.NON_EMPTY_STRING }, find)) {
68
+ return getLodash(data, find.$dynamicValue) || null;
69
+ } else if (Validator.OBJECT(find)) {
70
+ return Object.fromEntries(
71
+ Object.entries(find).map(([k, v]) =>
72
+ Validator.JSON(v) ? [k, assignExtractionFind(data, v)] : [k, v]
73
+ )
74
+ );
75
+ } else if (Array.isArray(find)) {
76
+ return find.map(v => assignExtractionFind(data, v));
77
+ } else return find;
78
+ };
79
+
58
80
  export const validateCollectionName = collectionName => {
59
81
  // Check if the collection name is empty
60
82
  if (!collectionName || typeof collectionName !== 'string')
@@ -1,5 +1,5 @@
1
1
  import { Buffer } from "buffer";
2
- import { deserializeE2E, listenReachableServer, niceHash, serializeE2E } from "../../helpers/peripherals";
2
+ import { deserializeE2E, listenReachableServer, niceHash, normalizeRoute, serializeE2E } from "../../helpers/peripherals";
3
3
  import { awaitStore, getReachableServer, updateCacheStore } from "../../helpers/utils";
4
4
  import { RETRIEVAL } from "../../helpers/values";
5
5
  import { CacheStore, Scoped } from "../../helpers/variables";
@@ -7,6 +7,7 @@ import { awaitRefreshToken } from "../auth/accessor";
7
7
  import { simplifyCaughtError } from "simplify-error";
8
8
  import { guardObject, Validator } from "guard-object";
9
9
  import cloneDeep from "lodash.clonedeep";
10
+ import { stringify } from "json-buffer";
10
11
 
11
12
  const buildFetchData = (data) => {
12
13
  const { ok, type, status, statusText, redirected, url, headers, size, base64 } = data;
@@ -35,7 +36,7 @@ export const mfetch = async (input = '', init, config) => {
35
36
  const { projectUrl, serverE2E_PublicKey, method, maxRetries = 7, disableCache, accessKey, uglify } = config;
36
37
  const { headers, body } = init || {};
37
38
 
38
- if (config !== undefined)
39
+ if (method !== undefined)
39
40
  guardObject({
40
41
  enableMinimizer: t => t === undefined || Validator.BOOLEAN(t),
41
42
  rawApproach: t => t === undefined || Validator.BOOLEAN(t),
@@ -48,6 +49,8 @@ export const mfetch = async (input = '', init, config) => {
48
49
  const disableAuth = method?.disableAuth || isBaseUrl;
49
50
  const shouldCache = (retrieval !== RETRIEVAL.DEFAULT || !disableCache) &&
50
51
  retrieval !== RETRIEVAL.NO_CACHE_NO_AWAIT;
52
+ const uglified = !!(!isBaseUrl && uglify);
53
+
51
54
  const rawHeader = Object.fromEntries(
52
55
  [...new Headers(headers).entries()]
53
56
  );
@@ -58,16 +61,25 @@ export const mfetch = async (input = '', init, config) => {
58
61
  if ('uglified' in rawHeader)
59
62
  throw '"uglified" in header is a reserved prop';
60
63
 
61
- if (input.startsWith(projectUrl) && !rawApproach)
64
+ if (!input.startsWith(projectUrl) && !rawApproach)
62
65
  throw `please set { rawApproach: true } if you're trying to access different endpoint at "${input}"`;
63
66
 
64
- if (body !== undefined && typeof body !== 'string')
65
- throw `"body" must be a string value`;
67
+ if (body !== undefined) {
68
+ if (
69
+ typeof body !== 'string' &&
70
+ !Buffer.isBuffer(body) &&
71
+ !Validator.JSON(body) &&
72
+ !(body instanceof File) &&
73
+ !(body instanceof Blob)
74
+ ) throw `"body" must be any of string, buffer, object, File, Blob`;
75
+ }
76
+
77
+ const rawBody = stringify([(body instanceof File || body instanceof Blob) ? await body.arrayBuffer() : body]);
66
78
 
67
- const reqId = niceHash(
79
+ const reqId = await niceHash(
68
80
  JSON.stringify([
69
81
  rawHeader,
70
- body,
82
+ rawBody,
71
83
  !!disableAuth,
72
84
  input
73
85
  ])
@@ -131,19 +143,18 @@ export const mfetch = async (input = '', init, config) => {
131
143
  await awaitRefreshToken(projectUrl);
132
144
 
133
145
  const mtoken = Scoped.AuthJWTToken[projectUrl];
134
- const uglified = !!(!isBaseUrl && body && uglify);
135
146
  const initType = rawHeader['content-type'];
136
147
 
137
- const [reqBuilder, [privateKey]] = uglified ? serializeE2E(body, mtoken, serverE2E_PublicKey) : [null, []];
148
+ const [reqBuilder, [privateKey]] = uglified ? serializeE2E(rawBody, mtoken, serverE2E_PublicKey) : [null, []];
138
149
 
139
- const f = await fetch(isBaseUrl ? input : `${projectUrl}/${input}`, {
150
+ const f = await fetch(isBaseUrl ? input : `${projectUrl}/${normalizeRoute(input)}`, {
140
151
  ...isBaseUrl ? {} : { method: 'POST' },
141
152
  ...init,
142
153
  ...uglified ? { body: reqBuilder } : {},
143
154
  cache: 'no-cache',
144
155
  headers: {
145
- ...isBaseUrl ? {} : { 'Content-type': 'application/json' },
146
- ...headers,
156
+ ...isBaseUrl ? {} : { 'content-type': 'application/json' },
157
+ ...rawHeader,
147
158
  ...uglified ? {
148
159
  uglified,
149
160
  'content-type': 'text/plain',
@@ -1,9 +1,10 @@
1
1
  import EngineApi from "../../helpers/engine_api";
2
2
  import { encodeBinary } from "../../helpers/peripherals";
3
3
  import { Scoped } from "../../helpers/variables";
4
- import { awaitReachableServer, buildFetchInterface, simplifyError } from "../../helpers/utils";
4
+ import { awaitReachableServer, buildFetchInterface } from "../../helpers/utils";
5
5
  import { awaitRefreshToken } from "../auth/accessor";
6
6
  import { Buffer } from "buffer";
7
+ import { simplifyError } from "simplify-error";
7
8
 
8
9
  export class MTStorage {
9
10
  constructor(config) {