Module: KnoxCall::CLI::Common

Defined in:
lib/knoxcall/cli/common.rb

Overview

Shared CLI plumbing — token-endpoint POSTs, profile persistence.

Class Method Summary collapse

Class Method Details

.default_base_url(sandbox) ⇒ Object

Default management base: KNOXCALL_BASE_URL env > (sandbox|production) host. An explicit --base-url beats both (handled by the caller).



89
90
91
92
93
# File 'lib/knoxcall/cli/common.rb', line 89

def default_base_url(sandbox)
  env = ENV["KNOXCALL_BASE_URL"]
  return env if env && !env.empty?
  sandbox ? "https://sandbox.#{DEFAULT_CLOUD_HOST}" : DEFAULT_API_BASE
end

.persist_login(path:, profile:, base_url:, token_body:, fallback_tenant: nil) ⇒ Object

Store a successful token response as a credentials-file profile.

Persists the tenant and client_id extension members — refreshes must use the REAL per-tenant client id, not the knoxcall-cli alias. The write happens UNDER the cross-process file lock so a login racing a concurrent refresh never loses a rotation.



63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/knoxcall/cli/common.rb', line 63

def (path:, profile:, base_url:, token_body:, fallback_tenant: nil)
  expires_in = begin
    Float(token_body["expires_in"] || 3600)
  rescue ArgumentError, TypeError
    3600.0
  end
  record = {
    "tenant" => CredentialsFile.presence(token_body["tenant"]) || fallback_tenant,
    "base_url" => base_url,
    "client_id" => CredentialsFile.presence(token_body["client_id"]) || CLI_CLIENT_ID,
    "refresh_token" => token_body["refresh_token"],
    "access_token" => token_body["access_token"],
    "access_token_expires_at" => CredentialsFile.format_expiry(Time.now + expires_in),
    "scope" => token_body["scope"] || ""
  }
  # The lock file lives next to the target — on a first-ever login the
  # directory does not exist yet, so create it before acquiring.
  FileUtils.mkdir_p(File.dirname(path), mode: 0o700)
  CredentialsFile::Lock.new(path).with_lock do
    CredentialsFile.write_profile(path, profile, record)
  end
  record
end

.post_form(url, form, timeout: 30) ⇒ Object

POST a urlencoded form; return [status, parsed-JSON-or-empty-hash].

Connection failures raise CLI::Error with a human message. HTTP error statuses are returned, not raised — device polling needs the error codes.



24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# File 'lib/knoxcall/cli/common.rb', line 24

def post_form(url, form, timeout: 30)
  uri = URI.parse(url)
  req = Net::HTTP::Post.new(uri)
  req["Content-Type"] = "application/x-www-form-urlencoded"
  req["Accept"] = "application/json"
  req["User-Agent"] = SDK_VERSION
  req.body = URI.encode_www_form(form)

  resp = begin
    http = Net::HTTP.new(uri.host, uri.port)
    http.use_ssl = uri.scheme == "https"
    http.open_timeout = timeout
    http.read_timeout = timeout
    http.start { |h| h.request(req) }
  rescue Net::OpenTimeout, Net::ReadTimeout, OpenSSL::SSL::SSLError,
         EOFError, SocketError, SystemCallError, IOError => e
    raise Error, "could not reach #{url}: #{e.message}"
  end

  body = begin
    JSON.parse(resp.body.to_s)
  rescue JSON::ParserError
    nil
  end
  [resp.code.to_i, body.is_a?(Hash) ? body : {}]
end

.token_error_message(status, body) ⇒ Object



51
52
53
54
55
# File 'lib/knoxcall/cli/common.rb', line 51

def token_error_message(status, body)
  detail = CredentialsFile.presence(body["error_description"]) ||
           CredentialsFile.presence(body["error"]) || "HTTP #{status}"
  "sign-in failed: #{detail}"
end