Security Model: Authentication, Idempotency, and Price Integrity
Closed by Default
The ACP checkout endpoints reject every request, with a 401, until an ACP_BEARER_TOKEN is configured. There is no permissive development mode that accidentally ships, no default credential to forget to change: an unconfigured deployment is a closed one, and opening the API is a deliberate act of setting a secret. Token comparison is constant-time to deny timing probes, and the same conservatism runs through the neighbors: the Stripe webhook handler refuses to process events when no signing secret is configured (an unverifiable payment confirmation is treated as no confirmation), and POST bodies must be JSON with the required ACP headers present, anything else is rejected before business logic runs. The deliberately public surfaces are exactly two: the MCP tools, which can only do what a shopper could do, browse and build carts whose prices the server controls, and the product feed, which is marketing data by definition.
Signatures That Fail Closed
Three HMAC seams cover the trust boundaries, and each one fails closed. Inbound ACP requests can be signed (base64 SHA-256 over the exact raw body, with optional timestamp skew checking within a five minute window): once a signing secret is configured, a missing or wrong signature is a 401, a caller cannot skip verification by omitting the header, and the requireSignature config flag extends the protection to the misconfiguration case where the secret itself went missing. Inbound Stripe webhooks are verified with Stripe's signature scheme against the raw bytes. Outbound order payloads are signed with your ORDER_WEBHOOK_SECRET so your fulfillment endpoint can verify provenance, the verification recipe is in the order webhook guide. All comparisons are constant-time, and all of them hash raw bodies rather than parsed objects, which is why the server and Lambda handler are both careful to preserve bytes exactly.
Idempotency: Retries Can Never Double-Charge
Networks fail mid-request, and payment APIs must assume every request will eventually be sent twice. The module enforces the ACP idempotency contract on every POST: a required key, scoped to the caller identity and endpoint so keys can never collide across merchants or routes, with a body hash distinguishing legitimate replays from conflicting reuse. Replays return the stored response with a replay header, conflicts are rejected with a 422, concurrent duplicates serialize behind a 409 with a retry hint, and server errors release the key so a transient failure does not poison the retry. On top of that transport layer, the state machine adds its own guarantees: completing an already-completed session is an idempotent no-op, a canceled session can never be resurrected by a late webhook, and order emission is tracked per session so fulfillment receives one order no matter how many confirmations arrive. The mechanics are specified in the endpoints reference.
Price Integrity: The Catalog Is the Only Authority
The invariant that makes the rest of the module trustworthy: no amount that influences a charge is ever read from client input or trusted from storage. Cart lines store prices for display, but every read and mutation re-resolves each line against the live catalog, restoring tampered values and repricing stale ones. Checkout sessions re-derive every line on create, update, and retrieve, drop items that vanished or went unavailable (with buyer-visible warnings), and rebuild totals from scratch. The charge amount is always the recomputed total, never a figure from the request. Two refinements close the remaining gaps: frozen states, a session with a live hosted payment, or one completed or canceled, are never re-priced, so the amount a shopper saw on a payment page cannot shift underneath them; and money math is integer-only end to end, with non-integer prices rejected at catalog load, overflow guards on line totals, and a quantity ceiling that keeps every sum far inside safe integer range. Floating point money bugs are not mitigated here, they are structurally absent.
A Data Footprint Designed to Be Boring
The most effective breach response is having nothing to breach. The module never sees card numbers, direct payments happen on Stripe's hosted page, and delegated payments arrive as single-use scoped tokens that are spent on arrival. It keeps no order history: completed orders are delivered to your systems and the session that produced them expires on its TTL. Buyer details exist only transiently in those expiring sessions. Secrets live exclusively in environment variables, with no code path that reads a key from a committed file. Error responses are sanitized, internal failures log full detail server-side but cross the wire as a generic internal_error, so stack traces, database errors, and internal identifiers never leak to callers. What an attacker could obtain from a fully compromised state store is a list of expiring carts: titles, quantities, and prices of things almost bought.
What Remains Your Responsibility
The module draws its boundary at the commerce layer, and honest documentation says what sits outside it. Transport security: run it behind HTTPS, terminated by your load balancer, API Gateway, or reverse proxy. Rate limiting and DDoS posture: commerce endpoints should sit behind the same edge protections as the rest of your product. Secret management: the environment variables are only as safe as the platform injecting them, use a secrets manager and rotate on personnel changes, our data protection guide covers the discipline. Your fulfillment endpoint: verify the order signature, deduplicate on the idempotency key, and treat it as the production system it is. And the human layer, accurate policies in your links config and truthful product claims, which agent platforms increasingly audit, belongs to you, with our business legal pillar as a starting point. Fraud economics also deserve a word: digital goods sold through delegated tokens shift much of the classic card-testing surface to the platform side, but refund abuse and chargebacks remain ordinary merchant realities, covered in fraud prevention.
Reviewing It Yourself
Security claims about commerce code should be checkable, and this module's are. The codebase is around four thousand lines of TypeScript, small enough for one engineer to read in an afternoon, with the security-relevant logic concentrated in a handful of files: the ACP router (auth, idempotency, error sanitization), the auth module (token and signature checks), the checkout service (state rules and recomputation), the money helpers (integer guards), and the webhook handlers. The test suite exercises the contracts that matter, replay and conflict behavior, frozen-state rules, decline paths, tamper recovery, and runs with npm test on a clean checkout. Source, tests, and license are on GitHub, and security reports are welcome through the repository. For the broader checklist a store should run before launch, see our ecommerce security guide.
Threats, Mapped to Mechanisms
A quick translation of the pillars into attacker terms. Price manipulation, editing a cart in storage, replaying an old session, or sending a doctored amount, is defeated by catalog re-derivation and frozen states. Double-spend via retries, intentional or accidental, is defeated by scoped idempotency keys plus idempotent completion. Forged payment confirmations, a fake webhook claiming a session was paid, die on signature verification and on the rule that unverifiable events are not processed. Spoofed orders to your fulfillment endpoint fail the outbound HMAC check. Credential stuffing against the ACP API hits a single constant-time bearer comparison with no user-enumeration surface, there are no accounts to enumerate. And data exfiltration from the state store yields expiring carts rather than cardholder data, because the valuable records were never stored. None of this makes the deployment around the module automatically safe, but it does mean the commerce layer itself contributes no novel attack surface beyond the ordinary web service it runs inside.
