@dhyasama/totem-merge 7.0.3 → 7.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/bootstrap ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ var runtimeSecrets = require('../lib/runtimeSecrets');
5
+
6
+ runtimeSecrets.loadRuntimeSecrets()
7
+ .then(function() { require('../cli'); })
8
+ .catch(function(error) {
9
+ console.error('runtime_secret_bootstrap_failed', { code: error.name || error.code || 'UnknownError' });
10
+ process.exit(1);
11
+ });
package/config.js CHANGED
@@ -8,12 +8,8 @@ module.exports = {
8
8
 
9
9
  suppressDebugLog: true,
10
10
 
11
- // db: {
12
- // uri: 'mongodb://ffvc:ytnn5m7A9snIJSOLRqwo7Bpo2QkyYx6q@ds043952.mongolab.com:43952/ff-platform-test'
13
- // },
14
-
15
11
  db: {
16
- uri: 'mongodb://jason:T576G150HPXLA5q4BJ2o2zj747B5030x@development-shard-00-00-gv0f3.mongodb.net:27017,development-shard-00-01-gv0f3.mongodb.net:27017,development-shard-00-02-gv0f3.mongodb.net:27017/development?ssl=true&replicaSet=Development-shard-0&authSource=admin'
12
+ uri: process.env.DB_URI || ''
17
13
  },
18
14
 
19
15
  crypto: {
package/index.js CHANGED
@@ -23,12 +23,6 @@ var Merge = function(mongoose, config) {
23
23
  this.Round = mongoose.model('Round');
24
24
  this.Snapshot = mongoose.model('Snapshot');
25
25
 
26
- // can choose to flag an entity as needing to be merged rather than actually merging it
27
- // particularly useful for automated processes, e.g., a batch import that detects dupes
28
- // but you don't want to auto-merge 1000 records
29
- // defaults to false
30
- this.flagOnly = config.flag == true ? true : false;
31
-
32
26
  // turn on debug if in development and not explicitly turned off
33
27
  mongoose.set('debug', (!config.suppressDebugLog && this.env == 'development'));
34
28
 
@@ -0,0 +1,91 @@
1
+ 'use strict';
2
+ const assert = require('node:assert/strict');
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const vm = require('node:vm');
6
+ const { test } = require('node:test');
7
+ const root = path.resolve(__dirname, '../..');
8
+ const files = fs.existsSync(path.join(root, 'lib/runtimeSecrets.js'))
9
+ ? ['lib/runtimeSecrets.js']
10
+ : fs.readdirSync(root).flatMap(dir => ['runtimeSecrets.js', 'runtime_secrets.js']
11
+ .map(name => path.join(dir, 'src/libs', name)).filter(file => fs.existsSync(path.join(root, file))));
12
+ assert(files.length > 0, 'No runtime loaders found');
13
+
14
+ function fixture(file, env = {}) {
15
+ const process = { env: { NODE_ENV: 'test', ...env } };
16
+ const module = { exports: {} };
17
+ const warnings = [];
18
+ class Client { constructor(options) { this.options = options; } }
19
+ class Command { constructor(input) { this.input = input; } }
20
+ const sdk = { SecretsManager: Client, SecretsManagerClient: Client, GetSecretValueCommand: Command };
21
+ vm.runInNewContext(fs.readFileSync(path.join(root, file), 'utf8'), {
22
+ module, exports: module.exports, process, Buffer,
23
+ console: { warn: (...args) => warnings.push(args), info: () => {} },
24
+ require: name => {
25
+ assert(['aws-sdk', '@aws-sdk/client-secrets-manager'].includes(name));
26
+ return sdk;
27
+ }
28
+ }, { filename: file });
29
+ const loader = module.exports;
30
+ const values = Object.fromEntries((loader.REQUIRED_KEYS || ['DB_URI']).map(key => [key, 'synthetic-' + key]));
31
+ const response = () => ({ SecretString: JSON.stringify(values) });
32
+ const client = fn => ({ send: fn, getSecretValue: () => ({ promise: fn }) });
33
+ return { loader, env: process.env, values, response, client, warnings };
34
+ }
35
+
36
+ for (const file of files) {
37
+ test(file + ': explicit secret ID required in deployed runtimes', async () => {
38
+ for (const env of [
39
+ { NODE_ENV: 'production', RUNTIME_SECRET_REQUIRED: 'false' },
40
+ { AWS_LAMBDA_FUNCTION_NAME: 'synthetic-function', RUNTIME_SECRET_REQUIRED: 'false' },
41
+ { RUNTIME_SECRET_REQUIRED: 'true' },
42
+ { NODE_ENV: 'production', RUNTIME_SECRET_ID: ' ' }
43
+ ]) {
44
+ const f = fixture(file, env);
45
+ let called = false;
46
+ await assert.rejects(f.loader.loadRuntimeSecrets({ client: f.client(async () => { called = true; }) }), /RUNTIME_SECRET_ID is required/);
47
+ assert.equal(called, false);
48
+ }
49
+ });
50
+ test(file + ': failed production fetch can recover without restarting', async () => {
51
+ const f = fixture(file, { NODE_ENV: 'production', RUNTIME_SECRET_ID: '/synthetic/secret', RUNTIME_SECRET_REQUIRED: 'false' });
52
+ let calls = 0;
53
+ const client = f.client(async () => { if (++calls === 1) throw new Error('synthetic transient failure'); return f.response(); });
54
+ await assert.rejects(f.loader.loadRuntimeSecrets({ client }), /synthetic transient failure/);
55
+ assert.equal((await f.loader.loadRuntimeSecrets({ client })).source, 'secrets_manager');
56
+ assert.equal(calls, 2);
57
+ assert.equal(f.warnings.length, 0);
58
+ });
59
+ test(file + ': concurrent calls share one fetch and successful values stay cached', async () => {
60
+ const f = fixture(file, { RUNTIME_SECRET_ID: '/synthetic/secret' });
61
+ let calls = 0, resolve;
62
+ const client = f.client(() => { calls++; return new Promise(done => { resolve = done; }); });
63
+ const first = f.loader.loadRuntimeSecrets({ client });
64
+ const second = f.loader.loadRuntimeSecrets({ client });
65
+ assert.equal(first, second);
66
+ resolve(f.response());
67
+ await Promise.all([first, second]);
68
+ await f.loader.loadRuntimeSecrets({ client });
69
+ assert.equal(calls, 1);
70
+ for (const key of Object.keys(f.values)) assert.equal(f.env[key], f.values[key]);
71
+ });
72
+ test(file + ': invalid secret never partially overwrites configuration and can retry', async () => {
73
+ const f = fixture(file, { NODE_ENV: 'production', RUNTIME_SECRET_ID: '/synthetic/secret' });
74
+ const keys = Object.keys(f.values);
75
+ for (const key of keys) f.env[key] = 'old-synthetic-value';
76
+ let calls = 0;
77
+ const incomplete = { ...f.values, [keys.at(-1)]: '' };
78
+ const client = f.client(async () => ++calls === 1 ? { SecretString: JSON.stringify(incomplete) } : f.response());
79
+ await assert.rejects(f.loader.loadRuntimeSecrets({ client }));
80
+ for (const key of keys) assert.equal(f.env[key], 'old-synthetic-value');
81
+ assert.equal((await f.loader.loadRuntimeSecrets({ client })).source, 'secrets_manager');
82
+ });
83
+ test(file + ': development fallback does not prevent a later successful fetch', async () => {
84
+ const f = fixture(file, { RUNTIME_SECRET_ID: '/synthetic/secret' });
85
+ let calls = 0;
86
+ const client = f.client(async () => { if (++calls === 1) throw new Error('synthetic denied'); return f.response(); });
87
+ try { await f.loader.loadRuntimeSecrets({ client }); } catch (error) { assert.match(error.message, /synthetic denied/); }
88
+ assert.equal((await f.loader.loadRuntimeSecrets({ client })).source, 'secrets_manager');
89
+ assert.equal(calls, 2);
90
+ });
91
+ }
@@ -0,0 +1,16 @@
1
+ # Runtime secret loading and credential cleanup
2
+
3
+ Successful runtime secrets are loaded once, before configuration, clients, and
4
+ handlers initialize. Values remain cached until process replacement. Failed
5
+ fetches and development fallback results are not cached, so a later call can
6
+ retry; concurrent calls share one pending fetch. AWS SDK retries are bounded
7
+ (three attempts). Invalid secret contents never partially update configuration.
8
+
9
+ Production (`NODE_ENV=production`), deployed Lambda runtimes, and any runtime
10
+ with `RUNTIME_SECRET_REQUIRED=true` require an explicit, nonblank
11
+ `RUNTIME_SECRET_ID`. Setting the required flag to false cannot enable legacy
12
+ fallback in production or Lambda. Explicit development defaults remain unchanged.
13
+
14
+ See the [shared restart and cleanup runbook](https://github.com/Totem-VC/totem-services/blob/main/infrastructure/security-checks/runtime-secrets.md) for release verification, credential cleanup, and remaining limitations.
15
+
16
+ Run `node --test infrastructure/security-checks/runtime-secret-contract.cjs` for isolated loader checks. These source changes require the normal application release to affect running processes. No credential was rotated.
package/lib/helpers.js CHANGED
@@ -43,6 +43,10 @@ var mergeObjects = function mergeObjects(target, source, doNotMergeKeys) {
43
43
 
44
44
  for (var key in jsonSource) {
45
45
 
46
+ // Never follow prototype paths or merge inherited source properties.
47
+ // Check before reading target[key], which may resolve to a prototype.
48
+ if (!Object.prototype.hasOwnProperty.call(jsonSource, key)) continue;
49
+ if (key === '__proto__' || key === 'constructor' || key === 'prototype') continue;
46
50
  if (doNotMergeKeys.indexOf(key) >= 0) continue;
47
51
 
48
52
  var sourceItem = jsonSource[key];
@@ -70,12 +74,26 @@ var mergeObjects = function mergeObjects(target, source, doNotMergeKeys) {
70
74
 
71
75
  };
72
76
 
73
- var removeEntity = module.exports.removeEntity = function removeEntity(context, entity, cb) {
77
+ var removeEntity = module.exports.removeEntity = function removeEntity(context, modelName, entity, cb) {
74
78
 
75
79
  context.logger.writeToInfoLog('Removing entity - ' + entity._id.toString());
76
80
 
77
- entity.deleteOne().then(function() { cb(null, entity); }).catch(cb);
81
+ // entity may be a Mongoose document or a lean POJO (e.g. from Person.getById which uses .lean())
82
+ if (typeof entity.deleteOne === 'function') {
83
+ entity.deleteOne().then(function() { cb(null, entity); }).catch(cb);
84
+ }
85
+ else {
86
+ context[modelName].deleteOne({ _id: entity._id }).then(function() { cb(null, entity); }).catch(cb);
87
+ }
88
+
89
+ };
78
90
 
91
+ // Add source id to target.merged, deduped. Works on POJOs and Mongoose docs alike.
92
+ var addMerged = module.exports.addMerged = function addMerged(target, sourceId) {
93
+ target.merged = target.merged || [];
94
+ var sourceIdStr = sourceId.toString();
95
+ var alreadyMerged = _.some(target.merged, function(m) { return m && m.toString() === sourceIdStr; });
96
+ if (!alreadyMerged) { target.merged = target.merged.concat([sourceId]); }
79
97
  };
80
98
 
81
99
  // Used for determining if need to dive in recursively
@@ -32,70 +32,54 @@ var merge = function merge(context, target, source, mergedBy, customerId, cb) {
32
32
 
33
33
  };
34
34
 
35
- if (context.flagOnly) {
36
-
37
- var url = 'https://admin.ffvc.com/orgs/' + source.id;
38
- var text = 'This org should be merged with ' + url;
39
-
40
- context.Organization.addFlag(target._id, target._id, text, customerId, function(err, result) {
41
- if (err) return cb(err, null);
42
- return cb(null, result);
43
- });
44
-
45
- }
46
-
47
- else {
48
-
49
- // mergedBy can be either an account or a username
50
- var username = mergedBy.username ? mergedBy.username : mergedBy;
51
- logger.writeToInfoLog('Username: ' + username);
52
-
53
- async.series([
35
+ // mergedBy can be either an account or a username
36
+ var username = mergedBy.username ? mergedBy.username : mergedBy;
37
+ logger.writeToInfoLog('Username: ' + username);
54
38
 
55
- verifyOkToMerge.bind(null, target, source),
39
+ async.series([
56
40
 
57
- // Create a snapshot of the target
58
- helpers.createSnapshot.bind(null, context, 'Organization', target, username),
41
+ verifyOkToMerge.bind(null, target, source),
59
42
 
60
- // Create a snapshot of the source
61
- helpers.createSnapshot.bind(null, context, 'Organization', source, username),
43
+ // Create a snapshot of the target
44
+ helpers.createSnapshot.bind(null, context, 'Organization', target, username),
62
45
 
63
- // Merge the source into the target
64
- mergeSourceIntoTarget.bind(null, target, source),
46
+ // Create a snapshot of the source
47
+ helpers.createSnapshot.bind(null, context, 'Organization', source, username),
65
48
 
66
- // Delete the source
67
- // Do this prior to saving merged target in order to avoid unique key issues
68
- helpers.removeEntity.bind(null, context, source),
49
+ // Merge the source into the target
50
+ mergeSourceIntoTarget.bind(null, target, source),
69
51
 
70
- // Save the updated target
71
- saveOrganization.bind(null, target, username),
52
+ // Delete the source
53
+ // Do this prior to saving merged target in order to avoid unique key issues
54
+ helpers.removeEntity.bind(null, context, 'Organization', source),
72
55
 
73
- // Change source references to target
74
- updateReferences.bind(null, context, target, source, mergedBy, customerId)
56
+ // Save the updated target
57
+ saveOrganization.bind(null, target, username),
75
58
 
76
- ], function(err, results) {
59
+ // Change source references to target
60
+ updateReferences.bind(null, context, target, source, mergedBy, customerId)
77
61
 
78
- if (err) { return cb(err, null); }
62
+ ], function(err, results) {
79
63
 
80
- // result from saveOrganization
81
- var updatedTarget = results[5];
64
+ if (err) { return cb(err, null); }
82
65
 
83
- // result from updateOrganizations within updateReferences
84
- var updatedReferencesOrgs = results[6][5];
66
+ // result from saveOrganization
67
+ var updatedTarget = results[5];
85
68
 
86
- // Check if target had reference(s) updated
87
- var targetIdStr = target.id && target.id.toString();
88
- var targetMatch = targetIdStr && _.find(updatedReferencesOrgs, function(o) {
89
- return o && o.id && o.id.toString() === targetIdStr;
90
- });
91
-
92
- // Return up to date target
93
- if (targetMatch) { return cb(null, targetMatch); }
94
- else { return cb(null, updatedTarget); }
69
+ // result from updateOrganizations within updateReferences
70
+ var updatedReferencesOrgs = results[6][5];
95
71
 
72
+ // Check if target had reference(s) updated
73
+ var targetIdStr = target.id && target.id.toString();
74
+ var targetMatch = targetIdStr && _.find(updatedReferencesOrgs, function(o) {
75
+ return o && o.id && o.id.toString() === targetIdStr;
96
76
  });
97
77
 
98
- }
78
+ // Return up to date target
79
+ if (targetMatch) { return cb(null, targetMatch); }
80
+ else { return cb(null, updatedTarget); }
81
+
82
+ });
99
83
 
100
84
  };
101
85
 
@@ -113,7 +97,7 @@ var mergeSourceIntoTarget = function mergeEntities(target, source, cb) {
113
97
  helpers.mergeObjects(target, source, doNotMergeKeys);
114
98
 
115
99
  // keep a list of copied orgs
116
- target.merge(source.id);
100
+ helpers.addMerged(target, source.id);
117
101
 
118
102
  // add copied org name as an alias if it's different from source name
119
103
  if (target.name.toLowerCase() != source.name.toLowerCase()) {
package/lib/people.js CHANGED
@@ -28,56 +28,41 @@ var merge = function merge(context, target, source, mergedBy, customerId, cb) {
28
28
 
29
29
  };
30
30
 
31
- if (context.flagOnly) {
32
-
33
- var url = 'https://admin.totemvc.com/people/' + source.id;
34
- var text = 'This person should be merged with ' + url;
35
-
36
- context.Person.addFlag(target._id, target._id, text, customerId, function(err, result) {
37
- if (err) return cb(err, null);
38
- return cb(null, result);
39
- });
40
-
41
- }
42
- else {
43
-
44
- // mergedBy can be either an account or a username
45
- var username = mergedBy.username ? mergedBy.username : mergedBy;
31
+ // mergedBy can be either an account or a username
32
+ var username = mergedBy.username ? mergedBy.username : mergedBy;
46
33
 
47
- async.series([
34
+ async.series([
48
35
 
49
- // Create a snapshot of the target
50
- helpers.createSnapshot.bind(null, context, 'Person', target, username),
36
+ // Create a snapshot of the target
37
+ helpers.createSnapshot.bind(null, context, 'Person', target, username),
51
38
 
52
- // Create a snapshot of the source
53
- helpers.createSnapshot.bind(null, context, 'Person', source, username),
39
+ // Create a snapshot of the source
40
+ helpers.createSnapshot.bind(null, context, 'Person', source, username),
54
41
 
55
- // Merge the source into the target
56
- mergeSourceIntoTarget.bind(null, target, source),
42
+ // Merge the source into the target
43
+ mergeSourceIntoTarget.bind(null, target, source),
57
44
 
58
- // Delete the source
59
- // Do this prior to saving merged target in order to avoid unique key issues
60
- helpers.removeEntity.bind(null, context, source),
45
+ // Delete the source
46
+ // Do this prior to saving merged target in order to avoid unique key issues
47
+ helpers.removeEntity.bind(null, context, 'Person', source),
61
48
 
62
- // Save the updated target
63
- savePerson.bind(null, target, username),
49
+ // Save the updated target
50
+ savePerson.bind(null, target, username),
64
51
 
65
- // Change source references to target
66
- updateReferences.bind(null, context, target, source, mergedBy)
52
+ // Change source references to target
53
+ updateReferences.bind(null, context, target, source, mergedBy)
67
54
 
68
- ], function(err, results) {
55
+ ], function(err, results) {
69
56
 
70
- if (err) return cb(err, null);
57
+ if (err) return cb(err, null);
71
58
 
72
- // result from savePerson
73
- var updatedTarget = results[4];
59
+ // result from savePerson
60
+ var updatedTarget = results[4];
74
61
 
75
- // return updated target
76
- return cb(null, updatedTarget);
62
+ // return updated target
63
+ return cb(null, updatedTarget);
77
64
 
78
- });
79
-
80
- }
65
+ });
81
66
 
82
67
  };
83
68
 
@@ -121,7 +106,7 @@ var mergeSourceIntoTarget = function mergeEntities(target, source, cb) {
121
106
  target.sources = mergeSources(target, source);
122
107
 
123
108
  // keep a list of copied people
124
- target.addMerged(source.id);
109
+ helpers.addMerged(target, source.id);
125
110
 
126
111
  // add copied person name as an alias if it's different from source name
127
112
  if (target.name.full.toLowerCase() != source.name.full.toLowerCase()) {
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+
3
+ var SecretsManager = require('@aws-sdk/client-secrets-manager');
4
+ var SECRET_ID = '/totem/dev/totem-merge';
5
+ var loadPromise;
6
+
7
+ // Production and deployed Lambda runtimes must never use legacy credentials.
8
+ const secretsRequired = () => process.env.RUNTIME_SECRET_REQUIRED === 'true' ||
9
+ process.env.NODE_ENV === 'production' || Boolean(process.env.AWS_LAMBDA_FUNCTION_NAME);
10
+
11
+ function applySecret(response) {
12
+ var values = JSON.parse(response.SecretString || Buffer.from(response.SecretBinary).toString('utf8'));
13
+ if (!values.DB_URI || typeof values.DB_URI !== 'string') throw new Error('Runtime secret missing DB_URI');
14
+ process.env.DB_URI = values.DB_URI;
15
+ return { source: 'secrets_manager' };
16
+ }
17
+
18
+ async function load(client) {
19
+ var secretId = (process.env.RUNTIME_SECRET_ID || '').trim();
20
+ if (!secretId && secretsRequired()) throw new Error('RUNTIME_SECRET_ID is required for this runtime');
21
+ secretId = secretId || SECRET_ID;
22
+ return applySecret(await client.send(new SecretsManager.GetSecretValueCommand({ SecretId: secretId })));
23
+ }
24
+
25
+ function loadRuntimeSecrets(options) {
26
+ if (!loadPromise) {
27
+ const client = (options && options.client) || new SecretsManager.SecretsManagerClient({ region: process.env.AWS_REGION || 'us-east-1', maxAttempts: 3 });
28
+ // Cache successful loads until process replacement. Rejected/legacy loads
29
+ // are retried on the next call; concurrent callers share this attempt.
30
+ loadPromise = load(client).then(result => {
31
+ if (result.source !== 'secrets_manager') loadPromise = undefined;
32
+ return result;
33
+ }, error => {
34
+ loadPromise = undefined;
35
+ throw error;
36
+ });
37
+ }
38
+ return loadPromise;
39
+ }
40
+
41
+ function resetForTest() { loadPromise = undefined; }
42
+
43
+ module.exports = { SECRET_ID: SECRET_ID, applySecret: applySecret,
44
+ loadRuntimeSecrets: loadRuntimeSecrets, resetForTest: resetForTest };
package/package.json CHANGED
@@ -1,11 +1,10 @@
1
1
  {
2
2
  "name": "@dhyasama/totem-merge",
3
- "version": "7.0.3",
3
+ "version": "7.1.1",
4
4
  "description": "Org and person merging package",
5
5
  "main": "index.js",
6
6
  "scripts": {
7
- "prestart": "node ./lib/changeBSON",
8
- "start": "NODE_ENV=test node ./cli",
7
+ "start": "node ./bin/bootstrap",
9
8
  "test": "NODE_ENV=test mocha -R spec -t 10000"
10
9
  },
11
10
  "publishConfig": {
@@ -16,7 +15,8 @@
16
15
  "node": ">=24 <25"
17
16
  },
18
17
  "dependencies": {
19
- "@dhyasama/totem-logger": "^2.1.0",
18
+ "@aws-sdk/client-secrets-manager": "^3.883.0",
19
+ "@dhyasama/totem-logger": "3.0.0",
20
20
  "@dhyasama/totem-utilities": "^4.0.0",
21
21
  "async": "^2.6.4",
22
22
  "commander": "^2.20.3",
@@ -24,12 +24,21 @@
24
24
  "underscore": "^1.13.6"
25
25
  },
26
26
  "peerDependencies": {
27
- "mongoose": "^8.23.0"
27
+ "mongoose": "^8.24.3"
28
28
  },
29
29
  "devDependencies": {
30
30
  "@dhyasama/totem-models": "^12.14.0",
31
- "mocha": "^5.2.0",
31
+ "mocha": "^12.0.2",
32
32
  "mocha-mongoose": "^1.1.1",
33
33
  "should": "^8.2.2"
34
+ },
35
+ "overrides": {
36
+ "r7insight_node": {
37
+ "lodash": "4.18.1",
38
+ "semver": "5.7.2",
39
+ "codependency": {
40
+ "semver": "5.7.2"
41
+ }
42
+ }
34
43
  }
35
44
  }
package/test/config.js CHANGED
@@ -7,7 +7,7 @@ module.exports = {
7
7
  LPS_ENABLED: false,
8
8
 
9
9
  db: {
10
- uri: 'mongodb://gZvwpF5aLFvdc7KFa7qLLiZYv6CTLdfs:TKfdi3LD4583f8EL11k5Y0uukZ151d9n@test-shard-00-00-gv0f3.mongodb.net:27017,test-shard-00-01-gv0f3.mongodb.net:27017,test-shard-00-02-gv0f3.mongodb.net:27017/test?ssl=true&replicaSet=Test-shard-0&authSource=admin'
10
+ uri: process.env.TEST_DB_URI || 'mongodb://127.0.0.1:27017/totem-merge-test'
11
11
  },
12
12
 
13
13
  crypto: {
package/test/index.js CHANGED
@@ -2,11 +2,7 @@
2
2
 
3
3
  var config = require('./config');
4
4
  var dbUri = config.db.uri;
5
-
6
- if (dbUri.indexOf('@test-shard-') === -1) {
7
- console.log('Not a test db. Quitting...');
8
- process.exit();
9
- }
5
+ var describeMerge = process.env.TEST_DB_URI ? describe : describe.skip;
10
6
 
11
7
  var should = require("should");
12
8
  var mongoose = require('mongoose');
@@ -26,7 +22,7 @@ var customer;
26
22
  var org1, org2, org3, org4, org5, org6;
27
23
  var person1, person2, person3;
28
24
 
29
- describe('Merge', function() {
25
+ describeMerge('Merge', function() {
30
26
 
31
27
  // connect to test db
32
28
  before(async function() {
@@ -308,24 +304,8 @@ describe('Merge', function() {
308
304
 
309
305
  describe('Orgs', function() {
310
306
 
311
- it('Flags organization to be merged', function (done) {
312
-
313
- config.flag = true;
314
- merge = new Merge(mongoose, config);
315
-
316
- merge.organizations(org2, org1, 'testy', customer._id, function(err, result) {
317
- should.not.exist(err);
318
- should.exist(result);
319
- result.addedTo.flags.length.should.equal(1);
320
- return done();
321
- });
322
-
323
- });
324
-
325
307
  it('Merges organizations', function (done) {
326
308
 
327
- // reset to not flag
328
- config.flag = false;
329
309
  merge = new Merge(mongoose, config);
330
310
 
331
311
  Organization.upsert(org2, 'testy', function(err, result) {
@@ -362,8 +342,6 @@ describe('Merge', function() {
362
342
 
363
343
  it('Merges organizations with different people', function (done) {
364
344
 
365
- // reset to not flag
366
- config.flag = false;
367
345
  merge = new Merge(mongoose, config);
368
346
 
369
347
  org4.people = [];
@@ -408,7 +386,6 @@ describe('Merge', function() {
408
386
 
409
387
  it('Merges organizations with the same people', function (done) {
410
388
 
411
- config.flag = false;
412
389
  merge = new Merge(mongoose, config);
413
390
 
414
391
  org2.people = [];
@@ -456,7 +433,6 @@ describe('Merge', function() {
456
433
 
457
434
  it('Merges organizations when source has a deal', function (done) {
458
435
 
459
- config.flag = false;
460
436
  merge = new Merge(mongoose, config);
461
437
 
462
438
  var deal = new Deal({
@@ -497,7 +473,6 @@ describe('Merge', function() {
497
473
 
498
474
  it('Merges organizations when target has a list', function (done) {
499
475
 
500
- config.flag = false;
501
476
  merge = new Merge(mongoose, config);
502
477
 
503
478
  var list = new List({
@@ -542,7 +517,6 @@ describe('Merge', function() {
542
517
 
543
518
  it('Merges organizations when target has a message', function (done) {
544
519
 
545
- config.flag = false;
546
520
  merge = new Merge(mongoose, config);
547
521
 
548
522
  var newOrg = new Organization();
@@ -599,7 +573,6 @@ describe('Merge', function() {
599
573
 
600
574
  it('Merges organizations when target has news', function (done) {
601
575
 
602
- config.flag = false;
603
576
  merge = new Merge(mongoose, config);
604
577
 
605
578
  var newOrg = new Organization();
@@ -650,7 +623,6 @@ describe('Merge', function() {
650
623
 
651
624
  it('Will not merge customer', function (done) {
652
625
 
653
- config.flag = false;
654
626
  merge = new Merge(mongoose, config);
655
627
 
656
628
  var newOrg = new Organization();
@@ -685,24 +657,8 @@ describe('Merge', function() {
685
657
 
686
658
  describe('People', function() {
687
659
 
688
- it('Flags person to be merged', function (done) {
689
-
690
- config.flag = true;
691
- merge = new Merge(mongoose, config);
692
-
693
- merge.people(person2, person1, 'testy', customer._id, function(err, result) {
694
- should.not.exist(err);
695
- should.exist(result);
696
- result.addedTo.flags.length.should.equal(1);
697
- return done();
698
- });
699
-
700
- });
701
-
702
660
  it('Merges people', function (done) {
703
661
 
704
- // reset to not flag
705
- config.flag = false;
706
662
  merge = new Merge(mongoose, config);
707
663
 
708
664
  merge.peopleById(person2.id, person1.id, 'testy', customer._id, function(err, result) {
@@ -723,8 +679,6 @@ describe('Merge', function() {
723
679
 
724
680
  it('Merges people with different phone numbers', function (done) {
725
681
 
726
- // reset to not flag
727
- config.flag = false;
728
682
  merge = new Merge(mongoose, config);
729
683
 
730
684
  Person.getById(person2._id, { CUSTOMER_ID: customer._id }, function(err, result) {
@@ -831,8 +785,6 @@ describe('Merge', function() {
831
785
 
832
786
  it('Merges two people that are referenced by same org', function (done) {
833
787
 
834
- // reset to not flag
835
- config.flag = false;
836
788
  merge = new Merge(mongoose, config);
837
789
 
838
790
  var myNewPerson1 = new Person();
@@ -955,8 +907,6 @@ describe('Merge', function() {
955
907
 
956
908
  it('Merges two sources for same customer', function(done) {
957
909
 
958
- // reset to not flag
959
- config.flag = false;
960
910
  merge = new Merge(mongoose, config);
961
911
 
962
912
  var newPerson1 = new Person();
@@ -1013,8 +963,6 @@ describe('Merge', function() {
1013
963
 
1014
964
  it('Merges same source for different customers', function(done) {
1015
965
 
1016
- // reset to not flag
1017
- config.flag = false;
1018
966
  merge = new Merge(mongoose, config);
1019
967
 
1020
968
  var newPerson1 = new Person();
@@ -1071,8 +1019,6 @@ describe('Merge', function() {
1071
1019
 
1072
1020
  it('Merges two sources for two customers', function(done) {
1073
1021
 
1074
- // reset to not flag
1075
- config.flag = false;
1076
1022
  merge = new Merge(mongoose, config);
1077
1023
 
1078
1024
  var newPerson1 = new Person();
@@ -0,0 +1,119 @@
1
+ 'use strict';
2
+
3
+ var assert = require('node:assert/strict');
4
+ var mongoose = require('mongoose');
5
+ var mergeObjects = require('../lib/helpers').mergeObjects;
6
+
7
+ describe('mergeObjects security and compatibility', function() {
8
+ it('does not traverse __proto__ at the root or inside nested objects', function() {
9
+ [false, true].forEach(function(nested) {
10
+ var prototype = { mergeSecurityMarker: null };
11
+ var object = Object.create(prototype);
12
+ var payload = JSON.parse('{"__proto__":{"mergeSecurityMarker":"changed"}}');
13
+ mergeObjects(nested ? { profile: object } : object,
14
+ nested ? { profile: payload } : payload, []);
15
+ assert.equal(prototype.mergeSecurityMarker, null);
16
+ assert.equal(Object.getPrototypeOf(object), prototype);
17
+ });
18
+ });
19
+
20
+ it('does not write through Object.prototype', function() {
21
+ var key = '__totemMergeSecurityTest';
22
+ assert.equal(Object.hasOwn(Object.prototype, key), false);
23
+ Object.defineProperty(Object.prototype, key, {
24
+ value: null, writable: true, configurable: true
25
+ });
26
+ try {
27
+ mergeObjects({}, JSON.parse('{"__proto__":{"__totemMergeSecurityTest":true}}'), []);
28
+ assert.equal(Object.prototype[key], null);
29
+ assert.equal(({})[key], null);
30
+ } finally {
31
+ delete Object.prototype[key];
32
+ }
33
+ });
34
+
35
+ it('ignores constructor and prototype keys at each recursive level', function() {
36
+ var target = {
37
+ constructor: { prototype: { marker: null } },
38
+ prototype: { marker: null },
39
+ profile: { constructor: null, prototype: null, label: null }
40
+ };
41
+ var source = JSON.parse('{"constructor":{"prototype":{"marker":"bad"}},' +
42
+ '"prototype":{"marker":"bad"},' +
43
+ '"profile":{"constructor":"bad","prototype":"bad","label":"allowed"}}');
44
+ mergeObjects(target, source, []);
45
+ assert.equal(target.constructor.prototype.marker, null);
46
+ assert.equal(target.prototype.marker, null);
47
+ assert.deepEqual(target.profile, { constructor: null, prototype: null, label: 'allowed' });
48
+ });
49
+
50
+ it('ignores inherited source properties returned by a document conversion', function() {
51
+ var plain = Object.create({ inherited: 'not source data' });
52
+ plain.name = 'Allowed';
53
+ var target = { inherited: null, name: null };
54
+ mergeObjects(target, { toObject: function() { return plain; } }, []);
55
+ assert.deepEqual(target, { inherited: null, name: 'Allowed' });
56
+ });
57
+
58
+ it('preserves target precedence, exclusions, falsy values and source data', function() {
59
+ var target = {
60
+ name: 'Keep', description: null, enabled: false, count: 0,
61
+ empty: '', omitted: null, zero: null, disabled: null,
62
+ profile: { label: null, retained: 'Keep nested', omitted: null }
63
+ };
64
+ var source = {
65
+ name: 'Replace', description: 'Fill', enabled: true, count: 10,
66
+ empty: 'Replace', omitted: 'Excluded', zero: 0, disabled: false,
67
+ missing: 'Do not introduce',
68
+ profile: { label: 'Fill nested', retained: 'Replace', omitted: 'Excluded' }
69
+ };
70
+ var before = JSON.stringify(source);
71
+ assert.equal(mergeObjects(target, source, ['omitted']), target);
72
+ assert.deepEqual(target, {
73
+ name: 'Keep', description: 'Fill', enabled: false, count: 0,
74
+ empty: '', omitted: null, zero: null, disabled: null,
75
+ profile: { label: 'Fill nested', retained: 'Keep nested', omitted: null }
76
+ });
77
+ assert.equal(JSON.stringify(source), before);
78
+ });
79
+
80
+ it('preserves array ordering, ObjectId deduplication and people/phone keys', function() {
81
+ var first = new mongoose.Types.ObjectId();
82
+ var second = new mongoose.Types.ObjectId();
83
+ var target = {
84
+ aliases: ['First'], refs: [first],
85
+ people: [{ person: first, title: 'Keep' }],
86
+ phone: [{ number: '(212) 555-0100', label: 'Keep' }]
87
+ };
88
+ mergeObjects(target, {
89
+ aliases: ['First', 'Second'], refs: [first, second],
90
+ people: [{ person: first, title: 'Replace' }, { person: second, title: 'New' }],
91
+ phone: [{ number: '2125550100', label: 'Replace' }, { number: '2125550101', label: 'New' }]
92
+ }, []);
93
+ assert.deepEqual(target.aliases, ['First', 'Second']);
94
+ assert.deepEqual(target.refs.map(String), [String(first), String(second)]);
95
+ assert.equal(target.people.length, 2);
96
+ assert.equal(target.people[0].title, 'Keep');
97
+ assert.equal(target.people[1].title, 'New');
98
+ assert.equal(target.phone.length, 2);
99
+ assert.equal(target.phone[0].label, 'Keep');
100
+ });
101
+
102
+ it('merges real Mongoose documents without invoking virtual getters', function() {
103
+ var isolated = new mongoose.Mongoose();
104
+ var schema = new isolated.Schema({
105
+ name: String, description: { type: String, default: null }, aliases: [String],
106
+ profile: { label: { type: String, default: null } }
107
+ });
108
+ schema.virtual('unsafeVirtual').get(function() { throw new Error('Virtual invoked'); });
109
+ var Model = isolated.model('MergeSecurityFixture', schema);
110
+ var target = new Model({ name: 'Keep', aliases: ['First'] });
111
+ var source = new Model({ name: 'Replace', description: 'Fill', aliases: ['First', 'Second'], profile: { label: 'Nested' } });
112
+ mergeObjects(target, source, ['_id', '__v']);
113
+ assert.equal(target.name, 'Keep');
114
+ assert.equal(target.description, 'Fill');
115
+ assert.deepEqual(Array.from(target.aliases), ['First', 'Second']);
116
+ assert.equal(target.profile.label, 'Nested');
117
+ assert.equal(source.name, 'Replace');
118
+ });
119
+ });
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+
3
+ var assert = require('node:assert/strict');
4
+ var fs = require('node:fs');
5
+ var path = require('node:path');
6
+ var runtimeSecrets = require('../lib/runtimeSecrets');
7
+
8
+ describe('totem-merge runtime secrets', function() {
9
+ afterEach(function() { runtimeSecrets.resetForTest(); });
10
+
11
+ it('loads DB_URI from the dedicated development secret', async function() {
12
+ var client = { send: async function(command) {
13
+ assert.equal(command.input.SecretId, runtimeSecrets.SECRET_ID);
14
+ return { SecretString: JSON.stringify({ DB_URI: 'mongodb://test-host/test' }) };
15
+ } };
16
+ delete process.env.RUNTIME_SECRET_ID;
17
+ await runtimeSecrets.loadRuntimeSecrets({ client: client });
18
+ assert.equal(process.env.DB_URI, 'mongodb://test-host/test');
19
+ });
20
+
21
+ it('contains no credentialed MongoDB URI in source configuration', function() {
22
+ var root = path.join(__dirname, '..');
23
+ ['config.js', 'test/config.js'].forEach(function(file) {
24
+ assert.doesNotMatch(fs.readFileSync(path.join(root, file), 'utf8'), /mongodb(?:\+srv)?:\/\/[^\s]+:[^\s]+@/);
25
+ });
26
+ });
27
+ });
package/lib/changeBSON.js DELETED
@@ -1,22 +0,0 @@
1
- "use strict";
2
-
3
- var fs = require('fs');
4
- var filePath = './node_modules/mongoose-post-find/node_modules/bson/ext/index.js';
5
- var config = require('./../config');
6
- var Logger = require('@dhyasama/totem-logger');
7
- var logger = new Logger(config.logentries);
8
-
9
- fs.readFile(filePath, 'utf8', function (err, data) {
10
-
11
- if (err) { logger.handleErr(err); }
12
- else {
13
-
14
- var result = data.replace("bson = require('../build/Release/bson');", "bson = require('bson');");
15
-
16
- fs.writeFile(filePath, result, 'utf8', function (err) {
17
- if (err) { logger.handleErr(err); }
18
- });
19
-
20
- }
21
-
22
- });