You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
## Summary
Release 2.10.0, parity with node-commerce 2.13.0, plus the maintenance
sweep. Worked with Varun.
**Verification session without a payment credential.** On an
identity-gated `Checkout` the gate runs only on the settle leg, so a
buyer with no identity reached the session-bearing 403 only by first
sending a payment credential (a Stripe SPT buyer had to mint a token to
learn they needed to verify). Now the discovery 402 carries an
`identity_bootstrap` block (`header`, `value`, `instructions`) whenever
the store is identity-gated and the request has no identity header, and
a request carrying `X-Verification-Session: create` (case-insensitive),
no identity and no payment credential runs the gate, which returns its
existing session 403 with `verify_url`, `session_id`, `poll_secret` and
`poll_url`. It is opt-in because discovery scanners replay the Bazaar
example body on a schedule, and minting on every identity-less 402 would
create a session and a pending order per probe. Gateless merchants
neither advertise nor honor it. The generated `llms.txt` identity
section and the Stripe/Link onboarding step name the header.
**Concurrent PaymentIntent creation.** Concurrent creates under one
idempotency key collide at Stripe as a resource-specific 429 that says
not to retry (seen in production from the Node SDK).
`create_multichain_payment_intent` is synchronous, so the race exists
across the worker threads of a threaded server (Flask, Django); keyed
calls now share one in-flight call, with followers waiting on the leader
and a failure reaching every waiter before the key clears. Unkeyed calls
are unchanged.
**Sweep.** `uv lock --upgrade` (x402 2.23.0 to 2.24.0, starlette 1.7.0,
uvicorn 0.54.0, sentry-sdk 2.71.0, werkzeug 3.1.9, regex, plus the ruff
and ty dev bumps) and the uv pin passed to `setup-uv` in the CI, publish
and security workflows moves from 0.12.17 to 0.12.20. The x402 move was
checked against its wheels: every EVM `constants.py` (including the Base
USDC name that the EIP-712 domain depends on) is byte-identical; the
changes are in the HTTP server and middleware.
## Type of change
- [ ] Bug fix (no breaking change)
- [x] New feature (no breaking change)
- [ ] Breaking change (existing callers must update)
- [ ] Docs, tests, or internal maintenance only
## Public API
Additive. New export `VERIFICATION_SESSION_HEADER` from the top-level
package. Identity-gated 402 bodies gain a top-level `identity_bootstrap`
object; a merchant `body_extras` key of the same name still wins.
`create_multichain_payment_intent` keeps its signature. No migration
needed.
## Test plan
- `tests/test_checkout_verification_bootstrap.py` drives the real
`handle_fastapi` adapter (the Python gate reads identity from the native
request): the 402 advertises the header only on a gated store with no
identity; the header returns the session 403; name and value match
case-insensitively; an identity header or other value is ignored; a
gateless store neither advertises nor honors it. With the new logic
disabled, the three behavior tests fail and the three controls pass.
- `tests/test_stripe_multichain.py`: three threads sharing a key make
one Stripe call; distinct and unkeyed calls do not share; a failure
reaches every waiter and the key clears for the next call. The sharing
tests fail on the previous code.
- Ran locally, as CI runs them: ruff check, ruff format --check, ty
(package and examples), vulture, pytest (1878 passed, 4 skipped, 95.40%
coverage). osv-scanner over `uv.lock`: 144 packages, no issues. No
prerelease versions in the lock.
## Checklist
- [x] Tests cover the new behavior, and the suite passes locally
- [x] Lint, format, and type checks pass
- [x] Docs and README examples updated if the public surface changed
- [x] No secrets, credentials, or personal data in the diff or the tests
Copy file name to clipboardExpand all lines: CLAUDE.md
+2Lines changed: 2 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -83,6 +83,8 @@ Two identity types: wallet (`X-Wallet-Address`) and operator-token (`X-Operator-
83
83
84
84
`create_session_on_missing` auto-mints a verification session when no identity is present AND when `wallet_not_trusted` carries fixable reasons (`kyc_required` / `kyc_pending` / `kyc_failed`) — both paths rewrite the denial to `identity_verification_required` before reaching `on_denied`. When the merchant omits `create_session_on_missing` from `CheckoutGateConfig`, `Checkout` auto-defaults it from `gate.api_key` + `gate.base_url` + `gate.context` + `gate.merchant_name`. Merchants that need `on_before_session` side effects (e.g. pre-minting an order_id) supply their own config to override.
85
85
86
+
**Getting a verify_url before paying.** On an identity-gated `Checkout`, the gate runs only on a settle leg, so a buyer with no identity reached the session 403 only by sending a payment credential first (an SPT buyer had to mint one). The discovery 402 carries an `identity_bootstrap` block naming `X-Verification-Session: create` whenever the request has no identity header, and a request carrying that header, no identity and no payment credential runs the gate, which answers with the same session-bearing 403. It is opt-in on purpose: scanners replay the Bazaar example body on a schedule, so minting on every identity-less 402 would create a session and (on goods stores) a pending order per probe. Gateless merchants neither advertise nor honor it. Parity with node-commerce.
87
+
86
88
`build_verification_required_body(reason, message=?, agent_instructions=?, extra=?)` — canonical body builder for the `identity_verification_required` denial. Spreads `verify_url` / `session_id` / `poll_secret` / `poll_url` / `agent_instructions` from the gate-minted reason into a 4xx envelope with merchant-specific message + optional extras. Saves the per-merchant mapping boilerplate.
87
89
88
90
`get_signer_verdict(request)` (per-adapter) returns the cached `signer_match` + `signer_sanctions` verdicts the gate composed on its primary `/v1/assess` call (single round trip; merchants build a 403 with `build_signer_mismatch_body(result=verdict.signer_match)` when `kind != "pass"`).
0 commit comments