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,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../page"
4
+
5
+ module EndPointBlank
6
+ module Management
7
+ # The management API's resources, each reached from a {Client} (or a
8
+ # {ManagedClient} view) rather than built directly.
9
+ #
10
+ # Every method answers what the API sent, decoded from JSON into Hashes
11
+ # with String keys: a single resource's +data+, a {Page} for a list, and
12
+ # <tt>{"id" => ..., "deleted" => true}</tt> for a delete. Optional keyword
13
+ # arguments left nil are not sent.
14
+ module Resources
15
+ # Shared plumbing: paths, list pages and auto-paging.
16
+ class Base
17
+ MAX_LIMIT = 100
18
+
19
+ def initialize(transport, prefix = "")
20
+ @transport = transport
21
+ @prefix = prefix
22
+ end
23
+
24
+ def inspect
25
+ "#<#{self.class.name} prefix=#{@prefix.inspect}>"
26
+ end
27
+
28
+ # One path segment, percent-escaped. An empty id is refused, so it can
29
+ # never turn a "get one" into a "list all"; so is an id of dots only
30
+ # (".", ".."), a dot-segment that any proxy or URL normalizer may
31
+ # resolve into a different route (DELETE /clients/c1/grants/.. into
32
+ # DELETE /clients/c1). Percent-encoding the dots would not help:
33
+ # normalizers decode %2E first. Real ids are UUIDs.
34
+ def self.escape(segment)
35
+ value = segment.to_s
36
+ raise ArgumentError, "an id must be a non-empty String" if value.empty?
37
+ raise ArgumentError, "an id must not be only dots" if value.each_char.all?(".")
38
+
39
+ value.b.gsub(/[^A-Za-z0-9\-._~]/n) { |char| format("%%%02X", char.ord) }
40
+ end
41
+
42
+ private
43
+
44
+ # "/segment/escaped-id/..." under this resource's prefix. Every
45
+ # argument is one path segment (see {.escape}).
46
+ def path(*segments)
47
+ @prefix + segments.map { |segment| "/#{escape(segment)}" }.join
48
+ end
49
+
50
+ def escape(segment)
51
+ self.class.escape(segment)
52
+ end
53
+
54
+ def get_data(path, query = nil)
55
+ data(@transport.request("GET", path, query: query))
56
+ end
57
+
58
+ def post_data(path, body, idempotency_key)
59
+ data(@transport.request("POST", path, body: body, idempotency_key: idempotency_key))
60
+ end
61
+
62
+ def patch_data(path, body)
63
+ data(@transport.request("PATCH", path, body: body))
64
+ end
65
+
66
+ def delete_data(path)
67
+ data(@transport.request("DELETE", path))
68
+ end
69
+
70
+ def data(body)
71
+ body.is_a?(Hash) && body.key?("data") ? body["data"] : body
72
+ end
73
+
74
+ # One page of a list. +filters+ with a nil value are not sent.
75
+ def list_page(path, limit, after, filters = {})
76
+ check_limit(limit)
77
+ query = compact(filters.merge(limit: limit, after: after))
78
+ Page.from_body(@transport.request("GET", path, query: query))
79
+ end
80
+
81
+ # Every item of a list, fetching pages as it goes. With a block,
82
+ # yields each item and answers nil; without one, answers an
83
+ # Enumerator (lazy: no request is made until it is iterated).
84
+ def each_item(path, limit, filters = {}, &block)
85
+ check_limit(limit)
86
+ enumerator = Enumerator.new { |yielder| each_page_item(yielder, path, limit, filters) }
87
+ return enumerator unless block
88
+
89
+ enumerator.each(&block)
90
+ nil
91
+ end
92
+
93
+ def each_page_item(yielder, path, limit, filters)
94
+ after = nil
95
+ loop do
96
+ page = list_page(path, limit, after, filters)
97
+ page.each { |item| yielder << item }
98
+ break unless page.next_page?
99
+
100
+ after = page.next_cursor
101
+ end
102
+ end
103
+
104
+ def check_limit(limit)
105
+ return if limit.nil?
106
+ return if limit.is_a?(Integer) && limit.between?(1, MAX_LIMIT)
107
+
108
+ raise ArgumentError, "limit must be an Integer from 1 to #{MAX_LIMIT}, got #{limit.inspect}"
109
+ end
110
+
111
+ def compact(hash)
112
+ hash.reject { |_key, value| value.nil? }
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module EndPointBlank
6
+ module Management
7
+ module Resources
8
+ # +/clients+: the organizations you serve as their provider, as client
9
+ # invites (pending and accepted), and managed clients you run for your
10
+ # customers.
11
+ #
12
+ # A client: <tt>{"id", "name", "status" ("pending"/"accepted"),
13
+ # "invite_code" (write keys, while pending), "accepted_at", "managed",
14
+ # "claimed_at", "client_organization", "inserted_at", "updated_at"}</tt>;
15
+ # {#get} adds +contacts+ and +pre_assignments+.
16
+ class Clients < Base
17
+ # GET /clients. @return [Page]
18
+ def list(limit: nil, after: nil)
19
+ list_page(path("clients"), limit, after)
20
+ end
21
+
22
+ # Every client, across pages. @return [Enumerator, nil]
23
+ def each(limit: nil, &block)
24
+ each_item(path("clients"), limit, &block)
25
+ end
26
+
27
+ # GET /clients/:id. @return [Hash]
28
+ def get(id)
29
+ get_data(path("clients", id))
30
+ end
31
+
32
+ # POST /clients.
33
+ #
34
+ # @param contacts [Array<Hash>, nil] your contacts to share, each
35
+ # <tt>{email:, first_name:, last_name:, title: nil, phone_number: nil}</tt>
36
+ # @param packages [Array<Hash>, nil] API packages to assign when the
37
+ # client accepts, each <tt>{api_package_id:, environment_id:}</tt>
38
+ # @param grants [Array<Hash>, nil] direct grants to make when it
39
+ # accepts, each <tt>{target_application_id:, environment_id:,
40
+ # target_endpoint_id: nil}</tt>
41
+ # @param managed [Boolean, nil] true creates a managed client (no
42
+ # packages or grants with it; assign those afterwards)
43
+ # @return [Hash] the new client; +invite_code+ is what the client
44
+ # accepts with
45
+ # rubocop:disable Metrics/ParameterLists
46
+ def create(name:, contacts: nil, packages: nil, grants: nil, managed: nil, idempotency_key: nil)
47
+ body = compact(name: name, contacts: contacts, packages: packages, grants: grants, managed: managed)
48
+ post_data(path("clients"), body, idempotency_key)
49
+ end
50
+ # rubocop:enable Metrics/ParameterLists
51
+
52
+ # Invites a client: {#create} without +managed+.
53
+ def invite(name:, contacts: nil, packages: nil, grants: nil, idempotency_key: nil)
54
+ create(name: name, contacts: contacts, packages: packages, grants: grants,
55
+ idempotency_key: idempotency_key)
56
+ end
57
+
58
+ # Creates a managed client: {#create} with <tt>managed: true</tt>. Set
59
+ # it up with {Client#for_managed_client}.
60
+ def create_managed(name:, contacts: nil, idempotency_key: nil)
61
+ create(name: name, contacts: contacts, managed: true, idempotency_key: idempotency_key)
62
+ end
63
+
64
+ # DELETE /clients/:id. Refused with +managed_client_has_credentials+
65
+ # for a managed client that still holds credentials.
66
+ # @return [Hash] <tt>{"id", "deleted"}</tt>
67
+ def delete(id)
68
+ delete_data(path("clients", id))
69
+ end
70
+
71
+ # POST /clients/:client_id/claim_invites: emails your customer an
72
+ # invite to claim a managed client.
73
+ # @return [Hash] <tt>{"client_id", "email", "sent_at", "expires_at"}</tt>
74
+ def claim_invite(client_id, email:, idempotency_key: nil)
75
+ post_data(path("clients", client_id, "claim_invites"), { email: email }, idempotency_key)
76
+ end
77
+ end
78
+
79
+ # +/clients/:client_id/packages+: the API packages a client holds from
80
+ # you, or (on a pending invite) is set up to get when it accepts.
81
+ #
82
+ # An assignment: <tt>{"id", "api_package_id", "api_package_name",
83
+ # "environment_id", "status" ("active"/"pending"/"refused"), "refusal",
84
+ # "inserted_at", "updated_at"}</tt>.
85
+ class PackageAssignments < Base
86
+ # @return [Page]
87
+ def list(client_id, limit: nil, after: nil)
88
+ list_page(path("clients", client_id, "packages"), limit, after)
89
+ end
90
+
91
+ # @return [Enumerator, nil]
92
+ def each(client_id, limit: nil, &block)
93
+ each_item(path("clients", client_id, "packages"), limit, &block)
94
+ end
95
+
96
+ # POST /clients/:client_id/packages. Refused with e.g.
97
+ # +nothing_published_in_environment+ or +already_assigned+.
98
+ # @return [Hash] the assignment
99
+ def create(client_id, api_package_id:, environment_id:, idempotency_key: nil)
100
+ post_data(path("clients", client_id, "packages"),
101
+ { api_package_id: api_package_id, environment_id: environment_id }, idempotency_key)
102
+ end
103
+ alias assign create
104
+
105
+ # PATCH /clients/:client_id/packages/:id: move the assignment to
106
+ # another environment. @return [Hash]
107
+ def update(client_id, id, environment_id:)
108
+ patch_data(path("clients", client_id, "packages", id), { environment_id: environment_id })
109
+ end
110
+
111
+ # DELETE /clients/:client_id/packages/:id. @return [Hash] <tt>{"id", "deleted"}</tt>
112
+ def delete(client_id, id)
113
+ delete_data(path("clients", client_id, "packages", id))
114
+ end
115
+ alias unassign delete
116
+ end
117
+
118
+ # +/clients/:client_id/grants+: the grants a client holds directly from
119
+ # you (package-derived grants are managed through {PackageAssignments}).
120
+ #
121
+ # A grant: <tt>{"id", "client_organization_id", "target_application_id",
122
+ # "target_endpoint_id", "all_endpoints", "environment_id",
123
+ # "also_granted_by_api_package_ids", "synced_at", "status", "refusal",
124
+ # "inserted_at", "updated_at"}</tt>.
125
+ class Grants < Base
126
+ # @return [Page]
127
+ def list(client_id, limit: nil, after: nil)
128
+ list_page(path("clients", client_id, "grants"), limit, after)
129
+ end
130
+
131
+ # @return [Enumerator, nil]
132
+ def each(client_id, limit: nil, &block)
133
+ each_item(path("clients", client_id, "grants"), limit, &block)
134
+ end
135
+
136
+ # POST /clients/:client_id/grants. Leave +target_endpoint_id+ nil to
137
+ # grant every endpoint of the application. @return [Hash] the grant
138
+ def create(client_id, target_application_id:, environment_id:, target_endpoint_id: nil, idempotency_key: nil)
139
+ body = compact(target_application_id: target_application_id, target_endpoint_id: target_endpoint_id,
140
+ environment_id: environment_id)
141
+ post_data(path("clients", client_id, "grants"), body, idempotency_key)
142
+ end
143
+
144
+ # DELETE /clients/:client_id/grants/:id. @return [Hash] <tt>{"id", "deleted"}</tt>
145
+ def delete(client_id, id)
146
+ delete_data(path("clients", client_id, "grants", id))
147
+ end
148
+ alias revoke delete
149
+ end
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "error"
4
+
5
+ module EndPointBlank
6
+ module Management
7
+ # Decides whether a failed management API request is sent again, and
8
+ # after how long. See {Transport} for the rules.
9
+ class RetryPolicy
10
+ RETRYABLE_STATUSES = [500, 502, 503, 504].freeze
11
+
12
+ # GET and DELETE are idempotent; POST always carries an Idempotency-Key.
13
+ # PATCH is neither, so a 5xx or a lost answer to one is never re-sent.
14
+ RETRYABLE_METHODS = %w[GET DELETE POST].freeze
15
+
16
+ # Wait before retrying a 409 in progress that has no Retry-After header.
17
+ IN_PROGRESS_WAIT = 1
18
+ # Wait before retrying a 429 that has no Retry-After header (app_portal
19
+ # always sends one; a proxy's 429 may not).
20
+ RATE_LIMITED_WAIT = 1
21
+ # First backoff for a 5xx or a request with no answer; doubles per attempt.
22
+ BACKOFF_BASE = 0.5
23
+
24
+ attr_reader :max_retries, :max_retry_wait
25
+
26
+ def initialize(max_retries:, max_retry_wait:)
27
+ @max_retries = max_retries
28
+ @max_retry_wait = max_retry_wait
29
+ end
30
+
31
+ # Seconds to wait before attempt number +attempt+ + 1 (0 is the first
32
+ # retry), or nil when +error+ must be raised now.
33
+ def delay(method, error, attempt)
34
+ return nil if attempt >= max_retries
35
+
36
+ seconds = wanted_wait(method, error, attempt)
37
+ # A wait longer than max_retry_wait is not waited out.
38
+ seconds && seconds <= max_retry_wait ? seconds : nil
39
+ end
40
+
41
+ private
42
+
43
+ def wanted_wait(method, error, attempt)
44
+ return error.retry_after || RATE_LIMITED_WAIT if error.status == 429
45
+ return in_progress_wait(method, error) if error.code?(ErrorCodes::IDEMPOTENCY_REQUEST_IN_PROGRESS)
46
+ return nil unless server_side_failure?(error) && RETRYABLE_METHODS.include?(method)
47
+
48
+ error.retry_after || backoff(attempt)
49
+ end
50
+
51
+ # Only a POST carries the key the first request is still running under.
52
+ def in_progress_wait(method, error)
53
+ method == "POST" ? error.retry_after || IN_PROGRESS_WAIT : nil
54
+ end
55
+
56
+ def server_side_failure?(error)
57
+ error.code?(ErrorCodes::CONNECTION_ERROR) || RETRYABLE_STATUSES.include?(error.status)
58
+ end
59
+
60
+ def backoff(attempt)
61
+ [BACKOFF_BASE * (2**attempt), max_retry_wait].min
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,167 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "excon"
4
+ require "json"
5
+ require "uri"
6
+ require_relative "../version"
7
+ require_relative "error"
8
+ require_relative "retry_policy"
9
+ require_relative "idempotency_key"
10
+ require_relative "url_path"
11
+
12
+ module EndPointBlank
13
+ module Management
14
+ # The HTTP layer under {Client}: one request to the management API, with
15
+ # its headers, its Idempotency-Key and its retries. Built on Excon, the
16
+ # HTTP library the rest of this gem already uses.
17
+ #
18
+ # Retries (at most +max_retries+ after the first attempt, each wait passed
19
+ # to +sleeper+):
20
+ #
21
+ # - 429 +rate_limited+, any method: after Retry-After seconds. Nothing was
22
+ # done, so even a PATCH is safe to send again. A Retry-After longer than
23
+ # +max_retry_wait+ is not waited out; the error is raised instead.
24
+ # - 409 +idempotency_request_in_progress+ (POST only): shortly, with the
25
+ # same key, so the first request's answer is replayed once it finishes.
26
+ # - 5xx (+internal_server_error+, +audit_unavailable+,
27
+ # +intake_unavailable+, or a proxy's 502/503/504) and requests that never
28
+ # got an answer: for GET and DELETE, which are idempotent, and for POST,
29
+ # which always carries an Idempotency-Key. Never for PATCH.
30
+ #
31
+ # Every POST carries an Idempotency-Key (a random UUID v4 unless the
32
+ # caller passed one), and every retry of it sends the same key.
33
+ #
34
+ # 409 +idempotency_replay_unavailable+ is never retried: the first POST
35
+ # succeeded and its answer held a secret shown only once.
36
+ class Transport
37
+ API_PREFIX = "/api/v1"
38
+
39
+ attr_reader :base_url
40
+
41
+ # rubocop:disable Metrics/ParameterLists
42
+ def initialize(api_key:, base_url:, max_retries:, max_retry_wait:, connect_timeout:, read_timeout:,
43
+ sleeper:, excon_options: {})
44
+ @api_key = api_key
45
+ @base_url = base_url
46
+ @base_path = UrlPath.strip_trailing_slashes(URI.parse(base_url).path)
47
+ @retry_policy = RetryPolicy.new(max_retries: max_retries, max_retry_wait: max_retry_wait)
48
+ @connect_timeout = connect_timeout
49
+ @read_timeout = read_timeout
50
+ @sleeper = sleeper
51
+ @excon_options = excon_options
52
+ end
53
+ # rubocop:enable Metrics/ParameterLists
54
+
55
+ # Sends one API request and answers its decoded JSON body (nil for an
56
+ # empty body). +path+ is under /api/v1, e.g. "/organization".
57
+ #
58
+ # @raise [Error] for any answer outside 2xx, once retries are spent
59
+ def request(method, path, query: nil, body: nil, idempotency_key: nil)
60
+ method = method.to_s.upcase
61
+ idempotency_key = IdempotencyKey.resolve(method, idempotency_key)
62
+ attempt = 0
63
+
64
+ loop do
65
+ result = attempt_request(method, path, query, body, idempotency_key)
66
+ return result unless result.is_a?(Error)
67
+
68
+ wait_before_retry(method, path, result, attempt)
69
+ attempt += 1
70
+ end
71
+ end
72
+
73
+ def self.user_agent
74
+ "end_point_blank-ruby/#{EndPointBlank::VERSION} (management)"
75
+ end
76
+
77
+ def inspect
78
+ "#<#{self.class.name} base_url=#{base_url.inspect}>"
79
+ end
80
+
81
+ private
82
+
83
+ # The decoded body on success, or the Error to raise or retry on.
84
+ def attempt_request(method, path, query, body, idempotency_key)
85
+ response = perform(method, path, query, body, idempotency_key)
86
+ return Error.from_response(**error_fields(response, method, path)) unless success?(response)
87
+
88
+ decode(response, method, path)
89
+ rescue Excon::Error::StubNotFound
90
+ raise
91
+ rescue Excon::Error => e
92
+ connection_error(e, method, path)
93
+ end
94
+
95
+ def perform(method, path, query, body, idempotency_key)
96
+ connection = Excon.new(base_url, connect_timeout: @connect_timeout, read_timeout: @read_timeout,
97
+ write_timeout: @read_timeout, persistent: false, **@excon_options)
98
+ params = { method: method, path: "#{@base_path}#{API_PREFIX}#{path}",
99
+ headers: headers(body, idempotency_key) }
100
+ params[:query] = query if query && !query.empty?
101
+ params[:body] = JSON.generate(body) unless body.nil?
102
+ connection.request(params)
103
+ end
104
+
105
+ def headers(body, idempotency_key)
106
+ headers = {
107
+ "Authorization" => "Bearer #{@api_key}",
108
+ "Accept" => "application/json",
109
+ "User-Agent" => self.class.user_agent
110
+ }
111
+ headers["Content-Type"] = "application/json" unless body.nil?
112
+ headers["Idempotency-Key"] = idempotency_key if idempotency_key
113
+ headers
114
+ end
115
+
116
+ def success?(response)
117
+ (200..299).cover?(response.status)
118
+ end
119
+
120
+ def decode(response, method, path)
121
+ text = response.body.to_s
122
+ return nil if text.strip.empty?
123
+
124
+ JSON.parse(text)
125
+ rescue JSON::ParserError
126
+ Error.new("The management API answered HTTP #{response.status} with a body that is not JSON.",
127
+ code: ErrorCodes::INVALID_RESPONSE, status: response.status, http_method: method, path: path)
128
+ end
129
+
130
+ def error_fields(response, method, path)
131
+ { status: response.status, headers: response.headers, body: response.body,
132
+ http_method: method, path: path }
133
+ end
134
+
135
+ def connection_error(error, method, path)
136
+ detail = redact("#{error.class.name}: #{error.message}")
137
+ Error.new("Could not reach the management API at #{base_url} (#{detail}).",
138
+ code: ErrorCodes::CONNECTION_ERROR, http_method: method, path: path)
139
+ end
140
+
141
+ # Sleeps before the next attempt, or raises +error+ when it is not to
142
+ # be retried.
143
+ def wait_before_retry(method, path, error, attempt)
144
+ delay = @retry_policy.delay(method, error, attempt)
145
+ raise error if delay.nil?
146
+
147
+ log_retry(method, path, error, delay)
148
+ @sleeper.call(delay)
149
+ end
150
+
151
+ # Method, path, status and code only: never headers, query or bodies,
152
+ # which carry the key, ids and credential secrets.
153
+ def log_retry(method, path, error, delay)
154
+ EndPointBlank.logger.debug(
155
+ "[EndPointBlank] management API #{method} #{path} answered " \
156
+ "#{error.status || "no response"} (#{error.code}); retrying in #{delay}s"
157
+ )
158
+ end
159
+
160
+ def redact(text)
161
+ return text if @api_key.nil? || @api_key.empty?
162
+
163
+ text.gsub(@api_key, "[REDACTED]")
164
+ end
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EndPointBlank
4
+ module Management
5
+ # URL string helpers for the management client.
6
+ module UrlPath
7
+ SLASH = "/".ord
8
+
9
+ # +text+ without trailing slashes. One backward scan over the bytes, not
10
+ # a regex, so its cost is linear however the input is shaped.
11
+ def self.strip_trailing_slashes(text)
12
+ stop = text.bytesize
13
+ stop -= 1 while stop.positive? && text.getbyte(stop - 1) == SLASH
14
+ text.byteslice(0, stop)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "configuration_error"
4
+ require_relative "management/configuration"
5
+ require_relative "management/error_codes"
6
+ require_relative "management/error"
7
+ require_relative "management/page"
8
+ require_relative "management/retry_policy"
9
+ require_relative "management/idempotency_key"
10
+ require_relative "management/url_path"
11
+ require_relative "management/transport"
12
+ require_relative "management/client"
13
+
14
+ module EndPointBlank
15
+ # The organization management API client. See {Management::Client}.
16
+ #
17
+ # Configure defaults once, e.g. in config/initializers/end_point_blank.rb,
18
+ # apart from the runtime's EndPointBlank.configure:
19
+ #
20
+ # EndPointBlank::Management.configure do |m|
21
+ # m.api_key = Rails.application.credentials.dig(:end_point_blank, :management_key)
22
+ # end
23
+ #
24
+ # EndPointBlank::Management.client.organization
25
+ module Management
26
+ @configuration_mutex = Mutex.new
27
+
28
+ class << self
29
+ # The defaults {Client.new} reads. @return [Configuration]
30
+ def configuration
31
+ @configuration_mutex.synchronize { @configuration ||= Configuration.new }
32
+ end
33
+
34
+ # Yields {configuration} to change the defaults.
35
+ def configure
36
+ yield configuration
37
+ configuration
38
+ end
39
+
40
+ # A new {Client} from {configuration}; +options+ override it.
41
+ # @return [Client]
42
+ def client(**options)
43
+ Client.new(**options)
44
+ end
45
+
46
+ # Back to the built-in defaults (for tests).
47
+ def reset_configuration!
48
+ @configuration_mutex.synchronize { @configuration = Configuration.new }
49
+ end
50
+ end
51
+ end
52
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module EndPointBlank
4
- VERSION = "0.12.0"
4
+ VERSION = "0.13.0"
5
5
  end
@@ -36,6 +36,7 @@ require_relative "end_point_blank/rack/headers"
36
36
  require_relative "end_point_blank/unauthorized_error"
37
37
  require_relative "end_point_blank/token_unavailable_error"
38
38
  require_relative "end_point_blank/configuration_error"
39
+ require_relative "end_point_blank/management"
39
40
  if defined?(::Rails)
40
41
  require_relative "end_point_blank/rails/authenticated"
41
42
  require_relative "end_point_blank/rails/authorized"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: end_point_blank
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.12.0
4
+ version: 0.13.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Robert A. Lasch
@@ -120,6 +120,20 @@ files:
120
120
  - lib/end_point_blank/deprecation_headers.rb
121
121
  - lib/end_point_blank/fast_json_truncator.rb
122
122
  - lib/end_point_blank/log_entry.rb
123
+ - lib/end_point_blank/management.rb
124
+ - lib/end_point_blank/management/client.rb
125
+ - lib/end_point_blank/management/configuration.rb
126
+ - lib/end_point_blank/management/error.rb
127
+ - lib/end_point_blank/management/error_codes.rb
128
+ - lib/end_point_blank/management/idempotency_key.rb
129
+ - lib/end_point_blank/management/page.rb
130
+ - lib/end_point_blank/management/resources/api_packages.rb
131
+ - lib/end_point_blank/management/resources/applications.rb
132
+ - lib/end_point_blank/management/resources/base.rb
133
+ - lib/end_point_blank/management/resources/clients.rb
134
+ - lib/end_point_blank/management/retry_policy.rb
135
+ - lib/end_point_blank/management/transport.rb
136
+ - lib/end_point_blank/management/url_path.rb
123
137
  - lib/end_point_blank/masking.rb
124
138
  - lib/end_point_blank/middleware/rack/report_interaction.rb
125
139
  - lib/end_point_blank/rack/env_store.rb