open-loam 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (287) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +148 -8
  3. data/LICENSE +1 -1
  4. data/README.md +27 -25
  5. data/app/jobs/{loam → open_loam}/custom_field_reindex_job.rb +6 -6
  6. data/app/jobs/{loam → open_loam}/event_delivery_job.rb +8 -8
  7. data/app/jobs/open_loam/event_log_prune_job.rb +15 -0
  8. data/app/jobs/{loam → open_loam}/event_redelivery_sweep_job.rb +4 -4
  9. data/app/jobs/{loam → open_loam}/webhook_delivery_job.rb +17 -8
  10. data/app/models/open_loam/api_token.rb +63 -0
  11. data/app/models/{loam → open_loam}/audit_record.rb +3 -3
  12. data/app/models/{loam → open_loam}/auth_attempt.rb +5 -5
  13. data/app/models/{loam → open_loam}/business_rule.rb +10 -10
  14. data/app/models/{loam → open_loam}/business_rule_run.rb +5 -5
  15. data/app/models/{loam → open_loam}/comment.rb +9 -9
  16. data/app/models/{loam → open_loam}/config.rb +7 -7
  17. data/app/models/{loam → open_loam}/custom_field_value.rb +5 -5
  18. data/app/models/{loam → open_loam}/dashboard_widget.rb +5 -5
  19. data/app/models/open_loam/dictionary.rb +20 -0
  20. data/app/models/{loam → open_loam}/dictionary_entry.rb +7 -7
  21. data/app/models/{loam → open_loam}/event_delivery.rb +4 -4
  22. data/app/models/open_loam/event_record.rb +42 -0
  23. data/app/models/{loam → open_loam}/field_definition.rb +8 -8
  24. data/app/models/{loam → open_loam}/inbound_webhook_delivery.rb +4 -4
  25. data/app/models/{loam → open_loam}/inbound_webhook_source.rb +13 -13
  26. data/app/models/{loam → open_loam}/membership.rb +6 -6
  27. data/app/models/{loam → open_loam}/mfa_credential.rb +8 -8
  28. data/app/models/{loam → open_loam}/notification.rb +10 -10
  29. data/app/models/{loam → open_loam}/pending_action.rb +70 -26
  30. data/app/models/{loam → open_loam}/perspective.rb +5 -5
  31. data/app/models/{loam → open_loam}/progress_job.rb +7 -7
  32. data/app/models/{loam → open_loam}/record_lock.rb +5 -5
  33. data/app/models/{loam → open_loam}/scheduled_job.rb +7 -7
  34. data/app/models/open_loam/search_token.rb +9 -0
  35. data/app/models/{loam → open_loam}/sso_identity.rb +4 -4
  36. data/app/models/open_loam/sso_provider.rb +66 -0
  37. data/app/models/open_loam/tenant.rb +20 -0
  38. data/app/models/{loam → open_loam}/translation.rb +5 -5
  39. data/app/models/open_loam/webhook_endpoint.rb +39 -0
  40. data/app/views/{loam → open_loam}/attachments/_attachments.html.erb +1 -1
  41. data/app/views/{loam → open_loam}/comments/_comments.html.erb +1 -1
  42. data/app/views/{loam → open_loam}/custom_fields/_fields.html.erb +6 -2
  43. data/lib/generators/{loam → open_loam}/entity/entity_generator.rb +9 -9
  44. data/lib/generators/{loam → open_loam}/entity/templates/api_controller.rb +2 -1
  45. data/lib/generators/{loam → open_loam}/entity/templates/controller.rb +32 -22
  46. data/lib/generators/{loam → open_loam}/entity/templates/entity_test.rb +12 -12
  47. data/lib/generators/{loam → open_loam}/entity/templates/migration.rb +5 -5
  48. data/lib/generators/{loam → open_loam}/entity/templates/model.rb +10 -10
  49. data/lib/generators/{loam → open_loam}/entity/templates/policy.rb +1 -1
  50. data/lib/generators/{loam → open_loam}/entity/templates/views/_form.html.erb +8 -5
  51. data/lib/generators/{loam → open_loam}/entity/templates/views/deleted.html.erb +7 -7
  52. data/lib/generators/open_loam/entity/templates/views/edit.html.erb +3 -0
  53. data/lib/generators/open_loam/entity/templates/views/index.html.erb +94 -0
  54. data/lib/generators/open_loam/entity/templates/views/new.html.erb +3 -0
  55. data/lib/generators/open_loam/entity/templates/views/show.html.erb +38 -0
  56. data/lib/generators/{loam → open_loam}/install/install_generator.rb +58 -47
  57. data/lib/generators/open_loam/install/templates/AGENTS.md +345 -0
  58. data/lib/generators/{loam → open_loam}/install/templates/admin/api_docs_controller.rb +2 -2
  59. data/lib/generators/{loam → open_loam}/install/templates/admin/api_docs_index.html.erb +1 -1
  60. data/lib/generators/open_loam/install/templates/admin/api_tokens_controller.rb +48 -0
  61. data/lib/generators/{loam → open_loam}/install/templates/admin/api_tokens_index.html.erb +23 -0
  62. data/lib/generators/{loam → open_loam}/install/templates/admin/base_controller.rb +101 -38
  63. data/lib/generators/{loam → open_loam}/install/templates/admin/business_rules_controller.rb +7 -7
  64. data/lib/generators/{loam → open_loam}/install/templates/admin/business_rules_form.html.erb +2 -2
  65. data/lib/generators/{loam → open_loam}/install/templates/admin/comments_controller.rb +4 -4
  66. data/lib/generators/{loam → open_loam}/install/templates/admin/configs_controller.rb +9 -9
  67. data/lib/generators/open_loam/install/templates/admin/dashboard_controller.rb +12 -0
  68. data/lib/generators/{loam → open_loam}/install/templates/admin/dashboard_widgets_controller.rb +5 -5
  69. data/lib/generators/{loam → open_loam}/install/templates/admin/dictionaries_controller.rb +5 -5
  70. data/lib/generators/{loam → open_loam}/install/templates/admin/dictionaries_form.html.erb +1 -1
  71. data/lib/generators/{loam → open_loam}/install/templates/admin/dictionary_entries_controller.rb +2 -2
  72. data/lib/generators/{loam → open_loam}/install/templates/admin/event_deliveries_controller.rb +5 -5
  73. data/lib/generators/{loam → open_loam}/install/templates/admin/event_deliveries_index.html.erb +1 -1
  74. data/lib/generators/{loam → open_loam}/install/templates/admin/events_controller.rb +7 -5
  75. data/lib/generators/{loam → open_loam}/install/templates/admin/features_controller.rb +9 -9
  76. data/lib/generators/{loam → open_loam}/install/templates/admin/field_definitions_controller.rb +5 -5
  77. data/lib/generators/{loam → open_loam}/install/templates/admin/field_definitions_new.html.erb +2 -2
  78. data/lib/generators/open_loam/install/templates/admin/history_controller.rb +64 -0
  79. data/lib/generators/{loam → open_loam}/install/templates/admin/history_index.html.erb +2 -2
  80. data/lib/generators/{loam → open_loam}/install/templates/admin/imports_controller.rb +25 -13
  81. data/lib/generators/{loam → open_loam}/install/templates/admin/inbound_webhook_sources_controller.rb +7 -7
  82. data/lib/generators/{loam → open_loam}/install/templates/admin/inbound_webhook_sources_new.html.erb +2 -4
  83. data/lib/generators/open_loam/install/templates/admin/layout.html.erb +76 -0
  84. data/lib/generators/{loam → open_loam}/install/templates/admin/mfa_controller.rb +11 -6
  85. data/lib/generators/{loam → open_loam}/install/templates/admin/notifications_controller.rb +5 -3
  86. data/lib/generators/{loam → open_loam}/install/templates/admin/overrides_controller.rb +3 -3
  87. data/lib/generators/{loam → open_loam}/install/templates/admin/overrides_index.html.erb +2 -2
  88. data/lib/generators/{loam → open_loam}/install/templates/admin/pending_actions_controller.rb +3 -3
  89. data/lib/generators/{loam → open_loam}/install/templates/admin/perspectives_controller.rb +13 -7
  90. data/lib/generators/{loam → open_loam}/install/templates/admin/perspectives_index.html.erb +1 -1
  91. data/lib/generators/open_loam/install/templates/admin/progress_jobs_controller.rb +23 -0
  92. data/lib/generators/{loam → open_loam}/install/templates/admin/progress_jobs_index.html.erb +1 -1
  93. data/lib/generators/{loam → open_loam}/install/templates/admin/record_locks_controller.rb +4 -4
  94. data/lib/generators/{loam → open_loam}/install/templates/admin/scheduled_jobs_controller.rb +7 -7
  95. data/lib/generators/{loam → open_loam}/install/templates/admin/scheduled_jobs_form.html.erb +1 -1
  96. data/lib/generators/{loam → open_loam}/install/templates/admin/scheduled_jobs_index.html.erb +1 -1
  97. data/lib/generators/{loam → open_loam}/install/templates/admin/search_controller.rb +7 -5
  98. data/lib/generators/{loam → open_loam}/install/templates/admin/sessions_controller.rb +33 -34
  99. data/lib/generators/{loam → open_loam}/install/templates/admin/sso_providers_controller.rb +6 -6
  100. data/lib/generators/{loam → open_loam}/install/templates/admin/sso_providers_form.html.erb +2 -2
  101. data/lib/generators/{loam → open_loam}/install/templates/admin/sso_providers_index.html.erb +8 -1
  102. data/lib/generators/{loam → open_loam}/install/templates/admin/sudo_controller.rb +7 -5
  103. data/lib/generators/{loam → open_loam}/install/templates/admin/sudo_new.html.erb +1 -1
  104. data/lib/generators/{loam → open_loam}/install/templates/admin/translations_controller.rb +7 -7
  105. data/lib/generators/{loam → open_loam}/install/templates/admin/translations_index.html.erb +3 -3
  106. data/lib/generators/{loam → open_loam}/install/templates/admin/webhook_endpoints_controller.rb +4 -4
  107. data/lib/generators/{loam → open_loam}/install/templates/admin/webhook_endpoints_index.html.erb +1 -1
  108. data/lib/generators/{loam → open_loam}/install/templates/admin.css +1 -1
  109. data/lib/generators/open_loam/install/templates/api_base_controller.rb +127 -0
  110. data/lib/generators/{loam → open_loam}/install/templates/guardrails_test.rb +17 -17
  111. data/lib/generators/open_loam/install/templates/import_job.rb +38 -0
  112. data/lib/generators/{loam → open_loam}/install/templates/inbound_webhooks_controller.rb +4 -4
  113. data/lib/generators/{loam → open_loam}/install/templates/initializer.rb +87 -75
  114. data/lib/generators/open_loam/install/templates/migrations/create_open_loam_api_tokens.rb +15 -0
  115. data/lib/generators/open_loam/install/templates/migrations/create_open_loam_audit_records.rb +14 -0
  116. data/lib/generators/{loam/install/templates/migrations/create_loam_auth_attempts.rb → open_loam/install/templates/migrations/create_open_loam_auth_attempts.rb} +3 -3
  117. data/lib/generators/{loam/install/templates/migrations/create_loam_business_rules.rb → open_loam/install/templates/migrations/create_open_loam_business_rules.rb} +10 -10
  118. data/lib/generators/open_loam/install/templates/migrations/create_open_loam_comments.rb +13 -0
  119. data/lib/generators/{loam/install/templates/migrations/create_loam_configs.rb → open_loam/install/templates/migrations/create_open_loam_configs.rb} +7 -7
  120. data/lib/generators/open_loam/install/templates/migrations/create_open_loam_custom_field_values.rb +19 -0
  121. data/lib/generators/open_loam/install/templates/migrations/create_open_loam_dashboard_widgets.rb +12 -0
  122. data/lib/generators/open_loam/install/templates/migrations/create_open_loam_dictionaries.rb +11 -0
  123. data/lib/generators/{loam/install/templates/migrations/create_loam_dictionary_entries.rb → open_loam/install/templates/migrations/create_open_loam_dictionary_entries.rb} +6 -6
  124. data/lib/generators/{loam/install/templates/migrations/create_loam_event_deliveries.rb → open_loam/install/templates/migrations/create_open_loam_event_deliveries.rb} +5 -5
  125. data/lib/generators/open_loam/install/templates/migrations/create_open_loam_event_records.rb +13 -0
  126. data/lib/generators/{loam/install/templates/migrations/create_loam_field_definitions.rb → open_loam/install/templates/migrations/create_open_loam_field_definitions.rb} +4 -4
  127. data/lib/generators/{loam/install/templates/migrations/create_loam_inbound_webhooks.rb → open_loam/install/templates/migrations/create_open_loam_inbound_webhooks.rb} +9 -10
  128. data/lib/generators/open_loam/install/templates/migrations/create_open_loam_memberships.rb +11 -0
  129. data/lib/generators/{loam/install/templates/migrations/create_loam_mfa_credentials.rb → open_loam/install/templates/migrations/create_open_loam_mfa_credentials.rb} +4 -4
  130. data/lib/generators/open_loam/install/templates/migrations/create_open_loam_notifications.rb +15 -0
  131. data/lib/generators/{loam/install/templates/migrations/create_loam_pending_actions.rb → open_loam/install/templates/migrations/create_open_loam_pending_actions.rb} +9 -9
  132. data/lib/generators/{loam/install/templates/migrations/create_loam_perspectives.rb → open_loam/install/templates/migrations/create_open_loam_perspectives.rb} +5 -5
  133. data/lib/generators/{loam/install/templates/migrations/create_loam_progress_jobs.rb → open_loam/install/templates/migrations/create_open_loam_progress_jobs.rb} +5 -5
  134. data/lib/generators/open_loam/install/templates/migrations/create_open_loam_record_locks.rb +16 -0
  135. data/lib/generators/{loam/install/templates/migrations/create_loam_scheduled_jobs.rb → open_loam/install/templates/migrations/create_open_loam_scheduled_jobs.rb} +5 -5
  136. data/lib/generators/open_loam/install/templates/migrations/create_open_loam_search_tokens.rb +18 -0
  137. data/lib/generators/{loam/install/templates/migrations/create_loam_sso_providers.rb → open_loam/install/templates/migrations/create_open_loam_sso_providers.rb} +15 -12
  138. data/lib/generators/open_loam/install/templates/migrations/create_open_loam_tenants.rb +9 -0
  139. data/lib/generators/open_loam/install/templates/migrations/create_open_loam_translations.rb +15 -0
  140. data/lib/generators/open_loam/install/templates/migrations/create_open_loam_webhook_endpoints.rb +13 -0
  141. data/lib/generators/{loam → open_loam}/install/templates/migrations/create_users.rb +1 -1
  142. data/lib/generators/{loam → open_loam}/install/templates/user.rb +5 -5
  143. data/lib/generators/{loam → open_loam}/primary_key_options.rb +23 -23
  144. data/lib/open-loam.rb +2 -2
  145. data/lib/{loam → open_loam}/attachable.rb +4 -4
  146. data/lib/{loam → open_loam}/auditable.rb +23 -23
  147. data/lib/{loam → open_loam}/auth_throttle.rb +20 -12
  148. data/lib/{loam → open_loam}/base32.rb +1 -1
  149. data/lib/{loam → open_loam}/bulk.rb +7 -7
  150. data/lib/{loam → open_loam}/business_rules/actions.rb +7 -7
  151. data/lib/{loam → open_loam}/business_rules/condition.rb +5 -5
  152. data/lib/{loam → open_loam}/business_rules.rb +19 -19
  153. data/lib/{loam → open_loam}/commentable.rb +6 -6
  154. data/lib/{loam → open_loam}/configs.rb +19 -19
  155. data/lib/{loam → open_loam}/cron.rb +2 -2
  156. data/lib/{loam → open_loam}/csv.rb +2 -2
  157. data/lib/{loam → open_loam}/current.rb +5 -5
  158. data/lib/{loam → open_loam}/custom_field_index.rb +33 -30
  159. data/lib/{loam → open_loam}/custom_fields.rb +17 -17
  160. data/lib/{loam → open_loam}/dashboard.rb +5 -5
  161. data/lib/{loam → open_loam}/dictionaries.rb +12 -12
  162. data/lib/{loam → open_loam}/durable_events.rb +19 -19
  163. data/lib/{loam → open_loam}/encryptable.rb +39 -34
  164. data/lib/{loam → open_loam}/encryption/cipher.rb +3 -3
  165. data/lib/{loam → open_loam}/encryption/key_provider.rb +19 -3
  166. data/lib/{loam → open_loam}/encryption.rb +58 -15
  167. data/lib/open_loam/engine.rb +65 -0
  168. data/lib/{loam → open_loam}/enrichers.rb +5 -5
  169. data/lib/{loam → open_loam}/errors.rb +20 -10
  170. data/lib/{loam → open_loam}/eval.rb +2 -2
  171. data/lib/open_loam/event_log.rb +77 -0
  172. data/lib/{loam → open_loam}/event_stream.rb +14 -14
  173. data/lib/{loam → open_loam}/eventful.rb +12 -12
  174. data/lib/{loam → open_loam}/events.rb +5 -5
  175. data/lib/{loam → open_loam}/export.rb +12 -12
  176. data/lib/{loam → open_loam}/features.rb +15 -15
  177. data/lib/{loam → open_loam}/generated_key.rb +6 -6
  178. data/lib/{loam → open_loam}/import.rb +12 -15
  179. data/lib/{loam → open_loam}/inbound_webhooks.rb +19 -18
  180. data/lib/{loam → open_loam}/lifecycle.rb +54 -25
  181. data/lib/{loam/locales/loam.en.yml → open_loam/locales/open_loam.en.yml} +5 -5
  182. data/lib/{loam → open_loam}/mcp/server.rb +9 -9
  183. data/lib/{loam → open_loam}/mcp.rb +31 -22
  184. data/lib/{loam → open_loam}/notifications.rb +5 -5
  185. data/lib/{loam → open_loam}/open_api.rb +16 -16
  186. data/lib/open_loam/outbound_url.rb +100 -0
  187. data/lib/{loam → open_loam}/overrides.rb +10 -10
  188. data/lib/{loam → open_loam}/pending_actions.rb +9 -9
  189. data/lib/{loam → open_loam}/permissions.rb +8 -8
  190. data/lib/{loam → open_loam}/perspectives.rb +9 -9
  191. data/lib/{loam → open_loam}/policy.rb +21 -8
  192. data/lib/{loam → open_loam}/progress.rb +6 -6
  193. data/lib/{loam → open_loam}/record_locks.rb +9 -9
  194. data/lib/{loam → open_loam}/scheduler.rb +20 -20
  195. data/lib/{loam → open_loam}/search/driver.rb +3 -3
  196. data/lib/{loam → open_loam}/search/like_driver.rb +5 -5
  197. data/lib/{loam → open_loam}/search/token_driver.rb +16 -16
  198. data/lib/{loam → open_loam}/search.rb +5 -5
  199. data/lib/{loam → open_loam}/searchable.rb +22 -22
  200. data/lib/{loam → open_loam}/soft_deletable.rb +10 -10
  201. data/lib/{loam → open_loam}/sso/claims.rb +1 -1
  202. data/lib/{loam → open_loam}/sso/fake_provider.rb +1 -1
  203. data/lib/open_loam/sso/http_client.rb +51 -0
  204. data/lib/{loam → open_loam}/sso/oidc_provider.rb +2 -2
  205. data/lib/{loam → open_loam}/sso.rb +39 -18
  206. data/lib/{loam → open_loam}/telemetry.rb +6 -6
  207. data/lib/{loam → open_loam}/tenant_record.rb +10 -10
  208. data/lib/{loam → open_loam}/test_helpers.rb +4 -4
  209. data/lib/{loam → open_loam}/totp.rb +3 -3
  210. data/lib/{loam → open_loam}/translatable.rb +24 -24
  211. data/lib/{loam → open_loam}/undo.rb +11 -11
  212. data/lib/open_loam/version.rb +3 -0
  213. data/lib/{loam → open_loam}/webhooks.rb +4 -4
  214. data/lib/{loam → open_loam}/widgets.rb +9 -9
  215. data/lib/{loam → open_loam}/workflow.rb +45 -45
  216. data/lib/open_loam.rb +124 -0
  217. data/lib/tasks/open_loam.rake +203 -0
  218. data/lib/tasks/{loam_eval.rake → open_loam_eval.rake} +7 -7
  219. data/lib/tasks/open_loam_mcp.rake +20 -0
  220. metadata +247 -242
  221. data/app/models/loam/api_token.rb +0 -40
  222. data/app/models/loam/dictionary.rb +0 -20
  223. data/app/models/loam/search_token.rb +0 -9
  224. data/app/models/loam/sso_provider.rb +0 -35
  225. data/app/models/loam/tenant.rb +0 -20
  226. data/app/models/loam/webhook_endpoint.rb +0 -26
  227. data/lib/generators/loam/entity/templates/views/edit.html.erb +0 -3
  228. data/lib/generators/loam/entity/templates/views/index.html.erb +0 -90
  229. data/lib/generators/loam/entity/templates/views/new.html.erb +0 -3
  230. data/lib/generators/loam/entity/templates/views/show.html.erb +0 -36
  231. data/lib/generators/loam/install/templates/AGENTS.md +0 -341
  232. data/lib/generators/loam/install/templates/admin/api_tokens_controller.rb +0 -30
  233. data/lib/generators/loam/install/templates/admin/dashboard_controller.rb +0 -10
  234. data/lib/generators/loam/install/templates/admin/history_controller.rb +0 -38
  235. data/lib/generators/loam/install/templates/admin/layout.html.erb +0 -76
  236. data/lib/generators/loam/install/templates/admin/progress_jobs_controller.rb +0 -19
  237. data/lib/generators/loam/install/templates/api_base_controller.rb +0 -90
  238. data/lib/generators/loam/install/templates/import_job.rb +0 -25
  239. data/lib/generators/loam/install/templates/migrations/create_loam_api_tokens.rb +0 -15
  240. data/lib/generators/loam/install/templates/migrations/create_loam_audit_records.rb +0 -14
  241. data/lib/generators/loam/install/templates/migrations/create_loam_comments.rb +0 -13
  242. data/lib/generators/loam/install/templates/migrations/create_loam_custom_field_values.rb +0 -19
  243. data/lib/generators/loam/install/templates/migrations/create_loam_dashboard_widgets.rb +0 -12
  244. data/lib/generators/loam/install/templates/migrations/create_loam_dictionaries.rb +0 -11
  245. data/lib/generators/loam/install/templates/migrations/create_loam_memberships.rb +0 -11
  246. data/lib/generators/loam/install/templates/migrations/create_loam_notifications.rb +0 -15
  247. data/lib/generators/loam/install/templates/migrations/create_loam_record_locks.rb +0 -16
  248. data/lib/generators/loam/install/templates/migrations/create_loam_search_tokens.rb +0 -18
  249. data/lib/generators/loam/install/templates/migrations/create_loam_tenants.rb +0 -9
  250. data/lib/generators/loam/install/templates/migrations/create_loam_translations.rb +0 -15
  251. data/lib/generators/loam/install/templates/migrations/create_loam_webhook_endpoints.rb +0 -13
  252. data/lib/loam/engine.rb +0 -57
  253. data/lib/loam/sso/http_client.rb +0 -42
  254. data/lib/loam/version.rb +0 -3
  255. data/lib/loam.rb +0 -122
  256. data/lib/tasks/loam.rake +0 -171
  257. data/lib/tasks/loam_mcp.rake +0 -20
  258. /data/lib/generators/{loam → open_loam}/install/templates/admin/business_rules_edit.html.erb +0 -0
  259. /data/lib/generators/{loam → open_loam}/install/templates/admin/business_rules_index.html.erb +0 -0
  260. /data/lib/generators/{loam → open_loam}/install/templates/admin/business_rules_new.html.erb +0 -0
  261. /data/lib/generators/{loam → open_loam}/install/templates/admin/configs_edit.html.erb +0 -0
  262. /data/lib/generators/{loam → open_loam}/install/templates/admin/configs_index.html.erb +0 -0
  263. /data/lib/generators/{loam → open_loam}/install/templates/admin/dashboard_index.html.erb +0 -0
  264. /data/lib/generators/{loam → open_loam}/install/templates/admin/dashboard_widgets_index.html.erb +0 -0
  265. /data/lib/generators/{loam → open_loam}/install/templates/admin/dictionaries_edit.html.erb +0 -0
  266. /data/lib/generators/{loam → open_loam}/install/templates/admin/dictionaries_index.html.erb +0 -0
  267. /data/lib/generators/{loam → open_loam}/install/templates/admin/dictionaries_new.html.erb +0 -0
  268. /data/lib/generators/{loam → open_loam}/install/templates/admin/features_index.html.erb +0 -0
  269. /data/lib/generators/{loam → open_loam}/install/templates/admin/field_definitions_index.html.erb +0 -0
  270. /data/lib/generators/{loam → open_loam}/install/templates/admin/imports_new.html.erb +0 -0
  271. /data/lib/generators/{loam → open_loam}/install/templates/admin/imports_preview.html.erb +0 -0
  272. /data/lib/generators/{loam → open_loam}/install/templates/admin/imports_summary.html.erb +0 -0
  273. /data/lib/generators/{loam → open_loam}/install/templates/admin/inbound_webhook_sources_index.html.erb +0 -0
  274. /data/lib/generators/{loam → open_loam}/install/templates/admin/mfa_activated.html.erb +0 -0
  275. /data/lib/generators/{loam → open_loam}/install/templates/admin/mfa_new.html.erb +0 -0
  276. /data/lib/generators/{loam → open_loam}/install/templates/admin/mfa_show.html.erb +0 -0
  277. /data/lib/generators/{loam → open_loam}/install/templates/admin/notifications_index.html.erb +0 -0
  278. /data/lib/generators/{loam → open_loam}/install/templates/admin/pagination.rb +0 -0
  279. /data/lib/generators/{loam → open_loam}/install/templates/admin/pending_actions_index.html.erb +0 -0
  280. /data/lib/generators/{loam → open_loam}/install/templates/admin/scheduled_jobs_edit.html.erb +0 -0
  281. /data/lib/generators/{loam → open_loam}/install/templates/admin/scheduled_jobs_new.html.erb +0 -0
  282. /data/lib/generators/{loam → open_loam}/install/templates/admin/search_index.html.erb +0 -0
  283. /data/lib/generators/{loam → open_loam}/install/templates/admin/sessions_mfa_challenge.html.erb +0 -0
  284. /data/lib/generators/{loam → open_loam}/install/templates/admin/sessions_new.html.erb +0 -0
  285. /data/lib/generators/{loam → open_loam}/install/templates/admin/sso_providers_edit.html.erb +0 -0
  286. /data/lib/generators/{loam → open_loam}/install/templates/admin/sso_providers_new.html.erb +0 -0
  287. /data/lib/generators/{loam → open_loam}/install/templates/admin/webhook_endpoints_new.html.erb +0 -0
@@ -1,14 +1,14 @@
1
- module Loam
2
- # The real-time bridge: pushes selected Loam events to a browser over
1
+ module OpenLoam
2
+ # The real-time bridge: pushes selected OpenLoam events to a browser over
3
3
  # Server-Sent Events, so the admin updates live instead of polling.
4
4
  #
5
5
  # SECURITY POSTURE — default OFF. An event reaches a browser only if ALL hold:
6
- # * its name matches a declared Loam.broadcast_events pattern (opt-in), AND
6
+ # * its name matches a declared OpenLoam.broadcast_events pattern (opt-in), AND
7
7
  # * its tenant matches the connected tenant (isolation), AND
8
8
  # * its audience includes the connected actor (a payload `user_id`, if any,
9
9
  # is the sole recipient; no `user_id` means tenant-wide).
10
10
  #
11
- # FAN-OUT is behind a swappable broadcaster seam (Loam::EventStream.broadcaster).
11
+ # FAN-OUT is behind a swappable broadcaster seam (OpenLoam::EventStream.broadcaster).
12
12
  # The default in-process broadcaster only sees events published in THIS process
13
13
  # — fine for the single-process prototype; a multi-process deploy swaps in a
14
14
  # Redis/SolidCable-backed broadcaster with no controller change (see
@@ -24,10 +24,10 @@ module Loam
24
24
  # Is this event name allowed to reach browsers at all? Empty allow-list →
25
25
  # false, always (nothing leaks by default).
26
26
  def broadcastable?(event_name)
27
- Loam.broadcast_events.any? do |pattern|
28
- next false if Loam::Overrides.disabled?(:broadcast_events, pattern) # an app can turn a default pattern off
27
+ OpenLoam.broadcast_events.any? do |pattern|
28
+ next false if OpenLoam::Overrides.disabled?(:broadcast_events, pattern) # an app can turn a default pattern off
29
29
 
30
- Loam::Events.pattern_matches?(pattern, event_name)
30
+ OpenLoam::Events.pattern_matches?(pattern, event_name)
31
31
  end
32
32
  end
33
33
 
@@ -42,9 +42,9 @@ module Loam
42
42
  recipient.nil? || recipient == actor&.id
43
43
  end
44
44
 
45
- # One SSE message: an `event:` line (the Loam event name) and a `data:`
45
+ # One SSE message: an `event:` line (the OpenLoam event name) and a `data:`
46
46
  # line (JSON), ended by a blank line. Only small id-ish keys ride along —
47
- # Loam events carry no attribute values, and this slices to a safe set as
47
+ # OpenLoam events carry no attribute values, and this slices to a safe set as
48
48
  # belt-and-suspenders (tenant_id is dropped; it is implied by the connection).
49
49
  def frame(event_name, payload)
50
50
  "event: #{event_name}\ndata: #{safe_payload(payload).to_json}\n\n"
@@ -54,21 +54,21 @@ module Loam
54
54
 
55
55
  def safe_payload(payload)
56
56
  # id-ish keys only — never attribute values. percent/status ride along for
57
- # the progress bar (Loam::ProgressJob); both are non-sensitive.
57
+ # the progress bar (OpenLoam::ProgressJob); both are non-sensitive.
58
58
  payload.symbolize_keys.slice(:id, :type, :user_id, :from, :to, :percent, :status).compact
59
59
  end
60
60
  end
61
61
 
62
- # The default fan-out: subscribe to Loam::Events in THIS process and forward
62
+ # The default fan-out: subscribe to OpenLoam::Events in THIS process and forward
63
63
  # the events deliverable to (tenant, actor) to a sink. A `sink` is anything
64
64
  # answering #call(sse_string) — the controller's is a Queue push.
65
65
  class InProcessBroadcaster
66
66
  # Returns an opaque handle to pass back to #unsubscribe.
67
67
  def subscribe(tenant:, actor:, &sink)
68
- Loam::Events.subscribe_all do |event_name, payload|
69
- next unless Loam::EventStream.deliverable?(event_name, payload, tenant: tenant, actor: actor)
68
+ OpenLoam::Events.subscribe_all do |event_name, payload|
69
+ next unless OpenLoam::EventStream.deliverable?(event_name, payload, tenant: tenant, actor: actor)
70
70
 
71
- sink.call(Loam::EventStream.frame(event_name, payload))
71
+ sink.call(OpenLoam::EventStream.frame(event_name, payload))
72
72
  end
73
73
  end
74
74
 
@@ -1,18 +1,18 @@
1
- module Loam
1
+ module OpenLoam
2
2
  # Lifecycle events by convention. Included in every generated entity:
3
3
  #
4
- # class Equipment < Loam::TenantRecord
5
- # include Loam::Eventful
4
+ # class Equipment < OpenLoam::TenantRecord
5
+ # include OpenLoam::Eventful
6
6
  # event_domain :rental # -> "rental.equipment.created" etc.
7
7
  # end
8
8
  #
9
- # Custom business events are published explicitly with Loam::Events.publish.
9
+ # Custom business events are published explicitly with OpenLoam::Events.publish.
10
10
  module Eventful
11
11
  extend ActiveSupport::Concern
12
12
 
13
13
  included do
14
- class_attribute :loam_event_domain, default: "app"
15
- class_attribute :loam_event_entity, default: nil
14
+ class_attribute :open_loam_event_domain, default: "app"
15
+ class_attribute :open_loam_event_entity, default: nil
16
16
 
17
17
  after_create_commit { publish_lifecycle_event("created") }
18
18
  after_update_commit { publish_lifecycle_event("updated") }
@@ -21,23 +21,23 @@ module Loam
21
21
 
22
22
  class_methods do
23
23
  def event_domain(domain)
24
- self.loam_event_domain = domain.to_s
24
+ self.open_loam_event_domain = domain.to_s
25
25
  end
26
26
 
27
27
  # The middle segment of the event name. It defaults to the model's param
28
28
  # key, which is right for app models but awkward for namespaced ones —
29
- # Loam::Comment would publish "loam.loam_comment.created" rather than
30
- # "loam.comment.created".
29
+ # OpenLoam::Comment would publish "open_loam.open_loam_comment.created" rather than
30
+ # "open_loam.comment.created".
31
31
  def event_entity(name)
32
- self.loam_event_entity = name.to_s
32
+ self.open_loam_event_entity = name.to_s
33
33
  end
34
34
  end
35
35
 
36
36
  private
37
37
 
38
38
  def publish_lifecycle_event(happened)
39
- Loam::Events.publish(
40
- "#{loam_event_domain}.#{loam_event_entity || model_name.param_key}.#{happened}",
39
+ OpenLoam::Events.publish(
40
+ "#{open_loam_event_domain}.#{open_loam_event_entity || model_name.param_key}.#{happened}",
41
41
  { id: id, type: self.class.name }
42
42
  )
43
43
  end
@@ -1,4 +1,4 @@
1
- module Loam
1
+ module OpenLoam
2
2
  # A thin domain event bus over ActiveSupport::Notifications.
3
3
  #
4
4
  # Convention: event names are `domain.thing.happened`, e.g.
@@ -7,7 +7,7 @@ module Loam
7
7
  # subscribers are always tenant-aware.
8
8
  module Events
9
9
  NAME_FORMAT = /\A[a-z0-9_]+(\.[a-z0-9_]+){2,}\z/
10
- PREFIX = "loam.event."
10
+ PREFIX = "open_loam.event."
11
11
 
12
12
  def self.publish(name, payload = {})
13
13
  name = name.to_s
@@ -17,12 +17,12 @@ module Loam
17
17
 
18
18
  ActiveSupport::Notifications.instrument(
19
19
  PREFIX + name,
20
- payload.merge(tenant_id: Loam::Current.tenant&.id, actor_id: Loam::Current.actor&.id)
20
+ payload.merge(tenant_id: OpenLoam::Current.tenant&.id, actor_id: OpenLoam::Current.actor&.id)
21
21
  )
22
22
  end
23
23
 
24
24
  # The subscription rule, in one place: a trailing dot is a domain prefix,
25
- # anything else is an exact event name. Loam::WebhookEndpoint matches
25
+ # anything else is an exact event name. OpenLoam::WebhookEndpoint matches
26
26
  # against this too, so a pattern means the same thing everywhere.
27
27
  def self.pattern_matches?(pattern, event_name)
28
28
  pattern = pattern.to_s
@@ -31,7 +31,7 @@ module Loam
31
31
  pattern.end_with?(".") ? event_name.start_with?(pattern) : event_name == pattern
32
32
  end
33
33
 
34
- # Every Loam event, whatever its domain — the empty prefix matches them
34
+ # Every OpenLoam event, whatever its domain — the empty prefix matches them
35
35
  # all. Used by the webhook dispatcher, which decides per event which
36
36
  # endpoints care.
37
37
  def self.subscribe_all(&block)
@@ -1,16 +1,17 @@
1
1
  require "csv"
2
2
 
3
- module Loam
3
+ module OpenLoam
4
4
  # CSV export of a tenant-scoped relation, POLICY- and ENCRYPTION-aware:
5
5
  #
6
- # Loam::Export.csv(Equipment.all, actor: current_actor)
6
+ # OpenLoam::Export.csv(Equipment.all, actor: current_actor)
7
7
  #
8
- # * only fields the actor's role may READ are columns (Loam::Policy#readable?);
8
+ # * only fields the actor's role may READ are columns (OpenLoam::Policy#readable?);
9
9
  # * an ENCRYPTED column is NEVER exported in the clear — its cell is
10
10
  # "[encrypted]" (the same redaction as the audit trail), so a bulk export can
11
11
  # never become a plaintext dump of PII a role shouldn't see;
12
- # * declared custom fields are included (a dictionary field exports its stored
13
- # code, so the file round-trips back through Loam::Import);
12
+ # * declared custom fields go through the same read check
13
+ # (OpenLoam::Policy#custom_field_readable?) — a dictionary field exports its
14
+ # stored code, so the file round-trips back through OpenLoam::Import;
14
15
  # * tenant isolation is free — the relation is already scoped.
15
16
  #
16
17
  # Prototype scale: builds the CSV in memory with the stdlib CSV. A very large
@@ -37,10 +38,10 @@ module Loam
37
38
  # redacted), then declared custom fields.
38
39
  def exportable_columns(model, actor)
39
40
  policy = policy_for(model, actor)
40
- encrypted = model.respond_to?(:loam_encrypted_attributes) ? model.loam_encrypted_attributes : []
41
+ encrypted = model.respond_to?(:open_loam_encrypted_attributes) ? model.open_loam_encrypted_attributes : []
41
42
  # The blind-index columns behind searchable encrypted fields (e.g.
42
43
  # email_hash) are internal HMACs — never export them either.
43
- blind = model.respond_to?(:loam_searchable_encrypted_attributes) ? model.loam_searchable_encrypted_attributes.map { |a| "#{a}_hash" } : []
44
+ blind = model.respond_to?(:open_loam_searchable_encrypted_attributes) ? model.open_loam_searchable_encrypted_attributes.map { |a| "#{a}_hash" } : []
44
45
 
45
46
  columns = model.column_names.reject { |c| SKIP_COLUMNS.include?(c) || blind.include?(c) || c == "custom_fields" }
46
47
  .select { |c| policy.readable?(c) }
@@ -48,6 +49,8 @@ module Loam
48
49
 
49
50
  if model.respond_to?(:custom_field_definitions)
50
51
  model.custom_field_definitions.order(:name).each do |definition|
52
+ next unless policy.custom_field_readable?(definition.name)
53
+
51
54
  columns << { header: definition.name, name: definition.name, kind: :custom }
52
55
  end
53
56
  end
@@ -61,14 +64,11 @@ module Loam
61
64
  when :custom then (record.custom_field(column[:name]) rescue nil)
62
65
  else record.public_send(column[:name])
63
66
  end
64
- Loam::Csv.safe(value) # neutralize CSV formula injection (=, +, -, @, tab/CR)
67
+ OpenLoam::Csv.safe(value) # neutralize CSV formula injection (=, +, -, @, tab/CR)
65
68
  end
66
69
 
67
70
  def policy_for(model, actor)
68
- klass = "#{model.name}Policy".safe_constantize
69
- # A blank instance as the record: readable? keys off the role, but the
70
- # policy's custom-field checks read record.class.
71
- (klass || Loam::Policy).new(actor, model.new)
71
+ OpenLoam::Policy.for_model(model, actor)
72
72
  end
73
73
  end
74
74
  end
@@ -1,18 +1,18 @@
1
- module Loam
1
+ module OpenLoam
2
2
  # Runtime feature toggles: is a capability turned ON for this tenant right now.
3
3
  #
4
- # Loam::Features.on?(:beta_dashboard) # for the current tenant
5
- # Loam::Features.enable(:beta_dashboard) # this tenant only
6
- # Loam::Features.enable(:beta_dashboard, scope: :global) # everyone
7
- # Loam::Features.disable(:beta_dashboard)
8
- # Loam::Features.reset(:beta_dashboard) # drop the override → default
4
+ # OpenLoam::Features.on?(:beta_dashboard) # for the current tenant
5
+ # OpenLoam::Features.enable(:beta_dashboard) # this tenant only
6
+ # OpenLoam::Features.enable(:beta_dashboard, scope: :global) # everyone
7
+ # OpenLoam::Features.disable(:beta_dashboard)
8
+ # OpenLoam::Features.reset(:beta_dashboard) # drop the override → default
9
9
  #
10
10
  # A flag gates a CAPABILITY (is this feature live for the tenant), which is
11
11
  # orthogonal to a policy — that gates a PERSON (may this user act). The two
12
12
  # coexist: a manager may be allowed to approve reports AND the approvals
13
13
  # feature may be switched off for their tenant during a rollout.
14
14
  #
15
- # This is deliberately a thin wrapper over Loam::Configs: a flag is a boolean
15
+ # This is deliberately a thin wrapper over OpenLoam::Configs: a flag is a boolean
16
16
  # setting with the same override → global → declared-default resolution, so it
17
17
  # reuses that store (no new table) and its per-request cache. Flags live under
18
18
  # the reserved `features.` key prefix and get their own admin screen, because
@@ -22,7 +22,7 @@ module Loam
22
22
 
23
23
  class << self
24
24
  def on?(name)
25
- !!Loam::Configs.get(key_for(name), default: default_for(name))
25
+ !!OpenLoam::Configs.get(key_for(name), default: default_for(name))
26
26
  end
27
27
 
28
28
  def off?(name)
@@ -32,37 +32,37 @@ module Loam
32
32
  # Flip a flag on. scope: :tenant (default) overrides for the current tenant
33
33
  # and requires a tenant in context; scope: :global sets the app-wide state.
34
34
  def enable(name, scope: :tenant)
35
- Loam::Configs.set(key_for(name), true, scope: scope)
35
+ OpenLoam::Configs.set(key_for(name), true, scope: scope)
36
36
  end
37
37
 
38
38
  def disable(name, scope: :tenant)
39
- Loam::Configs.set(key_for(name), false, scope: scope)
39
+ OpenLoam::Configs.set(key_for(name), false, scope: scope)
40
40
  end
41
41
 
42
42
  # Drop the current tenant's override so the flag falls back to the global
43
43
  # state / declared default.
44
44
  def reset(name)
45
- Loam::Configs.reset(key_for(name))
45
+ OpenLoam::Configs.reset(key_for(name))
46
46
  end
47
47
 
48
48
  def overridden?(name)
49
- Loam::Configs.overridden?(key_for(name))
49
+ OpenLoam::Configs.overridden?(key_for(name))
50
50
  end
51
51
 
52
52
  # Every declared flag, sorted — the admin lists these whether or not a row
53
53
  # exists yet, so a flag is visible the moment it is declared.
54
54
  def declared
55
- Loam.feature_defaults.keys.sort
55
+ OpenLoam.feature_defaults.keys.sort
56
56
  end
57
57
 
58
58
  def description(name)
59
- Loam.feature_defaults.dig(name.to_s, :description)
59
+ OpenLoam.feature_defaults.dig(name.to_s, :description)
60
60
  end
61
61
 
62
62
  # The declared-default state of a flag (false for an unknown one), used as
63
63
  # the last resort when there is no override and no global row.
64
64
  def default_for(name)
65
- !!Loam.feature_defaults.dig(name.to_s, :default)
65
+ !!OpenLoam.feature_defaults.dig(name.to_s, :default)
66
66
  end
67
67
 
68
68
  private
@@ -1,4 +1,4 @@
1
- module Loam
1
+ module OpenLoam
2
2
  # Primary-key generation for tables whose key is not an integer.
3
3
  #
4
4
  # Rails leaves key generation to the database, which works for integer keys
@@ -7,22 +7,22 @@ module Loam
7
7
  # has neither: the INSERT sends NULL and the row is rejected or lands with an
8
8
  # empty key.
9
9
  #
10
- # Loam's tables follow the host app's key type (see
11
- # Loam::Generators::PrimaryKeyOptions), so on a string-keyed app every Loam
10
+ # OpenLoam's tables follow the host app's key type (see
11
+ # OpenLoam::Generators::PrimaryKeyOptions), so on a string-keyed app every OpenLoam
12
12
  # model needs its key generated in Ruby. Before this, hosts patched it in
13
13
  # themselves with a global before_create on ActiveRecord::Base — a lot to ask
14
14
  # of an app whose only deviation was not using bigints, and a hook broad
15
- # enough to reach models that had nothing to do with Loam.
15
+ # enough to reach models that had nothing to do with OpenLoam.
16
16
  #
17
17
  # Integer keys take the early return, so nothing changes for the default app.
18
18
  module GeneratedKey
19
19
  def self.included(base)
20
- base.before_create :assign_loam_generated_key
20
+ base.before_create :assign_open_loam_generated_key
21
21
  end
22
22
 
23
23
  private
24
24
 
25
- def assign_loam_generated_key
25
+ def assign_open_loam_generated_key
26
26
  key = self.class.primary_key
27
27
  return if key.nil? # a join table declared with id: false
28
28
  return if self[key].present? # the caller chose the key
@@ -1,14 +1,14 @@
1
1
  require "csv"
2
2
  require "set"
3
3
 
4
- module Loam
4
+ module OpenLoam
5
5
  # CSV import mapping engine — reusable, tenant-scoped, policy-safe.
6
6
  #
7
- # Loam::Import.preview(csv) # headers + first rows for the mapping UI
8
- # Loam::Import.run(csv, model:, mapping:, actor:, # map CSV header => field
7
+ # OpenLoam::Import.preview(csv) # headers + first rows for the mapping UI
8
+ # OpenLoam::Import.run(csv, model:, mapping:, actor:, # map CSV header => field
9
9
  # match_key: "name", # update-or-create by a key (nil = create-only)
10
10
  # dry_run: true, # validate + report, commit nothing
11
- # progress: progress_job) # advance a Loam::ProgressJob per row
11
+ # progress: progress_job) # advance a OpenLoam::ProgressJob per row
12
12
  #
13
13
  # Safety: the mapping may only target fields the actor's role can WRITE (real
14
14
  # columns + declared custom fields); tenant_id, plumbing, and non-permitted
@@ -27,14 +27,14 @@ module Loam
27
27
 
28
28
  module_function
29
29
 
30
- # Resolve an entity_type string to a model — ONLY a Loam::TenantRecord
30
+ # Resolve an entity_type string to a model — ONLY a OpenLoam::TenantRecord
31
31
  # subclass (never an arbitrary constant), so an import target can't be
32
32
  # smuggled to a global model like User (same guard as the business-rules
33
33
  # engine).
34
34
  def allowed_model(entity_type)
35
35
  klass = entity_type.to_s.safe_constantize
36
- unless klass.is_a?(Class) && klass < Loam::TenantRecord
37
- raise Loam::Error, "import target #{entity_type.inspect} is not a Loam entity"
36
+ unless klass.is_a?(Class) && klass < OpenLoam::TenantRecord
37
+ raise OpenLoam::Error, "import target #{entity_type.inspect} is not a OpenLoam entity"
38
38
  end
39
39
 
40
40
  klass
@@ -79,7 +79,7 @@ module Loam
79
79
  is_new ? (result.created += 1) : (result.updated += 1)
80
80
  rescue StandardError => error
81
81
  # Store ONLY the row number + message — NEVER the raw cell values. The
82
- # result is persisted (Loam::ProgressJob.result); a failed row into an
82
+ # result is persisted (OpenLoam::ProgressJob.result); a failed row into an
83
83
  # encrypted field would otherwise write PLAINTEXT PII at rest.
84
84
  result.failed += 1
85
85
  result.errors << { "row" => line, "message" => error.message }
@@ -103,7 +103,7 @@ module Loam
103
103
  out << (Array(headers) + [ "_error" ])
104
104
  result.errors.each do |error|
105
105
  original = data_rows[error["row"] - 2] || [] # row is 1-based incl. the header
106
- out << original.map { |cell| Loam::Csv.safe(cell) } + [ error["message"] ]
106
+ out << original.map { |cell| OpenLoam::Csv.safe(cell) } + [ error["message"] ]
107
107
  end
108
108
  end
109
109
  end
@@ -115,7 +115,7 @@ module Loam
115
115
  next if target.to_s.blank?
116
116
  next if allowed.include?(target.to_s)
117
117
 
118
- raise Loam::Error, "import mapping refuses #{target.inspect} — not a writable column or custom field"
118
+ raise OpenLoam::Error, "import mapping refuses #{target.inspect} — not a writable column or custom field"
119
119
  end
120
120
  end
121
121
 
@@ -155,14 +155,11 @@ module Loam
155
155
  def safe_parse(csv_string)
156
156
  CSV.parse(csv_string.to_s)
157
157
  rescue CSV::MalformedCSVError => error
158
- raise Loam::Error, "could not parse CSV: #{error.message}"
158
+ raise OpenLoam::Error, "could not parse CSV: #{error.message}"
159
159
  end
160
160
 
161
161
  def policy_for(model, actor)
162
- klass = "#{model.name}Policy".safe_constantize
163
- # A blank instance as the record: readable?/writable? key off the role, but
164
- # custom_field_writable? reads record.class.custom_field_definitions.
165
- (klass || Loam::Policy).new(actor, model.new)
162
+ OpenLoam::Policy.for_model(model, actor)
166
163
  end
167
164
  end
168
165
  end
@@ -1,9 +1,9 @@
1
1
  require "openssl"
2
2
  require "digest"
3
3
 
4
- module Loam
4
+ module OpenLoam
5
5
  # Receiving webhooks FROM external systems (the inbound sibling of
6
- # Loam::Webhooks). One entry point — `ingest` — does the whole verified,
6
+ # OpenLoam::Webhooks). One entry point — `ingest` — does the whole verified,
7
7
  # replay-resistant pipeline and returns a Result the controller turns into an
8
8
  # HTTP status. Kept here (not in a controller) so it is testable without HTTP
9
9
  # and shared by the generated app and the demo.
@@ -19,9 +19,12 @@ module Loam
19
19
  # Every AUTH failure returns 401 with no distinguishing body, so a sender can't
20
20
  # probe which check failed; the specific reason is logged server-side only.
21
21
  #
22
- # REPLAY: the real defense is the (source_id, external_id) dedupe. The timestamp
23
- # window is defense-in-depth: unless the sender signs the timestamp too, a
24
- # replayer can refresh an unsigned timestamp header. Don't over-trust it.
22
+ # REPLAY: the real defense is the (source_id, external_id) dedupe, and
23
+ # external_id is derived from the SIGNED body — never from a header. Anything
24
+ # an attacker can vary without invalidating the signature is a way to mint a
25
+ # fresh dedupe key and replay the delivery. The timestamp window is
26
+ # defense-in-depth only, for the same reason: unless the sender signs the
27
+ # timestamp, a replayer just refreshes it.
25
28
  module InboundWebhooks
26
29
  MAX_BYTES = 1_000_000
27
30
 
@@ -30,13 +33,13 @@ module Loam
30
33
  module_function
31
34
 
32
35
  def ingest(token:, raw_body:, headers:)
33
- Loam::Telemetry.span("inbound_webhook") { run_ingest(token, raw_body.to_s, headers) }
36
+ OpenLoam::Telemetry.span("inbound_webhook") { run_ingest(token, raw_body.to_s, headers) }
34
37
  end
35
38
 
36
39
  def run_ingest(token, raw_body, headers)
37
40
  return Result.new(status: 413, reason: "body too large") if raw_body.bytesize > MAX_BYTES
38
41
 
39
- source = Loam::InboundWebhookSource.resolve(token)
42
+ source = OpenLoam::InboundWebhookSource.resolve(token)
40
43
  return Result.new(status: 404, reason: "unknown or inactive source") if source.nil?
41
44
 
42
45
  signature = header(headers, source.signature_header_key)
@@ -51,8 +54,8 @@ module Loam
51
54
 
52
55
  begin
53
56
  delivery = nil
54
- Loam::InboundWebhookDelivery.transaction do
55
- delivery = Loam::InboundWebhookDelivery.create!(
57
+ OpenLoam::InboundWebhookDelivery.transaction do
58
+ delivery = OpenLoam::InboundWebhookDelivery.create!(
56
59
  source: source, external_id: external_id, event_name: source.event_name,
57
60
  status: "received", received_at: Time.current, payload: parse(raw_body)
58
61
  )
@@ -60,7 +63,7 @@ module Loam
60
63
  # lives on the delivery row, subscribers read it from there. Publishing
61
64
  # inside the txn ties capture to the row — a publish failure rolls the
62
65
  # row back so the sender's retry isn't deduped away.
63
- Loam::Events.publish(source.event_name, { source_id: source.id, delivery_id: delivery.id })
66
+ OpenLoam::Events.publish(source.event_name, { source_id: source.id, delivery_id: delivery.id })
64
67
  end
65
68
  Result.new(status: 202, reason: "accepted", delivery: delivery)
66
69
  rescue ActiveRecord::RecordNotUnique
@@ -92,14 +95,12 @@ module Loam
92
95
  (Time.current.to_i - seconds).abs <= tolerance.to_i
93
96
  end
94
97
 
95
- def delivery_id(source, headers, body)
96
- if source.delivery_id_header.present?
97
- value = header(headers, source.delivery_id_header)
98
- return value if value.present?
99
- end
100
- # No delivery-id header configured (or absent): fall back to a body hash.
101
- # Consequence: identical bodies dedupe. A sender with a real delivery id
102
- # should configure delivery_id_header so distinct-but-identical bodies pass.
98
+ # The dedupe key must come from SIGNED material, which means the body: the
99
+ # HMAC covers the body alone, so a captured (body, signature) replayed with a
100
+ # fresh delivery-id header used to mint a new external_id every time and
101
+ # re-publish the event. Cost: a sender emitting distinct deliveries with
102
+ # identical bodies sees the second deduped — its nonce belongs in the body.
103
+ def delivery_id(_source, _headers, body)
103
104
  Digest::SHA256.hexdigest(body)
104
105
  end
105
106