@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
@@ -0,0 +1,555 @@
1
+ const { Requester } = require('./requester');
2
+ const { get } = require('../../assertions');
3
+ const { ModuleConstants } = require('../ModuleConstants');
4
+
5
+ /**
6
+ * OAuth 2.0 Requester - Base class for API modules using OAuth 2.0 authentication.
7
+ *
8
+ * Supports multiple OAuth 2.0 grant types:
9
+ * - `authorization_code` (default): Standard OAuth flow with user consent
10
+ * - `client_credentials`: Server-to-server authentication without user
11
+ * - `password`: Resource Owner Password Credentials grant
12
+ *
13
+ * @extends Requester
14
+ *
15
+ * @example
16
+ * // Authorization Code flow (default)
17
+ * const api = new MyApi({ grant_type: 'authorization_code' });
18
+ * const authUrl = api.getAuthorizationUri();
19
+ * // After user authorizes...
20
+ * await api.getTokenFromCode(code);
21
+ *
22
+ * @example
23
+ * // Client Credentials flow
24
+ * const api = new MyApi({
25
+ * grant_type: 'client_credentials',
26
+ * client_id: process.env.CLIENT_ID,
27
+ * client_secret: process.env.CLIENT_SECRET,
28
+ * audience: 'https://api.example.com',
29
+ * });
30
+ * await api.getTokenFromClientCredentials();
31
+ */
32
+ class OAuth2Requester extends Requester {
33
+ static requesterType = ModuleConstants.authType.oauth2;
34
+
35
+ /**
36
+ * Creates an OAuth2Requester instance.
37
+ *
38
+ * @param {Object} params - Configuration parameters
39
+ * @param {string} [params.grant_type='authorization_code'] - OAuth grant type:
40
+ * 'authorization_code', 'client_credentials', or 'password'
41
+ * @param {string} [params.client_id] - OAuth client ID
42
+ * @param {string} [params.client_secret] - OAuth client secret
43
+ * @param {string} [params.redirect_uri] - OAuth redirect URI for authorization code flow
44
+ * @param {string} [params.scope] - OAuth scopes (space-separated)
45
+ * @param {string} [params.authorizationUri] - Authorization endpoint URL
46
+ * @param {string} [params.tokenUri] - Token endpoint URL for exchanging codes/credentials
47
+ * @param {string} [params.baseURL] - Base URL for API requests
48
+ * @param {string} [params.access_token] - Existing access token
49
+ * @param {string} [params.refresh_token] - Existing refresh token
50
+ * @param {Date} [params.accessTokenExpire] - Access token expiration date
51
+ * @param {Date} [params.refreshTokenExpire] - Refresh token expiration date
52
+ * @param {string} [params.audience] - Token audience (for client_credentials)
53
+ * @param {string} [params.username] - Username (for password grant)
54
+ * @param {string} [params.password] - Password (for password grant)
55
+ * @param {string} [params.state] - OAuth state parameter for CSRF protection
56
+ */
57
+ constructor(params) {
58
+ super(params);
59
+ /** @type {string} Delegate type for token update notifications */
60
+ this.DLGT_TOKEN_UPDATE = 'TOKEN_UPDATE';
61
+ /** @type {string} Delegate type for token deauthorization notifications */
62
+ this.DLGT_TOKEN_DEAUTHORIZED = 'TOKEN_DEAUTHORIZED';
63
+ /**
64
+ * @type {string} Delegate type that asks the Module for the stored
65
+ * credential. The requester can then adopt a concurrent invocation's
66
+ * refresh and does not race it. See _adoptNewerCredential.
67
+ */
68
+ this.DLGT_CREDENTIAL_RELOAD = 'CREDENTIAL_RELOAD';
69
+
70
+ this.delegateTypes.push(this.DLGT_TOKEN_UPDATE);
71
+ this.delegateTypes.push(this.DLGT_TOKEN_DEAUTHORIZED);
72
+ this.delegateTypes.push(this.DLGT_CREDENTIAL_RELOAD);
73
+
74
+ /**
75
+ * Re-read delays after an invalid_grant, in ms. The winner's write
76
+ * can arrive after the loser's rejection (716 ms measured in
77
+ * production). Wait between re-reads before you decide that the
78
+ * credential is dead. Tests can inject other values.
79
+ */
80
+ this.credentialReloadBackoffMs = params?.credentialReloadBackoffMs ?? [
81
+ 500, 1000, 1500,
82
+ ];
83
+
84
+ /** @type {string} OAuth grant type */
85
+ this.grant_type = get(params, 'grant_type', 'authorization_code');
86
+ /** @type {string|null} OAuth client ID */
87
+ this.client_id = get(params, 'client_id', null);
88
+ /** @type {string|null} OAuth client secret */
89
+ this.client_secret = get(params, 'client_secret', null);
90
+ /** @type {string|null} OAuth redirect URI */
91
+ this.redirect_uri = get(params, 'redirect_uri', null);
92
+ /** @type {string|null} OAuth scopes */
93
+ this.scope = get(params, 'scope', null);
94
+ /** @type {string|null} Authorization endpoint URL */
95
+ this.authorizationUri = get(params, 'authorizationUri', null);
96
+ /** @type {string|null} Token endpoint URL */
97
+ this.tokenUri = get(params, 'tokenUri', null);
98
+ /** @type {string|null} Base URL for API requests */
99
+ this.baseURL = get(params, 'baseURL', null);
100
+ /** @type {string|null} Current access token */
101
+ this.access_token = get(params, 'access_token', null);
102
+ /** @type {string|null} Current refresh token */
103
+ this.refresh_token = get(params, 'refresh_token', null);
104
+ /** @type {Date|null} Access token expiration */
105
+ this.accessTokenExpire = get(params, 'accessTokenExpire', null);
106
+ /** @type {Date|null} Refresh token expiration */
107
+ this.refreshTokenExpire = get(params, 'refreshTokenExpire', null);
108
+ /** @type {string|null} Token audience */
109
+ this.audience = get(params, 'audience', null);
110
+ /** @type {string|null} Username for password grant */
111
+ this.username = get(params, 'username', null);
112
+ /** @type {string|null} Password for password grant */
113
+ this.password = get(params, 'password', null);
114
+ /** @type {string|null} OAuth state for CSRF protection */
115
+ this.state = get(params, 'state', null);
116
+
117
+ /** @type {boolean} Whether this requester supports token refresh */
118
+ this.isRefreshable = true;
119
+ }
120
+
121
+ /**
122
+ * Sets OAuth tokens and calculates expiration times.
123
+ * Notifies delegates of token update via DLGT_TOKEN_UPDATE.
124
+ *
125
+ * @param {Object} params - Token response from OAuth server
126
+ * @param {string} params.access_token - The access token
127
+ * @param {string} [params.refresh_token] - The refresh token (if provided)
128
+ * @param {number} [params.expires_in] - Access token lifetime in seconds
129
+ * @param {number} [params.x_refresh_token_expires_in] - Refresh token lifetime in seconds
130
+ * @returns {Promise<void>}
131
+ */
132
+ async setTokens(params) {
133
+ this.access_token = get(params, 'access_token');
134
+ const newRefreshToken = get(params, 'refresh_token', null);
135
+ if (newRefreshToken !== null) {
136
+ this.refresh_token = newRefreshToken;
137
+ } else {
138
+ if (this.refresh_token) {
139
+ console.log(
140
+ '[Frigg] No refresh_token in response, preserving existing'
141
+ );
142
+ } else {
143
+ console.log(
144
+ '[Frigg] Current refresh_token is null and no new refresh_token in response'
145
+ );
146
+ }
147
+ }
148
+ const accessExpiresIn = get(params, 'expires_in', null);
149
+ const refreshExpiresIn = get(
150
+ params,
151
+ 'x_refresh_token_expires_in',
152
+ null
153
+ );
154
+
155
+ this.accessTokenExpire = new Date(Date.now() + accessExpiresIn * 1000);
156
+ if (refreshExpiresIn !== null) {
157
+ this.refreshTokenExpire = new Date(
158
+ Date.now() + refreshExpiresIn * 1000
159
+ );
160
+ }
161
+
162
+ await this.notify(this.DLGT_TOKEN_UPDATE);
163
+ }
164
+
165
+ /**
166
+ * Gets the OAuth authorization URL for initiating the authorization code flow.
167
+ *
168
+ * @returns {string|null} The authorization URL
169
+ */
170
+ getAuthorizationUri() {
171
+ return this.authorizationUri;
172
+ }
173
+
174
+ /**
175
+ * Returns authorization requirements for this OAuth flow.
176
+ *
177
+ * @returns {{url: string|null, type: string}} Authorization requirements
178
+ */
179
+ getAuthorizationRequirements() {
180
+ return {
181
+ url: this.getAuthorizationUri(),
182
+ type: 'oauth2',
183
+ };
184
+ }
185
+
186
+ /**
187
+ * Exchanges an authorization code for access and refresh tokens.
188
+ * Requires client_id, client_secret, redirect_uri, and tokenUri to be set.
189
+ *
190
+ * @param {string} code - The authorization code from the OAuth callback
191
+ * @returns {Promise<Object>} Token response containing access_token, refresh_token, etc.
192
+ */
193
+ async getTokenFromCode(code) {
194
+ const params = new URLSearchParams();
195
+ params.append('grant_type', 'authorization_code');
196
+ params.append('client_id', this.client_id);
197
+ params.append('client_secret', this.client_secret);
198
+ params.append('redirect_uri', this.redirect_uri);
199
+ params.append('scope', this.scope);
200
+ params.append('code', code);
201
+ const options = {
202
+ body: params,
203
+ headers: {
204
+ 'Content-Type': 'application/x-www-form-urlencoded',
205
+ },
206
+ url: this.tokenUri,
207
+ };
208
+ const response = await this._post(options, false);
209
+ await this.setTokens(response);
210
+ return response;
211
+ }
212
+
213
+ /**
214
+ * Exchanges an authorization code for tokens using Basic Auth header.
215
+ * Alternative to getTokenFromCode() for OAuth servers requiring Basic Auth.
216
+ * Override getTokenFromCode() in child class to use this instead.
217
+ *
218
+ * @param {string} code - The authorization code from the OAuth callback
219
+ * @returns {Promise<Object>} Token response containing access_token, refresh_token, etc.
220
+ */
221
+ async getTokenFromCodeBasicAuthHeader(code) {
222
+ const params = new URLSearchParams();
223
+ params.append('grant_type', 'authorization_code');
224
+ params.append('client_id', this.client_id);
225
+ params.append('redirect_uri', this.redirect_uri);
226
+ params.append('code', code);
227
+
228
+ const options = {
229
+ body: params,
230
+ headers: {
231
+ 'Content-Type': 'application/x-www-form-urlencoded',
232
+ Authorization: `Basic ${Buffer.from(
233
+ `${this.client_id}:${this.client_secret}`
234
+ ).toString('base64')}`,
235
+ },
236
+ url: this.tokenUri,
237
+ };
238
+
239
+ const response = await this._post(options, false);
240
+ await this.setTokens(response);
241
+ return response;
242
+ }
243
+
244
+ /**
245
+ * Refreshes the access token using the refresh token.
246
+ * Used for authorization_code and password grant types.
247
+ *
248
+ * @param {Object} refreshTokenObject - Object containing refresh_token
249
+ * @param {string} refreshTokenObject.refresh_token - The refresh token
250
+ * @returns {Promise<Object>} New token response
251
+ */
252
+ async refreshAccessToken(refreshTokenObject) {
253
+ this.access_token = undefined;
254
+ const params = new URLSearchParams();
255
+ params.append('grant_type', 'refresh_token');
256
+ params.append('client_id', this.client_id);
257
+ params.append('client_secret', this.client_secret);
258
+ params.append('refresh_token', refreshTokenObject.refresh_token);
259
+ params.append('redirect_uri', this.redirect_uri);
260
+
261
+ const options = {
262
+ body: params,
263
+ url: this.tokenUri,
264
+ headers: {
265
+ 'Content-Type': 'application/x-www-form-urlencoded',
266
+ },
267
+ };
268
+ console.log('[Frigg] Refreshing access token with options');
269
+ const response = await this._post(options, false);
270
+ await this.setTokens(response);
271
+ return response;
272
+ }
273
+
274
+ /**
275
+ * Adds OAuth Bearer token to request headers.
276
+ * Clears any existing Authorization header first to prevent stale tokens
277
+ * from being reused after failed refresh attempts.
278
+ *
279
+ * @param {Object} headers - Headers object to modify
280
+ * @returns {Promise<Object>} Headers with Authorization added
281
+ */
282
+ async addAuthHeaders(headers) {
283
+ delete headers.Authorization;
284
+ if (this.access_token) {
285
+ headers.Authorization = `Bearer ${this.access_token}`;
286
+ }
287
+
288
+ return headers;
289
+ }
290
+
291
+ /**
292
+ * Checks if the requester has valid authentication.
293
+ *
294
+ * @returns {boolean} True if authenticated with valid tokens
295
+ */
296
+ isAuthenticated() {
297
+ return !!(
298
+ this.access_token !== null &&
299
+ this.refresh_token !== null &&
300
+ this.accessTokenExpire &&
301
+ this.refreshTokenExpire
302
+ );
303
+ }
304
+
305
+ /**
306
+ * Refreshes authentication based on the configured grant type.
307
+ * - For authorization_code/password: Uses refreshAccessToken() with refresh_token
308
+ * - For client_credentials: Uses getTokenFromClientCredentials() to get new token
309
+ *
310
+ * On failure, notifies delegates via DLGT_INVALID_AUTH.
311
+ *
312
+ * @returns {Promise<boolean>} True if refresh succeeded, false if failed
313
+ */
314
+ async refreshAuth() {
315
+ // The wrapper runs this check before it calls refreshAuth(). Callers
316
+ // that reach this method directly skip the wrapper, so the check runs
317
+ // here for them. The guard keeps it at one read per refresh.
318
+ if (
319
+ !this._isInsideRefreshFlow() &&
320
+ (await this._adoptNewerCredential())
321
+ ) {
322
+ return true;
323
+ }
324
+
325
+ try {
326
+ console.log('[Frigg] Starting token refresh', {
327
+ grant_type: this.grant_type,
328
+ has_refresh_token: !!this.refresh_token,
329
+ has_client_id: !!this.client_id,
330
+ has_client_secret: !!this.client_secret,
331
+ has_token_uri: !!this.tokenUri,
332
+ tokenUri: this.tokenUri,
333
+ });
334
+
335
+ if (this.grant_type !== 'client_credentials') {
336
+ await this.refreshAccessToken({
337
+ refresh_token: this.refresh_token,
338
+ });
339
+ } else {
340
+ // getTokenFromClientCredentials() reports a failed token
341
+ // request itself and resolves to undefined. Without this
342
+ // check, the refresh counts as a success with no new token.
343
+ const tokenRes = await this.getTokenFromClientCredentials();
344
+ if (!tokenRes) return false;
345
+ }
346
+ console.log('[Frigg] Token refresh succeeded');
347
+ return true;
348
+ } catch (error) {
349
+ const moduleName = this.delegate?.name ?? 'unknown module';
350
+ console.error(`[Frigg] Token refresh failed for ${moduleName}`, {
351
+ error_message: error?.message,
352
+ error_name: error?.name,
353
+ response_status: error?.response?.status,
354
+ response_data: error?.response?.data,
355
+ });
356
+
357
+ if (!this._isDefinitiveAuthRejection(error)) {
358
+ throw this._transportFailureError(error, moduleName);
359
+ }
360
+
361
+ if (await this._adoptNewerCredentialWithBackoff()) return true;
362
+
363
+ // The provider rejected the grant, and the store has nothing
364
+ // newer. The credential is dead.
365
+ this.telemetry?.count?.('frigg.auth.refresh_race_lost', 1, {
366
+ module: this._telemetryModuleLabel(),
367
+ });
368
+ // Send the status only. The refresh body contains the
369
+ // client_secret, and FetchError puts the body in its message
370
+ // outside prod.
371
+ await this.notify(this.DLGT_INVALID_AUTH, {
372
+ statusCode: error?.statusCode,
373
+ });
374
+ return false;
375
+ }
376
+ }
377
+
378
+ /**
379
+ * A timeout, a 429, or a 5xx from the token endpoint says nothing about
380
+ * the credential. The error must stay retryable: the caller fails loudly
381
+ * (worker throw → SQS retry → DLQ) and does not flag a healthy
382
+ * credential. This is a fresh Error on purpose. Outside prod, the
383
+ * original message can contain the request body, and the body carries
384
+ * the client_secret.
385
+ */
386
+ _transportFailureError(error, moduleName) {
387
+ const status =
388
+ error?.statusCode ?? error?.status ?? error?.response?.status;
389
+ const transportError = new Error(
390
+ `[Frigg] Token refresh transport failure for ${moduleName}` +
391
+ (status != null ? ` (status ${status})` : '')
392
+ );
393
+ transportError.statusCode = status;
394
+ transportError.isTokenRefreshTransportFailure = true;
395
+ return transportError;
396
+ }
397
+
398
+ /**
399
+ * A definitive rejection can mean that another invocation consumed this
400
+ * refresh token first. That invocation's write can be unreadable for a
401
+ * short time. Re-read with a bounded backoff before you decide that the
402
+ * credential is dead.
403
+ */
404
+ async _adoptNewerCredentialWithBackoff() {
405
+ for (const delayMs of this.credentialReloadBackoffMs) {
406
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
407
+ if (await this._adoptNewerCredential()) {
408
+ console.log(
409
+ '[Frigg] Adopted a newer credential after a refresh rejection',
410
+ { module: this._telemetryModuleLabel() }
411
+ );
412
+ this.telemetry?.count?.('frigg.auth.refresh_race_recovered', 1, {
413
+ module: this._telemetryModuleLabel(),
414
+ });
415
+ return true;
416
+ }
417
+ }
418
+ return false;
419
+ }
420
+
421
+ /**
422
+ * True when the token endpoint refused the grant. RFC 6749 §5.2 sets
423
+ * the status: 400, or 401 for invalid_client. A 429 or a 5xx is never a
424
+ * verdict on the credential. Body markers are only a fallback for SDK
425
+ * errors that have no status code. Production FetchErrors have a
426
+ * sanitized body, so a marker cannot be the primary signal.
427
+ */
428
+ _isDefinitiveAuthRejection(error) {
429
+ const status =
430
+ error?.statusCode ?? error?.status ?? error?.response?.status;
431
+ if (status !== undefined && status !== null) {
432
+ return status === 400 || status === 401;
433
+ }
434
+ const haystack = [
435
+ error?.message,
436
+ error?.body,
437
+ typeof error?.error === 'string' ? error.error : null,
438
+ error?.response?.data && JSON.stringify(error.response.data),
439
+ ]
440
+ .filter(Boolean)
441
+ .join(' ');
442
+ return /\b(invalid_grant|invalid_client)\b/i.test(haystack);
443
+ }
444
+
445
+ /**
446
+ * Adopts the credential stored in the database if it is newer than the
447
+ * credential from the instance. The refresh token decides "newer": a
448
+ * provider can rotate it and return an identical access-token string.
449
+ * The reload only reads. A reload failure is not fatal: a database blip
450
+ * must not change the auth behavior.
451
+ *
452
+ * @returns {Promise<boolean>} True if the module adopted a newer
453
+ * credential.
454
+ */
455
+ async _adoptNewerCredential() {
456
+ let stored = null;
457
+ try {
458
+ stored = await this.notify(this.DLGT_CREDENTIAL_RELOAD);
459
+ } catch (_) {
460
+ return false;
461
+ }
462
+ if (!stored?.refresh_token) return false;
463
+ if (stored.refresh_token === this.refresh_token) return false;
464
+
465
+ if (stored.access_token) {
466
+ this.access_token = stored.access_token;
467
+ }
468
+ this.refresh_token = stored.refresh_token;
469
+ if (stored.accessTokenExpire !== undefined) {
470
+ this.accessTokenExpire = stored.accessTokenExpire;
471
+ }
472
+ if (stored.refreshTokenExpire !== undefined) {
473
+ this.refreshTokenExpire = stored.refreshTokenExpire;
474
+ }
475
+ return true;
476
+ }
477
+
478
+ /**
479
+ * Obtains tokens using the Resource Owner Password Credentials grant.
480
+ * Requires username and password to be set.
481
+ *
482
+ * @returns {Promise<Object|undefined>} Token response or undefined on error
483
+ */
484
+ async getTokenFromUsernamePassword() {
485
+ try {
486
+ const url = this.tokenUri;
487
+
488
+ const body = {
489
+ username: this.username,
490
+ password: this.password,
491
+ grant_type: 'password',
492
+ };
493
+ const headers = {
494
+ 'Content-Type': 'application/json',
495
+ };
496
+
497
+ const tokenRes = await this._post({
498
+ url,
499
+ body,
500
+ headers,
501
+ });
502
+
503
+ await this.setTokens(tokenRes);
504
+ return tokenRes;
505
+ } catch (error) {
506
+ // Status only. This request's body holds the password or client
507
+ // secret, and FetchError embeds the body in its message outside
508
+ // prod, so forwarding the error itself would log the credential.
509
+ await this.notify(this.DLGT_INVALID_AUTH, {
510
+ statusCode: error?.statusCode,
511
+ });
512
+ }
513
+ }
514
+
515
+ /**
516
+ * Obtains tokens using the Client Credentials grant.
517
+ * Used for server-to-server authentication without a user context.
518
+ * Requires client_id, client_secret, and optionally audience to be set.
519
+ *
520
+ * @returns {Promise<Object|undefined>} Token response or undefined on error
521
+ */
522
+ async getTokenFromClientCredentials() {
523
+ try {
524
+ const url = this.tokenUri;
525
+
526
+ const body = {
527
+ audience: this.audience,
528
+ client_id: this.client_id,
529
+ client_secret: this.client_secret,
530
+ grant_type: 'client_credentials',
531
+ };
532
+ const headers = {
533
+ 'Content-Type': 'application/json',
534
+ };
535
+
536
+ const tokenRes = await this._post({
537
+ url,
538
+ body,
539
+ headers,
540
+ });
541
+
542
+ await this.setTokens(tokenRes);
543
+ return tokenRes;
544
+ } catch (error) {
545
+ // Status only. This request's body holds the password or client
546
+ // secret, and FetchError embeds the body in its message outside
547
+ // prod, so forwarding the error itself would log the credential.
548
+ await this.notify(this.DLGT_INVALID_AUTH, {
549
+ statusCode: error?.statusCode,
550
+ });
551
+ }
552
+ }
553
+ }
554
+
555
+ module.exports = { OAuth2Requester };