@friggframework/core 2.0.0-next.12 → 2.0.0-next.121

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