ridebuilder-affiliate 0.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 421efc119490992dea3cb78e24bb7b0c380ebc458b60f3d4253ac5669e28c429
4
+ data.tar.gz: 9eb9d1a0ba25f32d74ad6145e6fab9b0c382aacdf2ae539ac1274000c70e4dd1
5
+ SHA512:
6
+ metadata.gz: 00b8b927dacb04a95ea7bce0e87c55479f73748d884124178db8a9638321bd395e2ddb7746ebb79a6779755441117d40df80ebd3e2613d82c9d07c8bbac474b9
7
+ data.tar.gz: de5af83438ff7083f3a79fe6688a6c542d8a82a0323493fc719b998445b91aafb2ca76aea679b75c248debe578daba6345ed26eb78b109ff53a2e5280fc4a046
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 RideBuilder
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,112 @@
1
+ # ridebuilder-affiliate (Ruby)
2
+
3
+ Server-side SDK for RideBuilder's FirstParty affiliate program. It does two things:
4
+
5
+ 1. **Capture** the `click_id` a shopper arrives with, so your backend can bind it to the cart/order.
6
+ 2. **Report** checkout and return postbacks to RideBuilder (auth, retries, idempotency handled).
7
+
8
+ Mirrors the Node/.NET/Python/PHP/Java SDKs — same contract, verified by the shared [conformance suite](../conformance).
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ gem install ridebuilder-affiliate
14
+ ```
15
+
16
+ or in a `Gemfile`: `gem "ridebuilder-affiliate"`. Requires **Ruby 3.0+**. **No runtime dependencies** — the
17
+ default transport uses the stdlib `net/http`.
18
+
19
+ ## The pattern: capture at landing, bind to the order
20
+
21
+ ```ruby
22
+ require "ridebuilder/affiliate"
23
+
24
+ # 1. On landing, read a validated click_id off the request URL and persist it onto YOUR cart record.
25
+ click_id = RideBuilder::Affiliate::Capture.from_url(request.fullpath)
26
+ cart.ridebuilder_click_id = click_id if click_id
27
+
28
+ # 2. At order time, send the postback from your backend.
29
+ rb = RideBuilder::Affiliate::Client.new(api_key: ENV.fetch("RIDEBUILDER_API_KEY"))
30
+ rb.report_checkout(
31
+ order_id: order.id,
32
+ subtotal: "199.99", # major units; a string keeps it exact
33
+ currency: "USD",
34
+ click_id: order.ridebuilder_click_id
35
+ )
36
+ ```
37
+
38
+ Store the API key server-side (env/secrets) — never in frontend code.
39
+
40
+ ## Decoupled frontend (e.g. React) + separate Ruby backend
41
+
42
+ The browser **snippet** captures the `click_id` into a first-party cookie; get it to your backend one of two ways:
43
+
44
+ ```ruby
45
+ # Same registrable domain — the cookie rides along; read it off the Cookie header:
46
+ click_id = RideBuilder::Affiliate::Capture.from_cookie_header(request.get_header("HTTP_COOKIE"))
47
+
48
+ # Cross-domain / mobile — the frontend forwards it in the checkout call:
49
+ click_id = RideBuilder::Affiliate::Capture.from_headers(request.headers) # default: X-RideBuilder-Click-Id
50
+ ```
51
+
52
+ Either way, `report_checkout` is unchanged — that's the SDK's real value in a decoupled setup.
53
+
54
+ ### Refunds
55
+
56
+ ```ruby
57
+ rb.report_return(return_id: refund.id, order_id: order.id, refund_amount: "49.95", currency: "USD")
58
+ ```
59
+
60
+ ## Integration protocol (register / verify / heartbeat)
61
+
62
+ ```ruby
63
+ rb = RideBuilder::Affiliate::Client.new(api_key: api_key, environment: "production") # or "sandbox"
64
+
65
+ reg = rb.register # handshake on install/startup; returns a stable integration id
66
+ rb.verify # deploy/CI self-test — raises RideBuilder::Affiliate::Error on a bad/rotated key
67
+ rb.heartbeat # periodic liveness (call on a schedule)
68
+ ```
69
+
70
+ The SDK reports its own `type` (`ruby_sdk`), `version`, and default capabilities.
71
+
72
+ ## Capture helpers
73
+
74
+ All validate `ref == "ridebuilder"` and the UUID-v4 click_id, returning `nil` otherwise:
75
+
76
+ - `Capture.from_url(url)` — from an absolute or relative URL.
77
+ - `Capture.from_query(hash)` — from a decoded query map.
78
+ - `Capture.from_cookie_header(cookie_header)` — recover it from the `ridebuilder_attribution` cookie.
79
+ - `Capture.from_headers(headers, name = "X-RideBuilder-Click-Id")` — from a forwarding header (decoupled path).
80
+
81
+ ## Client options
82
+
83
+ ```ruby
84
+ RideBuilder::Affiliate::Client.new(
85
+ api_key:, # required
86
+ base_url: nil, # defaults to https://api.ridebuilder.com/v1
87
+ max_retries: 3, # retries on network errors, timeouts, 5xx, 429
88
+ timeout_ms: 10_000, # per-attempt timeout
89
+ environment: "production",
90
+ transport: nil # inject an object responding to #call(method, url, headers, body)
91
+ )
92
+ ```
93
+
94
+ `report_checkout` / `report_return` return `PostbackResult` (`.accepted`, `.status`; `202` = accepted,
95
+ validated asynchronously). Invalid input raises a non-retryable `RideBuilder::Affiliate::Error`; auth/size
96
+ failures (`401`, `413`) raise with `.status` and `.error_code`. Amounts must be `> 0` with at most 2 decimal
97
+ places (pass a **string** to avoid float rounding) or the call raises up front.
98
+
99
+ ## Tests
100
+
101
+ Plain Ruby, no test framework required:
102
+
103
+ ```bash
104
+ ruby test/conformance.rb # the shared cross-language fixtures
105
+ ruby test/unit.rb # validation, capture, money, retry/error, identity
106
+ ```
107
+
108
+ ## Contract
109
+
110
+ Wraps the RideBuilder affiliate REST contract — `POST /v1/postback/checkout`, `/postback/return`,
111
+ `/postback/health`, the `/integration/*` endpoints, the `/redirect` link format, and API-key provisioning.
112
+ Verified byte-for-byte against the Node/.NET/Python/PHP/Java SDKs by the shared conformance fixtures.
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RideBuilder
4
+ module Affiliate
5
+ # Framework-free capture helpers — turn an incoming request into a validated click_id (or nil). Every
6
+ # helper enforces ref == "ridebuilder" and the UUID-v4 click_id, the same rules the browser snippet uses.
7
+ module Capture
8
+ module_function
9
+
10
+ # Read a validated click_id from a raw URL (absolute or relative like "/landing?...").
11
+ def from_url(url)
12
+ return nil if url.nil? || url.empty?
13
+
14
+ query = url.split("?", 2)[1]
15
+ return nil if query.nil?
16
+
17
+ query = query.split("#", 2)[0]
18
+ from_query(parse_query(query))
19
+ end
20
+
21
+ # Read a validated click_id from a decoded query map.
22
+ def from_query(query)
23
+ return nil unless query && query["ref"] == ClickId::REF
24
+
25
+ click_id = query["click_id"]
26
+ ClickId.valid?(click_id) ? click_id : nil
27
+ end
28
+
29
+ # Recover the click_id from the ridebuilder_attribution cookie the browser snippet set.
30
+ def from_cookie_header(cookie_header)
31
+ return nil if cookie_header.nil? || cookie_header.empty?
32
+
33
+ raw = parse_cookie_header(cookie_header)[ClickId::COOKIE_NAME]
34
+ return nil if raw.nil? || raw.empty?
35
+
36
+ parse_attribution_cookie(raw)&.click_id
37
+ end
38
+
39
+ # Read a validated click_id off a forwarding header (case-insensitive) — the decoupled path.
40
+ def from_headers(headers, name = "X-RideBuilder-Click-Id")
41
+ return nil unless headers
42
+
43
+ target = name.downcase
44
+ headers.each do |key, value|
45
+ next unless key.to_s.downcase == target
46
+
47
+ val = value.is_a?(Array) ? value.first : value
48
+ return ClickId.valid?(val) ? val : nil
49
+ end
50
+ nil
51
+ end
52
+
53
+ # Parse the raw ridebuilder_attribution cookie value (URL-encoded JSON the snippet wrote).
54
+ def parse_attribution_cookie(raw_value)
55
+ parsed = JSON.parse(CGI.unescape(raw_value))
56
+ return nil unless parsed.is_a?(Hash) && ClickId.valid?(parsed["click_id"]) && parsed["ref"] == ClickId::REF
57
+
58
+ Attribution.new(parsed["click_id"], parsed["ref"], parsed["clicked_at"])
59
+ rescue JSON::ParserError
60
+ nil
61
+ end
62
+
63
+ # Split a Cookie header ("a=1; b=2") into a name=>value hash (first '=' separates).
64
+ def parse_cookie_header(cookie_header)
65
+ out = {}
66
+ cookie_header.split(";").each do |part|
67
+ i = part.index("=")
68
+ out[part[0...i].strip] = part[(i + 1)..].strip if i && i.positive?
69
+ end
70
+ out
71
+ end
72
+
73
+ def parse_query(query)
74
+ out = {}
75
+ return out if query.nil? || query.empty?
76
+
77
+ query.split("&").each do |pair|
78
+ next if pair.empty?
79
+
80
+ key, value = pair.split("=", 2)
81
+ key = CGI.unescape(key)
82
+ out[key] = value.nil? ? "" : CGI.unescape(value) unless out.key?(key)
83
+ end
84
+ out
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RideBuilder
4
+ module Affiliate
5
+ # Shared attribution constants and the strict UUID-v4 click_id check the browser snippet also enforces.
6
+ module ClickId
7
+ REF = "ridebuilder"
8
+ COOKIE_NAME = "ridebuilder_attribution"
9
+ PATTERN = /\A[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\z/i
10
+
11
+ def self.valid?(value)
12
+ value.is_a?(String) && !(PATTERN =~ value).nil?
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,156 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RideBuilder
4
+ module Affiliate
5
+ # Server-side client for RideBuilder's FirstParty affiliate program: report checkout/return postbacks and
6
+ # prove integration liveness. Handles auth, per-attempt timeout, retry with exponential backoff + jitter,
7
+ # and idempotency. Port of the Node RideBuilderClient — same wire contract, verified by sdk/conformance.
8
+ class Client
9
+ def initialize(api_key:, base_url: nil, max_retries: 3, timeout_ms: 10_000, environment: "production", transport: nil)
10
+ raise Error.new("api_key must be a non-empty string", retryable: false) if api_key.nil? || api_key.strip.empty?
11
+
12
+ @api_key = api_key
13
+ @base_url = (base_url || DEFAULT_BASE_URL).sub(%r{/+\z}, "")
14
+ @max_retries = max_retries
15
+ @environment = environment == "sandbox" ? "sandbox" : "production"
16
+ @transport = transport || NetHttpTransport.new(timeout_ms)
17
+ end
18
+
19
+ # order_id is the server-side idempotency key, so a retried checkout never double-counts.
20
+ def report_checkout(order_id:, subtotal:, currency:, click_id:)
21
+ assert_non_empty(order_id, "order_id")
22
+ amount = normalize_amount(subtotal, "subtotal")
23
+ assert_currency(currency)
24
+ unless ClickId.valid?(click_id)
25
+ raise Error.new("click_id must be a valid RideBuilder click_id (UUID v4)", retryable: false)
26
+ end
27
+
28
+ res = send_request("POST", "/postback/checkout", {
29
+ "order_id" => order_id, "order_subtotal" => amount, "currency" => currency,
30
+ "ref" => ClickId::REF, "click_id" => click_id
31
+ })
32
+ PostbackResult.new(true, res.status)
33
+ end
34
+
35
+ # return_id is the server-side idempotency key, so a retried refund never double-counts.
36
+ def report_return(return_id:, order_id:, refund_amount:, currency:)
37
+ assert_non_empty(return_id, "return_id")
38
+ assert_non_empty(order_id, "order_id")
39
+ amount = normalize_amount(refund_amount, "refund_amount")
40
+ assert_currency(currency)
41
+
42
+ res = send_request("POST", "/postback/return", {
43
+ "return_id" => return_id, "order_id" => order_id, "refund_amount" => amount, "currency" => currency
44
+ })
45
+ PostbackResult.new(true, res.status)
46
+ end
47
+
48
+ # Authenticated liveness ping (legacy alias). Raises a terminal Error on a bad key.
49
+ def health_check
50
+ HealthResult.new(true, send_request("POST", "/postback/health").status)
51
+ end
52
+
53
+ # Announce this integration on install/startup — the handshake. Idempotent; returns the integration id.
54
+ def register(capabilities: nil)
55
+ res = send_request("POST", "/integration/register", {
56
+ "type" => SDK_TYPE, "environment" => @environment, "version" => VERSION,
57
+ "capabilities" => capabilities || DEFAULT_CAPABILITIES
58
+ })
59
+ body = res.json.is_a?(Hash) ? res.json : {}
60
+ RegisterResult.new(body["integrationId"] || "", body["status"] || "connected")
61
+ end
62
+
63
+ # Self-test: verifies the API key is valid + active. Raises a terminal Error on a bad/rotated key.
64
+ def verify
65
+ HealthResult.new(true, send_request("GET", "/integration/verify").status)
66
+ end
67
+
68
+ # Periodic liveness. Call on a schedule so RideBuilder can tell "alive" from "went dark".
69
+ def heartbeat
70
+ res = send_request("POST", "/integration/heartbeat", {
71
+ "type" => SDK_TYPE, "environment" => @environment, "version" => VERSION
72
+ })
73
+ HealthResult.new(true, res.status)
74
+ end
75
+
76
+ private
77
+
78
+ # Shared transport: auth header, retry with backoff on 5xx/429/network, terminal raise on other 4xx.
79
+ def send_request(method, path, payload = nil)
80
+ url = @base_url + path
81
+ body = payload ? JSON.generate(payload) : nil
82
+ headers = { "Authorization" => "Bearer #{@api_key}" }
83
+ headers["Content-Type"] = "application/json" if body
84
+
85
+ last_error = nil
86
+ (0..@max_retries).each do |attempt|
87
+ sleep(backoff_ms(attempt) / 1000.0) if attempt.positive?
88
+
89
+ begin
90
+ res = @transport.call(method, url, headers, body)
91
+ rescue TransportError => e
92
+ last_error = Error.new(e.message, retryable: true)
93
+ next
94
+ end
95
+
96
+ case classify(res.status)
97
+ when :success
98
+ return res
99
+ when :retry
100
+ last_error = Error.new("RideBuilder request failed with #{res.status}", retryable: true, status: res.status)
101
+ next
102
+ else
103
+ code = error_code(res)
104
+ message = code ? "RideBuilder request rejected (#{res.status}): #{code}" : "RideBuilder request rejected with #{res.status}"
105
+ raise Error.new(message, retryable: false, status: res.status, error_code: code)
106
+ end
107
+ end
108
+
109
+ raise(last_error || Error.new("RideBuilder request failed after retries", retryable: true))
110
+ end
111
+
112
+ def classify(status)
113
+ return :success if (200..299).cover?(status)
114
+ return :retry if status >= 500 || status == 429
115
+
116
+ :terminal
117
+ end
118
+
119
+ def error_code(res)
120
+ body = res.json
121
+ body.is_a?(Hash) && body["error"].is_a?(String) ? body["error"] : nil
122
+ end
123
+
124
+ # 250 * 2^(attempt-1) ms + up to 100 ms jitter — identical to the Node/.NET/Python/PHP/Java backoff.
125
+ def backoff_ms(attempt)
126
+ 250 * (2**(attempt - 1)) + rand(0..99)
127
+ end
128
+
129
+ def assert_non_empty(value, field)
130
+ return if value.is_a?(String) && !value.strip.empty?
131
+
132
+ raise Error.new("#{field} must be a non-empty string", retryable: false)
133
+ end
134
+
135
+ def assert_currency(value)
136
+ return if value.is_a?(String) && value.match?(/\A[A-Z]{3}\z/)
137
+
138
+ raise Error.new("currency must be a 3-letter uppercase ISO-4217 code", retryable: false)
139
+ end
140
+
141
+ # Validate an amount and return it as a Float for JSON-number emission. Accepts Integer/Float/String;
142
+ # rejects <= 0 and > 2 decimal places up front (matching the stricter .NET/Python/Java clients).
143
+ def normalize_amount(value, field)
144
+ str = case value
145
+ when Integer, Float then value.to_s
146
+ when String then value.strip
147
+ end
148
+ num = str.nil? ? nil : (Float(str) rescue nil)
149
+ raise Error.new("#{field} must be a positive number", retryable: false) if num.nil? || num <= 0
150
+ raise Error.new("#{field} must have at most 2 decimal places", retryable: false) unless str.match?(/\A\d+(\.\d{1,2})?\z/)
151
+
152
+ num
153
+ end
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RideBuilder
4
+ module Affiliate
5
+ # Every failure the SDK raises. +retryable+ is false for input-validation and terminal 4xx errors, true
6
+ # for 5xx/429/network failures (surfaced only after retries are exhausted). Port of the Node RideBuilderError.
7
+ class Error < StandardError
8
+ attr_reader :status, :error_code, :retryable
9
+
10
+ def initialize(message, retryable:, status: nil, error_code: nil)
11
+ super(message)
12
+ @status = status
13
+ @error_code = error_code
14
+ @retryable = retryable
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RideBuilder
4
+ module Affiliate
5
+ # A 202 status means received, not yet validated — RideBuilder validates asynchronously.
6
+ PostbackResult = Struct.new(:accepted, :status)
7
+ HealthResult = Struct.new(:ok, :status)
8
+ RegisterResult = Struct.new(:integration_id, :status)
9
+ Attribution = Struct.new(:click_id, :ref, :clicked_at)
10
+ end
11
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "uri"
5
+ require "json"
6
+
7
+ module RideBuilder
8
+ module Affiliate
9
+ # A network-level failure (connect/DNS/timeout). Always treated as retryable by the client.
10
+ class TransportError < StandardError; end
11
+
12
+ # A completed HTTP response — any status. Non-2xx is returned here, not raised.
13
+ Response = Struct.new(:status, :body) do
14
+ def json
15
+ return nil if body.nil? || body.empty?
16
+
17
+ JSON.parse(body)
18
+ rescue JSON::ParserError
19
+ nil
20
+ end
21
+ end
22
+
23
+ # Default transport, backed by the stdlib Net::HTTP — zero third-party dependencies. Inject any object
24
+ # that responds to #call(method, url, headers, body) to route through your own HTTP client.
25
+ class NetHttpTransport
26
+ def initialize(timeout_ms = 10_000)
27
+ @timeout = timeout_ms / 1000.0
28
+ end
29
+
30
+ def call(method, url, headers, body)
31
+ uri = URI(url)
32
+ http = Net::HTTP.new(uri.host, uri.port)
33
+ http.use_ssl = uri.scheme == "https"
34
+ http.open_timeout = @timeout
35
+ http.read_timeout = @timeout
36
+
37
+ request = build_request(method, uri)
38
+ headers.each { |k, v| request[k] = v }
39
+ request.body = body if body
40
+
41
+ response = http.request(request)
42
+ Response.new(response.code.to_i, response.body)
43
+ rescue SocketError, SystemCallError, IOError, Timeout::Error => e
44
+ raise TransportError, e.message
45
+ end
46
+
47
+ private
48
+
49
+ def build_request(method, uri)
50
+ case method
51
+ when "GET" then Net::HTTP::Get.new(uri)
52
+ when "POST" then Net::HTTP::Post.new(uri)
53
+ when "PUT" then Net::HTTP::Put.new(uri)
54
+ when "DELETE" then Net::HTTP::Delete.new(uri)
55
+ else raise ArgumentError, "unsupported method #{method}"
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RideBuilder
4
+ module Affiliate
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "cgi"
5
+ require "net/http"
6
+ require "uri"
7
+
8
+ module RideBuilder
9
+ # Server-side SDK for RideBuilder's FirstParty affiliate program.
10
+ module Affiliate
11
+ DEFAULT_BASE_URL = "https://api.ridebuilder.com/v1"
12
+ SDK_TYPE = "ruby_sdk"
13
+ DEFAULT_CAPABILITIES = %w[click_capture order_events refund_events].freeze
14
+ end
15
+ end
16
+
17
+ require_relative "affiliate/version"
18
+ require_relative "affiliate/error"
19
+ require_relative "affiliate/click_id"
20
+ require_relative "affiliate/models"
21
+ require_relative "affiliate/capture"
22
+ require_relative "affiliate/transport"
23
+ require_relative "affiliate/client"
metadata ADDED
@@ -0,0 +1,55 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ridebuilder-affiliate
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - RideBuilder
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-08-03 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Capture click_id and send checkout/return postbacks to RideBuilder. Zero
14
+ runtime dependencies (stdlib net/http + json).
15
+ email:
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - LICENSE
21
+ - README.md
22
+ - lib/ridebuilder/affiliate.rb
23
+ - lib/ridebuilder/affiliate/capture.rb
24
+ - lib/ridebuilder/affiliate/click_id.rb
25
+ - lib/ridebuilder/affiliate/client.rb
26
+ - lib/ridebuilder/affiliate/error.rb
27
+ - lib/ridebuilder/affiliate/models.rb
28
+ - lib/ridebuilder/affiliate/transport.rb
29
+ - lib/ridebuilder/affiliate/version.rb
30
+ homepage: https://ridebuilder.com
31
+ licenses:
32
+ - MIT
33
+ metadata:
34
+ source_code_uri: https://github.com/RideBuilder/Backend
35
+ rubygems_mfa_required: 'true'
36
+ post_install_message:
37
+ rdoc_options: []
38
+ require_paths:
39
+ - lib
40
+ required_ruby_version: !ruby/object:Gem::Requirement
41
+ requirements:
42
+ - - ">="
43
+ - !ruby/object:Gem::Version
44
+ version: '3.0'
45
+ required_rubygems_version: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: '0'
50
+ requirements: []
51
+ rubygems_version: 3.5.22
52
+ signing_key:
53
+ specification_version: 4
54
+ summary: Server-side SDK for RideBuilder FirstParty affiliate tracking.
55
+ test_files: []