end_point_blank 0.12.0 → 0.13.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,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require_relative "error_codes"
5
+
6
+ module EndPointBlank
7
+ # Reopened with the same superclass in end_point_blank.rb; declared here too
8
+ # so this file can be required on its own.
9
+ class Error < StandardError; end
10
+
11
+ module Management
12
+ # Raised by {Client} for every refused or failed management API call.
13
+ #
14
+ # The API answers errors as
15
+ # <tt>{"error": {"code", "message", "details"}}</tt>. +code+ is stable and
16
+ # meant for programs; match on it (see {ErrorCodes}). +message+ is for
17
+ # people. +details+ is present only when there is something field-level to
18
+ # say, e.g. a +validation_failed+ error's fields.
19
+ #
20
+ # A code this SDK does not know still raises, carrying that code. An answer
21
+ # that is not the API's error shape (an HTML page from a proxy) raises with
22
+ # code +http_error+; a request that never got an answer raises with code
23
+ # +connection_error+ and no status.
24
+ class Error < EndPointBlank::Error
25
+ # @return [String] the API's error code, or one of {ErrorCodes::SDK}
26
+ attr_reader :code
27
+ # @return [Object, nil] field-level details, as the API sent them
28
+ attr_reader :details
29
+ # @return [Integer, nil] the HTTP status; nil for +connection_error+
30
+ attr_reader :status
31
+ # @return [Numeric, nil] the Retry-After header's seconds, when present
32
+ attr_reader :retry_after
33
+ # @return [String, nil] the Location header, e.g. on +idempotency_replay_unavailable+
34
+ attr_reader :location
35
+ # @return [String, nil] the X-Request-Id header, for support requests
36
+ attr_reader :request_id
37
+ # @return [String, nil] the HTTP method of the failed request
38
+ attr_reader :http_method
39
+ # @return [String, nil] the path of the failed request (no query, no host)
40
+ attr_reader :path
41
+
42
+ # rubocop:disable Metrics/ParameterLists
43
+ def initialize(message, code:, status: nil, details: nil, retry_after: nil, location: nil,
44
+ request_id: nil, http_method: nil, path: nil)
45
+ @code = code
46
+ @status = status
47
+ @details = details
48
+ @retry_after = retry_after
49
+ @location = location
50
+ @request_id = request_id
51
+ @http_method = http_method
52
+ @path = path
53
+ super(message)
54
+ end
55
+ # rubocop:enable Metrics/ParameterLists
56
+
57
+ # Builds the error for an answer outside 2xx. +headers+ is any Hash-like
58
+ # whose keys are header names in any case.
59
+ def self.from_response(status:, headers:, body:, http_method: nil, path: nil)
60
+ code, message, details = parse_body(status, body)
61
+ if code == ErrorCodes::IDEMPOTENCY_REPLAY_UNAVAILABLE
62
+ message = replay_unavailable_message(header(headers, "location"))
63
+ end
64
+
65
+ new(message,
66
+ code: code, status: status, details: details,
67
+ retry_after: parse_retry_after(header(headers, "retry-after")),
68
+ location: header(headers, "location"), request_id: header(headers, "x-request-id"),
69
+ http_method: http_method, path: path)
70
+ end
71
+
72
+ # Whether this error is +code+ (a String or Symbol).
73
+ def code?(code)
74
+ self.code == code.to_s
75
+ end
76
+
77
+ def inspect
78
+ "#<#{self.class.name} code=#{code.inspect} status=#{status.inspect} message=#{message.inspect}>"
79
+ end
80
+
81
+ class << self
82
+ # The case-insensitive value of header +name+, or nil.
83
+ def header(headers, name)
84
+ return nil if headers.nil?
85
+
86
+ headers.each { |key, value| return value.to_s if key.to_s.casecmp?(name) }
87
+ nil
88
+ end
89
+
90
+ # Seconds to wait from a Retry-After header: delta-seconds, or an
91
+ # HTTP-date. nil when absent or unreadable; never negative.
92
+ def parse_retry_after(value)
93
+ return nil if value.nil? || value.strip.empty?
94
+ return Integer(value.strip, 10) if value.strip.match?(/\A\d+\z/)
95
+
96
+ require "time"
97
+ [Time.httpdate(value.strip) - Time.now, 0].max
98
+ rescue ArgumentError
99
+ nil
100
+ end
101
+
102
+ private
103
+
104
+ def parse_body(status, body)
105
+ error = JSON.parse(body.to_s)["error"]
106
+ if error.is_a?(Hash) && error["code"].is_a?(String)
107
+ message = error["message"].is_a?(String) ? error["message"] : "The request failed (#{error["code"]})."
108
+ return [error["code"], message, error["details"]]
109
+ end
110
+
111
+ unexpected(status, body)
112
+ rescue JSON::ParserError, TypeError, NoMethodError
113
+ unexpected(status, body)
114
+ end
115
+
116
+ def unexpected(status, body)
117
+ snippet = body.to_s.strip.gsub(/\s+/, " ")[0, 200]
118
+ message = "The management API answered HTTP #{status} without its JSON error shape"
119
+ message += snippet.empty? ? "." : ": #{snippet}"
120
+ [ErrorCodes::HTTP_ERROR, message, nil]
121
+ end
122
+
123
+ def replay_unavailable_message(location)
124
+ message = +"A request with this Idempotency-Key already succeeded, and its answer held a secret " \
125
+ "that is shown only once, so it can't be replayed and was not retried. Read or list " \
126
+ "the resource to see its current state"
127
+ message << " (#{location})" if location
128
+ message << ". If you need a secret you never received, rotate the credential."
129
+ message
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EndPointBlank
4
+ module Management
5
+ # Every +error.code+ the management API can answer, with its HTTP status, as
6
+ # app_portal's docs list them, plus the few codes this SDK raises itself
7
+ # when there is no API answer to read one from.
8
+ #
9
+ # Match on these rather than on messages, which are for people and may
10
+ # change. The list is not closed: an {Error} carrying a code missing here
11
+ # still raises with that code, so a newer API never breaks an older SDK.
12
+ module ErrorCodes
13
+ # Authentication and limits
14
+ MISSING_KEY = "missing_key"
15
+ INVALID_KEY = "invalid_key"
16
+ RUNTIME_CREDENTIAL_REFUSED = "runtime_credential_refused"
17
+ INSUFFICIENT_SCOPE = "insufficient_scope"
18
+ AUDIT_UNAVAILABLE = "audit_unavailable"
19
+ RATE_LIMITED = "rate_limited"
20
+ PLAN_LIMIT = "plan_limit"
21
+
22
+ # Requests
23
+ VALIDATION_FAILED = "validation_failed"
24
+ # No such resource in your organization (another organization's id
25
+ # answers this too), or no such /api/v1 path.
26
+ NOT_FOUND = "not_found"
27
+ INVALID_PAGINATION = "invalid_pagination"
28
+ INVALID_FILTER = "invalid_filter"
29
+ INVALID_IDEMPOTENCY_KEY = "invalid_idempotency_key"
30
+ IDEMPOTENCY_KEY_REUSED = "idempotency_key_reused"
31
+ IDEMPOTENCY_REQUEST_IN_PROGRESS = "idempotency_request_in_progress"
32
+ IDEMPOTENCY_REPLAY_UNAVAILABLE = "idempotency_replay_unavailable"
33
+ # Any request the API cannot read (e.g. a body that is not JSON).
34
+ BAD_REQUEST = "bad_request"
35
+ INTERNAL_SERVER_ERROR = "internal_server_error"
36
+
37
+ # Applications, environments and API packages
38
+ HAS_DEPENDENTS = "has_dependents"
39
+ PROTECTED = "protected"
40
+ INVALID_ENVIRONMENT_BASE_URLS = "invalid_environment_base_urls"
41
+ API_PACKAGE_ASSIGNED = "api_package_assigned"
42
+ INTAKE_SYNC_FAILED = "intake_sync_failed"
43
+
44
+ # Credentials
45
+ DELETE_REFUSED = "delete_refused"
46
+ INTAKE_CREDENTIAL = "intake_credential"
47
+ INTAKE_REJECTED = "intake_rejected"
48
+ INTAKE_UNAVAILABLE = "intake_unavailable"
49
+
50
+ # Clients, packages and grants
51
+ INVALID_CONTACTS = "invalid_contacts"
52
+ INVALID_PACKAGES = "invalid_packages"
53
+ INVALID_GRANTS = "invalid_grants"
54
+ INVALID_MANAGED = "invalid_managed"
55
+ CLIENT_NOT_ACCEPTED = "client_not_accepted"
56
+ CLIENT_ACCEPTED = "client_accepted"
57
+ CLIENT_NOT_MANAGED = "client_not_managed"
58
+ ALREADY_A_MEMBER = "already_a_member"
59
+ MANAGED_CLIENT_HAS_CREDENTIALS = "managed_client_has_credentials"
60
+ API_PACKAGE_NOT_FOUND = "api_package_not_found"
61
+ ENVIRONMENT_NOT_FOUND = "environment_not_found"
62
+ ALREADY_ASSIGNED = "already_assigned"
63
+ NOTHING_PUBLISHED_IN_ENVIRONMENT = "nothing_published_in_environment"
64
+ APPLICATION_NOT_FOUND = "application_not_found"
65
+ ENDPOINT_NOT_FOUND = "endpoint_not_found"
66
+ ENVIRONMENT_NOT_IN_APPLICATION = "environment_not_in_application"
67
+ ALREADY_GRANTED = "already_granted"
68
+ GRANT_REVOKED_CONCURRENTLY = "grant_revoked_concurrently"
69
+
70
+ # The warning code API package endpoint writes answer in +warnings+ (not
71
+ # an error): a client assignment of the package now derives no grant.
72
+ ASSIGNMENT_DERIVES_NOTHING = "assignment_derives_nothing"
73
+
74
+ # Raised by this SDK, never sent by the API: the request never got an
75
+ # answer (DNS, TLS, refused connection, timeout).
76
+ CONNECTION_ERROR = "connection_error"
77
+ # Raised by this SDK: an error answer whose body is not the API's JSON
78
+ # error shape, such as an HTML page from a proxy. +status+ still says
79
+ # what happened.
80
+ HTTP_ERROR = "http_error"
81
+ # Raised by this SDK: a success answer whose body is not JSON.
82
+ INVALID_RESPONSE = "invalid_response"
83
+
84
+ # The HTTP status the API answers each of its codes with.
85
+ STATUSES = {
86
+ MISSING_KEY => 401, INVALID_KEY => 401, RUNTIME_CREDENTIAL_REFUSED => 401,
87
+ INSUFFICIENT_SCOPE => 403, AUDIT_UNAVAILABLE => 503, RATE_LIMITED => 429, PLAN_LIMIT => 402,
88
+ VALIDATION_FAILED => 422, NOT_FOUND => 404, INVALID_PAGINATION => 400, INVALID_FILTER => 422,
89
+ INVALID_IDEMPOTENCY_KEY => 400, IDEMPOTENCY_KEY_REUSED => 422,
90
+ IDEMPOTENCY_REQUEST_IN_PROGRESS => 409, IDEMPOTENCY_REPLAY_UNAVAILABLE => 409,
91
+ BAD_REQUEST => 400, INTERNAL_SERVER_ERROR => 500,
92
+ HAS_DEPENDENTS => 422, PROTECTED => 422, INVALID_ENVIRONMENT_BASE_URLS => 422,
93
+ API_PACKAGE_ASSIGNED => 422, INTAKE_SYNC_FAILED => 422,
94
+ DELETE_REFUSED => 422, INTAKE_CREDENTIAL => 409, INTAKE_REJECTED => 422, INTAKE_UNAVAILABLE => 503,
95
+ INVALID_CONTACTS => 422, INVALID_PACKAGES => 422, INVALID_GRANTS => 422, INVALID_MANAGED => 422,
96
+ CLIENT_NOT_ACCEPTED => 422, CLIENT_ACCEPTED => 422, CLIENT_NOT_MANAGED => 422,
97
+ ALREADY_A_MEMBER => 422, MANAGED_CLIENT_HAS_CREDENTIALS => 422, API_PACKAGE_NOT_FOUND => 422,
98
+ ENVIRONMENT_NOT_FOUND => 422, ALREADY_ASSIGNED => 422, NOTHING_PUBLISHED_IN_ENVIRONMENT => 422,
99
+ APPLICATION_NOT_FOUND => 422, ENDPOINT_NOT_FOUND => 422, ENVIRONMENT_NOT_IN_APPLICATION => 422,
100
+ ALREADY_GRANTED => 422, GRANT_REVOKED_CONCURRENTLY => 409
101
+ }.freeze
102
+
103
+ # Every code the API can send, as strings.
104
+ ALL = STATUSES.keys.freeze
105
+
106
+ # Codes this SDK raises itself.
107
+ SDK = [CONNECTION_ERROR, HTTP_ERROR, INVALID_RESPONSE].freeze
108
+
109
+ # Whether +code+ is one this version of the SDK knows about.
110
+ def self.known?(code)
111
+ STATUSES.key?(code) || SDK.include?(code)
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+
5
+ module EndPointBlank
6
+ module Management
7
+ # The Idempotency-Key a management API request carries: every POST has
8
+ # one, so a retry of it is replayed by the API rather than run twice.
9
+ module IdempotencyKey
10
+ MAX_LENGTH = 255
11
+
12
+ # The key to send for +method+: the caller's (stripped, as the API reads
13
+ # it), or a new random UUID v4 for a POST without one; nil for any other
14
+ # method, which must not be given one.
15
+ #
16
+ # @raise [ArgumentError] for a key on a non-POST, or an empty or
17
+ # too-long key
18
+ def self.resolve(method, key)
19
+ unless method == "POST"
20
+ raise ArgumentError, "idempotency_key applies to POST requests only" unless key.nil?
21
+
22
+ return nil
23
+ end
24
+ return SecureRandom.uuid if key.nil?
25
+
26
+ key = key.to_s.strip
27
+ return key if key.length.between?(1, MAX_LENGTH)
28
+
29
+ raise ArgumentError, "idempotency_key must be 1 to #{MAX_LENGTH} characters"
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EndPointBlank
4
+ module Management
5
+ # One page of a management API list: the items (+data+, Hashes with String
6
+ # keys, as the API sent them) and the cursor for the next page, nil on the
7
+ # last one. Enumerable over its items.
8
+ #
9
+ # Pass +next_cursor+ as +after:+ to the same list call for the next page,
10
+ # or use the resource's +each+ to walk every page.
11
+ class Page
12
+ include Enumerable
13
+
14
+ # @return [Array<Hash>]
15
+ attr_reader :data
16
+ # @return [String, nil]
17
+ attr_reader :next_cursor
18
+
19
+ def initialize(data:, next_cursor:)
20
+ @data = data
21
+ @next_cursor = next_cursor
22
+ end
23
+
24
+ # Builds a page from a list answer, <tt>{"data" => [...], "next_cursor" => ...}</tt>.
25
+ def self.from_body(body)
26
+ body = {} unless body.is_a?(Hash)
27
+ new(data: Array(body["data"]), next_cursor: body["next_cursor"])
28
+ end
29
+
30
+ def each(&block)
31
+ return enum_for(:each) unless block
32
+
33
+ data.each(&block)
34
+ self
35
+ end
36
+
37
+ # Whether there is a page after this one.
38
+ def next_page?
39
+ !next_cursor.nil? && !next_cursor.empty?
40
+ end
41
+
42
+ def size
43
+ data.size
44
+ end
45
+
46
+ def empty?
47
+ data.empty?
48
+ end
49
+
50
+ def inspect
51
+ "#<#{self.class.name} size=#{size} next_cursor=#{next_cursor.inspect}>"
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module EndPointBlank
6
+ module Management
7
+ module Resources
8
+ # +/api_packages+: your organization's API packages, and what each one
9
+ # publishes (+/api_packages/:id/endpoints+).
10
+ #
11
+ # A package: <tt>{"id", "name", "organization_id", "inserted_at",
12
+ # "updated_at"}</tt>.
13
+ class ApiPackages < Base
14
+ # GET /api_packages. @return [Page]
15
+ def list(limit: nil, after: nil)
16
+ list_page(path("api_packages"), limit, after)
17
+ end
18
+
19
+ # Every API package, across pages. @return [Enumerator, nil]
20
+ def each(limit: nil, &block)
21
+ each_item(path("api_packages"), limit, &block)
22
+ end
23
+
24
+ # GET /api_packages/:id. @return [Hash]
25
+ def get(id)
26
+ get_data(path("api_packages", id))
27
+ end
28
+
29
+ # POST /api_packages. @return [Hash] the new package
30
+ def create(name:, idempotency_key: nil)
31
+ post_data(path("api_packages"), { name: name }, idempotency_key)
32
+ end
33
+
34
+ # PATCH /api_packages/:id (rename). @return [Hash]
35
+ def update(id, name:)
36
+ patch_data(path("api_packages", id), { name: name })
37
+ end
38
+
39
+ # DELETE /api_packages/:id. Refused with +api_package_assigned+ while
40
+ # a client holds it. @return [Hash] <tt>{"id", "deleted"}</tt>
41
+ def delete(id)
42
+ delete_data(path("api_packages", id))
43
+ end
44
+
45
+ # GET /api_packages/:id/endpoints: what the package publishes, one
46
+ # entry per application endpoint (or whole application) and
47
+ # environment. An entry: <tt>{"id", "api_package_id",
48
+ # "application_id", "endpoint_id", "all_endpoints", "endpoint",
49
+ # "environment_id", "inserted_at"}</tt>. @return [Page]
50
+ def list_endpoints(api_package_id, limit: nil, after: nil)
51
+ list_page(path("api_packages", api_package_id, "endpoints"), limit, after)
52
+ end
53
+
54
+ # Every entry of the package, across pages. @return [Enumerator, nil]
55
+ def each_endpoint(api_package_id, limit: nil, &block)
56
+ each_item(path("api_packages", api_package_id, "endpoints"), limit, &block)
57
+ end
58
+
59
+ # POST /api_packages/:id/endpoints. Leave +endpoint_id+ nil to publish
60
+ # every endpoint of the application.
61
+ #
62
+ # Answers the whole body, not only +data+: <tt>{"data" => entry,
63
+ # "warnings" => [...]}</tt>. Each warning (code
64
+ # +assignment_derives_nothing+) names a client assignment of the
65
+ # package that derives no grant.
66
+ #
67
+ # @return [Hash] <tt>{"data", "warnings"}</tt>
68
+ def add_endpoint(api_package_id, application_id:, environment_id:, endpoint_id: nil, idempotency_key: nil)
69
+ body = compact(application_id: application_id, endpoint_id: endpoint_id, environment_id: environment_id)
70
+ @transport.request("POST", path("api_packages", api_package_id, "endpoints"),
71
+ body: body, idempotency_key: idempotency_key)
72
+ end
73
+
74
+ # DELETE /api_packages/:id/endpoints/:access_id. Answers the whole
75
+ # body, as {#add_endpoint} does.
76
+ #
77
+ # @return [Hash] <tt>{"data" => {"id", "deleted"}, "warnings" => [...]}</tt>
78
+ def remove_endpoint(api_package_id, access_id)
79
+ @transport.request("DELETE", path("api_packages", api_package_id, "endpoints", access_id))
80
+ end
81
+ end
82
+
83
+ # +GET /endpoints+: the endpoints of your organization's applications,
84
+ # to find the ids {ApiPackages#add_endpoint} takes.
85
+ #
86
+ # An endpoint: <tt>{"id", "application_id", "path", "action", "public",
87
+ # "inserted_at", "updated_at"}</tt>.
88
+ class Endpoints < Base
89
+ # @param application_id [String, nil] only this application's endpoints
90
+ # @param version [String, nil] only endpoints deployed in an
91
+ # application version of this name
92
+ # @return [Page]
93
+ def list(application_id: nil, version: nil, limit: nil, after: nil)
94
+ list_page(path("endpoints"), limit, after, application_id: application_id, version: version)
95
+ end
96
+
97
+ # Every matching endpoint, across pages. @return [Enumerator, nil]
98
+ def each(application_id: nil, version: nil, limit: nil, &block)
99
+ each_item(path("endpoints"), limit, { application_id: application_id, version: version }, &block)
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,167 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module EndPointBlank
6
+ module Management
7
+ module Resources
8
+ # +/applications+, and each application's environments
9
+ # (+/applications/:application_id/environments+, an "application
10
+ # environment": the application placed in an environment at a base URL).
11
+ #
12
+ # An application: <tt>{"id", "name", "public", "organization_group_id",
13
+ # "synced_at", "inserted_at", "updated_at"}</tt>. An application
14
+ # environment: <tt>{"id", "application_id", "environment_id",
15
+ # "base_url", ...}</tt>.
16
+ class Applications < Base
17
+ # @return [Page]
18
+ def list(limit: nil, after: nil)
19
+ list_page(path("applications"), limit, after)
20
+ end
21
+
22
+ # @return [Enumerator, nil]
23
+ def each(limit: nil, &block)
24
+ each_item(path("applications"), limit, &block)
25
+ end
26
+
27
+ # @return [Hash]
28
+ def get(id)
29
+ get_data(path("applications", id))
30
+ end
31
+
32
+ # POST /applications.
33
+ #
34
+ # @param environment_base_urls [Hash] environment id => base URL; at
35
+ # least one is required
36
+ # @return [Hash] the new application
37
+ def create(name:, environment_base_urls:, public: nil, organization_group_id: nil, idempotency_key: nil)
38
+ body = compact(name: name, environment_base_urls: environment_base_urls, public: public,
39
+ organization_group_id: organization_group_id)
40
+ post_data(path("applications"), body, idempotency_key)
41
+ end
42
+
43
+ # PATCH /applications/:id. Only the arguments given are sent.
44
+ # @return [Hash]
45
+ def update(id, name: nil, public: nil)
46
+ patch_data(path("applications", id), compact(name: name, public: public))
47
+ end
48
+
49
+ # DELETE /applications/:id. Refused with +has_dependents+ while grants
50
+ # or credentials depend on it. @return [Hash] <tt>{"id", "deleted"}</tt>
51
+ def delete(id)
52
+ delete_data(path("applications", id))
53
+ end
54
+
55
+ # GET /applications/:application_id/environments. @return [Page]
56
+ def list_environments(application_id, limit: nil, after: nil)
57
+ list_page(path("applications", application_id, "environments"), limit, after)
58
+ end
59
+
60
+ # @return [Enumerator, nil]
61
+ def each_environment(application_id, limit: nil, &block)
62
+ each_item(path("applications", application_id, "environments"), limit, &block)
63
+ end
64
+
65
+ # POST /applications/:application_id/environments: place the
66
+ # application in an environment at +base_url+.
67
+ # @return [Hash] the application environment
68
+ def add_environment(application_id, environment_id:, base_url:, idempotency_key: nil)
69
+ post_data(path("applications", application_id, "environments"),
70
+ { environment_id: environment_id, base_url: base_url }, idempotency_key)
71
+ end
72
+
73
+ # DELETE /applications/:application_id/environments/:id, where +id+ is
74
+ # the application environment's id. @return [Hash] <tt>{"id", "deleted"}</tt>
75
+ def remove_environment(application_id, id)
76
+ delete_data(path("applications", application_id, "environments", id))
77
+ end
78
+ end
79
+
80
+ # +/environments+: deployment environments.
81
+ #
82
+ # An environment: <tt>{"id", "name", "domain", "is_default", ...}</tt>.
83
+ class Environments < Base
84
+ # @return [Page]
85
+ def list(limit: nil, after: nil)
86
+ list_page(path("environments"), limit, after)
87
+ end
88
+
89
+ # @return [Enumerator, nil]
90
+ def each(limit: nil, &block)
91
+ each_item(path("environments"), limit, &block)
92
+ end
93
+
94
+ # @return [Hash]
95
+ def get(id)
96
+ get_data(path("environments", id))
97
+ end
98
+
99
+ # POST /environments. @return [Hash] the new environment
100
+ def create(name:, domain:, is_default: nil, idempotency_key: nil)
101
+ post_data(path("environments"), compact(name: name, domain: domain, is_default: is_default),
102
+ idempotency_key)
103
+ end
104
+
105
+ # PATCH /environments/:id. Only the arguments given are sent. The
106
+ # production environment answers +protected+. @return [Hash]
107
+ def update(id, name: nil, domain: nil, is_default: nil)
108
+ patch_data(path("environments", id), compact(name: name, domain: domain, is_default: is_default))
109
+ end
110
+
111
+ # DELETE /environments/:id. @return [Hash] <tt>{"id", "deleted"}</tt>
112
+ def delete(id)
113
+ delete_data(path("environments", id))
114
+ end
115
+ end
116
+
117
+ # +/credentials+: runtime client credentials (the client_id and
118
+ # client_secret an SDK authenticates with).
119
+ #
120
+ # A credential: <tt>{"id", "client_id", "secret_last_4",
121
+ # "application_environment_id", "application_id", "environment",
122
+ # "previous_secret", "expired_at", "created_at", "updated_at"}</tt>.
123
+ # {#create} and {#rotate} add +client_secret+, the only time the secret
124
+ # is ever returned: store it then. This SDK never logs it.
125
+ class Credentials < Base
126
+ # @param application_environment_id [String, nil] only this
127
+ # application environment's credentials
128
+ # @return [Page] metadata only, no secrets
129
+ def list(application_environment_id: nil, limit: nil, after: nil)
130
+ list_page(path("credentials"), limit, after, application_environment_id: application_environment_id)
131
+ end
132
+
133
+ # @return [Enumerator, nil]
134
+ def each(application_environment_id: nil, limit: nil, &block)
135
+ each_item(path("credentials"), limit, { application_environment_id: application_environment_id }, &block)
136
+ end
137
+
138
+ # @return [Hash] metadata only, no secret
139
+ def get(id)
140
+ get_data(path("credentials", id))
141
+ end
142
+
143
+ # POST /credentials. A retry of a create that succeeded but whose
144
+ # answer was lost raises +idempotency_replay_unavailable+: list the
145
+ # application environment's credentials instead.
146
+ # @return [Hash] the credential, with its one-time +client_secret+
147
+ def create(application_environment_id:, idempotency_key: nil)
148
+ post_data(path("credentials"), { application_environment_id: application_environment_id }, idempotency_key)
149
+ end
150
+
151
+ # POST /credentials/:id/rotate: a new secret; the old one keeps
152
+ # working for the grace window (see +previous_secret+).
153
+ # @return [Hash] the credential, with its new one-time +client_secret+
154
+ def rotate(id, idempotency_key: nil)
155
+ post_data(path("credentials", id, "rotate"), nil, idempotency_key)
156
+ end
157
+
158
+ # DELETE /credentials/:id: revokes the credential on intake, then
159
+ # removes it. @return [Hash] <tt>{"id", "deleted"}</tt>
160
+ def delete(id)
161
+ delete_data(path("credentials", id))
162
+ end
163
+ alias revoke delete
164
+ end
165
+ end
166
+ end
167
+ end