laneful-ruby 1.0.1 → 1.1.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/README.md +2 -115
- data/examples/simple_example.rb +4 -2
- data/lib/laneful/version.rb +1 -1
- data/lib/laneful/webhooks.rb +134 -5
- data/lib/laneful.rb +1 -1
- metadata +2 -3
- data/examples/README.md +0 -53
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3b7c5f86e1e341b49312a27486067c2427323569459dba65f8fb2b247629a1fb
|
4
|
+
data.tar.gz: 649ca87028b16f0ede2b1e063390fe13043e12401e70e4637d6c69922d7b415e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1d0b108965868436f5b4cbdc6208891bfa51b877a3ec7b70cc4bf13745b793aeb8676d284e010e446acd27de221637c38e005d8399dde5a60adfaf3bf2ae9749
|
7
|
+
data.tar.gz: a50c27e76b6f4b47a8cc6ecee7e5b863bc6f3106127ba9e22e9e822e3b803c809c42202e78412b4b2afe5ab5afcafcbcb2f10c16d78dc923f57e453ca8b14c6b
|
data/README.md
CHANGED
@@ -203,7 +203,7 @@ client = Laneful::Client.new(
|
|
203
203
|
```ruby
|
204
204
|
# In your webhook handler
|
205
205
|
payload = request.body.read
|
206
|
-
signature = request.headers['
|
206
|
+
signature = request.headers['x-webhook-signature']
|
207
207
|
secret = 'your-webhook-secret'
|
208
208
|
|
209
209
|
if Laneful::WebhookVerifier.verify_signature(secret, payload, signature)
|
@@ -337,120 +337,7 @@ bundle exec rubocop
|
|
337
337
|
bundle exec yard doc
|
338
338
|
```
|
339
339
|
|
340
|
-
## Publishing to RubyGems
|
341
|
-
|
342
|
-
This SDK can be published to RubyGems using a simple, automated workflow.
|
343
|
-
|
344
|
-
### Prerequisites
|
345
|
-
|
346
|
-
1. **Ruby 3.0+**: Ensure you have Ruby 3.0 or higher installed
|
347
|
-
2. **Bundler**: Install bundler if not already installed (`gem install bundler`)
|
348
|
-
3. **RubyGems API Key**: Get your API key from [RubyGems.org](https://rubygems.org/settings/edit)
|
349
|
-
|
350
|
-
### Setting up your API Key
|
351
|
-
|
352
|
-
```bash
|
353
|
-
export GEM_HOST_API_KEY=your_api_key_here
|
354
|
-
```
|
355
|
-
|
356
|
-
### Publishing Methods
|
357
|
-
|
358
|
-
#### Method 1: Using the Automated Script (Recommended)
|
359
|
-
|
360
|
-
```bash
|
361
|
-
./scripts/publish.sh
|
362
|
-
```
|
363
|
-
|
364
|
-
This script will:
|
365
|
-
|
366
|
-
- Check Ruby version compatibility
|
367
|
-
- Install dependencies
|
368
|
-
- Run all tests
|
369
|
-
- Perform code linting
|
370
|
-
- Build the gem
|
371
|
-
- Publish to RubyGems
|
372
|
-
- Clean up built gem files
|
373
|
-
|
374
|
-
#### Method 2: Using Make Commands
|
375
|
-
|
376
|
-
**Install dependencies:**
|
377
|
-
|
378
|
-
```bash
|
379
|
-
make setup
|
380
|
-
```
|
381
|
-
|
382
|
-
**Run tests:**
|
383
|
-
|
384
|
-
```bash
|
385
|
-
make test
|
386
|
-
```
|
387
|
-
|
388
|
-
**Run linting:**
|
389
|
-
|
390
|
-
```bash
|
391
|
-
make lint
|
392
|
-
```
|
393
|
-
|
394
|
-
**Build the gem:**
|
395
|
-
|
396
|
-
```bash
|
397
|
-
make build
|
398
|
-
```
|
399
|
-
|
400
|
-
**Publish to RubyGems:**
|
401
|
-
|
402
|
-
```bash
|
403
|
-
make publish
|
404
|
-
```
|
405
|
-
|
406
|
-
#### Method 3: Manual Publishing
|
407
|
-
|
408
|
-
**Install dependencies:**
|
409
|
-
|
410
|
-
```bash
|
411
|
-
bundle install
|
412
|
-
```
|
413
|
-
|
414
|
-
**Run tests:**
|
415
|
-
|
416
|
-
```bash
|
417
|
-
bundle exec rspec
|
418
|
-
```
|
419
|
-
|
420
|
-
**Run linting:**
|
421
|
-
|
422
|
-
```bash
|
423
|
-
bundle exec rubocop
|
424
|
-
```
|
425
|
-
|
426
|
-
**Build the gem:**
|
427
|
-
|
428
|
-
```bash
|
429
|
-
gem build laneful-ruby.gemspec
|
430
|
-
```
|
431
|
-
|
432
|
-
**Publish to RubyGems:**
|
433
|
-
|
434
|
-
```bash
|
435
|
-
gem push laneful-ruby-*.gem
|
436
|
-
```
|
437
|
-
|
438
|
-
### Version Management
|
439
|
-
|
440
|
-
Before publishing, make sure to update the version in `lib/laneful/version.rb`:
|
441
|
-
|
442
|
-
```ruby
|
443
|
-
module Laneful
|
444
|
-
VERSION = "1.0.1".freeze # Update this version
|
445
|
-
end
|
446
|
-
```
|
447
|
-
|
448
|
-
Follow [Semantic Versioning](https://semver.org/) guidelines:
|
449
|
-
|
450
|
-
- **MAJOR** version for incompatible API changes
|
451
|
-
- **MINOR** version for backwards-compatible functionality additions
|
452
|
-
- **PATCH** version for backwards-compatible bug fixes
|
453
|
-
|
454
340
|
## License
|
455
341
|
|
456
342
|
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
343
|
+
|
data/examples/simple_example.rb
CHANGED
@@ -8,8 +8,8 @@ puts '📧 Laneful Ruby SDK - Simple Example'
|
|
8
8
|
puts "====================================\n"
|
9
9
|
|
10
10
|
# Configuration - Replace with your actual credentials
|
11
|
-
base_url = 'https://your-
|
12
|
-
auth_token = '
|
11
|
+
base_url = 'https://your-subdomain.z1.send.dev.laneful.net'
|
12
|
+
auth_token = 'priv-YourAuthTokenHere'
|
13
13
|
|
14
14
|
# Email addresses - Replace with your actual addresses
|
15
15
|
sender_email = 'sender@yourdomain.com'
|
@@ -50,6 +50,7 @@ rescue Laneful::ValidationException => e
|
|
50
50
|
puts "❌ Validation error: #{e.message}"
|
51
51
|
rescue Laneful::ApiException => e
|
52
52
|
puts "❌ API error: #{e.message} (Status: #{e.status_code})"
|
53
|
+
puts " Details: #{e.error_message}" if e.error_message
|
53
54
|
rescue Laneful::HttpException => e
|
54
55
|
puts "❌ HTTP error: #{e.message} (Status: #{e.status_code})"
|
55
56
|
end
|
@@ -108,6 +109,7 @@ rescue Laneful::ValidationException => e
|
|
108
109
|
puts "❌ Validation error: #{e.message}"
|
109
110
|
rescue Laneful::ApiException => e
|
110
111
|
puts "❌ API error: #{e.message} (Status: #{e.status_code})"
|
112
|
+
puts " Details: #{e.error_message}" if e.error_message
|
111
113
|
rescue Laneful::HttpException => e
|
112
114
|
puts "❌ HTTP error: #{e.message} (Status: #{e.status_code})"
|
113
115
|
end
|
data/lib/laneful/version.rb
CHANGED
data/lib/laneful/webhooks.rb
CHANGED
@@ -1,31 +1,116 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'json'
|
4
|
+
require 'openssl'
|
5
|
+
|
3
6
|
module Laneful
|
4
|
-
# Utility class for verifying webhook signatures
|
7
|
+
# Utility class for verifying webhook signatures and processing webhook payloads
|
5
8
|
class WebhookVerifier
|
6
9
|
ALGORITHM = 'sha256'
|
10
|
+
SIGNATURE_PREFIX = 'sha256='
|
11
|
+
SIGNATURE_HEADER_NAME = 'x-webhook-signature'
|
12
|
+
|
13
|
+
# Valid event types as documented
|
14
|
+
VALID_EVENT_TYPES = %w[
|
15
|
+
delivery open click drop spam_complaint unsubscribe bounce
|
16
|
+
].freeze
|
7
17
|
|
8
|
-
#
|
18
|
+
# UUID pattern for lane_id validation
|
19
|
+
UUID_PATTERN = /\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/i
|
20
|
+
|
21
|
+
# Verifies a webhook signature with support for sha256= prefix
|
22
|
+
# @param secret [String] The webhook secret
|
23
|
+
# @param payload [String] The webhook payload
|
24
|
+
# @param signature [String] The signature to verify (may include 'sha256=' prefix)
|
25
|
+
# @return [Boolean] true if the signature is valid, false otherwise
|
9
26
|
def self.verify_signature(secret, payload, signature)
|
10
27
|
return false if secret.nil? || secret.strip.empty?
|
11
28
|
return false if payload.nil?
|
12
29
|
return false if signature.nil? || signature.strip.empty?
|
13
30
|
|
31
|
+
# Handle sha256= prefix as documented
|
32
|
+
clean_signature = if signature.start_with?(SIGNATURE_PREFIX)
|
33
|
+
signature[SIGNATURE_PREFIX.length..]
|
34
|
+
else
|
35
|
+
signature
|
36
|
+
end
|
37
|
+
|
14
38
|
expected_signature = generate_signature(secret, payload)
|
15
|
-
secure_compare?(expected_signature,
|
39
|
+
secure_compare?(expected_signature, clean_signature)
|
16
40
|
rescue StandardError
|
17
41
|
false
|
18
42
|
end
|
19
43
|
|
20
44
|
# Generates a signature for the given payload
|
21
|
-
|
45
|
+
# @param secret [String] The webhook secret
|
46
|
+
# @param payload [String] The payload to sign
|
47
|
+
# @param include_prefix [Boolean] Whether to include the 'sha256=' prefix
|
48
|
+
# @return [String] The generated signature
|
49
|
+
def self.generate_signature(secret, payload, include_prefix: false)
|
22
50
|
digest = OpenSSL::Digest.new(ALGORITHM)
|
23
51
|
hmac = OpenSSL::HMAC.new(secret, digest)
|
24
52
|
hmac.update(payload)
|
25
|
-
hmac.hexdigest
|
53
|
+
signature = hmac.hexdigest
|
54
|
+
include_prefix ? "#{SIGNATURE_PREFIX}#{signature}" : signature
|
55
|
+
end
|
56
|
+
|
57
|
+
# Parse and validate webhook payload structure
|
58
|
+
# @param payload [String] The raw webhook payload JSON
|
59
|
+
# @return [Hash] Hash containing :is_batch boolean and :events array
|
60
|
+
# @raise [ArgumentError] If payload is invalid JSON or structure
|
61
|
+
def self.parse_webhook_payload(payload)
|
62
|
+
raise ArgumentError, 'Payload cannot be empty' if payload.nil? || payload.strip.empty?
|
63
|
+
|
64
|
+
data = JSON.parse(payload)
|
65
|
+
|
66
|
+
if data.is_a?(Array)
|
67
|
+
# Batch mode: array of events
|
68
|
+
events = data.map { |event| validate_and_parse_event(event) }
|
69
|
+
{ is_batch: true, events: events }
|
70
|
+
elsif data.is_a?(Hash) && data.key?('event')
|
71
|
+
# Single event mode
|
72
|
+
event = validate_and_parse_event(data)
|
73
|
+
{ is_batch: false, events: [event] }
|
74
|
+
else
|
75
|
+
raise ArgumentError, 'Invalid webhook payload structure'
|
76
|
+
end
|
77
|
+
rescue JSON::ParserError => e
|
78
|
+
raise ArgumentError, "Invalid JSON payload: #{e.message}"
|
79
|
+
end
|
80
|
+
|
81
|
+
# Get the webhook header name as documented
|
82
|
+
# @return [String] The correct header name for webhook signatures
|
83
|
+
def self.signature_header_name
|
84
|
+
SIGNATURE_HEADER_NAME
|
85
|
+
end
|
86
|
+
|
87
|
+
# Extract webhook signature from HTTP headers (supports multiple formats)
|
88
|
+
# @param headers [Hash] HTTP headers hash
|
89
|
+
# @return [String, nil] The webhook signature or nil if not found
|
90
|
+
def self.extract_signature_from_headers(headers)
|
91
|
+
return nil if headers.nil?
|
92
|
+
|
93
|
+
# Try documented header name first
|
94
|
+
signature = headers[SIGNATURE_HEADER_NAME]
|
95
|
+
return signature if signature
|
96
|
+
|
97
|
+
# Try uppercase version
|
98
|
+
upper_header = SIGNATURE_HEADER_NAME.upcase.tr('-', '_')
|
99
|
+
signature = headers[upper_header]
|
100
|
+
return signature if signature
|
101
|
+
|
102
|
+
# Try with HTTP_ prefix (common in Rack environments)
|
103
|
+
server_header = "HTTP_#{upper_header}"
|
104
|
+
signature = headers[server_header]
|
105
|
+
return signature if signature
|
106
|
+
|
107
|
+
nil
|
26
108
|
end
|
27
109
|
|
28
110
|
# Compares two strings in constant time to prevent timing attacks
|
111
|
+
# @param str_a [String] First string
|
112
|
+
# @param str_b [String] Second string
|
113
|
+
# @return [Boolean] true if strings are equal, false otherwise
|
29
114
|
def self.secure_compare?(str_a, str_b)
|
30
115
|
return false unless str_a.bytesize == str_b.bytesize
|
31
116
|
|
@@ -34,5 +119,49 @@ module Laneful
|
|
34
119
|
str_b.each_byte { |byte| res |= byte ^ l.shift }
|
35
120
|
res.zero?
|
36
121
|
end
|
122
|
+
|
123
|
+
# Validate individual event structure according to documentation
|
124
|
+
# @param event [Hash] The event data
|
125
|
+
# @return [Hash] Parsed event hash
|
126
|
+
# @raise [ArgumentError] If event structure is invalid
|
127
|
+
def self.validate_and_parse_event(event)
|
128
|
+
raise ArgumentError, 'Event must be a hash' unless event.is_a?(Hash)
|
129
|
+
|
130
|
+
# Required fields
|
131
|
+
required_fields = %w[event email lane_id message_id timestamp]
|
132
|
+
required_fields.each do |field|
|
133
|
+
raise ArgumentError, "Missing required field: #{field}" unless event.key?(field)
|
134
|
+
end
|
135
|
+
|
136
|
+
# Validate event type
|
137
|
+
event_type = event['event']
|
138
|
+
raise ArgumentError, "Invalid event type: #{event_type}" unless VALID_EVENT_TYPES.include?(event_type)
|
139
|
+
|
140
|
+
# Validate email format
|
141
|
+
email = event['email']
|
142
|
+
raise ArgumentError, "Invalid email format: #{email}" unless valid_email?(email)
|
143
|
+
|
144
|
+
# Validate timestamp is numeric
|
145
|
+
timestamp = event['timestamp']
|
146
|
+
begin
|
147
|
+
Integer(timestamp)
|
148
|
+
rescue ArgumentError, TypeError
|
149
|
+
raise ArgumentError, 'Invalid timestamp format'
|
150
|
+
end
|
151
|
+
|
152
|
+
# Validate lane_id is a valid UUID format
|
153
|
+
lane_id = event['lane_id']
|
154
|
+
raise ArgumentError, "Invalid lane_id format: #{lane_id}" unless UUID_PATTERN.match?(lane_id)
|
155
|
+
|
156
|
+
# Return event with all fields (required + optional)
|
157
|
+
event
|
158
|
+
end
|
159
|
+
|
160
|
+
# Simple email validation
|
161
|
+
# @param email [String] The email to validate
|
162
|
+
# @return [Boolean] true if email format is valid
|
163
|
+
def self.valid_email?(email)
|
164
|
+
email.is_a?(String) && email.include?('@') && email.include?('.')
|
165
|
+
end
|
37
166
|
end
|
38
167
|
end
|
data/lib/laneful.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: laneful-ruby
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0
|
4
|
+
version: 1.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Laneful Team
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-09-
|
11
|
+
date: 2025-09-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: httparty
|
@@ -52,7 +52,6 @@ files:
|
|
52
52
|
- README.md
|
53
53
|
- Rakefile
|
54
54
|
- examples/Gemfile
|
55
|
-
- examples/README.md
|
56
55
|
- examples/simple_example.rb
|
57
56
|
- laneful-ruby.gemspec
|
58
57
|
- lib/laneful.rb
|
data/examples/README.md
DELETED
@@ -1,53 +0,0 @@
|
|
1
|
-
# Laneful Ruby SDK Examples
|
2
|
-
|
3
|
-
This directory contains example code demonstrating how to use the Laneful Ruby SDK.
|
4
|
-
|
5
|
-
## Quick Start
|
6
|
-
|
7
|
-
1. **Install the gem:**
|
8
|
-
```bash
|
9
|
-
gem install laneful-ruby
|
10
|
-
```
|
11
|
-
|
12
|
-
2. **Or use Bundler:**
|
13
|
-
```bash
|
14
|
-
cd examples
|
15
|
-
bundle install
|
16
|
-
```
|
17
|
-
|
18
|
-
3. **Configure the example:**
|
19
|
-
Edit `simple_example.rb` and replace the placeholder values:
|
20
|
-
- `base_url`: Your Laneful endpoint URL
|
21
|
-
- `auth_token`: Your authentication token
|
22
|
-
- Email addresses: Replace with your actual email addresses
|
23
|
-
|
24
|
-
4. **Run the example:**
|
25
|
-
```bash
|
26
|
-
ruby simple_example.rb
|
27
|
-
```
|
28
|
-
|
29
|
-
## Example Files
|
30
|
-
|
31
|
-
- **`simple_example.rb`**: Demonstrates basic email sending functionality including:
|
32
|
-
- Simple text email
|
33
|
-
- HTML email with tracking
|
34
|
-
- Email with CC recipients
|
35
|
-
- Error handling
|
36
|
-
|
37
|
-
## Configuration
|
38
|
-
|
39
|
-
Before running the examples, make sure to:
|
40
|
-
|
41
|
-
1. **Get your credentials** from your Laneful dashboard
|
42
|
-
2. **Update the configuration** in the example files:
|
43
|
-
```ruby
|
44
|
-
base_url = 'https://your-endpoint.send.laneful.net'
|
45
|
-
auth_token = 'your-auth-token'
|
46
|
-
```
|
47
|
-
3. **Use valid email addresses** for testing
|
48
|
-
|
49
|
-
## More Examples
|
50
|
-
|
51
|
-
For more comprehensive examples and API documentation, visit:
|
52
|
-
- [Laneful Ruby SDK Documentation](https://docs.laneful.com/ruby)
|
53
|
-
- [GitHub Repository](https://github.com/laneful/laneful-ruby)
|