@friggframework/core 2.0.0-next.1 → 2.0.0-next.100

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 (342) hide show
  1. package/CLAUDE.md +702 -0
  2. package/README.md +959 -50
  3. package/admin-scripts/index.js +52 -0
  4. package/admin-scripts/repositories/admin-script-execution-repository-documentdb.js +21 -0
  5. package/admin-scripts/repositories/admin-script-execution-repository-factory.js +51 -0
  6. package/admin-scripts/repositories/admin-script-execution-repository-interface.js +166 -0
  7. package/admin-scripts/repositories/admin-script-execution-repository-mongo.js +228 -0
  8. package/admin-scripts/repositories/admin-script-execution-repository-postgres.js +268 -0
  9. package/admin-scripts/repositories/script-schedule-repository-documentdb.js +21 -0
  10. package/admin-scripts/repositories/script-schedule-repository-factory.js +51 -0
  11. package/admin-scripts/repositories/script-schedule-repository-interface.js +108 -0
  12. package/admin-scripts/repositories/script-schedule-repository-mongo.js +179 -0
  13. package/admin-scripts/repositories/script-schedule-repository-postgres.js +210 -0
  14. package/application/commands/README.md +451 -0
  15. package/application/commands/admin-script-commands.js +358 -0
  16. package/application/commands/credential-commands.js +245 -0
  17. package/application/commands/entity-commands.js +336 -0
  18. package/application/commands/integration-commands.js +343 -0
  19. package/application/commands/process-commands.js +135 -0
  20. package/application/commands/scheduler-commands.js +263 -0
  21. package/application/commands/user-commands.js +312 -0
  22. package/application/index.js +82 -0
  23. package/assertions/index.js +0 -3
  24. package/core/CLAUDE.md +690 -0
  25. package/core/Worker.js +60 -24
  26. package/core/create-handler.js +84 -6
  27. package/credential/repositories/credential-repository-documentdb.js +304 -0
  28. package/credential/repositories/credential-repository-factory.js +54 -0
  29. package/credential/repositories/credential-repository-interface.js +98 -0
  30. package/credential/repositories/credential-repository-mongo.js +269 -0
  31. package/credential/repositories/credential-repository-postgres.js +287 -0
  32. package/credential/repositories/credential-repository.js +300 -0
  33. package/credential/use-cases/get-credential-for-user.js +25 -0
  34. package/credential/use-cases/update-authentication-status.js +15 -0
  35. package/database/MONGODB_TRANSACTION_FIX.md +198 -0
  36. package/database/adapters/lambda-invoker.js +97 -0
  37. package/database/config.js +154 -0
  38. package/database/documentdb-encryption-service.js +330 -0
  39. package/database/documentdb-utils.js +136 -0
  40. package/database/encryption/README.md +839 -0
  41. package/database/encryption/documentdb-encryption-service.md +3575 -0
  42. package/database/encryption/encryption-schema-registry.js +401 -0
  43. package/database/encryption/field-encryption-service.js +254 -0
  44. package/database/encryption/logger.js +79 -0
  45. package/database/encryption/prisma-encryption-extension.js +230 -0
  46. package/database/index.js +21 -21
  47. package/database/prisma.js +182 -0
  48. package/database/repositories/health-check-repository-documentdb.js +138 -0
  49. package/database/repositories/health-check-repository-factory.js +48 -0
  50. package/database/repositories/health-check-repository-interface.js +82 -0
  51. package/database/repositories/health-check-repository-mongodb.js +89 -0
  52. package/database/repositories/health-check-repository-postgres.js +82 -0
  53. package/database/repositories/migration-status-repository-s3.js +137 -0
  54. package/database/use-cases/check-database-health-use-case.js +29 -0
  55. package/database/use-cases/check-database-state-use-case.js +81 -0
  56. package/database/use-cases/check-encryption-health-use-case.js +83 -0
  57. package/database/use-cases/get-database-state-via-worker-use-case.js +61 -0
  58. package/database/use-cases/get-migration-status-use-case.js +93 -0
  59. package/database/use-cases/run-database-migration-use-case.js +139 -0
  60. package/database/use-cases/test-encryption-use-case.js +253 -0
  61. package/database/use-cases/trigger-database-migration-use-case.js +157 -0
  62. package/database/utils/mongodb-collection-utils.js +94 -0
  63. package/database/utils/mongodb-schema-init.js +108 -0
  64. package/database/utils/prisma-runner.js +477 -0
  65. package/database/utils/prisma-schema-parser.js +182 -0
  66. package/docs/PROCESS_MANAGEMENT_QUEUE_SPEC.md +517 -0
  67. package/encrypt/Cryptor.js +34 -168
  68. package/encrypt/index.js +1 -2
  69. package/errors/client-safe-error.js +26 -0
  70. package/errors/fetch-error.js +15 -7
  71. package/errors/index.js +2 -0
  72. package/generated/prisma-mongodb/client.d.ts +1 -0
  73. package/generated/prisma-mongodb/client.js +5 -0
  74. package/generated/prisma-mongodb/default.d.ts +1 -0
  75. package/generated/prisma-mongodb/default.js +5 -0
  76. package/generated/prisma-mongodb/edge.d.ts +1 -0
  77. package/generated/prisma-mongodb/edge.js +374 -0
  78. package/generated/prisma-mongodb/index-browser.js +356 -0
  79. package/generated/prisma-mongodb/index.d.ts +25667 -0
  80. package/generated/prisma-mongodb/index.js +399 -0
  81. package/generated/prisma-mongodb/libquery_engine-debian-openssl-3.0.x.so.node +0 -0
  82. package/generated/prisma-mongodb/libquery_engine-rhel-openssl-3.0.x.so.node +0 -0
  83. package/generated/prisma-mongodb/package.json +183 -0
  84. package/generated/prisma-mongodb/runtime/edge-esm.js +35 -0
  85. package/generated/prisma-mongodb/runtime/edge.js +35 -0
  86. package/generated/prisma-mongodb/runtime/index-browser.d.ts +370 -0
  87. package/generated/prisma-mongodb/runtime/index-browser.js +17 -0
  88. package/generated/prisma-mongodb/runtime/library.d.ts +3982 -0
  89. package/generated/prisma-mongodb/runtime/library.js +147 -0
  90. package/generated/prisma-mongodb/runtime/react-native.js +84 -0
  91. package/generated/prisma-mongodb/runtime/wasm-compiler-edge.js +85 -0
  92. package/generated/prisma-mongodb/runtime/wasm-engine-edge.js +38 -0
  93. package/generated/prisma-mongodb/schema.prisma +432 -0
  94. package/generated/prisma-mongodb/wasm-edge-light-loader.mjs +5 -0
  95. package/generated/prisma-mongodb/wasm-worker-loader.mjs +5 -0
  96. package/generated/prisma-mongodb/wasm.d.ts +1 -0
  97. package/generated/prisma-mongodb/wasm.js +381 -0
  98. package/generated/prisma-postgresql/client.d.ts +1 -0
  99. package/generated/prisma-postgresql/client.js +5 -0
  100. package/generated/prisma-postgresql/default.d.ts +1 -0
  101. package/generated/prisma-postgresql/default.js +5 -0
  102. package/generated/prisma-postgresql/edge.d.ts +1 -0
  103. package/generated/prisma-postgresql/edge.js +395 -0
  104. package/generated/prisma-postgresql/index-browser.js +377 -0
  105. package/generated/prisma-postgresql/index.d.ts +28455 -0
  106. package/generated/prisma-postgresql/index.js +420 -0
  107. package/generated/prisma-postgresql/libquery_engine-debian-openssl-3.0.x.so.node +0 -0
  108. package/generated/prisma-postgresql/libquery_engine-rhel-openssl-3.0.x.so.node +0 -0
  109. package/generated/prisma-postgresql/package.json +183 -0
  110. package/generated/prisma-postgresql/query_engine_bg.js +2 -0
  111. package/generated/prisma-postgresql/query_engine_bg.wasm +0 -0
  112. package/generated/prisma-postgresql/runtime/edge-esm.js +35 -0
  113. package/generated/prisma-postgresql/runtime/edge.js +35 -0
  114. package/generated/prisma-postgresql/runtime/index-browser.d.ts +370 -0
  115. package/generated/prisma-postgresql/runtime/index-browser.js +17 -0
  116. package/generated/prisma-postgresql/runtime/library.d.ts +3982 -0
  117. package/generated/prisma-postgresql/runtime/library.js +147 -0
  118. package/generated/prisma-postgresql/runtime/react-native.js +84 -0
  119. package/generated/prisma-postgresql/runtime/wasm-compiler-edge.js +85 -0
  120. package/generated/prisma-postgresql/runtime/wasm-engine-edge.js +38 -0
  121. package/generated/prisma-postgresql/schema.prisma +414 -0
  122. package/generated/prisma-postgresql/wasm-edge-light-loader.mjs +5 -0
  123. package/generated/prisma-postgresql/wasm-worker-loader.mjs +5 -0
  124. package/generated/prisma-postgresql/wasm.d.ts +1 -0
  125. package/generated/prisma-postgresql/wasm.js +402 -0
  126. package/handlers/WEBHOOKS.md +653 -0
  127. package/handlers/app-definition-loader.js +43 -0
  128. package/handlers/app-handler-helpers.js +57 -0
  129. package/handlers/backend-utils.js +316 -0
  130. package/handlers/database-migration-handler.js +227 -0
  131. package/handlers/integration-event-dispatcher.js +54 -0
  132. package/handlers/middleware/admin-auth.js +73 -0
  133. package/handlers/routers/HEALTHCHECK.md +342 -0
  134. package/handlers/routers/auth.js +15 -0
  135. package/handlers/routers/db-migration.handler.js +29 -0
  136. package/handlers/routers/db-migration.js +311 -0
  137. package/handlers/routers/health.js +518 -0
  138. package/handlers/routers/integration-defined-routers.js +117 -0
  139. package/handlers/routers/integration-webhook-routers.js +67 -0
  140. package/handlers/routers/reporting.js +9 -0
  141. package/handlers/routers/user.js +63 -0
  142. package/handlers/routers/websocket.js +57 -0
  143. package/handlers/use-cases/check-external-apis-health-use-case.js +81 -0
  144. package/handlers/use-cases/check-integrations-health-use-case.js +44 -0
  145. package/handlers/workers/db-migration.js +352 -0
  146. package/handlers/workers/dlq-processor.js +63 -0
  147. package/handlers/workers/integration-defined-workers.js +30 -0
  148. package/index.js +111 -50
  149. package/infrastructure/scheduler/eventbridge-scheduler-adapter.js +184 -0
  150. package/infrastructure/scheduler/index.js +33 -0
  151. package/infrastructure/scheduler/mock-scheduler-adapter.js +143 -0
  152. package/infrastructure/scheduler/scheduler-service-factory.js +73 -0
  153. package/infrastructure/scheduler/scheduler-service-interface.js +47 -0
  154. package/integrations/EXTENSIONS.md +240 -0
  155. package/integrations/WEBHOOK-QUICKSTART.md +151 -0
  156. package/integrations/extension.js +254 -0
  157. package/integrations/index.js +20 -10
  158. package/integrations/integration-base.js +619 -72
  159. package/integrations/integration-router.js +460 -218
  160. package/integrations/options.js +3 -4
  161. package/integrations/repositories/config-patch-shared.js +43 -0
  162. package/integrations/repositories/integration-mapping-repository-documentdb.js +280 -0
  163. package/integrations/repositories/integration-mapping-repository-factory.js +57 -0
  164. package/integrations/repositories/integration-mapping-repository-interface.js +106 -0
  165. package/integrations/repositories/integration-mapping-repository-mongo.js +161 -0
  166. package/integrations/repositories/integration-mapping-repository-postgres.js +227 -0
  167. package/integrations/repositories/integration-mapping-repository.js +156 -0
  168. package/integrations/repositories/integration-repository-documentdb.js +284 -0
  169. package/integrations/repositories/integration-repository-factory.js +51 -0
  170. package/integrations/repositories/integration-repository-interface.js +173 -0
  171. package/integrations/repositories/integration-repository-mongo.js +414 -0
  172. package/integrations/repositories/integration-repository-postgres.js +452 -0
  173. package/integrations/repositories/process-repository-documentdb.js +311 -0
  174. package/integrations/repositories/process-repository-factory.js +53 -0
  175. package/integrations/repositories/process-repository-interface.js +136 -0
  176. package/integrations/repositories/process-repository-mongo.js +262 -0
  177. package/integrations/repositories/process-repository-postgres.js +380 -0
  178. package/integrations/repositories/process-update-ops-shared.js +112 -0
  179. package/integrations/tests/doubles/config-capturing-integration.js +81 -0
  180. package/integrations/tests/doubles/dummy-integration-class.js +113 -0
  181. package/integrations/tests/doubles/test-integration-repository.js +146 -0
  182. package/integrations/use-cases/create-integration.js +215 -0
  183. package/integrations/use-cases/create-process.js +130 -0
  184. package/integrations/use-cases/delete-integration-for-user.js +119 -0
  185. package/integrations/use-cases/find-integration-by-entity-external-id.js +74 -0
  186. package/integrations/use-cases/find-integration-context-by-external-entity-id.js +76 -0
  187. package/integrations/use-cases/get-integration-for-user.js +78 -0
  188. package/integrations/use-cases/get-integration-instance-by-definition.js +67 -0
  189. package/integrations/use-cases/get-integration-instance.js +83 -0
  190. package/integrations/use-cases/get-integrations-for-user.js +88 -0
  191. package/integrations/use-cases/get-possible-integrations.js +27 -0
  192. package/integrations/use-cases/get-process.js +89 -0
  193. package/integrations/use-cases/index.js +19 -0
  194. package/integrations/use-cases/list-integrations-by-entity-external-id.js +46 -0
  195. package/integrations/use-cases/load-integration-context.js +71 -0
  196. package/integrations/use-cases/patch-integration-config.js +39 -0
  197. package/integrations/use-cases/process-errors.js +28 -0
  198. package/integrations/use-cases/update-integration-config.js +32 -0
  199. package/integrations/use-cases/update-integration-messages.js +44 -0
  200. package/integrations/use-cases/update-integration-status.js +32 -0
  201. package/integrations/use-cases/update-integration.js +92 -0
  202. package/integrations/use-cases/update-process-metrics.js +216 -0
  203. package/integrations/use-cases/update-process-state.js +163 -0
  204. package/integrations/utils/map-integration-dto.js +37 -0
  205. package/jest-global-setup-noop.js +3 -0
  206. package/jest-global-teardown-noop.js +3 -0
  207. package/logs/logger.js +0 -4
  208. package/{module-plugin → modules}/index.js +0 -10
  209. package/modules/module-factory.js +56 -0
  210. package/modules/module.js +274 -0
  211. package/modules/repositories/module-repository-documentdb.js +350 -0
  212. package/modules/repositories/module-repository-factory.js +40 -0
  213. package/modules/repositories/module-repository-interface.js +145 -0
  214. package/modules/repositories/module-repository-mongo.js +436 -0
  215. package/modules/repositories/module-repository-postgres.js +481 -0
  216. package/modules/repositories/module-repository.js +369 -0
  217. package/modules/requester/api-key.js +52 -0
  218. package/modules/requester/oauth-2.js +396 -0
  219. package/modules/requester/requester.js +285 -0
  220. package/{module-plugin → modules}/test/mock-api/api.js +8 -3
  221. package/{module-plugin → modules}/test/mock-api/definition.js +14 -10
  222. package/modules/tests/doubles/test-module-factory.js +16 -0
  223. package/modules/tests/doubles/test-module-repository.js +39 -0
  224. package/modules/use-cases/get-entities-for-user.js +32 -0
  225. package/modules/use-cases/get-entity-options-by-id.js +71 -0
  226. package/modules/use-cases/get-entity-options-by-type.js +34 -0
  227. package/modules/use-cases/get-module-instance-from-type.js +34 -0
  228. package/modules/use-cases/get-module.js +74 -0
  229. package/modules/use-cases/process-authorization-callback.js +243 -0
  230. package/modules/use-cases/refresh-entity-options.js +72 -0
  231. package/modules/use-cases/test-module-auth.js +72 -0
  232. package/modules/utils/map-module-dto.js +18 -0
  233. package/package.json +82 -50
  234. package/prisma-mongodb/schema.prisma +432 -0
  235. package/prisma-postgresql/migrations/20250930193005_init/migration.sql +315 -0
  236. package/prisma-postgresql/migrations/20251006135218_init/migration.sql +9 -0
  237. package/prisma-postgresql/migrations/20251010000000_remove_unused_entity_reference_map/migration.sql +3 -0
  238. package/prisma-postgresql/migrations/20251112195422_update_user_unique_constraints/migration.sql +25 -0
  239. package/prisma-postgresql/migrations/20260422120000_add_entity_data_column/migration.sql +10 -0
  240. package/prisma-postgresql/migrations/20260422120001_create_process_table/migration.sql +48 -0
  241. package/prisma-postgresql/migrations/20260625000000_add_user_organization_id_index/migration.sql +2 -0
  242. package/prisma-postgresql/migrations/20260703000000_add_integration_status_in_creation_in_deletion/migration.sql +11 -0
  243. package/prisma-postgresql/migrations/20260703000001_integration_status_default_in_creation/migration.sql +8 -0
  244. package/prisma-postgresql/migrations/20260706000000_add_admin_script_execution_and_schedule/migration.sql +61 -0
  245. package/prisma-postgresql/migrations/migration_lock.toml +3 -0
  246. package/prisma-postgresql/schema.prisma +414 -0
  247. package/queues/queuer-util.js +103 -21
  248. package/reporting/README.md +86 -0
  249. package/reporting/index.js +17 -0
  250. package/reporting/reporting-router.js +77 -0
  251. package/reporting/repositories/reporting-repository-documentdb.js +127 -0
  252. package/reporting/repositories/reporting-repository-factory.js +35 -0
  253. package/reporting/repositories/reporting-repository-interface.js +16 -0
  254. package/reporting/repositories/reporting-repository-mongo.js +54 -0
  255. package/reporting/repositories/reporting-repository-postgres.js +70 -0
  256. package/reporting/use-cases/index.js +6 -0
  257. package/reporting/use-cases/list-integrations-report.js +142 -0
  258. package/syncs/manager.js +468 -443
  259. package/syncs/repositories/sync-repository-documentdb.js +240 -0
  260. package/syncs/repositories/sync-repository-factory.js +43 -0
  261. package/syncs/repositories/sync-repository-interface.js +109 -0
  262. package/syncs/repositories/sync-repository-mongo.js +239 -0
  263. package/syncs/repositories/sync-repository-postgres.js +319 -0
  264. package/syncs/sync.js +0 -1
  265. package/token/repositories/token-repository-documentdb.js +137 -0
  266. package/token/repositories/token-repository-factory.js +40 -0
  267. package/token/repositories/token-repository-interface.js +131 -0
  268. package/token/repositories/token-repository-mongo.js +219 -0
  269. package/token/repositories/token-repository-postgres.js +264 -0
  270. package/token/repositories/token-repository.js +219 -0
  271. package/types/associations/index.d.ts +0 -17
  272. package/types/core/index.d.ts +12 -4
  273. package/types/database/index.d.ts +10 -2
  274. package/types/encrypt/index.d.ts +5 -3
  275. package/types/integrations/index.d.ts +3 -8
  276. package/types/module-plugin/index.d.ts +17 -69
  277. package/types/syncs/index.d.ts +0 -17
  278. package/user/repositories/user-repository-documentdb.js +458 -0
  279. package/user/repositories/user-repository-factory.js +52 -0
  280. package/user/repositories/user-repository-interface.js +214 -0
  281. package/user/repositories/user-repository-mongo.js +323 -0
  282. package/user/repositories/user-repository-postgres.js +377 -0
  283. package/user/tests/doubles/test-user-repository.js +72 -0
  284. package/user/use-cases/authenticate-user.js +127 -0
  285. package/user/use-cases/authenticate-with-shared-secret.js +48 -0
  286. package/user/use-cases/create-individual-user.js +61 -0
  287. package/user/use-cases/create-organization-user.js +47 -0
  288. package/user/use-cases/create-token-for-user-id.js +30 -0
  289. package/user/use-cases/get-user-from-adopter-jwt.js +149 -0
  290. package/user/use-cases/get-user-from-bearer-token.js +77 -0
  291. package/user/use-cases/get-user-from-x-frigg-headers.js +132 -0
  292. package/user/use-cases/login-user.js +122 -0
  293. package/user/user.js +125 -0
  294. package/utils/backend-path.js +38 -0
  295. package/utils/index.js +6 -0
  296. package/websocket/repositories/websocket-connection-repository-documentdb.js +119 -0
  297. package/websocket/repositories/websocket-connection-repository-factory.js +44 -0
  298. package/websocket/repositories/websocket-connection-repository-interface.js +106 -0
  299. package/websocket/repositories/websocket-connection-repository-mongo.js +156 -0
  300. package/websocket/repositories/websocket-connection-repository-postgres.js +196 -0
  301. package/websocket/repositories/websocket-connection-repository.js +161 -0
  302. package/assertions/is-equal.js +0 -17
  303. package/associations/model.js +0 -54
  304. package/database/models/IndividualUser.js +0 -76
  305. package/database/models/OrganizationUser.js +0 -29
  306. package/database/models/State.js +0 -9
  307. package/database/models/Token.js +0 -70
  308. package/database/models/UserModel.js +0 -7
  309. package/database/models/WebsocketConnection.js +0 -49
  310. package/database/mongo.js +0 -45
  311. package/database/mongoose.js +0 -5
  312. package/encrypt/Cryptor.test.js +0 -32
  313. package/encrypt/encrypt.js +0 -132
  314. package/encrypt/encrypt.test.js +0 -1069
  315. package/encrypt/test-encrypt.js +0 -107
  316. package/errors/base-error.test.js +0 -32
  317. package/errors/fetch-error.test.js +0 -79
  318. package/errors/halt-error.test.js +0 -11
  319. package/errors/validation-errors.test.js +0 -120
  320. package/integrations/create-frigg-backend.js +0 -31
  321. package/integrations/integration-factory.js +0 -251
  322. package/integrations/integration-mapping.js +0 -43
  323. package/integrations/integration-model.js +0 -46
  324. package/integrations/integration-user.js +0 -144
  325. package/integrations/test/integration-base.test.js +0 -144
  326. package/lambda/TimeoutCatcher.test.js +0 -68
  327. package/logs/logger.test.js +0 -76
  328. package/module-plugin/auther.js +0 -350
  329. package/module-plugin/credential.js +0 -22
  330. package/module-plugin/entity-manager.js +0 -70
  331. package/module-plugin/entity.js +0 -46
  332. package/module-plugin/manager.js +0 -169
  333. package/module-plugin/module-factory.js +0 -61
  334. package/module-plugin/requester/api-key.js +0 -36
  335. package/module-plugin/requester/oauth-2.js +0 -219
  336. package/module-plugin/requester/requester.js +0 -165
  337. package/module-plugin/requester/requester.test.js +0 -28
  338. package/module-plugin/test/auther.test.js +0 -97
  339. package/syncs/model.js +0 -62
  340. /package/{module-plugin → modules}/ModuleConstants.js +0 -0
  341. /package/{module-plugin → modules}/requester/basic.js +0 -0
  342. /package/{module-plugin → modules}/test/mock-api/mocks/hubspot.js +0 -0
@@ -1,5 +1,24 @@
1
- const { IntegrationMapping } = require('./integration-mapping');
1
+ const {
2
+ createIntegrationMappingRepository,
3
+ } = require('./repositories/integration-mapping-repository-factory');
2
4
  const { Options } = require('./options');
5
+ const {
6
+ UpdateIntegrationStatus,
7
+ } = require('./use-cases/update-integration-status');
8
+ const {
9
+ createIntegrationRepository,
10
+ } = require('./repositories/integration-repository-factory');
11
+ const {
12
+ UpdateIntegrationMessages,
13
+ } = require('./use-cases/update-integration-messages');
14
+ const {
15
+ PatchIntegrationConfig,
16
+ } = require('./use-cases/patch-integration-config');
17
+ const {
18
+ UpdateIntegrationConfig,
19
+ } = require('./use-cases/update-integration-config');
20
+ const { validateExtensionBinding } = require('./extension');
21
+
3
22
  const constantsToBeMigrated = {
4
23
  defaultEvents: {
5
24
  ON_CREATE: 'ON_CREATE',
@@ -10,6 +29,8 @@ const constantsToBeMigrated = {
10
29
  GET_USER_ACTIONS: 'GET_USER_ACTIONS',
11
30
  GET_USER_ACTION_OPTIONS: 'GET_USER_ACTION_OPTIONS',
12
31
  REFRESH_USER_ACTION_OPTIONS: 'REFRESH_USER_ACTION_OPTIONS',
32
+ WEBHOOK_RECEIVED: 'WEBHOOK_RECEIVED', // HTTP handler, no DB
33
+ ON_WEBHOOK: 'ON_WEBHOOK', // Queue worker, DB-connected
13
34
  // etc...
14
35
  },
15
36
  types: {
@@ -19,6 +40,22 @@ const constantsToBeMigrated = {
19
40
  };
20
41
 
21
42
  class IntegrationBase {
43
+ // todo: maybe we can pass this as Dependency Injection in the sub-class constructor
44
+ integrationRepository = createIntegrationRepository();
45
+ integrationMappingRepository = createIntegrationMappingRepository();
46
+ updateIntegrationStatus = new UpdateIntegrationStatus({
47
+ integrationRepository: this.integrationRepository,
48
+ });
49
+ updateIntegrationMessages = new UpdateIntegrationMessages({
50
+ integrationRepository: this.integrationRepository,
51
+ });
52
+ patchIntegrationConfig = new PatchIntegrationConfig({
53
+ integrationRepository: this.integrationRepository,
54
+ });
55
+ updateIntegrationConfig = new UpdateIntegrationConfig({
56
+ integrationRepository: this.integrationRepository,
57
+ });
58
+
22
59
  static getOptionDetails() {
23
60
  const options = new Options({
24
61
  module: Object.values(this.Definition.modules)[0], // This is a placeholder until we revamp the frontend
@@ -26,6 +63,7 @@ class IntegrationBase {
26
63
  });
27
64
  return options.get();
28
65
  }
66
+
29
67
  /**
30
68
  * CHILDREN SHOULD SPECIFY A DEFINITION FOR THE INTEGRATION
31
69
  */
@@ -35,6 +73,9 @@ class IntegrationBase {
35
73
  supportedVersions: [], // Eventually usable for deprecation and future test version purposes
36
74
 
37
75
  modules: {},
76
+ // Tier 3 Integration Extensions — see packages/core/integrations/EXTENSIONS.md
77
+ // Shape: { [bindingName]: { extension, handlers?: { [eventName]: methodName } } }
78
+ extensions: {},
38
79
  display: {
39
80
  name: 'Integration Name',
40
81
  logo: '',
@@ -50,29 +91,30 @@ class IntegrationBase {
50
91
  static getCurrentVersion() {
51
92
  return this.Definition.version;
52
93
  }
53
- loadModules() {
54
- // Load all the modules defined in Definition.modules
55
- const moduleNames = Object.keys(this.constructor.Definition.modules);
56
- for (const moduleName of moduleNames) {
57
- const { definition } =
58
- this.constructor.Definition.modules[moduleName];
59
- if (typeof definition.API === 'function') {
60
- this[moduleName] = { api: new definition.API() };
61
- } else {
62
- throw new Error(
63
- `Module ${moduleName} must be a function that extends IntegrationModule`
64
- );
65
- }
94
+
95
+ // REMOVED: registerEventHandlers() - Event handling is now done by IntegrationEventDispatcher
96
+
97
+ constructor(params = {}) {
98
+ this.modules = {};
99
+ this.events = this.events || {};
100
+ this.messages = { errors: [], warnings: [] };
101
+ this._isHydrated = false;
102
+
103
+ if (params && Object.keys(params).length > 0) {
104
+ this.setIntegrationRecord({
105
+ record: {
106
+ id: params.id,
107
+ userId: params.userId,
108
+ entities: params.entities,
109
+ config: params.config,
110
+ status: params.status,
111
+ version: params.version,
112
+ messages: params.messages,
113
+ },
114
+ modules: params.modules || [],
115
+ });
66
116
  }
67
- }
68
- registerEventHandlers() {
69
- this.on = {
70
- ...this.defaultEvents,
71
- ...this.events,
72
- };
73
- }
74
117
 
75
- constructor(params) {
76
118
  this.defaultEvents = {
77
119
  [constantsToBeMigrated.defaultEvents.ON_CREATE]: {
78
120
  type: constantsToBeMigrated.types.LIFE_CYCLE_EVENT,
@@ -106,83 +148,242 @@ class IntegrationBase {
106
148
  type: constantsToBeMigrated.types.LIFE_CYCLE_EVENT,
107
149
  handler: this.refreshActionOptions,
108
150
  },
151
+ [constantsToBeMigrated.defaultEvents.WEBHOOK_RECEIVED]: {
152
+ type: constantsToBeMigrated.types.LIFE_CYCLE_EVENT,
153
+ handler: this.onWebhookReceived,
154
+ },
155
+ [constantsToBeMigrated.defaultEvents.ON_WEBHOOK]: {
156
+ type: constantsToBeMigrated.types.LIFE_CYCLE_EVENT,
157
+ handler: this.onWebhook,
158
+ },
109
159
  };
110
- this.loadModules();
111
160
  }
112
161
 
113
- async send(event, object) {
114
- if (!this.on[event]) {
115
- throw new Error(
116
- `Event ${event} is not defined in the Integration event object`
117
- );
162
+ // todo: debate wether we want to keep this pattern to set the record or not.
163
+ /**
164
+ * Persist the database record and module instances onto this integration instance.
165
+ * Accepts either a plain object containing the persisted fields or an object with
166
+ * a `record` property plus a `modules` collection.
167
+ * @param {Object} payload
168
+ * @param {Object} [payload.record]
169
+ * @param {Array} [payload.modules]
170
+ */
171
+ setIntegrationRecord(payload = {}) {
172
+ if (!payload || Object.keys(payload).length === 0) {
173
+ throw new Error('setIntegrationRecord requires integration data');
118
174
  }
119
- return this.on[event].handler.call(this, object);
175
+
176
+ const integrationRecord = payload.record;
177
+ const integrationModules = payload.modules ?? [];
178
+
179
+ if (!integrationRecord) {
180
+ throw new Error('Integration record not provided');
181
+ }
182
+
183
+ const { id, userId, entities, config, status, version, messages } =
184
+ integrationRecord;
185
+
186
+ this.id = id;
187
+ this.userId = userId;
188
+ this.entities = entities;
189
+ this.config = config;
190
+ this.status = status;
191
+ this.version = version;
192
+ this.messages = messages || { errors: [], warnings: [] };
193
+
194
+ this.modules = this._appendModules(integrationModules);
195
+
196
+ this.record = {
197
+ id: this.id,
198
+ userId: this.userId,
199
+ entities: this.entities,
200
+ config: this.config,
201
+ status: this.status,
202
+ version: this.version,
203
+ messages: this.messages,
204
+ };
205
+
206
+ this._isHydrated = Boolean(this.id);
207
+ return this;
120
208
  }
121
209
 
122
- async validateConfig() {
123
- const configOptions = await this.getConfigOptions();
124
- const currentConfig = this.record.config;
125
- let needsConfig = false;
126
- for (const option of configOptions) {
127
- if (option.required) {
128
- // For now, just make sure the key exists. We should add more dynamic/better validation later.
129
- if (
130
- !Object.prototype.hasOwnProperty.call(
131
- currentConfig,
132
- option.key
133
- )
134
- ) {
135
- needsConfig = true;
136
- this.record.messages.warnings.push({
137
- title: 'Config Validation Error',
138
- message: `Missing required field of ${option.label}`,
139
- timestamp: Date.now(),
140
- });
210
+ get isHydrated() {
211
+ return this._isHydrated;
212
+ }
213
+
214
+ assertHydrated(message = 'Integration instance is not hydrated') {
215
+ if (!this.isHydrated) {
216
+ throw new Error(message);
217
+ }
218
+ }
219
+
220
+ /**
221
+ * Returns the modules as object with keys as module names.
222
+ * Uses the keys from Definition.modules to attach modules correctly.
223
+ *
224
+ * Example:
225
+ * Definition.modules = { attio: {...}, quo: { definition: { getName: () => 'quo-attio' } } }
226
+ * Module with getName()='quo-attio' gets attached as this.quo (not this['quo-attio'])
227
+ *
228
+ * @private
229
+ * @param {Array} integrationModules - Array of module instances
230
+ * @returns {Object} The modules object
231
+ */
232
+ _appendModules(integrationModules) {
233
+ const modules = {};
234
+
235
+ // Build reverse mapping: definition.getName() → referenceKey
236
+ // e.g., 'quo-attio' → 'quo', 'attio' → 'attio'
237
+ const moduleNameToKey = {};
238
+ if (this.constructor.Definition?.modules) {
239
+ for (const [key, moduleConfig] of Object.entries(this.constructor.Definition.modules)) {
240
+ const definition = moduleConfig.definition;
241
+ if (definition) {
242
+ // Use getName() if available, fallback to moduleName
243
+ const definitionName = typeof definition.getName === 'function'
244
+ ? definition.getName()
245
+ : definition.moduleName;
246
+ if (definitionName) {
247
+ moduleNameToKey[definitionName] = key;
248
+ }
141
249
  }
142
250
  }
143
251
  }
144
- if (needsConfig) {
145
- this.record.status = 'NEEDS_CONFIG';
146
- await this.record.save();
252
+
253
+ for (const module of integrationModules) {
254
+ const moduleName =
255
+ typeof module.getName === 'function'
256
+ ? module.getName()
257
+ : module.name;
258
+
259
+ // Use the reference key from Definition.modules if available,
260
+ // otherwise fall back to moduleName
261
+ const key = moduleNameToKey[moduleName] || moduleName;
262
+
263
+ if (key) {
264
+ modules[key] = module;
265
+ this[key] = module;
266
+ }
267
+
268
+ // Wire the Delegate pattern so Module can notify this integration
269
+ // of events it cannot handle itself (e.g. credential invalidation
270
+ // needing an Integration.status flip). Without this, Module.notify
271
+ // silently no-ops and Integration.status never updates on auth
272
+ // failure.
273
+ if (module && typeof module === 'object') {
274
+ module.delegate = this;
275
+ }
276
+ }
277
+
278
+ return modules;
279
+ }
280
+
281
+ /**
282
+ * Check the current config against the required fields declared by
283
+ * `getConfigOptions()`. Config options use the react-jsonschema-form shape,
284
+ * so the required top-level keys live in `jsonSchema.required`. Records a
285
+ * warning for each missing field. Does not change integration status —
286
+ * the caller decides the consequence (see `onCreate`/`onUpdate`), the same
287
+ * separation `testAuth`/`reconcileAuthStatus` use for the auth axis.
288
+ * @returns {Promise<boolean>} True when a required field is missing.
289
+ */
290
+ async validateConfig() {
291
+ const { jsonSchema } = await this.getConfigOptions();
292
+ const currentConfig = this.getConfig() || {};
293
+ const requiredKeys = Array.isArray(jsonSchema?.required)
294
+ ? jsonSchema.required
295
+ : [];
296
+ let needsConfig = false;
297
+ for (const key of requiredKeys) {
298
+ if (!Object.prototype.hasOwnProperty.call(currentConfig, key)) {
299
+ needsConfig = true;
300
+ const label = jsonSchema?.properties?.[key]?.title || key;
301
+ await this.updateIntegrationMessages.execute(
302
+ this.id,
303
+ 'warnings',
304
+ 'Config Validation Error',
305
+ `Missing required field of ${label}`,
306
+ Date.now()
307
+ );
308
+ }
147
309
  }
310
+ return needsConfig;
148
311
  }
149
312
 
313
+ /**
314
+ * Verify every module's credentials. Records a diagnostic error message
315
+ * per failing module and returns whether all passed. Does not directly
316
+ * change integration status — the caller decides the consequence (see
317
+ * reconcileAuthStatus), so a passive check and an active reconnect can
318
+ * react differently. (A module can still fire a credential-invalidated
319
+ * delegate that flips status via receiveNotification, independent of this
320
+ * return value.)
321
+ * @returns {Promise<boolean>} True when every module authenticated.
322
+ */
150
323
  async testAuth() {
151
324
  let didAuthPass = true;
152
325
 
153
- for (const module of Object.keys(IntegrationBase.Definition.modules)) {
326
+ for (const module of Object.keys(this.constructor.Definition.modules)) {
154
327
  try {
155
- await this[module].testAuth();
328
+ const authPassed = await this[module].testAuth();
329
+ if (!authPassed) {
330
+ throw new Error(`testAuth returned false for module ${module}`);
331
+ }
156
332
  } catch {
157
333
  didAuthPass = false;
158
- this.record.messages.errors.push({
159
- title: 'Authentication Error',
160
- message: `There was an error with your ${this[
334
+ await this.updateIntegrationMessages.execute(
335
+ this.id,
336
+ 'errors',
337
+ 'Authentication Error',
338
+ `There was an error with your ${this[
161
339
  module
162
- ].constructor.getName()} Entity.
340
+ ].getName()} Entity.
163
341
  Please reconnect/re-authenticate, or reach out to Support for assistance.`,
164
- timestamp: Date.now(),
165
- });
342
+ Date.now()
343
+ );
166
344
  }
167
345
  }
168
346
 
169
- if (!didAuthPass) {
170
- this.record.status = 'ERROR';
171
- this.record.markModified('messages.error');
172
- await this.record.save();
347
+ return didAuthPass;
348
+ }
349
+
350
+ /**
351
+ * Reconcile the auth-health axis (ERROR ↔ ENABLED) from a testAuth result.
352
+ * On success it never clears DISABLED — a user pause is not an auth-health
353
+ * state, so it is only lifted by a deliberate reconnect. On failure the
354
+ * integration is marked ERROR regardless of its prior status.
355
+ * @param {boolean} authPassed - The result of testAuth().
356
+ */
357
+ async reconcileAuthStatus(authPassed) {
358
+ if (!authPassed) {
359
+ console.log(
360
+ `[Frigg] Integration ${this.id} failed to authenticate`
361
+ );
362
+ await this.persistStatus('ERROR');
363
+ }
364
+ if (authPassed && this.status === 'ERROR') {
365
+ console.log(
366
+ `[Frigg] auth confirmed for integration ${this.id} — clearing ERROR → ENABLED`
367
+ );
368
+ await this.persistStatus('ENABLED');
173
369
  }
174
370
  }
175
371
 
176
372
  async getMapping(sourceId) {
177
- return IntegrationMapping.findBy(this.record.id, sourceId);
373
+ // todo: not sure we should call the repository directly from here
374
+ return this.integrationMappingRepository.findMappingBy(
375
+ this.id,
376
+ sourceId
377
+ );
178
378
  }
179
379
 
180
380
  async upsertMapping(sourceId, mapping) {
181
381
  if (!sourceId) {
182
382
  throw new Error(`sourceId must be set`);
183
383
  }
184
- return await IntegrationMapping.upsert(
185
- this.record.id,
384
+ // todo: not sure we should call the repository directly from here
385
+ return await this.integrationMappingRepository.upsertMapping(
386
+ this.id,
186
387
  sourceId,
187
388
  mapping
188
389
  );
@@ -191,13 +392,40 @@ class IntegrationBase {
191
392
  /**
192
393
  * CHILDREN CAN OVERRIDE THESE CONFIGURATION METHODS
193
394
  */
194
- async onCreate(params) {
195
- this.record.status = 'ENABLED';
196
- await this.record.save();
197
- return this.record;
395
+ /**
396
+ * Default post-create lifecycle hook. The integration is born IN_CREATION;
397
+ * moved to NEEDS_CONFIG when validateConfig finds a required field
398
+ * missing, otherwise enabled. If this hook throws, the integration is
399
+ * left IN_CREATION — a visibly incomplete create rather than a healthy
400
+ * looking one. Children can override to run their own setup (and then own
401
+ * their status transition, calling `super.onCreate()` to keep this default).
402
+ */
403
+ async onCreate() {
404
+ const needsConfig = await this.validateConfig();
405
+ await this.persistStatus(needsConfig ? 'NEEDS_CONFIG' : 'ENABLED');
198
406
  }
199
407
 
200
- async onUpdate(params) {}
408
+ /**
409
+ * Default post-update lifecycle hook: merges any submitted config in as a
410
+ * patch, then re-validates. A NEEDS_CONFIG integration moves to ENABLED
411
+ * once nothing required is missing — other statuses (DISABLED, ERROR,
412
+ * IN_CREATION, IN_DELETION) are left alone; a config edit shouldn't
413
+ * silently un-pause or auto-heal those. Children can override to run
414
+ * their own update logic.
415
+ * @param {Object} [params]
416
+ * @param {Object} [params.config] - Keys to merge into the existing config.
417
+ */
418
+ async onUpdate(params) {
419
+ if (params?.config) {
420
+ await this.patchConfig(params.config);
421
+ }
422
+ const needsConfig = await this.validateConfig();
423
+ if (needsConfig) {
424
+ await this.persistStatus('NEEDS_CONFIG');
425
+ } else if (this.status === 'NEEDS_CONFIG') {
426
+ await this.persistStatus('ENABLED');
427
+ }
428
+ }
201
429
 
202
430
  async onDelete(params) {}
203
431
 
@@ -224,7 +452,6 @@ class IntegrationBase {
224
452
  return {};
225
453
  }
226
454
  async loadUserActions({ actionType } = {}) {
227
- console.log('loadUserActions called with actionType:', actionType);
228
455
  const userActions = {};
229
456
  for (const [key, event] of Object.entries(this.events)) {
230
457
  if (event.type === constantsToBeMigrated.types.USER_ACTION) {
@@ -259,6 +486,326 @@ class IntegrationBase {
259
486
  };
260
487
  return options;
261
488
  }
489
+
490
+ /**
491
+ * WEBHOOK EVENT HANDLERS
492
+ */
493
+ async onWebhookReceived({ req, res }) {
494
+ // Default: queue webhook for processing
495
+ const body = req.body;
496
+ const integrationId = req.params.integrationId || null;
497
+
498
+ await this.queueWebhook({
499
+ integrationId,
500
+ body,
501
+ headers: req.headers,
502
+ query: req.query,
503
+ });
504
+
505
+ res.status(200).json({ received: true });
506
+ }
507
+
508
+ async onWebhook({ data }) {
509
+ // Default: no-op, integrations override this
510
+ }
511
+
512
+ /**
513
+ * Queue a webhook for asynchronous worker dispatch.
514
+ *
515
+ * The dispatch event defaults to `ON_WEBHOOK` for backward compatibility
516
+ * with the `Definition.webhooks: true` path. Extensions (and any caller
517
+ * that needs the worker to invoke a specific bound handler) can override
518
+ * by passing `event` in the payload — it's stripped from the payload and
519
+ * used as the SQS message's dispatch event.
520
+ *
521
+ * @param {Object} data - Webhook payload. May include `event` to override
522
+ * the default `ON_WEBHOOK` dispatch event. All other fields are passed
523
+ * through to the worker as the `data` field of the SQS message.
524
+ */
525
+ async queueWebhook(data) {
526
+ const { QueuerUtil } = require('../queues');
527
+
528
+ const queueName = `${this.constructor.Definition.name
529
+ .toUpperCase()
530
+ .replace(/-/g, '_')}_QUEUE_URL`;
531
+ const queueUrl = process.env[queueName];
532
+
533
+ if (!queueUrl) {
534
+ throw new Error(`Queue URL not found for ${queueName}`);
535
+ }
536
+
537
+ const { event: dispatchEvent, ...payload } = data || {};
538
+
539
+ return QueuerUtil.send(
540
+ {
541
+ event: dispatchEvent || 'ON_WEBHOOK',
542
+ data: payload,
543
+ },
544
+ queueUrl
545
+ );
546
+ }
547
+
548
+ // === Domain Methods (moved from Integration.js) ===
549
+
550
+ getConfig() {
551
+ return this.config;
552
+ }
553
+
554
+ getModule(key) {
555
+ return this.modules[key];
556
+ }
557
+
558
+ setModule(key, module) {
559
+ this.modules[key] = module;
560
+ this[key] = module;
561
+ }
562
+
563
+ addError(error) {
564
+ if (!this.messages.errors) {
565
+ this.messages.errors = [];
566
+ }
567
+ this.messages.errors.push(error);
568
+ this.status = 'ERROR';
569
+ }
570
+
571
+ addWarning(warning) {
572
+ if (!this.messages.warnings) {
573
+ this.messages.warnings = [];
574
+ }
575
+ this.messages.warnings.push(warning);
576
+ }
577
+
578
+ /**
579
+ * Persist a status change and keep the in-memory field in sync. The
580
+ * single place that couples both writes, so no caller can update the
581
+ * database while leaving `this.status` stale.
582
+ * @param {string} status - The new integration status.
583
+ */
584
+ async persistStatus(status) {
585
+ await this.updateIntegrationStatus.execute(this.id, status);
586
+ this.status = status;
587
+ console.log(`[Frigg] Integration ${this.id} status changed to ${status}`);
588
+ }
589
+
590
+ /**
591
+ * Merge a partial update into config and keep the in-memory field in
592
+ * sync with what was actually persisted — not a local `{...this.config,
593
+ * ...patch}` guess, which would silently drop keys a concurrent writer
594
+ * already landed. Throws if the merge fails.
595
+ * @param {Object} patch - Keys to merge into the existing config.
596
+ */
597
+ async patchConfig(patch) {
598
+ const updated = await this.patchIntegrationConfig.execute(
599
+ this.id,
600
+ patch
601
+ );
602
+ this.config = updated.config;
603
+ return this.config;
604
+ }
605
+
606
+ /**
607
+ * Replace config entirely and keep the in-memory field in sync. Keys
608
+ * omitted from the new config are deleted. Throws if the write fails.
609
+ * @param {Object} config - The new configuration object.
610
+ */
611
+ async updateConfig(config) {
612
+ const updated = await this.updateIntegrationConfig.execute(
613
+ this.id,
614
+ config
615
+ );
616
+ this.config = updated.config;
617
+ return this.config;
618
+ }
619
+
620
+ isActive() {
621
+ return this.status === 'ENABLED' || this.status === 'ACTIVE';
622
+ }
623
+
624
+ needsConfiguration() {
625
+ return this.status === 'NEEDS_CONFIG';
626
+ }
627
+
628
+ hasErrors() {
629
+ return this.status === 'ERROR';
630
+ }
631
+
632
+ belongsToUser(userId) {
633
+ return this.userId.toString() === userId.toString();
634
+ }
635
+
636
+ registerEventHandlers() {
637
+ this.on = {
638
+ ...this.defaultEvents,
639
+ ...this.events,
640
+ };
641
+ }
642
+
643
+ /**
644
+ * Merge Tier 3 Integration Extension events into `this.events`.
645
+ *
646
+ * For each binding declared on `static Definition.extensions`, this method:
647
+ * 1. Validates the extension bundle shape and binding handlers
648
+ * 2. For each event the extension declares, resolves the handler in priority order:
649
+ * a. Subclass-defined `this.events[eventName]` (set in the constructor) — wins; if a
650
+ * binding tried to override that event with `handlers`, we log a warning so the
651
+ * author knows their override is shadowed.
652
+ * b. A method-name string in `binding.handlers[eventName]` → method on this instance
653
+ * c. The extension's own default `handler` function
654
+ * d. Otherwise throw — neither side provided a handler
655
+ * 3. Binds the resolved function to this instance and writes it to `this.events[eventName]`
656
+ *
657
+ * Two bindings declaring the same event throw a deterministic conflict error — silent
658
+ * "first/last writer wins" makes routing bugs nearly impossible to diagnose.
659
+ *
660
+ * @private
661
+ */
662
+ _mergeExtensions() {
663
+ const extensions = this.constructor.Definition?.extensions || {};
664
+ const integrationName = this.constructor.Definition?.name;
665
+ // Tracks which event names have been claimed by an extension binding during
666
+ // this merge — distinct from subclass-defined events on `this.events`.
667
+ const mergedByExtension = new Map();
668
+
669
+ for (const [bindingName, binding] of Object.entries(extensions)) {
670
+ if (!binding || typeof binding !== 'object') {
671
+ throw new Error(
672
+ `Integration "${integrationName}" extension binding "${bindingName}" must be an object`
673
+ );
674
+ }
675
+ const { extension, handlers = {} } = binding;
676
+ validateExtensionBinding(
677
+ extension,
678
+ bindingName,
679
+ integrationName,
680
+ binding
681
+ );
682
+
683
+ const extEvents = extension.events || {};
684
+ for (const [eventName, eventDef] of Object.entries(extEvents)) {
685
+ // Conflict detection: another extension binding already claimed this event name.
686
+ if (mergedByExtension.has(eventName)) {
687
+ const prev = mergedByExtension.get(eventName);
688
+ throw new Error(
689
+ `Integration "${integrationName}" extension event conflict: ` +
690
+ `event "${eventName}" is declared by both binding "${prev}" and binding "${bindingName}" — ` +
691
+ `use distinct event names per binding or omit duplicates`
692
+ );
693
+ }
694
+
695
+ // Subclass shadowing: if the subclass set this.events[eventName] before initialize(),
696
+ // it wins. Warn if the binding tried to wire an override that's now ignored.
697
+ if (this.events[eventName]) {
698
+ if (typeof handlers[eventName] === 'string') {
699
+ console.warn(
700
+ `[Frigg] Integration "${integrationName}" binding "${bindingName}": ` +
701
+ `handler "${handlers[eventName]}" for event "${eventName}" is ignored because ` +
702
+ `this.events["${eventName}"] was already set (subclass constructor or earlier merge)`
703
+ );
704
+ }
705
+ continue;
706
+ }
707
+
708
+ let fn;
709
+ const override = handlers[eventName];
710
+ if (typeof override === 'string') {
711
+ if (typeof this[override] !== 'function') {
712
+ throw new Error(
713
+ `Integration "${integrationName}" extension binding "${bindingName}": handler method "${override}" not found on instance`
714
+ );
715
+ }
716
+ fn = this[override];
717
+ } else if (typeof eventDef.handler === 'function') {
718
+ fn = eventDef.handler;
719
+ } else {
720
+ throw new Error(
721
+ `Extension "${extension.name}" event "${eventName}" has no default handler and binding "${bindingName}" did not provide one`
722
+ );
723
+ }
724
+
725
+ this.events[eventName] = {
726
+ type: eventDef.type,
727
+ handler: fn.bind(this),
728
+ };
729
+ mergedByExtension.set(eventName, bindingName);
730
+ }
731
+ }
732
+ }
733
+
734
+ async initialize() {
735
+ try {
736
+ const additionalUserActions = await this.loadDynamicUserActions();
737
+ this.events = { ...this.events, ...additionalUserActions };
738
+ } catch (e) {
739
+ this.addError(e);
740
+ }
741
+
742
+ this._mergeExtensions();
743
+ this.registerEventHandlers();
744
+ }
745
+
746
+ async send(event, object) {
747
+ if (!this.on[event]) {
748
+ throw new Error(
749
+ `Event ${event} is not defined in the Integration event object`
750
+ );
751
+ }
752
+ return this.on[event].handler.call(this, object);
753
+ }
754
+
755
+ getOptionDetails() {
756
+ const options = new Options({
757
+ module: Object.values(this.constructor.Definition.modules)[0],
758
+ ...this.constructor.Definition,
759
+ });
760
+ return options.get();
761
+ }
762
+
763
+ // Legacy method for backward compatibility
764
+ async loadModules() {
765
+ // This method was used in the old architecture for loading modules
766
+ // In the new architecture, modules are injected via constructor
767
+ // For backward compatibility, this is a no-op
768
+ return;
769
+ }
770
+
771
+ /**
772
+ * Receives notifications from modules (the Delegate pattern) when
773
+ * something integration-level needs attention. Today this catches the
774
+ * `CREDENTIAL_INVALIDATED` event Module fires from `markCredentialsInvalid`
775
+ * and flips this integration's status to ERROR so the queue worker
776
+ * stops processing further webhooks until the user re-authorizes.
777
+ *
778
+ * Modules are wired to this delegate in `_appendModules()`, which runs
779
+ * during `setIntegrationRecord()` — this covers every construction path
780
+ * (HTTP read, queue worker, create/update/delete flows, etc.).
781
+ *
782
+ * The delegate string below must match `Module.DLGT_CREDENTIAL_INVALIDATED`
783
+ * in `packages/core/modules/module.js`.
784
+ *
785
+ * @param {Object} notifier - The module that fired the event
786
+ * @param {string} delegateString - Event type string
787
+ * @param {Object} [object] - Optional event payload
788
+ * @returns {Promise<void>}
789
+ */
790
+ async receiveNotification(notifier, delegateString, object = null) {
791
+ if (!this.id) return;
792
+
793
+ if (delegateString === 'CREDENTIAL_INVALIDATED') {
794
+ console.log(
795
+ `[Frigg] Module ${notifier?.name || '?'} reported invalid credentials for integration ${this.id} — marking ERROR`
796
+ );
797
+ await this.persistStatus('ERROR');
798
+ return;
799
+ }
800
+
801
+ if (delegateString === 'CREDENTIAL_VALIDATED') {
802
+ if (this.status !== 'ERROR') return;
803
+ console.log(
804
+ `[Frigg] Module ${notifier?.name || '?'} reported valid credentials for integration ${this.id} — clearing ERROR → ENABLED`
805
+ );
806
+ await this.persistStatus('ENABLED');
807
+ }
808
+ }
262
809
  }
263
810
 
264
811
  module.exports = { IntegrationBase };