Skip to content

feat(connector): [CHECKOUT] complete wallet decryption — mandates, token passthrough, and tokenization - #2121

Open
pixincreate wants to merge 11 commits into
mainfrom
feat/checkout-wallet-decrypt-recurring
Open

feat(connector): [CHECKOUT] complete wallet decryption — mandates, token passthrough, and tokenization#2121
pixincreate wants to merge 11 commits into
mainfrom
feat/checkout-wallet-decrypt-recurring

Conversation

@pixincreate

@pixincreate pixincreate commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

What

Three gaps in Checkout's wallet handling.

1. Decrypted wallets were rejected by SetupMandate

Authorize already built ApplePayPredecrypt / GooglePayPredecrypt sources and set store_for_future_use: true on mandate payments, but the SetupMandate transformer matched only Card and AchBankDebit — a zero-amount mandate setup with Apple Pay or Google Pay was rejected as NotImplemented.

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::Wallets was 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 misleading MissingRequiredField. Both Authorize and SetupMandate now consume PaymentMethodData::PaymentMethodToken and map it to source.type = "token".

3. UCS could not mint the token itself

Implements the PaymentMethodToken flow (POST /tokens, CheckoutTokenRequest / CheckoutTokenResponse) and removes it from not_implemented.

Auth uses the existing auth fields — no new credential. Checkout's /tokens authenticates with the account public key and /payments with the secret key. Those map onto CheckoutAuthType as api_key = public key, api_secret = secret key. build_tokenization_headers reads api_key; payment flows stay on api_secret. The mapping is counter-intuitive, so it is documented on CheckoutAuthType and 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

Check Result
Card Authorize baseline CHARGED pay_wsibhwybbydibfsh45aj2wfa6m
Google Pay decrypted (Authorize) Accepted 201; issuer simulator declined the dummy cryptogram (20068)
Tokenize auth via api_key Credential accepted — 422 on payload, not 401/403
Missing-config error surface Missing required field named precisely
Encrypted wallet → Authorize Unimplemented, pointing at the tokenize step

Tokenization request bytes, verified against a capture server:

POST /tokens
authorization: Bearer <public key>
{"type":"applepay","token_data":{"version":"EC_v1","data":"…","signature":"…","header":{…}}}
{"type":"googlepay","token_data":{"protocolVersion":"ECv1","signature":"…","signedMessage":"…"}}

cargo clippy -p connector-integration --all-targets warning-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:

  1. The available sandbox pk_ and sk_ are from different Checkout accounts. Proven with UCS out of the picture — mint with pk_201; spend that token with sk_422 token_invalid; control: the same sk_ charges a raw card → 201. Needs a pk_sbox_… from the same account as the sk_.
  2. Wallet tokenization additionally needs a live wallet SDK payload and, for Apple Pay, a processing certificate. Hand-built payloads return 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 06

GOOGLE_PAY_DEFAULT_ECI = "06" is sent for every decrypted Google Pay network token; eci_indicator is not read. Confirmed at runtime — input 05 reaches Checkout as 06.

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 05 correctly for the same input.

Kept as-is on purpose so the two implementations stay byte-identical on this connector. Consequence worth knowing: 05 is fully-authenticated and 06 is attempted, so this forfeits the liability shift on CRYPTOGRAM_3DS tokens. Any fix should land on both sides together.

🤖 Generated with Claude Code

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>
@pixincreate pixincreate added rust Pull requests that update rust code enhancement New feature or request labels Aug 13, 2026
@pixincreate pixincreate self-assigned this Aug 13, 2026
pixincreate and others added 2 commits August 14, 2026 00:31
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.
@pixincreate pixincreate changed the title feat(connector): [CHECKOUT] accept decrypted wallets in SetupMandate feat(connector): [CHECKOUT] complete wallet decryption — mandates, and connector-side tokenization Aug 14, 2026
…decrypt-recurring

* origin/main:
  chore(version): 2026.08.14.0
  feat(observability): add typed connector request/response fields (#2036)
  fix(connectors): [tsys_transit] add support for psync and rsync (#2100)
  chore(version): 2026.08.13.1
  fix(grabpay,maya): added superposition urls (#2118)
…_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 pixincreate changed the title feat(connector): [CHECKOUT] complete wallet decryption — mandates, and connector-side tokenization feat(connector): [CHECKOUT] complete wallet decryption — mandates, token passthrough, and tokenization Aug 14, 2026
@pixincreate
pixincreate marked this pull request as ready for review August 14, 2026 06:24
@pixincreate
pixincreate requested review from a team as code owners 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.
@hyperswitch-bot
hyperswitch-bot Bot requested a review from a team as a code owner August 14, 2026 06:44
Comment thread crates/integrations/connector-integration/src/connectors/checkout.rs Outdated
Comment thread crates/integrations/connector-integration/src/connectors/checkout.rs Outdated
Comment thread crates/integrations/connector-integration/src/connectors/checkout.rs Outdated
pixincreate and others added 3 commits August 14, 2026 14:26
…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
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>
…decrypt-recurring

* origin/main:
  docs(grace): require superposition URL registration + URL patching for new connectors (#2123)
  chore(version): 2026.08.14.1
  fix(connector): read calida shop name from connector specific config (#2106)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request rust Pull requests that update rust code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants