Class: Axn::Webhooks::Outbound::Signer::StandardWebhooksSigner
- Inherits:
-
Object
- Object
- Axn::Webhooks::Outbound::Signer::StandardWebhooksSigner
- Defined in:
- lib/axn/webhooks/outbound/signer.rb
Overview
Standard Webhooks: secret is whsec_<base64>; sign id.timestamp.body (sha256/base64);
emit v1,<sig> alongside the id/timestamp headers the inbound verifier reads.
Class Method Summary collapse
-
.decode_secret(secret) ⇒ Object
The raw HMAC key behind a Standard Webhooks secret, or nil if it isn't one:
whsec_+ a base64 body that decodes to something non-empty.
Instance Method Summary collapse
- #call(id:, timestamp:, body:, subscriber: nil) ⇒ Object
-
#initialize(secret:) ⇒ StandardWebhooksSigner
constructor
A new instance of StandardWebhooksSigner.
Constructor Details
#initialize(secret:) ⇒ StandardWebhooksSigner
Returns a new instance of StandardWebhooksSigner.
273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 |
# File 'lib/axn/webhooks/outbound/signer.rb', line 273 def initialize(secret:) # A pure declaration mistake, decided once at boot from the callable's own shape (not # from what it resolves to) — ArgumentError, matching Config's misconfiguration split. # `resolve_secret` below calls `@secret.call` with NO arguments, or with the PRO-3214 # `Subscriber` for a 1-arity per-subscriber secret; a callable needing MORE than that # would otherwise boot successfully and raise ArgumentError on every real signing attempt # (Codex P2 finding, widened for the subscriber-aware case). if secret.respond_to?(:call) unless CallableArity.accepts?(secret, 0) || CallableArity.accepts?(secret, 1) raise ArgumentError, "sign :standard_webhooks secret callable must accept zero or one arguments " \ "(resolved with no args, or the Subscriber, per signing attempt)" end else # A LITERAL secret is fully knowable now, so the whsec_ check that guards every signing # attempt runs once here instead. Otherwise the natural mistake — pasting the raw key a # vendor's dashboard shows you, without the `whsec_` prefix — declares cleanly and then # raises inside EVERY delivery attempt, which is the worst place for it: an async # adapter retries that as if it were a transient network failure. ArgumentError (not # Axn::Webhooks::Error) to match the arity check above and Config's misconfiguration # split: a declaration mistake, decided at boot. # # A CALLABLE secret is deliberately NOT resolved here — it may read a secret store or # be per-subscriber, so its VALUE stays a per-attempt check (its arity is all that's # knowable at boot). Documented in the README's "Boot-time validation" section. raise ArgumentError, (secret) unless Verifiers::StandardWebhooks.secret_key(secret) end @secret = secret end |
Class Method Details
.decode_secret(secret) ⇒ Object
The raw HMAC key behind a Standard Webhooks secret, or nil if it isn't one: whsec_ +
a base64 body that decodes to something non-empty. Both the boot-time check above and
the per-attempt decoded_secret below go through this, so validity and the decoded
bytes can never disagree, and the decode happens exactly once per caller.
An unprefixed or blank secret would otherwise decode "successfully" (both are valid
base64) and sign every delivery with an empty or wrong key — silently, since the
receiver's 401 is indistinguishable from any other misconfiguration (Codex P1 finding).
The rescue is scoped to ONLY the decode: a callable secret's own resolver may raise its
own ArgumentError for an unrelated reason (a secret-store wrapper rejecting a malformed
response), and that diagnostic must reach Axn.config.on_exception intact rather than
being rewritten as a generic invalid-secret message (Codex P2 finding) — which is why
resolution happens in decoded_secret, outside this method.
317 318 319 320 321 322 323 324 |
# File 'lib/axn/webhooks/outbound/signer.rb', line 317 def self.decode_secret(secret) return nil unless secret.is_a?(String) && secret.start_with?("whsec_") decoded = Verifiers::StandardWebhooks.decode_secret(secret) decoded.empty? ? nil : decoded rescue ArgumentError nil end |
Instance Method Details
#call(id:, timestamp:, body:, subscriber: nil) ⇒ Object
326 327 328 329 330 331 332 333 334 335 336 337 338 |
# File 'lib/axn/webhooks/outbound/signer.rb', line 326 def call(id:, timestamp:, body:, subscriber: nil) sig = Signature.compute( secret: decoded_secret(subscriber), payload: "#{id}.#{}.#{body}", digest: :sha256, encoding: :base64, ) { "webhook-id" => id.to_s, "webhook-timestamp" => .to_s, "webhook-signature" => "v1,#{sig}", } end |