feat(connector): [CHECKOUT] complete wallet decryption — mandates, token passthrough, and tokenization - #2121
Open
pixincreate wants to merge 11 commits into
Open
feat(connector): [CHECKOUT] complete wallet decryption — mandates, token passthrough, and tokenization#2121pixincreate wants to merge 11 commits into
pixincreate wants to merge 11 commits into
Conversation
The Authorize flow already builds ApplePayPredecrypt/GooglePayPredecrypt sources from wallet tokens Hyperswitch has decrypted, but the SetupMandate transformer matched only Card and AchBankDebit, so a zero-amount mandate setup with Apple Pay or Google Pay was rejected as NotImplemented. Extract the wallet-decrypt source construction into build_predecrypted_wallet_source and call it from both flows, so a mandate setup accepts the same wallets as a regular payment. Hard-coded source type strings are lifted to named constants. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Checkout decrypts wallet payloads at its end, but only behind a separate POST /tokens exchange that yields a single-use tok_...; POST /payments cannot carry a raw wallet payload. Hyperswitch performs that exchange and hands the token down, but UCS never consumed it: PaymentSource::Wallets was declared and never constructed, so the connector-decryption path was unreachable and an encrypted wallet died with a misleading MissingRequiredField. Consume PaymentMethodData::PaymentMethodToken in both Authorize and SetupMandate, mapping it to source.type = "token" — the same shape hyperswitch builds from PaymentMethodToken::Token. Encrypted wallet payloads now fail with NotSupported carrying the remediation and the /tokens doc URL instead of a field-missing error. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds the connector-decryption head for Checkout wallets: UCS now performs Checkout's `POST /tokens` exchange itself, turning a raw Apple Pay / Google Pay payload into a single-use `tok_...`. The tail already existed — `PaymentMethodData::PaymentMethodToken` is consumed by Authorize and SetupMandate as `source.type = "token"` — so this closes the loop. - Implement the `PaymentMethodToken` flow (`CheckoutTokenRequest` / `CheckoutTokenResponse`) and remove it from the connector's `not_implemented` list. - Checkout authenticates `/tokens` with the account *public* key (`pk_...`), not the secret key: `Bearer sk_...` is rejected 403 and an `api_key` header 401. Add an optional `public_key` to `ConnectorSpecificConfig::Checkout` and `CheckoutConfig` in the proto, and give the tokenization flow its own header builder. The field is optional so merchants that never tokenize encrypted wallets are unaffected; a missing key surfaces as an actionable `connector_config.checkout.public_key` error rather than a 401. - Keep the encrypted-wallet Authorize arms as errors, but restate them: an encrypted wallet is no longer unsupported, it is simply at the wrong step of a two-call sequence. Tokenizing inline from Authorize was rejected deliberately — Checkout's tokens are single-use and expire in 15 minutes, so it would hide a second network call (with different credentials) inside a flow the caller believes is one request, and break on any Authorize retry.
…_key The previous commit added a `public_key` field to `CheckoutConfig` (proto) and `ConnectorSpecificConfig::Checkout`. That was wrong: hyperswitch already carries the public key in `api_key` for Checkout, and UCS simply was not using it — `api_key` fed no header at all, which was the actual bug rather than a reason to add a field. Revert the whole `public_key` addition — proto, domain type (both conversion sites), and the field-probe value — and point `build_tokenization_headers` at the existing `api_key`, matching hyperswitch: crates/hyperswitch_connectors/src/connectors/checkout.rs:131 -> api_secret crates/hyperswitch_connectors/src/connectors/checkout.rs:278 -> api_key payment.proto is now byte-identical to origin/main, so this PR no longer touches the proto at all. Checkout's key roles are genuinely counter-intuitive — `api_key` is the *public* key and `api_secret` the *secret* key — so record that on `CheckoutAuthType` and at both header builders to stop it being "fixed" back. Confirmed against the sandbox: sk_ on /tokens -> 403, pk_ on /tokens -> 201, pk_ on /payments -> 401.
pixincreate
marked this pull request as ready for review
August 14, 2026 06:24
Auto-applied by CI: - cargo +nightly fmt --all - make -C sdk generate (if applicable) - make docs (if applicable) This commit was automatically generated by GitHub Actions.
Shubhodip900
requested changes
Aug 14, 2026
…omments Addresses review feedback on #2121: - both header builders now call common_get_content_type() instead of repeating the "application/json" literal - comments no longer reference hyperswitch by name Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…urring' into feat/checkout-wallet-decrypt-recurring * origin/feat/checkout-wallet-decrypt-recurring: chore: auto-fix formatting and generated code
… builders Both header builders call common_get_content_type() rather than repeating the "application/json" literal, and the auth comment no longer names another product. An earlier attempt at this staged `connectors/checkout`, which matches only the module directory and never `checkout.rs` beside it, so the change was left in the working tree. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Shubhodip900
previously approved these changes
Aug 14, 2026
The connector specs coverage check requires a declared suite for every implemented flow. PaymentMethodToken was implemented here without declaring PaymentMethodService/Tokenize, which fails that check the same way the worldpayxml recurring flows did. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
What
Three gaps in Checkout's wallet handling.
1. Decrypted wallets were rejected by SetupMandate
Authorize already built
ApplePayPredecrypt/GooglePayPredecryptsources and setstore_for_future_use: trueon mandate payments, but theSetupMandatetransformer matched onlyCardandAchBankDebit— a zero-amount mandate setup with Apple Pay or Google Pay was rejected asNotImplemented.Extracted into
build_predecrypted_wallet_source, now shared by Authorize and SetupMandate; magic strings lifted to named constants.2. The connector-decryption path was unreachable
PaymentSource::Walletswas declared and never constructed, so a wallet Checkout should decrypt at its own end had nowhere to go, and an encrypted payload died with a misleadingMissingRequiredField. Both Authorize and SetupMandate now consumePaymentMethodData::PaymentMethodTokenand map it tosource.type = "token".3. UCS could not mint the token itself
Implements the
PaymentMethodTokenflow (POST /tokens,CheckoutTokenRequest/CheckoutTokenResponse) and removes it fromnot_implemented.Auth uses the existing auth fields — no new credential. Checkout's
/tokensauthenticates with the account public key and/paymentswith the secret key. Those map ontoCheckoutAuthTypeasapi_key= public key,api_secret= secret key.build_tokenization_headersreadsapi_key; payment flows stay onapi_secret. The mapping is counter-intuitive, so it is documented onCheckoutAuthTypeand at both header builders with an explicit warning against re-introducing a separate public-key field.Verified on the sandbox:
sk_→/tokens= 403,pk_→/tokens= 201,pk_→/payments= 401.No inline tokenization from Authorize. Checkout's tokens are single-use with a 15-minute expiry, so an inline mint would hide a second network call inside a request the caller believes is atomic and would break on retry. Callers tokenize first, then authorize.
Testing — real Checkout sandbox
pay_wsibhwybbydibfsh45aj2wfa6m20068)api_keyMissing required fieldnamed preciselyUnimplemented, pointing at the tokenize stepTokenization request bytes, verified against a capture server:
cargo clippy -p connector-integration --all-targetswarning-free. No proto change — this PR touches only the two connector files plus generated docs/examples.Not proven end to end
Minting a real
tok_and spending it in one pass is not demonstrated, for two reasons outside this code:pk_andsk_are from different Checkout accounts. Proven with UCS out of the picture — mint withpk_→201; spend that token withsk_→422 token_invalid; control: the samesk_charges a raw card →201. Needs apk_sbox_…from the same account as thesk_.merchant_certificate_not_found/token_data_invalid— wallet blobs are cryptographically signed and cannot be synthesized.SetupMandate with a decrypted wallet (change 1) has also not been exercised against the sandbox.
Deliberate divergence: Google Pay ECI hardcoded to
06GOOGLE_PAY_DEFAULT_ECI = "06"is sent for every decrypted Google Pay network token;eci_indicatoris not read. Confirmed at runtime — input05reaches Checkout as06.This matches the upstream integration exactly, including the asymmetry that the Apple Pay path in the same file forwards the real ECI. Inherited, not introduced here — worldpayxml forwards
05correctly for the same input.Kept as-is on purpose so the two implementations stay byte-identical on this connector. Consequence worth knowing:
05is fully-authenticated and06is attempted, so this forfeits the liability shift on CRYPTOGRAM_3DS tokens. Any fix should land on both sides together.🤖 Generated with Claude Code