lex-webhook 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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 9579600380a6b56342f0243c7ed666aae7cd77ede4a90ac780a0d46af4b772c0
4
+ data.tar.gz: 885f7bf1a903e40cbd07bb033e58de3d8ed6128371cf1eec0fb920e811cfdb4c
5
+ SHA512:
6
+ metadata.gz: 20bf93dd85ec020cf81c6d8b1859585df3dd3f30fb1ea06a2ca80bcdb60402627061ddfa2b92d756e6e060bd76436867f1b1afb87fc0dc3e458da3fdb2ad3ce5
7
+ data.tar.gz: 277c023cee59b78e4457aa42b74a7c514b2ab0ac0e2015df53571a2c22ed376141dd5241705bad0d6672494ffdd4bc5d181e79cb0a8fc93003d4e2fd6dd842f3
@@ -0,0 +1,16 @@
1
+ name: CI
2
+ on:
3
+ push:
4
+ branches: [origin]
5
+ pull_request:
6
+
7
+ jobs:
8
+ ci:
9
+ uses: LegionIO/.github/.github/workflows/ci.yml@main
10
+
11
+ release:
12
+ needs: ci
13
+ if: github.event_name == 'push' && github.ref == 'refs/heads/origin'
14
+ uses: LegionIO/.github/.github/workflows/release.yml@main
15
+ secrets:
16
+ rubygems-api-key: ${{ secrets.RUBYGEMS_API_KEY }}
data/.gitignore ADDED
@@ -0,0 +1,13 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ Gemfile.lock
11
+
12
+ # rspec failure tracking
13
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,53 @@
1
+ AllCops:
2
+ TargetRubyVersion: 3.4
3
+ NewCops: enable
4
+ SuggestExtensions: false
5
+
6
+ Layout/LineLength:
7
+ Max: 160
8
+
9
+ Layout/SpaceAroundEqualsInParameterDefault:
10
+ EnforcedStyle: space
11
+
12
+ Layout/HashAlignment:
13
+ EnforcedHashRocketStyle: table
14
+ EnforcedColonStyle: table
15
+
16
+ Metrics/MethodLength:
17
+ Max: 50
18
+
19
+ Metrics/ClassLength:
20
+ Max: 1500
21
+
22
+ Metrics/ModuleLength:
23
+ Max: 1500
24
+
25
+ Metrics/BlockLength:
26
+ Max: 40
27
+ Exclude:
28
+ - 'spec/**/*'
29
+
30
+ Metrics/AbcSize:
31
+ Max: 60
32
+
33
+ Metrics/CyclomaticComplexity:
34
+ Max: 15
35
+
36
+ Metrics/PerceivedComplexity:
37
+ Max: 17
38
+
39
+ Style/Documentation:
40
+ Enabled: false
41
+
42
+ Style/SymbolArray:
43
+ Enabled: true
44
+
45
+ Style/FrozenStringLiteralComment:
46
+ Enabled: true
47
+ EnforcedStyle: always
48
+
49
+ Naming/FileName:
50
+ Enabled: false
51
+
52
+ Gemspec/DevelopmentDependencies:
53
+ Enabled: false
data/CHANGELOG.md ADDED
@@ -0,0 +1,11 @@
1
+ # Changelog
2
+
3
+ ## [0.1.0] - 2026-03-21
4
+
5
+ ### Added
6
+ - Initial release
7
+ - `Helpers::Signature` with HMAC compute, verify, and constant-time secure_compare
8
+ - `Runners::Receive` for webhook ingestion with optional signature verification and JSON body parsing
9
+ - `Runners::Verify` for explicit signature validation and computation
10
+ - `Runners::Endpoints` for in-memory endpoint registry (list, register, remove)
11
+ - Standalone `Client` class including all runner modules
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gemspec
data/README.md ADDED
@@ -0,0 +1,47 @@
1
+ # lex-webhook
2
+
3
+ Generic webhook receiving and HMAC signature verification for LegionIO.
4
+
5
+ ## Installation
6
+
7
+ Add to your Gemfile:
8
+
9
+ ```ruby
10
+ gem 'lex-webhook'
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ### Standalone client
16
+
17
+ ```ruby
18
+ client = Legion::Extensions::Webhook::Client.new
19
+
20
+ # Receive a webhook (with optional HMAC verification)
21
+ result = client.receive(
22
+ path: '/hooks/github',
23
+ headers: { 'x-hub-signature-256' => 'sha256=abc123...' },
24
+ body: '{"action":"push"}',
25
+ method: 'POST',
26
+ secret: 'my-secret'
27
+ )
28
+ # => { received: true, path: '/hooks/github', payload: { action: 'push' }, verified: true }
29
+
30
+ # Verify a signature
31
+ result = client.verify(secret: 'my-secret', signature: 'sha256=abc123...', payload: '{"action":"push"}')
32
+ # => { valid: true, algorithm: 'sha256' }
33
+
34
+ # Compute a signature
35
+ result = client.compute_signature(secret: 'my-secret', payload: '{"action":"push"}')
36
+ # => { signature: 'abc123...' }
37
+
38
+ # Endpoint registry
39
+ client.register_endpoint(path: '/hooks/github', secret: 'secret1', description: 'GitHub events')
40
+ client.list_endpoints
41
+ # => { endpoints: [{ path: '/hooks/github', secret: 'secret1', description: 'GitHub events' }] }
42
+ client.remove_endpoint(path: '/hooks/github')
43
+ ```
44
+
45
+ ## License
46
+
47
+ MIT
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'legion/extensions/webhook/version'
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = 'lex-webhook'
9
+ spec.version = Legion::Extensions::Webhook::VERSION
10
+ spec.authors = ['Esity']
11
+ spec.email = ['matthewdiverson@gmail.com']
12
+
13
+ spec.summary = 'Legion::Extensions::Webhook'
14
+ spec.description = 'Generic webhook receiving and HMAC signature verification for LegionIO'
15
+ spec.homepage = 'https://github.com/LegionIO/lex-webhook'
16
+ spec.license = 'MIT'
17
+ spec.required_ruby_version = '>= 3.4'
18
+
19
+ spec.metadata['homepage_uri'] = spec.homepage
20
+ spec.metadata['source_code_uri'] = 'https://github.com/LegionIO/lex-webhook'
21
+ spec.metadata['changelog_uri'] = 'https://github.com/LegionIO/lex-webhook/blob/main/CHANGELOG.md'
22
+ spec.metadata['documentation_uri'] = 'https://github.com/LegionIO/lex-webhook'
23
+ spec.metadata['bug_tracker_uri'] = 'https://github.com/LegionIO/lex-webhook/issues'
24
+ spec.metadata['rubygems_mfa_required'] = 'true'
25
+
26
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
27
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
28
+ end
29
+ spec.require_paths = ['lib']
30
+
31
+ spec.add_development_dependency 'rake'
32
+ spec.add_development_dependency 'rspec'
33
+ spec.add_development_dependency 'rubocop'
34
+ spec.add_development_dependency 'rubocop-rspec'
35
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/webhook/runners/receive'
4
+ require 'legion/extensions/webhook/runners/verify'
5
+ require 'legion/extensions/webhook/runners/endpoints'
6
+
7
+ module Legion
8
+ module Extensions
9
+ module Webhook
10
+ class Client
11
+ include Runners::Receive
12
+ include Runners::Verify
13
+ include Runners::Endpoints
14
+
15
+ def initialize(**opts)
16
+ @opts = opts
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Webhook
6
+ module Helpers
7
+ module Client
8
+ module_function
9
+
10
+ def settings
11
+ return Legion::Settings[:webhook] if defined?(Legion::Settings)
12
+
13
+ {}
14
+ end
15
+
16
+ def endpoints_store
17
+ @endpoints_store ||= []
18
+ end
19
+
20
+ def reset_endpoints!
21
+ @endpoints_store = []
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'openssl'
4
+
5
+ module Legion
6
+ module Extensions
7
+ module Webhook
8
+ module Helpers
9
+ module Signature
10
+ module_function
11
+
12
+ def verify(secret:, signature:, payload:, algorithm: 'sha256') # rubocop:disable Naming/PredicateMethod
13
+ computed = compute(secret: secret, payload: payload, algorithm: algorithm)
14
+ bytes_match?(computed, signature)
15
+ end
16
+
17
+ def compute(secret:, payload:, algorithm: 'sha256')
18
+ OpenSSL::HMAC.hexdigest(algorithm, secret, payload)
19
+ end
20
+
21
+ def bytes_match?(lhs, rhs)
22
+ return false if lhs.nil? || rhs.nil?
23
+
24
+ lhs_clean = lhs.sub(/\A\w+=/, '')
25
+ rhs_clean = rhs.sub(/\A\w+=/, '')
26
+ return false if lhs_clean.bytesize != rhs_clean.bytesize
27
+
28
+ lhs_clean.bytes.zip(rhs_clean.bytes).reduce(0) { |acc, (x, y)| acc | (x ^ y) }.zero?
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Webhook
6
+ module Runners
7
+ module Endpoints
8
+ def list_endpoints(**)
9
+ { endpoints: endpoint_registry.dup }
10
+ end
11
+
12
+ def register_endpoint(path:, secret: nil, description: nil, **)
13
+ existing = endpoint_registry.find { |e| e[:path] == path }
14
+ if existing
15
+ existing[:secret] = secret
16
+ existing[:description] = description
17
+ return { registered: false, updated: true, path: path }
18
+ end
19
+
20
+ entry = { path: path, secret: secret, description: description }.compact
21
+ endpoint_registry << entry
22
+ { registered: true, updated: false, path: path }
23
+ end
24
+
25
+ def remove_endpoint(path:, **)
26
+ before = endpoint_registry.size
27
+ endpoint_registry.reject! { |e| e[:path] == path }
28
+ removed = endpoint_registry.size < before
29
+ { removed: removed, path: path }
30
+ end
31
+
32
+ private
33
+
34
+ def endpoint_registry
35
+ @endpoint_registry ||= []
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'legion/extensions/webhook/helpers/signature'
5
+
6
+ module Legion
7
+ module Extensions
8
+ module Webhook
9
+ module Runners
10
+ module Receive
11
+ def receive(path:, **opts)
12
+ headers = opts.fetch(:headers, {})
13
+ body = opts.fetch(:body, '')
14
+ method = opts.fetch(:method, 'POST')
15
+ secret = opts[:secret]
16
+
17
+ verified = verify_signature(headers: headers, body: body, secret: secret)
18
+ payload = parse_body(headers: headers, body: body)
19
+
20
+ { received: true, path: path, method: method, payload: payload, verified: verified }
21
+ end
22
+
23
+ private
24
+
25
+ def verify_signature(headers:, body:, secret:)
26
+ return false if secret.nil?
27
+
28
+ sig_header = find_signature_header(headers)
29
+ return false if sig_header.nil?
30
+
31
+ Helpers::Signature.verify(secret: secret, signature: sig_header, payload: body)
32
+ end
33
+
34
+ def find_signature_header(headers)
35
+ normalized = headers.transform_keys { |key| key.to_s.downcase }
36
+ normalized['x-hub-signature-256'] ||
37
+ normalized['x-hub-signature'] ||
38
+ normalized['x-signature'] ||
39
+ normalized['x-webhook-signature']
40
+ end
41
+
42
+ def parse_body(headers:, body:)
43
+ return body if body.nil? || body.empty?
44
+
45
+ content_type = headers.transform_keys { |key| key.to_s.downcase }['content-type'].to_s
46
+ return ::JSON.parse(body, symbolize_names: true) if content_type.include?('json')
47
+
48
+ body
49
+ rescue ::JSON::ParserError
50
+ body
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/webhook/helpers/signature'
4
+
5
+ module Legion
6
+ module Extensions
7
+ module Webhook
8
+ module Runners
9
+ module Verify
10
+ def verify(secret:, signature:, payload:, algorithm: 'sha256', **)
11
+ valid = Helpers::Signature.verify(
12
+ secret: secret,
13
+ signature: signature,
14
+ payload: payload,
15
+ algorithm: algorithm
16
+ )
17
+ { valid: valid, algorithm: algorithm }
18
+ end
19
+
20
+ def compute_signature(secret:, payload:, algorithm: 'sha256', **)
21
+ sig = Helpers::Signature.compute(secret: secret, payload: payload, algorithm: algorithm)
22
+ { signature: sig }
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Webhook
6
+ VERSION = '0.1.1'
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/webhook/version'
4
+ require 'legion/extensions/webhook/helpers/signature'
5
+ require 'legion/extensions/webhook/helpers/client'
6
+ require 'legion/extensions/webhook/runners/receive'
7
+ require 'legion/extensions/webhook/runners/verify'
8
+ require 'legion/extensions/webhook/runners/endpoints'
9
+ require 'legion/extensions/webhook/client'
10
+
11
+ module Legion
12
+ module Extensions
13
+ module Webhook
14
+ extend Legion::Extensions::Core if Legion::Extensions.const_defined?(:Core)
15
+ end
16
+ end
17
+ end
metadata ADDED
@@ -0,0 +1,118 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lex-webhook
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Esity
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rake
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '0'
19
+ type: :development
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: rspec
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: rubocop
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: rubocop-rspec
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ description: Generic webhook receiving and HMAC signature verification for LegionIO
69
+ email:
70
+ - matthewdiverson@gmail.com
71
+ executables: []
72
+ extensions: []
73
+ extra_rdoc_files: []
74
+ files:
75
+ - ".github/workflows/ci.yml"
76
+ - ".gitignore"
77
+ - ".rspec"
78
+ - ".rubocop.yml"
79
+ - CHANGELOG.md
80
+ - Gemfile
81
+ - README.md
82
+ - lex-webhook.gemspec
83
+ - lib/legion/extensions/webhook.rb
84
+ - lib/legion/extensions/webhook/client.rb
85
+ - lib/legion/extensions/webhook/helpers/client.rb
86
+ - lib/legion/extensions/webhook/helpers/signature.rb
87
+ - lib/legion/extensions/webhook/runners/endpoints.rb
88
+ - lib/legion/extensions/webhook/runners/receive.rb
89
+ - lib/legion/extensions/webhook/runners/verify.rb
90
+ - lib/legion/extensions/webhook/version.rb
91
+ homepage: https://github.com/LegionIO/lex-webhook
92
+ licenses:
93
+ - MIT
94
+ metadata:
95
+ homepage_uri: https://github.com/LegionIO/lex-webhook
96
+ source_code_uri: https://github.com/LegionIO/lex-webhook
97
+ changelog_uri: https://github.com/LegionIO/lex-webhook/blob/main/CHANGELOG.md
98
+ documentation_uri: https://github.com/LegionIO/lex-webhook
99
+ bug_tracker_uri: https://github.com/LegionIO/lex-webhook/issues
100
+ rubygems_mfa_required: 'true'
101
+ rdoc_options: []
102
+ require_paths:
103
+ - lib
104
+ required_ruby_version: !ruby/object:Gem::Requirement
105
+ requirements:
106
+ - - ">="
107
+ - !ruby/object:Gem::Version
108
+ version: '3.4'
109
+ required_rubygems_version: !ruby/object:Gem::Requirement
110
+ requirements:
111
+ - - ">="
112
+ - !ruby/object:Gem::Version
113
+ version: '0'
114
+ requirements: []
115
+ rubygems_version: 3.6.9
116
+ specification_version: 4
117
+ summary: Legion::Extensions::Webhook
118
+ test_files: []