Repository navigation
In plain terms
Until now, a person had to be at the checkout to approve each purchase. With 0.6.0, a person can sign a spending permission once on their phone, for example "up to $50 at this store". An AI agent then spends it with a key of its own, and the store checks each purchase against the limits in that permission.
This is the version the README has been describing. If you followed the README and got Package subpath './agent' is not defined by "exports", upgrading fixes it.
It also fixes a security bug present in 0.2.0 through 0.5.0. A crafted checkout link could run a script on the gate's consent pages. Upgrading is recommended even if you don't use the new features. See "Security fixes" below.
Demo trust only. This is not a real safety control.
- Nothing checks yet who issued the wallet's credential, so a credential someone made themselves would pass. The built-in pages also accept a typed-in claim with no wallet at all, and that can't be turned off yet (#352).
- For agent purchases, that means an agent can create its own "permission" with any limits it likes, and the permission still verifies. The store enforces the limits, but nothing proves a person set them.
- The cryptography on the wire is real. Results carry a label that says how far to trust them:
presence-only-demoon the wallet rails, anddevice-signedorserver-issued-demoon spending permissions. These labels aren't consistent yet (#267).- Signing on a real phone needs a wallet that understands AP2's
delegaterequest, and no released wallet does yet (see "An AI agent can spend…" below).- Use this to build and to demo.
Security fixes
- A crafted checkout link could run a script on the consent pages (#332, fixing #321 and #330). It affected the passkey, credential and phone-wallet payment pages, and the storefront's checkout page. The
cartvalue in a link is now encoded, and every value the pages write into a script goes through one escaping helper. This bug is in every published version from 0.2.0 to 0.5.0 (storefront 0.2.1 too). - An agent could lower an order's total by adding a line with a negative quantity, paying $12.50 for a $22 coffee (#346, fixing #286). A line with a negative quantity is new since 0.5.0, so no published version has that. The same fix refuses a fractional quantity (half a coffee at half price) in
grant.spend, which 0.3.0 to 0.5.0 accept if your code passes the agent's quantity through unchecked.
Breaking changes
0.6.0 renames some fields, changes the phone-signing format, and removes one entry point that never worked. You're affected if you use spending grants, because phone signing (signing: "device") is the default in both 0.5.0 and 0.6.0. You're also affected if you call the intent-sign building blocks directly, or import @openmobilehub/credentagent-storefront/firestore. Stores that only use the checkout gates are not, apart from the doctor() boot check described under Upgrading.
| 0.5.0 | 0.6.0 |
|---|---|
grant.mandate.credentialDoctype, GrantMandateEvidence.credentialDoctype |
credentialType (an SD-JWT VC type, not an mdoc doctype) |
IntentVerifyResult.credentialDoctype, IntentTrustVerdict.docType |
credentialType |
IntentVerifyBackend receives { deviceResponseB64url, sessionTranscript } |
receives { sdjwt, audience, nonce }, and must return delegatePayload (the key-binding JWT's delegate_payload, verbatim). The type marks it optional, but without it every verify fails with "mandate mismatch". |
buildIntentSignRequest(…), verifyIntentPresentation(…) |
also require merchantUrl (the store's public URL, path included), delegate, mandateExp and allowedSkus. Without merchantUrl, buildIntentSignRequest throws a TypeError and verifyIntentPresentation refuses. |
SimulateOptions.overrideDocType |
overrideVct |
The phone signs with the mdoc payment credential org.multipaz.payment.sca.1 (payment.mpzpass) |
The phone signs with an SD-JWT payment credential (vct urn:emvco:dpc:card:1 or com.emvco.dpc). A wallet set up for 0.5.0 can't sign a grant until it holds one; tools/demo-pki/mint/mint-dpc-sdjwt.mjs --mpzpass mints a demo one. No released wallet supports AP2's delegate request yet, so test with devSimulateWalletSignature. |
@openmobilehub/credentagent-storefront/firestore (firestoreCatalog), and firebase-admin as an optional peer |
Removed (#314). It was never built, so importing it from npm always failed. Pass a CatalogSource over your own database instead; the storefront README shows one. |
Upgrading
npm i @openmobilehub/credentagent-gate@^0.6.0 @openmobilehub/credentagent-storefront@^0.6.0- Change your version range by hand. Below 1.0, npm treats the middle number as breaking, so
^0.5.0won't pick up 0.6.0 on its own. - If you call
doctor()and stop the server when it isn't ok (the boot check the README shows), your deployment won't start until you passnew CredentAgent({ mandateSigningKey }), a stable private P-256 key from your secret manager. This applies even if you never use AP2. Only mandates your own code mints withcredentagent.ap2are signed with that key, and those are what would stop verifying after a restart. Whether this should stay an error is open in #361.doctor()also warns aboutcheckoutSigningKey: without it each instance generates its own, so an agent purchase whose cart was quoted before a restart, or by another instance, is refused. Local development is unaffected.
What's new
An AI agent can spend a permission the person signed (gate)
- A new entry point for the agent,
@openmobilehub/credentagent-gate/agent.AgentKeyholds the agent's key, andDelegatedIntentturns the signed permission into a purchase proof. It's kept apart from the package root, so when the agent holds its own key (the host passesagentKeywhen it creates the grant), that key never enters the store's process. WithoutagentKey, the gate generates and holds the delegate key itself. - The store checks that proof with
verifyDelegatedPurchase, imported from the package root. A purchase over the limits, paying someone other than the store, or at a price the catalog doesn't charge is refused with a typed reason. - The phone signs the permission with the wallet's own key. The permission is an AP2 mandate in SD-JWT format; AP2 is the Agent Payments Protocol, a standard shape for "this agent may spend this much".
- Signing on a real phone needs a wallet that understands AP2's
delegaterequest, and no released wallet does yet. For Multipaz it exists only in a patched build; the upstream change (openwallet-foundation/multipaz#2011) was closed without merging, and a released Multipaz wallet rejects the request. To try the whole flow without a phone, usedevSimulateWalletSignature(examples/device-signed-grants.mjs). - The store's own mandates are signed with its key.
credentagent.ap2mints them with the store'smandateSigningKey, and anyone holding the public key checks one withverifyMandate. A phone-signed permission is checked when the person signs it, and again at each purchase byverifyDelegatedPurchasewhen the agent holds its key. The gate publishes the public key so others can check them. A store at the site root publishes it at/.well-known/did.json; a store under a path publishes it at<walletOrigin>/did.json, where did:web looks for it (#347). - A permission names its store, path included. A permission for
shop.example/coffeeis refused atshop.example/wine(#334). - A normal checkout still records the older-style payment mandate, which is not key-signed. The passkey rail's carries a dev integrity hash (
MOCK-DEV-SIGNER), and the phone-wallet payment's carries none. - AP2 mandates and spending grants count money in whole cents, with helpers such as
toMinorUnits,toMajorUnitsandformatAmount. The checkout itself still adds dollar amounts as floats, which can refuse a valid payment (#360, in Known issues).
An outside verifier can check who someone is, with nothing to pay (gate)
- The
verifierseam now works for identity-only checks (#160), for example releasing a record only to someone who proves they're 21 or older. The approve page no longer talks about a payment that won't happen, follows yourreturnUrl, doesn't show "$0.00" totals, and reports "Credential checked" with the verifier's trust level rather than calling it verified.examples/delegated-verifier-identity/shows it end to end.
Seeing what the wallet sent (gate + storefront)
inspectPresentations: trueadds an "Inspect this presentation" link to the consent page. It opens what the wallet sent in Multipaz Tools, only in the browser that just presented it. The answer is never stored: not on the order, not in any status read, and not in the webhook (#327). It's off by default, because an answer from a real ID can hold personal data.- Each completed order keeps a record of what was proven: which checks ran, on which rail, and the trust level. The order page, the checkout page and the shopping widget show it, with a link to the order's record.
Running the store (storefront)
GET /versionreports the package versions and the MCP versions the store serves.createStorefront({ build })labels a deployment, so two copies of the same version can be told apart.- A store behind a proxy can live under a path, for example
https://example.com/shop/. Set the gate'swalletOriginand the storefront'sbaseUrlto the full public URL, and every checkout link, redirect and consent page keeps the path. - Fixes:
- the cart empties when the order completes on another server instance;
- the product picker stays on one cart;
- MCP listen streams that only burn serverless time are refused;
- the order-complete screen says you can close the window;
- the product-picker widget's MCP Apps policy (
_meta.ui.csp) no longer lists the baredata:scheme, and the ChatGPT copy declares that policy too. ChatGPT's ownopenai/widgetCSPstill listsdata:, so product images keep rendering there (#385).
The card kit (gate): first slice only
@openmobilehub/credentagent-gate/cardsexists but is small. It holds only the permission card's data contract (GRANT_VIEW_KIND,GrantViewData,GrantViewProduct), moved out of the storefront. The rest of the kit, cards that any MCP server can show in Claude and ChatGPT, comes in a later release.
Known issues
All of these are open in 0.6.0.
Checks and policies
- The built-in pages accept a typed-in claim such as "over 21" with no wallet, and there's no switch to turn this off, even on a server that uses an outside verifier (#352). This one is also in 0.5.0.
- An age check in the policy is only enforced when the item has a minimum age, so an action with no
minimumAgetag can complete on an 18+ proof (#354). - Custom credential checks, such as a prescription, are skipped when a spending grant buys (#139). This one is also in 0.5.0.
- The delegated page decides whether there's a payment from every policy the server has seen, not this order's (#353).
- On the outside verifier's identity-only page no amount is shown, but if the store also configured its own payment step (
settle), completing the request still charges the order (#409). - The outside verifier's page asks for whichever age the server resolved last, not this order's age, so someone joining an 18+ forum can be asked to prove 21+ and get stuck (#359).
Agent purchases and spending permissions
- The AI can switch a spending grant to click-to-approve and approve it itself (#280).
- Two purchases at the same moment can spend past the budget the person signed (#261), and the same is true in the demo stores (#287).
- A store accepts an agent's purchase that names no way to pay (#262).
- Two deployments that share one
walletOriginand one checkout signing key accept each other's price quotes; give each store its owncheckoutSigningKey(#335). - A store's price quote never expires, so an agent can keep an old quote and spend it later; a changed price is still refused (#249).
- A phone-signed permission lasts a year, and revoking the grant doesn't end it (#239).
- The wallet asks people to approve an agent's spending permission without showing it (#192).
- An agent-held grant drops the loyalty discount the person proved, so the two paths disagree on price (#281).
- A signed $50 budget reads as $5,000 to AP2's reference code, because of cents versus dollars (#282).
- Spending a grant can report a purchase that didn't happen, or the wrong reason for a refusal (#283).
- A "coffee or tea" permission can only buy one of each per purchase (#265).
verifyDelegatedPurchaseaccepts a missing nonce or audience from a JavaScript caller (#324).- A huge quantity keeps the permission-limits check busy before the price check rejects it (#325).
Orders and privacy
- An order whose total isn't exact in binary, such as 3 × $4.90, is refused at payment: the checkout compares float dollar totals, and
14.700000000000001isn't14.7(#360). This one is also in 0.5.0. - The order's proof receipt still repeats some values the wallet disclosed (a membership number, a card id and expiry, the cardholder's name), and anyone with the order id can read it (#350).
- Double-clicking "Complete purchase (demo)" can fulfil an order twice when orders live in a shared store such as Redis (#358).
Wallet pages and the gate
- The phone-wallet payment page shows check results without escaping them (#196).
- The gate decides its own web address from a header the client can set (#229).
- With no request parser, the gate reads a wallet answer of any size into memory (#299).
- The presentation inspector can show a different wallet answer from the one the gate decided on (#301).
- The gate reads one wallet answer from several documents at once, and shows raw errors for malformed ones (#305).
- A checkout link's
cartvalue isn't refused when it carries extra characters after a valid signed cart; nothing is known to exploit this (#337).
Docs
- The docs still say AP2 mandates are "dev-signed", one permission shows two trust labels, and the grant card says "dev-sealed" even for a phone-signed grant (#267).
Full list of changes: v0.5.0...v0.6.0