feat(checkout): seamless merchant helpers + Tier 1+2+3 SDK lifts - #49
Merged
Conversation
Cross-language port of python-commerce #46. **Seamless helpers:** - Checkout framework adapters: `handleHono` / `handleExpress` / `handleFastify` / `handleNextjs` / `handleWeb` (parity with python's per-framework adapters). - `validationEnvelope` + per-framework `validationResponse*` wrappers for the canonical 4xx body. - Lazy server helpers: `lazyX402Server` / `lazyMppxServer` return memoized async getters; `Checkout` auto-derives both from flat `cdpApiKeyId` / `cdpApiKeySecret` / `mppxSecretKey` kwargs. - `makeMppxComposeHook` factory: the canonical `composeMppx` hook for mppx, including DID-based signer extraction. - `preValidate` hook + `ctx.state` pattern so a module-level Checkout supports per-request validation without surrendering encapsulation. - `CheckoutGateConfig` wires the AgentScore gate into Checkout; per-request policy is resolved via `perRequestPolicy(ctx)`. - `SettleOutcome` normalized (`railKey`, `signerAddress`, `signerNetwork`, `txHash`). `CheckoutContext.identityStatus` property. - `zeroSettleCarveOut` handles the EIP-3009 `value=0` and mppx `proof` carve-outs. - `extractSignerForPrecheck` reads x402 OR MPP credentials. - `Checkout.acceptedRails` + `acceptedMethodNames` accessors. - Signed discovery surfaces: `buildSignedUcpResponse`, `buildSignedJwksResponse`, `wellKnownCorsPreflightHeaders`, `wellKnownPreflightResponse`, `bootstrapUcpSigningKey`, `defaultA2aServices`. UCP section 6 cache, CORS, and `X-Request-ID` handling are folded in. - `agentscore_content`: `PURCHASE_MODE_NOTES`, `buildAgentscoreOnboardingSteps`, `standardEndpointDescriptions`, `buildSuccessNextSteps`, `buildMerchantIndexJson`, `buildRedemptionSkillMd`. - `formatUsdCents` helper. **Tier 1+2+3 lifts + integration fixes:** - Gate-hook extensions on `Checkout` (`perRequestPolicy`, `onDenied`, `runGate`) and `ctx.captureWallet` closure for clean settle-time wallet capture without framework-bound helpers (#94: fixes martin captureWallet integration). - `Checkout.discoveryExtensions` plumbed through `build402Body` so per-route bazaar (and other x402 body extensions) merge into the challenge body. - `xServiceInfoExtension` + `xPaymentInfoFromCheckout` openapi helpers. - `xPaymentInfoExtension` emits `authMode: "payment"` + `description`. - `loadSolanaFeePayer` accepting base58 (64-byte or 32-byte seed) and 128-char hex; merchants drop the per-merchant Solana keypair parsing boilerplate. - Auto-CDP-toggle in `createX402Server`: when `CDP_API_KEY_ID` + `CDP_API_KEY_SECRET` env vars are set, defaults to `facilitator="coinbase"`. - `body.extensions` passthrough on x402 PaymentRequired body. - `processX402Settle` calls `verifyPayment` (#95: was the non-existent `processPaymentRequest`; both verify + settle phases now exercise the right x402/core v2 method). Tests: 1055 passed, 4 skipped. Includes coverage for `loadSolanaFeePayer`, `buildSignedUcpResponse` misconfigured branch + X-Request-ID echo, `bootstrapUcpSigningKey` malformed-env throw, the new openapi/agentscore_content helpers, and `wellKnownPreflightResponse`. Smoke-tested live against the migrated martin-estate and AgentScore Store: real orders end-to-end on tempo + x402-base + Solana MPP. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… coverage Addresses inline review on PR #49: - `specRailKey` and `specMethodName` return types narrowed from `RailKey | null` / `string | null` to non-nullable. Every code path in both helpers returns a value; the `| null` was dead and triggered `Comparison between inconvertible types` static analysis on `key === null` / `name === null` checks in `Checkout.acceptedRails` + `Checkout.acceptedMethodNames` (which were therefore unreachable). - Adds tests covering `buildSignedUcpResponse` happy path (single + multi-rail + empty-string sentinel recipient), `buildSignedJwksResponse`, `bootstrapUcpSigningKey` with a real `UCP_SIGNING_KEY_JWK_PRIVATE` env, plus extras for `loadSolanaFeePayer` + `wellKnownPreflightResponse` Headers-instance input. Tests: 1063 passed, 4 skipped. Coverage: 89.62/81.61/94.84/90.71 (up from 88.13/80.54/93.63/89.34). Threshold (95/90/95/95) still failing on `checkout.ts` gate-hook paths; follow-up. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ssor/zero-settle coverage Two behaviors users hit during gate-config testing: - `gate.runGate` returning `undefined` (the common case for a no-op pass-through) previously left `denial.status` undefined and the handler crashed with `Cannot read properties of undefined (reading 'status')`. Now normalize: `undefined` or `null` → allow; any other shape that isn't an object with a numeric `status` throws TypeError (parity with python's `_coerce_run_gate_result`). New tests cover: - `Checkout.acceptedRails` dedupes tempo + tempo_session → single `tempo_mpp`. - `Checkout.acceptedMethodNames` emits each protocol slug. - `gate.runGate` allow / deny / invalid-shape branches. - `gate.perRequestPolicy` returning null skips the gate. - `zeroSettleCarveOut=true` at $0 MPP path: skips compose, lifts signer. Coverage: 89.62/81.61/94.84/90.71 → 91.21/83.49/95.15/92.00 (functions threshold now met; statements/branches/lines pending). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ettle coverage Covers Checkout settle paths that were previously only exercised by processX402Settle unit tests: - handleX402 verify_failed branch (verifyPayment returns success:false) - handleX402 facilitator error → 503 payment_provider_unavailable - handleMppx 200 path: composeMppx returns success, signer + txHash propagate to onSettled Coverage: 91.21/83.49/95.15/92.00 → 91.93/84.42/95.45/92.78. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- preValidate populating ctx.state, throwing CheckoutValidationError, and rethrowing non-validation errors. - handleHono / handleNextjs / handleWeb / handleExpress / handleFastify emitting 200 with the on_settled body on the settle leg (only the 402 discovery leg was previously tested). Coverage: 91.93/84.42/95.45/92.78 → 92.74/84.89/96.06/93.56. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- SDK gate allow → ctx.captureWallet wired with operator-token; closure call
reaches AgentScoreCore.captureWallet through the gate-installed closure.
- SDK gate deny → 403/401/503 with denialReasonToBody envelope; reason code
surfaces in error.code on the body.
- SDK gate onDenied callback can reshape the canonical body and return a
custom { status, body, headers } envelope.
Coverage: 92.74/84.89/96.06/93.56 → 93.88/86.71/96.96/94.82.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…requires_wallet_signing) The post-allow signer-match enforcement branch in the SDK gate converts non-pass signer_match verdicts into wallet_signer_mismatch (for cross-operator) and wallet_auth_requires_wallet_signing (for non-signing wallet auth) denials. Coverage: 93.88/86.71/96.96/94.82 → 94.18/87.26/96.96/95.17 (lines green, statements + branches still pending). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…2 server - Checkout auto-derives composeMppx from mppxSecretKey (lazy.lazyMppxServer + makeMppxComposeHook wiring). - 402 emit with x402Server configured: buildPaymentRequirements happy path + the catch-fallback when scheme registration fails. - getIdentityStatus: anonymous/verified/unverified branches. Coverage: 94.18/87.26/96.96/95.17 → 95.11/88.19/97.57/96.14. Statements, lines, and functions thresholds now met. Branches still at 88.19 vs 90. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…arding-steps variations - makeMppxComposeHook with did:pkh:solana credential → signerNetwork=solana - non-did credential.source → signer null (compose still returns 200) - receipt.reference unset → falls back to receipt.transaction for txHash - buildAgentscoreOnboardingSteps api+stripe / goods+stripe / api-no-stripe branches - buildSuccessNextSteps omits order_status_url + fulfillment_eta when unset - buildSignedJwksResponse with Headers instance request (vs plain object) Coverage: 95.11/88.19/97.57/96.14 → 95.33/88.74/97.57/96.34. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…recipient rail Brings all four coverage thresholds green (95/90/95/95): - zero-settle x402-base carve-out: $0 amount with valid x402 header lifts signer from EIP-3009 from-field; invalid base64 still completes the carve-out. - handleX402 fallback: when verified.payload lacks .authorization.from, signer falls back to settleResult.payer (no lowercase conversion). - discoveryExtensions on 402 body: bazaar extension propagates verbatim; empty discoveryExtensions object skips the extensions field entirely. - Express + Fastify adapters with array-valued headers (multi-value normalization) and 400 invalid_body branch. - mintRecipients drops rails with empty-string recipients (per-order-mint pattern). - CheckoutValidationError defaults (status=400, action=fix_request) when unset. Coverage: 95.33/88.74/97.57/96.34 → 96.13/90.01/97.57/97.07 (all four thresholds passing). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (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.
Summary
Cross-language port of python-commerce #46 (seamless merchant helpers).
Collapses ~190 lines of per-merchant boilerplate into spec-rooted SDK helpers. After this PR a goods-merchant
/purchaseroute can be a one-liner that callsCheckout.handleHono(c), and a UCP-publishing merchant's/.well-known/{ucp,jwks.json}handlers are ~10 lines instead of ~150.Seamless helpers:
handleHono/handleExpress/handleFastify/handleNextjs/handleWeb.validationEnvelope+ per-frameworkvalidationResponse*wrappers for the canonical 4xx body.lazyX402Server/lazyMppxServerreturn memoized async getters;Checkoutauto-derives both from flatcdpApiKeyId/cdpApiKeySecret/mppxSecretKeykwargs.makeMppxComposeHookfactory: the canonicalcomposeMppxhook for mppx.preValidatehook +ctx.statepattern so a module-level Checkout supports per-request validation without surrendering encapsulation.CheckoutGateConfigwires the AgentScore gate into Checkout; per-request policy viaperRequestPolicy(ctx).SettleOutcomenormalized (railKey,signerAddress,signerNetwork,txHash).CheckoutContext.identityStatus.zeroSettleCarveOuthandles the EIP-3009value=0and mppxproofcarve-outs.extractSignerForPrecheckreads x402 OR MPP credentials.Checkout.acceptedRails+acceptedMethodNamesaccessors.buildSignedUcpResponse,buildSignedJwksResponse,wellKnownCorsPreflightHeaders,wellKnownPreflightResponse,bootstrapUcpSigningKey,defaultA2aServices. UCP section 6 cache, CORS, andX-Request-IDhandling folded in.agentscore_content:PURCHASE_MODE_NOTES,buildAgentscoreOnboardingSteps,standardEndpointDescriptions,buildSuccessNextSteps,buildMerchantIndexJson,buildRedemptionSkillMd.formatUsdCentshelper.Tier 1+2+3 lifts + integration fixes:
Checkout(perRequestPolicy,onDenied,runGate) andctx.captureWalletclosure for clean settle-time wallet capture without framework-bound helpers.Checkout.discoveryExtensionsplumbed throughbuild402Bodyso per-route bazaar merges into the challenge body.xServiceInfoExtension+xPaymentInfoFromCheckoutopenapi helpers.xPaymentInfoExtensionemitsauthMode: "payment"+description.loadSolanaFeePayeraccepting base58 (64-byte or 32-byte seed) and 128-char hex.createX402Server: whenCDP_API_KEY_ID+CDP_API_KEY_SECRETenv vars are set, defaults tofacilitator="coinbase".body.extensionspassthrough on x402 PaymentRequired body.processX402SettlecallsverifyPayment(was a non-existent method; both verify + settle phases now exercise the right x402/core v2 method).Verification
bun run lint,bun run typecheckclean.bun run test1055 passed, 4 skipped.buildSignedUcpResponsehappy path (~50 uncovered lines inwell_known.ts); follow-up.Test plan
martin-estate+core/store) already migrated and smoke-clean🤖 Generated with Claude Code