mosquito-transport-js 0.3.0 → 0.3.2

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.3.0",
3
+ "version": "0.3.2",
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.2",
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;
@@ -98,7 +103,21 @@ export const decryptString = (txt, password, iv) => {
98
103
  return decrypt(txt, `${password || ''}${iv || ''}`).toString(Utf8Encoder);
99
104
  };
100
105
 
101
- export const serializeE2E = (data, auth_token, serverPublicKey) => {
106
+ export const serializeE2E = async (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 = async (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];
@@ -116,9 +116,9 @@ export const getReachableServer = (projectUrl) => new Promise(resolve => {
116
116
  }, true);
117
117
  });
118
118
 
119
- export const buildFetchInterface = ({ body, accessKey, authToken, method, uglify, serverE2E_PublicKey }) => {
119
+ export const buildFetchInterface = async ({ body, accessKey, authToken, method, uglify, serverE2E_PublicKey }) => {
120
120
  if (!uglify) body = JSON.stringify({ ...body });
121
- const [plate, keyPair] = uglify ? serializeE2E(body, authToken, serverE2E_PublicKey) : [undefined, []];
121
+ const [plate, keyPair] = uglify ? await serializeE2E(body, authToken, serverE2E_PublicKey) : [undefined, []];
122
122
 
123
123
  return [{
124
124
  body: uglify ? plate : body,
package/src/index.d.ts CHANGED
@@ -115,7 +115,7 @@ interface MTSocket {
115
115
  emitWithAck: (...args: any) => Promise<any>;
116
116
  });
117
117
  emit: (...args: any) => void;
118
- emitWithAck: () => Promise<any>;
118
+ emitWithAck: (...args: any) => Promise<any>;
119
119
  on: (route: string, callback?: () => any) => void;
120
120
  once: (route: string, callback?: () => any) => void;
121
121
  destroy: () => void;
@@ -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
@@ -1,3 +1,4 @@
1
+ import './polyfill';
1
2
  import { deserializeE2E, listenReachableServer, serializeE2E } from "./helpers/peripherals";
2
3
  import { releaseCacheStore, awaitStore } from "./helpers/utils";
3
4
  import { CacheStore, Scoped } from "./helpers/variables";
@@ -5,7 +6,7 @@ import { MTAuth } from "./products/auth";
5
6
  import { MTCollection, batchWrite, trySendPendingWrite } from "./products/database";
6
7
  import { MTStorage } from "./products/storage";
7
8
  import { ServerReachableListener, TokenRefreshListener } from "./helpers/listeners";
8
- import { initTokenRefresher, listenToken, listenTokenReady, triggerAuthToken } from "./products/auth/accessor";
9
+ import { initTokenRefresher, listenToken, listenTokenReady, parseToken, triggerAuthToken } from "./products/auth/accessor";
9
10
  import { TIMESTAMP, DOCUMENT_EXTRACTION, FIND_GEO_JSON, GEO_JSON } from "./products/database/types";
10
11
  import { mfetch } from "./products/http_callable";
11
12
  import { io } from "socket.io-client";
@@ -15,6 +16,7 @@ import { parse, stringify } from 'json-buffer';
15
16
  import { Validator } from 'guard-object';
16
17
  import sendMessage from "./helpers/broadcaster";
17
18
  import cloneDeep from "lodash.clonedeep";
19
+ import { Buffer } from "buffer";
18
20
 
19
21
  const {
20
22
  _listenCollection,
@@ -24,11 +26,22 @@ const {
24
26
  _listenUserVerification
25
27
  } = EngineApi;
26
28
 
29
+ // https://socket.io/docs/v3/emit-cheatsheet/#reserved-events
30
+ const reservedEventName = [
31
+ 'connect',
32
+ 'connect_error',
33
+ 'disconnect',
34
+ 'disconnecting',
35
+ 'newListener',
36
+ 'removeListener'
37
+ ];
38
+
27
39
  export class MosquitoTransport {
28
40
  constructor(config) {
29
41
  validateMTConfig(config, this);
30
42
  this.config = {
31
43
  ...config,
44
+ serverE2E_PublicKey: config.serverE2E_PublicKey && new Uint8Array(Buffer.from(config.serverE2E_PublicKey, 'base64')),
32
45
  castBSON: config.castBSON === undefined || config.castBSON,
33
46
  maxRetries: config.maxRetries || 3,
34
47
  uglify: config.enableE2E_Encryption
@@ -166,20 +179,25 @@ export class MosquitoTransport {
166
179
  tokenListener,
167
180
  clientPrivateKey;
168
181
 
169
- const listenerCallback = (callback) => function () {
182
+ const listenerCallback = (route, callback) => async function () {
183
+ if (reservedEventName.includes(route)) {
184
+ callback?.(...[...arguments]);
185
+ return;
186
+ }
187
+
170
188
  const [args, emitable] = [...arguments];
171
189
  let res;
172
190
 
173
191
  if (uglify) {
174
- res = parse(deserializeE2E(args, serverE2E_PublicKey, clientPrivateKey));
192
+ res = parse(await deserializeE2E(args, serverE2E_PublicKey, clientPrivateKey));
175
193
  } else res = args;
176
194
 
177
- callback?.(...res || [], ...typeof emitable === 'function' ? [function () {
195
+ callback?.(...res || [], ...typeof emitable === 'function' ? [async function () {
178
196
  const args = [...arguments];
179
197
  let res;
180
198
 
181
199
  if (uglify) {
182
- res = serializeE2E(stringify(args), undefined, serverE2E_PublicKey)[0];
200
+ res = (await serializeE2E(stringify(args), undefined, serverE2E_PublicKey))[0];
183
201
  } else res = args;
184
202
 
185
203
  emitable(res);
@@ -213,26 +231,26 @@ export class MosquitoTransport {
213
231
  const hasEmitable = typeof lastEmit === 'function';
214
232
  const mit = hasEmitable ? emittion.slice(0, -1) : emittion;
215
233
 
216
- const [reqBuilder, [privateKey]] = uglify ? serializeE2E(stringify(mit), undefined, serverE2E_PublicKey) : [undefined, []];
234
+ const [reqBuilder, [privateKey]] = uglify ? await serializeE2E(stringify(mit), undefined, serverE2E_PublicKey) : [undefined, []];
217
235
 
218
236
  if (hasEmitable && promise)
219
237
  throw 'emitWithAck cannot have function in it argument';
220
238
 
221
239
  const result = await thisSocket[promise ? 'emitWithAck' : 'emit'](route,
222
240
  uglify ? reqBuilder : mit,
223
- ...hasEmitable ? [function () {
241
+ ...hasEmitable ? [async function () {
224
242
  const [args] = [...arguments];
225
243
  let res;
226
244
 
227
245
  if (uglify) {
228
- res = parse(deserializeE2E(args, serverE2E_PublicKey, privateKey));
246
+ res = parse(await deserializeE2E(args, serverE2E_PublicKey, privateKey));
229
247
  } else res = args;
230
248
 
231
249
  lastEmit(...res || []);
232
250
  }] : []
233
251
  );
234
252
 
235
- resolve((promise && result) ? uglify ? parse(deserializeE2E(result, serverE2E_PublicKey, privateKey))[0] : result[0] : undefined);
253
+ resolve((promise && result) ? uglify ? parse(await deserializeE2E(result, serverE2E_PublicKey, privateKey))[0] : result[0] : undefined);
236
254
  } catch (e) {
237
255
  reject(e);
238
256
  }
@@ -241,7 +259,7 @@ export class MosquitoTransport {
241
259
  const init = async () => {
242
260
  if (hasCancelled) return;
243
261
  const mtoken = disableAuth ? undefined : Scoped.AuthJWTToken[projectUrl];
244
- const [reqBuilder, [privateKey]] = uglify ? serializeE2E({ accessKey, a_extras: authHandshake }, mtoken, serverE2E_PublicKey) : [null, []];
262
+ const [reqBuilder, [privateKey]] = uglify ? await serializeE2E({ accessKey, a_extras: authHandshake }, mtoken, serverE2E_PublicKey) : [null, []];
245
263
 
246
264
  socket = io(`${wsPrefix}://${projectUrl.split('://')[1]}`, {
247
265
  transports: ['websocket', 'polling', 'flashsocket'],
@@ -307,7 +325,7 @@ export class MosquitoTransport {
307
325
  if (restrictedRoute.includes(route))
308
326
  throw `${route} is a restricted socket path, avoid using any of ${restrictedRoute}`;
309
327
  const ref = ++socketListenerIte,
310
- listener = listenerCallback(callback);
328
+ listener = listenerCallback(route, callback);
311
329
 
312
330
  socketListenerList.push([ref, 'on', route, listener]);
313
331
  if (socket) socket.on(route, listener);
@@ -321,7 +339,7 @@ export class MosquitoTransport {
321
339
  if (restrictedRoute.includes(route))
322
340
  throw `${route} is a restricted socket path, avoid using any of ${restrictedRoute}`;
323
341
  const ref = ++socketListenerIte,
324
- listener = listenerCallback(callback);
342
+ listener = listenerCallback(route, callback);
325
343
 
326
344
  socketListenerList.push([ref, 'once', route, listener]);
327
345
  if (socket) socket.once(route, listener);
@@ -0,0 +1,3 @@
1
+ import { Buffer } from "buffer";
2
+
3
+ if (!globalThis.Buffer) globalThis.Buffer = Buffer;
@@ -81,7 +81,7 @@ const refreshToken = (builder, processRef, remainRetries = 7, initialRetries = 7
81
81
  try {
82
82
  const { token, refreshToken: r_token } = CacheStore.AuthStore[projectUrl];
83
83
 
84
- const [reqBuilder, [privateKey]] = buildFetchInterface({
84
+ const [reqBuilder, [privateKey]] = await buildFetchInterface({
85
85
  body: { token, r_token },
86
86
  accessKey,
87
87
  uglify,
@@ -96,7 +96,7 @@ const refreshToken = (builder, processRef, remainRetries = 7, initialRetries = 7
96
96
  }
97
97
  if (r.simpleError) throw r;
98
98
 
99
- const f = uglify ? deserializeE2E(r.e2e, serverE2E_PublicKey, privateKey) : r;
99
+ const f = uglify ? await deserializeE2E(r.e2e, serverE2E_PublicKey, privateKey) : r;
100
100
 
101
101
  if (CacheStore.AuthStore[projectUrl]) {
102
102
  CacheStore.AuthStore[projectUrl].token = f.result.token;
@@ -61,7 +61,7 @@ export class MTAuth {
61
61
  }
62
62
  if (processID !== lastInitRef) return;
63
63
  const mtoken = Scoped.AuthJWTToken[projectUrl],
64
- [reqBuilder, [privateKey]] = uglify ? serializeE2E({ mtoken }, undefined, serverE2E_PublicKey) : [null, []];
64
+ [reqBuilder, [privateKey]] = uglify ? await serializeE2E({ mtoken }, undefined, serverE2E_PublicKey) : [null, []];
65
65
 
66
66
  socket = io(`${wsPrefix}://${baseUrl}`, {
67
67
  transports: ['websocket', 'polling', 'flashsocket'],
@@ -74,11 +74,11 @@ export class MTAuth {
74
74
 
75
75
  socket.emit(_listenUserVerification(uglify));
76
76
 
77
- socket.on("onVerificationChanged", ([err, verified]) => {
77
+ socket.on("onVerificationChanged", async ([err, verified]) => {
78
78
  if (err) {
79
79
  onError?.(simplifyCaughtError(err).simpleError);
80
80
  } else {
81
- callback?.(uglify ? deserializeE2E(verified, serverE2E_PublicKey, privateKey) : verified);
81
+ callback?.(uglify ? await deserializeE2E(verified, serverE2E_PublicKey, privateKey) : verified);
82
82
  }
83
83
  });
84
84
 
@@ -171,7 +171,7 @@ const doCustomSignin = (builder, email, password) => new Promise(async (resolve,
171
171
 
172
172
  try {
173
173
  await awaitStore();
174
- const [reqBuilder, [privateKey]] = buildFetchInterface({
174
+ const [reqBuilder, [privateKey]] = await buildFetchInterface({
175
175
  body: { data: `${encodeBinary(email)}.${encodeBinary(password)}` },
176
176
  accessKey,
177
177
  serverE2E_PublicKey,
@@ -181,7 +181,7 @@ const doCustomSignin = (builder, email, password) => new Promise(async (resolve,
181
181
  const f = await (await fetch(_customSignin(projectUrl, uglify), reqBuilder)).json();
182
182
  if (f.simpleError) throw f;
183
183
 
184
- const r = uglify ? deserializeE2E(f.e2e, serverE2E_PublicKey, privateKey) : f;
184
+ const r = uglify ? await deserializeE2E(f.e2e, serverE2E_PublicKey, privateKey) : f;
185
185
 
186
186
  resolve({
187
187
  user: parseToken(r.result.token),
@@ -199,7 +199,7 @@ const doCustomSignup = (builder, email, password, name, metadata) => new Promise
199
199
 
200
200
  try {
201
201
  await awaitStore();
202
- const [reqBuilder, [privateKey]] = buildFetchInterface({
202
+ const [reqBuilder, [privateKey]] = await buildFetchInterface({
203
203
  body: {
204
204
  data: `${encodeBinary(email)}.${encodeBinary(password)}.${(encodeBinary((name || '').trim()))}`,
205
205
  metadata,
@@ -212,7 +212,7 @@ const doCustomSignup = (builder, email, password, name, metadata) => new Promise
212
212
  const f = await (await fetch(_customSignup(projectUrl, uglify), reqBuilder)).json();
213
213
  if (f.simpleError) throw f;
214
214
 
215
- const r = uglify ? deserializeE2E(f.e2e, serverE2E_PublicKey, privateKey) : f;
215
+ const r = uglify ? await deserializeE2E(f.e2e, serverE2E_PublicKey, privateKey) : f;
216
216
 
217
217
  resolve({
218
218
  user: parseToken(r.result.token),
@@ -249,7 +249,7 @@ export const doSignOut = async (builder) => {
249
249
  try {
250
250
  await awaitReachableServer(projectUrl);
251
251
 
252
- const [reqBuilder] = buildFetchInterface({
252
+ const [reqBuilder] = await buildFetchInterface({
253
253
  body: { token, r_token },
254
254
  accessKey,
255
255
  uglify,
@@ -269,7 +269,7 @@ const doGoogleSignin = (builder, token) => new Promise(async (resolve, reject) =
269
269
 
270
270
  try {
271
271
  await awaitStore();
272
- const [reqBuilder, [privateKey]] = buildFetchInterface({
272
+ const [reqBuilder, [privateKey]] = await buildFetchInterface({
273
273
  body: { token },
274
274
  accessKey,
275
275
  uglify,
@@ -279,7 +279,7 @@ const doGoogleSignin = (builder, token) => new Promise(async (resolve, reject) =
279
279
  const r = await (await fetch(_googleSignin(projectUrl, uglify), reqBuilder)).json();
280
280
  if (r.simpleError) throw r;
281
281
 
282
- const f = uglify ? deserializeE2E(r.e2e, serverE2E_PublicKey, privateKey) : r;
282
+ const f = uglify ? await deserializeE2E(r.e2e, serverE2E_PublicKey, privateKey) : r;
283
283
 
284
284
  resolve({
285
285
  user: parseToken(f.result.token),
@@ -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;
@@ -193,7 +197,7 @@ const listenDocument = (callback, onError, builder, config) => {
193
197
  dbUrl
194
198
  };
195
199
 
196
- const [encPlate, [privateKey]] = uglify ? serializeE2E({ accessKey, _body: authObj }, mtoken, serverE2E_PublicKey) : ['', []];
200
+ const [encPlate, [privateKey]] = uglify ? await serializeE2E({ accessKey, _body: authObj }, mtoken, serverE2E_PublicKey) : ['', []];
197
201
 
198
202
  socket = io(`${wsPrefix}://${baseUrl}`, {
199
203
  transports: ['websocket', 'polling', 'flashsocket'],
@@ -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
- if (uglify) snapshot = deserializeE2E(snapshot, serverE2E_PublicKey, privateKey);
221
+ if (uglify) snapshot = await 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: (await 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)
@@ -359,7 +365,7 @@ const countCollection = async (builder, config) => {
359
365
  if (!disableAuth && await getReachableServer(projectUrl))
360
366
  await awaitRefreshToken(projectUrl);
361
367
 
362
- const [reqBuilder, [privateKey]] = buildFetchInterface({
368
+ const [reqBuilder, [privateKey]] = await buildFetchInterface({
363
369
  body: {
364
370
  commands: { path, find: serializeToBase64(find) },
365
371
  dbName,
@@ -374,7 +380,7 @@ const countCollection = async (builder, config) => {
374
380
  const r = await (await fetch(_documentCount(projectUrl, uglify), reqBuilder)).json();
375
381
  if (r.simpleError) throw r;
376
382
 
377
- const f = uglify ? deserializeE2E(r.e2e, serverE2E_PublicKey, privateKey) : r;
383
+ const f = uglify ? await deserializeE2E(r.e2e, serverE2E_PublicKey, privateKey) : r;
378
384
 
379
385
  if (!disableCache)
380
386
  setLodash(CacheStore.DatabaseCountResult, [projectUrl, dbUrl, dbName, accessId], f.result);
@@ -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) &&
@@ -491,7 +497,7 @@ const findObject = async (builder, config) => {
491
497
  if (!disableAuth && await getReachableServer(projectUrl))
492
498
  await awaitRefreshToken(projectUrl);
493
499
 
494
- const [reqBuilder, [privateKey]] = buildFetchInterface({
500
+ const [reqBuilder, [privateKey]] = await buildFetchInterface({
495
501
  body: {
496
502
  commands: {
497
503
  config: pureConfig && serializeToBase64(pureConfig),
@@ -514,7 +520,7 @@ const findObject = async (builder, config) => {
514
520
  const r = await (await fetch((findOne ? _readDocument : _queryCollection)(projectUrl, uglify), reqBuilder)).json();
515
521
  if (r.simpleError) throw r;
516
522
 
517
- const result = deserializeBSON((uglify ? deserializeE2E(r.e2e, serverE2E_PublicKey, privateKey) : r).result)._;
523
+ const result = deserializeBSON((uglify ? await deserializeE2E(r.e2e, serverE2E_PublicKey, privateKey) : r).result)._;
518
524
 
519
525
  if (shouldCache) insertRecord(builder, config, accessId, result);
520
526
  finalize({ liveResult: result || null });
@@ -628,7 +634,7 @@ const commitData = async (builder, value, type, config) => {
628
634
  if (!disableAuth && await getReachableServer(projectUrl))
629
635
  await awaitRefreshToken(projectUrl);
630
636
 
631
- const [reqBuilder, [privateKey]] = buildFetchInterface({
637
+ const [reqBuilder, [privateKey]] = await buildFetchInterface({
632
638
  body: {
633
639
  commands: {
634
640
  value: value && serializeToBase64({ _: value }),
@@ -650,7 +656,7 @@ const commitData = async (builder, value, type, config) => {
650
656
  const r = await (await fetch((isBatchWrite ? _writeMapDocument : _writeDocument)(projectUrl, uglify), reqBuilder)).json();
651
657
  if (r.simpleError) throw r;
652
658
 
653
- const f = uglify ? deserializeE2E(r.e2e, serverE2E_PublicKey, privateKey) : r;
659
+ const f = uglify ? await deserializeE2E(r.e2e, serverE2E_PublicKey, privateKey) : r;
654
660
 
655
661
  finalize({ ...f }, undefined, { removeCache: true });
656
662
  } catch (e) {
@@ -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 (isBaseUrl && !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) ? Buffer.from(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 ? await 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',
@@ -159,7 +170,7 @@ export const mfetch = async (input = '', init, config) => {
159
170
  if (!isBaseUrl && simple) throw { simpleError: JSON.parse(simple) };
160
171
 
161
172
  const base64 = uglified ?
162
- Buffer.from(deserializeE2E(await f.text(), serverE2E_PublicKey, privateKey), 'base64') :
173
+ Buffer.from(await deserializeE2E(await f.text(), serverE2E_PublicKey, privateKey), 'base64') :
163
174
  Buffer.from(await f.arrayBuffer()).toString('base64');
164
175
 
165
176
  const resObj = {
@@ -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) {
@@ -104,7 +105,7 @@ const deleteContent = async (builder, path, isFolder) => {
104
105
  try {
105
106
  const r = await (await fetch(
106
107
  EngineApi[isFolder ? '_deleteFolder' : '_deleteFile'](projectUrl, uglify),
107
- buildFetchInterface({ path }, accessKey, Scoped.AuthJWTToken[projectUrl], 'DELETE')
108
+ await buildFetchInterface({ path }, accessKey, Scoped.AuthJWTToken[projectUrl], 'DELETE')
108
109
  )).json();
109
110
  if (r.simpleError) throw r;
110
111
  if (r.status !== 'success') throw 'operation not successful';