omni_events 0.1.1

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.
@@ -0,0 +1,14 @@
1
+ class CreateOmniEventLogs < ActiveRecord::Migration[6.1]
2
+ def change
3
+ create_table :omni_event_logs do |t|
4
+ t.references :loggable, polymorphic: true, null: true
5
+ t.string :action_type, null: false, default: 'system_info'
6
+ t.text :content
7
+ t.jsonb :metadata, null: false, default: {}
8
+ t.timestamps
9
+ end
10
+
11
+ add_index :omni_event_logs, :action_type
12
+ add_index :omni_event_logs, :created_at
13
+ end
14
+ end
@@ -0,0 +1,8 @@
1
+ class AddSecurityToOmniEventNotifiers < ActiveRecord::Migration[6.1]
2
+ def change
3
+ add_column :omni_event_notifiers, :secret_key, :string
4
+ add_column :omni_event_notifiers, :timestamp_tolerance, :integer, default: 300, null: false
5
+
6
+ add_index :omni_event_notifiers, :secret_key, unique: true, where: "secret_key IS NOT NULL"
7
+ end
8
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators/migration'
4
+
5
+ module OmniEvent
6
+ module Generators
7
+ class InstallGenerator < Rails::Generators::Base
8
+ include Rails::Generators::Migration
9
+
10
+ source_root File.expand_path('templates', __dir__)
11
+
12
+ def copy_migrations
13
+ rake "omni_event:install:migrations"
14
+ end
15
+
16
+ def create_initializer
17
+ create_file "config/initializers/omni_event.rb", <<~RUBY
18
+ OmniEvent.configure do |config|
19
+ # ── Monitoring ────────────────────────────────────────────────────────────
20
+ config.new_relic_enabled = false
21
+ config.new_relic_api_key = ENV['NEW_RELIC_KEY']
22
+ config.new_relic_account_id = ENV['NEW_RELIC_ACCOUNT_ID']
23
+
24
+ # ── Processing ────────────────────────────────────────────────────────────
25
+ config.process_async = true # set false to process synchronously
26
+ config.retention_days = 30
27
+
28
+ # ── Custom log types ──────────────────────────────────────────────────────
29
+ # Define your domain-specific action types here.
30
+ config.custom_log_types = {
31
+ system_info: 0,
32
+ system_error: 1
33
+ }
34
+
35
+ # ── Processor registry ────────────────────────────────────────────────────
36
+ # Map each OmniEvent::Notifier name to its processor class.
37
+ #
38
+ # config.processors = {
39
+ # "Siscomex" => SiscomexProcessor,
40
+ # "DHL" => DHLWebhookProcessor
41
+ # }
42
+ config.processors = {}
43
+ end
44
+ RUBY
45
+ end
46
+
47
+ def create_local_models
48
+ create_file "app/models/log.rb", <<~RUBY
49
+ class Log < OmniEvent::Log
50
+ # Add custom scopes, validations or methods here.
51
+ end
52
+ RUBY
53
+
54
+ create_file "app/models/webhook_event.rb", <<~RUBY
55
+ class WebhookEvent < OmniEvent::WebhookEvent
56
+ end
57
+ RUBY
58
+ end
59
+
60
+ def display_post_install_message
61
+ puts ""
62
+ puts "=" * 64
63
+ puts " OmniEvent #{OmniEvent::VERSION} installed successfully!"
64
+ puts ""
65
+ puts " Next steps:"
66
+ puts " 1. rails db:migrate"
67
+ puts " 2. Configure config/initializers/omni_event.rb"
68
+ puts " 3. Mount the engine in config/routes.rb:"
69
+ puts " mount OmniEvent::Engine => '/omni_events'"
70
+ puts " 4. Create a Notifier:"
71
+ puts " OmniEvent::Notifier.create!(name: 'My Partner')"
72
+ puts " 5. Register a processor in the initializer:"
73
+ puts " config.processors = { 'My Partner' => MyProcessor }"
74
+ puts "=" * 64
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmniEvent
4
+ class BaseProcessor
5
+ attr_reader :event, :current_step
6
+
7
+ def self.steps(*method_names)
8
+ @steps_list = method_names
9
+ end
10
+
11
+ def self.steps_list
12
+ @steps_list || []
13
+ end
14
+
15
+ def initialize(event)
16
+ @event = event
17
+ end
18
+
19
+ def process!
20
+ self.class.steps_list.each do |method_name|
21
+ execute_step(method_name)
22
+ end
23
+ rescue => e
24
+ raise e
25
+ end
26
+
27
+ private
28
+
29
+ def execute_step(method_name)
30
+ @current_step = method_name.to_s.humanize
31
+
32
+ Rails.logger.info "[OmniEvent] Executing step: #{@current_step}"
33
+
34
+ send(method_name)
35
+
36
+ rescue => e
37
+ OmniEvent::Log.create!(
38
+ loggable: event,
39
+ action_type: :system_error,
40
+ content: "FAILURE in step [#{@current_step}]: #{e.message}",
41
+ metadata: {
42
+ error_class: e.class.name,
43
+ method: method_name,
44
+ backtrace: e.backtrace.first(3)
45
+ }
46
+ )
47
+ raise e
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmniEvent
4
+ class Configuration
5
+ attr_accessor :new_relic_enabled,
6
+ :new_relic_api_key,
7
+ :new_relic_account_id,
8
+ :retention_days,
9
+ :process_async,
10
+ :custom_log_types,
11
+ :processors
12
+
13
+ def initialize
14
+ @new_relic_enabled = false
15
+ @new_relic_api_key = nil
16
+ @new_relic_account_id = nil
17
+ @retention_days = 30
18
+ @process_async = true
19
+ @custom_log_types = { system_info: 0, system_error: 1 }
20
+ @processors = {}
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,5 @@
1
+ module OmniEvent
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace OmniEvent
4
+ end
5
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmniEvent
4
+ # Finds the registered processor for a given WebhookEvent and runs it.
5
+ #
6
+ # Register processors in your initializer:
7
+ #
8
+ # OmniEvent.configure do |config|
9
+ # config.processors = {
10
+ # "Siscomex" => SiscomexProcessor,
11
+ # "DHL" => DHLProcessor
12
+ # }
13
+ # end
14
+ #
15
+ class ProcessDispatcher
16
+ def self.call(event)
17
+ processor_class = resolve(event)
18
+
19
+ unless processor_class
20
+ Rails.logger.warn "[OmniEvent] No processor registered for notifier '#{event.webhook_notifier.name}'. Marking as processed."
21
+ event.update!(status: :processed)
22
+ return
23
+ end
24
+
25
+ processor_class.new(event).process!
26
+ event.update!(status: :processed)
27
+ rescue => e
28
+ event.update!(status: :failed)
29
+ raise e
30
+ end
31
+
32
+ private_class_method def self.resolve(event)
33
+ notifier_name = event.webhook_notifier.name
34
+ config = OmniEvent.configuration.processors
35
+
36
+ config[notifier_name] ||
37
+ config[notifier_name.to_sym] ||
38
+ config[notifier_name.downcase] ||
39
+ config[notifier_name.downcase.to_sym]
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'openssl'
4
+
5
+ module OmniEvent
6
+ # Verifies the authenticity and freshness of an incoming webhook request.
7
+ #
8
+ # Two independent checks (both only active when secret_key is set):
9
+ #
10
+ # 1. Timestamp — rejects requests outside the tolerance window (replay attack protection).
11
+ # Header: X-OmniEvent-Timestamp (Unix timestamp, e.g. 1711800000)
12
+ #
13
+ # 2. HMAC Signature — verifies the payload was signed with the shared secret.
14
+ # Header: X-OmniEvent-Signature (e.g. "sha256=abc123...")
15
+ # Algorithm: HMAC-SHA256(secret_key, raw_body)
16
+ #
17
+ # If the Notifier has no secret_key, all checks are skipped (backward compatible).
18
+ #
19
+ # Usage:
20
+ # result = OmniEvent::SignatureVerifier.call(notifier, request)
21
+ # result.success? # => true / false
22
+ # result.error # => "Invalid signature — payload may have been tampered"
23
+ #
24
+ class SignatureVerifier
25
+ Result = Struct.new(:success?, :error)
26
+
27
+ TIMESTAMP_HEADER = 'X-OmniEvent-Timestamp'
28
+ SIGNATURE_HEADER = 'X-OmniEvent-Signature'
29
+
30
+ def self.call(notifier, request)
31
+ new(notifier, request).verify
32
+ end
33
+
34
+ def initialize(notifier, request)
35
+ @notifier = notifier
36
+ @request = request
37
+ end
38
+
39
+ def verify
40
+ # No secret configured — skip all checks (opt-in security)
41
+ return success unless @notifier.secret_key.present?
42
+
43
+ result = verify_timestamp
44
+ return result unless result.success?
45
+
46
+ verify_signature
47
+ end
48
+
49
+ private
50
+
51
+ # ── Timestamp check ────────────────────────────────────────────────────────
52
+ def verify_timestamp
53
+ tolerance = @notifier.timestamp_tolerance.to_i
54
+ return success if tolerance.zero?
55
+
56
+ header = @request.headers[TIMESTAMP_HEADER]
57
+ return failure("Missing #{TIMESTAMP_HEADER} header") if header.blank?
58
+
59
+ request_time = Time.at(header.to_i)
60
+ elapsed = (Time.current - request_time).abs
61
+
62
+ if elapsed > tolerance
63
+ failure("Request timestamp is #{elapsed.to_i}s old — outside the #{tolerance}s window (possible replay attack)")
64
+ else
65
+ success
66
+ end
67
+ end
68
+
69
+ # ── HMAC signature check ───────────────────────────────────────────────────
70
+ def verify_signature
71
+ header = @request.headers[SIGNATURE_HEADER]
72
+ return failure("Missing #{SIGNATURE_HEADER} header") if header.blank?
73
+
74
+ raw_body = @request.raw_post
75
+ expected = "sha256=#{OpenSSL::HMAC.hexdigest('SHA256', @notifier.secret_key, raw_body)}"
76
+
77
+ unless ActiveSupport::SecurityUtils.secure_compare(expected, header)
78
+ failure("Invalid signature — payload may have been tampered or secret_key is wrong")
79
+ else
80
+ success
81
+ end
82
+ end
83
+
84
+ def success = Result.new(true, nil)
85
+ def failure(msg) = Result.new(false, msg)
86
+ end
87
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmniEvent
4
+ VERSION = "0.1.1"
5
+ end
data/lib/omni_event.rb ADDED
@@ -0,0 +1,17 @@
1
+ require "omni_event/version"
2
+ require "omni_event/engine"
3
+ require "omni_event/configuration"
4
+ require "omni_event/base_processor"
5
+ require "omni_event/process_dispatcher"
6
+ require "omni_event/signature_verifier"
7
+
8
+ module OmniEvent
9
+ class << self
10
+ attr_accessor :configuration
11
+ end
12
+
13
+ def self.configure
14
+ self.configuration ||= Configuration.new
15
+ yield(configuration)
16
+ end
17
+ end
@@ -0,0 +1,12 @@
1
+ namespace :omni_event do
2
+ desc "Delete logs and webhook events older than the configured retention_days"
3
+ task cleanup: :environment do
4
+ retention_days = OmniEvent.configuration.retention_days
5
+ cutoff = retention_days.days.ago
6
+
7
+ deleted_logs = OmniEvent::Log.where("created_at < ?", cutoff).delete_all
8
+ deleted_events = OmniEvent::WebhookEvent.where("created_at < ?", cutoff).delete_all
9
+
10
+ puts "[OmniEvent] Cleanup complete: #{deleted_logs} logs and #{deleted_events} webhook events deleted (older than #{retention_days} days)."
11
+ end
12
+ end
@@ -0,0 +1,4 @@
1
+ module OmniEvent
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,148 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: omni_events
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Antonio Neto
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2026-03-31 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '6.1'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '6.1'
27
+ - !ruby/object:Gem::Dependency
28
+ name: httparty
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0.21'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0.21'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec-rails
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '6.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '6.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: sqlite3
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.6'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.6'
69
+ - !ruby/object:Gem::Dependency
70
+ name: factory_bot_rails
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '6.2'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '6.2'
83
+ description: OmniEvent unifies external event ingestion with detailed internal auditing
84
+ via a step-based pipeline and asynchronous monitoring.
85
+ email:
86
+ - antoniocneto.dev@gmail.com
87
+ executables: []
88
+ extensions: []
89
+ extra_rdoc_files: []
90
+ files:
91
+ - ".rspec"
92
+ - CHANGELOG.md
93
+ - CODE_OF_CONDUCT.md
94
+ - Gemfile
95
+ - Gemfile.lock
96
+ - LICENSE.txt
97
+ - README.md
98
+ - Rakefile
99
+ - app/controllers/omni_event/receiver_controller.rb
100
+ - app/jobs/omni_event/new_relic_job.rb
101
+ - app/jobs/omni_event/process_webhook_job.rb
102
+ - app/models/omni_event/application_record.rb
103
+ - app/models/omni_event/log.rb
104
+ - app/models/omni_event/notifier.rb
105
+ - app/models/omni_event/webhook_event.rb
106
+ - config/routes.rb
107
+ - db/migrate/20240101000001_create_omni_event_notifiers.rb
108
+ - db/migrate/20240101000002_create_omni_event_webhook_events.rb
109
+ - db/migrate/20240101000003_create_omni_event_logs.rb
110
+ - db/migrate/20240101000004_add_security_to_omni_event_notifiers.rb
111
+ - lib/generators/omni_event/install_generator.rb
112
+ - lib/omni_event.rb
113
+ - lib/omni_event/base_processor.rb
114
+ - lib/omni_event/configuration.rb
115
+ - lib/omni_event/engine.rb
116
+ - lib/omni_event/process_dispatcher.rb
117
+ - lib/omni_event/signature_verifier.rb
118
+ - lib/omni_event/version.rb
119
+ - lib/tasks/omni_event_tasks.rake
120
+ - sig/omni_event.rbs
121
+ homepage: https://github.com/antoniocneto/omni_event
122
+ licenses:
123
+ - MIT
124
+ metadata:
125
+ allowed_push_host: https://rubygems.org
126
+ homepage_uri: https://github.com/antoniocneto/omni_event
127
+ source_code_uri: https://github.com/antoniocneto/omni_event
128
+ post_install_message:
129
+ rdoc_options: []
130
+ require_paths:
131
+ - lib
132
+ required_ruby_version: !ruby/object:Gem::Requirement
133
+ requirements:
134
+ - - ">="
135
+ - !ruby/object:Gem::Version
136
+ version: 3.0.0
137
+ required_rubygems_version: !ruby/object:Gem::Requirement
138
+ requirements:
139
+ - - ">="
140
+ - !ruby/object:Gem::Version
141
+ version: '0'
142
+ requirements: []
143
+ rubygems_version: 3.4.1
144
+ signing_key:
145
+ specification_version: 4
146
+ summary: Rails Engine for unified Webhook ingestion, Polymorphic Logging, and Process
147
+ Traceability.
148
+ test_files: []