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,8 +1,8 @@
1
- module Loam
1
+ module OpenLoam
2
2
  # Field-level encryption at rest, keyed per tenant. Declare it on a model:
3
3
  #
4
- # class Patient < Loam::TenantRecord
5
- # include Loam::Encryptable
4
+ # class Patient < OpenLoam::TenantRecord
5
+ # include OpenLoam::Encryptable
6
6
  # encrypts :ssn # encrypted at rest, not searchable
7
7
  # encrypts :email, searchable: true # + a blind index for exact-match lookup
8
8
  # end
@@ -11,7 +11,7 @@ module Loam
11
11
  # patient.ssn # => "078-05-1120" (decrypted on read)
12
12
  # Patient.find_by_email("a@b.com") # exact match via the blind index
13
13
  #
14
- # The value is sealed with the CURRENT tenant's key (Loam.tenant!), never the
14
+ # The value is sealed with the CURRENT tenant's key (OpenLoam.tenant!), never the
15
15
  # record's stored tenant_id — so a read in the wrong tenant's context fails the
16
16
  # GCM auth tag instead of quietly decrypting another tenant's data. Reading or
17
17
  # writing an encrypted field with no tenant in context raises
@@ -24,13 +24,13 @@ module Loam
24
24
  extend ActiveSupport::Concern
25
25
 
26
26
  included do
27
- class_attribute :loam_encrypted_attributes, default: [].freeze, instance_writer: false
28
- class_attribute :loam_searchable_encrypted_attributes, default: [].freeze, instance_writer: false
27
+ class_attribute :open_loam_encrypted_attributes, default: [].freeze, instance_writer: false
28
+ class_attribute :open_loam_searchable_encrypted_attributes, default: [].freeze, instance_writer: false
29
29
  end
30
30
 
31
31
  class_methods do
32
32
  # `scope:` chooses whose key seals the field. `:tenant` (default) keys off
33
- # Loam.tenant! — right for entity data. A Proc `->(record) { "user/#{...}" }`
33
+ # OpenLoam.tenant! — right for entity data. A Proc `->(record) { "user/#{...}" }`
34
34
  # keys off something else, for genuinely non-tenant data (an MFA secret
35
35
  # belongs to the person and must decrypt in any tenant, and at login before
36
36
  # a tenant is chosen). Non-tenant scopes cannot be `searchable`.
@@ -38,14 +38,14 @@ module Loam
38
38
  name = name.to_s
39
39
 
40
40
  if searchable && scope != :tenant
41
- raise Loam::Error, "#{self.name}: `#{name}` cannot be both `searchable` and non-tenant-scoped."
41
+ raise OpenLoam::Error, "#{self.name}: `#{name}` cannot be both `searchable` and non-tenant-scoped."
42
42
  end
43
43
 
44
44
  # Encrypted ciphertext is meaningless to a LIKE scan, so the two are a
45
45
  # contradiction. Caught whichever declaration comes second (Searchable
46
46
  # runs the mirror check), so order in the model does not matter.
47
- if respond_to?(:loam_searchable_columns) && loam_searchable_columns.include?(name)
48
- raise Loam::Error,
47
+ if respond_to?(:open_loam_searchable_columns) && open_loam_searchable_columns.include?(name)
48
+ raise OpenLoam::Error,
49
49
  "#{self.name}: `#{name}` is declared both `searchable_by` (LIKE) and `encrypts` — " \
50
50
  "ciphertext cannot be LIKE-searched. Drop it from `searchable_by` and use " \
51
51
  "`encrypts :#{name}, searchable: true` for exact-match lookup instead."
@@ -54,41 +54,45 @@ module Loam
54
54
  # A translation row would store the field's PLAINTEXT — recreating the
55
55
  # leak encryption closes. Caught whichever declaration comes second
56
56
  # (Translatable runs the mirror check), so model order does not matter.
57
- if respond_to?(:loam_translatable_attributes) && loam_translatable_attributes.include?(name)
58
- raise Loam::Error,
57
+ if respond_to?(:open_loam_translatable_attributes) && open_loam_translatable_attributes.include?(name)
58
+ raise OpenLoam::Error,
59
59
  "#{self.name}: `#{name}` is declared both `translates` and `encrypts` — a translation " \
60
60
  "would store plaintext. Encrypted data is not translatable."
61
61
  end
62
62
 
63
- self.loam_encrypted_attributes = (loam_encrypted_attributes + [name]).freeze
64
- self.loam_searchable_encrypted_attributes = (loam_searchable_encrypted_attributes + [name]).freeze if searchable
63
+ self.open_loam_encrypted_attributes = (open_loam_encrypted_attributes + [name]).freeze
64
+ self.open_loam_searchable_encrypted_attributes = (open_loam_searchable_encrypted_attributes + [name]).freeze if searchable
65
65
 
66
- include loam_encryption_reader_writer(name, searchable, scope)
67
- define_loam_blind_index_finder(name) if searchable
66
+ include open_loam_encryption_reader_writer(name, searchable, scope)
67
+ define_open_loam_blind_index_finder(name) if searchable
68
68
  end
69
69
 
70
- # Reader/writer live in their own module (the Loam::Workflow precedent) so
70
+ # Reader/writer live in their own module (the OpenLoam::Workflow precedent) so
71
71
  # an app can override and still call `super`, and so they sit ABOVE Active
72
72
  # Record's generated attribute methods in the ancestor chain and win.
73
- def loam_encryption_reader_writer(name, searchable, scope)
73
+ def open_loam_encryption_reader_writer(name, searchable, scope)
74
74
  hash_column = "#{name}_hash"
75
75
 
76
76
  Module.new do
77
77
  define_method(name) do
78
- resolved = loam_encryption_scope(scope)
79
- aad = Loam::Encryption.aad(resolved, self.class.table_name, name)
80
- Loam::Encryption.decrypt_scoped(read_attribute(name), resolved, aad: aad)
78
+ resolved = open_loam_encryption_scope(scope)
79
+ aad = OpenLoam::Encryption.aad(resolved, self.class.table_name, name)
80
+ OpenLoam::Encryption.decrypt_scoped(read_attribute(name), resolved, aad: aad)
81
81
  end
82
82
 
83
83
  define_method("#{name}=") do |value|
84
- resolved = loam_encryption_scope(scope)
84
+ resolved = open_loam_encryption_scope(scope)
85
85
  # Bind this ciphertext to its (scope, table, column) so it can't be
86
86
  # transplanted to another column/table/tenant (v2 AAD).
87
- aad = Loam::Encryption.aad(resolved, self.class.table_name, name)
88
- write_attribute(name, Loam::Encryption.encrypt_scoped(value, resolved, aad: aad))
87
+ aad = OpenLoam::Encryption.aad(resolved, self.class.table_name, name)
88
+ write_attribute(name, OpenLoam::Encryption.encrypt_scoped(value, resolved, aad: aad))
89
89
  # The blind index tracks the ciphertext column: rewrite it in the
90
90
  # same breath, so an exact-match lookup can never go stale.
91
- write_attribute(hash_column, Loam::Encryption.blind_index_scoped(value, resolved)) if searchable
91
+ if searchable
92
+ write_attribute(hash_column,
93
+ OpenLoam::Encryption.blind_index_scoped(value, resolved,
94
+ table: self.class.table_name, column: name))
95
+ end
92
96
  end
93
97
  end
94
98
  end
@@ -97,11 +101,12 @@ module Loam
97
101
  # deliberately shadow Active Record's dynamic `find_by_<attr>`: the obvious
98
102
  # call must hash-and-compare, never match a plaintext query against the
99
103
  # ciphertext column (which would silently find nothing).
100
- def define_loam_blind_index_finder(name)
101
- hash_column = "#{name}_hash"
104
+ def define_open_loam_blind_index_finder(name)
105
+ hash_column = :"#{name}_hash"
102
106
 
103
107
  define_singleton_method("where_#{name}") do |value|
104
- where(hash_column => Loam::Encryption.blind_index(value, Loam.tenant!.id))
108
+ where(hash_column => OpenLoam::Encryption.blind_index(value, OpenLoam.tenant!.id,
109
+ table: table_name, column: name))
105
110
  end
106
111
 
107
112
  define_singleton_method("find_by_#{name}") do |value|
@@ -117,11 +122,11 @@ module Loam
117
122
  # same safety property as an entity write); a Proc computes it from the
118
123
  # record. A blank or `.../`-terminated result (e.g. a nil user_id) raises
119
124
  # rather than deriving a degenerate shared key.
120
- def loam_encryption_scope(scope)
121
- resolved = scope == :tenant ? "tenant/#{Loam.tenant!.id}" : scope.call(self).to_s
125
+ def open_loam_encryption_scope(scope)
126
+ resolved = scope == :tenant ? "tenant/#{OpenLoam.tenant!.id}" : scope.call(self).to_s
122
127
 
123
128
  if resolved.strip.empty? || resolved.end_with?("/")
124
- raise Loam::Encryption::Error, "#{self.class}: cannot derive an encryption key from a blank scope (#{resolved.inspect})"
129
+ raise OpenLoam::Encryption::Error, "#{self.class}: cannot derive an encryption key from a blank scope (#{resolved.inspect})"
125
130
  end
126
131
  resolved
127
132
  end
@@ -134,9 +139,9 @@ module Loam
134
139
  # version tag lets old and new ciphertext coexist while this runs across a
135
140
  # tenant's records, so rotation is a lazy re-encrypt, not a stop-the-world
136
141
  # migration. Writes an ordinary audited "[encrypted]" update — see
137
- # lib/tasks/loam.rake (loam:encryption:rotate).
138
- def loam_reencrypt!
139
- self.class.loam_encrypted_attributes.each do |name|
142
+ # lib/tasks/open_loam.rake (open_loam:encryption:rotate).
143
+ def open_loam_reencrypt!
144
+ self.class.open_loam_encrypted_attributes.each do |name|
140
145
  public_send("#{name}=", public_send(name))
141
146
  end
142
147
  save!
@@ -1,4 +1,4 @@
1
- module Loam
1
+ module OpenLoam
2
2
  module Encryption
3
3
  # AES-256-GCM sealing. GCM is *authenticated* encryption: the 16-byte auth
4
4
  # tag turns tampering — or decrypting with the wrong key — into a loud
@@ -13,7 +13,7 @@ module Loam
13
13
  # v2 binds Additional Authenticated Data (the field's tenant+table+column) into
14
14
  # the auth tag, so a ciphertext moved to a DIFFERENT column/table/tenant fails
15
15
  # the tag on read — it can't be transplanted. Old "v1:" rows (no AAD) keep
16
- # decrypting, so upgrading is a lazy re-encrypt (loam:encryption:rotate writes
16
+ # decrypting, so upgrading is a lazy re-encrypt (open_loam:encryption:rotate writes
17
17
  # v2), never a data migration. The AAD is authenticated but NOT secret — it
18
18
  # never conceals anything, it only pins WHERE the ciphertext is allowed to live.
19
19
  module Cipher
@@ -37,7 +37,7 @@ module Loam
37
37
  "#{version}:" + [iv + tag + ciphertext].pack("m0")
38
38
  end
39
39
 
40
- # Decrypt, or raise Loam::Encryption::DecryptionError. The wrong tenant's
40
+ # Decrypt, or raise OpenLoam::Encryption::DecryptionError. The wrong tenant's
41
41
  # key, a tampered blob, a truncated tag, a v2 blob read with the WRONG (or
42
42
  # missing) AAD, or plain garbage all fail the same loud way — never a
43
43
  # partial or silently-wrong plaintext. A v1 blob carries no AAD, so the
@@ -1,8 +1,8 @@
1
- module Loam
1
+ module OpenLoam
2
2
  module Encryption
3
3
  # The seam a real KMS plugs into. A provider turns (scope, purpose) into a
4
4
  # 32-byte data key; swap the default for a Vault/AWS-KMS-backed provider via
5
- # `Loam::Encryption.key_provider = MyKmsProvider.new` and NO call site
5
+ # `OpenLoam::Encryption.key_provider = MyKmsProvider.new` and NO call site
6
6
  # changes — Cipher and Encryptable only ever ask for a key.
7
7
  #
8
8
  # `scope` is a namespaced owner string: "tenant/5" for an entity field,
@@ -26,10 +26,26 @@ module Loam
26
26
  KEY_BYTES = 32 # AES-256 and HMAC-SHA256 both take a 32-byte key
27
27
 
28
28
  def data_key(scope:, purpose:)
29
+ derive(scope, purpose, OpenLoam::Encryption.master_key)
30
+ end
31
+
32
+ # The same derivation under the key being rotated away from, so decryption
33
+ # can fall back to it while open_loam:encryption:rotate rewrites rows under
34
+ # the new one. nil when no previous key is configured.
35
+ def previous_data_key(scope:, purpose:)
36
+ previous = OpenLoam::Encryption.previous_master_key
37
+ return nil if previous.nil?
38
+
39
+ derive(scope, purpose, previous)
40
+ end
41
+
42
+ private
43
+
44
+ def derive(scope, purpose, master)
29
45
  raise ArgumentError, "scope is required to derive a key" if scope.nil? || scope.to_s.empty?
30
46
 
31
47
  OpenSSL::KDF.hkdf(
32
- Loam::Encryption.master_key,
48
+ master,
33
49
  salt: SALT,
34
50
  # info binds the key to owner AND purpose. "tenant/5" here reproduces
35
51
  # the pre-scope format exactly, so existing ciphertext still decrypts.
@@ -1,22 +1,22 @@
1
1
  require "openssl"
2
- require "loam/encryption/key_provider"
3
- require "loam/encryption/cipher"
2
+ require "open_loam/encryption/key_provider"
3
+ require "open_loam/encryption/cipher"
4
4
 
5
- module Loam
5
+ module OpenLoam
6
6
  # Field-level encryption at rest, keyed per tenant.
7
7
  #
8
- # The facade the rest of Loam calls: `encrypt`/`decrypt` seal and open a value
8
+ # The facade the rest of OpenLoam calls: `encrypt`/`decrypt` seal and open a value
9
9
  # with the tenant's derived AES-256-GCM key, and `blind_index` computes the
10
10
  # per-tenant HMAC used to find an encrypted field by exact value. Key
11
11
  # derivation is delegated to a pluggable `key_provider` (HKDF by default, a
12
12
  # KMS in production), so this module holds the scheme, not the key material.
13
13
  module Encryption
14
- class Error < Loam::Error; end
14
+ class Error < OpenLoam::Error; end
15
15
 
16
16
  # Raised when a crypto operation is attempted with no master key configured.
17
17
  class MissingMasterKeyError < Error
18
- def initialize(msg = "Loam::Encryption has no master key. Set LOAM_MASTER_KEY (or " \
19
- "`Loam::Encryption.master_key = ...`) to a high-entropy secret, e.g. " \
18
+ def initialize(msg = "OpenLoam::Encryption has no master key. Set OPEN_LOAM_MASTER_KEY (or " \
19
+ "`OpenLoam::Encryption.master_key = ...`) to a high-entropy secret, e.g. " \
20
20
  "`SecureRandom.hex(32)`. NEVER commit it; use ENV or Rails credentials.")
21
21
  super
22
22
  end
@@ -41,19 +41,32 @@ module Loam
41
41
  @master_key = value
42
42
  end
43
43
 
44
+ attr_writer :previous_master_key
45
+
46
+ # The key being rotated AWAY from. Set it alongside the new master and
47
+ # decryption falls back to it, which is what makes rotation possible at
48
+ # all: open_loam:encryption:rotate has to READ every row under the old key
49
+ # before it can rewrite it under the new one. Unset it once the rotation
50
+ # has run everywhere.
51
+ def previous_master_key
52
+ key = defined?(@previous_master_key) ? @previous_master_key : nil
53
+ key ||= ENV["OPEN_LOAM_PREVIOUS_MASTER_KEY"]
54
+ key.presence
55
+ end
56
+
44
57
  def master_key
45
- key = @master_key || ENV["LOAM_MASTER_KEY"]
58
+ key = @master_key || ENV["OPEN_LOAM_MASTER_KEY"]
46
59
  raise MissingMasterKeyError if key.nil? || key.empty?
47
60
  if key.bytesize < MASTER_KEY_MIN_BYTES
48
61
  raise MissingMasterKeyError,
49
- "LOAM_MASTER_KEY is too short (#{key.bytesize} bytes); use at least " \
62
+ "OPEN_LOAM_MASTER_KEY is too short (#{key.bytesize} bytes); use at least " \
50
63
  "#{MASTER_KEY_MIN_BYTES}, e.g. `SecureRandom.hex(32)`."
51
64
  end
52
65
  key
53
66
  end
54
67
 
55
68
  # Tenant-scoped operations — the default for entity fields via
56
- # Loam::Encryptable. nil stays nil (an unset field is not "the empty
69
+ # OpenLoam::Encryptable. nil stays nil (an unset field is not "the empty
57
70
  # string encrypted"); any other value is stringified and sealed.
58
71
  def encrypt(plaintext, tenant_id)
59
72
  encrypt_scoped(plaintext, tenant_scope(tenant_id))
@@ -68,8 +81,8 @@ module Loam
68
81
  # hash) — the accepted trade-off for searchability — but the per-tenant
69
82
  # HMAC key means the same value hashes differently across tenants, so
70
83
  # equality never leaks between them. Only searchable fields get one.
71
- def blind_index(value, tenant_id)
72
- blind_index_scoped(value, tenant_scope(tenant_id))
84
+ def blind_index(value, tenant_id, table: nil, column: nil)
85
+ blind_index_scoped(value, tenant_scope(tenant_id), table: table, column: column)
73
86
  end
74
87
 
75
88
  # Explicit-scope variants, for data owned by something OTHER than a tenant
@@ -82,23 +95,44 @@ module Loam
82
95
 
83
96
  def decrypt_scoped(payload, scope, aad: nil)
84
97
  return nil if payload.nil?
98
+
85
99
  Cipher.open(payload, data_key(scope, :encryption), aad: aad)
100
+ rescue DecryptionError
101
+ # GCM's auth tag makes "wrong key" a loud, unambiguous failure, so
102
+ # falling back is safe: a blob that opens under the previous key really
103
+ # was sealed with it. Writes always use the CURRENT key, so a row is
104
+ # rotated the moment anything saves it.
105
+ previous = previous_data_key(scope, :encryption)
106
+ raise if previous.nil?
107
+
108
+ Cipher.open(payload, previous, aad: aad)
86
109
  end
87
110
 
88
111
  # The Additional Authenticated Data that BINDS a ciphertext to where it
89
112
  # lives — the key scope (tenant/owner) + table + column. Reconstructed
90
113
  # identically on read and write, so a blob moved to a different column,
91
114
  # table, or tenant fails the auth tag. NOT the record id (see
92
- # Loam::Encryptable): the id is unknown at INSERT time, and binding it would
115
+ # OpenLoam::Encryptable): the id is unknown at INSERT time, and binding it would
93
116
  # force an ugly post-insert double-write; record-swap within one
94
117
  # tenant+table+column stays a documented residual.
95
118
  def aad(scope, table, column)
96
119
  "loam-aad:v2:#{scope}:#{table}:#{column}"
97
120
  end
98
121
 
99
- def blind_index_scoped(value, scope)
122
+ # The key is bound to (scope, table, column) for the same reason the
123
+ # ciphertext AAD is. A key scoped only to the tenant makes one value hash
124
+ # identically in every searchable column in that tenant: a dump correlates
125
+ # rows across tables, and anyone who can write one such field gets an
126
+ # equality oracle against columns they cannot read.
127
+ #
128
+ # table/column default to nil so an unbound caller still works — the
129
+ # per-tenant key, which is what OpenLoam::PendingActions wants for an
130
+ # idempotency digest that is not a column at all.
131
+ def blind_index_scoped(value, scope, table: nil, column: nil)
100
132
  return nil if value.nil?
101
- OpenSSL::HMAC.hexdigest("SHA256", data_key(scope, :blind_index), value.to_s)
133
+
134
+ purpose = table && column ? "blind_index/#{table}/#{column}" : :blind_index
135
+ OpenSSL::HMAC.hexdigest("SHA256", data_key(scope, purpose), value.to_s)
102
136
  end
103
137
 
104
138
  private
@@ -120,6 +154,15 @@ module Loam
120
154
 
121
155
  key_provider.data_key(scope: scope, purpose: purpose)
122
156
  end
157
+
158
+ # nil unless a previous master is configured AND the provider supports the
159
+ # fallback — a KMS-backed provider manages its own key versions, so the
160
+ # base KeyProvider answers nil and nothing changes for it.
161
+ def previous_data_key(scope, purpose)
162
+ return nil unless key_provider.respond_to?(:previous_data_key)
163
+
164
+ key_provider.previous_data_key(scope: scope, purpose: purpose)
165
+ end
123
166
  end
124
167
  end
125
168
  end
@@ -0,0 +1,65 @@
1
+ module OpenLoam
2
+ class Engine < ::Rails::Engine
3
+ # Not isolated on purpose: OpenLoam models live under the OpenLoam:: namespace but
4
+ # share the host app's routes/helpers, keeping the prototype surface small.
5
+
6
+ # Webhook dispatch listens to every OpenLoam event. Wired after initialization
7
+ # so the models it queries are loadable, and guarded against subscribing
8
+ # twice (see OpenLoam::Webhooks.subscribe!).
9
+ config.after_initialize do
10
+ OpenLoam::Webhooks.subscribe!
11
+ OpenLoam::DurableEvents.subscribe! # persist + retry durable subscribers (L-706)
12
+ OpenLoam::EventLog.subscribe! # capture every event as queryable history (L-204)
13
+ OpenLoam::BusinessRules.subscribe!
14
+ OpenLoam::Widgets.register_builtins! # the default dashboard widgets
15
+ OpenLoam::Overrides.check! # warn about any stale disable/replace overrides
16
+
17
+ # The durability sweep runs per-tenant on a schedule (materialized into
18
+ # each tenant by OpenLoam::Scheduler.sync_tenant). interval:300 = every 5 min.
19
+ OpenLoam::Scheduler.register(
20
+ key: OpenLoam::DurableEvents::SWEEP_KEY, name: "Event redelivery sweep",
21
+ job_class: "OpenLoam::EventRedeliverySweepJob", schedule: "interval:300", scope: "tenant"
22
+ )
23
+
24
+ # Event-log retention, also per-tenant. interval:86400 = daily; the window
25
+ # itself is OpenLoam.event_log_retention.
26
+ OpenLoam::Scheduler.register(
27
+ key: OpenLoam::EventLog::PRUNE_KEY, name: "Event log prune",
28
+ job_class: "OpenLoam::EventLogPruneJob", schedule: "interval:86400", scope: "tenant"
29
+ )
30
+ end
31
+
32
+ # OpenLoam's Active Record layer — TenantRecord, Auditable, Eventful and the
33
+ # rest — is defined inside ActiveSupport.on_load(:active_record) in
34
+ # lib/open_loam.rb, so those constants exist only once something has referenced
35
+ # ActiveRecord::Base.
36
+ #
37
+ # Zeitwerk eager-loads app/models alphabetically. A host model that inherits
38
+ # from OpenLoam::TenantRecord and sorts ahead of application_record.rb reaches
39
+ # the constant before anything has touched Base, and eager load dies with
40
+ # "uninitialized constant OpenLoam::TenantRecord". Whether an app hits it
41
+ # depends on its model names, so it shows up on a rename rather than on the
42
+ # change that caused it.
43
+ #
44
+ # Referencing Base here fires the hook while load order is still the
45
+ # engine's to decide instead of each host app's. :eager_load! is late enough
46
+ # that every active_record.* initializer has applied its configuration, and
47
+ # it runs in every environment (in development it is a no-op that still
48
+ # executes), so lazily-loaded apps are settled by the same line.
49
+ initializer "open_loam.active_record_layer", before: :eager_load! do
50
+ ActiveRecord::Base
51
+ end
52
+
53
+ # OpenLoam's own UI strings ship under lib/ (the gemspec packages lib/**/*), so
54
+ # register them on the app's I18n load path explicitly rather than relying on
55
+ # the default config/locales path (which the gem does not ship). An app
56
+ # overrides any key with its own config/locales/open_loam.<locale>.yml.
57
+ initializer "open_loam.i18n" do |app|
58
+ app.config.i18n.load_path += Dir[File.expand_path("locales/*.yml", __dir__)]
59
+ end
60
+
61
+ # lib/tasks/open_loam.rake (bin/rails open_loam:sync) is picked up by Rails::Engine's
62
+ # own lib/tasks loading. Loading it again from a `rake_tasks` block here
63
+ # would define the task twice and run its body twice.
64
+ end
65
+ end
@@ -1,14 +1,14 @@
1
- module Loam
1
+ module OpenLoam
2
2
  # Response enrichers: one module attaches a computed block onto ANOTHER
3
3
  # module's entity in admin/API responses, with no foreign-key coupling.
4
4
  #
5
5
  # # billing knows about equipment; equipment knows nothing about billing:
6
- # Loam::Enrichers.register("Equipment", key: "outstanding_balance") do |equipment|
6
+ # OpenLoam::Enrichers.register("Equipment", key: "outstanding_balance") do |equipment|
7
7
  # Invoice.where(equipment_id: equipment.id).sum(:balance)
8
8
  # end
9
9
  #
10
- # Loam::Enrichers.enrich(equipment) # => { "outstanding_balance" => 1200 }
11
- # Loam::Enrichers.enrich_many(equipments) # => { id => { key => value }, ... }
10
+ # OpenLoam::Enrichers.enrich(equipment) # => { "outstanding_balance" => 1200 }
11
+ # OpenLoam::Enrichers.enrich_many(equipments) # => { id => { key => value }, ... }
12
12
  #
13
13
  # Distinct from custom fields: a custom field is STORED on the record; an
14
14
  # enricher is COMPUTED at read time by different (possibly cross-module) code.
@@ -97,7 +97,7 @@ module Loam
97
97
 
98
98
  def warn_failure(enricher, error)
99
99
  Rails.logger&.warn(
100
- "[Loam::Enrichers] #{enricher.entity_type}/#{enricher.key} raised #{error.class}: #{error.message} — key omitted"
100
+ "[OpenLoam::Enrichers] #{enricher.entity_type}/#{enricher.key} raised #{error.class}: #{error.message} — key omitted"
101
101
  )
102
102
  end
103
103
  end
@@ -1,31 +1,31 @@
1
- module Loam
1
+ module OpenLoam
2
2
  class Error < StandardError; end
3
3
 
4
4
  # Raised whenever a tenant-scoped model is touched with no tenant in
5
- # Loam::Current. This is a structural guardrail, not a convention: forgetting
5
+ # OpenLoam::Current. This is a structural guardrail, not a convention: forgetting
6
6
  # the tenant context fails loudly (in tests, before it leaks) instead of
7
7
  # silently returning or writing cross-tenant data.
8
8
  class MissingTenantError < Error
9
- def initialize(msg = "No tenant set in Loam::Current — wrap this call in Loam.as_tenant(tenant) { ... }")
9
+ def initialize(msg = "No tenant set in OpenLoam::Current — wrap this call in OpenLoam.as_tenant(tenant) { ... }")
10
10
  super
11
11
  end
12
12
  end
13
13
 
14
14
  # Raised by admin controllers / callers when a policy check fails, and by a
15
- # Loam::Workflow transition the current actor's role may not perform.
15
+ # OpenLoam::Workflow transition the current actor's role may not perform.
16
16
  class NotAuthorizedError < Error; end
17
17
 
18
- # Raised when a Loam::Workflow transition is attempted from a state it does
18
+ # Raised when a OpenLoam::Workflow transition is attempted from a state it does
19
19
  # not move from ("approve a report that was never submitted"). Like every
20
- # Loam guardrail it fails at the call site rather than writing a state the
20
+ # OpenLoam guardrail it fails at the call site rather than writing a state the
21
21
  # machine says is impossible.
22
22
  class InvalidTransitionError < Error; end
23
23
 
24
24
  # Raised when an event name does not follow the `domain.thing.happened` convention.
25
25
  class InvalidEventNameError < Error; end
26
26
 
27
- # Raised by Loam::CustomFields#custom_field/#set_custom_field when the name
28
- # has no matching Loam::FieldDefinition for this tenant + entity. Fails
27
+ # Raised by OpenLoam::CustomFields#custom_field/#set_custom_field when the name
28
+ # has no matching OpenLoam::FieldDefinition for this tenant + entity. Fails
29
29
  # loudly at the access site rather than silently reading/writing nil.
30
30
  class UnknownCustomFieldError < Error; end
31
31
 
@@ -36,12 +36,22 @@ module Loam
36
36
  class FeatureDisabledError < Error; end
37
37
 
38
38
  # Raised when a filter/sort is attempted on a custom field the current role may
39
- # not read (Loam::CustomFieldIndex) — otherwise a filter would be an inference
39
+ # not read (OpenLoam::CustomFieldIndex) — otherwise a filter would be an inference
40
40
  # oracle on a restricted field. A NotAuthorizedError so admin controllers render
41
41
  # it as 403, like any other field-access denial.
42
42
  class FieldAccessError < NotAuthorizedError; end
43
43
 
44
- # Raised when a Loam::BusinessRule with a `block_transition` action vetoes a
44
+ # Raised when a controller action finishes without having authorized anything
45
+ # (see the `verify_authorized!` guard in the generated base controllers).
46
+ #
47
+ # Deliberately NOT a NotAuthorizedError: that one means "this person may not",
48
+ # and the base controllers render it as a tidy 403. This means "this code
49
+ # forgot to ask", which is a developer bug that must not be dressed up as a
50
+ # refusal — nothing rescues it, so it surfaces as a 500 in dev and a failure
51
+ # in tests.
52
+ class AuthorizationNotPerformedError < Error; end
53
+
54
+ # Raised when a OpenLoam::BusinessRule with a `block_transition` action vetoes a
45
55
  # workflow transition. Distinct from NotAuthorizedError (a role gate) and
46
56
  # InvalidTransitionError (an illegal move): the move is legal and permitted,
47
57
  # but a rule says "not under these conditions".
@@ -1,6 +1,6 @@
1
- module Loam
1
+ module OpenLoam
2
2
  # Scripted evaluation scoring for the golden tasks (L-303). An agent (or a
3
- # human) implements a task against a fresh Loam app; this scores the RESULT
3
+ # human) implements a task against a fresh OpenLoam app; this scores the RESULT
4
4
  # consistently — the test outcome plus the structural invariants — into a
5
5
  # machine-readable scorecard, so runs are comparable over time
6
6
  # (ai/benchmark_runs/). Driving the agent is out of scope; consistent scoring
@@ -0,0 +1,77 @@
1
+ module OpenLoam
2
+ # Captures every published event as a queryable OpenLoam::EventRecord row —
3
+ # the capture half of the event system, where OpenLoam::DurableEvents is the
4
+ # delivery half ("durability is of DELIVERY, not CAPTURE").
5
+ #
6
+ # Capture is ON by default and captures everything but OpenLoam.uncaptured_events.
7
+ # That inverts OpenLoam.broadcast_events on purpose; see ADR 0007 for why, and
8
+ # for why this stayed in-gem rather than adopting Rails Event Store.
9
+ #
10
+ # Capture runs INLINE in the publisher's thread, so a failed insert propagates
11
+ # into whatever published the event. Deliberate — do not rescue it. Swallowing
12
+ # the error re-opens the silent gap this module exists to close.
13
+ module EventLog
14
+ PRUNE_KEY = "open_loam_event_log_prune".freeze
15
+
16
+ class << self
17
+ # Idempotent: subscribing twice would capture every event twice.
18
+ def subscribe!
19
+ @subscription ||= OpenLoam::Events.subscribe_all { |event_name, payload| capture(event_name, payload) }
20
+ end
21
+
22
+ # Exclusion is the only knob, because pattern_matches? has no match-all
23
+ # spelling ("" is an exact match on the empty name). Do not add an
24
+ # allow-list: capture-all has to stay the absence of configuration.
25
+ def capturable?(event_name)
26
+ OpenLoam.uncaptured_events.none? do |pattern|
27
+ next false if OpenLoam::Overrides.disabled?(:uncaptured_events, pattern) # an app can re-enable a default exclusion
28
+
29
+ OpenLoam::Events.pattern_matches?(pattern, event_name)
30
+ end
31
+ end
32
+
33
+ def capture(event_name, payload)
34
+ return unless capturable?(event_name)
35
+
36
+ tenant = OpenLoam::Tenant.find_by(id: payload[:tenant_id])
37
+ return if tenant.nil? # nil-tenant events are not captured (as Webhooks.dispatch / DurableEvents.capture)
38
+
39
+ OpenLoam.as_tenant(tenant) do
40
+ OpenLoam::EventRecord.create!(
41
+ name: event_name.to_s,
42
+ payload: payload.transform_keys(&:to_s),
43
+ occurred_at: Time.current
44
+ )
45
+ end
46
+ end
47
+
48
+ # name_or_prefix takes an exact event name or a trailing-dot domain
49
+ # prefix, the same rule as Events.subscribe. Oldest first.
50
+ def read(name_or_prefix = nil, since: nil, limit: nil)
51
+ scope = OpenLoam::EventRecord.chronological
52
+ scope = scope.matching(name_or_prefix) if name_or_prefix.present?
53
+ scope = scope.where(occurred_at: since..) if since
54
+ scope = scope.limit(limit) if limit
55
+ scope
56
+ end
57
+
58
+ # A re-read of history, NOT a second publish: nothing else on the bus fires
59
+ # and a replayed event is not re-captured. Handlers must be idempotent, and
60
+ # get string payload keys (from the row) where a live subscriber got symbols.
61
+ def replay(name_or_prefix = nil, since: nil, limit: nil, &handler)
62
+ read(name_or_prefix, since: since, limit: limit).each do |record|
63
+ handler.call(record.name, record.payload_hash)
64
+ end
65
+ end
66
+
67
+ # delete_all, NOT destroy_all: rows are readonly once persisted, and
68
+ # destroy on a readonly record raises ActiveRecord::ReadOnlyRecord.
69
+ def prune(now: Time.current)
70
+ retention = OpenLoam.event_log_retention
71
+ return 0 if retention.nil?
72
+
73
+ OpenLoam::EventRecord.where(occurred_at: ...(now - retention)).delete_all
74
+ end
75
+ end
76
+ end
77
+ end