Class: Leash::Transport Private

Inherits:
Object
  • Object
show all
Defined in:
lib/leash/transport.rb

Overview

This class is part of a private API. You should avoid using this class if possible, as it may be removed or be changed in the future.

Shared HTTP transport used by every integration POST. Mirrors the ‘_call` / `_post` private methods on the TS `Leash` class and the Python `_Transport`.

Critical platform contract (Critical #1 in the 0.4 plan):

* `X-API-Key` carries the app key (`LEASH_API_KEY`)
* `Cookie: leash-auth=…` forwards the browser session

The user JWT extracted from an inbound ‘Authorization: Bearer …` header is intentionally NOT forwarded — the TS SDK warns that the JWT path causes the platform’s ‘verifyToken()` to reject before the X-API-Key check runs, producing a misleading 401 on every integration call. Bearer tokens are still accepted by `Leash` for other code paths (env.get fallback, future CLI flows) but never sent on integration POSTs. A negative-assertion test covers this.

Constant Summary collapse

DEFAULT_OPEN_TIMEOUT =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

10
DEFAULT_READ_TIMEOUT =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

30

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(platform_url:, api_key: nil, cookie_value: nil, open_timeout: DEFAULT_OPEN_TIMEOUT, read_timeout: DEFAULT_READ_TIMEOUT, http_runner: nil) ⇒ Transport

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns a new instance of Transport.



34
35
36
37
38
39
40
41
42
43
44
45
46
# File 'lib/leash/transport.rb', line 34

def initialize(platform_url:, api_key: nil, cookie_value: nil,
               open_timeout: DEFAULT_OPEN_TIMEOUT,
               read_timeout: DEFAULT_READ_TIMEOUT,
               http_runner: nil)
  @platform_url = platform_url.to_s.sub(%r{/+\z}, "")
  @api_key = api_key
  @cookie_value = cookie_value
  @open_timeout = open_timeout
  @read_timeout = read_timeout
  # Test seam — allow injecting a custom HTTP runner block.
  # If nil, calls go through `Net::HTTP.start`.
  @http_runner = http_runner
end

Instance Attribute Details

#api_keyObject (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



32
33
34
# File 'lib/leash/transport.rb', line 32

def api_key
  @api_key
end

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



32
33
34
# File 'lib/leash/transport.rb', line 32

def cookie_value
  @cookie_value
end

#platform_urlObject (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



32
33
34
# File 'lib/leash/transport.rb', line 32

def platform_url
  @platform_url
end

Instance Method Details

#build_request(uri, method:, headers:, body:) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



101
102
103
104
105
106
107
108
109
110
111
112
113
# File 'lib/leash/transport.rb', line 101

def build_request(uri, method:, headers:, body:)
  request =
    case method
    when :post then Net::HTTP::Post.new(uri.request_uri)
    when :get  then Net::HTTP::Get.new(uri.request_uri)
    else raise ArgumentError, "Unsupported HTTP method: #{method}"
    end

  headers.each { |k, v| request[k] = v }
  request.body = body if body

  request
end

#call(provider, action, params = nil) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

POST to ‘/api/integrations/provider/action` and return the parsed response body (after unwrapping `data`).



50
51
52
53
54
# File 'lib/leash/transport.rb', line 50

def call(provider, action, params = nil)
  url = "#{@platform_url}/api/integrations/#{provider}/#{action}"
  docs_url = "https://leash.build/docs/integrations/#{provider}"
  post_json(url, params, docs_url: docs_url)
end

#get_json(url, headers: {}) ⇒ Object



57
58
59
# File 'lib/leash/transport.rb', line 57

def get_json(url, headers: {})
  run_request(url, method: :get, headers: headers, body: nil)
end

#handle_response(response, docs_url:) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
# File 'lib/leash/transport.rb', line 116

def handle_response(response, docs_url:)
  status = response.respond_to?(:code) ? response.code.to_i : 0
  body_raw = response.respond_to?(:body) ? response.body : nil

  parsed = parse_json(body_raw)

  if status >= 400
    raise_for_status(status, parsed, docs_url: docs_url)
  end

  if parsed.is_a?(Hash)
    # Platform contract: { success, data } envelope OR raw shape.
    if parsed["success"] == false
      err_message = parsed["error"].is_a?(String) ? parsed["error"] : "Integration error"
      err_code = parsed["code"].is_a?(String) ? parsed["code"] : "INTEGRATION_ERROR"
      raise Error.new(err_message,
                      code: err_code,
                      action: "Check your integration configuration and try again.",
                      see_also: docs_url,
                      status: status,
                      connect_url: parsed["connectUrl"])
    end
    return parsed["data"] if parsed.key?("data")

    return parsed
  end

  parsed
end

#parse_json(body) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



147
148
149
150
151
152
153
# File 'lib/leash/transport.rb', line 147

def parse_json(body)
  return nil if body.nil? || body.empty?

  JSON.parse(body)
rescue JSON::ParserError
  nil
end

#post_json(url, params, docs_url:) ⇒ Object



62
63
64
65
66
67
68
69
70
71
# File 'lib/leash/transport.rb', line 62

def post_json(url, params, docs_url:)
  headers = { "Content-Type" => "application/json" }
  headers["X-API-Key"] = @api_key if @api_key
  headers["Cookie"] = "leash-auth=#{@cookie_value}" if @cookie_value

  body = (params.nil? ? {} : params).to_json
  response = run_request(url, method: :post, headers: headers, body: body)

  handle_response(response, docs_url: docs_url)
end

#raise_for_status(status, body, docs_url:) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
# File 'lib/leash/transport.rb', line 156

def raise_for_status(status, body, docs_url:)
  message = "HTTP #{status}"
  if body.is_a?(Hash) && body["error"].is_a?(String)
    message = body["error"]
  end

  case status
  when 401
    raise UnauthorizedError.new(message,
                                 action: "Ensure the leash-auth cookie is present, or open your app from the Leash dashboard to get a valid session.",
                                 see_also: "https://leash.build/docs/sdk",
                                 status: status)
  when 402
    msg = (body.is_a?(Hash) && body["message"].is_a?(String) ? body["message"] : message)
    raise UpgradeRequiredError.new(msg,
                                    action: "Upgrade your plan at https://leash.build/dashboard/billing.",
                                    see_also: "https://leash.build/pricing",
                                    status: status)
  when 403
    raise ConnectionRequiredError.new(message,
                                       action: "Connect the integration at /dashboard/integrations and make sure this app is on the allow-list.",
                                       see_also: "https://leash.build/dashboard/integrations",
                                       status: status)
  else
    raise Error.new(message,
                    code: "INTEGRATION_ERROR",
                    action: "Check your integration configuration and try again — the upstream provider returned an error.",
                    see_also: docs_url,
                    status: status)
  end
end

#run_request(url, method:, headers:, body:) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
# File 'lib/leash/transport.rb', line 74

def run_request(url, method:, headers:, body:)
  uri = URI.parse(url)
  request = build_request(uri, method: method, headers: headers, body: body)

  if @http_runner
    return @http_runner.call(uri, request)
  end

  use_ssl = uri.scheme == "https"
  opts = { use_ssl: use_ssl, open_timeout: @open_timeout, read_timeout: @read_timeout }

  Net::HTTP.start(uri.host, uri.port, **opts) do |http|
    http.request(request)
  end
rescue Net::OpenTimeout, Net::ReadTimeout => e
  raise NetworkError.new("Timed out reaching the Leash platform: #{e.message}",
                          action: "Check your network connection and that the Leash platform is reachable.",
                          see_also: "https://leash.build/docs/sdk",
                          cause: e)
rescue SocketError, Errno::ECONNREFUSED, Errno::EHOSTUNREACH, IOError => e
  raise NetworkError.new(e.message,
                          action: "Check your network connection and that the Leash platform is reachable.",
                          see_also: "https://leash.build/docs/sdk",
                          cause: e)
end