open-loam 0.1.0 → 0.2.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +48 -8
- data/LICENSE +1 -1
- data/README.md +24 -24
- data/app/jobs/{loam → open_loam}/custom_field_reindex_job.rb +6 -6
- data/app/jobs/{loam → open_loam}/event_delivery_job.rb +8 -8
- data/app/jobs/{loam → open_loam}/event_redelivery_sweep_job.rb +4 -4
- data/app/jobs/{loam → open_loam}/webhook_delivery_job.rb +7 -7
- data/app/models/{loam → open_loam}/api_token.rb +7 -7
- data/app/models/{loam → open_loam}/audit_record.rb +3 -3
- data/app/models/{loam → open_loam}/auth_attempt.rb +5 -5
- data/app/models/{loam → open_loam}/business_rule.rb +10 -10
- data/app/models/{loam → open_loam}/business_rule_run.rb +5 -5
- data/app/models/{loam → open_loam}/comment.rb +9 -9
- data/app/models/{loam → open_loam}/config.rb +7 -7
- data/app/models/{loam → open_loam}/custom_field_value.rb +5 -5
- data/app/models/{loam → open_loam}/dashboard_widget.rb +5 -5
- data/app/models/open_loam/dictionary.rb +20 -0
- data/app/models/{loam → open_loam}/dictionary_entry.rb +7 -7
- data/app/models/{loam → open_loam}/event_delivery.rb +4 -4
- data/app/models/{loam → open_loam}/field_definition.rb +8 -8
- data/app/models/{loam → open_loam}/inbound_webhook_delivery.rb +4 -4
- data/app/models/{loam → open_loam}/inbound_webhook_source.rb +13 -13
- data/app/models/{loam → open_loam}/membership.rb +6 -6
- data/app/models/{loam → open_loam}/mfa_credential.rb +8 -8
- data/app/models/{loam → open_loam}/notification.rb +10 -10
- data/app/models/{loam → open_loam}/pending_action.rb +23 -23
- data/app/models/{loam → open_loam}/perspective.rb +5 -5
- data/app/models/{loam → open_loam}/progress_job.rb +7 -7
- data/app/models/{loam → open_loam}/record_lock.rb +5 -5
- data/app/models/{loam → open_loam}/scheduled_job.rb +7 -7
- data/app/models/open_loam/search_token.rb +9 -0
- data/app/models/{loam → open_loam}/sso_identity.rb +4 -4
- data/app/models/{loam → open_loam}/sso_provider.rb +8 -8
- data/app/models/open_loam/tenant.rb +20 -0
- data/app/models/{loam → open_loam}/translation.rb +5 -5
- data/app/models/{loam → open_loam}/webhook_endpoint.rb +6 -6
- data/app/views/{loam → open_loam}/attachments/_attachments.html.erb +1 -1
- data/app/views/{loam → open_loam}/comments/_comments.html.erb +1 -1
- data/app/views/{loam → open_loam}/custom_fields/_fields.html.erb +2 -2
- data/lib/generators/{loam → open_loam}/entity/entity_generator.rb +9 -9
- data/lib/generators/{loam → open_loam}/entity/templates/api_controller.rb +1 -1
- data/lib/generators/{loam → open_loam}/entity/templates/controller.rb +21 -21
- data/lib/generators/{loam → open_loam}/entity/templates/entity_test.rb +12 -12
- data/lib/generators/{loam → open_loam}/entity/templates/migration.rb +5 -5
- data/lib/generators/{loam → open_loam}/entity/templates/model.rb +10 -10
- data/lib/generators/{loam → open_loam}/entity/templates/policy.rb +1 -1
- data/lib/generators/{loam → open_loam}/entity/templates/views/_form.html.erb +4 -4
- data/lib/generators/{loam → open_loam}/entity/templates/views/deleted.html.erb +7 -7
- data/lib/generators/open_loam/entity/templates/views/edit.html.erb +3 -0
- data/lib/generators/open_loam/entity/templates/views/index.html.erb +90 -0
- data/lib/generators/open_loam/entity/templates/views/new.html.erb +3 -0
- data/lib/generators/open_loam/entity/templates/views/show.html.erb +36 -0
- data/lib/generators/{loam → open_loam}/install/install_generator.rb +47 -47
- data/lib/generators/open_loam/install/templates/AGENTS.md +341 -0
- data/lib/generators/{loam → open_loam}/install/templates/admin/api_docs_controller.rb +2 -2
- data/lib/generators/{loam → open_loam}/install/templates/admin/api_docs_index.html.erb +1 -1
- data/lib/generators/{loam → open_loam}/install/templates/admin/api_tokens_controller.rb +1 -1
- data/lib/generators/{loam → open_loam}/install/templates/admin/base_controller.rb +38 -38
- data/lib/generators/{loam → open_loam}/install/templates/admin/business_rules_controller.rb +7 -7
- data/lib/generators/{loam → open_loam}/install/templates/admin/business_rules_form.html.erb +2 -2
- data/lib/generators/{loam → open_loam}/install/templates/admin/comments_controller.rb +4 -4
- data/lib/generators/{loam → open_loam}/install/templates/admin/configs_controller.rb +9 -9
- data/lib/generators/open_loam/install/templates/admin/dashboard_controller.rb +10 -0
- data/lib/generators/{loam → open_loam}/install/templates/admin/dashboard_widgets_controller.rb +5 -5
- data/lib/generators/{loam → open_loam}/install/templates/admin/dictionaries_controller.rb +5 -5
- data/lib/generators/{loam → open_loam}/install/templates/admin/dictionaries_form.html.erb +1 -1
- data/lib/generators/{loam → open_loam}/install/templates/admin/dictionary_entries_controller.rb +2 -2
- data/lib/generators/{loam → open_loam}/install/templates/admin/event_deliveries_controller.rb +5 -5
- data/lib/generators/{loam → open_loam}/install/templates/admin/event_deliveries_index.html.erb +1 -1
- data/lib/generators/{loam → open_loam}/install/templates/admin/events_controller.rb +5 -5
- data/lib/generators/{loam → open_loam}/install/templates/admin/features_controller.rb +9 -9
- data/lib/generators/{loam → open_loam}/install/templates/admin/field_definitions_controller.rb +5 -5
- data/lib/generators/{loam → open_loam}/install/templates/admin/field_definitions_new.html.erb +2 -2
- data/lib/generators/{loam → open_loam}/install/templates/admin/history_controller.rb +7 -7
- data/lib/generators/{loam → open_loam}/install/templates/admin/history_index.html.erb +1 -1
- data/lib/generators/{loam → open_loam}/install/templates/admin/imports_controller.rb +12 -12
- data/lib/generators/{loam → open_loam}/install/templates/admin/inbound_webhook_sources_controller.rb +6 -6
- data/lib/generators/{loam → open_loam}/install/templates/admin/inbound_webhook_sources_new.html.erb +2 -2
- data/lib/generators/open_loam/install/templates/admin/layout.html.erb +76 -0
- data/lib/generators/{loam → open_loam}/install/templates/admin/mfa_controller.rb +6 -6
- data/lib/generators/{loam → open_loam}/install/templates/admin/notifications_controller.rb +3 -3
- data/lib/generators/{loam → open_loam}/install/templates/admin/overrides_controller.rb +3 -3
- data/lib/generators/{loam → open_loam}/install/templates/admin/overrides_index.html.erb +2 -2
- data/lib/generators/{loam → open_loam}/install/templates/admin/pending_actions_controller.rb +3 -3
- data/lib/generators/{loam → open_loam}/install/templates/admin/perspectives_controller.rb +7 -7
- data/lib/generators/{loam → open_loam}/install/templates/admin/perspectives_index.html.erb +1 -1
- data/lib/generators/open_loam/install/templates/admin/progress_jobs_controller.rb +19 -0
- data/lib/generators/{loam → open_loam}/install/templates/admin/progress_jobs_index.html.erb +1 -1
- data/lib/generators/{loam → open_loam}/install/templates/admin/record_locks_controller.rb +4 -4
- data/lib/generators/{loam → open_loam}/install/templates/admin/scheduled_jobs_controller.rb +7 -7
- data/lib/generators/{loam → open_loam}/install/templates/admin/scheduled_jobs_form.html.erb +1 -1
- data/lib/generators/{loam → open_loam}/install/templates/admin/scheduled_jobs_index.html.erb +1 -1
- data/lib/generators/{loam → open_loam}/install/templates/admin/search_controller.rb +5 -5
- data/lib/generators/{loam → open_loam}/install/templates/admin/sessions_controller.rb +32 -32
- data/lib/generators/{loam → open_loam}/install/templates/admin/sso_providers_controller.rb +6 -6
- data/lib/generators/{loam → open_loam}/install/templates/admin/sso_providers_form.html.erb +2 -2
- data/lib/generators/{loam → open_loam}/install/templates/admin/sudo_controller.rb +5 -5
- data/lib/generators/{loam → open_loam}/install/templates/admin/sudo_new.html.erb +1 -1
- data/lib/generators/{loam → open_loam}/install/templates/admin/translations_controller.rb +7 -7
- data/lib/generators/{loam → open_loam}/install/templates/admin/translations_index.html.erb +3 -3
- data/lib/generators/{loam → open_loam}/install/templates/admin/webhook_endpoints_controller.rb +4 -4
- data/lib/generators/{loam → open_loam}/install/templates/admin/webhook_endpoints_index.html.erb +1 -1
- data/lib/generators/{loam → open_loam}/install/templates/admin.css +1 -1
- data/lib/generators/{loam → open_loam}/install/templates/api_base_controller.rb +15 -15
- data/lib/generators/{loam → open_loam}/install/templates/guardrails_test.rb +17 -17
- data/lib/generators/{loam → open_loam}/install/templates/import_job.rb +7 -7
- data/lib/generators/{loam → open_loam}/install/templates/inbound_webhooks_controller.rb +4 -4
- data/lib/generators/{loam → open_loam}/install/templates/initializer.rb +74 -74
- data/lib/generators/open_loam/install/templates/migrations/create_open_loam_api_tokens.rb +15 -0
- data/lib/generators/open_loam/install/templates/migrations/create_open_loam_audit_records.rb +14 -0
- 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
- 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
- data/lib/generators/open_loam/install/templates/migrations/create_open_loam_comments.rb +13 -0
- data/lib/generators/{loam/install/templates/migrations/create_loam_configs.rb → open_loam/install/templates/migrations/create_open_loam_configs.rb} +7 -7
- data/lib/generators/open_loam/install/templates/migrations/create_open_loam_custom_field_values.rb +19 -0
- data/lib/generators/open_loam/install/templates/migrations/create_open_loam_dashboard_widgets.rb +12 -0
- data/lib/generators/open_loam/install/templates/migrations/create_open_loam_dictionaries.rb +11 -0
- 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
- 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
- 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
- data/lib/generators/{loam/install/templates/migrations/create_loam_inbound_webhooks.rb → open_loam/install/templates/migrations/create_open_loam_inbound_webhooks.rb} +9 -9
- data/lib/generators/open_loam/install/templates/migrations/create_open_loam_memberships.rb +11 -0
- 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
- data/lib/generators/open_loam/install/templates/migrations/create_open_loam_notifications.rb +15 -0
- 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
- data/lib/generators/{loam/install/templates/migrations/create_loam_perspectives.rb → open_loam/install/templates/migrations/create_open_loam_perspectives.rb} +5 -5
- 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
- data/lib/generators/open_loam/install/templates/migrations/create_open_loam_record_locks.rb +16 -0
- 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
- data/lib/generators/open_loam/install/templates/migrations/create_open_loam_search_tokens.rb +18 -0
- data/lib/generators/{loam/install/templates/migrations/create_loam_sso_providers.rb → open_loam/install/templates/migrations/create_open_loam_sso_providers.rb} +12 -12
- data/lib/generators/open_loam/install/templates/migrations/create_open_loam_tenants.rb +9 -0
- data/lib/generators/open_loam/install/templates/migrations/create_open_loam_translations.rb +15 -0
- data/lib/generators/open_loam/install/templates/migrations/create_open_loam_webhook_endpoints.rb +13 -0
- data/lib/generators/{loam → open_loam}/install/templates/migrations/create_users.rb +1 -1
- data/lib/generators/{loam → open_loam}/install/templates/user.rb +5 -5
- data/lib/generators/{loam → open_loam}/primary_key_options.rb +23 -23
- data/lib/open-loam.rb +2 -2
- data/lib/{loam → open_loam}/attachable.rb +4 -4
- data/lib/{loam → open_loam}/auditable.rb +23 -23
- data/lib/{loam → open_loam}/auth_throttle.rb +10 -10
- data/lib/{loam → open_loam}/base32.rb +1 -1
- data/lib/{loam → open_loam}/bulk.rb +7 -7
- data/lib/{loam → open_loam}/business_rules/actions.rb +7 -7
- data/lib/{loam → open_loam}/business_rules/condition.rb +5 -5
- data/lib/{loam → open_loam}/business_rules.rb +19 -19
- data/lib/{loam → open_loam}/commentable.rb +6 -6
- data/lib/{loam → open_loam}/configs.rb +19 -19
- data/lib/{loam → open_loam}/cron.rb +2 -2
- data/lib/{loam → open_loam}/csv.rb +2 -2
- data/lib/{loam → open_loam}/current.rb +5 -5
- data/lib/{loam → open_loam}/custom_field_index.rb +23 -23
- data/lib/{loam → open_loam}/custom_fields.rb +17 -17
- data/lib/{loam → open_loam}/dashboard.rb +5 -5
- data/lib/{loam → open_loam}/dictionaries.rb +12 -12
- data/lib/{loam → open_loam}/durable_events.rb +19 -19
- data/lib/{loam → open_loam}/encryptable.rb +33 -33
- data/lib/{loam → open_loam}/encryption/cipher.rb +3 -3
- data/lib/{loam → open_loam}/encryption/key_provider.rb +3 -3
- data/lib/{loam → open_loam}/encryption.rb +11 -11
- data/lib/open_loam/engine.rb +57 -0
- data/lib/{loam → open_loam}/enrichers.rb +5 -5
- data/lib/{loam → open_loam}/errors.rb +10 -10
- data/lib/{loam → open_loam}/eval.rb +2 -2
- data/lib/{loam → open_loam}/event_stream.rb +14 -14
- data/lib/{loam → open_loam}/eventful.rb +12 -12
- data/lib/{loam → open_loam}/events.rb +5 -5
- data/lib/{loam → open_loam}/export.rb +8 -8
- data/lib/{loam → open_loam}/features.rb +15 -15
- data/lib/{loam → open_loam}/generated_key.rb +6 -6
- data/lib/{loam → open_loam}/import.rb +12 -12
- data/lib/{loam → open_loam}/inbound_webhooks.rb +7 -7
- data/lib/{loam → open_loam}/lifecycle.rb +25 -25
- data/lib/{loam/locales/loam.en.yml → open_loam/locales/open_loam.en.yml} +5 -5
- data/lib/{loam → open_loam}/mcp/server.rb +9 -9
- data/lib/{loam → open_loam}/mcp.rb +21 -21
- data/lib/{loam → open_loam}/notifications.rb +5 -5
- data/lib/{loam → open_loam}/open_api.rb +16 -16
- data/lib/{loam → open_loam}/overrides.rb +10 -10
- data/lib/{loam → open_loam}/pending_actions.rb +9 -9
- data/lib/{loam → open_loam}/permissions.rb +8 -8
- data/lib/{loam → open_loam}/perspectives.rb +9 -9
- data/lib/{loam → open_loam}/policy.rb +6 -6
- data/lib/{loam → open_loam}/progress.rb +6 -6
- data/lib/{loam → open_loam}/record_locks.rb +9 -9
- data/lib/{loam → open_loam}/scheduler.rb +20 -20
- data/lib/{loam → open_loam}/search/driver.rb +3 -3
- data/lib/{loam → open_loam}/search/like_driver.rb +5 -5
- data/lib/{loam → open_loam}/search/token_driver.rb +15 -15
- data/lib/{loam → open_loam}/search.rb +5 -5
- data/lib/{loam → open_loam}/searchable.rb +22 -22
- data/lib/{loam → open_loam}/soft_deletable.rb +10 -10
- data/lib/{loam → open_loam}/sso/claims.rb +1 -1
- data/lib/{loam → open_loam}/sso/fake_provider.rb +1 -1
- data/lib/{loam → open_loam}/sso/http_client.rb +3 -3
- data/lib/{loam → open_loam}/sso/oidc_provider.rb +2 -2
- data/lib/{loam → open_loam}/sso.rb +17 -17
- data/lib/{loam → open_loam}/telemetry.rb +6 -6
- data/lib/{loam → open_loam}/tenant_record.rb +10 -10
- data/lib/{loam → open_loam}/test_helpers.rb +4 -4
- data/lib/{loam → open_loam}/totp.rb +3 -3
- data/lib/{loam → open_loam}/translatable.rb +24 -24
- data/lib/{loam → open_loam}/undo.rb +11 -11
- data/lib/open_loam/version.rb +3 -0
- data/lib/{loam → open_loam}/webhooks.rb +4 -4
- data/lib/{loam → open_loam}/widgets.rb +9 -9
- data/lib/{loam → open_loam}/workflow.rb +45 -45
- data/lib/open_loam.rb +122 -0
- data/lib/tasks/{loam.rake → open_loam.rake} +43 -43
- data/lib/tasks/{loam_eval.rake → open_loam_eval.rake} +7 -7
- data/lib/tasks/open_loam_mcp.rake +20 -0
- metadata +242 -242
- data/app/models/loam/dictionary.rb +0 -20
- data/app/models/loam/search_token.rb +0 -9
- data/app/models/loam/tenant.rb +0 -20
- data/lib/generators/loam/entity/templates/views/edit.html.erb +0 -3
- data/lib/generators/loam/entity/templates/views/index.html.erb +0 -90
- data/lib/generators/loam/entity/templates/views/new.html.erb +0 -3
- data/lib/generators/loam/entity/templates/views/show.html.erb +0 -36
- data/lib/generators/loam/install/templates/AGENTS.md +0 -341
- data/lib/generators/loam/install/templates/admin/dashboard_controller.rb +0 -10
- data/lib/generators/loam/install/templates/admin/layout.html.erb +0 -76
- data/lib/generators/loam/install/templates/admin/progress_jobs_controller.rb +0 -19
- data/lib/generators/loam/install/templates/migrations/create_loam_api_tokens.rb +0 -15
- data/lib/generators/loam/install/templates/migrations/create_loam_audit_records.rb +0 -14
- data/lib/generators/loam/install/templates/migrations/create_loam_comments.rb +0 -13
- data/lib/generators/loam/install/templates/migrations/create_loam_custom_field_values.rb +0 -19
- data/lib/generators/loam/install/templates/migrations/create_loam_dashboard_widgets.rb +0 -12
- data/lib/generators/loam/install/templates/migrations/create_loam_dictionaries.rb +0 -11
- data/lib/generators/loam/install/templates/migrations/create_loam_memberships.rb +0 -11
- data/lib/generators/loam/install/templates/migrations/create_loam_notifications.rb +0 -15
- data/lib/generators/loam/install/templates/migrations/create_loam_record_locks.rb +0 -16
- data/lib/generators/loam/install/templates/migrations/create_loam_search_tokens.rb +0 -18
- data/lib/generators/loam/install/templates/migrations/create_loam_tenants.rb +0 -9
- data/lib/generators/loam/install/templates/migrations/create_loam_translations.rb +0 -15
- data/lib/generators/loam/install/templates/migrations/create_loam_webhook_endpoints.rb +0 -13
- data/lib/loam/engine.rb +0 -57
- data/lib/loam/version.rb +0 -3
- data/lib/loam.rb +0 -122
- data/lib/tasks/loam_mcp.rake +0 -20
- /data/lib/generators/{loam → open_loam}/install/templates/admin/api_tokens_index.html.erb +0 -0
- /data/lib/generators/{loam → open_loam}/install/templates/admin/business_rules_edit.html.erb +0 -0
- /data/lib/generators/{loam → open_loam}/install/templates/admin/business_rules_index.html.erb +0 -0
- /data/lib/generators/{loam → open_loam}/install/templates/admin/business_rules_new.html.erb +0 -0
- /data/lib/generators/{loam → open_loam}/install/templates/admin/configs_edit.html.erb +0 -0
- /data/lib/generators/{loam → open_loam}/install/templates/admin/configs_index.html.erb +0 -0
- /data/lib/generators/{loam → open_loam}/install/templates/admin/dashboard_index.html.erb +0 -0
- /data/lib/generators/{loam → open_loam}/install/templates/admin/dashboard_widgets_index.html.erb +0 -0
- /data/lib/generators/{loam → open_loam}/install/templates/admin/dictionaries_edit.html.erb +0 -0
- /data/lib/generators/{loam → open_loam}/install/templates/admin/dictionaries_index.html.erb +0 -0
- /data/lib/generators/{loam → open_loam}/install/templates/admin/dictionaries_new.html.erb +0 -0
- /data/lib/generators/{loam → open_loam}/install/templates/admin/features_index.html.erb +0 -0
- /data/lib/generators/{loam → open_loam}/install/templates/admin/field_definitions_index.html.erb +0 -0
- /data/lib/generators/{loam → open_loam}/install/templates/admin/imports_new.html.erb +0 -0
- /data/lib/generators/{loam → open_loam}/install/templates/admin/imports_preview.html.erb +0 -0
- /data/lib/generators/{loam → open_loam}/install/templates/admin/imports_summary.html.erb +0 -0
- /data/lib/generators/{loam → open_loam}/install/templates/admin/inbound_webhook_sources_index.html.erb +0 -0
- /data/lib/generators/{loam → open_loam}/install/templates/admin/mfa_activated.html.erb +0 -0
- /data/lib/generators/{loam → open_loam}/install/templates/admin/mfa_new.html.erb +0 -0
- /data/lib/generators/{loam → open_loam}/install/templates/admin/mfa_show.html.erb +0 -0
- /data/lib/generators/{loam → open_loam}/install/templates/admin/notifications_index.html.erb +0 -0
- /data/lib/generators/{loam → open_loam}/install/templates/admin/pagination.rb +0 -0
- /data/lib/generators/{loam → open_loam}/install/templates/admin/pending_actions_index.html.erb +0 -0
- /data/lib/generators/{loam → open_loam}/install/templates/admin/scheduled_jobs_edit.html.erb +0 -0
- /data/lib/generators/{loam → open_loam}/install/templates/admin/scheduled_jobs_new.html.erb +0 -0
- /data/lib/generators/{loam → open_loam}/install/templates/admin/search_index.html.erb +0 -0
- /data/lib/generators/{loam → open_loam}/install/templates/admin/sessions_mfa_challenge.html.erb +0 -0
- /data/lib/generators/{loam → open_loam}/install/templates/admin/sessions_new.html.erb +0 -0
- /data/lib/generators/{loam → open_loam}/install/templates/admin/sso_providers_edit.html.erb +0 -0
- /data/lib/generators/{loam → open_loam}/install/templates/admin/sso_providers_index.html.erb +0 -0
- /data/lib/generators/{loam → open_loam}/install/templates/admin/sso_providers_new.html.erb +0 -0
- /data/lib/generators/{loam → open_loam}/install/templates/admin/webhook_endpoints_new.html.erb +0 -0
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
module
|
|
2
|
-
# Durable (persistent) event subscribers — the twin of
|
|
1
|
+
module OpenLoam
|
|
2
|
+
# Durable (persistent) event subscribers — the twin of OpenLoam::Events.subscribe,
|
|
3
3
|
# and the formal contract L-706 pins down.
|
|
4
4
|
#
|
|
5
5
|
# THE CONTRACT
|
|
6
|
-
# * Ephemeral —
|
|
6
|
+
# * Ephemeral — OpenLoam::Events.subscribe(&block): runs INLINE in the publisher's
|
|
7
7
|
# thread, synchronously, best-effort. An exception in the block PROPAGATES
|
|
8
8
|
# into whatever published the event. No persistence, no retry. Right for
|
|
9
9
|
# cheap in-process fan-out (the webhook dispatcher enqueues its own jobs
|
|
10
10
|
# this way).
|
|
11
|
-
# * Persistent —
|
|
12
|
-
#
|
|
11
|
+
# * Persistent — OpenLoam::DurableEvents.register(...): publishing commits a
|
|
12
|
+
# OpenLoam::EventDelivery row in the event's tenant, then hands the handler to a
|
|
13
13
|
# background job. The handler's exception is CONTAINED in the job; delivery
|
|
14
14
|
# is retried with backoff and, past MAX_ATTEMPTS, parked as `dead` for an
|
|
15
15
|
# operator (Admin::EventDeliveriesController).
|
|
@@ -29,10 +29,10 @@ module Loam
|
|
|
29
29
|
MAX_ATTEMPTS = 5
|
|
30
30
|
# Backoff (seconds) indexed by attempt number; the last value is the cap.
|
|
31
31
|
BACKOFF = [ 0, 60, 300, 1800, 7200 ].freeze
|
|
32
|
-
SWEEP_KEY = "
|
|
32
|
+
SWEEP_KEY = "open_loam_event_redelivery_sweep".freeze
|
|
33
33
|
|
|
34
34
|
class << self
|
|
35
|
-
# --- declarative registry (mirrors
|
|
35
|
+
# --- declarative registry (mirrors OpenLoam::Scheduler.register) ---
|
|
36
36
|
|
|
37
37
|
# key: a stable identifier stored on every delivery row it produces.
|
|
38
38
|
# to: an event name ("billing.invoice.paid") or a domain prefix
|
|
@@ -51,7 +51,7 @@ module Loam
|
|
|
51
51
|
end
|
|
52
52
|
|
|
53
53
|
def subscribers_for(event_name)
|
|
54
|
-
registered.select { |entry|
|
|
54
|
+
registered.select { |entry| OpenLoam::Events.pattern_matches?(entry[:pattern], event_name) }
|
|
55
55
|
end
|
|
56
56
|
|
|
57
57
|
def handler_for(key)
|
|
@@ -59,19 +59,19 @@ module Loam
|
|
|
59
59
|
entry && resolve(entry[:handler])
|
|
60
60
|
end
|
|
61
61
|
|
|
62
|
-
# --- wiring (mirrors
|
|
62
|
+
# --- wiring (mirrors OpenLoam::Webhooks.subscribe!) ---
|
|
63
63
|
|
|
64
|
-
# Wired once from
|
|
64
|
+
# Wired once from OpenLoam::Engine. Idempotent: subscribing twice would persist
|
|
65
65
|
# every event twice.
|
|
66
66
|
def subscribe!
|
|
67
|
-
@subscription ||=
|
|
67
|
+
@subscription ||= OpenLoam::Events.subscribe_all { |event_name, payload| capture(event_name, payload) }
|
|
68
68
|
end
|
|
69
69
|
|
|
70
70
|
# Persist a delivery row per matching durable subscriber, in the event's
|
|
71
71
|
# tenant, then nudge a job per row. The ROW is the durable record; the job
|
|
72
72
|
# is only the accelerator (the sweep redelivers rows whose job was lost).
|
|
73
73
|
def capture(event_name, payload)
|
|
74
|
-
tenant =
|
|
74
|
+
tenant = OpenLoam::Tenant.find_by(id: payload[:tenant_id])
|
|
75
75
|
return if tenant.nil? # nil-tenant events are not durably delivered (as Webhooks.dispatch)
|
|
76
76
|
|
|
77
77
|
matches = subscribers_for(event_name)
|
|
@@ -81,18 +81,18 @@ module Loam
|
|
|
81
81
|
# scalars by convention, never records (same rule as the webhook path).
|
|
82
82
|
deliverable = payload.transform_keys(&:to_s)
|
|
83
83
|
|
|
84
|
-
|
|
84
|
+
OpenLoam.as_tenant(tenant) do
|
|
85
85
|
matches.each do |sub|
|
|
86
|
-
delivery =
|
|
86
|
+
delivery = OpenLoam::EventDelivery.create!(
|
|
87
87
|
subscriber_key: sub[:key], event_name: event_name.to_s,
|
|
88
88
|
payload: deliverable, status: "pending", attempts: 0
|
|
89
89
|
)
|
|
90
|
-
|
|
90
|
+
OpenLoam::EventDeliveryJob.perform_later(tenant.id, delivery.id)
|
|
91
91
|
end
|
|
92
92
|
end
|
|
93
93
|
end
|
|
94
94
|
|
|
95
|
-
# --- delivery (called by
|
|
95
|
+
# --- delivery (called by OpenLoam::EventDeliveryJob) ---
|
|
96
96
|
|
|
97
97
|
# Run one delivery and advance the row's state. Manual, ROW-STATE retries —
|
|
98
98
|
# deliberately NOT ActiveJob retry_on, which keeps retry state in the queue
|
|
@@ -102,7 +102,7 @@ module Loam
|
|
|
102
102
|
return unless delivery.status == "pending"
|
|
103
103
|
return if delivery.next_attempt_at && delivery.next_attempt_at > now # duplicate nudge, backoff not elapsed
|
|
104
104
|
|
|
105
|
-
|
|
105
|
+
OpenLoam::Telemetry.span("durable_event_delivery",
|
|
106
106
|
subscriber_key: delivery.subscriber_key, event_name: delivery.event_name) do
|
|
107
107
|
run_delivery(delivery, now)
|
|
108
108
|
end
|
|
@@ -136,8 +136,8 @@ module Loam
|
|
|
136
136
|
# no cross-tenant scan is needed.
|
|
137
137
|
def redeliver_stuck(now: Time.current, limit: 500)
|
|
138
138
|
count = 0
|
|
139
|
-
|
|
140
|
-
|
|
139
|
+
OpenLoam::EventDelivery.due(now).limit(limit).find_each do |delivery|
|
|
140
|
+
OpenLoam::EventDeliveryJob.perform_later(delivery.tenant_id, delivery.id)
|
|
141
141
|
count += 1
|
|
142
142
|
end
|
|
143
143
|
count
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
module
|
|
1
|
+
module OpenLoam
|
|
2
2
|
# Field-level encryption at rest, keyed per tenant. Declare it on a model:
|
|
3
3
|
#
|
|
4
|
-
# class Patient <
|
|
5
|
-
# include
|
|
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 (
|
|
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 :
|
|
28
|
-
class_attribute :
|
|
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
|
-
#
|
|
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
|
|
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?(:
|
|
48
|
-
raise
|
|
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,41 @@ 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?(:
|
|
58
|
-
raise
|
|
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.
|
|
64
|
-
self.
|
|
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
|
|
67
|
-
|
|
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
|
|
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
|
|
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 =
|
|
79
|
-
aad =
|
|
80
|
-
|
|
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 =
|
|
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 =
|
|
88
|
-
write_attribute(name,
|
|
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,
|
|
91
|
+
write_attribute(hash_column, OpenLoam::Encryption.blind_index_scoped(value, resolved)) if searchable
|
|
92
92
|
end
|
|
93
93
|
end
|
|
94
94
|
end
|
|
@@ -97,11 +97,11 @@ module Loam
|
|
|
97
97
|
# deliberately shadow Active Record's dynamic `find_by_<attr>`: the obvious
|
|
98
98
|
# call must hash-and-compare, never match a plaintext query against the
|
|
99
99
|
# ciphertext column (which would silently find nothing).
|
|
100
|
-
def
|
|
100
|
+
def define_open_loam_blind_index_finder(name)
|
|
101
101
|
hash_column = "#{name}_hash"
|
|
102
102
|
|
|
103
103
|
define_singleton_method("where_#{name}") do |value|
|
|
104
|
-
where(hash_column =>
|
|
104
|
+
where(hash_column => OpenLoam::Encryption.blind_index(value, OpenLoam.tenant!.id))
|
|
105
105
|
end
|
|
106
106
|
|
|
107
107
|
define_singleton_method("find_by_#{name}") do |value|
|
|
@@ -117,11 +117,11 @@ module Loam
|
|
|
117
117
|
# same safety property as an entity write); a Proc computes it from the
|
|
118
118
|
# record. A blank or `.../`-terminated result (e.g. a nil user_id) raises
|
|
119
119
|
# rather than deriving a degenerate shared key.
|
|
120
|
-
def
|
|
121
|
-
resolved = scope == :tenant ? "tenant/#{
|
|
120
|
+
def open_loam_encryption_scope(scope)
|
|
121
|
+
resolved = scope == :tenant ? "tenant/#{OpenLoam.tenant!.id}" : scope.call(self).to_s
|
|
122
122
|
|
|
123
123
|
if resolved.strip.empty? || resolved.end_with?("/")
|
|
124
|
-
raise
|
|
124
|
+
raise OpenLoam::Encryption::Error, "#{self.class}: cannot derive an encryption key from a blank scope (#{resolved.inspect})"
|
|
125
125
|
end
|
|
126
126
|
resolved
|
|
127
127
|
end
|
|
@@ -134,9 +134,9 @@ module Loam
|
|
|
134
134
|
# version tag lets old and new ciphertext coexist while this runs across a
|
|
135
135
|
# tenant's records, so rotation is a lazy re-encrypt, not a stop-the-world
|
|
136
136
|
# migration. Writes an ordinary audited "[encrypted]" update — see
|
|
137
|
-
# lib/tasks/
|
|
138
|
-
def
|
|
139
|
-
self.class.
|
|
137
|
+
# lib/tasks/open_loam.rake (open_loam:encryption:rotate).
|
|
138
|
+
def open_loam_reencrypt!
|
|
139
|
+
self.class.open_loam_encrypted_attributes.each do |name|
|
|
140
140
|
public_send("#{name}=", public_send(name))
|
|
141
141
|
end
|
|
142
142
|
save!
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
module
|
|
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 (
|
|
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
|
|
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
|
|
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
|
-
# `
|
|
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,
|
|
@@ -29,7 +29,7 @@ module Loam
|
|
|
29
29
|
raise ArgumentError, "scope is required to derive a key" if scope.nil? || scope.to_s.empty?
|
|
30
30
|
|
|
31
31
|
OpenSSL::KDF.hkdf(
|
|
32
|
-
|
|
32
|
+
OpenLoam::Encryption.master_key,
|
|
33
33
|
salt: SALT,
|
|
34
34
|
# info binds the key to owner AND purpose. "tenant/5" here reproduces
|
|
35
35
|
# the pre-scope format exactly, so existing ciphertext still decrypts.
|
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
require "openssl"
|
|
2
|
-
require "
|
|
3
|
-
require "
|
|
2
|
+
require "open_loam/encryption/key_provider"
|
|
3
|
+
require "open_loam/encryption/cipher"
|
|
4
4
|
|
|
5
|
-
module
|
|
5
|
+
module OpenLoam
|
|
6
6
|
# Field-level encryption at rest, keyed per tenant.
|
|
7
7
|
#
|
|
8
|
-
# The facade the rest of
|
|
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 <
|
|
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 = "
|
|
19
|
-
"`
|
|
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
|
|
@@ -42,18 +42,18 @@ module Loam
|
|
|
42
42
|
end
|
|
43
43
|
|
|
44
44
|
def master_key
|
|
45
|
-
key = @master_key || ENV["
|
|
45
|
+
key = @master_key || ENV["OPEN_LOAM_MASTER_KEY"]
|
|
46
46
|
raise MissingMasterKeyError if key.nil? || key.empty?
|
|
47
47
|
if key.bytesize < MASTER_KEY_MIN_BYTES
|
|
48
48
|
raise MissingMasterKeyError,
|
|
49
|
-
"
|
|
49
|
+
"OPEN_LOAM_MASTER_KEY is too short (#{key.bytesize} bytes); use at least " \
|
|
50
50
|
"#{MASTER_KEY_MIN_BYTES}, e.g. `SecureRandom.hex(32)`."
|
|
51
51
|
end
|
|
52
52
|
key
|
|
53
53
|
end
|
|
54
54
|
|
|
55
55
|
# Tenant-scoped operations — the default for entity fields via
|
|
56
|
-
#
|
|
56
|
+
# OpenLoam::Encryptable. nil stays nil (an unset field is not "the empty
|
|
57
57
|
# string encrypted"); any other value is stringified and sealed.
|
|
58
58
|
def encrypt(plaintext, tenant_id)
|
|
59
59
|
encrypt_scoped(plaintext, tenant_scope(tenant_id))
|
|
@@ -89,7 +89,7 @@ module Loam
|
|
|
89
89
|
# lives — the key scope (tenant/owner) + table + column. Reconstructed
|
|
90
90
|
# identically on read and write, so a blob moved to a different column,
|
|
91
91
|
# table, or tenant fails the auth tag. NOT the record id (see
|
|
92
|
-
#
|
|
92
|
+
# OpenLoam::Encryptable): the id is unknown at INSERT time, and binding it would
|
|
93
93
|
# force an ugly post-insert double-write; record-swap within one
|
|
94
94
|
# tenant+table+column stays a documented residual.
|
|
95
95
|
def aad(scope, table, column)
|
|
@@ -0,0 +1,57 @@
|
|
|
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::BusinessRules.subscribe!
|
|
13
|
+
OpenLoam::Widgets.register_builtins! # the default dashboard widgets
|
|
14
|
+
OpenLoam::Overrides.check! # warn about any stale disable/replace overrides
|
|
15
|
+
|
|
16
|
+
# The durability sweep runs per-tenant on a schedule (materialized into
|
|
17
|
+
# each tenant by OpenLoam::Scheduler.sync_tenant). interval:300 = every 5 min.
|
|
18
|
+
OpenLoam::Scheduler.register(
|
|
19
|
+
key: OpenLoam::DurableEvents::SWEEP_KEY, name: "Event redelivery sweep",
|
|
20
|
+
job_class: "OpenLoam::EventRedeliverySweepJob", schedule: "interval:300", scope: "tenant"
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# OpenLoam's Active Record layer — TenantRecord, Auditable, Eventful and the
|
|
25
|
+
# rest — is defined inside ActiveSupport.on_load(:active_record) in
|
|
26
|
+
# lib/open_loam.rb, so those constants exist only once something has referenced
|
|
27
|
+
# ActiveRecord::Base.
|
|
28
|
+
#
|
|
29
|
+
# Zeitwerk eager-loads app/models alphabetically. A host model that inherits
|
|
30
|
+
# from OpenLoam::TenantRecord and sorts ahead of application_record.rb reaches
|
|
31
|
+
# the constant before anything has touched Base, and eager load dies with
|
|
32
|
+
# "uninitialized constant OpenLoam::TenantRecord". Whether an app hits it
|
|
33
|
+
# depends on its model names, so it shows up on a rename rather than on the
|
|
34
|
+
# change that caused it.
|
|
35
|
+
#
|
|
36
|
+
# Referencing Base here fires the hook while load order is still the
|
|
37
|
+
# engine's to decide instead of each host app's. :eager_load! is late enough
|
|
38
|
+
# that every active_record.* initializer has applied its configuration, and
|
|
39
|
+
# it runs in every environment (in development it is a no-op that still
|
|
40
|
+
# executes), so lazily-loaded apps are settled by the same line.
|
|
41
|
+
initializer "open_loam.active_record_layer", before: :eager_load! do
|
|
42
|
+
ActiveRecord::Base
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# OpenLoam's own UI strings ship under lib/ (the gemspec packages lib/**/*), so
|
|
46
|
+
# register them on the app's I18n load path explicitly rather than relying on
|
|
47
|
+
# the default config/locales path (which the gem does not ship). An app
|
|
48
|
+
# overrides any key with its own config/locales/open_loam.<locale>.yml.
|
|
49
|
+
initializer "open_loam.i18n" do |app|
|
|
50
|
+
app.config.i18n.load_path += Dir[File.expand_path("locales/*.yml", __dir__)]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# lib/tasks/open_loam.rake (bin/rails open_loam:sync) is picked up by Rails::Engine's
|
|
54
|
+
# own lib/tasks loading. Loading it again from a `rake_tasks` block here
|
|
55
|
+
# would define the task twice and run its body twice.
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
module
|
|
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
|
-
#
|
|
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
|
-
#
|
|
11
|
-
#
|
|
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
|
-
"[
|
|
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
|
|
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
|
-
#
|
|
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
|
|
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
|
-
#
|
|
15
|
+
# OpenLoam::Workflow transition the current actor's role may not perform.
|
|
16
16
|
class NotAuthorizedError < Error; end
|
|
17
17
|
|
|
18
|
-
# Raised when a
|
|
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
|
-
#
|
|
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
|
|
28
|
-
# has no matching
|
|
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,12 @@ 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 (
|
|
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
|
|
44
|
+
# Raised when a OpenLoam::BusinessRule with a `block_transition` action vetoes a
|
|
45
45
|
# workflow transition. Distinct from NotAuthorizedError (a role gate) and
|
|
46
46
|
# InvalidTransitionError (an illegal move): the move is legal and permitted,
|
|
47
47
|
# but a rule says "not under these conditions".
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
module
|
|
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
|
|
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
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
module
|
|
2
|
-
# The real-time bridge: pushes selected
|
|
1
|
+
module OpenLoam
|
|
2
|
+
# The real-time bridge: pushes selected OpenLoam events to a browser over
|
|
3
3
|
# Server-Sent Events, so the admin updates live instead of polling.
|
|
4
4
|
#
|
|
5
5
|
# SECURITY POSTURE — default OFF. An event reaches a browser only if ALL hold:
|
|
6
|
-
# * its name matches a declared
|
|
6
|
+
# * its name matches a declared OpenLoam.broadcast_events pattern (opt-in), AND
|
|
7
7
|
# * its tenant matches the connected tenant (isolation), AND
|
|
8
8
|
# * its audience includes the connected actor (a payload `user_id`, if any,
|
|
9
9
|
# is the sole recipient; no `user_id` means tenant-wide).
|
|
10
10
|
#
|
|
11
|
-
# FAN-OUT is behind a swappable broadcaster seam (
|
|
11
|
+
# FAN-OUT is behind a swappable broadcaster seam (OpenLoam::EventStream.broadcaster).
|
|
12
12
|
# The default in-process broadcaster only sees events published in THIS process
|
|
13
13
|
# — fine for the single-process prototype; a multi-process deploy swaps in a
|
|
14
14
|
# Redis/SolidCable-backed broadcaster with no controller change (see
|
|
@@ -24,10 +24,10 @@ module Loam
|
|
|
24
24
|
# Is this event name allowed to reach browsers at all? Empty allow-list →
|
|
25
25
|
# false, always (nothing leaks by default).
|
|
26
26
|
def broadcastable?(event_name)
|
|
27
|
-
|
|
28
|
-
next false if
|
|
27
|
+
OpenLoam.broadcast_events.any? do |pattern|
|
|
28
|
+
next false if OpenLoam::Overrides.disabled?(:broadcast_events, pattern) # an app can turn a default pattern off
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
OpenLoam::Events.pattern_matches?(pattern, event_name)
|
|
31
31
|
end
|
|
32
32
|
end
|
|
33
33
|
|
|
@@ -42,9 +42,9 @@ module Loam
|
|
|
42
42
|
recipient.nil? || recipient == actor&.id
|
|
43
43
|
end
|
|
44
44
|
|
|
45
|
-
# One SSE message: an `event:` line (the
|
|
45
|
+
# One SSE message: an `event:` line (the OpenLoam event name) and a `data:`
|
|
46
46
|
# line (JSON), ended by a blank line. Only small id-ish keys ride along —
|
|
47
|
-
#
|
|
47
|
+
# OpenLoam events carry no attribute values, and this slices to a safe set as
|
|
48
48
|
# belt-and-suspenders (tenant_id is dropped; it is implied by the connection).
|
|
49
49
|
def frame(event_name, payload)
|
|
50
50
|
"event: #{event_name}\ndata: #{safe_payload(payload).to_json}\n\n"
|
|
@@ -54,21 +54,21 @@ module Loam
|
|
|
54
54
|
|
|
55
55
|
def safe_payload(payload)
|
|
56
56
|
# id-ish keys only — never attribute values. percent/status ride along for
|
|
57
|
-
# the progress bar (
|
|
57
|
+
# the progress bar (OpenLoam::ProgressJob); both are non-sensitive.
|
|
58
58
|
payload.symbolize_keys.slice(:id, :type, :user_id, :from, :to, :percent, :status).compact
|
|
59
59
|
end
|
|
60
60
|
end
|
|
61
61
|
|
|
62
|
-
# The default fan-out: subscribe to
|
|
62
|
+
# The default fan-out: subscribe to OpenLoam::Events in THIS process and forward
|
|
63
63
|
# the events deliverable to (tenant, actor) to a sink. A `sink` is anything
|
|
64
64
|
# answering #call(sse_string) — the controller's is a Queue push.
|
|
65
65
|
class InProcessBroadcaster
|
|
66
66
|
# Returns an opaque handle to pass back to #unsubscribe.
|
|
67
67
|
def subscribe(tenant:, actor:, &sink)
|
|
68
|
-
|
|
69
|
-
next unless
|
|
68
|
+
OpenLoam::Events.subscribe_all do |event_name, payload|
|
|
69
|
+
next unless OpenLoam::EventStream.deliverable?(event_name, payload, tenant: tenant, actor: actor)
|
|
70
70
|
|
|
71
|
-
sink.call(
|
|
71
|
+
sink.call(OpenLoam::EventStream.frame(event_name, payload))
|
|
72
72
|
end
|
|
73
73
|
end
|
|
74
74
|
|
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
module
|
|
1
|
+
module OpenLoam
|
|
2
2
|
# Lifecycle events by convention. Included in every generated entity:
|
|
3
3
|
#
|
|
4
|
-
# class Equipment <
|
|
5
|
-
# include
|
|
4
|
+
# class Equipment < OpenLoam::TenantRecord
|
|
5
|
+
# include OpenLoam::Eventful
|
|
6
6
|
# event_domain :rental # -> "rental.equipment.created" etc.
|
|
7
7
|
# end
|
|
8
8
|
#
|
|
9
|
-
# Custom business events are published explicitly with
|
|
9
|
+
# Custom business events are published explicitly with OpenLoam::Events.publish.
|
|
10
10
|
module Eventful
|
|
11
11
|
extend ActiveSupport::Concern
|
|
12
12
|
|
|
13
13
|
included do
|
|
14
|
-
class_attribute :
|
|
15
|
-
class_attribute :
|
|
14
|
+
class_attribute :open_loam_event_domain, default: "app"
|
|
15
|
+
class_attribute :open_loam_event_entity, default: nil
|
|
16
16
|
|
|
17
17
|
after_create_commit { publish_lifecycle_event("created") }
|
|
18
18
|
after_update_commit { publish_lifecycle_event("updated") }
|
|
@@ -21,23 +21,23 @@ module Loam
|
|
|
21
21
|
|
|
22
22
|
class_methods do
|
|
23
23
|
def event_domain(domain)
|
|
24
|
-
self.
|
|
24
|
+
self.open_loam_event_domain = domain.to_s
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
# The middle segment of the event name. It defaults to the model's param
|
|
28
28
|
# key, which is right for app models but awkward for namespaced ones —
|
|
29
|
-
#
|
|
30
|
-
# "
|
|
29
|
+
# OpenLoam::Comment would publish "open_loam.open_loam_comment.created" rather than
|
|
30
|
+
# "open_loam.comment.created".
|
|
31
31
|
def event_entity(name)
|
|
32
|
-
self.
|
|
32
|
+
self.open_loam_event_entity = name.to_s
|
|
33
33
|
end
|
|
34
34
|
end
|
|
35
35
|
|
|
36
36
|
private
|
|
37
37
|
|
|
38
38
|
def publish_lifecycle_event(happened)
|
|
39
|
-
|
|
40
|
-
"#{
|
|
39
|
+
OpenLoam::Events.publish(
|
|
40
|
+
"#{open_loam_event_domain}.#{open_loam_event_entity || model_name.param_key}.#{happened}",
|
|
41
41
|
{ id: id, type: self.class.name }
|
|
42
42
|
)
|
|
43
43
|
end
|