Class: Axn::Webhooks::Outbound::Signer::HmacSigner
- Inherits:
-
Object
- Object
- Axn::Webhooks::Outbound::Signer::HmacSigner
- Defined in:
- lib/axn/webhooks/outbound/signer.rb
Overview
Parametric HMAC, the outbound face of verify :hmac. Emits ONE signature header plus an
optional timestamp header. header: is required: unlike Standard Webhooks there is no
universal header name, which is exactly why the inbound verifier requires signature:.
Constant Summary collapse
- PLACEHOLDERS =
%w[timestamp body].freeze
- DEFAULT_SIGNING_STRING =
"{body}"
Instance Method Summary collapse
-
#call(timestamp:, body:, subscriber: nil) ⇒ Object
id:is part of the signer contract but unused here — an id-bearing signature is what :standard_webhooks is for, and this preset emits no id header for a receiver to read one back from. -
#initialize(secret:, header:, digest: :sha256, encoding: :hex, prefix: nil, signing_string: DEFAULT_SIGNING_STRING, timestamp_header: nil) ⇒ HmacSigner
constructor
A new instance of HmacSigner.
Constructor Details
#initialize(secret:, header:, digest: :sha256, encoding: :hex, prefix: nil, signing_string: DEFAULT_SIGNING_STRING, timestamp_header: nil) ⇒ HmacSigner
Returns a new instance of HmacSigner.
110 111 112 113 114 115 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 145 146 147 148 149 150 151 152 153 154 |
# File 'lib/axn/webhooks/outbound/signer.rb', line 110 def initialize(secret:, header:, digest: :sha256, encoding: :hex, prefix: nil, signing_string: DEFAULT_SIGNING_STRING, timestamp_header: nil) validate_header_name!(:header, header) unless .nil? validate_header_name!(:timestamp_header, ) # The timestamp assignment in `call` lands SECOND and would overwrite the signature, # shipping every delivery unverifiable — silently. HTTP header names are # case-insensitive, so compare that way (Codex review). if header.casecmp?() raise ArgumentError, "sign :hmac `header:` and `timestamp_header:` are the same header name " \ "(#{header.inspect}) — the timestamp would overwrite the signature" end end # Same reasoning as :standard_webhooks — `resolved_secret` calls with NO arguments, or # with the PRO-3214 `Subscriber` for a 1-arity per-subscriber secret. A callable needing # MORE than that boots fine and raises on every real signing attempt. if secret.respond_to?(:call) && !(CallableArity.accepts?(secret, 0) || CallableArity.accepts?(secret, 1)) raise ArgumentError, "sign :hmac secret callable must accept zero or one arguments (resolved with no " \ "args, or the Subscriber, per signing attempt)" end # Both are finite sets in Signature; an unvalidated typo boots fine and then raises # inside EVERY delivery attempt — on the async path, after the job is enqueued, so it # retries the same broken config (Codex review). raise ArgumentError, "sign :hmac unsupported digest: #{digest.inspect}" unless Signature::DIGESTS.key?(digest) raise ArgumentError, "sign :hmac unsupported encoding: #{encoding.inspect}" unless Signature::ENCODINGS.include?(encoding) validate_template!(signing_string, ) # Copy every String we validated or emit. Validation runs ONCE, here; retaining the # caller's mutable object lets an app change what ships afterwards — # `header.replace("Content-Type")` walks straight past both the field-name grammar and # the MANAGED_HEADERS collision rule, and Deliver then overwrites the signature (Codex # review). Same validate-then-alias shape as Config's static `to:` array. @secret = secret # NOT copied: may be a callable, and a String secret is re-read per call anyway @header = dup_frozen(header) @digest = digest @encoding = encoding @prefix = dup_frozen(prefix) @signing_string = dup_frozen(signing_string) @timestamp_header = dup_frozen() end |
Instance Method Details
#call(timestamp:, body:, subscriber: nil) ⇒ Object
id: is part of the signer contract but unused here — an id-bearing signature is what
:standard_webhooks is for, and this preset emits no id header for a receiver to read one
back from. Absorbed by ** rather than named, so it isn't an unused argument.
159 160 161 162 163 164 165 166 167 168 169 170 |
# File 'lib/axn/webhooks/outbound/signer.rb', line 159 def call(timestamp:, body:, subscriber: nil, **) sig = Signature.compute( secret: resolved_secret(subscriber), payload: render(timestamp:, body:), digest: @digest, encoding: @encoding, ) headers = { @header => "#{@prefix}#{sig}" } headers[@timestamp_header] = .to_s if @timestamp_header headers end |