@friggframework/core 0.2.31-v1-alpha-package-update.0 → 1.0.1-canary.a414ea2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assertions/CHANGELOG.md +87 -0
- package/assertions/LICENSE.md +9 -0
- package/assertions/README.md +3 -0
- package/assertions/bump.txt +1 -0
- package/assertions/get.js +139 -0
- package/assertions/index.js +19 -0
- package/assertions/is-equal.js +17 -0
- package/associations/LICENSE.md +9 -0
- package/associations/README.md +3 -0
- package/associations/association.js +78 -0
- package/associations/bump3.txt +0 -0
- package/associations/jest.config.js +5 -0
- package/associations/model.js +54 -0
- package/bump0.txt +1 -0
- package/core/.eslintrc.json +3 -0
- package/{Delegate.js → core/Delegate.js} +1 -1
- package/core/LICENSE.md +9 -0
- package/{Worker.js → core/Worker.js} +2 -2
- package/core/bump3.txt +0 -0
- package/{create-handler.js → core/create-handler.js} +2 -2
- package/core/index.js +6 -0
- package/core/jest.config.js +5 -0
- package/database/.eslintrc.json +3 -0
- package/database/CHANGELOG.md +97 -0
- package/database/LICENSE.md +9 -0
- package/database/README.md +3 -0
- package/database/bump3.txt +0 -0
- package/database/index.js +23 -0
- package/database/jest.config.js +5 -0
- package/database/models/IndividualUser.js +76 -0
- package/database/models/OrganizationUser.js +29 -0
- package/database/models/State.js +9 -0
- package/database/models/Token.js +70 -0
- package/database/models/UserModel.js +7 -0
- package/database/mongo.js +45 -0
- package/database/mongoose.js +5 -0
- package/encrypt/.eslintrc.json +3 -0
- package/encrypt/CHANGELOG.md +78 -0
- package/encrypt/Cryptor.js +236 -0
- package/encrypt/Cryptor.test.js +32 -0
- package/encrypt/LICENSE.md +9 -0
- package/encrypt/README.md +13 -0
- package/encrypt/aes.js +27 -0
- package/encrypt/bump3.txt +0 -0
- package/encrypt/encrypt.js +132 -0
- package/encrypt/encrypt.test.js +1069 -0
- package/encrypt/index.js +4 -0
- package/encrypt/jest.config.js +5 -0
- package/encrypt/test-encrypt.js +107 -0
- package/errors/.eslintrc.json +3 -0
- package/errors/CHANGELOG.md +44 -0
- package/errors/LICENSE.md +9 -0
- package/errors/README.md +3 -0
- package/errors/base-error.js +23 -0
- package/errors/base-error.test.js +32 -0
- package/errors/bump.txt +1 -0
- package/errors/bump3.txt +0 -0
- package/errors/fetch-error.js +76 -0
- package/errors/fetch-error.test.js +79 -0
- package/errors/halt-error.js +10 -0
- package/errors/halt-error.test.js +11 -0
- package/errors/index.js +15 -0
- package/errors/jest.config.js +5 -0
- package/errors/validation-errors.js +23 -0
- package/errors/validation-errors.test.js +120 -0
- package/index.js +123 -5
- package/integrations/.eslintrc.json +3 -0
- package/integrations/CHANGELOG.md +244 -0
- package/integrations/LICENSE.md +9 -0
- package/integrations/README.md +3 -0
- package/integrations/bump3.txt +0 -0
- package/integrations/create-frigg-backend.js +31 -0
- package/integrations/index.js +19 -0
- package/integrations/integration-base.js +162 -0
- package/integrations/integration-factory.js +166 -0
- package/integrations/integration-mapping.js +43 -0
- package/integrations/integration-model.js +42 -0
- package/integrations/integration-router.js +367 -0
- package/integrations/integration-user.js +144 -0
- package/integrations/jest-setup.js +2 -0
- package/integrations/jest-teardown.js +2 -0
- package/integrations/jest.config.js +12 -0
- package/integrations/options.js +54 -0
- package/integrations/test/integration-base.test.js +143 -0
- package/lambda/README.md +3 -0
- package/lambda/TimeoutCatcher.js +43 -0
- package/lambda/TimeoutCatcher.test.js +68 -0
- package/lambda/bump3.txt +0 -0
- package/lambda/index.js +3 -0
- package/lambda/jest.config.js +3 -0
- package/logs/.eslintrc.json +3 -0
- package/logs/CHANGELOG.md +57 -0
- package/logs/LICENSE.md +9 -0
- package/logs/README.md +3 -0
- package/logs/bump3.txt +0 -0
- package/logs/index.js +7 -0
- package/logs/jest.config.js +5 -0
- package/logs/logger.js +69 -0
- package/logs/logger.test.js +76 -0
- package/module-plugin/.eslintrc.json +3 -0
- package/module-plugin/CHANGELOG.md +224 -0
- package/module-plugin/LICENSE.md +9 -0
- package/module-plugin/ModuleConstants.js +10 -0
- package/module-plugin/README.md +3 -0
- package/module-plugin/auther.js +342 -0
- package/module-plugin/bump3.txt +0 -0
- package/module-plugin/credential.js +22 -0
- package/module-plugin/entity-manager.js +70 -0
- package/module-plugin/entity.js +46 -0
- package/module-plugin/index.js +25 -0
- package/module-plugin/jest-setup.js +3 -0
- package/module-plugin/jest-teardown.js +2 -0
- package/module-plugin/jest.config.js +20 -0
- package/module-plugin/manager.js +169 -0
- package/module-plugin/module-factory.js +60 -0
- package/module-plugin/requester/api-key.js +36 -0
- package/module-plugin/requester/basic.js +43 -0
- package/module-plugin/requester/oauth-2.js +219 -0
- package/module-plugin/requester/requester.js +151 -0
- package/module-plugin/requester/requester.test.js +28 -0
- package/module-plugin/test/auther.test.js +97 -0
- package/module-plugin/test/mock-api/api.js +29 -0
- package/module-plugin/test/mock-api/definition.js +48 -0
- package/module-plugin/test/mock-api/mocks/hubspot.js +43 -0
- package/package.json +28 -12
- package/syncs/README.md +3 -0
- package/syncs/bump3.txt +0 -0
- package/syncs/jest.config.js +5 -0
- package/syncs/manager.js +464 -0
- package/syncs/model.js +62 -0
- package/syncs/sync.js +114 -0
- package/types/CHANGELOG.md +49 -0
- package/types/README.md +24 -0
- package/types/assertions/index.d.ts +83 -0
- package/types/associations/index.d.ts +74 -0
- package/types/bump3.txt +0 -0
- package/types/core/index.d.ts +54 -0
- package/types/database/index.d.ts +3 -0
- package/types/encrypt/index.d.ts +5 -0
- package/types/errors/index.d.ts +66 -0
- package/types/eslint-config/index.d.ts +41 -0
- package/types/index.d.ts +14 -0
- package/types/integrations/index.d.ts +191 -0
- package/types/lambda/index.d.ts +31 -0
- package/types/logs/index.d.ts +5 -0
- package/types/module-plugin/index.d.ts +293 -0
- package/types/prettier-config/index.d.ts +6 -0
- package/types/syncs/index.d.ts +128 -0
- package/types/test-environment/index.d.ts +17 -0
- package/types/tsconfig.json +103 -0
- /package/{.eslintrc.json → assertions/.eslintrc.json} +0 -0
- /package/{bump3.txt → assertions/bump3.txt} +0 -0
- /package/{jest.config.js → assertions/jest.config.js} +0 -0
- /package/{CHANGELOG.md → core/CHANGELOG.md} +0 -0
- /package/{README.md → core/README.md} +0 -0
- /package/{load-installed-modules.js → core/load-installed-modules.js} +0 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
const { mongoose } = require('../mongoose');
|
|
2
|
+
const bcrypt = require('bcryptjs');
|
|
3
|
+
const { UserModel: Parent } = require('./UserModel');
|
|
4
|
+
|
|
5
|
+
const collectionName = 'IndividualUser';
|
|
6
|
+
|
|
7
|
+
const schema = new mongoose.Schema({
|
|
8
|
+
email: { type: String },
|
|
9
|
+
username: { type: String, unique: true },
|
|
10
|
+
hashword: { type: String },
|
|
11
|
+
appUserId: { type: String },
|
|
12
|
+
organizationUser: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
schema.pre('save', async function () {
|
|
16
|
+
if (this.hashword) {
|
|
17
|
+
this.hashword = await bcrypt.hashSync(
|
|
18
|
+
this.hashword,
|
|
19
|
+
parseInt(this.schema.statics.decimals)
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
schema.static({
|
|
25
|
+
decimals: 10,
|
|
26
|
+
update: async function (id, options) {
|
|
27
|
+
if ('password' in options) {
|
|
28
|
+
options.hashword = await bcrypt.hashSync(
|
|
29
|
+
options.password,
|
|
30
|
+
parseInt(this.decimals)
|
|
31
|
+
);
|
|
32
|
+
delete options.password;
|
|
33
|
+
}
|
|
34
|
+
return this.findOneAndUpdate(
|
|
35
|
+
{_id: id},
|
|
36
|
+
options,
|
|
37
|
+
{new: true, useFindAndModify: true}
|
|
38
|
+
);
|
|
39
|
+
},
|
|
40
|
+
getUserByUsername: async function (username) {
|
|
41
|
+
let getByUser;
|
|
42
|
+
try{
|
|
43
|
+
getByUser = await this.find({username});
|
|
44
|
+
} catch (e) {
|
|
45
|
+
console.log('oops')
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (getByUser.length > 1) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
'Unique username or email? Please reach out to our developers'
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (getByUser.length === 1) {
|
|
55
|
+
return getByUser[0];
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
getUserByAppUserId: async function (appUserId) {
|
|
59
|
+
const getByUser = await this.find({ appUserId });
|
|
60
|
+
|
|
61
|
+
if (getByUser.length > 1) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
'Supposedly using a unique appUserId? Please reach out to our developers'
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
if (getByUser.length === 1) {
|
|
69
|
+
return getByUser[0];
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
const IndividualUser = Parent.discriminators?.IndividualUser || Parent.discriminator(collectionName, schema);
|
|
75
|
+
|
|
76
|
+
module.exports = {IndividualUser};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const { mongoose } = require('../mongoose');
|
|
2
|
+
const { UserModel: Parent } = require('./UserModel');
|
|
3
|
+
|
|
4
|
+
const collectionName = 'OrganizationUser';
|
|
5
|
+
|
|
6
|
+
const schema = new mongoose.Schema({
|
|
7
|
+
appOrgId: { type: String, required: true, unique: true },
|
|
8
|
+
name: { type: String },
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
schema.static({
|
|
12
|
+
getUserByAppOrgId: async function (appOrgId) {
|
|
13
|
+
const getByUser = await this.find({ appOrgId });
|
|
14
|
+
|
|
15
|
+
if (getByUser.length > 1) {
|
|
16
|
+
throw new Error(
|
|
17
|
+
'Supposedly using a unique appOrgId? Please reach out to our developers'
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (getByUser.length === 1) {
|
|
22
|
+
return getByUser[0];
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
const OrganizationUser = Parent.discriminators?.OrganizationUser || Parent.discriminator(collectionName, schema);
|
|
28
|
+
|
|
29
|
+
module.exports = {OrganizationUser};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
const { mongoose } = require('../mongoose');
|
|
2
|
+
const bcrypt = require('bcryptjs');
|
|
3
|
+
|
|
4
|
+
const collectionName = 'Token';
|
|
5
|
+
const decimals = 10;
|
|
6
|
+
|
|
7
|
+
const schema = new mongoose.Schema({
|
|
8
|
+
token: { type: String, required: true },
|
|
9
|
+
created: { type: Date, default: Date.now },
|
|
10
|
+
expires: { type: Date },
|
|
11
|
+
user: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
schema.static({
|
|
15
|
+
createTokenWithExpire: async function (userId, rawToken, minutes) {
|
|
16
|
+
// Create user token
|
|
17
|
+
let tokenHash = await bcrypt.hashSync(rawToken, parseInt(decimals));
|
|
18
|
+
|
|
19
|
+
let session = {
|
|
20
|
+
token: tokenHash,
|
|
21
|
+
expires: new Date(Date.now() + minutes * 60000).toISOString(),
|
|
22
|
+
user: userId,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
return this.create(session);
|
|
26
|
+
},
|
|
27
|
+
// Takes in a token object and that has been created in the database and the raw token value.
|
|
28
|
+
// Returns a json of just the token and id to return to the browser
|
|
29
|
+
createJSONToken: function (token, rawToken) {
|
|
30
|
+
let returnArr = {
|
|
31
|
+
id: token.id,
|
|
32
|
+
token: rawToken,
|
|
33
|
+
};
|
|
34
|
+
return JSON.stringify(returnArr);
|
|
35
|
+
},
|
|
36
|
+
// Takes in a token object and that has been created in the database and the raw token value.
|
|
37
|
+
// Returns a base64 buffer of just the token and id to return to the browser
|
|
38
|
+
createBase64BufferToken: function (token, rawToken) {
|
|
39
|
+
let jsonVal = Token.createJSONToken(token, rawToken);
|
|
40
|
+
return Buffer.from(jsonVal).toString('base64');
|
|
41
|
+
},
|
|
42
|
+
getJSONTokenFromBase64BufferToken: function (buffer) {
|
|
43
|
+
let tokenStr = Buffer.from(buffer.trim(), 'base64').toString('ascii');
|
|
44
|
+
return JSON.parse(tokenStr);
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
// Takes in a JSON Token with id and token in it and verifies the token
|
|
48
|
+
// is valid from the database. If it is not va
|
|
49
|
+
validateAndGetTokenFromJSONToken: async function (tokenObj) {
|
|
50
|
+
let sessionToken = await this.findById(tokenObj.id);
|
|
51
|
+
if (sessionToken) {
|
|
52
|
+
if (
|
|
53
|
+
!(await bcrypt.compareSync(tokenObj.token, sessionToken.token))
|
|
54
|
+
) {
|
|
55
|
+
throw new Error('Invalid Token: Token does not match');
|
|
56
|
+
}
|
|
57
|
+
if (new Date(sessionToken.expires) < new Date()) {
|
|
58
|
+
throw new Error('Invalid Token: Token is expired');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return sessionToken;
|
|
62
|
+
} else {
|
|
63
|
+
throw new Error('Invalid Token: Token does not exist');
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
const Token = mongoose.models.Token || mongoose.model(collectionName, schema);
|
|
69
|
+
|
|
70
|
+
module.exports = { Token };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Best Practices Connecting from AWS Lambda:
|
|
2
|
+
// https://dev.to/adnanrahic/building-a-serverless-rest-api-with-nodejs-and-mongodb-43db
|
|
3
|
+
// https://mongoosejs.com/docs/lambda.html
|
|
4
|
+
// https://www.mongodb.com/blog/post/optimizing-aws-lambda-performance-with-mongodb-atlas-and-nodejs
|
|
5
|
+
const { Encrypt } = require('../encrypt');
|
|
6
|
+
const { mongoose } = require('./mongoose');
|
|
7
|
+
const { debug, flushDebugLog } = require('../logs');
|
|
8
|
+
|
|
9
|
+
mongoose.plugin(Encrypt);
|
|
10
|
+
mongoose.set('applyPluginsToDiscriminators', true); // Needed for LHEncrypt
|
|
11
|
+
|
|
12
|
+
// Buffering means mongoose will queue up operations if it gets
|
|
13
|
+
// With serverless, better to fail fast if not connected.
|
|
14
|
+
// disconnected from MongoDB and send them when it reconnects.
|
|
15
|
+
const mongoConfig = {
|
|
16
|
+
useNewUrlParser: true,
|
|
17
|
+
bufferCommands: false, // Disable mongoose buffering
|
|
18
|
+
autoCreate: false, // Disable because auto creation does not work without buffering
|
|
19
|
+
useUnifiedTopology: true,
|
|
20
|
+
serverSelectionTimeoutMS: 5000,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const checkIsConnected = () => mongoose.connection?.readyState > 0;
|
|
24
|
+
|
|
25
|
+
const connectToDatabase = async () => {
|
|
26
|
+
if (checkIsConnected()) {
|
|
27
|
+
debug('=> using existing database connection');
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
debug('=> using new database connection');
|
|
32
|
+
await mongoose.connect(process.env.MONGO_URI, mongoConfig);
|
|
33
|
+
debug('Connection state:', mongoose.STATES[mongoose.connection.readyState]);
|
|
34
|
+
mongoose.connection.on('error', (error) => flushDebugLog(error));
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const disconnectFromDatabase = async () => mongoose.disconnect();
|
|
38
|
+
|
|
39
|
+
const createObjectId = () => new mongoose.Types.ObjectId();
|
|
40
|
+
|
|
41
|
+
module.exports = {
|
|
42
|
+
connectToDatabase,
|
|
43
|
+
disconnectFromDatabase,
|
|
44
|
+
createObjectId,
|
|
45
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# v1.1.8 (Fri Feb 02 2024)
|
|
2
|
+
|
|
3
|
+
#### 🐛 Bug Fix
|
|
4
|
+
|
|
5
|
+
- Added variable BYPASS_ENCRYPTION_STAGE to encrypt module [#248](https://github.com/friggframework/frigg/pull/248) ([@leofmds](https://github.com/leofmds))
|
|
6
|
+
- Added variable BYPASS_ENCRYPTION_STAGE to encrypt module ([@leofmds](https://github.com/leofmds))
|
|
7
|
+
|
|
8
|
+
#### Authors: 1
|
|
9
|
+
|
|
10
|
+
- Leonardo Ferreira ([@leofmds](https://github.com/leofmds))
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# v1.1.7 (Tue Apr 04 2023)
|
|
15
|
+
|
|
16
|
+
:tada: This release contains work from a new contributor! :tada:
|
|
17
|
+
|
|
18
|
+
Thank you, null[@debbie-yu](https://github.com/debbie-yu), for all your work!
|
|
19
|
+
|
|
20
|
+
#### 🐛 Bug Fix
|
|
21
|
+
|
|
22
|
+
- Adding new IntegrationMapping collection [#142](https://github.com/friggframework/frigg/pull/142) ([@debbie-yu](https://github.com/debbie-yu))
|
|
23
|
+
- Merge branch 'main' of https://github.com/friggframework/frigg into debbie.yu/integration-mapping ([@debbie-yu](https://github.com/debbie-yu))
|
|
24
|
+
- addressing PR feedback and adding unit tests around IntegrationMapping ([@debbie-yu](https://github.com/debbie-yu))
|
|
25
|
+
- Bump independent versions \[skip ci\] ([@seanspeaks](https://github.com/seanspeaks))
|
|
26
|
+
|
|
27
|
+
#### Authors: 2
|
|
28
|
+
|
|
29
|
+
- [@debbie-yu](https://github.com/debbie-yu)
|
|
30
|
+
- Sean Matthews ([@seanspeaks](https://github.com/seanspeaks))
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
# v1.1.6 (Tue Jan 31 2023)
|
|
35
|
+
|
|
36
|
+
#### 🐛 Bug Fix
|
|
37
|
+
|
|
38
|
+
- Bump independent versions \[skip ci\] ([@seanspeaks](https://github.com/seanspeaks))
|
|
39
|
+
|
|
40
|
+
#### Authors: 1
|
|
41
|
+
|
|
42
|
+
- Sean Matthews ([@seanspeaks](https://github.com/seanspeaks))
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
# v1.1.5 (Mon Jan 09 2023)
|
|
47
|
+
|
|
48
|
+
#### 🐛 Bug Fix
|
|
49
|
+
|
|
50
|
+
- Merge remote-tracking branch 'origin/main' into gitbook-updates [#48](https://github.com/friggframework/frigg/pull/48) ([@seanspeaks](https://github.com/seanspeaks))
|
|
51
|
+
- Bump independent versions \[skip ci\] ([@seanspeaks](https://github.com/seanspeaks))
|
|
52
|
+
- Merge remote-tracking branch 'origin/main' into simplify-mongoose-models ([@seanspeaks](https://github.com/seanspeaks))
|
|
53
|
+
- Add READMEs for all packages and api-modules [#20](https://github.com/friggframework/frigg/pull/20) ([@seanspeaks](https://github.com/seanspeaks))
|
|
54
|
+
- Add READMEs for all packages and api-modules ([@seanspeaks](https://github.com/seanspeaks))
|
|
55
|
+
|
|
56
|
+
#### ⚠️ Pushed to `main`
|
|
57
|
+
|
|
58
|
+
- Merge branch 'main' into gitbook-updates ([@seanspeaks](https://github.com/seanspeaks))
|
|
59
|
+
- Refactored for more conventional naming (at least for packages) ([@seanspeaks](https://github.com/seanspeaks))
|
|
60
|
+
|
|
61
|
+
#### Authors: 1
|
|
62
|
+
|
|
63
|
+
- Sean Matthews ([@seanspeaks](https://github.com/seanspeaks))
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
# v1.1.4 (Tue Dec 06 2022)
|
|
68
|
+
|
|
69
|
+
#### 🐛 Bug Fix
|
|
70
|
+
|
|
71
|
+
- fix modules to @friggframework [#74](https://github.com/friggframework/frigg/pull/74) ([@sheehantoufiq](https://github.com/sheehantoufiq))
|
|
72
|
+
- fix modules to @friggframework ([@sheehantoufiq](https://github.com/sheehantoufiq))
|
|
73
|
+
- Bump independent versions \[skip ci\] ([@seanspeaks](https://github.com/seanspeaks))
|
|
74
|
+
|
|
75
|
+
#### Authors: 2
|
|
76
|
+
|
|
77
|
+
- Sean Matthews ([@seanspeaks](https://github.com/seanspeaks))
|
|
78
|
+
- Sheehan Toufiq Khan ([@sheehantoufiq](https://github.com/sheehantoufiq))
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
2
|
+
const AWS = require('aws-sdk');
|
|
3
|
+
const { get, set } = require('lodash');
|
|
4
|
+
const aes = require('./aes');
|
|
5
|
+
|
|
6
|
+
const hasValue = (a) => a !== undefined && a !== null && a !== '';
|
|
7
|
+
|
|
8
|
+
class Cryptor {
|
|
9
|
+
constructor({ fields, shouldUseAws }) {
|
|
10
|
+
this.shouldUseAws = shouldUseAws;
|
|
11
|
+
this.fields = fields;
|
|
12
|
+
|
|
13
|
+
this.permutationsByField = {};
|
|
14
|
+
|
|
15
|
+
for (const field of fields) {
|
|
16
|
+
this.permutationsByField[field] = this.calculatePermutations(
|
|
17
|
+
field.split('.')
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async generateDataKey() {
|
|
23
|
+
if (this.shouldUseAws) {
|
|
24
|
+
const kmsClient = new AWS.KMS();
|
|
25
|
+
const dataKey = await kmsClient
|
|
26
|
+
.generateDataKey({
|
|
27
|
+
KeyId: process.env.KMS_KEY_ARN,
|
|
28
|
+
KeySpec: 'AES_256',
|
|
29
|
+
})
|
|
30
|
+
.promise();
|
|
31
|
+
|
|
32
|
+
const keyId = Buffer.from(dataKey.KeyId).toString('base64');
|
|
33
|
+
const encryptedKey = dataKey.CiphertextBlob.toString('base64');
|
|
34
|
+
const plaintext = dataKey.Plaintext;
|
|
35
|
+
return { keyId, encryptedKey, plaintext };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const { AES_KEY, AES_KEY_ID } = process.env;
|
|
39
|
+
const randomKey = crypto.randomBytes(32).toString('hex').slice(0, 32);
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
keyId: Buffer.from(AES_KEY_ID).toString('base64'),
|
|
43
|
+
encryptedKey: Buffer.from(aes.encrypt(randomKey, AES_KEY)).toString(
|
|
44
|
+
'base64'
|
|
45
|
+
),
|
|
46
|
+
plaintext: randomKey,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
getKeyFromEnvironment(keyId) {
|
|
51
|
+
const availableKeys = {
|
|
52
|
+
[process.env.AES_KEY_ID]: process.env.AES_KEY,
|
|
53
|
+
[process.env.DEPRECATED_AES_KEY_ID]: process.env.DEPRECATED_AES_KEY,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const key = availableKeys[keyId];
|
|
57
|
+
|
|
58
|
+
if (!key) {
|
|
59
|
+
throw new Error(`No encryption key found with ID "${keyId}"`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return key;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async decryptDataKey(keyId, encryptedKey) {
|
|
66
|
+
if (this.shouldUseAws) {
|
|
67
|
+
const kmsClient = new AWS.KMS();
|
|
68
|
+
const dataKey = await kmsClient
|
|
69
|
+
.decrypt({
|
|
70
|
+
KeyId: keyId,
|
|
71
|
+
CiphertextBlob: encryptedKey,
|
|
72
|
+
})
|
|
73
|
+
.promise();
|
|
74
|
+
|
|
75
|
+
return dataKey.Plaintext;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const key = this.getKeyFromEnvironment(keyId);
|
|
79
|
+
return aes.decrypt(encryptedKey, key);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// If the field has a value in the document, apply async function f to that field.
|
|
83
|
+
async setInDocument(doc, f) {
|
|
84
|
+
// Use the Mongoose document get/set when available (not for insertMany)
|
|
85
|
+
if (doc.get) {
|
|
86
|
+
for (const field of this.fields) {
|
|
87
|
+
const value = doc.get(field);
|
|
88
|
+
if (hasValue(value)) {
|
|
89
|
+
doc.set(field, await f(value));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Otherwise use permutations.
|
|
96
|
+
for (const field of this.fields) {
|
|
97
|
+
const updatedDoc = await this.applyAll(doc, field, f);
|
|
98
|
+
Object.assign(doc, updatedDoc);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Calculate all possible permutations for a nested field. For example a
|
|
103
|
+
// field "deeply.nested.field" might be referred to in a Mongo query as
|
|
104
|
+
// { deeply: { 'nested.field': {} } } or { 'deeply.nested.field': {} }
|
|
105
|
+
// etc. For a given path, this gives all path parts to check in a format
|
|
106
|
+
// that lodash understands when using get and set with an array of path
|
|
107
|
+
// parts e.g. get(o, ['deeply', 'nested.parts'])
|
|
108
|
+
calculatePermutations = (parts) => {
|
|
109
|
+
if (!parts.length) return [];
|
|
110
|
+
if (parts.length === 1) return [parts];
|
|
111
|
+
|
|
112
|
+
const combos = [];
|
|
113
|
+
|
|
114
|
+
for (let i = 0; i < parts.length; i += 1) {
|
|
115
|
+
const frontPath = parts.slice(0, i + 1).join('.');
|
|
116
|
+
const rest = parts.slice(i + 1);
|
|
117
|
+
|
|
118
|
+
if (rest.length) {
|
|
119
|
+
combos.push(
|
|
120
|
+
...this.calculatePermutations(rest).map((child) => [
|
|
121
|
+
frontPath,
|
|
122
|
+
...child,
|
|
123
|
+
])
|
|
124
|
+
);
|
|
125
|
+
} else {
|
|
126
|
+
combos.push([frontPath]);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return combos;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
// Encrypt all possible permutations of a field (possibly nested), if there
|
|
134
|
+
// is a value at that path permutation.
|
|
135
|
+
async applyAll(o, field, f) {
|
|
136
|
+
const clone = { ...o };
|
|
137
|
+
const permutations = this.permutationsByField[field];
|
|
138
|
+
|
|
139
|
+
for (const path of permutations) {
|
|
140
|
+
const value = get(o, path);
|
|
141
|
+
if (hasValue(value)) {
|
|
142
|
+
set(clone, path, await f(value));
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return clone;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async processFieldsInDocuments(docs, f) {
|
|
150
|
+
const promises = docs
|
|
151
|
+
.filter(Boolean)
|
|
152
|
+
.flatMap((doc) => this.setInDocument(doc, f));
|
|
153
|
+
|
|
154
|
+
return Promise.all(promises);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async encryptFieldsInDocuments(docs) {
|
|
158
|
+
await this.processFieldsInDocuments(docs, this.encrypt.bind(this));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async decryptFieldsInDocuments(docs) {
|
|
162
|
+
await this.processFieldsInDocuments(docs, this.decrypt.bind(this));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async encryptFieldsInQuery(query) {
|
|
166
|
+
for (const field of this.fields) {
|
|
167
|
+
const originalUpdate = query.getUpdate();
|
|
168
|
+
const updatedUpdate = await this.applyAll(
|
|
169
|
+
originalUpdate,
|
|
170
|
+
field,
|
|
171
|
+
this.encrypt.bind(this)
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
if (originalUpdate.$set) {
|
|
175
|
+
const updatedSetUpdate = await this.applyAll(
|
|
176
|
+
originalUpdate.$set,
|
|
177
|
+
field,
|
|
178
|
+
this.encrypt.bind(this)
|
|
179
|
+
);
|
|
180
|
+
updatedUpdate.$set = { ...updatedSetUpdate };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (originalUpdate.$setOnInsert) {
|
|
184
|
+
const updatedSetOnInsertUpdate = await this.applyAll(
|
|
185
|
+
originalUpdate.$setOnInsert,
|
|
186
|
+
field,
|
|
187
|
+
this.encrypt.bind(this)
|
|
188
|
+
);
|
|
189
|
+
updatedUpdate.$setOnInsert = { ...updatedSetOnInsertUpdate };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
query.setUpdate(updatedUpdate);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
expectNotToUpdateManyEncrypted(update) {
|
|
197
|
+
for (const field of this.fields) {
|
|
198
|
+
if (update.$set && hasValue(update.$set[field])) {
|
|
199
|
+
throw new Error(
|
|
200
|
+
'Attempted to update encrypted field of multiple documents'
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (update.$setOnInsert && hasValue(update.$setOnInsert[field])) {
|
|
205
|
+
throw new Error(
|
|
206
|
+
'Attempted to update encrypted field of multiple documents'
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (hasValue(update[field])) {
|
|
211
|
+
throw new Error(
|
|
212
|
+
'Attempted to update encrypted field of multiple documents'
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async encrypt(text) {
|
|
219
|
+
const { keyId, encryptedKey, plaintext } = await this.generateDataKey();
|
|
220
|
+
const encryptedText = aes.encrypt(text, plaintext);
|
|
221
|
+
|
|
222
|
+
return `${keyId}:${encryptedText}:${encryptedKey}`;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async decrypt(text) {
|
|
226
|
+
const split = text.split(':');
|
|
227
|
+
const keyId = Buffer.from(split[0], 'base64').toString();
|
|
228
|
+
const encryptedText = `${split[1]}:${split[2]}`;
|
|
229
|
+
const encryptedKey = Buffer.from(split[3], 'base64');
|
|
230
|
+
const plaintext = await this.decryptDataKey(keyId, encryptedKey);
|
|
231
|
+
|
|
232
|
+
return aes.decrypt(encryptedText, plaintext);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
module.exports = { Cryptor };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const { Cryptor } = require('./Cryptor');
|
|
2
|
+
|
|
3
|
+
describe('Cryptor', () => {
|
|
4
|
+
describe('Permutations', () => {
|
|
5
|
+
it('calculates permutations correctly', async () => {
|
|
6
|
+
// Given a nested field, we want all possible paths that could access it.
|
|
7
|
+
const cryptor = new Cryptor({ fields: ['a.b.c.d', 'e'] });
|
|
8
|
+
expect(cryptor.permutationsByField).toEqual({
|
|
9
|
+
'a.b.c.d': [
|
|
10
|
+
['a', 'b', 'c', 'd'],
|
|
11
|
+
['a', 'b', 'c.d'],
|
|
12
|
+
['a', 'b.c', 'd'],
|
|
13
|
+
['a', 'b.c.d'],
|
|
14
|
+
['a.b', 'c', 'd'],
|
|
15
|
+
['a.b', 'c.d'],
|
|
16
|
+
['a.b.c', 'd'],
|
|
17
|
+
['a.b.c.d'],
|
|
18
|
+
],
|
|
19
|
+
e: [['e']],
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe('Keys', () => {
|
|
25
|
+
it('raises error on missing environment', () => {
|
|
26
|
+
const cryptor = new Cryptor({ fields: ['a.b.c.d', 'e'] });
|
|
27
|
+
expect(cryptor.getKeyFromEnvironment).toThrow(
|
|
28
|
+
'No encryption key found with ID "undefined"'
|
|
29
|
+
);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022 Left Hook Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# encrypt
|
|
2
|
+
|
|
3
|
+
This package exports the `encrypt` mongoose plugin used in [Frigg](https://friggframework.org). You can find its documentation [on Frigg's website](https://docs.friggframework.org/packages/encrypt).
|
|
4
|
+
|
|
5
|
+
## Configuration
|
|
6
|
+
|
|
7
|
+
| Environment variable | Description |
|
|
8
|
+
|-------------------------|------------------------------------------------------------------------------------------------------------|
|
|
9
|
+
| KMS_KEY_ARN | The AWS KMS Key ARN, if using it to encryption/decryption. |
|
|
10
|
+
| AES_KEY | AES key, used in conjunction with AES_KEY_ID. AES option is mutually exclusive with KMS_KEY_ARN. |
|
|
11
|
+
| AES_KEY_ID | AES key ID, used in conjunction with AES_KEY. |
|
|
12
|
+
| STAGE | The stage in which the application is running. It is usually defined in Serverless configuration, if used. |
|
|
13
|
+
| BYPASS_ENCRYPTION_STAGE | Stages to bypass encryption/decryption, separated by comma. |
|
package/encrypt/aes.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
2
|
+
|
|
3
|
+
const algorithm = 'aes-256-ctr';
|
|
4
|
+
|
|
5
|
+
function encrypt(text, key) {
|
|
6
|
+
const iv = crypto.randomBytes(16);
|
|
7
|
+
const randomString = iv.toString('hex');
|
|
8
|
+
|
|
9
|
+
const cipher = crypto.createCipheriv(algorithm, key, iv);
|
|
10
|
+
let crypted = cipher.update(text, 'utf8', 'hex');
|
|
11
|
+
crypted += cipher.final('hex');
|
|
12
|
+
return `${randomString}:${crypted}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function decrypt(text, key) {
|
|
16
|
+
const parts = text.toString().split(':');
|
|
17
|
+
const iv = Buffer.from(parts[0], 'hex');
|
|
18
|
+
const decipher = crypto.createDecipheriv(algorithm, key, iv);
|
|
19
|
+
let dec = decipher.update(parts[1], 'hex', 'utf8');
|
|
20
|
+
dec += decipher.final('utf8');
|
|
21
|
+
return dec;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
module.exports = {
|
|
25
|
+
encrypt,
|
|
26
|
+
decrypt,
|
|
27
|
+
};
|
|
File without changes
|