@artos-commerce/ucp-client 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +160 -0
  3. package/dist/ap2/canonical-json.d.ts +13 -0
  4. package/dist/ap2/canonical-json.js +28 -0
  5. package/dist/ap2/canonical-json.js.map +1 -0
  6. package/dist/ap2/index.d.ts +3 -0
  7. package/dist/ap2/index.js +4 -0
  8. package/dist/ap2/index.js.map +1 -0
  9. package/dist/ap2/signer.d.ts +67 -0
  10. package/dist/ap2/signer.js +79 -0
  11. package/dist/ap2/signer.js.map +1 -0
  12. package/dist/ap2/verify.d.ts +15 -0
  13. package/dist/ap2/verify.js +60 -0
  14. package/dist/ap2/verify.js.map +1 -0
  15. package/dist/checkout/handlers.d.ts +76 -0
  16. package/dist/checkout/handlers.js +288 -0
  17. package/dist/checkout/handlers.js.map +1 -0
  18. package/dist/checkout/index.d.ts +4 -0
  19. package/dist/checkout/index.js +4 -0
  20. package/dist/checkout/index.js.map +1 -0
  21. package/dist/checkout/money.d.ts +24 -0
  22. package/dist/checkout/money.js +41 -0
  23. package/dist/checkout/money.js.map +1 -0
  24. package/dist/checkout/rails.d.ts +36 -0
  25. package/dist/checkout/rails.js +62 -0
  26. package/dist/checkout/rails.js.map +1 -0
  27. package/dist/checkout/types.d.ts +56 -0
  28. package/dist/checkout/types.js +2 -0
  29. package/dist/checkout/types.js.map +1 -0
  30. package/dist/config.d.ts +54 -0
  31. package/dist/config.js +41 -0
  32. package/dist/config.js.map +1 -0
  33. package/dist/crypto/deps.d.ts +27 -0
  34. package/dist/crypto/deps.js +23 -0
  35. package/dist/crypto/deps.js.map +1 -0
  36. package/dist/index.d.ts +6 -0
  37. package/dist/index.js +12 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/sui/signer.d.ts +27 -0
  40. package/dist/sui/signer.js +69 -0
  41. package/dist/sui/signer.js.map +1 -0
  42. package/dist/ucp/client.d.ts +169 -0
  43. package/dist/ucp/client.js +269 -0
  44. package/dist/ucp/client.js.map +1 -0
  45. package/package.json +73 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Artos
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,160 @@
1
+ # @artos-commerce/ucp-client
2
+
3
+ **Path B Direct UCP client for Artos.** A typed UCP transport, AP2 mandate
4
+ signing, and checkout orchestration so you can build your own commerce-grade UCP
5
+ client against `api.artos.sh` — without hand-rolling the JSON-RPC envelope, the
6
+ canonical-JSON signing bytes, the AP2 mandates, or the payment-rail rules.
7
+
8
+ This is the reusable core extracted from the hosted Artos MCP bridge. If you just
9
+ want an MCP endpoint for AI agents, point them at the hosted bridge
10
+ (`https://agent.artos.sh/mcp`, **Path A**) and skip this package. Reach for
11
+ `@artos-commerce/ucp-client` when you are building your **own** client/integration
12
+ (**Path B**) and want the hard parts — signing, verification, and rail selection
13
+ — done correctly for you.
14
+
15
+ ## What it does for you
16
+
17
+ - **Transport** (`UcpClient`): global catalog search (REST) and per-store /
18
+ buyer-account shopping over the UCP MCP JSON-RPC transport, with the UCP-Agent
19
+ preamble, idempotency keys, the two-credential auth model, and the 401
20
+ silent-refresh relay all handled.
21
+ - **AP2 signing** (`Ap2Signer`): mints the compact ES256 `checkout_mandate` /
22
+ `payment_mandate` the API requires, over byte-parity canonical JSON.
23
+ - **Verification** (`verifyMerchantAuthorization`): re-checks the store-signed
24
+ terms before you authorize a spend.
25
+ - **Checkout orchestration** (`createCheckoutHandlers`): a single
26
+ `confirmPurchase` re-prices, verifies, mints the mandate, routes to the
27
+ `$0` / card / crypto rail, and returns a structured `CheckoutOutcome`.
28
+
29
+ ## Install
30
+
31
+ ```bash
32
+ npm install @artos-commerce/ucp-client
33
+ # Only if you need the crypto settlement rail:
34
+ npm install @mysten/sui
35
+ ```
36
+
37
+ Requires Node 20+ (for global `fetch` and `base64url`). `@mysten/sui` is an
38
+ **optional** peer dependency — card-only integrations don't need it.
39
+
40
+ ## Quickstart
41
+
42
+ ```ts
43
+ import {
44
+ UcpClient,
45
+ Ap2Signer,
46
+ createCheckoutHandlers,
47
+ resolveCryptoDeps,
48
+ } from '@artos-commerce/ucp-client';
49
+
50
+ const client = new UcpClient(
51
+ {
52
+ artosBaseUrl: 'https://api.artos.sh',
53
+ platformApiKey: process.env.UCP_PLATFORM_API_KEY!, // "<clientId>.<secret>"
54
+ platformProfileUrl: 'https://you.example/.well-known/ucp',
55
+ },
56
+ {
57
+ // The current buyer's OAuth bearer (server-side only — never ship it to a
58
+ // browser). Omit for anonymous/card-only sessions.
59
+ buyerToken: session.buyerBearer,
60
+ // Relay a 401 challenge so the agent can silently refresh its token.
61
+ onAuthChallenge: (wwwAuthenticate) => respondWith401(wwwAuthenticate),
62
+ },
63
+ );
64
+
65
+ const signer = new Ap2Signer(
66
+ JSON.parse(process.env.AGENT_PRIVATE_JWK!), // EC P-256 private JWK
67
+ process.env.AGENT_KID!, // must match your published profile's public JWK kid
68
+ );
69
+
70
+ // Optional crypto rail (needs @mysten/sui):
71
+ const crypto = resolveCryptoDeps({
72
+ agentSuiPrivateKey: process.env.AGENT_SUI_PRIVATE_KEY, // suiprivkey1...
73
+ suiNetwork: 'mainnet',
74
+ agentMaxSpendAmount: 50_000, // optional client-side cap (minor units)
75
+ });
76
+
77
+ const shop = createCheckoutHandlers({ client, signer, crypto });
78
+ ```
79
+
80
+ ### Search → cart → checkout → buy
81
+
82
+ ```ts
83
+ // 1. Discover (price filters are MAJOR units; converted to minor for you).
84
+ const results = await shop.searchProducts({
85
+ query: 'running shoes',
86
+ filters: { price: { max: 150, currency: 'USD' } },
87
+ });
88
+
89
+ // 2. Build a checkout (per store, by slug from the search result metadata).
90
+ const checkout = await shop.createCheckout({
91
+ storeSlug: 'energy-sport',
92
+ items: [{ id: 'prod_123', quantity: 1 }],
93
+ shippingAddress: { country: 'US', postal_code: '94016' },
94
+ });
95
+
96
+ // 3. Confirm: re-prices, verifies the store signature, mints the AP2 mandate,
97
+ // routes the rail, and returns a structured outcome.
98
+ const outcome = await shop.confirmPurchase({
99
+ storeSlug: 'energy-sport',
100
+ checkoutId: checkout.id as string,
101
+ paymentMethod: 'artos.card', // omit on a multi-rail store to be asked
102
+ });
103
+
104
+ switch (outcome.status) {
105
+ case 'completed':
106
+ return renderOrder(outcome.checkout);
107
+ case 'escalation_required':
108
+ return redirect(outcome.continueUrl); // 3-DS / hosted step
109
+ case 'payment_selection_required':
110
+ return askBuyerToPickRail(outcome.rails);
111
+ case 'error':
112
+ return showError(outcome.code, outcome.message);
113
+ }
114
+ ```
115
+
116
+ `confirmPurchase` never throws for a business outcome — it always resolves to a
117
+ `CheckoutOutcome`. Transport failures (network / non-2xx / JSON-RPC error) still
118
+ throw `UcpClientError`; an expired buyer bearer throws `UcpAuthError` carrying
119
+ the `WWW-Authenticate` challenge.
120
+
121
+ ## The two-credential auth model
122
+
123
+ The Artos API's identity guard prefers `X-API-Key` over a bearer. `UcpClient`
124
+ encodes this for you:
125
+
126
+ - **Platform `X-API-Key`** (default) authenticates at any store: catalog, cart,
127
+ checkout create/update, `get_order`, card completion.
128
+ - **Buyer OAuth bearer only** is used on `auth: 'buyer'` calls — crypto
129
+ `prepare`/`complete` and all `/account/*` tools. The API key is omitted so the
130
+ API resolves the buyer account and enforces their stored authorization + caps.
131
+
132
+ Never send both for a buyer-bound call.
133
+
134
+ ## API surface
135
+
136
+ - `UcpClient` — `globalSearch`, `callGlobalTool`, `callStoreTool`,
137
+ `listAccountTools`, `callAccountTool`, `fetchStoreProfile`, `fetchImage`,
138
+ `hasBuyerToken`, `ucpAgentHeader`.
139
+ - `Ap2Signer` — `mintCheckoutMandate`, `mintPaymentMandate`.
140
+ - `canonicalJson`, `verifyDetachedJws`, `verifyMerchantAuthorization`.
141
+ - `createCheckoutHandlers` — read tools + `confirmPurchase`.
142
+ - `SuiSigner`, `resolveCryptoDeps` (crypto rail; need `@mysten/sui`).
143
+ - Helpers: `toMinorUnits`, `normalizeSearchFilters`, `resolveRail`,
144
+ `grandTotal`, `currencyOf`, `asAllowanceId`, `isUcpError`, `messageOf`.
145
+
146
+ Subpath entries are also published: `@artos-commerce/ucp-client/ucp`,
147
+ `/ap2`, `/checkout`, `/sui`, `/crypto`.
148
+
149
+ ## Security notes
150
+
151
+ - **Money is integer minor units** everywhere; only the search-filter / display
152
+ edge speaks major units.
153
+ - **Canonical JSON is byte-parity** with `artos-api` so a store's
154
+ `merchant_authorization` (and your minted mandates) verify. Don't fork it.
155
+ - **Keep the buyer bearer server-side.** This package mints/forwards it from a
156
+ server; never expose it to a browser.
157
+
158
+ ## License
159
+
160
+ MIT
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Sorted-key canonical JSON serialization, byte-for-byte identical to the
3
+ * artos-api implementation (`src/modules/ucp/crypto/canonical-json.ts`) and the
4
+ * copies in artos-mcp-bridge / artos-my / artos-storefront. Used to recompute
5
+ * the bytes a store's `merchant_authorization` (and an AP2 mandate) is signed
6
+ * over, so a signature verifies across every repo.
7
+ *
8
+ * Caveat: this is NOT full RFC 8785 (JCS) — it does not apply JCS number
9
+ * canonicalization. Safe here because every monetary value is an integer
10
+ * (ISO-4217 minor units); we never emit floats. If that ever changes, this and
11
+ * every parity copy must change in lockstep.
12
+ */
13
+ export declare function canonicalJson(value: unknown): string;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Sorted-key canonical JSON serialization, byte-for-byte identical to the
3
+ * artos-api implementation (`src/modules/ucp/crypto/canonical-json.ts`) and the
4
+ * copies in artos-mcp-bridge / artos-my / artos-storefront. Used to recompute
5
+ * the bytes a store's `merchant_authorization` (and an AP2 mandate) is signed
6
+ * over, so a signature verifies across every repo.
7
+ *
8
+ * Caveat: this is NOT full RFC 8785 (JCS) — it does not apply JCS number
9
+ * canonicalization. Safe here because every monetary value is an integer
10
+ * (ISO-4217 minor units); we never emit floats. If that ever changes, this and
11
+ * every parity copy must change in lockstep.
12
+ */
13
+ export function canonicalJson(value) {
14
+ if (value === null || typeof value !== 'object') {
15
+ return JSON.stringify(value);
16
+ }
17
+ if (Array.isArray(value)) {
18
+ return `[${value.map(canonicalJson).join(',')}]`;
19
+ }
20
+ const obj = value;
21
+ const keys = Object.keys(obj)
22
+ .filter((k) => obj[k] !== undefined)
23
+ .sort();
24
+ return `{${keys
25
+ .map((k) => `${JSON.stringify(k)}:${canonicalJson(obj[k])}`)
26
+ .join(',')}}`;
27
+ }
28
+ //# sourceMappingURL=canonical-json.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"canonical-json.js","sourceRoot":"","sources":["../../src/ap2/canonical-json.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,aAAa,CAAC,KAAc;IAC1C,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAChD,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;IAC/B,CAAC;IACD,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,IAAI,KAAK,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;IACnD,CAAC;IACD,MAAM,GAAG,GAAG,KAAgC,CAAC;IAC7C,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC;SAC1B,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,SAAS,CAAC;SACnC,IAAI,EAAE,CAAC;IACV,OAAO,IAAI,IAAI;SACZ,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;SAC3D,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;AAClB,CAAC"}
@@ -0,0 +1,3 @@
1
+ export { canonicalJson } from './canonical-json.js';
2
+ export { verifyDetachedJws, verifyMerchantAuthorization } from './verify.js';
3
+ export { Ap2Signer, type CheckoutMandateClaims, type PaymentMandateClaims, } from './signer.js';
@@ -0,0 +1,4 @@
1
+ export { canonicalJson } from './canonical-json.js';
2
+ export { verifyDetachedJws, verifyMerchantAuthorization } from './verify.js';
3
+ export { Ap2Signer, } from './signer.js';
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/ap2/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,iBAAiB,EAAE,2BAA2B,EAAE,MAAM,aAAa,CAAC;AAC7E,OAAO,EACL,SAAS,GAGV,MAAM,aAAa,CAAC"}
@@ -0,0 +1,67 @@
1
+ import type { Jwk } from '../config.js';
2
+ /** Claims of an AP2 `checkout_mandate`: the full signed checkout body + binding. */
3
+ export interface CheckoutMandateClaims {
4
+ /** The full get_checkout body (carrying the store's merchant_authorization). */
5
+ checkout: Record<string, unknown>;
6
+ /** The merchant (public store slug) this mandate is bound to. */
7
+ merchant: string;
8
+ jti: string;
9
+ exp: number;
10
+ /** Optional server-side allowance (PaymentMandate) id to enforce. */
11
+ payment_mandate_id?: string;
12
+ }
13
+ /** Claims of an AP2 `payment_mandate`: the authorized charge. */
14
+ export interface PaymentMandateClaims {
15
+ payment: {
16
+ amount: number;
17
+ currency: string;
18
+ merchant: string;
19
+ };
20
+ jti: string;
21
+ exp: number;
22
+ payment_mandate_id?: string;
23
+ }
24
+ /** Common minting options shared by both mandate types. */
25
+ interface MintOptions {
26
+ exp?: number;
27
+ jti?: string;
28
+ paymentMandateId?: string;
29
+ }
30
+ /**
31
+ * An AP2 mandate signer bound to one EC P-256 key. Mints compact ES256 JWS
32
+ * credentials the Artos verifier checks against the agent profile's published
33
+ * signing keys (whose `kid` must match `kid` here):
34
+ *
35
+ * - {@link mintCheckoutMandate}: embeds the full signed checkout body (so the
36
+ * verifier can re-check the store's nested `merchant_authorization`) and
37
+ * binds the merchant slug, an expiry, and a unique `jti`.
38
+ * - {@link mintPaymentMandate}: authorizes the charge amount/currency/merchant.
39
+ */
40
+ export declare class Ap2Signer {
41
+ private readonly privateKey;
42
+ readonly kid: string;
43
+ constructor(privateJwk: Jwk, kid: string);
44
+ /**
45
+ * Mints a `checkout_mandate` embedding the full, freshly priced checkout body
46
+ * and bound to `merchant` (the store slug). `exp` defaults to 5 minutes out.
47
+ * The embedded body MUST be the verbatim get_checkout response so its nested
48
+ * `merchant_authorization` still verifies server-side.
49
+ */
50
+ mintCheckoutMandate(input: {
51
+ checkout: Record<string, unknown>;
52
+ merchant: string;
53
+ }, opts?: MintOptions): string;
54
+ /**
55
+ * Mints a `payment_mandate` authorizing a charge for `amount` (minor units),
56
+ * `currency`, and `merchant` (the store slug). The amount MUST equal the live
57
+ * priced total or the server rejects it as a scope mismatch.
58
+ */
59
+ mintPaymentMandate(input: {
60
+ amount: number;
61
+ currency: string;
62
+ merchant: string;
63
+ }, opts?: MintOptions): string;
64
+ /** Signs claims as a compact ES256 JWS (`header.payload.signature`). */
65
+ private sign;
66
+ }
67
+ export {};
@@ -0,0 +1,79 @@
1
+ import { createPrivateKey, randomUUID, sign as cryptoSign, } from 'node:crypto';
2
+ const DEFAULT_TTL_SECONDS = 300;
3
+ function b64url(buf) {
4
+ return buf.toString('base64url');
5
+ }
6
+ /**
7
+ * An AP2 mandate signer bound to one EC P-256 key. Mints compact ES256 JWS
8
+ * credentials the Artos verifier checks against the agent profile's published
9
+ * signing keys (whose `kid` must match `kid` here):
10
+ *
11
+ * - {@link mintCheckoutMandate}: embeds the full signed checkout body (so the
12
+ * verifier can re-check the store's nested `merchant_authorization`) and
13
+ * binds the merchant slug, an expiry, and a unique `jti`.
14
+ * - {@link mintPaymentMandate}: authorizes the charge amount/currency/merchant.
15
+ */
16
+ export class Ap2Signer {
17
+ privateKey;
18
+ kid;
19
+ constructor(privateJwk, kid) {
20
+ if (privateJwk.kty !== 'EC' || privateJwk.crv !== 'P-256') {
21
+ throw new Error('AP2 signer requires an EC P-256 key.');
22
+ }
23
+ this.privateKey = createPrivateKey({
24
+ key: privateJwk,
25
+ format: 'jwk',
26
+ });
27
+ this.kid = kid;
28
+ }
29
+ /**
30
+ * Mints a `checkout_mandate` embedding the full, freshly priced checkout body
31
+ * and bound to `merchant` (the store slug). `exp` defaults to 5 minutes out.
32
+ * The embedded body MUST be the verbatim get_checkout response so its nested
33
+ * `merchant_authorization` still verifies server-side.
34
+ */
35
+ mintCheckoutMandate(input, opts = {}) {
36
+ const claims = {
37
+ checkout: input.checkout,
38
+ merchant: input.merchant,
39
+ jti: opts.jti ?? `m_${randomUUID()}`,
40
+ exp: opts.exp ?? Math.floor(Date.now() / 1000) + DEFAULT_TTL_SECONDS,
41
+ ...(opts.paymentMandateId
42
+ ? { payment_mandate_id: opts.paymentMandateId }
43
+ : {}),
44
+ };
45
+ return this.sign(claims);
46
+ }
47
+ /**
48
+ * Mints a `payment_mandate` authorizing a charge for `amount` (minor units),
49
+ * `currency`, and `merchant` (the store slug). The amount MUST equal the live
50
+ * priced total or the server rejects it as a scope mismatch.
51
+ */
52
+ mintPaymentMandate(input, opts = {}) {
53
+ const claims = {
54
+ payment: {
55
+ amount: input.amount,
56
+ currency: input.currency,
57
+ merchant: input.merchant,
58
+ },
59
+ jti: opts.jti ?? `p_${randomUUID()}`,
60
+ exp: opts.exp ?? Math.floor(Date.now() / 1000) + DEFAULT_TTL_SECONDS,
61
+ ...(opts.paymentMandateId
62
+ ? { payment_mandate_id: opts.paymentMandateId }
63
+ : {}),
64
+ };
65
+ return this.sign(claims);
66
+ }
67
+ /** Signs claims as a compact ES256 JWS (`header.payload.signature`). */
68
+ sign(claims) {
69
+ const header = b64url(Buffer.from(JSON.stringify({ alg: 'ES256', kid: this.kid }), 'utf8'));
70
+ const payload = b64url(Buffer.from(JSON.stringify(claims), 'utf8'));
71
+ const signingInput = Buffer.from(`${header}.${payload}`, 'utf8');
72
+ const signature = cryptoSign('sha256', signingInput, {
73
+ key: this.privateKey,
74
+ dsaEncoding: 'ieee-p1363',
75
+ });
76
+ return `${header}.${payload}.${b64url(signature)}`;
77
+ }
78
+ }
79
+ //# sourceMappingURL=signer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"signer.js","sourceRoot":"","sources":["../../src/ap2/signer.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Verifies a detached-content JWS (`base64url(header)..base64url(signature)`),
3
+ * the format artos-api uses for `merchant_authorization`. The signature covers
4
+ * `base64url(header).base64url(payload)` where `payload` is the supplied
5
+ * canonical byte string. The signing key is selected from `keys` by the header
6
+ * `kid` (or the first key when the header carries none). ES256 only; returns
7
+ * false on any failure rather than throwing.
8
+ */
9
+ export declare function verifyDetachedJws(token: string, payload: string, keys: Record<string, unknown>[]): boolean;
10
+ /**
11
+ * Verifies a checkout body's embedded `merchant_authorization` against the
12
+ * store's published signing keys. Recomputes the canonical bytes over the body
13
+ * with its `ap2` field removed (the field is excluded from the signing input).
14
+ */
15
+ export declare function verifyMerchantAuthorization(body: Record<string, unknown>, keys: Record<string, unknown>[]): boolean;
@@ -0,0 +1,60 @@
1
+ import { createPublicKey, verify as ecdsaVerify } from 'node:crypto';
2
+ import { canonicalJson } from './canonical-json.js';
3
+ /**
4
+ * Verifies a detached-content JWS (`base64url(header)..base64url(signature)`),
5
+ * the format artos-api uses for `merchant_authorization`. The signature covers
6
+ * `base64url(header).base64url(payload)` where `payload` is the supplied
7
+ * canonical byte string. The signing key is selected from `keys` by the header
8
+ * `kid` (or the first key when the header carries none). ES256 only; returns
9
+ * false on any failure rather than throwing.
10
+ */
11
+ export function verifyDetachedJws(token, payload, keys) {
12
+ const parts = token.split('.');
13
+ // Detached serialization has an empty middle segment (the omitted payload).
14
+ if (parts.length !== 3 || !parts[0] || parts[1] !== '' || !parts[2]) {
15
+ return false;
16
+ }
17
+ let header;
18
+ try {
19
+ header = JSON.parse(Buffer.from(parts[0], 'base64url').toString('utf8'));
20
+ }
21
+ catch {
22
+ return false;
23
+ }
24
+ if (!header || header.alg !== 'ES256') {
25
+ return false;
26
+ }
27
+ const kid = typeof header.kid === 'string' ? header.kid : undefined;
28
+ const jwk = kid
29
+ ? keys.find((k) => k.kid === kid)
30
+ : keys[0];
31
+ if (!jwk) {
32
+ return false;
33
+ }
34
+ const encodedPayload = Buffer.from(payload, 'utf8').toString('base64url');
35
+ const signingInput = Buffer.from(`${parts[0]}.${encodedPayload}`, 'utf8');
36
+ const signature = Buffer.from(parts[2], 'base64url');
37
+ try {
38
+ const key = createPublicKey({ key: jwk, format: 'jwk' });
39
+ return ecdsaVerify('sha256', signingInput, { key, dsaEncoding: 'ieee-p1363' }, signature);
40
+ }
41
+ catch {
42
+ return false;
43
+ }
44
+ }
45
+ /**
46
+ * Verifies a checkout body's embedded `merchant_authorization` against the
47
+ * store's published signing keys. Recomputes the canonical bytes over the body
48
+ * with its `ap2` field removed (the field is excluded from the signing input).
49
+ */
50
+ export function verifyMerchantAuthorization(body, keys) {
51
+ const ap2 = body.ap2;
52
+ const token = ap2?.merchant_authorization;
53
+ if (!token) {
54
+ return false;
55
+ }
56
+ const bodyMinusAp2 = { ...body };
57
+ delete bodyMinusAp2.ap2;
58
+ return verifyDetachedJws(token, canonicalJson(bodyMinusAp2), keys);
59
+ }
60
+ //# sourceMappingURL=verify.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verify.js","sourceRoot":"","sources":["../../src/ap2/verify.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,IAAI,WAAW,EAAE,MAAM,aAAa,CAAC;AACrE,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEpD;;;;;;;GAOG;AACH,MAAM,UAAU,iBAAiB,CAC/B,KAAa,EACb,OAAe,EACf,IAA+B;IAE/B,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC/B,4EAA4E;IAC5E,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;QACpE,OAAO,KAAK,CAAC;IACf,CAAC;IACD,IAAI,MAA+B,CAAC;IACpC,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CACjB,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,WAAW,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CACzB,CAAC;IAC/B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;IACD,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,GAAG,KAAK,OAAO,EAAE,CAAC;QACtC,OAAO,KAAK,CAAC;IACf,CAAC;IACD,MAAM,GAAG,GAAG,OAAO,MAAM,CAAC,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC;IACpE,MAAM,GAAG,GAAG,GAAG;QACb,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAE,CAAsB,CAAC,GAAG,KAAK,GAAG,CAAC;QACvD,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACZ,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,OAAO,KAAK,CAAC;IACf,CAAC;IACD,MAAM,cAAc,GAAG,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;IAC1E,MAAM,YAAY,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,IAAI,cAAc,EAAE,EAAE,MAAM,CAAC,CAAC;IAC1E,MAAM,SAAS,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,WAAW,CAAC,CAAC;IACrD,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,eAAe,CAAC,EAAE,GAAG,EAAE,GAAY,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC;QAClE,OAAO,WAAW,CAChB,QAAQ,EACR,YAAY,EACZ,EAAE,GAAG,EAAE,WAAW,EAAE,YAAY,EAAE,EAClC,SAAS,CACV,CAAC;IACJ,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,2BAA2B,CACzC,IAA6B,EAC7B,IAA+B;IAE/B,MAAM,GAAG,GAAG,IAAI,CAAC,GAAsD,CAAC;IACxE,MAAM,KAAK,GAAG,GAAG,EAAE,sBAAsB,CAAC;IAC1C,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,OAAO,KAAK,CAAC;IACf,CAAC;IACD,MAAM,YAAY,GAAG,EAAE,GAAG,IAAI,EAAE,CAAC;IACjC,OAAO,YAAY,CAAC,GAAG,CAAC;IACxB,OAAO,iBAAiB,CAAC,KAAK,EAAE,aAAa,CAAC,YAAY,CAAC,EAAE,IAAI,CAAC,CAAC;AACrE,CAAC"}
@@ -0,0 +1,76 @@
1
+ import { type UcpResponse } from '../ucp/client.js';
2
+ import { type SearchFilters } from './money.js';
3
+ import type { CheckoutDeps, CheckoutLineItem, CheckoutOutcome, ConfirmPurchaseInput } from './types.js';
4
+ /**
5
+ * Pure checkout orchestration over an injected {@link CheckoutDeps}. Read methods
6
+ * return the raw UCP envelope (inspect with `isUcpError`); `confirmPurchase`
7
+ * returns a structured {@link CheckoutOutcome}. No presentation, no MCP — a host
8
+ * (e.g. the Artos MCP bridge) maps the result to text/widgets.
9
+ */
10
+ export declare function createCheckoutHandlers(deps: CheckoutDeps): {
11
+ /** Cross-store catalog search (price filters in MAJOR units). */
12
+ searchProducts(args: {
13
+ query?: string;
14
+ filters?: SearchFilters;
15
+ sort?: string;
16
+ pagination?: Record<string, unknown>;
17
+ context?: Record<string, unknown>;
18
+ }): Promise<UcpResponse>;
19
+ /** Resolve products by id/handle/SKU/barcode (global). */
20
+ lookupProducts(args: {
21
+ ids: unknown;
22
+ context?: Record<string, unknown>;
23
+ }): Promise<UcpResponse>;
24
+ /** Fetch a single product by id (global, no store slug needed). */
25
+ getProductGlobal(args: {
26
+ id: string;
27
+ selected?: unknown;
28
+ context?: Record<string, unknown>;
29
+ }): Promise<UcpResponse>;
30
+ /** Fetch a single product at a specific store. */
31
+ viewProduct(args: {
32
+ storeSlug: string;
33
+ id: string;
34
+ }): Promise<UcpResponse>;
35
+ createCart(args: {
36
+ storeSlug: string;
37
+ items: CheckoutLineItem[];
38
+ }): Promise<UcpResponse>;
39
+ updateCart(args: {
40
+ storeSlug: string;
41
+ id: string;
42
+ items: CheckoutLineItem[];
43
+ }): Promise<UcpResponse>;
44
+ viewCart(args: {
45
+ storeSlug: string;
46
+ id: string;
47
+ }): Promise<UcpResponse>;
48
+ createCheckout(args: {
49
+ storeSlug: string;
50
+ cartId?: string;
51
+ items?: CheckoutLineItem[];
52
+ buyer?: Record<string, unknown>;
53
+ shippingAddress?: Record<string, unknown>;
54
+ }): Promise<UcpResponse>;
55
+ updateCheckout(args: {
56
+ storeSlug: string;
57
+ id: string;
58
+ buyer?: Record<string, unknown>;
59
+ shippingAddress?: Record<string, unknown>;
60
+ shippingMethodId?: string;
61
+ discounts?: unknown;
62
+ }): Promise<UcpResponse>;
63
+ getOrder(args: {
64
+ storeSlug: string;
65
+ id: string;
66
+ }): Promise<UcpResponse>;
67
+ /**
68
+ * Re-prices the checkout, verifies the store-signed terms, mints the AP2
69
+ * mandate(s), and places the order on the card, crypto, or $0 rail. Returns a
70
+ * {@link CheckoutOutcome} for every branch (no exceptions for business
71
+ * outcomes; transport failures still throw {@link UcpClientError}).
72
+ */
73
+ confirmPurchase(input: ConfirmPurchaseInput): Promise<CheckoutOutcome>;
74
+ };
75
+ /** The handler object returned by {@link createCheckoutHandlers}. */
76
+ export type CheckoutHandlers = ReturnType<typeof createCheckoutHandlers>;