@friggframework/core 2.0.0-next.11 → 2.0.0-next.110

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