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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 92cd0ae4c28744530a747d496d4b22e65dc92e6f2238739b59e75f7dd36769a7
4
- data.tar.gz: 87e2c139cb4e26e60c6209f87b88241a0e4161e19b1ffc2dd9da0f254654843c
3
+ metadata.gz: 3b7c5f86e1e341b49312a27486067c2427323569459dba65f8fb2b247629a1fb
4
+ data.tar.gz: 649ca87028b16f0ede2b1e063390fe13043e12401e70e4637d6c69922d7b415e
5
5
  SHA512:
6
- metadata.gz: f8218f49eb9b96cce7b4941fdfe058ef88017c356b0dedc8c18f7bd46cf0398a598255fcbe0a74737d695a0282f0a5e7660a229a94de7406e9694c07b0dcfa72
7
- data.tar.gz: 61498ac2d546d3971e6054b5603dbb777dac4b14efb4ac946aa4ac90f89765ffc5d347192b7d5200800ef4dfda2af033d91b1c58d6cdd3704bac9f9c208a199a
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['X-Laneful-Signature']
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
+
@@ -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-endpoint.send.laneful.net'
12
- auth_token = 'your-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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Laneful
4
- VERSION = '1.0.1'
4
+ VERSION = '1.1.0'
5
5
  end
@@ -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
- # Verifies a webhook signature
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, 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
- def self.generate_signature(secret, payload)
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
@@ -23,5 +23,5 @@ module Laneful
23
23
  DEFAULT_TIMEOUT = 30
24
24
 
25
25
  # User agent string
26
- USER_AGENT = 'laneful-ruby/1.0.0'
26
+ USER_AGENT = 'laneful-ruby/1.0.1'
27
27
  end
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.1
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-05 00:00:00.000000000 Z
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)