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
@@ -0,0 +1,127 @@
1
+ module Api
2
+ # Base class for every JSON endpoint. Authentication is a bearer token:
3
+ #
4
+ # curl -H "Authorization: Bearer <token>" http://localhost:3000/api/equipment
5
+ #
6
+ # A token identifies one user in one tenant, and `OpenLoam::ApiToken.authenticate`
7
+ # establishes both in OpenLoam::Current before any action runs. From that point
8
+ # on this is ordinary tenant-scoped OpenLoam code: every query is filtered, every
9
+ # write is audited, every policy applies — the API is not a side door.
10
+ #
11
+ # Finding a token is the one lookup that cannot start from a tenant (the
12
+ # token is what reveals the tenant), which is why it lives in the gem rather
13
+ # than here: host app code must never reach across tenants.
14
+ class BaseController < ActionController::API
15
+ before_action :authenticate_api_token!
16
+
17
+ # Same guard as the admin base controller: an endpoint that never authorized
18
+ # anything fails loudly rather than silently serving. Runs after the action,
19
+ # so it catches the omission in tests, not at runtime.
20
+ after_action :verify_authorized!
21
+
22
+ def self.skip_authorization!(reason, **options)
23
+ raise ArgumentError, "skip_authorization! needs a reason" if reason.to_s.strip.empty?
24
+
25
+ skip_after_action :verify_authorized!, **options
26
+ end
27
+
28
+ rescue_from OpenLoam::NotAuthorizedError do
29
+ render json: { error: "forbidden" }, status: :forbidden
30
+ end
31
+
32
+ rescue_from ActiveRecord::RecordNotFound do
33
+ render json: { error: "not_found" }, status: :not_found
34
+ end
35
+
36
+ rescue_from OpenLoam::UnknownCustomFieldError do |error|
37
+ render json: { error: error.message }, status: :unprocessable_entity
38
+ end
39
+
40
+ private
41
+
42
+ def authenticate_api_token!
43
+ @api_token = OpenLoam::ApiToken.authenticate(bearer_token)
44
+
45
+ render json: { error: "unauthorized" }, status: :unauthorized unless @api_token
46
+ end
47
+
48
+ def bearer_token
49
+ request.headers["Authorization"].to_s[/\ABearer (.+)\z/, 1]
50
+ end
51
+
52
+ def current_tenant = OpenLoam::Current.tenant
53
+ def current_actor = OpenLoam::Current.actor
54
+
55
+ def policy_for(record)
56
+ OpenLoam::Policy.for(record)
57
+ end
58
+
59
+ def authorize!(policy, action)
60
+ authorized!
61
+ raise OpenLoam::NotAuthorizedError unless policy.public_send(action)
62
+ end
63
+
64
+ # Marked whether the answer was yes or no — a refusal is still a check.
65
+ def authorized!
66
+ @open_loam_authorized = true
67
+ end
68
+
69
+ def verify_authorized!
70
+ return if @open_loam_authorized
71
+
72
+ raise OpenLoam::AuthorizationNotPerformedError,
73
+ "#{self.class.name}##{action_name} finished without authorizing. Call authorize!, or declare " \
74
+ "`skip_authorization! \"<why>\"` if it is authorized structurally."
75
+ end
76
+
77
+ # The JSON shape of an entity: the columns this actor's role may READ, plus
78
+ # the readable custom fields (which live in the `custom_fields` column that
79
+ # every generated entity carries).
80
+ #
81
+ # The read check is the same one the CSV export and the admin screens apply.
82
+ # Without it the API is a way around every `field readable:` rule in the app:
83
+ # the record is already loaded and `record.attributes` emits everything on it.
84
+ #
85
+ # Encrypted fields (OpenLoam::Encryptable) are returned DECRYPTED — the caller is
86
+ # authenticated, tenant-scoped and policy-gated, exactly like the admin show
87
+ # screen — and their blind-index `<field>_hash` column is dropped, so the
88
+ # equality-leaking hash never goes over the wire.
89
+ def entity_json(record, enrichments: nil)
90
+ policy = policy_for(record)
91
+ json = record.attributes.slice(*policy.readable_fields(record.attribute_names))
92
+
93
+ if record.class.respond_to?(:open_loam_encrypted_attributes)
94
+ record.class.open_loam_encrypted_attributes.each do |name|
95
+ json[name] = record.public_send(name) if json.key?(name)
96
+ json.delete("#{name}_hash")
97
+ end
98
+ end
99
+
100
+ # `custom_fields` is one JSON column, so it is all-or-nothing at the
101
+ # column level — filter its contents per definition instead.
102
+ if json.key?("custom_fields") && record.class.respond_to?(:custom_field_definitions)
103
+ readable = record.class.custom_field_definitions.map(&:name).select { |n| policy.custom_field_readable?(n) }
104
+ json["custom_fields"] = json["custom_fields"].to_h.slice(*readable)
105
+ end
106
+
107
+ # Computed cross-module blocks (OpenLoam::Enrichers), under a separate key so
108
+ # they're never confused with the record's own columns. An index passes the
109
+ # batched result (avoiding N+1); a single show computes it here.
110
+ enrichments = OpenLoam::Enrichers.enrich(record) if enrichments.nil?
111
+ json["enrichments"] = enrichments if enrichments.present?
112
+ json
113
+ end
114
+
115
+ # Same field-level enforcement as the admin screens: only a definition the
116
+ # actor's role may write is assigned. Custom fields ride at the top level
117
+ # of the request body, beside the entity key.
118
+ def assign_custom_fields!(record, params, policy)
119
+ submitted = params[:custom_fields]
120
+ return unless submitted
121
+
122
+ submitted.each do |name, value|
123
+ record.set_custom_field(name, value) if policy.custom_field_writable?(name)
124
+ end
125
+ end
126
+ end
127
+ end
@@ -1,21 +1,21 @@
1
1
  require "test_helper"
2
2
 
3
- # Structural guardrails. These are not feature tests — they enforce Loam's
3
+ # Structural guardrails. These are not feature tests — they enforce OpenLoam's
4
4
  # invariants on the codebase itself. If one of these fails, an agent (or a
5
5
  # human) stepped outside the conventions.
6
- class LoamGuardrailsTest < ActiveSupport::TestCase
7
- # Models that are legitimately not tenant-scoped. Loam::Config holds both
6
+ class OpenLoamGuardrailsTest < ActiveSupport::TestCase
7
+ # Models that are legitimately not tenant-scoped. OpenLoam::Config holds both
8
8
  # global (tenant_id NULL) and per-tenant override rows, so tenancy is a
9
- # nullable column and resolution lives in vetted gem code (Loam::Configs).
10
- # Loam::MfaCredential belongs to the person, who spans tenants — MFA is
9
+ # nullable column and resolution lives in vetted gem code (OpenLoam::Configs).
10
+ # OpenLoam::MfaCredential belongs to the person, who spans tenants — MFA is
11
11
  # verified at login before any tenant is chosen.
12
- TENANCY_ALLOWLIST = %w[ApplicationRecord User Loam::Tenant Loam::Config Loam::MfaCredential Loam::AuthAttempt].freeze
12
+ TENANCY_ALLOWLIST = %w[ApplicationRecord User OpenLoam::Tenant OpenLoam::Config OpenLoam::MfaCredential OpenLoam::AuthAttempt].freeze
13
13
 
14
14
  # Rails' own engine models (storage, rich text, jobs) are framework
15
15
  # plumbing, not business data — they are out of scope for tenant linting.
16
16
  FRAMEWORK_NAMESPACES = %w[ActiveStorage:: ActionText:: ActionMailbox:: SolidQueue:: SolidCache:: SolidCable::].freeze
17
17
 
18
- test "every app model is tenant-scoped (inherits Loam::TenantRecord)" do
18
+ test "every app model is tenant-scoped (inherits OpenLoam::TenantRecord)" do
19
19
  Rails.application.eager_load!
20
20
 
21
21
  offenders = ActiveRecord::Base.descendants.reject do |model|
@@ -23,20 +23,20 @@ class LoamGuardrailsTest < ActiveSupport::TestCase
23
23
  model.name.nil? ||
24
24
  TENANCY_ALLOWLIST.include?(model.name) ||
25
25
  FRAMEWORK_NAMESPACES.any? { |ns| model.name.start_with?(ns) } ||
26
- model <= Loam::TenantRecord
26
+ model <= OpenLoam::TenantRecord
27
27
  end
28
28
 
29
29
  assert_empty offenders,
30
30
  "These models are NOT tenant-scoped: #{offenders.map(&:name).join(', ')}. " \
31
- "Business models must inherit Loam::TenantRecord (use `rails g loam:entity`). " \
31
+ "Business models must inherit OpenLoam::TenantRecord (use `rails g open_loam:entity`). " \
32
32
  "If a model is intentionally global, add it to TENANCY_ALLOWLIST here — in review, on purpose."
33
33
  end
34
34
 
35
35
  test "touching a tenant-scoped model with no tenant context raises" do
36
- Loam::Current.reset
36
+ OpenLoam::Current.reset
37
37
 
38
- assert_raises(Loam::MissingTenantError) { Loam::AuditRecord.count }
39
- assert_raises(Loam::MissingTenantError) { Loam::Membership.first }
38
+ assert_raises(OpenLoam::MissingTenantError) { OpenLoam::AuditRecord.count }
39
+ assert_raises(OpenLoam::MissingTenantError) { OpenLoam::Membership.first }
40
40
  end
41
41
 
42
42
  test "nobody uses .unscoped outside vetted framework code" do
@@ -64,16 +64,16 @@ class LoamGuardrailsTest < ActiveSupport::TestCase
64
64
  "docs/ and link it, rather than raising the budget."
65
65
  end
66
66
 
67
- test "every Loam::FieldDefinition entity_type resolves to a class that uses Loam::CustomFields" do
67
+ test "every OpenLoam::FieldDefinition entity_type resolves to a class that uses OpenLoam::CustomFields" do
68
68
  Rails.application.eager_load!
69
69
 
70
- offenders = Loam::FieldDefinition.unscoped.distinct.pluck(:entity_type).reject do |entity_type|
70
+ offenders = OpenLoam::FieldDefinition.unscoped.distinct.pluck(:entity_type).reject do |entity_type|
71
71
  klass = entity_type.safe_constantize
72
- klass && klass.include?(Loam::CustomFields)
72
+ klass && klass.include?(OpenLoam::CustomFields)
73
73
  end
74
74
 
75
75
  assert_empty offenders,
76
- "These Loam::FieldDefinition entity_type values don't resolve to a class that " \
77
- "`include Loam::CustomFields`: #{offenders.join(', ')}. Likely a typo when the field was created."
76
+ "These OpenLoam::FieldDefinition entity_type values don't resolve to a class that " \
77
+ "`include OpenLoam::CustomFields`: #{offenders.join(', ')}. Likely a typo when the field was created."
78
78
  end
79
79
  end
@@ -0,0 +1,38 @@
1
+ # Runs a CSV import in the background, advancing a OpenLoam::ProgressJob so the admin
2
+ # watches it live (L-915). Carries tenant + actor explicitly, like every OpenLoam job.
3
+ # The per-row summary (created/updated/failed + error rows) is stored on the
4
+ # ProgressJob's `result` for the summary screen.
5
+ #
6
+ # The file arrives as a blob id, never as a job argument. ActiveJob arguments are
7
+ # serialized into the queue backend and echoed in the job log, so passing the CSV
8
+ # itself would persist every row — including columns bound for encrypted fields —
9
+ # in the clear, outside the encryption the destination columns exist to provide.
10
+ class ImportJob < ApplicationJob
11
+ queue_as :default
12
+
13
+ def perform(entity_type:, blob_id:, mapping:, match_key:, tenant_id:, actor_id:)
14
+ tenant = OpenLoam::Tenant.find(tenant_id)
15
+ actor = User.find_by(id: actor_id)
16
+ model = OpenLoam::Import.allowed_model(entity_type)
17
+ blob = ActiveStorage::Blob.find_by(id: blob_id)
18
+ return if blob.nil? # already purged: nothing to import, not an error
19
+
20
+ csv = blob.download.force_encoding("UTF-8")
21
+
22
+ OpenLoam.as_tenant(tenant, actor: actor) do
23
+ total = [ CSV.parse(csv).size - 1, 0 ].max
24
+ progress = OpenLoam::Progress.start(name: "Import #{entity_type}", total: total)
25
+ begin
26
+ result = OpenLoam::Import.run(csv, model: model, mapping: mapping, actor: actor, match_key: match_key, progress: progress)
27
+ progress.update_columns(result: result.to_h)
28
+ progress.complete!
29
+ rescue StandardError => error
30
+ progress.fail!(error.message)
31
+ end
32
+ end
33
+ ensure
34
+ # The upload has done its job; leaving it behind is a second copy of the
35
+ # same data with none of the destination's protections.
36
+ blob&.purge_later
37
+ end
38
+ end
@@ -1,17 +1,17 @@
1
1
  # Public endpoint that receives webhooks FROM external systems at
2
2
  # /webhooks/:token. No session, no bearer token: the HMAC signature is the auth
3
3
  # (ActionController::API, so there is no CSRF to skip). All the verified,
4
- # replay-resistant work lives in Loam::InboundWebhooks.ingest; this controller
4
+ # replay-resistant work lives in OpenLoam::InboundWebhooks.ingest; this controller
5
5
  # only maps its Result to an HTTP status and never leaks tenant context to the
6
6
  # next request on this thread.
7
7
  class InboundWebhooksController < ActionController::API
8
8
  def receive
9
- result = Loam::InboundWebhooks.ingest(
9
+ result = OpenLoam::InboundWebhooks.ingest(
10
10
  token: params[:token], raw_body: request.raw_post, headers: request.headers
11
11
  )
12
- Rails.logger.info("[loam inbound] #{result.status} #{result.reason}") if result.status >= 400
12
+ Rails.logger.info("[open_loam inbound] #{result.status} #{result.reason}") if result.status >= 400
13
13
  head result.status
14
14
  ensure
15
- Loam::Current.reset
15
+ OpenLoam::Current.reset
16
16
  end
17
17
  end
@@ -1,214 +1,226 @@
1
- # Loam configuration, tenant lifecycle hooks, and domain event subscriptions.
1
+ # OpenLoam configuration, tenant lifecycle hooks, and domain event subscriptions.
2
2
 
3
- # Master key for field encryption at rest (Loam::Encryptable). Per-tenant keys
3
+ # Master key for field encryption at rest (OpenLoam::Encryptable). Per-tenant keys
4
4
  # are derived from it via HKDF, so it must be STABLE and SECRET — never commit it.
5
- # Set LOAM_MASTER_KEY in the environment (or wire Rails.application.credentials)
5
+ # Set OPEN_LOAM_MASTER_KEY in the environment (or wire Rails.application.credentials)
6
6
  # to a high-entropy value, e.g. `SecureRandom.hex(32)`. Only apps that actually
7
7
  # `encrypts` a field need it; encryption raises a clear error if it is missing.
8
8
  #
9
- # Loam::Encryption.master_key = ENV.fetch("LOAM_MASTER_KEY")
9
+ # OpenLoam::Encryption.master_key = ENV.fetch("OPEN_LOAM_MASTER_KEY")
10
10
  #
11
- # Rotating the key without re-encrypting orphans existing ciphertext — see
12
- # `bin/rails loam:encryption:rotate[Model,tenant_id]`.
11
+ # To ROTATE: keep the outgoing key in OPEN_LOAM_PREVIOUS_MASTER_KEY while the new
12
+ # one is in OPEN_LOAM_MASTER_KEY. Reads fall back to the old key, writes always use
13
+ # the new one, so `bin/rails open_loam:encryption:rotate[Model,tenant_id]` can walk
14
+ # each tenant's rows. Drop the previous key once every model has been rotated —
15
+ # until then, both keys are live and the old one is still worth protecting.
13
16
 
14
17
  # Roles every tenant of this app is expected to have. A registry read by your
15
- # own seeding/admin code — Loam does not create memberships for you, because
18
+ # own seeding/admin code — OpenLoam does not create memberships for you, because
16
19
  # who gets which role is business logic.
17
- Loam.default_roles = %w[manager employee]
20
+ OpenLoam.default_roles = %w[manager employee]
18
21
 
19
- # Locales that CONTENT translations (Loam::Translatable) may be authored in — the
22
+ # Locales that CONTENT translations (OpenLoam::Translatable) may be authored in — the
20
23
  # admin's language switcher and the per-record Translations screen offer these.
21
24
  # The first is the base/default; the rest overlay onto it. This is for user data
22
- # (a product name), NOT developer UI strings — those stay Rails i18n. Loam ships
23
- # a `loam.*` base locale (English) for its own chrome; the admin switcher sets
24
- # I18n.locale too, so add `config/locales/loam.pl.yml` (etc.) to translate the UI
25
+ # (a product name), NOT developer UI strings — those stay Rails i18n. OpenLoam ships
26
+ # a `open_loam.*` base locale (English) for its own chrome; the admin switcher sets
27
+ # I18n.locale too, so add `config/locales/open_loam.pl.yml` (etc.) to translate the UI
25
28
  # for each locale you list here.
26
- Loam.locales = %w[en]
29
+ OpenLoam.locales = %w[en]
27
30
 
28
- # Observability (Loam::Telemetry). By default Loam wraps its async hot paths
31
+ # Observability (OpenLoam::Telemetry). By default OpenLoam wraps its async hot paths
29
32
  # (scheduler tick, durable event delivery, inbound webhook ingest) in spans that
30
- # emit ActiveSupport::Notifications events ("loam.span.*") — subscribe to those,
33
+ # emit ActiveSupport::Notifications events ("open_loam.span.*") — subscribe to those,
31
34
  # or plug a real tracer in here:
32
35
  #
33
- # Loam::Telemetry.backend = ->(name, attributes, work) do
34
- # OpenTelemetry.tracer_provider.tracer("loam").in_span(name, attributes: attributes.transform_keys(&:to_s)) { work.call }
36
+ # OpenLoam::Telemetry.backend = ->(name, attributes, work) do
37
+ # OpenTelemetry.tracer_provider.tracer("open_loam").in_span(name, attributes: attributes.transform_keys(&:to_s)) { work.call }
35
38
  # end
36
39
 
37
- # Feature-string permissions (Loam::Permissions) — a finer capability layer under
40
+ # Feature-string permissions (OpenLoam::Permissions) — a finer capability layer under
38
41
  # the coarse role. Deny-by-default; `*` grants everything, a trailing `.*` is a
39
- # prefix ("equipment.*"). Check with `Loam.can?("equipment.edit")`,
42
+ # prefix ("equipment.*"). Check with `OpenLoam.can?("equipment.edit")`,
40
43
  # `require_permission!("...")` in a controller, or the `can?` view helper.
41
- # Orthogonal to roles (Loam::Membership) and field-level policies (Loam::Policy).
44
+ # Orthogonal to roles (OpenLoam::Membership) and field-level policies (OpenLoam::Policy).
42
45
  #
43
- # Loam::Permissions.configure do
46
+ # OpenLoam::Permissions.configure do
44
47
  # role :admin, allow: "*"
45
48
  # role :manager, allow: %w[equipment.* billing.read]
46
49
  # role :clerk, allow: %w[equipment.read]
47
50
  # end
48
51
 
49
- # Customization WITHOUT forking (Loam::Overrides): disable or replace an entry in
50
- # one of Loam's keyed registries (:widgets, :broadcast_events). A stale override
52
+ # Customization WITHOUT forking (OpenLoam::Overrides): disable or replace an entry in
53
+ # one of OpenLoam's keyed registries (:widgets, :broadcast_events). A stale override
51
54
  # (a key that no longer exists) is warned about at boot by `check!`, so a
52
55
  # typo'd override is visible instead of silently doing nothing.
53
56
  #
54
- # Loam::Overrides.disable(:widgets, "open_progress") # drop a built-in widget
55
- # Loam::Overrides.replace(:widgets, "audit_recent") { |actor| { kind: "count", value: 0 } }
56
- # Loam::Overrides.disable(:broadcast_events, "loam.progress.") # stop pushing it over SSE
57
+ # OpenLoam::Overrides.disable(:widgets, "open_progress") # drop a built-in widget
58
+ # OpenLoam::Overrides.replace(:widgets, "audit_recent") { |actor| { kind: "count", value: 0 } }
59
+ # OpenLoam::Overrides.disable(:broadcast_events, "open_loam.progress.") # stop pushing it over SSE
57
60
  #
58
- # BOUNDARY: this is only for Loam's in-gem registries. Override a VIEW, CONTROLLER,
61
+ # BOUNDARY: this is only for OpenLoam's in-gem registries. Override a VIEW, CONTROLLER,
59
62
  # or ROUTE the standard Rails way (create a file at the same path to shadow the
60
63
  # engine's, or `prepend` a module) — not here.
61
64
 
62
- # Events pushed live to the browser over SSE (Loam::EventStream). Default off; the
65
+ # Events pushed live to the browser over SSE (OpenLoam::EventStream). Default off; the
63
66
  # notification pattern is enabled here so the admin bell increments without a
64
67
  # reload. Add patterns (e.g. "billing.") to stream domain events to live widgets —
65
68
  # nothing reaches the browser unless it matches a pattern here (security posture).
66
- Loam.broadcast_events = [ "loam.notification.", "loam.progress." ]
69
+ OpenLoam.broadcast_events = [ "open_loam.notification.", "open_loam.progress." ]
67
70
 
68
- # Response enrichers (Loam::Enrichers): attach a computed block onto ANOTHER
71
+ # Events EXCLUDED from the event log (OpenLoam::EventLog), which otherwise
72
+ # captures everything. Progress ticks are excluded because a bulk import fires
73
+ # one per row — volume without history worth keeping.
74
+ OpenLoam.uncaptured_events = [ "open_loam.progress." ]
75
+
76
+ # How long captured events are kept before the daily per-tenant prune. nil keeps
77
+ # everything, which means an unbounded table.
78
+ OpenLoam.event_log_retention = 90.days
79
+
80
+ # Response enrichers (OpenLoam::Enrichers): attach a computed block onto ANOTHER
69
81
  # module's entity in admin/API responses, with no foreign-key coupling — billing
70
82
  # can annotate an Equipment without Equipment knowing billing exists. Pass a
71
83
  # `batch:` resolver (array -> { id => value }) so an index stays one query, not N.
72
84
  #
73
- # Loam::Enrichers.register("Equipment", key: "outstanding_balance", batch: ->(equipments) do
85
+ # OpenLoam::Enrichers.register("Equipment", key: "outstanding_balance", batch: ->(equipments) do
74
86
  # totals = Invoice.where(equipment_id: equipments.map(&:id)).group(:equipment_id).sum(:balance)
75
87
  # equipments.map(&:id).index_with { |id| totals.fetch(id, 0) }
76
88
  # end)
77
89
 
78
- # Search backend (Loam::Search). The default is a substring LIKE — portable and
90
+ # Search backend (OpenLoam::Search). The default is a substring LIKE — portable and
79
91
  # zero-setup. Opt into the word-level TokenDriver (also portable, no external
80
- # service: it keeps a normalized-token index in loam_search_tokens) for
92
+ # service: it keeps a normalized-token index in open_loam_search_tokens) for
81
93
  # order-independent, whole-word matching. `searchable_by` and every
82
94
  # `Model.search(q)` call site are UNCHANGED — only this line differs; an external
83
95
  # engine (Meilisearch/Elasticsearch) is a third driver behind the same seam.
84
- # After switching, backfill existing rows once with `bin/rails loam:search:reindex`
96
+ # After switching, backfill existing rows once with `bin/rails open_loam:search:reindex`
85
97
  # (new and updated records index themselves on save).
86
98
  #
87
- # Loam::Search.driver = Loam::Search::TokenDriver
99
+ # OpenLoam::Search.driver = OpenLoam::Search::TokenDriver
88
100
 
89
- # Custom-field read-model index (Loam::CustomFieldIndex). Filtering/sorting on a
90
- # custom field is index-backed (a typed projection in loam_custom_field_values)
101
+ # Custom-field read-model index (OpenLoam::CustomFieldIndex). Filtering/sorting on a
102
+ # custom field is index-backed (a typed projection in open_loam_custom_field_values)
91
103
  # instead of a per-row JSON scan. Records project themselves on save; backfill
92
- # existing data once with `bin/rails loam:index:reindex` (or
93
- # `Loam::CustomFieldIndex.reindex(Model)`). Query with
94
- # `Loam::CustomFieldIndex.filter(Model, field_key, op, value)` — the generated
104
+ # existing data once with `bin/rails open_loam:index:reindex` (or
105
+ # `OpenLoam::CustomFieldIndex.reindex(Model)`). Query with
106
+ # `OpenLoam::CustomFieldIndex.filter(Model, field_key, op, value)` — the generated
95
107
  # entity index routes a `cf_field`/`cf_op`/`cf_value` filter through it.
96
108
 
97
- # Single sign-on (Loam::Sso). OIDC is shipped end-to-end: configure a per-tenant
109
+ # Single sign-on (OpenLoam::Sso). OIDC is shipped end-to-end: configure a per-tenant
98
110
  # provider under /admin/sso_providers (issuer, client_id, client_secret, email
99
111
  # domain, default JIT role). Home-realm discovery routes a user to their tenant's
100
112
  # IdP by email domain; a verified identity is JIT-provisioned or linked to an
101
- # existing User. The client_secret is encrypted at rest, so LOAM_MASTER_KEY must
102
- # be set (same key as the rest of Loam::Encryptable). SAML and SCIM are documented
113
+ # existing User. The client_secret is encrypted at rest, so OPEN_LOAM_MASTER_KEY must
114
+ # be set (same key as the rest of OpenLoam::Encryptable). SAML and SCIM are documented
103
115
  # seams (see docs/_foundation/overview.md). Nothing to register here for real OIDC.
104
116
  #
105
117
  # In tests, inject the offline fake so the flow never hits the network:
106
118
  #
107
- # Loam::Sso.builder = ->(record, redirect_uri) { Loam::Sso::FakeProvider.new(record, redirect_uri: redirect_uri) }
119
+ # OpenLoam::Sso.builder = ->(record, redirect_uri) { OpenLoam::Sso::FakeProvider.new(record, redirect_uri: redirect_uri) }
108
120
 
109
- # Roles that MUST use two-factor auth (Loam::MfaCredential). At login, a user
121
+ # Roles that MUST use two-factor auth (OpenLoam::MfaCredential). At login, a user
110
122
  # whose role in the chosen tenant is on this list and who has not enrolled is
111
- # sent to set MFA up before they can work. Resolved via Loam::Configs, so it can
123
+ # sent to set MFA up before they can work. Resolved via OpenLoam::Configs, so it can
112
124
  # be a global default or a per-tenant override; empty means MFA is optional.
113
125
  #
114
- # Loam::Configs.set("security.mfa_required_roles", ["manager"], scope: :global)
126
+ # OpenLoam::Configs.set("security.mfa_required_roles", ["manager"], scope: :global)
115
127
  #
116
- # Auth rate-limiting / lockout (Loam::AuthThrottle) guards failed password/TOTP/
128
+ # Auth rate-limiting / lockout (OpenLoam::AuthThrottle) guards failed password/TOTP/
117
129
  # sudo attempts. Defaults: lock after 10 failures within a 15-minute window.
118
- # Tune via Loam::Configs (global or per-tenant):
130
+ # Tune via OpenLoam::Configs (global or per-tenant):
119
131
  #
120
- # Loam::Configs.set("security.max_auth_attempts", 5, scope: :global)
121
- # Loam::Configs.set("security.auth_window_minutes", 10, scope: :global)
132
+ # OpenLoam::Configs.set("security.max_auth_attempts", 5, scope: :global)
133
+ # OpenLoam::Configs.set("security.auth_window_minutes", 10, scope: :global)
122
134
  #
123
135
  # Rack::Attack (with a cache store) is the production-scale path across multiple
124
136
  # processes; the built-in DB counter is the portable single-process default.
125
137
 
126
- # App-wide setting defaults (Loam::Configs). A key resolves override → global
138
+ # App-wide setting defaults (OpenLoam::Configs). A key resolves override → global
127
139
  # row → this declared default, so declaring one here needs no migration and no
128
140
  # row — a tenant reads the default until someone overrides it in the admin
129
141
  # Settings screen (/admin/configs). Values keep their type (bool/number/string/
130
142
  # hash), and a per-tenant override never leaks to another tenant.
131
143
  #
132
- # Loam.config_defaults = { "billing.currency" => "USD", "billing.net_terms" => 30 }
144
+ # OpenLoam.config_defaults = { "billing.currency" => "USD", "billing.net_terms" => 30 }
133
145
  #
134
146
  # To change the company-wide baseline at runtime (not per tenant), write a
135
- # global row: `Loam::Configs.set("billing.currency", "EUR", scope: :global)`.
147
+ # global row: `OpenLoam::Configs.set("billing.currency", "EUR", scope: :global)`.
136
148
 
137
- # Feature flags (Loam::Features). Declare known capabilities and their default
149
+ # Feature flags (OpenLoam::Features). Declare known capabilities and their default
138
150
  # state; a flag with no row resolves to its declared default, and managers flip
139
151
  # a tenant's override at /admin/features. A flag gates a CAPABILITY (is this on
140
152
  # for the tenant), orthogonal to roles/policies, which gate a PERSON.
141
153
  #
142
- # Loam.feature_defaults = {
154
+ # OpenLoam.feature_defaults = {
143
155
  # "beta_dashboard" => { default: false, description: "New dashboard, rolled out per tenant." }
144
156
  # }
145
157
  #
146
158
  # In a controller: `require_feature!(:beta_dashboard)` (404s when off); in a
147
159
  # view: `<%% if feature_on?(:beta_dashboard) %>`. Flip app-wide with
148
- # `Loam::Features.enable(:beta_dashboard, scope: :global)`.
160
+ # `OpenLoam::Features.enable(:beta_dashboard, scope: :global)`.
149
161
 
150
162
  # What a brand-new tenant gets for free. Registered at file scope on purpose:
151
163
  # inside `to_prepare` this would re-register on every code reload.
152
- Loam.on_tenant_created do |tenant|
164
+ OpenLoam.on_tenant_created do |tenant|
153
165
  # Seed roles/defaults for a new tenant here. MUST be idempotent —
154
- # `bin/rails loam:sync` re-runs these for every existing tenant, which is how
166
+ # `bin/rails open_loam:sync` re-runs these for every existing tenant, which is how
155
167
  # a default added in a later release reaches tenants that already exist.
156
168
  # Use find_or_create_by!, never create!. The block runs inside
157
- # Loam.as_tenant(tenant), so tenant-scoped writes need no extra ceremony.
169
+ # OpenLoam.as_tenant(tenant), so tenant-scoped writes need no extra ceremony.
158
170
  #
159
- # Baseline managed lookup lists (Loam::Dictionary) — a FieldDefinition of type
171
+ # Baseline managed lookup lists (OpenLoam::Dictionary) — a FieldDefinition of type
160
172
  # "dictionary" can then point a custom field at the key, rendering a select:
161
173
  #
162
- # severity = Loam::Dictionary.find_or_create_by!(key: "damage_severity") { |d| d.name = "Damage severity" }
174
+ # severity = OpenLoam::Dictionary.find_or_create_by!(key: "damage_severity") { |d| d.name = "Damage severity" }
163
175
  # [%w[minor Minor], %w[major Major], %w[critical Critical]].each_with_index do |(value, label), i|
164
176
  # severity.entries.find_or_create_by!(value: value) { |e| e.label = label; e.position = i }
165
177
  # end
166
178
 
167
- # Materialize the registered tenant-scope schedules (see Loam::Scheduler.register
179
+ # Materialize the registered tenant-scope schedules (see OpenLoam::Scheduler.register
168
180
  # below) as rows for this tenant. A no-op until you register one.
169
- Loam::Scheduler.sync_tenant(tenant)
181
+ OpenLoam::Scheduler.sync_tenant(tenant)
170
182
  end
171
183
 
172
- # Recurring jobs (Loam::Scheduler). Register default schedules here at file scope;
184
+ # Recurring jobs (OpenLoam::Scheduler). Register default schedules here at file scope;
173
185
  # on_tenant_created materializes the tenant-scope ones per tenant. A "tenant" job
174
- # is enqueued with `tenant_id:` (establish it with Loam.as_tenant); a "system" job
186
+ # is enqueued with `tenant_id:` (establish it with OpenLoam.as_tenant); a "system" job
175
187
  # runs once with no tenant. job_class MUST be a real ActiveJob (validated — never
176
188
  # arbitrary code). Wire the runner to system cron, every minute:
177
189
  #
178
- # * * * * * cd /path/to/app && bin/rails loam:scheduler:tick
190
+ # * * * * * cd /path/to/app && bin/rails open_loam:scheduler:tick
179
191
  #
180
192
  # The claim is atomic, so running it from several hosts never double-fires a job.
181
193
  #
182
- # Loam::Scheduler.register(key: "nightly_digest", name: "Nightly digest",
194
+ # OpenLoam::Scheduler.register(key: "nightly_digest", name: "Nightly digest",
183
195
  # job_class: "DigestJob", schedule: "0 7 * * *", scope: "tenant")
184
196
  #
185
197
  # Only ALLOWLISTED job classes are schedulable (a registered one, or one listed
186
198
  # here) — so a tenant admin can't schedule ActiveStorage::PurgeJob or a mailer:
187
199
  #
188
- # Loam.schedulable_jobs = %w[DigestJob ReindexJob]
200
+ # OpenLoam.schedulable_jobs = %w[DigestJob ReindexJob]
189
201
 
190
202
  # Domain event subscriptions. Subscribe to a single event or a whole domain
191
203
  # (trailing dot = prefix). Register them here at file scope, not inside
192
204
  # `to_prepare`: subscriptions are global, so a reload would add a second copy
193
205
  # of each one and every event would be handled twice.
194
206
  #
195
- # Loam::Events.subscribe("billing.subscription.renewed") do |name, payload|
207
+ # OpenLoam::Events.subscribe("billing.subscription.renewed") do |name, payload|
196
208
  # BillingMailer.renewal_receipt(payload[:id]).deliver_later
197
209
  # end
198
210
  #
199
- # Loam::Events.subscribe("rental.") do |name, payload|
200
- # Rails.logger.info("[loam event] #{name} #{payload.inspect}")
211
+ # OpenLoam::Events.subscribe("rental.") do |name, payload|
212
+ # Rails.logger.info("[open_loam event] #{name} #{payload.inspect}")
201
213
  # end
202
214
  #
203
215
  # Event -> in-app notification is the intended pattern for telling someone
204
216
  # something. The subscriber runs in the publisher's tenant context, so the
205
217
  # notification lands in the right tenant with no extra ceremony:
206
218
  #
207
- # Loam::Events.subscribe("rental.damage_report.approve") do |_name, payload|
208
- # Loam::Notifications.notify_role(:manager, title: "Damage report approved")
219
+ # OpenLoam::Events.subscribe("rental.damage_report.approve") do |_name, payload|
220
+ # OpenLoam::Notifications.notify_role(:manager, title: "Damage report approved")
209
221
  # end
210
222
 
211
- # Business rules (Loam::BusinessRule) are the admin-editable cousin of the
223
+ # Business rules (OpenLoam::BusinessRule) are the admin-editable cousin of the
212
224
  # subscriptions above: a manager declares, per tenant, WHEN a condition holds on
213
225
  # a triggering record THEN run a fixed vocabulary of safe actions (notify,
214
226
  # emit_event, set_field, block_transition). The condition is DATA — a
@@ -0,0 +1,15 @@
1
+ class CreateOpenLoamApiTokens < ActiveRecord::Migration[<%= ActiveRecord::VERSION::STRING.to_f %>]
2
+ def change
3
+ create_table :open_loam_api_tokens<%= open_loam_id_option %> do |t|
4
+ t.references :tenant, null: false, foreign_key: { to_table: :open_loam_tenants }<%= open_loam_type_option %>
5
+ t.references :user, null: false, foreign_key: true<%= open_loam_type_option %>
6
+ t.string :token_digest, null: false # SHA-256; the plaintext is never stored
7
+ t.string :label
8
+ t.datetime :last_used_at
9
+ t.timestamps
10
+ end
11
+ # Tokens are looked up before a tenant is known, so the uniqueness that
12
+ # matters is global.
13
+ add_index :open_loam_api_tokens, :token_digest, unique: true
14
+ end
15
+ end
@@ -0,0 +1,14 @@
1
+ class CreateOpenLoamAuditRecords < ActiveRecord::Migration[<%= ActiveRecord::VERSION::STRING.to_f %>]
2
+ def change
3
+ create_table :open_loam_audit_records<%= open_loam_id_option %> do |t|
4
+ t.references :tenant, null: false, foreign_key: { to_table: :open_loam_tenants }<%= open_loam_type_option %>
5
+ t.string :auditable_type, null: false
6
+ t.<%= open_loam_key_column_type %> :auditable_id<%= open_loam_key_limit_option %>, null: false
7
+ t.string :action, null: false
8
+ t.<%= open_loam_key_column_type %> :actor_id<%= open_loam_key_limit_option %>
9
+ t.text :changeset
10
+ t.timestamps
11
+ end
12
+ add_index :open_loam_audit_records, %i[tenant_id auditable_type auditable_id]
13
+ end
14
+ end
@@ -1,11 +1,11 @@
1
- class CreateLoamAuthAttempts < ActiveRecord::Migration[<%= ActiveRecord::VERSION::STRING.to_f %>]
1
+ class CreateOpenLoamAuthAttempts < ActiveRecord::Migration[<%= ActiveRecord::VERSION::STRING.to_f %>]
2
2
  def change
3
- create_table :loam_auth_attempts<%= loam_id_option %> do |t|
3
+ create_table :open_loam_auth_attempts<%= open_loam_id_option %> do |t|
4
4
  t.string :identifier, null: false # the submitted login (normalized), NOT tenant-scoped
5
5
  t.string :kind, null: false # password / totp / recovery / sudo
6
6
  t.string :ip # nullable; for an optional per-ip throttle
7
7
  t.timestamps
8
8
  end
9
- add_index :loam_auth_attempts, %i[identifier created_at]
9
+ add_index :open_loam_auth_attempts, %i[identifier created_at]
10
10
  end
11
11
  end