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.
- package/bin/extract_backup.js +4 -2
- package/bin/install_backup.js +7 -3
- package/bin/utils.js +5 -0
- package/lib/helpers/variables.js +5 -1
- package/lib/index.d.ts +24 -12
- package/lib/index.js +42 -26
- package/lib/products/auth/email_auth.js +28 -6
- package/lib/products/auth/tokenizer.js +70 -47
- package/lib/products/database/base.js +14 -11
- package/lib/products/database/index.js +83 -22
- package/package.json +2 -1
package/bin/extract_backup.js
CHANGED
|
@@ -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 =
|
|
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) {
|
package/bin/install_backup.js
CHANGED
|
@@ -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]:',
|
package/lib/helpers/variables.js
CHANGED
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
|
-
|
|
887
|
-
|
|
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
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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) =>
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
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) =>
|
|
1035
|
-
|
|
1036
|
-
|
|
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
|
|
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
|
-
...
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
100
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
126
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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 {
|
|
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
|
|
13
|
+
export const getDbNaming = (projectName, name, dbRef = DEFAULT_DB) => {
|
|
19
14
|
if (!projectName) throw 'expected projectName in getDb()';
|
|
20
|
-
if (
|
|
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
|
-
|
|
23
|
-
|
|
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,
|
|
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,
|
|
83
|
-
const {
|
|
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
|
|
291
|
+
const naming = getDbNaming(projectName, dbName, dbUrl);
|
|
293
292
|
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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",
|