dk_payment_gateway 1.0.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.
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "faraday"
4
+ require "json"
5
+
6
+ module DkPaymentGateway
7
+ class Client
8
+ attr_reader :config, :access_token, :private_key
9
+
10
+ def initialize(config = nil)
11
+ @config = config || DkPaymentGateway.configuration
12
+ validate_configuration!
13
+ @access_token = nil
14
+ @private_key = nil
15
+ end
16
+
17
+ # Authentication methods
18
+ def authenticate!
19
+ auth = Authentication.new(self)
20
+ @access_token = auth.fetch_token
21
+ @private_key = auth.fetch_private_key
22
+ self
23
+ end
24
+
25
+ # Pull Payment methods
26
+ def pull_payment
27
+ @pull_payment ||= PullPayment.new(self)
28
+ end
29
+
30
+ # Intra Transaction methods
31
+ def intra_transaction
32
+ @intra_transaction ||= IntraTransaction.new(self)
33
+ end
34
+
35
+ # QR Payment methods
36
+ def qr_payment
37
+ @qr_payment ||= QrPayment.new(self)
38
+ end
39
+
40
+ # Transaction Status methods
41
+ def transaction_status
42
+ @transaction_status ||= TransactionStatus.new(self)
43
+ end
44
+
45
+ # HTTP request methods
46
+ def post(path, body: {}, headers: {}, skip_auth: false)
47
+ request(:post, path, body: body, headers: headers, skip_auth: skip_auth)
48
+ end
49
+
50
+ def get(path, params: {}, headers: {}, skip_auth: false)
51
+ request(:get, path, params: params, headers: headers, skip_auth: skip_auth)
52
+ end
53
+
54
+ private
55
+
56
+ def validate_configuration!
57
+ raise ConfigurationError, "Configuration is required" if config.nil?
58
+ unless config.valid?
59
+ raise ConfigurationError, "Missing required configuration fields: #{config.missing_fields.join(', ')}"
60
+ end
61
+ end
62
+
63
+ def request(method, path, body: {}, params: {}, headers: {}, skip_auth: false)
64
+ url = "#{config.base_url}#{path}"
65
+
66
+ response = connection.send(method) do |req|
67
+ req.url path
68
+ req.headers = build_headers(headers, skip_auth)
69
+ req.body = body.to_json if method == :post && !body.empty?
70
+ req.params = params if method == :get && !params.empty?
71
+ end
72
+
73
+ handle_response(response)
74
+ rescue Faraday::Error => e
75
+ raise NetworkError, "Network error: #{e.message}"
76
+ end
77
+
78
+ def connection
79
+ @connection ||= Faraday.new(url: config.base_url) do |conn|
80
+ conn.request :json
81
+ conn.response :json, content_type: /\bjson$/
82
+ conn.adapter Faraday.default_adapter
83
+ conn.options.timeout = config.timeout
84
+ conn.options.open_timeout = config.open_timeout
85
+ end
86
+ end
87
+
88
+ def build_headers(custom_headers = {}, skip_auth = false)
89
+ headers = {
90
+ "Content-Type" => "application/json",
91
+ "X-gravitee-api-key" => config.api_key
92
+ }
93
+
94
+ unless skip_auth
95
+ headers["Authorization"] = "Bearer #{access_token}" if access_token
96
+ headers["source_app"] = config.source_app
97
+ end
98
+
99
+ headers.merge(custom_headers)
100
+ end
101
+
102
+ def handle_response(response)
103
+ case response.status
104
+ when 200..299
105
+ response.body
106
+ when 400..499
107
+ handle_client_error(response)
108
+ when 500..599
109
+ handle_server_error(response)
110
+ else
111
+ raise APIError, "Unexpected response status: #{response.status}"
112
+ end
113
+ end
114
+
115
+ def handle_client_error(response)
116
+ body = response.body || {}
117
+ error_message = body["response_message"] || body["response_detail"] || "Client error"
118
+
119
+ raise InvalidParameterError.new(
120
+ error_message,
121
+ response_code: body["response_code"],
122
+ response_detail: body["response_detail"]
123
+ )
124
+ end
125
+
126
+ def handle_server_error(response)
127
+ body = response.body || {}
128
+ error_message = body["response_description"] || body["response_message"] || "Server error"
129
+
130
+ raise APIError.new(
131
+ error_message,
132
+ response_code: body["response_code"],
133
+ response_message: body["response_message"],
134
+ response_description: body["response_description"]
135
+ )
136
+ end
137
+ end
138
+ end
139
+
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DkPaymentGateway
4
+ class Configuration
5
+ attr_accessor :base_url, :api_key, :username, :password, :client_id,
6
+ :client_secret, :source_app, :timeout, :open_timeout
7
+
8
+ def initialize
9
+ @base_url = ENV['DK_BASE_URL']
10
+ @api_key = ENV['DK_API_KEY']
11
+ @username = ENV['DK_USERNAME']
12
+ @password = ENV['DK_PASSWORD']
13
+ @client_id = ENV['DK_CLIENT_ID']
14
+ @client_secret = ENV['DK_CLIENT_SECRET']
15
+ @source_app = ENV['DK_SOURCE_APP']
16
+ @timeout = 30
17
+ @open_timeout = 10
18
+ end
19
+
20
+ def valid?
21
+ required_fields.all? { |field| !send(field).nil? && !send(field).to_s.empty? }
22
+ end
23
+
24
+ def required_fields
25
+ %i[base_url api_key username password client_id client_secret source_app]
26
+ end
27
+
28
+ def missing_fields
29
+ required_fields.reject { |field| !send(field).nil? && !send(field).to_s.empty? }
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DkPaymentGateway
4
+ class Error < StandardError; end
5
+
6
+ class ConfigurationError < Error; end
7
+
8
+ class AuthenticationError < Error; end
9
+
10
+ class InvalidParameterError < Error
11
+ attr_reader :response_code, :response_detail
12
+
13
+ def initialize(message, response_code: nil, response_detail: nil)
14
+ super(message)
15
+ @response_code = response_code
16
+ @response_detail = response_detail
17
+ end
18
+ end
19
+
20
+ class APIError < Error
21
+ attr_reader :response_code, :response_message, :response_description, :response_detail
22
+
23
+ def initialize(message, response_code: nil, response_message: nil,
24
+ response_description: nil, response_detail: nil)
25
+ super(message)
26
+ @response_code = response_code
27
+ @response_message = response_message
28
+ @response_description = response_description
29
+ @response_detail = response_detail
30
+ end
31
+ end
32
+
33
+ class NetworkError < Error; end
34
+
35
+ class SignatureError < Error; end
36
+
37
+ class TransactionError < APIError; end
38
+ end
39
+
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DkPaymentGateway
4
+ class IntraTransaction
5
+ attr_reader :client
6
+
7
+ def initialize(client)
8
+ @client = client
9
+ end
10
+
11
+ # Intra (DK - DK) Beneficiary Account Inquiry
12
+ # Validates beneficiary account details before initiating a fund transfer
13
+ #
14
+ # @param params [Hash] Account inquiry parameters
15
+ # @option params [String] :request_id Unique identifier for the inquiry request
16
+ # @option params [Numeric] :amount Transaction amount
17
+ # @option params [String] :currency Currency code (e.g., "BTN")
18
+ # @option params [String] :bene_bank_code Beneficiary bank code (1060 for intra)
19
+ # @option params [String] :bene_account_number Beneficiary account number
20
+ # @option params [String] :source_account_name Source account holder name (optional)
21
+ # @option params [String] :source_account_number Source account number
22
+ #
23
+ # @return [Hash] Response containing inquiry_id and account_name
24
+ def account_inquiry(params)
25
+ validate_inquiry_params!(params)
26
+
27
+ request_body = build_inquiry_body(params)
28
+ signature_headers = generate_signature_headers(request_body)
29
+
30
+ response = client.post(
31
+ "/v1/beneficiary/account_inquiry",
32
+ body: request_body,
33
+ headers: signature_headers
34
+ )
35
+
36
+ validate_response!(response, "Account Inquiry")
37
+ response["response_data"]
38
+ end
39
+
40
+ # Intra (DK - DK) Fund Transfer
41
+ # Initiates a fund transfer after successful account inquiry
42
+ #
43
+ # @param params [Hash] Fund transfer parameters
44
+ # @option params [String] :request_id Unique identifier for the request
45
+ # @option params [String] :inquiry_id Inquiry ID from account_inquiry
46
+ # @option params [String] :source_app Source application identifier
47
+ # @option params [Numeric] :transaction_amount Amount to transfer
48
+ # @option params [String] :currency Currency code (e.g., "BTN")
49
+ # @option params [String] :transaction_datetime Transaction timestamp (ISO 8601)
50
+ # @option params [String] :bene_bank_code Beneficiary bank code
51
+ # @option params [String] :bene_account_number Beneficiary account number
52
+ # @option params [String] :bene_cust_name Beneficiary customer name
53
+ # @option params [String] :source_account_name Source account holder name (optional)
54
+ # @option params [String] :source_account_number Source account number
55
+ # @option params [String] :payment_type Payment type (should be "INTRA" for DK-DK)
56
+ # @option params [String] :narration Transaction description/purpose
57
+ #
58
+ # @return [Hash] Response containing inquiry_id and txn_status_id
59
+ def fund_transfer(params)
60
+ validate_transfer_params!(params)
61
+
62
+ request_body = build_transfer_body(params)
63
+ signature_headers = generate_signature_headers(request_body)
64
+
65
+ response = client.post(
66
+ "/v1/initiate/transaction",
67
+ body: request_body,
68
+ headers: signature_headers
69
+ )
70
+
71
+ validate_response!(response, "Fund Transfer")
72
+ response["response_data"]
73
+ end
74
+
75
+ private
76
+
77
+ def build_inquiry_body(params)
78
+ {
79
+ request_id: params[:request_id],
80
+ amount: params[:amount].to_s,
81
+ currency: params[:currency],
82
+ bene_bank_code: params[:bene_bank_code],
83
+ bene_account_number: params[:bene_account_number],
84
+ soure_account_number: params[:source_account_number] # Note: API has typo "soure"
85
+ }.tap do |body|
86
+ body[:source_account_name] = params[:source_account_name] if params[:source_account_name]
87
+ end
88
+ end
89
+
90
+ def build_transfer_body(params)
91
+ {
92
+ request_id: params[:request_id],
93
+ inquiry_id: params[:inquiry_id],
94
+ transaction_datetime: params[:transaction_datetime],
95
+ source_app: params[:source_app] || client.config.source_app,
96
+ transaction_amount: params[:transaction_amount],
97
+ currency: params[:currency],
98
+ payment_type: params[:payment_type] || "INTRA",
99
+ source_account_number: params[:source_account_number],
100
+ bene_cust_name: params[:bene_cust_name],
101
+ bene_account_number: params[:bene_account_number],
102
+ bene_bank_code: params[:bene_bank_code],
103
+ narration: params[:narration]
104
+ }.tap do |body|
105
+ body[:source_account_name] = params[:source_account_name] if params[:source_account_name]
106
+ end
107
+ end
108
+
109
+ def validate_inquiry_params!(params)
110
+ required = [:request_id, :amount, :currency, :bene_bank_code,
111
+ :bene_account_number, :source_account_number]
112
+
113
+ missing = required.select { |key| params[key].nil? || params[key].to_s.empty? }
114
+
115
+ raise InvalidParameterError, "Missing required parameters: #{missing.join(', ')}" unless missing.empty?
116
+ end
117
+
118
+ def validate_transfer_params!(params)
119
+ required = [:request_id, :inquiry_id, :transaction_amount, :currency,
120
+ :transaction_datetime, :bene_bank_code, :bene_account_number,
121
+ :bene_cust_name, :source_account_number, :narration]
122
+
123
+ missing = required.select { |key| params[key].nil? || params[key].to_s.empty? }
124
+
125
+ raise InvalidParameterError, "Missing required parameters: #{missing.join(', ')}" unless missing.empty?
126
+ end
127
+
128
+ def validate_response!(response, operation)
129
+ unless response.is_a?(Hash) && response["response_code"] == "0000"
130
+ error_msg = response["response_description"] || response["response_message"] || "Unknown error"
131
+ raise TransactionError.new(
132
+ "#{operation} failed: #{error_msg}",
133
+ response_code: response["response_code"],
134
+ response_message: response["response_message"],
135
+ response_description: response["response_description"]
136
+ )
137
+ end
138
+ end
139
+
140
+ def generate_signature_headers(request_body)
141
+ raise SignatureError, "Private key not available. Call client.authenticate! first" unless client.private_key
142
+
143
+ Signature.generate(client.private_key, request_body)
144
+ end
145
+ end
146
+ end
147
+
@@ -0,0 +1,155 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DkPaymentGateway
4
+ class PullPayment
5
+ attr_reader :client
6
+
7
+ def initialize(client)
8
+ @client = client
9
+ end
10
+
11
+ # Payment Gateway Authorization (Account inquiry and OTP request)
12
+ #
13
+ # @param params [Hash] Authorization parameters
14
+ # @option params [String] :transaction_datetime Transaction timestamp in UTC (ISO 8601)
15
+ # @option params [String] :stan_number 12-digit unique transaction number
16
+ # @option params [Numeric] :transaction_amount Amount of the transaction
17
+ # @option params [Numeric] :transaction_fee Transaction fee (use 0 if no fee)
18
+ # @option params [String] :payment_desc Payment description
19
+ # @option params [String] :account_number Beneficiary account number
20
+ # @option params [String] :account_name Beneficiary account name
21
+ # @option params [String] :email_id Beneficiary email (optional)
22
+ # @option params [String] :phone_number Beneficiary phone number
23
+ # @option params [String] :remitter_account_number Remitter account number
24
+ # @option params [String] :remitter_account_name Remitter account name
25
+ # @option params [String] :remitter_bank_id Remitter bank identifier
26
+ #
27
+ # @return [Hash] Response containing bfs_txn_id, stan_number, account_number, remitter_account_number
28
+ def authorize(params)
29
+ validate_authorization_params!(params)
30
+
31
+ request_body = build_authorization_body(params)
32
+ signature_headers = generate_signature_headers(request_body)
33
+
34
+ response = client.post(
35
+ "/v1/account_auth/pull-payment",
36
+ body: request_body,
37
+ headers: signature_headers
38
+ )
39
+
40
+ validate_response!(response, "Authorization")
41
+ response["response_data"]
42
+ end
43
+
44
+ # Payment Gateway Debit Request
45
+ # Completes a previously authorized payment by verifying OTP
46
+ #
47
+ # @param params [Hash] Debit request parameters
48
+ # @option params [String] :request_id Unique identifier for the request
49
+ # @option params [String] :bfs_txn_id Transaction ID from authorization
50
+ # @option params [String] :bfs_remitter_otp OTP sent to remitter
51
+ # @option params [String] :bfs_order_no Order number (optional)
52
+ #
53
+ # @return [Hash] Response containing bfs_txn_id, code, description
54
+ def debit(params)
55
+ validate_debit_params!(params)
56
+
57
+ request_body = build_debit_body(params)
58
+ signature_headers = generate_signature_headers(request_body)
59
+
60
+ response = client.post(
61
+ "/v1/debit_request/pull-payment",
62
+ body: request_body,
63
+ headers: signature_headers
64
+ )
65
+
66
+ validate_response!(response, "Debit")
67
+ response["response_data"]
68
+ end
69
+
70
+ # Generate STAN number
71
+ # @param source_app_suffix [String] Last 4 digits of source_app (e.g., "0201")
72
+ # @param transaction_identifier [String] 8-digit identifier (timestamp or counter)
73
+ # @return [String] 12-digit STAN number
74
+ def self.generate_stan(source_app_suffix, transaction_identifier = nil)
75
+ suffix = source_app_suffix.to_s[-4..]
76
+
77
+ identifier = if transaction_identifier
78
+ transaction_identifier.to_s[-8..]
79
+ else
80
+ # Generate from current timestamp (HHMMSSMS format)
81
+ time = Time.now
82
+ format("%02d%02d%02d%02d", time.hour, time.min, time.sec, time.usec / 10000)
83
+ end
84
+
85
+ "#{suffix}#{identifier}"
86
+ end
87
+
88
+ private
89
+
90
+ def build_authorization_body(params)
91
+ {
92
+ transaction_datetime: params[:transaction_datetime],
93
+ stan_number: params[:stan_number],
94
+ transaction_amount: params[:transaction_amount],
95
+ transaction_fee: params[:transaction_fee] || 0,
96
+ payment_desc: params[:payment_desc],
97
+ account_number: params[:account_number],
98
+ account_name: params[:account_name],
99
+ phone_number: params[:phone_number],
100
+ remitter_account_number: params[:remitter_account_number],
101
+ remitter_account_name: params[:remitter_account_name],
102
+ remitter_bank_id: params[:remitter_bank_id]
103
+ }.tap do |body|
104
+ body[:email_id] = params[:email_id] if params[:email_id]
105
+ end
106
+ end
107
+
108
+ def build_debit_body(params)
109
+ {
110
+ request_id: params[:request_id],
111
+ bfs_bfsTxnId: params[:bfs_txn_id],
112
+ bfs_remitter_Otp: params[:bfs_remitter_otp]
113
+ }.tap do |body|
114
+ body[:bfs_orderNo] = params[:bfs_order_no] if params[:bfs_order_no]
115
+ end
116
+ end
117
+
118
+ def validate_authorization_params!(params)
119
+ required = [:transaction_datetime, :stan_number, :transaction_amount, :payment_desc,
120
+ :account_number, :account_name, :phone_number, :remitter_account_number,
121
+ :remitter_account_name, :remitter_bank_id]
122
+
123
+ missing = required.select { |key| params[key].nil? || params[key].to_s.empty? }
124
+
125
+ raise InvalidParameterError, "Missing required parameters: #{missing.join(', ')}" unless missing.empty?
126
+ end
127
+
128
+ def validate_debit_params!(params)
129
+ required = [:request_id, :bfs_txn_id, :bfs_remitter_otp]
130
+
131
+ missing = required.select { |key| params[key].nil? || params[key].to_s.empty? }
132
+
133
+ raise InvalidParameterError, "Missing required parameters: #{missing.join(', ')}" unless missing.empty?
134
+ end
135
+
136
+ def validate_response!(response, operation)
137
+ unless response.is_a?(Hash) && response["response_code"] == "0000"
138
+ error_msg = response["response_description"] || response["response_message"] || "Unknown error"
139
+ raise TransactionError.new(
140
+ "#{operation} failed: #{error_msg}",
141
+ response_code: response["response_code"],
142
+ response_message: response["response_message"],
143
+ response_description: response["response_description"]
144
+ )
145
+ end
146
+ end
147
+
148
+ def generate_signature_headers(request_body)
149
+ raise SignatureError, "Private key not available. Call client.authenticate! first" unless client.private_key
150
+
151
+ Signature.generate(client.private_key, request_body)
152
+ end
153
+ end
154
+ end
155
+
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DkPaymentGateway
4
+ class QrPayment
5
+ attr_reader :client
6
+
7
+ def initialize(client)
8
+ @client = client
9
+ end
10
+
11
+ # Generate QR Code for payment transaction
12
+ #
13
+ # If amount = 0, generates a Static QR (payer enters amount)
14
+ # If amount > 0, generates a Dynamic QR (amount is fixed)
15
+ #
16
+ # @param params [Hash] QR generation parameters
17
+ # @option params [String] :request_id Unique identifier for the request
18
+ # @option params [String] :currency Currency code (e.g., "BTN")
19
+ # @option params [String] :bene_account_number Beneficiary account number
20
+ # @option params [Numeric] :amount Transaction amount (0 for static QR, >0 for dynamic QR)
21
+ # @option params [String] :mcc_code Merchant Category Code (ISO 18245)
22
+ # @option params [String] :remarks Optional notes or comments (optional)
23
+ #
24
+ # @return [Hash] Response containing base64 encoded QR image
25
+ def generate_qr(params)
26
+ validate_qr_params!(params)
27
+
28
+ request_body = build_qr_body(params)
29
+ signature_headers = generate_signature_headers(request_body)
30
+
31
+ response = client.post(
32
+ "/v1/generate_qr",
33
+ body: request_body,
34
+ headers: signature_headers
35
+ )
36
+
37
+ validate_response!(response, "QR Generation")
38
+ response["response_data"]
39
+ end
40
+
41
+ # Decode base64 QR image and save to file
42
+ # @param base64_image [String] Base64 encoded image
43
+ # @param file_path [String] Path to save the image
44
+ def save_qr_image(base64_image, file_path)
45
+ require "base64"
46
+
47
+ image_data = Base64.decode64(base64_image)
48
+ File.open(file_path, "wb") { |f| f.write(image_data) }
49
+
50
+ file_path
51
+ end
52
+
53
+ private
54
+
55
+ def build_qr_body(params)
56
+ {
57
+ request_id: params[:request_id],
58
+ currency: params[:currency],
59
+ bene_account_number: params[:bene_account_number],
60
+ amount: params[:amount],
61
+ mcc_code: params[:mcc_code]
62
+ }.tap do |body|
63
+ body[:remarks] = params[:remarks] if params[:remarks]
64
+ end
65
+ end
66
+
67
+ def validate_qr_params!(params)
68
+ required = [:request_id, :currency, :bene_account_number, :amount, :mcc_code]
69
+
70
+ missing = required.select { |key| params[key].nil? }
71
+
72
+ raise InvalidParameterError, "Missing required parameters: #{missing.join(', ')}" unless missing.empty?
73
+
74
+ # Validate amount is numeric
75
+ unless params[:amount].is_a?(Numeric) || params[:amount].to_s.match?(/^\d+(\.\d+)?$/)
76
+ raise InvalidParameterError, "Amount must be a valid number"
77
+ end
78
+ end
79
+
80
+ def validate_response!(response, operation)
81
+ unless response.is_a?(Hash) && response["response_code"] == "0000"
82
+ error_msg = response["response_detail"] || response["response_message"] || "Unknown error"
83
+ raise TransactionError.new(
84
+ "#{operation} failed: #{error_msg}",
85
+ response_code: response["response_code"],
86
+ response_detail: response["response_detail"]
87
+ )
88
+ end
89
+ end
90
+
91
+ def generate_signature_headers(request_body)
92
+ raise SignatureError, "Private key not available. Call client.authenticate! first" unless client.private_key
93
+
94
+ Signature.generate(client.private_key, request_body)
95
+ end
96
+ end
97
+ end
98
+
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "jwt"
4
+ require "base64"
5
+ require "json"
6
+ require "time"
7
+ require "securerandom"
8
+
9
+ module DkPaymentGateway
10
+ class Signature
11
+ attr_reader :private_key
12
+
13
+ def initialize(private_key)
14
+ @private_key = private_key
15
+ end
16
+
17
+ # Generate signature headers for a request
18
+ # Returns a hash with DK-Signature, DK-Timestamp, and DK-Nonce
19
+ def generate_headers(request_body)
20
+ timestamp = generate_timestamp
21
+ nonce = generate_nonce
22
+ signature = sign_request(request_body, timestamp, nonce)
23
+
24
+ {
25
+ "DK-Signature" => "DKSignature #{signature}",
26
+ "DK-Timestamp" => timestamp,
27
+ "DK-Nonce" => nonce
28
+ }
29
+ end
30
+
31
+ private
32
+
33
+ # Generate ISO 8601 timestamp
34
+ def generate_timestamp
35
+ Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ")
36
+ end
37
+
38
+ # Generate unique alphanumeric nonce
39
+ def generate_nonce
40
+ SecureRandom.hex(16)
41
+ end
42
+
43
+ # Sign the request using RS256 algorithm
44
+ def sign_request(request_body, timestamp, nonce)
45
+ # Serialize request body to canonical JSON (sorted keys, no spaces)
46
+ request_body_str = JSON.generate(request_body, space: "", object_nl: "", array_nl: "")
47
+
48
+ # Base64 encode the request body
49
+ body_base64 = Base64.strict_encode64(request_body_str)
50
+
51
+ # Create the payload for signing
52
+ token_payload = {
53
+ "data" => body_base64,
54
+ "timestamp" => timestamp,
55
+ "nonce" => nonce
56
+ }
57
+
58
+ # Sign using RS256
59
+ JWT.encode(token_payload, OpenSSL::PKey::RSA.new(private_key), "RS256")
60
+ rescue => e
61
+ raise SignatureError, "Failed to generate signature: #{e.message}"
62
+ end
63
+
64
+ class << self
65
+ # Convenience method to generate signature headers
66
+ def generate(private_key, request_body)
67
+ new(private_key).generate_headers(request_body)
68
+ end
69
+ end
70
+ end
71
+ end
72
+