fix: strict compliance with API and webhook security spec - #6
Open
danieldenis01 wants to merge 4 commits into
Open
fix: strict compliance with API and webhook security spec#6danieldenis01 wants to merge 4 commits into
danieldenis01 wants to merge 4 commits into
Conversation
Three fixes uncovered while integrating the SDK in a real-world Pay
adapter against the AbacatePay sandbox. All low-risk, no public API
changes:
1. Compact nil keys before sending JSON payloads.
The current API rejects requests like { cellphone: null } or
{ description: null } with HTTP 400 ("Expected property X to be
string but found: null"). The clients construct payloads with
optional fields and let nil through. Adding .compact matches what
the API actually accepts.
- clients/customer_client.rb (#create)
- clients/product_client.rb (#create)
2. HMAC webhook signature: switch from hex to base64.
https://docs.abacatepay.com/pages/webhooks/security specifies
HMAC-SHA256 base64-encoded. The previous verify! used
OpenSSL::HMAC.hexdigest, which never matched real deliveries.
3. Default the HMAC key to AbacatePay's documented PUBLIC_KEY.
The doc says webhooks are signed with a fixed public key (not the
per-webhook secret). Exposed as AbacatePay::Webhooks::PUBLIC_KEY;
verify!/valid? now default to it. Callers can still override
secret: for tests.
Adds base64 as an explicit runtime dependency (Ruby 3.4 no longer
ships it with the default gems).
Suite: 327 examples, 0 failures.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
…Error
AbacatePay production V2 keys use the `abc_prod_` prefix, which the version
heuristic in Configuration#api_url didn't recognize — only `abc_dev_` and
`abc_live_`. Production keys therefore resolved to v1, and v2-only endpoints
(e.g. POST /v2/products/create) returned {"error":"Not found"}. Sandbox keys
(abc_dev_) were unaffected, so the bug only surfaced in production.
Also stop the generic `rescue StandardError` in Client#request from
re-wrapping AbacatePay::Error (the ApiError raised just above) as
"Unexpected error: ...", which hid the real cause in Sentry.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The live API returns the copy-paste code as `brCode` and the image as `brCodeBase64` (data URI), plus `expiresAt`/`platformFee`/`receiptUrl` — the resource only knew the legacy `qrCode`/`qrCodeImage` names and silently dropped the real fields. Expose the new attributes and keep `qr_code`/`qr_code_image` as fallbacks so both shapes work. Co-Authored-By: Claude Opus 5 <[email protected]>
The API rejects the id in the JSON body ("Expected property 'id' to be
string but found: undefined") — it expects ?id= like #check does.
Co-Authored-By: Claude Opus 5 <[email protected]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Small fixes uncovered while building a
pay-rails/payadapter on top of the SDK and exercising it against the AbacatePay sandbox and production. All low-risk, no public API changes — defaults align the SDK with what the API and the docs actually require today.1. Compact
nilkeys before sending JSON payloadsThe current API rejects requests like
{ "cellphone": null }or{ "description": null }with HTTP 400:The clients build payloads with optional fields and pass
nilstraight through. Adding.compacton the request hash matches what the API actually accepts. Affected:clients/customer_client.rb#createclients/product_client.rb#createThis was hit in production-style flows where a
Pay::Customerhad nophone_numberset, and where one-time products were created without adescription.2. HMAC webhook signature: hex → base64
docs.abacatepay.com/pages/webhooks/securityspecifies HMAC-SHA256 base64-encoded in theX-Webhook-Signatureheader. The previous implementation usedOpenSSL::HMAC.hexdigest, which never matched the signatures the AbacatePay platform actually sends, soverify!rejected every real delivery as invalid.3. Default the HMAC key to AbacatePay's documented
PUBLIC_KEYThe same security doc states webhooks are signed with a fixed public key (not the per-webhook secret you configure in the dashboard — that one goes in the
?webhookSecret=query parameter). The constant is exposed asAbacatePay::Webhooks::PUBLIC_KEYand is now the default forverify!/valid?, so callers that just want to verify a delivery don't have to know about it. Thesecret:kwarg still works for tests and for any future per-webhook signing scheme.4. Recognize
abc_prod_production keys (and stop masking API errors)Configuration#api_urlselects the API version (v1/v2) from the token prefix. It matchedabc_dev_andabc_live_→ v2, but production keys are issued with theabc_prod_prefix, which fell through to v1. Since the product endpoints (POST /v2/products/create, etc.) only exist on v2, every production call to them came back as:…while the exact same code worked in sandbox (
abc_dev_→ v2). Widened the matcher to/\Aabc_(dev|live|prod)_/so production keys resolve to v2 too.While here,
Client#requestwrapped everyStandardError— including theApiErrorit raises itself for{"error": ...}responses — intoApiError, "Unexpected error: #{e.message}", double-wrapping the message and dropping the original context.AbacatePay::Error(and subclasses) now propagate untouched; only genuinely unexpected errors get theUnexpected errorwrapper. This is what made theNot foundabove surface as the less usefulUnexpected error: API error: Not found.Other
Adds
base64as an explicit runtime dependency. Ruby 3.4 stopped shipping it as a default gem, sorequire "base64"started warning (and would eventually break).Test plan
bundle exec rspec— 333 examples, 0 failuresspec/configuration_spec.rbcovering version detection forabc_dev_/abc_live_/abc_prod_(→ v2) and legacy/nil tokens (→ v1); client spec asserts API error messages are no longer double-wrappedabc_prod_→ v1Not foundagainst AbacatePay production, and confirmed the fix routes production keys to v2🤖 Generated with Claude Code