mosquito-transport 1.9.2 → 1.9.4

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.
@@ -1,6 +1,6 @@
1
1
  import { MongoClient } from "mongodb";
2
2
  import { serialize } from 'mongodb/lib/bson.js';
3
- import { BLOCKS_IDENTIFIERS, encryptData, isPath, isValidColName, isValidDbName, one_gb, resolvePath } from "./utils.js";
3
+ import { BLOCKS_IDENTIFIERS, encryptData, isPath, isValidColName, isValidDbName, one_gb, resolvePath, wait } from "./utils.js";
4
4
  import { readdir, stat } from "fs/promises";
5
5
  import { createReadStream } from "fs";
6
6
  import { Validator } from "guard-object";
@@ -8,7 +8,7 @@ import { WritableBit } from "@deflexable/bit-stream";
8
8
  import { join } from "path";
9
9
 
10
10
  const BIT_SIZE = one_gb * .2;
11
- const DOC_LIMITER = 500;
11
+ const DOC_LIMITER = 300;
12
12
 
13
13
  export const extractBackup = (config) => {
14
14
  let { database, storage, password, onMongodbOption } = { ...config };
@@ -90,6 +90,7 @@ export const extractBackup = (config) => {
90
90
  pushBuffer(Buffer.from(`${thisCol}`, 'utf8'));
91
91
 
92
92
  while (canLoadMore) {
93
+ await wait(7); // pause for garbage collection
93
94
  const data = await dbNameInstance.collection(thisCol).find({})
94
95
  .skip(offset).limit(DOC_LIMITER).toArray();
95
96
  offset += DOC_LIMITER;
@@ -149,6 +150,7 @@ export const extractBackup = (config) => {
149
150
  reject(err);
150
151
  });
151
152
  });
153
+ await wait(1); // pause for garbage collection
152
154
  } else {
153
155
  const files = await readdir(dir);
154
156
  if (files.length) {
@@ -1,4 +1,4 @@
1
- import { BLOCKS_IDENTIFIERS, decryptData, resolvePath } from "./utils.js";
1
+ import { BLOCKS_IDENTIFIERS, decryptData, resolvePath, wait } from "./utils.js";
2
2
  import { MongoClient } from "mongodb";
3
3
  import { deserialize } from 'mongodb/lib/bson.js';
4
4
  import { mkdir } from "fs/promises";
@@ -100,7 +100,9 @@ export const installBackup = (config) => new Promise((callResolve, callReject) =
100
100
  { ...docRest },
101
101
  { upsert: true }
102
102
  );
103
- ++installionStats.totalWrittenDocuments;
103
+ if (!(++installionStats.totalWrittenDocuments % 300)) {
104
+ await wait(7); // pause for garbage collection
105
+ }
104
106
  } else {
105
107
  lastBlocks.database = INIT_BLOCKS.database;
106
108
 
@@ -135,7 +137,9 @@ export const installBackup = (config) => new Promise((callResolve, callReject) =
135
137
  const writeStream = createWriteStream(lastBlocks.storage.path);
136
138
  writeStream.write(thisElem);
137
139
  lastBlocks.storage.file = writeStream;
138
- ++installionStats.totalWrittenFiles;
140
+ if (!(++installionStats.totalWrittenFiles % 50)) {
141
+ await wait(3); // pause for garbage collection
142
+ };
139
143
  };
140
144
  } else throw `unknown block identifier "${prevHeader}" at block_id ${BLOCK_ID}`;
141
145
  }
package/bin/utils.js CHANGED
@@ -4,6 +4,11 @@ import { createCipheriv, createDecipheriv, createHash } from 'node:crypto';
4
4
  export const one_mb = 1024 * 1024,
5
5
  one_gb = one_mb * 1024;
6
6
 
7
+ export const wait = (ms = 1000) =>
8
+ new Promise(resolve => {
9
+ setTimeout(resolve, ms);
10
+ });
11
+
7
12
  export const BLOCKS_IDENTIFIERS = {
8
13
  DB_URL: '--->[DB_URL]:',
9
14
  DB_NAME: '--->[DB_NAME]:',
@@ -22,5 +22,9 @@ export const Scoped = {
22
22
  * }}
23
23
  */
24
24
  InstancesData: {},
25
- BlacklistedTokens: {}
25
+ BlacklistedTokens: {},
26
+ /**
27
+ * @type {{ [key: string]: { callers: Map; destroy: () => void; }}}
28
+ */
29
+ AccumulatedDatabaseEmittions: {}
26
30
  };
package/lib/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Db, Document, MongoClient, MongoClientOptions, SortDirection, UpdateDescription } from "mongodb";
1
+ import { ChangeStreamOptions, Db, Document, MongoClient, MongoClientOptions, SortDirection, UpdateDescription } from "mongodb";
2
2
  import express from "express";
3
3
  import { CorsOptions } from "cors";
4
4
  import { Sort } from "mongodb";
@@ -6,7 +6,7 @@ import { Filter } from "mongodb";
6
6
  import { UpdateFilter } from "mongodb";
7
7
  import type { IncomingHttpHeaders } from "http";
8
8
  import type { ParsedUrlQuery } from "querystring";
9
- import { Socket } from "socket.io";
9
+ import { Server, Socket } from "socket.io";
10
10
  import { Transform, PassThrough } from "stream";
11
11
 
12
12
  interface GoogleTokenPayload {
@@ -520,7 +520,8 @@ interface MosquitoServerConfig {
520
520
  storageRules: (snapshot?: StorageRulesSnapshot) => Promise<void> | undefined;
521
521
  databaseRules: (snapshot?: DatabaseRulesSnapshot) => Promise<void> | undefined;
522
522
  onSocketSnapshot?: (snapshot?: MSocketSnapshot) => void;
523
- onSocketError?: (error?: MSocketError) => void;
523
+ onSocketError?: ((error?: MSocketError) => void) | undefined;
524
+ useSocketServer?: ((io: Server) => void) | undefined;
524
525
  /**
525
526
  * the port number you want mosquito-transport instance to be running on
526
527
  */
@@ -538,6 +539,15 @@ interface MosquitoServerConfig {
538
539
  * @default true
539
540
  */
540
541
  autoPurgeToken?: boolean;
542
+
543
+ /**
544
+ * By default, issued access tokens are stateless. Set this to `true` to enable stateful access tokens.
545
+ *
546
+ * Enabling this introduces an additional security layer during validation, where the system cross-checks the provided token against a refresh token reference stored in the database to confirm its legitimacy.
547
+ *
548
+ * @default false
549
+ */
550
+ enableStatefulAccessToken?: boolean | undefined;
541
551
  /**
542
552
  * can either be a string or array containing any of the following:
543
553
  *
@@ -882,22 +892,24 @@ interface MosquitoHttpOptions {
882
892
  allowDisabledAuth?: boolean;
883
893
  }
884
894
 
885
- interface DatabaseListenerOption {
886
- includeBeforeData?: boolean;
887
- includeAfterData?: boolean;
895
+ interface DatabaseListenerOption extends ChangeStreamOptions {
896
+ /**
897
+ * An array of {@link https://www.mongodb.com/docs/manual/reference/operator/aggregation-pipeline/|aggregation pipeline stages} through which to pass change stream documents. This allows for filtering (using $match) and manipulating the change stream documents.
898
+ */
888
899
  pipeline?: { pipeline?: Document[] }
889
900
  }
890
901
 
891
902
  interface DatabaseListenerCallbackData {
892
903
  insertion?: { _id: string };
904
+ update?: UpdateDescription;
905
+ replacement?: { _id: string };
893
906
  deletion?: string;
894
- update?: UpdateDescription,
895
- before?: Document,
896
- after?: Document,
897
- timestamp: number,
898
- auth?: AuthData | undefined,
899
- operation: 'insert' | 'delete' | 'update';
907
+ before?: Document;
908
+ after?: Document;
909
+ timestamp: number;
910
+ operation: 'insert' | 'update' | 'replace' | 'delete';
900
911
  documentKey: string;
912
+ extras: any
901
913
  }
902
914
 
903
915
  interface StorageSnapshot {
package/lib/index.js CHANGED
@@ -36,6 +36,7 @@ import mime from 'mime';
36
36
  import LimitTasks from "limit-task";
37
37
  import { cpus } from "os";
38
38
  import { deserialize } from "entity-serializer";
39
+ import { hash } from "argon2";
39
40
 
40
41
  const { box } = naclPkg;
41
42
 
@@ -341,7 +342,7 @@ const InternalRoutesList = [
341
342
  ];
342
343
 
343
344
  const useMosquitoServer = (app, config) => {
344
- const { projectName, port, corsOrigin, maxRequestBufferSize, onSocketSnapshot, onSocketError, enforceE2E_Encryption, preMiddlewares, onUserMounted, pingTimeout, pingInterval } = config;
345
+ const { projectName, port, corsOrigin, maxRequestBufferSize, onSocketSnapshot, onSocketError, enforceE2E_Encryption, preMiddlewares, onUserMounted, pingTimeout, pingInterval, useSocketServer } = config;
345
346
 
346
347
  app.disable("x-powered-by");
347
348
 
@@ -621,6 +622,8 @@ const useMosquitoServer = (app, config) => {
621
622
  }
622
623
  });
623
624
 
625
+ useSocketServer?.(io);
626
+
624
627
  server.listen(port, () => {
625
628
  console.log(`mosquito-transport server listening on port ${port}`);
626
629
  });
@@ -732,7 +735,7 @@ export default class MosquitoTransportServer {
732
735
  });
733
736
 
734
737
  this.config = config;
735
- if (autoPurgeToken === undefined || autoPurgeToken) releaseTokenSelfDestruction(this.projectName);
738
+ releaseTokenSelfDestruction(this.projectName, autoPurgeToken === undefined || autoPurgeToken);
736
739
 
737
740
  (async () => {
738
741
  try {
@@ -799,13 +802,14 @@ export default class MosquitoTransportServer {
799
802
  if (normalizeRoute(route) === normalizeRoute(e))
800
803
  throw `"${e}" is a reserved route used internally`;
801
804
  });
805
+ const { logger } = this.config;
806
+ const hasLogger = logger.includes('all') || logger.includes('external-requests'),
807
+ hasErrorLogger = logger.includes('all') || logger.includes('error');
808
+
802
809
  Scoped.expressInstances[this.port].use(
803
810
  express.Router({ caseSensitive: true }).all(`/${normalizeRoute(route)}`, async (req, res) => {
804
811
  const { mtoken, uglified } = req.headers;
805
- const { logger } = this.config;
806
- const hasLogger = logger.includes('all') || logger.includes('external-requests'),
807
- hasErrorLogger = logger.includes('all') || logger.includes('error'),
808
- now = hasLogger && Date.now();
812
+ const now = hasLogger && Date.now();
809
813
 
810
814
  if (hasLogger) console.log(`started route: /${req.url}`);
811
815
  res.set(NO_CACHE_HEADER);
@@ -917,13 +921,13 @@ export default class MosquitoTransportServer {
917
921
 
918
922
  listenDatabase = (path, callback, options) => {
919
923
  if (typeof path !== 'string') throw `listenDatabase first argument must be a string but got ${path}`;
920
- const { dbName, dbUrl } = options || {},
921
- { logger } = this.config;
924
+ const { dbName, dbUrl } = options || {};
925
+ const { logger } = this.config;
926
+ const hasLogger = logger.includes('all') || logger.includes('database-snapshot'),
927
+ hasErrorLogger = logger.includes('all') || logger.includes('error');
922
928
 
923
929
  return emitDatabase(path, async function () {
924
- const hasLogger = logger.includes('all') || logger.includes('database-snapshot'),
925
- hasErrorLogger = logger.includes('all') || logger.includes('error'),
926
- now = hasLogger && Date.now();
930
+ const now = hasLogger && Date.now();
927
931
  if (hasLogger) console.log(`db-snapshot ${path}: `, arguments[0]);
928
932
  try {
929
933
  await callback?.(...arguments);
@@ -1006,11 +1010,11 @@ export default class MosquitoTransportServer {
1006
1010
 
1007
1011
  listenStorage = (callback) => {
1008
1012
  const { logger } = this.config;
1013
+ const hasLogger = logger.includes('all') || logger.includes('storage'),
1014
+ hasErrorLogger = logger.includes('all') || logger.includes('error');
1009
1015
 
1010
1016
  return StorageListener.listenTo(this.projectName, async ({ dest, ...rest }) => {
1011
- const hasLogger = logger.includes('all') || logger.includes('storage'),
1012
- hasErrorLogger = logger.includes('all') || logger.includes('error'),
1013
- now = hasLogger && Date.now();
1017
+ const now = hasLogger && Date.now();
1014
1018
 
1015
1019
  if (hasLogger) console.log(`started listenStorage ${dest}:`);
1016
1020
  try {
@@ -1022,18 +1026,20 @@ export default class MosquitoTransportServer {
1022
1026
  });
1023
1027
  };
1024
1028
 
1025
- listenNewUser = (callback) => emitDatabase(EnginePath.userAcct, s => {
1026
- if (s.insertion) {
1027
- const j = { ...s.insertion };
1028
- j.uid = j._id;
1029
- if (j._id) delete j._id;
1030
- callback?.(j);
1031
- }
1032
- }, this.projectName, ADMIN_DB_NAME, ADMIN_DB_URL);
1029
+ listenNewUser = (callback) =>
1030
+ emitDatabase(EnginePath.userAcct, s => {
1031
+ if (s.insertion) {
1032
+ const j = { ...s.insertion };
1033
+ j.uid = j._id;
1034
+ if (j._id) delete j._id;
1035
+ callback?.(j);
1036
+ }
1037
+ }, this.projectName, ADMIN_DB_NAME, ADMIN_DB_URL);
1033
1038
 
1034
- listenDeletedUser = (callback) => emitDatabase(EnginePath.userAcct, s => {
1035
- if (s.deletion) callback?.(s.deletion);
1036
- }, this.projectName, ADMIN_DB_NAME, ADMIN_DB_URL);
1039
+ listenDeletedUser = (callback) =>
1040
+ emitDatabase(EnginePath.userAcct, s => {
1041
+ if (s.deletion) callback?.(s.deletion);
1042
+ }, this.projectName, ADMIN_DB_NAME, ADMIN_DB_URL);
1037
1043
 
1038
1044
  updateUserProfile = async (uid, profile) => {
1039
1045
  if (!Validator.OBJECT(profile)) throw 'updateUserProfile() second argument must be an object';
@@ -1132,7 +1138,9 @@ export default class MosquitoTransportServer {
1132
1138
 
1133
1139
  updateUserPassword = async (uid, password) => {
1134
1140
  if (typeof uid !== 'string' || !uid.trim()) throw 'uid requires a string value';
1135
- if (typeof password !== 'string' || !password.trim()) throw 'email requires a string value';
1141
+ if (typeof password !== 'string' || !password.trim()) throw ERRORS.PASSWORD_REQUIRED.simpleError.message;
1142
+
1143
+ password = await hash(password);
1136
1144
 
1137
1145
  await writeDocument({
1138
1146
  scope: 'updateOne',
@@ -1270,7 +1278,9 @@ const validateServerConfig = (config, that) => {
1270
1278
  internals,
1271
1279
  onSocketSnapshot,
1272
1280
  onSocketError,
1281
+ useSocketServer,
1273
1282
  autoPurgeToken,
1283
+ enableStatefulAccessToken,
1274
1284
  ffmpegEncoderArg,
1275
1285
  maxFfmpegTasks,
1276
1286
  pingTimeout,
@@ -1353,6 +1363,9 @@ const validateServerConfig = (config, that) => {
1353
1363
  if (onSocketError !== undefined && typeof onSocketError !== 'function')
1354
1364
  throw `onSocketError type must be function but got ${typeof onSocketError}`;
1355
1365
 
1366
+ if (useSocketServer !== undefined && typeof useSocketServer !== 'function')
1367
+ throw `useSocketServer type must be function but got ${typeof useSocketServer}`;
1368
+
1356
1369
  if (typeof signerKey !== 'string' || signerKey.length < 32)
1357
1370
  throw `signerKey must be at least 32 characters`;
1358
1371
 
@@ -1362,6 +1375,9 @@ const validateServerConfig = (config, that) => {
1362
1375
  if (autoPurgeToken !== undefined && typeof autoPurgeToken !== 'boolean')
1363
1376
  throw `invalid value supplied to autoPurgeToken, expected a boolean but got ${typeof autoPurgeToken}`;
1364
1377
 
1378
+ if (enableStatefulAccessToken !== undefined && typeof enableStatefulAccessToken !== 'boolean')
1379
+ throw `invalid value supplied to enableStatefulAccessToken, expected a boolean but got ${typeof enableStatefulAccessToken}`;
1380
+
1365
1381
  if (castBSON !== undefined && typeof castBSON !== 'boolean')
1366
1382
  throw `invalid value supplied to castBSON, expected a boolean but got ${typeof castBSON}`;
1367
1383
 
@@ -6,6 +6,7 @@ import { Scoped } from "../../helpers/variables";
6
6
  import { queryDocument, readDocument, writeDocument } from "../database";
7
7
  import { destroyToken, signJWT, signRefreshToken, validateRefreshToken, verifyJWT } from "./tokenizer";
8
8
  import { simplifyError } from 'simplify-error';
9
+ import { hash, verify } from "argon2";
9
10
 
10
11
  export const signupCustom = async (
11
12
  email = '',
@@ -23,7 +24,13 @@ export const signupCustom = async (
23
24
  Scoped.pendingSignups[processID] = true;
24
25
 
25
26
  const { enableSequentialUid, uidLength, mergeAuthAccount, interceptNewAuth } = Scoped.InstancesData[projectName];
27
+ let hashed_password;
26
28
 
29
+ const doHash = async () => {
30
+ if (hashed_password || !password) return hashed_password;
31
+ return hashed_password = await hash(password);
32
+ }
33
+
27
34
  if (signupMethod === AUTH_PROVIDER_ID.PASSWORD) {
28
35
  if (!password || typeof password !== 'string') throw ERRORS.PASSWORD_REQUIRED;
29
36
  if (!Validator.EMAIL(email)) throw ERRORS.INVALID_EMAIL;
@@ -38,7 +45,7 @@ export const signupCustom = async (
38
45
  if (mergeAuthAccount) {
39
46
  await writeDocument({
40
47
  find: { _id: prevData[0]._id },
41
- value: { $set: { password } },
48
+ value: { $set: { password: await doHash() } },
42
49
  path: EnginePath.userAcct,
43
50
  scope: 'updateOne'
44
51
  }, projectName, ADMIN_DB_NAME, ADMIN_DB_URL);
@@ -49,9 +56,12 @@ export const signupCustom = async (
49
56
  }
50
57
  }
51
58
  }
59
+ await doHash();
60
+
52
61
  const aBuild = {
53
62
  email,
54
63
  password,
64
+ ...hashed_password ? { hashed_password } : {},
55
65
  name: customExtras.name,
56
66
  request: customExtras.req,
57
67
  metadata: customExtras.metadata,
@@ -113,7 +123,7 @@ export const signupCustom = async (
113
123
  path: EnginePath.userAcct,
114
124
  value: {
115
125
  ...tokenData,
116
- ...password ? { password } : {},
126
+ ...hashed_password ? { password: hashed_password } : {},
117
127
  ...sub ? { [signupMethod]: sub } : {},
118
128
  _id: newUid
119
129
  }
@@ -152,10 +162,22 @@ export const signinCustom = async (email = '', password = '', signinMethod = AUT
152
162
  }, projectName, ADMIN_DB_NAME, ADMIN_DB_URL);
153
163
 
154
164
  if (userData.length) {
155
- const passworded = userData.find(v => v.password === password) || userData.find(v => v.password);
156
-
157
- if (passworded) {
158
- if (passworded.password === password) {
165
+ let hasPassword;
166
+
167
+ const passworded =
168
+ await Promise.all(
169
+ userData.map(async v => {
170
+ if (!v.password) return v;
171
+ hasPassword = true;
172
+ const pass = await verify(v.password, password);
173
+ return { ...v, _passwork_hash_verified: pass };
174
+ })
175
+ ).then(r =>
176
+ r.find(v => v._passwork_hash_verified)
177
+ );
178
+
179
+ if (hasPassword) {
180
+ if (passworded) {
159
181
  userData = passworded;
160
182
  } else throw ERRORS.INCORRECT_PASSWORD;
161
183
  } else throw ERRORS.ACCOUNT_NO_PASSWORD;
@@ -2,7 +2,7 @@ import pkg from 'jsonwebtoken';
2
2
  import { simplifyError } from 'simplify-error';
3
3
  import { ADMIN_DB_NAME, ADMIN_DB_URL, EnginePath, ERRORS, REFRESH_TOKEN_EXPIRY, TOKEN_EXPIRY } from "../../helpers/values";
4
4
  import { Scoped } from "../../helpers/variables"
5
- import { queryDocument, readDocument, writeDocument } from '../database';
5
+ import { emitDatabase, queryDocument, readDocument, writeDocument } from '../database';
6
6
  import { setLargeTimeout, setLargeInterval } from "set-large-timeout";
7
7
 
8
8
  const { sign, verify } = pkg;
@@ -63,6 +63,7 @@ export const signJWT = async (payload, projectName, isRefreshToken) => {
63
63
 
64
64
  export const validateJWT = async (token, projectName, isRefreshToken) => {
65
65
  try {
66
+ const crossCheckToken = Scoped.InstancesData[projectName].enableStatefulAccessToken;
66
67
  const auth = await verifyJWT(token, projectName, isRefreshToken);
67
68
  const expiry = (auth.exp || 0) * 1000;
68
69
  let tokenData;
@@ -75,7 +76,15 @@ export const validateJWT = async (token, projectName, isRefreshToken) => {
75
76
  find: { _id: auth.tokenID }
76
77
  }, projectName, ADMIN_DB_NAME, ADMIN_DB_URL))
77
78
  :
78
- Scoped.BlacklistedTokens?.[projectName]?.[auth.tokenID])
79
+ (Scoped.BlacklistedTokens?.[projectName]?.[auth.tokenID] ||
80
+ (crossCheckToken &&
81
+ !(tokenData = await readDocument({
82
+ path: EnginePath.refreshTokenStore,
83
+ find: { _id: auth.entityOf }
84
+ }, projectName, ADMIN_DB_NAME, ADMIN_DB_URL))
85
+ )
86
+ )
87
+ )
79
88
  )) {
80
89
  if (Date.now() > expiry) throw ERRORS.TOKEN_EXPIRED;
81
90
  throw ERRORS.TOKEN_NOT_FOUND;
@@ -95,44 +104,56 @@ export const signRefreshToken = (payload, projectName) => signJWT(payload, proje
95
104
  export const validateRefreshToken = async (token, projectName) => validateJWT(token, projectName, true);
96
105
 
97
106
  // Token store manager
98
- export const releaseTokenSelfDestruction = (projectName) => {
99
- const lifetime = REFRESH_TOKEN_EXPIRY(projectName);
100
- const interval = Math.round(lifetime * .25);
107
+ export const releaseTokenSelfDestruction = (projectName, shouldPurge) => {
108
+ if (shouldPurge) {
109
+ const lifetime = REFRESH_TOKEN_EXPIRY(projectName);
110
+ const interval = Math.round(lifetime * .25);
101
111
 
102
- const cleanUpTokens = async () => {
103
- await writeDocument({
104
- path: EnginePath.refreshTokenStore,
105
- find: { createdOn: { $lt: Date.now() - lifetime } },
106
- scope: 'deleteMany'
107
- }, projectName, ADMIN_DB_NAME, ADMIN_DB_URL);
108
-
109
- const hotExpires = await queryDocument({
110
- path: EnginePath.refreshTokenStore,
111
- find: { createdOn: { $lt: Date.now() - (lifetime - interval) } }
112
- }, projectName, ADMIN_DB_NAME, ADMIN_DB_URL);
113
-
114
- hotExpires.forEach(e => {
115
- setLargeTimeout(() => {
116
- writeDocument({
117
- path: EnginePath.refreshTokenStore,
118
- find: { _id: e._id },
119
- scope: 'deleteOne'
120
- }, projectName, ADMIN_DB_NAME, ADMIN_DB_URL);
121
- }, Math.max(0, (e.createdOn + lifetime) - Date.now()));
122
- });
123
- };
112
+ const cleanUpTokens = async () => {
113
+ await writeDocument({
114
+ path: EnginePath.refreshTokenStore,
115
+ find: { createdOn: { $lt: Date.now() - lifetime } },
116
+ scope: 'deleteMany'
117
+ }, projectName, ADMIN_DB_NAME, ADMIN_DB_URL);
124
118
 
125
- cleanUpTokens();
126
- setLargeInterval(cleanUpTokens, interval);
119
+ const hotExpires = await queryDocument({
120
+ path: EnginePath.refreshTokenStore,
121
+ find: { createdOn: { $lt: Date.now() - (lifetime - interval) } }
122
+ }, projectName, ADMIN_DB_NAME, ADMIN_DB_URL);
123
+
124
+ hotExpires.forEach(e => {
125
+ setLargeTimeout(() => {
126
+ writeDocument({
127
+ path: EnginePath.refreshTokenStore,
128
+ find: { _id: e._id },
129
+ scope: 'deleteOne'
130
+ }, projectName, ADMIN_DB_NAME, ADMIN_DB_URL);
131
+ }, Math.max(0, (e.createdOn + lifetime) - Date.now()));
132
+ });
133
+ };
134
+
135
+ cleanUpTokens();
136
+ setLargeInterval(cleanUpTokens, interval);
137
+ }
127
138
 
128
139
  queryDocument({
129
140
  path: EnginePath.revokedAccessToken,
130
141
  find: {}
131
142
  }, projectName, ADMIN_DB_NAME, ADMIN_DB_URL).then(r => {
132
143
  r.forEach(e => {
133
- setBlacklistedTokenTimer(e._id, projectName, e.pop_on - Date.now());
144
+ setBlacklistedTokenTimer(e._id, projectName, e.pop_on - Date.now(), shouldPurge);
134
145
  });
135
146
  });
147
+
148
+ return emitDatabase(EnginePath.revokedAccessToken, e => {
149
+ if (e.insertion) {
150
+ e = e.insertion;
151
+ setBlacklistedTokenTimer(e._id, projectName, e.pop_on - Date.now(), shouldPurge);
152
+ } else if (e.deletion) {
153
+ if (Scoped.BlacklistedTokens[projectName]?.[e.deletion])
154
+ delete Scoped.BlacklistedTokens[projectName][e.deletion];
155
+ }
156
+ }, projectName, ADMIN_DB_NAME, ADMIN_DB_URL);
136
157
  };
137
158
 
138
159
  export const destroyToken = async (ref, projectName, isRefreshToken) => {
@@ -147,29 +168,31 @@ export const destroyToken = async (ref, projectName, isRefreshToken) => {
147
168
  if (Scoped.BlacklistedTokens[projectName]?.[ref]) return false;
148
169
  const lifetime = TOKEN_EXPIRY(projectName);
149
170
 
150
- writeDocument({
171
+ return writeDocument({
151
172
  path: EnginePath.revokedAccessToken,
152
173
  value: {
153
174
  _id: ref,
154
175
  pop_on: Date.now() + lifetime
155
176
  }
156
- }, projectName, ADMIN_DB_NAME, ADMIN_DB_URL);
157
- setBlacklistedTokenTimer(ref, projectName, lifetime);
158
- return true;
177
+ }, projectName, ADMIN_DB_NAME, ADMIN_DB_URL)
178
+ .then(r => !!r.insertedCount);
159
179
  };
160
180
 
161
- const setBlacklistedTokenTimer = (ref, projectName, timeout) => {
162
- if (!Scoped.BlacklistedTokens[projectName])
163
- Scoped.BlacklistedTokens[projectName] = {};
164
- Scoped.BlacklistedTokens[projectName][ref] = true;
181
+ const setBlacklistedTokenTimer = (ref, projectName, timeout, shouldPurge) => {
182
+ timeout = Math.max(0, timeout);
165
183
 
166
- setLargeTimeout(() => {
167
- writeDocument({
168
- path: EnginePath.revokedAccessToken,
169
- find: { _id: ref },
170
- scope: 'deleteOne'
171
- }, projectName, ADMIN_DB_NAME, ADMIN_DB_URL);
172
- if (Scoped.BlacklistedTokens[projectName]?.[ref])
173
- delete Scoped.BlacklistedTokens[projectName][ref];
174
- }, Math.max(0, timeout));
184
+ if (timeout) {
185
+ if (!Scoped.BlacklistedTokens[projectName])
186
+ Scoped.BlacklistedTokens[projectName] = {};
187
+ Scoped.BlacklistedTokens[projectName][ref] = true;
188
+ }
189
+
190
+ if (shouldPurge)
191
+ setLargeTimeout(() => {
192
+ writeDocument({
193
+ path: EnginePath.revokedAccessToken,
194
+ find: { _id: ref },
195
+ scope: 'deleteOne'
196
+ }, projectName, ADMIN_DB_NAME, ADMIN_DB_URL);
197
+ }, timeout);
175
198
  }
@@ -6,19 +6,22 @@ import { Scoped } from "../../helpers/variables.js";
6
6
  */
7
7
  export const getDB = (projectName, name, url = DEFAULT_DB) => {
8
8
  if (!projectName) throw 'expected projectName in getDb()';
9
- const { defaultName: dbName, instance } = getDbInstance(projectName, url) || {};
10
-
11
- if (name === ADMIN_DB_NAME) name = dbName;
12
- if (!instance) throw `no MongoClient was found for database with dbRef "${url}"`;
13
- // if (!name && !dbName) throw `no dbName found for database with dbRef "${dbUrl}"`;
14
-
15
- return instance.db(name || dbName);
9
+ const { dbName, instance } = getDbNaming(projectName, name, url) || {};
10
+ return instance.instance.db(dbName);
16
11
  };
17
12
 
18
- export const getDbInstance = (projectName, dbUrl = DEFAULT_DB) => {
13
+ export const getDbNaming = (projectName, name, dbRef = DEFAULT_DB) => {
19
14
  if (!projectName) throw 'expected projectName in getDb()';
20
- if (dbUrl === 'admin' || dbUrl === 'default') throw `reserved keyword dbRef: "${dbUrl}"`;
15
+ if (dbRef === 'admin' || dbRef === 'default') throw `reserved keyword dbRef: "${dbRef}"`;
16
+
17
+ dbRef = dbRef === ADMIN_DB_URL ? 'admin' : dbRef === DEFAULT_DB ? 'default' : dbRef;
18
+ const instance = Scoped.InstancesData[projectName].mongoInstances[dbRef];
19
+
20
+ if (!instance) throw `no MongoClient was found for database with dbRef "${dbRef}"`;
21
21
 
22
- dbUrl = dbUrl === ADMIN_DB_URL ? 'admin' : dbUrl === DEFAULT_DB ? 'default' : dbUrl;
23
- return Scoped.InstancesData[projectName].mongoInstances[dbUrl];
22
+ return {
23
+ dbRef,
24
+ instance,
25
+ dbName: name === ADMIN_DB_NAME ? instance.defaultName : (name || instance.defaultName)
26
+ };
24
27
  }
@@ -1,6 +1,6 @@
1
1
  import express from "express";
2
2
  import { deserializeE2E, encodeBinary, niceTry, serializeE2E } from "../../helpers/utils.js";
3
- import { getDB, getDbInstance } from "./base.js";
3
+ import { getDB, getDbNaming } from "./base.js";
4
4
  import { validateJWT } from "../auth/tokenizer.js";
5
5
  import { Scoped } from "../../helpers/variables.js";
6
6
  import { EngineRoutes, ERRORS, NO_CACHE_HEADER } from "../../helpers/values.js";
@@ -79,12 +79,11 @@ const deserializeWriteValue = (value) => {
79
79
  } else return value;
80
80
  };
81
81
 
82
- const cleanseFind = (path, find, projectName, dbName, dbUrl) => {
83
- const { defaultName, instance } = getDbInstance(projectName, dbUrl);
84
- dbName = dbName || defaultName;
82
+ const cleanseFind = (path, find, projectName, name, dbUrl) => {
83
+ const { instance, dbName } = getDbNaming(projectName, name, dbUrl);
85
84
 
86
- if (instance.__intercepted) {
87
- const d = instance.interceptMap?.map?.[dbName]?.[path];
85
+ if (instance.instance.__intercepted) {
86
+ const d = instance.instance.interceptMap?.map?.[dbName]?.[path];
88
87
  if (d?.fulltext) return find;
89
88
  }
90
89
  return cleanseFindCore(find);
@@ -289,33 +288,95 @@ const extractDocField = async (d, commands, projectName, dbName, dbUrl, doc_hold
289
288
  };
290
289
 
291
290
  export const emitDatabase = (path, callback, projectName, dbName, dbUrl, options) => {
292
- const { includeBeforeData, includeAfterData, pipeline } = options || {};
291
+ const naming = getDbNaming(projectName, dbName, dbUrl);
293
292
 
294
- const col = getDB(projectName, dbName, dbUrl).collection(path),
295
- stream = col.watch(pipeline, {
296
- fullDocument: includeAfterData ? 'whenAvailable' : undefined,
297
- fullDocumentBeforeChange: includeBeforeData ? 'whenAvailable' : undefined
298
- });
293
+ const nodeId = `${path}:${projectName}:${naming.dbName}:${naming.dbRef}:${options && serializeToBase64(options)}`;
294
+ let instance = Scoped.AccumulatedDatabaseEmittions[nodeId];
295
+
296
+ if (!instance) {
297
+ const callers = new Map();
298
+ const destroy = internalEmitDatabase(path, (...args) => {
299
+ callers.forEach(value => {
300
+ value(...args);
301
+ });
302
+ }, projectName, dbName, dbUrl, options);
303
+
304
+ Scoped.AccumulatedDatabaseEmittions[nodeId] = (instance = { callers, destroy });
305
+ }
306
+
307
+ const ref = {};
308
+ instance.callers.set(ref, callback);
309
+
310
+ return () => {
311
+ if (!instance.callers.has(ref)) return;
312
+ instance.callers.delete(ref);
313
+ if (!instance.callers.size) {
314
+ instance.destroy();
315
+ delete Scoped.AccumulatedDatabaseEmittions[nodeId];
316
+ }
317
+ }
318
+ }
319
+
320
+ const requiredEvent = ['insert', 'update', 'replace', 'delete'];
321
+
322
+ const internalEmitDatabase = (path, callback, projectName, dbName, dbUrl, options, resumeAfter) => {
323
+ const { pipeline, ...restOptions } = options || {};
324
+
325
+ const col = getDB(projectName, dbName, dbUrl).collection(path);
326
+ const stream = col.watch(pipeline || [
327
+ {
328
+ $match: {
329
+ operationType: { $in: requiredEvent }
330
+ }
331
+ }
332
+ ], {
333
+ ...restOptions,
334
+ resumeAfter
335
+ });
299
336
 
300
337
  stream.on('change', l => {
301
- const { operationType: ops, fullDocument, fullDocumentBeforeChange, documentKey, updateDescription, clusterTime } = l;
338
+ const { operationType: ops, fullDocument, fullDocumentBeforeChange, documentKey, updateDescription, clusterTime, ...rest } = l;
339
+
340
+ if (!requiredEvent.includes(ops)) return;
302
341
 
303
- if (ops !== 'insert' && ops !== 'delete' && ops !== 'update') return;
304
342
  callback?.({
305
343
  documentKey: documentKey._id,
306
344
  insertion: ops === 'insert' ? fullDocument : undefined,
307
- deletion: ops === 'delete' ? documentKey._id : undefined,
308
345
  update: ops === 'update' ? { ...updateDescription } : undefined,
309
- before: includeBeforeData ? fullDocumentBeforeChange : undefined,
310
- after: includeAfterData ? fullDocument : undefined,
311
- timestamp: clusterTime?.toNumber?.(),
312
- auth: undefined,
313
- operation: ops
346
+ replacement: ops === 'replace' ? fullDocument : undefined,
347
+ deletion: ops === 'delete' ? documentKey._id : undefined,
348
+ before: fullDocumentBeforeChange,
349
+ after: fullDocument,
350
+ timestamp: clusterTime,
351
+ operation: ops,
352
+ extras: rest
314
353
  });
315
354
  });
316
355
 
356
+ stream.on('resumeTokenChanged', (token) => {
357
+ resumeAfter = token;
358
+ });
359
+
360
+ let closure = null;
361
+
362
+ stream.on('error', async (error) => {
363
+ await stream.close();
364
+ setTimeout(() => {
365
+ if (closure === null)
366
+ closure = internalEmitDatabase(path, callback, projectName, dbName, dbUrl, options, resumeAfter);
367
+ }, 7000);
368
+ process.emit('uncaughtException', `emitDatabase error: ${error}`);
369
+ });
370
+
317
371
  return () => {
318
- stream.close();
372
+ const thisClosure = closure;
373
+ closure = undefined;
374
+
375
+ if (thisClosure === null) {
376
+ stream.close();
377
+ } else if (typeof thisClosure === 'function') {
378
+ thisClosure();
379
+ }
319
380
  }
320
381
  };
321
382
 
@@ -658,7 +719,7 @@ export const databaseLiveRoutesHandler = ({
658
719
  } catch (e) {
659
720
  socket.emit('mSnapshot', [simplifyCaughtError(e), undefined]);
660
721
  }
661
- }, projectName, dbName, dbUrl, { pipeline: { ...commands.find } });
722
+ }, projectName, dbName, dbUrl);
662
723
  } catch (e) {
663
724
  if (hasErrorLoger) console.error(`errRoute /${route} err:`, e);
664
725
  socket.emit('mSnapshot', [simplifyCaughtError(e), undefined]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mosquito-transport",
3
- "version": "1.9.2",
3
+ "version": "1.9.4",
4
4
  "description": "Quickly spawn server infrastructure along robust authentication, database, storage, and cross-platform compatibility",
5
5
  "main": "lib/index.js",
6
6
  "type": "module",
@@ -37,6 +37,7 @@
37
37
  "homepage": "https://github.com/brainbehindx/mosquito-transport#readme",
38
38
  "dependencies": {
39
39
  "@deflexable/bit-stream": "^1.0.4",
40
+ "argon2": "^0.44.0",
40
41
  "buffer": "^6.0.3",
41
42
  "compression": "^1.8.1",
42
43
  "cors": "^2.8.5",