@friggframework/core 2.0.0--canary.548.c8ae0ca.0 → 2.0.0--canary.545.c40eca4.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.
Files changed (237) hide show
  1. package/CHANGELOG.md +66 -67
  2. package/CLAUDE.md +148 -124
  3. package/README.md +165 -130
  4. package/admin-scripts/index.js +52 -0
  5. package/admin-scripts/repositories/admin-api-key-repository-documentdb.js +21 -0
  6. package/admin-scripts/repositories/admin-api-key-repository-factory.js +51 -0
  7. package/admin-scripts/repositories/admin-api-key-repository-interface.js +121 -0
  8. package/admin-scripts/repositories/admin-api-key-repository-mongo.js +155 -0
  9. package/admin-scripts/repositories/admin-api-key-repository-postgres.js +189 -0
  10. package/admin-scripts/repositories/admin-process-repository-documentdb.js +21 -0
  11. package/admin-scripts/repositories/admin-process-repository-factory.js +49 -0
  12. package/admin-scripts/repositories/admin-process-repository-interface.js +150 -0
  13. package/admin-scripts/repositories/admin-process-repository-mongo.js +213 -0
  14. package/admin-scripts/repositories/admin-process-repository-postgres.js +251 -0
  15. package/admin-scripts/repositories/script-execution-repository-documentdb.js +21 -0
  16. package/admin-scripts/repositories/script-execution-repository-factory.js +51 -0
  17. package/admin-scripts/repositories/script-execution-repository-interface.js +193 -0
  18. package/admin-scripts/repositories/script-execution-repository-mongo.js +278 -0
  19. package/admin-scripts/repositories/script-execution-repository-postgres.js +320 -0
  20. package/admin-scripts/repositories/script-schedule-repository-documentdb.js +21 -0
  21. package/admin-scripts/repositories/script-schedule-repository-factory.js +49 -0
  22. package/admin-scripts/repositories/script-schedule-repository-interface.js +108 -0
  23. package/admin-scripts/repositories/script-schedule-repository-mongo.js +179 -0
  24. package/admin-scripts/repositories/script-schedule-repository-postgres.js +210 -0
  25. package/application/commands/admin-script-commands.js +348 -0
  26. package/application/commands/integration-commands.js +12 -6
  27. package/application/index.js +5 -3
  28. package/assertions/get.js +17 -7
  29. package/associations/association.js +59 -57
  30. package/core/CLAUDE.md +222 -156
  31. package/core/Worker.js +33 -12
  32. package/credential/credential-router.js +262 -0
  33. package/credential/repositories/credential-repository-documentdb.js +71 -1
  34. package/credential/repositories/credential-repository-factory.js +4 -6
  35. package/credential/repositories/credential-repository-interface.js +8 -2
  36. package/credential/repositories/credential-repository-mongo.js +45 -4
  37. package/credential/repositories/credential-repository-postgres.js +44 -3
  38. package/credential/repositories/credential-repository.js +3 -1
  39. package/credential/use-cases/delete-credential-for-user.js +50 -0
  40. package/credential/use-cases/get-credential-for-user.js +21 -3
  41. package/credential/use-cases/list-credentials-for-user.js +31 -0
  42. package/credential/use-cases/reauthorize-credential.js +103 -0
  43. package/credential/use-cases/update-authentication-status.js +5 -2
  44. package/database/MONGODB_TRANSACTION_FIX.md +72 -52
  45. package/database/config.js +13 -59
  46. package/database/documentdb-encryption-service.js +29 -13
  47. package/database/documentdb-utils.js +21 -12
  48. package/database/encryption/README.md +53 -36
  49. package/database/encryption/encryption-schema-registry.js +29 -23
  50. package/database/encryption/field-encryption-service.js +15 -4
  51. package/database/encryption/logger.js +3 -2
  52. package/database/encryption/prisma-encryption-extension.js +5 -8
  53. package/database/prisma.js +22 -14
  54. package/database/repositories/health-check-repository-factory.js +7 -5
  55. package/database/repositories/health-check-repository-interface.js +15 -5
  56. package/database/repositories/health-check-repository-postgres.js +1 -1
  57. package/database/use-cases/check-database-health-use-case.js +3 -2
  58. package/database/use-cases/check-database-state-use-case.js +8 -7
  59. package/database/use-cases/get-database-state-via-worker-use-case.js +15 -12
  60. package/database/use-cases/get-migration-status-use-case.js +4 -2
  61. package/database/use-cases/run-database-migration-use-case.js +41 -9
  62. package/database/use-cases/test-encryption-use-case.js +4 -5
  63. package/database/use-cases/trigger-database-migration-use-case.js +9 -8
  64. package/database/utils/mongodb-collection-utils.js +11 -3
  65. package/database/utils/mongodb-schema-init.js +10 -4
  66. package/database/utils/prisma-runner.js +144 -74
  67. package/database/utils/prisma-schema-parser.js +2 -2
  68. package/docs/PROCESS_MANAGEMENT_QUEUE_SPEC.md +135 -80
  69. package/encrypt/Cryptor.js +54 -49
  70. package/encrypt/aes-encryption-key-provider.js +82 -0
  71. package/encrypt/encryption-key-provider-interface.js +50 -0
  72. package/errors/fetch-error.js +1 -1
  73. package/generated/prisma-mongodb/edge.js +59 -4
  74. package/generated/prisma-mongodb/index-browser.js +49 -1
  75. package/generated/prisma-mongodb/index.d.ts +4023 -61
  76. package/generated/prisma-mongodb/index.js +59 -4
  77. package/generated/prisma-mongodb/package.json +1 -1
  78. package/generated/prisma-mongodb/schema.prisma +87 -0
  79. package/generated/prisma-mongodb/wasm.js +59 -4
  80. package/generated/prisma-postgresql/edge.js +58 -4
  81. package/generated/prisma-postgresql/index-browser.js +48 -1
  82. package/generated/prisma-postgresql/index.d.ts +4738 -54
  83. package/generated/prisma-postgresql/index.js +58 -4
  84. package/generated/prisma-postgresql/package.json +1 -1
  85. package/generated/prisma-postgresql/schema.prisma +85 -0
  86. package/generated/prisma-postgresql/wasm.js +58 -4
  87. package/handlers/WEBHOOKS.md +104 -81
  88. package/handlers/app-definition-loader.js +53 -10
  89. package/handlers/app-handler-helpers.js +14 -3
  90. package/handlers/backend-utils.js +0 -1
  91. package/handlers/database-migration-handler.js +64 -45
  92. package/handlers/middleware/admin-auth.js +53 -0
  93. package/handlers/routers/HEALTHCHECK.md +170 -135
  94. package/handlers/routers/admin.js +474 -0
  95. package/handlers/routers/auth.js +51 -9
  96. package/handlers/routers/db-migration.handler.js +2 -3
  97. package/handlers/routers/db-migration.js +102 -66
  98. package/handlers/routers/docs.js +180 -0
  99. package/handlers/routers/health.js +84 -283
  100. package/handlers/routers/integration-defined-routers.js +44 -33
  101. package/handlers/routers/integration-webhook-routers.js +73 -52
  102. package/handlers/routers/middleware/loadUser.js +39 -0
  103. package/handlers/routers/middleware/requireAdmin.js +43 -0
  104. package/handlers/routers/middleware/requireLoggedInUser.js +19 -0
  105. package/handlers/routers/user.js +30 -14
  106. package/handlers/routers/websocket.js +18 -4
  107. package/handlers/use-cases/check-integrations-health-use-case.js +13 -6
  108. package/handlers/workers/db-migration.js +74 -35
  109. package/handlers/workers/integration-defined-workers.js +31 -20
  110. package/index.js +50 -16
  111. package/infrastructure/scheduler/index.js +15 -11
  112. package/infrastructure/scheduler/netlify-scheduler-adapter.js +185 -0
  113. package/infrastructure/scheduler/scheduler-service-factory.js +38 -31
  114. package/integrations/WEBHOOK-QUICKSTART.md +21 -14
  115. package/integrations/index.js +4 -0
  116. package/integrations/integration-base.js +9 -6
  117. package/integrations/integration-router.js +1828 -157
  118. package/integrations/options.js +47 -20
  119. package/integrations/repositories/integration-mapping-repository-factory.js +4 -8
  120. package/integrations/repositories/integration-repository-documentdb.js +53 -24
  121. package/integrations/repositories/integration-repository-factory.js +6 -6
  122. package/integrations/repositories/integration-repository-interface.js +27 -9
  123. package/integrations/repositories/integration-repository-postgres.js +3 -3
  124. package/integrations/repositories/process-repository-factory.js +4 -7
  125. package/integrations/repositories/process-repository-interface.js +9 -5
  126. package/integrations/repositories/process-repository-mongo.js +7 -3
  127. package/integrations/repositories/process-repository-postgres.js +2 -2
  128. package/integrations/tests/doubles/config-capturing-integration.js +9 -8
  129. package/integrations/tests/doubles/dummy-integration-class.js +81 -8
  130. package/integrations/tests/doubles/test-integration-repository.js +51 -11
  131. package/integrations/use-cases/create-integration.js +36 -32
  132. package/integrations/use-cases/create-process.js +20 -9
  133. package/integrations/use-cases/delete-integration-for-user.js +5 -4
  134. package/integrations/use-cases/execute-proxy-request.js +564 -0
  135. package/integrations/use-cases/get-integration-for-user.js +25 -10
  136. package/integrations/use-cases/get-integration-instance-by-definition.js +13 -7
  137. package/integrations/use-cases/get-integrations-for-user.js +4 -1
  138. package/integrations/use-cases/get-possible-integrations.js +1 -1
  139. package/integrations/use-cases/get-process.js +5 -6
  140. package/integrations/use-cases/index.js +3 -1
  141. package/integrations/use-cases/load-integration-context.js +1 -5
  142. package/integrations/use-cases/update-process-state.js +10 -8
  143. package/integrations/utils/map-integration-dto.js +9 -3
  144. package/jest-setup.js +1 -1
  145. package/jest-teardown.js +1 -1
  146. package/lambda/TimeoutCatcher.js +32 -30
  147. package/lambda/index.js +1 -1
  148. package/logs/index.js +3 -3
  149. package/modules/domain/entities/AuthorizationSession.js +131 -0
  150. package/modules/domain/entities/index.js +10 -0
  151. package/modules/module.js +4 -9
  152. package/modules/repositories/authorization-session-repository-factory.js +53 -0
  153. package/modules/repositories/authorization-session-repository-interface.js +75 -0
  154. package/modules/repositories/authorization-session-repository-mongo.js +169 -0
  155. package/modules/repositories/authorization-session-repository-postgres.js +170 -0
  156. package/modules/repositories/module-repository-documentdb.js +89 -41
  157. package/modules/repositories/module-repository-factory.js +4 -6
  158. package/modules/repositories/module-repository-interface.js +13 -0
  159. package/modules/repositories/module-repository-mongo.js +40 -5
  160. package/modules/repositories/module-repository-postgres.js +42 -5
  161. package/modules/repositories/module-repository.js +28 -0
  162. package/modules/requester/api-key.js +0 -2
  163. package/modules/requester/basic.js +0 -1
  164. package/modules/requester/oauth-2.js +3 -14
  165. package/modules/test/mock-api/definition.js +15 -10
  166. package/modules/test/mock-api/mocks/hubspot.js +36 -36
  167. package/modules/tests/doubles/test-module-factory.js +10 -4
  168. package/modules/tests/doubles/test-module-repository.js +1 -1
  169. package/modules/use-cases/delete-module-entity.js +23 -0
  170. package/modules/use-cases/get-authorization-requirements.js +110 -0
  171. package/modules/use-cases/get-entities-for-user.js +1 -1
  172. package/modules/use-cases/get-entity-options-by-id.js +8 -6
  173. package/modules/use-cases/get-module-entity-by-id.js +16 -0
  174. package/modules/use-cases/get-module.js +12 -9
  175. package/modules/use-cases/process-authorization-callback.js +12 -6
  176. package/modules/use-cases/process-authorization-step.js +160 -0
  177. package/modules/use-cases/refresh-entity-options.js +8 -6
  178. package/modules/use-cases/start-authorization-session.js +86 -0
  179. package/modules/use-cases/test-module-auth.js +8 -6
  180. package/modules/use-cases/update-module-entity.js +27 -0
  181. package/modules/utils/map-module-dto.js +2 -2
  182. package/openapi/openapi-spec-generator.js +370 -0
  183. package/openapi/openapi-v1.yaml +1360 -0
  184. package/openapi/openapi-v2.yaml +1535 -0
  185. package/package.json +7 -9
  186. package/prisma-mongodb/schema.prisma +87 -0
  187. package/prisma-postgresql/migrations/20260222000000_add_admin_process_and_script_schedule/migration.sql +55 -0
  188. package/prisma-postgresql/schema.prisma +85 -0
  189. package/providers/index.js +13 -0
  190. package/providers/resolve-provider.js +93 -0
  191. package/queues/index.js +19 -0
  192. package/queues/providers/index.js +7 -0
  193. package/queues/providers/netlify-background-provider.js +76 -0
  194. package/queues/providers/qstash-queue-provider.js +122 -0
  195. package/queues/queue-client-interface.js +55 -0
  196. package/queues/queue-provider-factory.js +93 -0
  197. package/queues/queue-provider.js +46 -0
  198. package/queues/queuer-util.js +33 -25
  199. package/syncs/repositories/sync-repository-documentdb.js +39 -19
  200. package/syncs/repositories/sync-repository-factory.js +4 -4
  201. package/syncs/repositories/sync-repository-interface.js +6 -2
  202. package/syncs/repositories/sync-repository-postgres.js +19 -13
  203. package/syncs/sync.js +104 -100
  204. package/token/repositories/token-repository-factory.js +4 -6
  205. package/types/assertions/index.d.ts +72 -72
  206. package/types/core/index.d.ts +98 -53
  207. package/types/errors/index.d.ts +61 -54
  208. package/types/eslint-config/index.d.ts +40 -40
  209. package/types/lambda/index.d.ts +26 -26
  210. package/types/logs/index.d.ts +4 -4
  211. package/types/prettier-config/index.d.ts +5 -5
  212. package/types/test-environment/index.d.ts +14 -14
  213. package/types/tsconfig.json +95 -93
  214. package/user/repositories/user-repository-factory.js +4 -4
  215. package/user/tests/doubles/test-user-repository.js +5 -2
  216. package/user/use-cases/authenticate-user.js +2 -4
  217. package/user/use-cases/authenticate-with-shared-secret.js +1 -1
  218. package/user/use-cases/create-individual-user.js +8 -7
  219. package/user/use-cases/create-organization-user.js +1 -1
  220. package/user/use-cases/create-token-for-user-id.js +1 -1
  221. package/user/use-cases/delete-user.js +47 -0
  222. package/user/use-cases/get-user-from-adopter-jwt.js +0 -2
  223. package/user/use-cases/get-user-from-bearer-token.js +7 -3
  224. package/user/use-cases/get-user-from-x-frigg-headers.js +14 -11
  225. package/user/use-cases/login-user.js +8 -8
  226. package/user/user.js +18 -4
  227. package/utils/backend-path.js +2 -2
  228. package/utils/index.js +4 -1
  229. package/websocket/repositories/websocket-connection-repository-documentdb.js +46 -21
  230. package/websocket/repositories/websocket-connection-repository-factory.js +4 -8
  231. package/websocket/repositories/websocket-connection-repository-mongo.js +26 -16
  232. package/websocket/repositories/websocket-connection-repository-postgres.js +26 -15
  233. package/websocket/repositories/websocket-connection-repository.js +26 -20
  234. package/websocket/websocket-message-sender-interface.js +38 -0
  235. package/database/adapters/lambda-invoker.js +0 -97
  236. package/database/repositories/migration-status-repository-s3.js +0 -137
  237. package/infrastructure/scheduler/eventbridge-scheduler-adapter.js +0 -184
@@ -5,7 +5,7 @@
5
5
  The current BaseCRMIntegration implementation has a **race condition** in process record updates:
6
6
 
7
7
  1. Multiple queue workers process batches concurrently
8
- 2. Each worker calls `processManager.updateMetrics()`
8
+ 2. Each worker calls `processManager.updateMetrics()`
9
9
  3. Multiple workers read-modify-write the same process record simultaneously
10
10
  4. **Result**: Lost updates, inconsistent metrics, potential data corruption
11
11
 
@@ -13,7 +13,7 @@ The current BaseCRMIntegration implementation has a **race condition** in proces
13
13
 
14
14
  ```
15
15
  Time 1: Worker A reads process.results.aggregateData.totalSynced = 100
16
- Time 2: Worker B reads process.results.aggregateData.totalSynced = 100
16
+ Time 2: Worker B reads process.results.aggregateData.totalSynced = 100
17
17
  Time 3: Worker A adds 50 → writes totalSynced = 150
18
18
  Time 4: Worker B adds 30 → writes totalSynced = 130 (overwrites Worker A's update!)
19
19
  ```
@@ -24,10 +24,10 @@ Time 4: Worker B adds 30 → writes totalSynced = 130 (overwrites Worker A's upd
24
24
 
25
25
  Create a dedicated FIFO SQS queue in **Frigg Core** for all process management operations:
26
26
 
27
- - **Queue Type**: FIFO (First-In-First-Out)
28
- - **Message Group ID**: `process-{processId}` (ensures ordered processing per process)
29
- - **Message Deduplication**: Enabled (prevents duplicate updates)
30
- - **Dead Letter Queue**: Enabled (captures failed updates)
27
+ - **Queue Type**: FIFO (First-In-First-Out)
28
+ - **Message Group ID**: `process-{processId}` (ensures ordered processing per process)
29
+ - **Message Deduplication**: Enabled (prevents duplicate updates)
30
+ - **Dead Letter Queue**: Enabled (captures failed updates)
31
31
 
32
32
  ### Architecture
33
33
 
@@ -80,7 +80,7 @@ class ProcessManagementQueueFactory {
80
80
  */
81
81
  async createProcessManagementQueue(integrationName) {
82
82
  const queueName = `${integrationName}-process-management.fifo`;
83
-
83
+
84
84
  const params = {
85
85
  QueueName: queueName,
86
86
  Attributes: {
@@ -92,7 +92,7 @@ class ProcessManagementQueueFactory {
92
92
  ReceiveMessageWaitTimeSeconds: '20', // Long polling
93
93
  DeadLetterTargetArn: `${queueName}-dlq.fifo`, // DLQ
94
94
  MaxReceiveCount: '3', // Retry failed messages 3 times
95
- }
95
+ },
96
96
  };
97
97
 
98
98
  const result = await this.sqs.createQueue(params).promise();
@@ -114,7 +114,7 @@ class ProcessManagementQueueFactory {
114
114
  processId,
115
115
  operation,
116
116
  data,
117
- timestamp: new Date().toISOString()
117
+ timestamp: new Date().toISOString(),
118
118
  }),
119
119
  MessageGroupId: `process-${processId}`,
120
120
  MessageDeduplicationId: `${processId}-${operation}-${Date.now()}`,
@@ -137,7 +137,9 @@ const {
137
137
  UpdateProcessMetrics,
138
138
  GetProcess,
139
139
  } = require('../use-cases');
140
- const { createProcessRepository } = require('../repositories/process-repository-factory');
140
+ const {
141
+ createProcessRepository,
142
+ } = require('../repositories/process-repository-factory');
141
143
 
142
144
  /**
143
145
  * Handler for process management FIFO queue messages
@@ -146,8 +148,12 @@ const { createProcessRepository } = require('../repositories/process-repository-
146
148
  class ProcessUpdateHandler {
147
149
  constructor() {
148
150
  const processRepository = createProcessRepository();
149
- this.updateProcessStateUseCase = new UpdateProcessState({ processRepository });
150
- this.updateProcessMetricsUseCase = new UpdateProcessMetrics({ processRepository });
151
+ this.updateProcessStateUseCase = new UpdateProcessState({
152
+ processRepository,
153
+ });
154
+ this.updateProcessMetricsUseCase = new UpdateProcessMetrics({
155
+ processRepository,
156
+ });
151
157
  this.getProcessUseCase = new GetProcess({ processRepository });
152
158
  }
153
159
 
@@ -164,35 +170,35 @@ class ProcessUpdateHandler {
164
170
  switch (operation) {
165
171
  case 'UPDATE_STATE':
166
172
  await this.updateProcessStateUseCase.execute(
167
- processId,
168
- data.state,
173
+ processId,
174
+ data.state,
169
175
  data.contextUpdates
170
176
  );
171
177
  break;
172
178
 
173
179
  case 'UPDATE_METRICS':
174
180
  await this.updateProcessMetricsUseCase.execute(
175
- processId,
181
+ processId,
176
182
  data.metricsUpdate
177
183
  );
178
184
  break;
179
185
 
180
186
  case 'COMPLETE_PROCESS':
181
187
  await this.updateProcessStateUseCase.execute(
182
- processId,
183
- 'COMPLETED',
188
+ processId,
189
+ 'COMPLETED',
184
190
  { endTime: new Date().toISOString() }
185
191
  );
186
192
  break;
187
193
 
188
194
  case 'HANDLE_ERROR':
189
195
  await this.updateProcessStateUseCase.execute(
190
- processId,
191
- 'ERROR',
196
+ processId,
197
+ 'ERROR',
192
198
  {
193
199
  error: data.error.message,
194
200
  errorStack: data.error.stack,
195
- errorTimestamp: new Date().toISOString()
201
+ errorTimestamp: new Date().toISOString(),
196
202
  }
197
203
  );
198
204
  break;
@@ -201,7 +207,9 @@ class ProcessUpdateHandler {
201
207
  throw new Error(`Unknown process operation: ${operation}`);
202
208
  }
203
209
 
204
- console.log(`Process update completed: ${operation} for process ${processId}`);
210
+ console.log(
211
+ `Process update completed: ${operation} for process ${processId}`
212
+ );
205
213
  } catch (error) {
206
214
  console.error('Process update failed:', error);
207
215
  throw error; // Will trigger SQS retry/DLQ
@@ -217,7 +225,9 @@ module.exports = { ProcessUpdateHandler };
217
225
  **File**: `/packages/core/integrations/queues/process-queue-manager.js`
218
226
 
219
227
  ```javascript
220
- const { ProcessManagementQueueFactory } = require('./process-management-queue-factory');
228
+ const {
229
+ ProcessManagementQueueFactory,
230
+ } = require('./process-management-queue-factory');
221
231
 
222
232
  /**
223
233
  * Manages process update operations via FIFO queue
@@ -236,7 +246,9 @@ class ProcessQueueManager {
236
246
  */
237
247
  async getProcessQueueUrl(integrationName) {
238
248
  if (!this.queueUrls.has(integrationName)) {
239
- const queueUrl = await this.factory.createProcessManagementQueue(integrationName);
249
+ const queueUrl = await this.factory.createProcessManagementQueue(
250
+ integrationName
251
+ );
240
252
  this.queueUrls.set(integrationName, queueUrl);
241
253
  }
242
254
  return this.queueUrls.get(integrationName);
@@ -250,12 +262,22 @@ class ProcessQueueManager {
250
262
  * @param {Object} contextUpdates - Context updates
251
263
  * @returns {Promise<void>}
252
264
  */
253
- async queueStateUpdate(integrationName, processId, state, contextUpdates = {}) {
265
+ async queueStateUpdate(
266
+ integrationName,
267
+ processId,
268
+ state,
269
+ contextUpdates = {}
270
+ ) {
254
271
  const queueUrl = await this.getProcessQueueUrl(integrationName);
255
- await this.factory.sendProcessUpdate(queueUrl, processId, 'UPDATE_STATE', {
256
- state,
257
- contextUpdates
258
- });
272
+ await this.factory.sendProcessUpdate(
273
+ queueUrl,
274
+ processId,
275
+ 'UPDATE_STATE',
276
+ {
277
+ state,
278
+ contextUpdates,
279
+ }
280
+ );
259
281
  }
260
282
 
261
283
  /**
@@ -267,9 +289,14 @@ class ProcessQueueManager {
267
289
  */
268
290
  async queueMetricsUpdate(integrationName, processId, metricsUpdate) {
269
291
  const queueUrl = await this.getProcessQueueUrl(integrationName);
270
- await this.factory.sendProcessUpdate(queueUrl, processId, 'UPDATE_METRICS', {
271
- metricsUpdate
272
- });
292
+ await this.factory.sendProcessUpdate(
293
+ queueUrl,
294
+ processId,
295
+ 'UPDATE_METRICS',
296
+ {
297
+ metricsUpdate,
298
+ }
299
+ );
273
300
  }
274
301
 
275
302
  /**
@@ -280,7 +307,12 @@ class ProcessQueueManager {
280
307
  */
281
308
  async queueProcessCompletion(integrationName, processId) {
282
309
  const queueUrl = await this.getProcessQueueUrl(integrationName);
283
- await this.factory.sendProcessUpdate(queueUrl, processId, 'COMPLETE_PROCESS', {});
310
+ await this.factory.sendProcessUpdate(
311
+ queueUrl,
312
+ processId,
313
+ 'COMPLETE_PROCESS',
314
+ {}
315
+ );
284
316
  }
285
317
 
286
318
  /**
@@ -292,12 +324,17 @@ class ProcessQueueManager {
292
324
  */
293
325
  async queueErrorHandling(integrationName, processId, error) {
294
326
  const queueUrl = await this.getProcessQueueUrl(integrationName);
295
- await this.factory.sendProcessUpdate(queueUrl, processId, 'HANDLE_ERROR', {
296
- error: {
297
- message: error.message,
298
- stack: error.stack
327
+ await this.factory.sendProcessUpdate(
328
+ queueUrl,
329
+ processId,
330
+ 'HANDLE_ERROR',
331
+ {
332
+ error: {
333
+ message: error.message,
334
+ stack: error.stack,
335
+ },
299
336
  }
300
- });
337
+ );
301
338
  }
302
339
  }
303
340
 
@@ -311,7 +348,9 @@ module.exports = { ProcessQueueManager };
311
348
  **File**: `/Users/sean/Documents/GitHub/quo--frigg/backend/src/base/services/ProcessManager.js`
312
349
 
313
350
  ```javascript
314
- const { ProcessQueueManager } = require('@friggframework/core/integrations/queues/process-queue-manager');
351
+ const {
352
+ ProcessQueueManager,
353
+ } = require('@friggframework/core/integrations/queues/process-queue-manager');
315
354
 
316
355
  class ProcessManager {
317
356
  constructor({
@@ -394,11 +433,11 @@ class ProcessManager {
394
433
  const attachProcessManagementQueues = (definition, AppDefinition) => {
395
434
  for (const integration of AppDefinition.integrations) {
396
435
  const integrationName = integration.Definition.name;
397
-
436
+
398
437
  // Create FIFO queue for process management
399
438
  const processQueueName = `${integrationName}ProcessManagementQueue`;
400
439
  const processDLQName = `${integrationName}ProcessManagementDLQ`;
401
-
440
+
402
441
  // FIFO Queue
403
442
  definition.resources.Resources[processQueueName] = {
404
443
  Type: 'AWS::SQS::Queue',
@@ -411,7 +450,9 @@ const attachProcessManagementQueues = (definition, AppDefinition) => {
411
450
  DelaySeconds: 0,
412
451
  ReceiveMessageWaitTimeSeconds: 20, // Long polling
413
452
  RedrivePolicy: {
414
- deadLetterTargetArn: { 'Fn::GetAtt': [processDLQName, 'Arn'] },
453
+ deadLetterTargetArn: {
454
+ 'Fn::GetAtt': [processDLQName, 'Arn'],
455
+ },
415
456
  maxReceiveCount: 3,
416
457
  },
417
458
  },
@@ -430,15 +471,18 @@ const attachProcessManagementQueues = (definition, AppDefinition) => {
430
471
  // Process Update Handler Function
431
472
  const processHandlerName = `${integrationName}ProcessUpdateHandler`;
432
473
  definition.functions[processHandlerName] = {
433
- handler: 'node_modules/@friggframework/core/handlers/process-update-handler.handler',
474
+ handler:
475
+ 'node_modules/@friggframework/core/handlers/process-update-handler.handler',
434
476
  reservedConcurrency: 1, // Process updates sequentially per integration
435
- events: [{
436
- sqs: {
437
- arn: { 'Fn::GetAtt': [processQueueName, 'Arn'] },
438
- batchSize: 1, // Process one update at a time
439
- maximumBatchingWindowInSeconds: 5,
477
+ events: [
478
+ {
479
+ sqs: {
480
+ arn: { 'Fn::GetAtt': [processQueueName, 'Arn'] },
481
+ batchSize: 1, // Process one update at a time
482
+ maximumBatchingWindowInSeconds: 5,
483
+ },
440
484
  },
441
- }],
485
+ ],
442
486
  timeout: 30,
443
487
  environment: {
444
488
  INTEGRATION_NAME: integrationName,
@@ -451,64 +495,75 @@ const attachProcessManagementQueues = (definition, AppDefinition) => {
451
495
  ## Benefits
452
496
 
453
497
  ### ✅ Race Condition Prevention
454
- - FIFO queue ensures ordered processing per process ID
455
- - MessageGroupId = `process-{processId}` guarantees sequential updates
456
- - No more lost updates or inconsistent metrics
498
+
499
+ - FIFO queue ensures ordered processing per process ID
500
+ - MessageGroupId = `process-{processId}` guarantees sequential updates
501
+ - No more lost updates or inconsistent metrics
457
502
 
458
503
  ### ✅ Cost Optimization
459
- - Only one FIFO queue per integration (not per process)
460
- - MessageGroupId provides ordering without expensive per-process queues
461
- - Long polling reduces API calls
504
+
505
+ - Only one FIFO queue per integration (not per process)
506
+ - MessageGroupId provides ordering without expensive per-process queues
507
+ - Long polling reduces API calls
462
508
 
463
509
  ### ✅ Reliability
464
- - Dead Letter Queue captures failed updates
465
- - Retry mechanism with exponential backoff
466
- - Content-based deduplication prevents duplicate processing
510
+
511
+ - Dead Letter Queue captures failed updates
512
+ - Retry mechanism with exponential backoff
513
+ - Content-based deduplication prevents duplicate processing
467
514
 
468
515
  ### ✅ Scalability
469
- - Each integration has its own process management queue
470
- - Process updates don't block data processing
471
- - Can scale process update handlers independently
516
+
517
+ - Each integration has its own process management queue
518
+ - Process updates don't block data processing
519
+ - Can scale process update handlers independently
472
520
 
473
521
  ## Migration Strategy
474
522
 
475
523
  ### Phase 1: Current Implementation (Native Queue)
476
- - Use existing integration queue for process updates
477
- - Accept potential race conditions for now
478
- - Focus on core functionality
524
+
525
+ - Use existing integration queue for process updates
526
+ - Accept potential race conditions for now
527
+ - Focus on core functionality
479
528
 
480
529
  ### Phase 2: FIFO Queue Implementation
481
- - Implement FIFO queue infrastructure in Frigg Core
482
- - Update ProcessManager to use FIFO queue
483
- - Deploy with feature flag
530
+
531
+ - Implement FIFO queue infrastructure in Frigg Core
532
+ - Update ProcessManager to use FIFO queue
533
+ - Deploy with feature flag
484
534
 
485
535
  ### Phase 3: Full Migration
486
- - Switch all integrations to FIFO queue
487
- - Remove native queue process update code
488
- - Monitor for race condition elimination
536
+
537
+ - Switch all integrations to FIFO queue
538
+ - Remove native queue process update code
539
+ - Monitor for race condition elimination
489
540
 
490
541
  ## Cost Analysis
491
542
 
492
543
  ### FIFO Queue Costs (per integration)
493
- - **Queue Creation**: Free
494
- - **Message Storage**: $0.40 per million messages
495
- - **Message Processing**: $0.40 per million requests
496
- - **Example**: 10 integrations, 1000 process updates/day = ~$2.40/month
544
+
545
+ - **Queue Creation**: Free
546
+ - **Message Storage**: $0.40 per million messages
547
+ - **Message Processing**: $0.40 per million requests
548
+ - **Example**: 10 integrations, 1000 process updates/day = ~$2.40/month
497
549
 
498
550
  ### Benefits vs Costs
499
- - **Cost**: ~$2.40/month for 10 integrations
500
- - **Benefit**: Eliminates race conditions, ensures data consistency
501
- - **ROI**: High - prevents data corruption and debugging time
551
+
552
+ - **Cost**: ~$2.40/month for 10 integrations
553
+ - **Benefit**: Eliminates race conditions, ensures data consistency
554
+ - **ROI**: High - prevents data corruption and debugging time
502
555
 
503
556
  ## Implementation Priority
504
557
 
505
558
  **High Priority** - Race conditions in process updates can cause:
506
- - Lost sync progress
507
- - Inconsistent metrics
508
- - Difficult debugging
509
- - Data integrity issues
559
+
560
+ - Lost sync progress
561
+ - Inconsistent metrics
562
+ - Difficult debugging
563
+ - Data integrity issues
510
564
 
511
565
  **Recommended Timeline**:
566
+
512
567
  1. **Week 1**: Implement FIFO queue infrastructure in Frigg Core
513
568
  2. **Week 2**: Update ProcessManager to use FIFO queue
514
569
  3. **Week 3**: Deploy and test with one integration
@@ -1,58 +1,75 @@
1
1
  /**
2
2
  * Cryptor - Encryption Service Adapter
3
3
  *
4
- * Infrastructure Layer adapter for AWS KMS and local AES encryption.
5
- * Provides envelope encryption pattern for field-level encryption.
4
+ * Infrastructure Layer adapter for envelope encryption.
5
+ * Key management is delegated to an EncryptionKeyProviderInterface adapter:
6
+ * - KmsEncryptionKeyProvider (in @friggframework/provider-aws) for AWS KMS
7
+ * - AesEncryptionKeyProvider (in core) for local AES keys
6
8
  *
7
9
  * Envelope Encryption Pattern:
8
- * 1. Generate Data Encryption Key (DEK) via KMS or locally
10
+ * 1. Generate Data Encryption Key (DEK) via key provider
9
11
  * 2. Encrypt field value with DEK using AES-256-CTR
10
- * 3. Encrypt DEK with Master Key (KMS CMK or AES_KEY)
12
+ * 3. Store encrypted DEK alongside ciphertext
11
13
  * 4. Return format: "keyId:encryptedText:encryptedKey"
12
14
  *
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
15
+ * BREAKING CHANGE (v3): A keyProvider must be explicitly provided,
16
+ * or pass { shouldUseAws: false } to auto-create AesEncryptionKeyProvider.
17
+ * For AWS KMS, pass `new KmsEncryptionKeyProvider()` from @friggframework/provider-aws.
18
+ * See docs/architecture-decisions/010-decouple-aws-from-core.md for migration guide.
17
19
  */
18
20
 
19
- const crypto = require('crypto');
20
- const { KMSClient, GenerateDataKeyCommand, DecryptCommand } = require('@aws-sdk/client-kms');
21
21
  const aes = require('./aes');
22
22
 
23
23
  class Cryptor {
24
- constructor({ shouldUseAws }) {
25
- this.shouldUseAws = shouldUseAws;
26
- }
27
-
28
- async generateDataKey() {
29
- if (this.shouldUseAws) {
30
- const kmsClient = new KMSClient({});
31
- const command = new GenerateDataKeyCommand({
32
- KeyId: process.env.KMS_KEY_ARN,
33
- KeySpec: 'AES_256',
34
- });
35
- const dataKey = await kmsClient.send(command);
36
-
37
- const keyId = Buffer.from(dataKey.KeyId).toString('base64');
38
- const encryptedKey = Buffer.from(dataKey.CiphertextBlob).toString('base64');
39
- const plaintext = dataKey.Plaintext;
40
- return { keyId, encryptedKey, plaintext };
24
+ /**
25
+ * @param {Object} options
26
+ * @param {boolean} [options.shouldUseAws] - true requires explicit keyProvider; false auto-creates AesEncryptionKeyProvider
27
+ * @param {EncryptionKeyProviderInterface} [options.keyProvider] - Explicit key provider (takes precedence)
28
+ */
29
+ constructor({ shouldUseAws, keyProvider } = {}) {
30
+ if (keyProvider) {
31
+ this._keyProvider = keyProvider;
32
+ } else if (shouldUseAws) {
33
+ throw new Error(
34
+ 'Cryptor with shouldUseAws=true requires an explicit keyProvider. Pass one via constructor options, e.g.:\n' +
35
+ ' const { KmsEncryptionKeyProvider } = require("@friggframework/provider-aws");\n' +
36
+ ' new Cryptor({ shouldUseAws: true, keyProvider: new KmsEncryptionKeyProvider() })\n' +
37
+ 'See docs/architecture-decisions/010-decouple-aws-from-core.md for migration guide.'
38
+ );
39
+ } else {
40
+ // AES mode — no AWS dependency needed
41
+ const { AesEncryptionKeyProvider } = require('./aes-encryption-key-provider');
42
+ this._keyProvider = new AesEncryptionKeyProvider();
41
43
  }
44
+ this._shouldUseAws = shouldUseAws;
45
+ }
42
46
 
43
- const { AES_KEY, AES_KEY_ID } = process.env;
44
- const randomKey = crypto.randomBytes(32).toString('hex').slice(0, 32);
47
+ /**
48
+ * Get the key provider.
49
+ * @returns {EncryptionKeyProviderInterface}
50
+ */
51
+ _getKeyProvider() {
52
+ return this._keyProvider;
53
+ }
45
54
 
46
- return {
47
- keyId: Buffer.from(AES_KEY_ID).toString('base64'),
48
- encryptedKey: Buffer.from(aes.encrypt(randomKey, AES_KEY)).toString(
49
- 'base64'
50
- ),
51
- plaintext: randomKey,
52
- };
55
+ async generateDataKey() {
56
+ return this._getKeyProvider().generateDataKey();
53
57
  }
54
58
 
59
+ /**
60
+ * Look up an AES key by identifier from environment variables.
61
+ * Kept for backward compatibility.
62
+ *
63
+ * @param {string} keyId - Key identifier
64
+ * @returns {string} The master key
65
+ */
55
66
  getKeyFromEnvironment(keyId) {
67
+ const provider = this._getKeyProvider();
68
+ if (typeof provider.getKeyFromEnvironment === 'function') {
69
+ return provider.getKeyFromEnvironment(keyId);
70
+ }
71
+
72
+ // Fallback for providers that don't implement getKeyFromEnvironment
56
73
  const availableKeys = {
57
74
  [process.env.AES_KEY_ID]: process.env.AES_KEY,
58
75
  [process.env.DEPRECATED_AES_KEY_ID]: process.env.DEPRECATED_AES_KEY,
@@ -68,19 +85,7 @@ class Cryptor {
68
85
  }
69
86
 
70
87
  async decryptDataKey(keyId, encryptedKey) {
71
- if (this.shouldUseAws) {
72
- const kmsClient = new KMSClient({});
73
- const command = new DecryptCommand({
74
- KeyId: keyId,
75
- CiphertextBlob: encryptedKey,
76
- });
77
- const dataKey = await kmsClient.send(command);
78
-
79
- return dataKey.Plaintext;
80
- }
81
-
82
- const key = this.getKeyFromEnvironment(keyId);
83
- return aes.decrypt(encryptedKey, key);
88
+ return this._getKeyProvider().decryptDataKey(keyId, encryptedKey);
84
89
  }
85
90
 
86
91
  async encrypt(text) {
@@ -0,0 +1,82 @@
1
+ /**
2
+ * AES Encryption Key Provider (Adapter)
3
+ *
4
+ * Local AES-based implementation of EncryptionKeyProviderInterface.
5
+ * Uses environment-variable-based master keys for envelope encryption.
6
+ *
7
+ * No external dependencies — works on any platform.
8
+ *
9
+ * Environment Variables:
10
+ * - AES_KEY_ID: Identifier for the current AES master key
11
+ * - AES_KEY: The current AES master key (32 chars)
12
+ * - DEPRECATED_AES_KEY_ID: (optional) Previous key ID for key rotation
13
+ * - DEPRECATED_AES_KEY: (optional) Previous key for decrypting old data
14
+ */
15
+
16
+ const crypto = require('crypto');
17
+ const aes = require('./aes');
18
+ const {
19
+ EncryptionKeyProviderInterface,
20
+ } = require('./encryption-key-provider-interface');
21
+
22
+ class AesEncryptionKeyProvider extends EncryptionKeyProviderInterface {
23
+ /**
24
+ * Generate a data encryption key using local AES
25
+ *
26
+ * Creates a random DEK and encrypts it with the AES master key
27
+ * from environment variables.
28
+ *
29
+ * @returns {Promise<{keyId: string, encryptedKey: string, plaintext: string}>}
30
+ */
31
+ async generateDataKey() {
32
+ const { AES_KEY, AES_KEY_ID } = process.env;
33
+ const randomKey = crypto.randomBytes(32).toString('hex').slice(0, 32);
34
+
35
+ return {
36
+ keyId: Buffer.from(AES_KEY_ID).toString('base64'),
37
+ encryptedKey: Buffer.from(aes.encrypt(randomKey, AES_KEY)).toString(
38
+ 'base64'
39
+ ),
40
+ plaintext: randomKey,
41
+ };
42
+ }
43
+
44
+ /**
45
+ * Decrypt a data encryption key using the AES master key
46
+ *
47
+ * Looks up the master key by keyId from environment variables,
48
+ * supporting key rotation via DEPRECATED_AES_KEY.
49
+ *
50
+ * @param {string} keyId - Key identifier to look up in environment
51
+ * @param {string|Buffer} encryptedKey - Encrypted DEK to decrypt
52
+ * @returns {Promise<string>} Decrypted plaintext DEK
53
+ */
54
+ async decryptDataKey(keyId, encryptedKey) {
55
+ const key = this.getKeyFromEnvironment(keyId);
56
+ return aes.decrypt(encryptedKey, key);
57
+ }
58
+
59
+ /**
60
+ * Look up an AES master key by its identifier
61
+ *
62
+ * @param {string} keyId - Key identifier
63
+ * @returns {string} The master key
64
+ * @throws {Error} If key not found in environment
65
+ */
66
+ getKeyFromEnvironment(keyId) {
67
+ const availableKeys = {
68
+ [process.env.AES_KEY_ID]: process.env.AES_KEY,
69
+ [process.env.DEPRECATED_AES_KEY_ID]: process.env.DEPRECATED_AES_KEY,
70
+ };
71
+
72
+ const key = availableKeys[keyId];
73
+
74
+ if (!key) {
75
+ throw new Error('Encryption key not found');
76
+ }
77
+
78
+ return key;
79
+ }
80
+ }
81
+
82
+ module.exports = { AesEncryptionKeyProvider };
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Encryption Key Provider Interface (Port)
3
+ *
4
+ * Defines the contract for envelope encryption key operations.
5
+ * Used by Cryptor for generating and decrypting data encryption keys.
6
+ *
7
+ * Following Frigg's hexagonal architecture pattern:
8
+ * - Port defines WHAT the service does (contract)
9
+ * - Adapters implement HOW (AWS KMS, local AES, Vault, etc.)
10
+ *
11
+ * Envelope Encryption Pattern:
12
+ * 1. generateDataKey() creates a fresh DEK (plaintext + encrypted form)
13
+ * 2. Caller encrypts data with the plaintext DEK
14
+ * 3. Caller stores the encrypted DEK alongside the ciphertext
15
+ * 4. decryptDataKey() recovers the plaintext DEK from the encrypted form
16
+ * 5. Caller decrypts data with the recovered DEK
17
+ */
18
+ class EncryptionKeyProviderInterface {
19
+ /**
20
+ * Generate a new data encryption key
21
+ *
22
+ * Returns both the plaintext key (for immediate encryption) and an
23
+ * encrypted copy (for storage alongside the ciphertext).
24
+ *
25
+ * @returns {Promise<{keyId: string, encryptedKey: string, plaintext: string|Buffer}>}
26
+ * - keyId: Base64-encoded identifier for the master key used
27
+ * - encryptedKey: Base64-encoded encrypted copy of the DEK
28
+ * - plaintext: The raw DEK (string or Buffer) for immediate use
29
+ */
30
+ async generateDataKey() {
31
+ throw new Error(
32
+ 'Method generateDataKey must be implemented by subclass'
33
+ );
34
+ }
35
+
36
+ /**
37
+ * Decrypt a previously encrypted data encryption key
38
+ *
39
+ * @param {string} keyId - Identifier of the master key used for encryption
40
+ * @param {string|Buffer} encryptedKey - The encrypted DEK to decrypt
41
+ * @returns {Promise<string|Buffer>} The decrypted plaintext DEK
42
+ */
43
+ async decryptDataKey(keyId, encryptedKey) {
44
+ throw new Error(
45
+ 'Method decryptDataKey must be implemented by subclass'
46
+ );
47
+ }
48
+ }
49
+
50
+ module.exports = { EncryptionKeyProviderInterface };