@friggframework/core 2.0.0-next.41 → 2.0.0-next.42

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.
Files changed (196) hide show
  1. package/CLAUDE.md +693 -0
  2. package/README.md +931 -50
  3. package/application/commands/README.md +421 -0
  4. package/application/commands/credential-commands.js +224 -0
  5. package/application/commands/entity-commands.js +315 -0
  6. package/application/commands/integration-commands.js +160 -0
  7. package/application/commands/integration-commands.test.js +123 -0
  8. package/application/commands/user-commands.js +213 -0
  9. package/application/index.js +69 -0
  10. package/core/CLAUDE.md +690 -0
  11. package/core/create-handler.js +0 -6
  12. package/credential/repositories/credential-repository-factory.js +47 -0
  13. package/credential/repositories/credential-repository-interface.js +98 -0
  14. package/credential/repositories/credential-repository-mongo.js +301 -0
  15. package/credential/repositories/credential-repository-postgres.js +307 -0
  16. package/credential/repositories/credential-repository.js +307 -0
  17. package/credential/use-cases/get-credential-for-user.js +21 -0
  18. package/credential/use-cases/update-authentication-status.js +15 -0
  19. package/database/config.js +117 -0
  20. package/database/encryption/README.md +683 -0
  21. package/database/encryption/encryption-integration.test.js +553 -0
  22. package/database/encryption/encryption-schema-registry.js +141 -0
  23. package/database/encryption/encryption-schema-registry.test.js +392 -0
  24. package/database/encryption/field-encryption-service.js +226 -0
  25. package/database/encryption/field-encryption-service.test.js +525 -0
  26. package/database/encryption/logger.js +79 -0
  27. package/database/encryption/mongo-decryption-fix-verification.test.js +348 -0
  28. package/database/encryption/postgres-decryption-fix-verification.test.js +371 -0
  29. package/database/encryption/postgres-relation-decryption.test.js +245 -0
  30. package/database/encryption/prisma-encryption-extension.js +222 -0
  31. package/database/encryption/prisma-encryption-extension.test.js +439 -0
  32. package/database/index.js +25 -12
  33. package/database/models/readme.md +1 -0
  34. package/database/prisma.js +162 -0
  35. package/database/repositories/health-check-repository-factory.js +38 -0
  36. package/database/repositories/health-check-repository-interface.js +86 -0
  37. package/database/repositories/health-check-repository-mongodb.js +72 -0
  38. package/database/repositories/health-check-repository-postgres.js +75 -0
  39. package/database/repositories/health-check-repository.js +108 -0
  40. package/database/use-cases/check-database-health-use-case.js +34 -0
  41. package/database/use-cases/check-encryption-health-use-case.js +82 -0
  42. package/database/use-cases/test-encryption-use-case.js +252 -0
  43. package/encrypt/Cryptor.js +20 -152
  44. package/encrypt/index.js +1 -2
  45. package/encrypt/test-encrypt.js +0 -2
  46. package/handlers/app-definition-loader.js +38 -0
  47. package/handlers/app-handler-helpers.js +0 -3
  48. package/handlers/auth-flow.integration.test.js +147 -0
  49. package/handlers/backend-utils.js +25 -45
  50. package/handlers/integration-event-dispatcher.js +54 -0
  51. package/handlers/integration-event-dispatcher.test.js +141 -0
  52. package/handlers/routers/HEALTHCHECK.md +103 -1
  53. package/handlers/routers/auth.js +3 -14
  54. package/handlers/routers/health.js +63 -424
  55. package/handlers/routers/health.test.js +7 -0
  56. package/handlers/routers/integration-defined-routers.js +8 -5
  57. package/handlers/routers/user.js +25 -5
  58. package/handlers/routers/websocket.js +5 -3
  59. package/handlers/use-cases/check-external-apis-health-use-case.js +81 -0
  60. package/handlers/use-cases/check-integrations-health-use-case.js +32 -0
  61. package/handlers/workers/integration-defined-workers.js +6 -3
  62. package/index.js +45 -22
  63. package/integrations/index.js +12 -10
  64. package/integrations/integration-base.js +224 -53
  65. package/integrations/integration-router.js +386 -178
  66. package/integrations/options.js +1 -1
  67. package/integrations/repositories/integration-mapping-repository-factory.js +50 -0
  68. package/integrations/repositories/integration-mapping-repository-interface.js +106 -0
  69. package/integrations/repositories/integration-mapping-repository-mongo.js +161 -0
  70. package/integrations/repositories/integration-mapping-repository-postgres.js +227 -0
  71. package/integrations/repositories/integration-mapping-repository.js +156 -0
  72. package/integrations/repositories/integration-repository-factory.js +44 -0
  73. package/integrations/repositories/integration-repository-interface.js +115 -0
  74. package/integrations/repositories/integration-repository-mongo.js +271 -0
  75. package/integrations/repositories/integration-repository-postgres.js +319 -0
  76. package/integrations/tests/doubles/dummy-integration-class.js +90 -0
  77. package/integrations/tests/doubles/test-integration-repository.js +99 -0
  78. package/integrations/tests/use-cases/create-integration.test.js +131 -0
  79. package/integrations/tests/use-cases/delete-integration-for-user.test.js +150 -0
  80. package/integrations/tests/use-cases/find-integration-context-by-external-entity-id.test.js +92 -0
  81. package/integrations/tests/use-cases/get-integration-for-user.test.js +150 -0
  82. package/integrations/tests/use-cases/get-integration-instance.test.js +176 -0
  83. package/integrations/tests/use-cases/get-integrations-for-user.test.js +176 -0
  84. package/integrations/tests/use-cases/get-possible-integrations.test.js +188 -0
  85. package/integrations/tests/use-cases/update-integration-messages.test.js +142 -0
  86. package/integrations/tests/use-cases/update-integration-status.test.js +103 -0
  87. package/integrations/tests/use-cases/update-integration.test.js +141 -0
  88. package/integrations/use-cases/create-integration.js +83 -0
  89. package/integrations/use-cases/delete-integration-for-user.js +73 -0
  90. package/integrations/use-cases/find-integration-context-by-external-entity-id.js +72 -0
  91. package/integrations/use-cases/get-integration-for-user.js +78 -0
  92. package/integrations/use-cases/get-integration-instance-by-definition.js +67 -0
  93. package/integrations/use-cases/get-integration-instance.js +83 -0
  94. package/integrations/use-cases/get-integrations-for-user.js +87 -0
  95. package/integrations/use-cases/get-possible-integrations.js +27 -0
  96. package/integrations/use-cases/index.js +11 -0
  97. package/integrations/use-cases/load-integration-context-full.test.js +329 -0
  98. package/integrations/use-cases/load-integration-context.js +71 -0
  99. package/integrations/use-cases/load-integration-context.test.js +114 -0
  100. package/integrations/use-cases/update-integration-messages.js +44 -0
  101. package/integrations/use-cases/update-integration-status.js +32 -0
  102. package/integrations/use-cases/update-integration.js +93 -0
  103. package/integrations/utils/map-integration-dto.js +36 -0
  104. package/jest-global-setup-noop.js +3 -0
  105. package/jest-global-teardown-noop.js +3 -0
  106. package/{module-plugin → modules}/entity.js +1 -0
  107. package/{module-plugin → modules}/index.js +0 -8
  108. package/modules/module-factory.js +56 -0
  109. package/modules/module-hydration.test.js +205 -0
  110. package/modules/module.js +221 -0
  111. package/modules/repositories/module-repository-factory.js +33 -0
  112. package/modules/repositories/module-repository-interface.js +129 -0
  113. package/modules/repositories/module-repository-mongo.js +386 -0
  114. package/modules/repositories/module-repository-postgres.js +437 -0
  115. package/modules/repositories/module-repository.js +327 -0
  116. package/{module-plugin → modules}/test/mock-api/api.js +8 -3
  117. package/{module-plugin → modules}/test/mock-api/definition.js +12 -8
  118. package/modules/tests/doubles/test-module-factory.js +16 -0
  119. package/modules/tests/doubles/test-module-repository.js +39 -0
  120. package/modules/use-cases/get-entities-for-user.js +32 -0
  121. package/modules/use-cases/get-entity-options-by-id.js +59 -0
  122. package/modules/use-cases/get-entity-options-by-type.js +34 -0
  123. package/modules/use-cases/get-module-instance-from-type.js +31 -0
  124. package/modules/use-cases/get-module.js +56 -0
  125. package/modules/use-cases/process-authorization-callback.js +121 -0
  126. package/modules/use-cases/refresh-entity-options.js +59 -0
  127. package/modules/use-cases/test-module-auth.js +55 -0
  128. package/modules/utils/map-module-dto.js +18 -0
  129. package/package.json +14 -6
  130. package/prisma-mongodb/schema.prisma +321 -0
  131. package/prisma-postgresql/migrations/20250930193005_init/migration.sql +315 -0
  132. package/prisma-postgresql/migrations/20251006135218_init/migration.sql +9 -0
  133. package/prisma-postgresql/migrations/migration_lock.toml +3 -0
  134. package/prisma-postgresql/schema.prisma +303 -0
  135. package/syncs/manager.js +468 -443
  136. package/syncs/repositories/sync-repository-factory.js +38 -0
  137. package/syncs/repositories/sync-repository-interface.js +109 -0
  138. package/syncs/repositories/sync-repository-mongo.js +239 -0
  139. package/syncs/repositories/sync-repository-postgres.js +319 -0
  140. package/syncs/sync.js +0 -1
  141. package/token/repositories/token-repository-factory.js +33 -0
  142. package/token/repositories/token-repository-interface.js +131 -0
  143. package/token/repositories/token-repository-mongo.js +212 -0
  144. package/token/repositories/token-repository-postgres.js +257 -0
  145. package/token/repositories/token-repository.js +219 -0
  146. package/types/integrations/index.d.ts +2 -6
  147. package/types/module-plugin/index.d.ts +5 -57
  148. package/types/syncs/index.d.ts +0 -2
  149. package/user/repositories/user-repository-factory.js +46 -0
  150. package/user/repositories/user-repository-interface.js +198 -0
  151. package/user/repositories/user-repository-mongo.js +250 -0
  152. package/user/repositories/user-repository-postgres.js +311 -0
  153. package/user/tests/doubles/test-user-repository.js +72 -0
  154. package/user/tests/use-cases/create-individual-user.test.js +24 -0
  155. package/user/tests/use-cases/create-organization-user.test.js +28 -0
  156. package/user/tests/use-cases/create-token-for-user-id.test.js +19 -0
  157. package/user/tests/use-cases/get-user-from-bearer-token.test.js +64 -0
  158. package/user/tests/use-cases/login-user.test.js +140 -0
  159. package/user/use-cases/create-individual-user.js +61 -0
  160. package/user/use-cases/create-organization-user.js +47 -0
  161. package/user/use-cases/create-token-for-user-id.js +30 -0
  162. package/user/use-cases/get-user-from-bearer-token.js +77 -0
  163. package/user/use-cases/login-user.js +122 -0
  164. package/user/user.js +77 -0
  165. package/websocket/repositories/websocket-connection-repository-factory.js +37 -0
  166. package/websocket/repositories/websocket-connection-repository-interface.js +106 -0
  167. package/websocket/repositories/websocket-connection-repository-mongo.js +155 -0
  168. package/websocket/repositories/websocket-connection-repository-postgres.js +195 -0
  169. package/websocket/repositories/websocket-connection-repository.js +160 -0
  170. package/database/models/State.js +0 -9
  171. package/database/models/Token.js +0 -70
  172. package/database/mongo.js +0 -171
  173. package/encrypt/Cryptor.test.js +0 -32
  174. package/encrypt/encrypt.js +0 -104
  175. package/encrypt/encrypt.test.js +0 -1069
  176. package/handlers/routers/middleware/loadUser.js +0 -15
  177. package/handlers/routers/middleware/requireLoggedInUser.js +0 -12
  178. package/integrations/create-frigg-backend.js +0 -31
  179. package/integrations/integration-factory.js +0 -251
  180. package/integrations/integration-mapping.js +0 -43
  181. package/integrations/integration-model.js +0 -46
  182. package/integrations/integration-user.js +0 -144
  183. package/integrations/test/integration-base.test.js +0 -144
  184. package/module-plugin/auther.js +0 -393
  185. package/module-plugin/credential.js +0 -22
  186. package/module-plugin/entity-manager.js +0 -70
  187. package/module-plugin/manager.js +0 -169
  188. package/module-plugin/module-factory.js +0 -61
  189. package/module-plugin/test/auther.test.js +0 -97
  190. /package/{module-plugin → modules}/ModuleConstants.js +0 -0
  191. /package/{module-plugin → modules}/requester/api-key.js +0 -0
  192. /package/{module-plugin → modules}/requester/basic.js +0 -0
  193. /package/{module-plugin → modules}/requester/oauth-2.js +0 -0
  194. /package/{module-plugin → modules}/requester/requester.js +0 -0
  195. /package/{module-plugin → modules}/requester/requester.test.js +0 -0
  196. /package/{module-plugin → modules}/test/mock-api/mocks/hubspot.js +0 -0
@@ -0,0 +1,252 @@
1
+ /**
2
+ * Use Case for testing encryption functionality.
3
+ * Contains business logic for verifying that encryption and decryption work correctly.
4
+ *
5
+ * Follows DDD/Hexagonal Architecture:
6
+ * - Application Layer (this use case)
7
+ * - Depends on Infrastructure Layer (HealthCheckRepository)
8
+ */
9
+ class TestEncryptionUseCase {
10
+ /**
11
+ * @param {Object} params
12
+ * @param {import('../health-check-repository-interface').HealthCheckRepositoryInterface} params.healthCheckRepository
13
+ */
14
+ constructor({ healthCheckRepository }) {
15
+ this.repository = healthCheckRepository;
16
+ }
17
+
18
+ /**
19
+ * Execute encryption test
20
+ * Orchestrates the full encryption test workflow using Prisma
21
+ * @returns {Promise<Object>} Test results with status and details
22
+ */
23
+ async execute() {
24
+ const testData = {
25
+ testSecret: 'This is a secret value that should be encrypted',
26
+ normalField: 'This is a normal field that should not be encrypted',
27
+ nestedSecret: {
28
+ value: 'This is a nested secret that should be encrypted',
29
+ },
30
+ };
31
+
32
+ const credentialData = this._mapTestDataToCredential(testData);
33
+
34
+ const credential = await this._withTimeout(
35
+ this.repository.createCredential(credentialData),
36
+ 5000,
37
+ 'Save operation timed out'
38
+ );
39
+
40
+ try {
41
+ const retrievedCredential = await this._withTimeout(
42
+ this.repository.findCredentialById(credential.id),
43
+ 5000,
44
+ 'Find operation timed out'
45
+ );
46
+
47
+ const retrievedTestData =
48
+ this._mapCredentialToTestData(retrievedCredential);
49
+ const decryptionWorks = this._verifyDecryption(
50
+ retrievedTestData,
51
+ testData
52
+ );
53
+
54
+ const rawCredential = await this._withTimeout(
55
+ this.repository.getRawCredentialById(credential.id),
56
+ 5000,
57
+ 'Database verification timed out'
58
+ );
59
+
60
+ const rawTestData = this._mapRawCredentialToTestData(rawCredential);
61
+ const encryptionResults = this._verifyEncryptionInDatabase(
62
+ rawTestData,
63
+ testData
64
+ );
65
+
66
+ return this._evaluateEncryptionResults(
67
+ decryptionWorks,
68
+ encryptionResults
69
+ );
70
+ } finally {
71
+ await this._withTimeout(
72
+ this.repository.deleteCredential(credential.id),
73
+ 5000,
74
+ 'Delete operation timed out'
75
+ );
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Map test data format to Credential model format
81
+ * @param {Object} testData - Test data with testSecret, normalField, nestedSecret
82
+ * @returns {Object} Credential data structure
83
+ * @private
84
+ */
85
+ _mapTestDataToCredential(testData) {
86
+ return {
87
+ user_id: 'test-encryption-user',
88
+ entity_id: 'test-encryption-entity',
89
+ data: {
90
+ access_token: testData.testSecret,
91
+ refresh_token: testData.nestedSecret?.value,
92
+ domain: testData.normalField,
93
+ },
94
+ };
95
+ }
96
+
97
+ /**
98
+ * Map Credential model format to test data format
99
+ * @param {Object} credential - Credential from database
100
+ * @returns {Object} Test data format
101
+ * @private
102
+ */
103
+ _mapCredentialToTestData(credential) {
104
+ if (!credential) {
105
+ return null;
106
+ }
107
+
108
+ return {
109
+ id: credential.id,
110
+ testSecret: credential.data.access_token,
111
+ normalField: credential.data.domain,
112
+ nestedSecret: {
113
+ value: credential.data.refresh_token,
114
+ },
115
+ };
116
+ }
117
+
118
+ /**
119
+ * Map raw Credential data to test data format
120
+ * @param {Object} rawCredential - Raw credential from database
121
+ * @returns {Object} Test data format with raw encrypted values
122
+ * @private
123
+ */
124
+ _mapRawCredentialToTestData(rawCredential) {
125
+ if (!rawCredential) {
126
+ return null;
127
+ }
128
+
129
+ return {
130
+ testSecret: rawCredential.data?.access_token,
131
+ normalField: rawCredential.data?.domain,
132
+ nestedSecret: {
133
+ value: rawCredential.data?.refresh_token,
134
+ },
135
+ };
136
+ }
137
+
138
+ /**
139
+ * Verify that a document was decrypted correctly
140
+ * @param {Object} retrievedDoc - Document retrieved from database
141
+ * @param {Object} originalData - Original unencrypted data
142
+ * @returns {boolean} True if decryption worked correctly
143
+ * @private
144
+ */
145
+ _verifyDecryption(retrievedDoc, originalData) {
146
+ return (
147
+ retrievedDoc &&
148
+ retrievedDoc.testSecret === originalData.testSecret &&
149
+ retrievedDoc.normalField === originalData.normalField &&
150
+ retrievedDoc.nestedSecret?.value === originalData.nestedSecret.value
151
+ );
152
+ }
153
+
154
+ /**
155
+ * Verify that data was encrypted in the database
156
+ * Business rule: Encrypted fields should contain ':' and differ from original
157
+ * @param {Object} rawDoc - Raw document from database
158
+ * @param {Object} originalData - Original unencrypted data
159
+ * @returns {Object} Encryption verification results
160
+ * @private
161
+ */
162
+ _verifyEncryptionInDatabase(rawDoc, originalData) {
163
+ const secretIsEncrypted =
164
+ rawDoc &&
165
+ typeof rawDoc.testSecret === 'string' &&
166
+ rawDoc.testSecret.includes(':') &&
167
+ rawDoc.testSecret !== originalData.testSecret;
168
+
169
+ const nestedIsEncrypted =
170
+ rawDoc?.nestedSecret?.value &&
171
+ typeof rawDoc.nestedSecret.value === 'string' &&
172
+ rawDoc.nestedSecret.value.includes(':') &&
173
+ rawDoc.nestedSecret.value !== originalData.nestedSecret.value;
174
+
175
+ const normalNotEncrypted =
176
+ rawDoc && rawDoc.normalField === originalData.normalField;
177
+
178
+ return {
179
+ secretIsEncrypted,
180
+ nestedIsEncrypted,
181
+ normalNotEncrypted,
182
+ };
183
+ }
184
+
185
+ /**
186
+ * Evaluate encryption test results
187
+ * Business logic for determining if encryption is healthy
188
+ * @param {boolean} decryptionWorks - Whether decryption succeeded
189
+ * @param {Object} encryptionResults - Encryption verification results
190
+ * @returns {Object} Test status and result message
191
+ * @private
192
+ */
193
+ _evaluateEncryptionResults(decryptionWorks, encryptionResults) {
194
+ const { secretIsEncrypted, nestedIsEncrypted, normalNotEncrypted } =
195
+ encryptionResults;
196
+
197
+ if (
198
+ decryptionWorks &&
199
+ secretIsEncrypted &&
200
+ nestedIsEncrypted &&
201
+ normalNotEncrypted
202
+ ) {
203
+ return {
204
+ status: 'enabled',
205
+ testResult:
206
+ 'Encryption and decryption verified successfully',
207
+ encryptionWorks: true,
208
+ };
209
+ }
210
+
211
+ if (decryptionWorks && (!secretIsEncrypted || !nestedIsEncrypted)) {
212
+ return {
213
+ status: 'unhealthy',
214
+ testResult: 'Fields are not being encrypted in database',
215
+ encryptionWorks: false,
216
+ };
217
+ }
218
+
219
+ if (decryptionWorks && !normalNotEncrypted) {
220
+ return {
221
+ status: 'unhealthy',
222
+ testResult: 'Normal fields are being incorrectly encrypted',
223
+ encryptionWorks: false,
224
+ };
225
+ }
226
+
227
+ return {
228
+ status: 'unhealthy',
229
+ testResult: 'Decryption failed or data mismatch',
230
+ encryptionWorks: false,
231
+ };
232
+ }
233
+
234
+ /**
235
+ * Execute promise with timeout
236
+ * @param {Promise} promise - Promise to execute
237
+ * @param {number} ms - Timeout in milliseconds
238
+ * @param {string} errorMessage - Error message for timeout
239
+ * @returns {Promise} Promise that rejects on timeout
240
+ * @private
241
+ */
242
+ _withTimeout(promise, ms, errorMessage) {
243
+ return Promise.race([
244
+ promise,
245
+ new Promise((_, reject) =>
246
+ setTimeout(() => reject(new Error(errorMessage)), ms)
247
+ ),
248
+ ]);
249
+ }
250
+ }
251
+
252
+ module.exports = { TestEncryptionUseCase };
@@ -1,22 +1,28 @@
1
+ /**
2
+ * Cryptor - Encryption Service Adapter
3
+ *
4
+ * Infrastructure Layer adapter for AWS KMS and local AES encryption.
5
+ * Provides envelope encryption pattern for field-level encryption.
6
+ *
7
+ * Envelope Encryption Pattern:
8
+ * 1. Generate Data Encryption Key (DEK) via KMS or locally
9
+ * 2. Encrypt field value with DEK using AES-256-CTR
10
+ * 3. Encrypt DEK with Master Key (KMS CMK or AES_KEY)
11
+ * 4. Return format: "keyId:encryptedText:encryptedKey"
12
+ *
13
+ * Benefits:
14
+ * - Reduces KMS API calls (unique DEK per operation)
15
+ * - Master key never leaves KMS
16
+ * - Enables key rotation without re-encrypting data
17
+ */
18
+
1
19
  const crypto = require('crypto');
2
20
  const AWS = require('aws-sdk');
3
- const { get, set } = require('lodash');
4
21
  const aes = require('./aes');
5
22
 
6
- const hasValue = (a) => a !== undefined && a !== null && a !== '';
7
-
8
23
  class Cryptor {
9
- constructor({ fields, shouldUseAws }) {
24
+ constructor({ shouldUseAws }) {
10
25
  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
26
  }
21
27
 
22
28
  async generateDataKey() {
@@ -56,7 +62,7 @@ class Cryptor {
56
62
  const key = availableKeys[keyId];
57
63
 
58
64
  if (!key) {
59
- throw new Error(`No encryption key found with ID "${keyId}"`);
65
+ throw new Error('Encryption key not found');
60
66
  }
61
67
 
62
68
  return key;
@@ -79,146 +85,9 @@ class Cryptor {
79
85
  return aes.decrypt(encryptedKey, key);
80
86
  }
81
87
 
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
88
  async encrypt(text) {
219
89
  const { keyId, encryptedKey, plaintext } = await this.generateDataKey();
220
90
  const encryptedText = aes.encrypt(text, plaintext);
221
-
222
91
  return `${keyId}:${encryptedText}:${encryptedKey}`;
223
92
  }
224
93
 
@@ -228,7 +97,6 @@ class Cryptor {
228
97
  const encryptedText = `${split[1]}:${split[2]}`;
229
98
  const encryptedKey = Buffer.from(split[3], 'base64');
230
99
  const plaintext = await this.decryptDataKey(keyId, encryptedKey);
231
-
232
100
  return aes.decrypt(encryptedText, plaintext);
233
101
  }
234
102
  }
package/encrypt/index.js CHANGED
@@ -1,4 +1,3 @@
1
- const { Encrypt } = require('./encrypt');
2
1
  const { Cryptor } = require('./Cryptor');
3
2
 
4
- module.exports = { Encrypt, Cryptor };
3
+ module.exports = { Cryptor };
@@ -1,7 +1,5 @@
1
- const AWS = require('aws-sdk');
2
1
  const { mongoose } = require('../database/mongoose');
3
2
  const crypto = require('crypto');
4
- const { Encrypt } = require('./encrypt');
5
3
 
6
4
  const hexPattern = /^[a-f0-9]+$/i; // match hex strings of length >= 1
7
5
 
@@ -0,0 +1,38 @@
1
+ const { findNearestBackendPackageJson } = require('@friggframework/core/utils');
2
+ const path = require('node:path');
3
+ const fs = require('fs-extra');
4
+
5
+ /**
6
+ * Loads the App definition from the nearest backend package
7
+ * @function loadAppDefinition
8
+ * @description Searches for the nearest backend package.json, loads the corresponding index.js file,
9
+ * and extracts the application definition containing integrations and user configuration.
10
+ * @returns {{integrations: Array<object>, userConfig: object | null}} An object containing the application definition.
11
+ * @throws {Error} Throws error if backend package.json cannot be found.
12
+ * @throws {Error} Throws error if index.js file cannot be found in the backend directory.
13
+ * @example
14
+ * const { integrations, userConfig } = loadAppDefinition();
15
+ * console.log(`Found ${integrations.length} integrations`);
16
+ */
17
+ function loadAppDefinition() {
18
+ const backendPath = findNearestBackendPackageJson();
19
+ if (!backendPath) {
20
+ throw new Error('Could not find backend package.json');
21
+ }
22
+
23
+ const backendDir = path.dirname(backendPath);
24
+ const backendFilePath = path.join(backendDir, 'index.js');
25
+ if (!fs.existsSync(backendFilePath)) {
26
+ throw new Error('Could not find index.js');
27
+ }
28
+
29
+ const backendJsFile = require(backendFilePath);
30
+ const appDefinition = backendJsFile.Definition;
31
+
32
+ const { integrations = [], user: userConfig = null } = appDefinition;
33
+ return { integrations, userConfig };
34
+ }
35
+
36
+ module.exports = {
37
+ loadAppDefinition,
38
+ };
@@ -3,7 +3,6 @@ const express = require('express');
3
3
  const bodyParser = require('body-parser');
4
4
  const cors = require('cors');
5
5
  const Boom = require('@hapi/boom');
6
- const loadUserManager = require('./routers/middleware/loadUser');
7
6
  const serverlessHttp = require('serverless-http');
8
7
 
9
8
  const createApp = (applyMiddleware) => {
@@ -20,8 +19,6 @@ const createApp = (applyMiddleware) => {
20
19
  })
21
20
  );
22
21
 
23
- app.use(loadUserManager);
24
-
25
22
  if (applyMiddleware) applyMiddleware(app);
26
23
 
27
24
  // Handle sending error response and logging server errors to console
@@ -0,0 +1,147 @@
1
+ jest.mock('../database/config', () => ({
2
+ DB_TYPE: 'mongodb',
3
+ getDatabaseType: jest.fn(() => 'mongodb'),
4
+ PRISMA_LOG_LEVEL: 'error,warn',
5
+ PRISMA_QUERY_LOGGING: false,
6
+ }));
7
+
8
+ const { IntegrationEventDispatcher } = require('./integration-event-dispatcher');
9
+ const { IntegrationBase } = require('../integrations/integration-base');
10
+
11
+ class SimulatedAsanaIntegration extends IntegrationBase {
12
+ static Definition = {
13
+ name: 'asana',
14
+ version: '1.0.0',
15
+ modules: {},
16
+ routes: [
17
+ { path: '/auth', method: 'GET', event: 'AUTH_REQUEST' },
18
+ { path: '/auth/redirect/:provider', method: 'GET', event: 'AUTH_REDIRECT' },
19
+ { path: '/form', method: 'GET', event: 'LOAD_FORM' },
20
+ ],
21
+ };
22
+
23
+ constructor(params = {}) {
24
+ super(params);
25
+ this.events = {
26
+ AUTH_REQUEST: { handler: this.authRequest.bind(this) },
27
+ AUTH_REDIRECT: { handler: this.authRedirect.bind(this) },
28
+ LOAD_FORM: { handler: this.loadForm.bind(this) },
29
+ };
30
+ }
31
+
32
+ async authRequest() {
33
+ return {
34
+ success: true,
35
+ action: 'redirect',
36
+ hydrated: this.isHydrated,
37
+ };
38
+ }
39
+
40
+ async authRedirect({ req }) {
41
+ const { code } = req.query || {};
42
+ return {
43
+ success: true,
44
+ action: 'tokens_received',
45
+ receivedCode: code,
46
+ hydrated: this.isHydrated,
47
+ };
48
+ }
49
+
50
+ async loadForm() {
51
+ if (!this.isHydrated && SimulatedAsanaIntegration.testRecord) {
52
+ this.setIntegrationRecord({
53
+ record: SimulatedAsanaIntegration.testRecord.record,
54
+ modules: SimulatedAsanaIntegration.testRecord.modules,
55
+ });
56
+ }
57
+
58
+ this.assertHydrated('Integration not found - must authenticate first');
59
+
60
+ return {
61
+ success: true,
62
+ form: {
63
+ fields: ['field1', 'field2'],
64
+ },
65
+ integrationId: this.id,
66
+ };
67
+ }
68
+ }
69
+
70
+ describe('IntegrationEventDispatcher auth flow', () => {
71
+ const createDispatcher = () =>
72
+ new IntegrationEventDispatcher(new SimulatedAsanaIntegration());
73
+
74
+ beforeEach(() => {
75
+ SimulatedAsanaIntegration.testRecord = null;
76
+ });
77
+
78
+ it('handles auth request without hydration', async () => {
79
+ const dispatcher = createDispatcher();
80
+ const result = await dispatcher.dispatchHttp({
81
+ event: 'AUTH_REQUEST',
82
+ req: { params: { provider: 'asana' }, query: {} },
83
+ res: {},
84
+ next: jest.fn(),
85
+ });
86
+
87
+ expect(result).toEqual({ success: true, action: 'redirect', hydrated: false });
88
+ });
89
+
90
+ it('handles auth redirect without hydration', async () => {
91
+ const dispatcher = createDispatcher();
92
+ const result = await dispatcher.dispatchHttp({
93
+ event: 'AUTH_REDIRECT',
94
+ req: { params: { provider: 'asana' }, query: { code: 'abc123' } },
95
+ res: {},
96
+ next: jest.fn(),
97
+ });
98
+
99
+ expect(result).toEqual({
100
+ success: true,
101
+ action: 'tokens_received',
102
+ receivedCode: 'abc123',
103
+ hydrated: false,
104
+ });
105
+ });
106
+
107
+ it('throws for protected routes when no record is loaded', async () => {
108
+ const dispatcher = createDispatcher();
109
+ await expect(
110
+ dispatcher.dispatchHttp({
111
+ event: 'LOAD_FORM',
112
+ req: { query: {} },
113
+ res: {},
114
+ next: jest.fn(),
115
+ })
116
+ ).rejects.toThrow('Integration not found - must authenticate first');
117
+ });
118
+
119
+ it('allows handlers to hydrate explicitly before continuing', async () => {
120
+ SimulatedAsanaIntegration.testRecord = {
121
+ record: {
122
+ id: 'integration-123',
123
+ userId: 'user-456',
124
+ config: { type: 'asana' },
125
+ status: 'ENABLED',
126
+ version: '1.0.0',
127
+ messages: { errors: [], warnings: [] },
128
+ entities: [],
129
+ },
130
+ modules: [],
131
+ };
132
+
133
+ const dispatcher = createDispatcher();
134
+ const result = await dispatcher.dispatchHttp({
135
+ event: 'LOAD_FORM',
136
+ req: { query: {} },
137
+ res: {},
138
+ next: jest.fn(),
139
+ });
140
+
141
+ expect(result).toEqual({
142
+ success: true,
143
+ form: { fields: ['field1', 'field2'] },
144
+ integrationId: 'integration-123',
145
+ });
146
+ });
147
+ });