Module: ZeroClick::Sellers
- Defined in:
- lib/zeroclick/sellers.rb,
lib/zeroclick/sellers/usage.rb,
lib/zeroclick/sellers/client.rb,
lib/zeroclick/sellers/errors.rb,
lib/zeroclick/sellers/verify.rb,
lib/zeroclick/sellers/railtie.rb,
lib/zeroclick/sellers/version.rb,
lib/zeroclick/sellers/contracts.rb,
lib/zeroclick/sellers/responses.rb,
lib/zeroclick/sellers/middleware.rb
Defined Under Namespace
Modules: Middleware, Responses, Usage, Verify Classes: Allow, AllowanceDecision, Client, Deny, Error, Railtie, ReportUsageResult, Request, Response, SyncUsageItem, UsageItem, VerifyFailure, VerifyOk, ZeroClickContext
Constant Summary collapse
- ALLOWANCE_API_ERROR_CODES =
Errors meaning "the allowance API did not give us an answer", as opposed to "the allowance API said no". Only these are subject to the configured allowance-unavailable policy; anything else propagates.
%w[ api_transport_error api_status_error api_response_invalid ].freeze
- VERSION =
Bump this to publish. The workflow reads it, asks RubyGems whether this version already exists, and publishes only when it does not.
"0.1.0"- DEFAULT_API_BASE_URL =
"https://api.zeroclick.io"- DEFAULT_TOLERANCE_SECONDS =
300- DEFAULT_CHECK_TIMEOUT_SECONDS =
1.5- USAGE_DENIAL_REASONS =
Reasons the allowance API gives for refusing. Any other reason on the wire is treated as a malformed response rather than silently passed through.
%w[ service_not_found access_not_found access_inactive plan_expired meter_not_found meter_not_priced usage_exhausted ].freeze
- ALLOWANCE_UNAVAILABLE_POLICIES =
%w[allow deny throw].freeze
Class Method Summary collapse
-
.configure(**options) ⇒ Object
Store the options a later .seller call will build from.
-
.create(**options) ⇒ Object
Create a seller client.
-
.error?(error, code = nil) ⇒ Boolean
True when
erroris an SDK error, optionally of a specific code. - .normalize_usage(usage, operation:, allow_empty:) ⇒ Object
- .require_positive_integer!(value, field_name, operation) ⇒ Object
- .require_slug!(value, field_name, operation) ⇒ Object
-
.secrets_from_env(value = ENV.fetch("ZEROCLICK_SIGNING_SECRETS", nil)) ⇒ Object
Parse ZEROCLICK_SIGNING_SECRETS, formatted
kid1:secret1,kid2:secret2. -
.seller ⇒ Object
The process-wide client built from .configure.
-
.verify_request(request, **options) ⇒ Object
Verify a ZeroClick-signed request.
Class Method Details
.configure(**options) ⇒ Object
Store the options a later .seller call will build from. Used by the Railtie; call it directly in a non-Rails app if you want the same process-wide singleton.
32 33 34 35 36 |
# File 'lib/zeroclick/sellers.rb', line 32 def configure(**) @config = @seller = nil self end |
.create(**options) ⇒ Object
Create a seller client. See Client#initialize for the options.
25 26 27 |
# File 'lib/zeroclick/sellers.rb', line 25 def create(**) Client.new(**) end |
.error?(error, code = nil) ⇒ Boolean
True when error is an SDK error, optionally of a specific code.
44 45 46 |
# File 'lib/zeroclick/sellers/errors.rb', line 44 def self.error?(error, code = nil) error.is_a?(Error) && (code.nil? || error.code == code.to_s) end |
.normalize_usage(usage, operation:, allow_empty:) ⇒ Object
230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 |
# File 'lib/zeroclick/sellers/contracts.rb', line 230 def normalize_usage(usage, operation:, allow_empty:) items = Array(usage) if items.empty? && !allow_empty raise Error.new("malformed_input", operation: operation, message: "usage must declare at least one meter") end seen = {} items.each do |item| unless item.is_a?(UsageItem) raise Error.new("malformed_input", operation: operation, message: "usage entries must be UsageItem instances") end if seen.key?(item.meter_slug) raise Error.new("malformed_input", operation: operation, message: "Duplicate meterSlug entries are not allowed", meter_slug: item.meter_slug) end seen[item.meter_slug] = true end items.freeze end |
.require_positive_integer!(value, field_name, operation) ⇒ Object
221 222 223 224 225 226 227 228 |
# File 'lib/zeroclick/sellers/contracts.rb', line 221 def require_positive_integer!(value, field_name, operation) # true is not an Integer in Ruby, so unlike Python there is no bool # subclass to exclude here. return if value.is_a?(Integer) && value.positive? raise Error.new("malformed_input", operation: operation, message: "#{field_name} must be a positive integer") end |
.require_slug!(value, field_name, operation) ⇒ Object
214 215 216 217 218 219 |
# File 'lib/zeroclick/sellers/contracts.rb', line 214 def require_slug!(value, field_name, operation) return if value.is_a?(String) && !value.empty? && value == value.strip raise Error.new("malformed_input", operation: operation, message: "#{field_name} must be a non-empty string without surrounding whitespace") end |
.secrets_from_env(value = ENV.fetch("ZEROCLICK_SIGNING_SECRETS", nil)) ⇒ Object
Parse ZEROCLICK_SIGNING_SECRETS, formatted kid1:secret1,kid2:secret2.
Rotation is why this is a map rather than a single secret: during a rotation both the old and the new kid must verify, or every in-flight request fails.
55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
# File 'lib/zeroclick/sellers.rb', line 55 def secrets_from_env(value = ENV.fetch("ZEROCLICK_SIGNING_SECRETS", nil)) if value.nil? || value.strip.empty? raise Error.new("malformed_input", operation: "secrets_from_env", message: "ZEROCLICK_SIGNING_SECRETS is unset or empty") end value.split(",").each_with_object({}) do |pair, out| kid, separator, secret = pair.partition(":") kid = kid.strip secret = secret.strip if separator.empty? || kid.empty? || secret.empty? raise Error.new("malformed_input", operation: "secrets_from_env", message: "Expected ZEROCLICK_SIGNING_SECRETS as kid1:secret1,kid2:secret2") end out[kid] = secret end end |
.seller ⇒ Object
The process-wide client built from .configure. Lazy, so boot does not depend on credentials being present in every environment.
40 41 42 43 44 45 46 47 48 |
# File 'lib/zeroclick/sellers.rb', line 40 def seller @seller ||= begin if @config.nil? || @config.empty? raise Error.new("malformed_input", operation: "seller", message: "Call ZeroClick::Sellers.configure first (or config.zeroclick under Rails)") end create(**@config) end end |