@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/dist/config.js ADDED
@@ -0,0 +1,41 @@
1
+ /**
2
+ * UCP spec version the Artos API implements. Sent in the `UCP-Agent` header and
3
+ * the `meta["ucp-agent"]` envelope; must match the server.
4
+ */
5
+ export const UCP_VERSION = '2026-04-08';
6
+ const SUI_NETWORKS = [
7
+ 'mainnet',
8
+ 'testnet',
9
+ 'devnet',
10
+ 'localnet',
11
+ ];
12
+ /** Strips trailing slashes so URL joins never produce a double slash. */
13
+ export function normalizeBaseUrl(url) {
14
+ const trimmed = url.trim();
15
+ if (!trimmed) {
16
+ throw new Error('artosBaseUrl must be a non-empty URL.');
17
+ }
18
+ return trimmed.replace(/\/+$/, '');
19
+ }
20
+ /**
21
+ * Validates that a JWK is an EC P-256 PRIVATE key (carries `d`) suitable for
22
+ * minting AP2 mandates. Throws with an actionable message otherwise.
23
+ */
24
+ export function assertEcP256PrivateJwk(jwk) {
25
+ if (jwk.kty !== 'EC' || jwk.crv !== 'P-256') {
26
+ throw new Error('agentPrivateJwk must be an EC P-256 key.');
27
+ }
28
+ if (!jwk.d) {
29
+ throw new Error('agentPrivateJwk must include the private component `d`.');
30
+ }
31
+ }
32
+ /** Narrows an arbitrary string to a known {@link SuiNetwork}, defaulting to mainnet. */
33
+ export function parseSuiNetwork(value) {
34
+ if (!value)
35
+ return 'mainnet';
36
+ if (!SUI_NETWORKS.includes(value)) {
37
+ throw new Error(`suiNetwork must be one of: ${SUI_NETWORKS.join(', ')}.`);
38
+ }
39
+ return value;
40
+ }
41
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,YAAY,CAAC;AA6CxC,MAAM,YAAY,GAA0B;IAC1C,SAAS;IACT,SAAS;IACT,QAAQ;IACR,UAAU;CACX,CAAC;AAEF,yEAAyE;AACzE,MAAM,UAAU,gBAAgB,CAAC,GAAW;IAC1C,MAAM,OAAO,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;IAC3B,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CAAC,uCAAuC,CAAC,CAAC;IAC3D,CAAC;IACD,OAAO,OAAO,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;AACrC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,sBAAsB,CAAC,GAAQ;IAC7C,IAAI,GAAG,CAAC,GAAG,KAAK,IAAI,IAAI,GAAG,CAAC,GAAG,KAAK,OAAO,EAAE,CAAC;QAC5C,MAAM,IAAI,KAAK,CAAC,0CAA0C,CAAC,CAAC;IAC9D,CAAC;IACD,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;QACX,MAAM,IAAI,KAAK,CAAC,yDAAyD,CAAC,CAAC;IAC7E,CAAC;AACH,CAAC;AAED,wFAAwF;AACxF,MAAM,UAAU,eAAe,CAAC,KAAyB;IACvD,IAAI,CAAC,KAAK;QAAE,OAAO,SAAS,CAAC;IAC7B,IAAI,CAAC,YAAY,CAAC,QAAQ,CAAC,KAAmB,CAAC,EAAE,CAAC;QAChD,MAAM,IAAI,KAAK,CAAC,8BAA8B,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC5E,CAAC;IACD,OAAO,KAAmB,CAAC;AAC7B,CAAC"}
@@ -0,0 +1,27 @@
1
+ import type { AgentCryptoConfig } from '../config.js';
2
+ import { SuiSigner } from '../sui/signer.js';
3
+ /**
4
+ * Everything the checkout layer needs to settle on the crypto rail. Present only
5
+ * when the agent Sui key is configured; absent => the card rail only. The buyer
6
+ * the spend is drawn from — and the signed limits — are resolved server-side per
7
+ * request from the session's OAuth bearer, so no per-buyer material lives here.
8
+ */
9
+ export interface CryptoDeps {
10
+ /** Signs + submits the server-built payment PTB as the buyer's alias. */
11
+ sui: SuiSigner;
12
+ /** Input coin the agent pays with (swapped to USDC server-side). */
13
+ inputCoinType: string;
14
+ /** Optional client-side hard cap (minor units) refused before signing. */
15
+ maxSpendAmount?: number;
16
+ }
17
+ /**
18
+ * Assembles the crypto settlement dependencies from config, or returns
19
+ * `undefined` when the crypto rail is intentionally unconfigured (card-only).
20
+ *
21
+ * The agent holds a single Sui key; each buyer aliases that address to their own
22
+ * wallet and signs a standing authorization (in My Artos), so the buyer the
23
+ * spend is drawn from — and the caps — are resolved server-side per request from
24
+ * the buyer's OAuth bearer. The crypto rail is therefore enabled by the agent
25
+ * key alone.
26
+ */
27
+ export declare function resolveCryptoDeps(cfg: AgentCryptoConfig): CryptoDeps | undefined;
@@ -0,0 +1,23 @@
1
+ import { SuiSigner } from '../sui/signer.js';
2
+ /** Input coin the agent pays with by default (swapped to USDC server-side). */
3
+ const DEFAULT_INPUT_COIN_TYPE = '0x2::sui::SUI';
4
+ /**
5
+ * Assembles the crypto settlement dependencies from config, or returns
6
+ * `undefined` when the crypto rail is intentionally unconfigured (card-only).
7
+ *
8
+ * The agent holds a single Sui key; each buyer aliases that address to their own
9
+ * wallet and signs a standing authorization (in My Artos), so the buyer the
10
+ * spend is drawn from — and the caps — are resolved server-side per request from
11
+ * the buyer's OAuth bearer. The crypto rail is therefore enabled by the agent
12
+ * key alone.
13
+ */
14
+ export function resolveCryptoDeps(cfg) {
15
+ if (!cfg.agentSuiPrivateKey)
16
+ return undefined;
17
+ return {
18
+ sui: new SuiSigner(cfg.agentSuiPrivateKey, cfg.suiNetwork),
19
+ inputCoinType: DEFAULT_INPUT_COIN_TYPE,
20
+ maxSpendAmount: cfg.agentMaxSpendAmount,
21
+ };
22
+ }
23
+ //# sourceMappingURL=deps.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deps.js","sourceRoot":"","sources":["../../src/crypto/deps.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAE7C,+EAA+E;AAC/E,MAAM,uBAAuB,GAAG,eAAe,CAAC;AAiBhD;;;;;;;;;GASG;AACH,MAAM,UAAU,iBAAiB,CAC/B,GAAsB;IAEtB,IAAI,CAAC,GAAG,CAAC,kBAAkB;QAAE,OAAO,SAAS,CAAC;IAE9C,OAAO;QACL,GAAG,EAAE,IAAI,SAAS,CAAC,GAAG,CAAC,kBAAkB,EAAE,GAAG,CAAC,UAAU,CAAC;QAC1D,aAAa,EAAE,uBAAuB;QACtC,cAAc,EAAE,GAAG,CAAC,mBAAmB;KACxC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,6 @@
1
+ export { UCP_VERSION, normalizeBaseUrl, assertEcP256PrivateJwk, parseSuiNetwork, type Jwk, type SuiNetwork, type UcpClientConfig, type AgentAp2Config, type AgentCryptoConfig, } from './config.js';
2
+ export { UcpClient, UcpClientError, UcpAuthError, messageOf, isUcpError, type UcpResponse, type UcpClientOptions, type UcpAuthMode, type StoreToolOptions, type AccountToolSpec, type ToolAnnotations, } from './ucp/client.js';
3
+ export { canonicalJson, verifyDetachedJws, verifyMerchantAuthorization, Ap2Signer, type CheckoutMandateClaims, type PaymentMandateClaims, } from './ap2/index.js';
4
+ export { createCheckoutHandlers, toMinorUnits, normalizeSearchFilters, availableRails, resolveRail, railKind, grandTotal, currencyOf, asAllowanceId, type CheckoutHandlers, type CheckoutDeps, type CheckoutLineItem, type ConfirmPurchaseInput, type CheckoutErrorCode, type CheckoutOutcome, type SearchFilters, type ResolvedRail, } from './checkout/index.js';
5
+ export { resolveCryptoDeps, type CryptoDeps } from './crypto/deps.js';
6
+ export { SuiSigner, SuiSignerError } from './sui/signer.js';
package/dist/index.js ADDED
@@ -0,0 +1,12 @@
1
+ // Config
2
+ export { UCP_VERSION, normalizeBaseUrl, assertEcP256PrivateJwk, parseSuiNetwork, } from './config.js';
3
+ // Transport
4
+ export { UcpClient, UcpClientError, UcpAuthError, messageOf, isUcpError, } from './ucp/client.js';
5
+ // AP2
6
+ export { canonicalJson, verifyDetachedJws, verifyMerchantAuthorization, Ap2Signer, } from './ap2/index.js';
7
+ // Checkout orchestration
8
+ export { createCheckoutHandlers, toMinorUnits, normalizeSearchFilters, availableRails, resolveRail, railKind, grandTotal, currencyOf, asAllowanceId, } from './checkout/index.js';
9
+ // Crypto rail (requires the optional `@mysten/sui` peer dependency)
10
+ export { resolveCryptoDeps } from './crypto/deps.js';
11
+ export { SuiSigner, SuiSignerError } from './sui/signer.js';
12
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,SAAS;AACT,OAAO,EACL,WAAW,EACX,gBAAgB,EAChB,sBAAsB,EACtB,eAAe,GAMhB,MAAM,aAAa,CAAC;AAErB,YAAY;AACZ,OAAO,EACL,SAAS,EACT,cAAc,EACd,YAAY,EACZ,SAAS,EACT,UAAU,GAOX,MAAM,iBAAiB,CAAC;AAEzB,MAAM;AACN,OAAO,EACL,aAAa,EACb,iBAAiB,EACjB,2BAA2B,EAC3B,SAAS,GAGV,MAAM,gBAAgB,CAAC;AAExB,yBAAyB;AACzB,OAAO,EACL,sBAAsB,EACtB,YAAY,EACZ,sBAAsB,EACtB,cAAc,EACd,WAAW,EACX,QAAQ,EACR,UAAU,EACV,UAAU,EACV,aAAa,GASd,MAAM,qBAAqB,CAAC;AAE7B,oEAAoE;AACpE,OAAO,EAAE,iBAAiB,EAAmB,MAAM,kBAAkB,CAAC;AACtE,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC"}
@@ -0,0 +1,27 @@
1
+ import type { SuiNetwork } from '../config.js';
2
+ /** Thrown when a crypto payment transaction fails to sign or execute. */
3
+ export declare class SuiSignerError extends Error {
4
+ }
5
+ /**
6
+ * Signs and submits the crypto-payment PTBs the Artos API hands back from
7
+ * `prepare_checkout_payment`. The agent's Ed25519 key is configured on-chain as
8
+ * an alias of the buyer's wallet, so signing here authorizes a spend from the
9
+ * buyer's funds without the buyer being present. The unsigned transaction is the
10
+ * server-built DeepBook swap that routes the order total to the merchant in
11
+ * USDC; the resulting digest becomes the payment credential on completion.
12
+ *
13
+ * Requires the optional `@mysten/sui` peer dependency (imported only by this
14
+ * module / the `@artos-commerce/ucp-client/sui` entry).
15
+ */
16
+ export declare class SuiSigner {
17
+ private readonly keypair;
18
+ private readonly client;
19
+ /** The agent's Sui address (the on-chain alias spending on the buyer's behalf). */
20
+ readonly address: string;
21
+ constructor(privateKey: string, network: SuiNetwork);
22
+ /**
23
+ * Signs the server-built unsigned transaction (a JSON-serialized PTB) and
24
+ * executes it, returning the on-chain transaction digest once finalized.
25
+ */
26
+ signAndSubmit(unsignedTx: string): Promise<string>;
27
+ }
@@ -0,0 +1,69 @@
1
+ import { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519';
2
+ import { SuiGrpcClient } from '@mysten/sui/grpc';
3
+ import { Transaction } from '@mysten/sui/transactions';
4
+ /** Public fullnode gRPC endpoints, keyed by network. */
5
+ const GRPC_URLS = {
6
+ mainnet: 'https://fullnode.mainnet.sui.io:443',
7
+ testnet: 'https://fullnode.testnet.sui.io:443',
8
+ devnet: 'https://fullnode.devnet.sui.io:443',
9
+ localnet: 'http://127.0.0.1:9000',
10
+ };
11
+ /** Thrown when a crypto payment transaction fails to sign or execute. */
12
+ export class SuiSignerError extends Error {
13
+ }
14
+ /**
15
+ * Signs and submits the crypto-payment PTBs the Artos API hands back from
16
+ * `prepare_checkout_payment`. The agent's Ed25519 key is configured on-chain as
17
+ * an alias of the buyer's wallet, so signing here authorizes a spend from the
18
+ * buyer's funds without the buyer being present. The unsigned transaction is the
19
+ * server-built DeepBook swap that routes the order total to the merchant in
20
+ * USDC; the resulting digest becomes the payment credential on completion.
21
+ *
22
+ * Requires the optional `@mysten/sui` peer dependency (imported only by this
23
+ * module / the `@artos-commerce/ucp-client/sui` entry).
24
+ */
25
+ export class SuiSigner {
26
+ keypair;
27
+ client;
28
+ /** The agent's Sui address (the on-chain alias spending on the buyer's behalf). */
29
+ address;
30
+ constructor(privateKey, network) {
31
+ this.keypair = Ed25519Keypair.fromSecretKey(privateKey);
32
+ this.address = this.keypair.toSuiAddress();
33
+ this.client = new SuiGrpcClient({
34
+ network,
35
+ baseUrl: GRPC_URLS[network],
36
+ });
37
+ }
38
+ /**
39
+ * Signs the server-built unsigned transaction (a JSON-serialized PTB) and
40
+ * executes it, returning the on-chain transaction digest once finalized.
41
+ */
42
+ async signAndSubmit(unsignedTx) {
43
+ let tx;
44
+ try {
45
+ tx = Transaction.from(unsignedTx);
46
+ }
47
+ catch (err) {
48
+ throw new SuiSignerError(`Could not parse the unsigned payment transaction: ${messageOf(err)}`);
49
+ }
50
+ const result = await this.client.core.signAndExecuteTransaction({
51
+ transaction: tx,
52
+ signer: this.keypair,
53
+ include: { effects: true },
54
+ });
55
+ if (result.FailedTransaction) {
56
+ // `status.error` is a structured ExecutionError (or null); JSON-encode it
57
+ // so the message is informative rather than "[object Object]".
58
+ throw new SuiSignerError(`Payment transaction failed on-chain: ${JSON.stringify(result.FailedTransaction.status.error)}`);
59
+ }
60
+ const digest = result.Transaction.digest;
61
+ // Wait for finality so the API can immediately verify the receipt.
62
+ await this.client.core.waitForTransaction({ digest });
63
+ return digest;
64
+ }
65
+ }
66
+ function messageOf(err) {
67
+ return err instanceof Error ? err.message : String(err);
68
+ }
69
+ //# sourceMappingURL=signer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"signer.js","sourceRoot":"","sources":["../../src/sui/signer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAC9D,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACjD,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAGvD,wDAAwD;AACxD,MAAM,SAAS,GAA+B;IAC5C,OAAO,EAAE,qCAAqC;IAC9C,OAAO,EAAE,qCAAqC;IAC9C,MAAM,EAAE,oCAAoC;IAC5C,QAAQ,EAAE,uBAAuB;CAClC,CAAC;AAEF,yEAAyE;AACzE,MAAM,OAAO,cAAe,SAAQ,KAAK;CAAG;AAE5C;;;;;;;;;;GAUG;AACH,MAAM,OAAO,SAAS;IACH,OAAO,CAAiB;IACxB,MAAM,CAAgB;IACvC,mFAAmF;IAC1E,OAAO,CAAS;IAEzB,YAAY,UAAkB,EAAE,OAAmB;QACjD,IAAI,CAAC,OAAO,GAAG,cAAc,CAAC,aAAa,CAAC,UAAU,CAAC,CAAC;QACxD,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,YAAY,EAAE,CAAC;QAC3C,IAAI,CAAC,MAAM,GAAG,IAAI,aAAa,CAAC;YAC9B,OAAO;YACP,OAAO,EAAE,SAAS,CAAC,OAAO,CAAC;SAC5B,CAAC,CAAC;IACL,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,aAAa,CAAC,UAAkB;QACpC,IAAI,EAAe,CAAC;QACpB,IAAI,CAAC;YACH,EAAE,GAAG,WAAW,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QACpC,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,IAAI,cAAc,CACtB,qDAAqD,SAAS,CAAC,GAAG,CAAC,EAAE,CACtE,CAAC;QACJ,CAAC;QAED,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,yBAAyB,CAAC;YAC9D,WAAW,EAAE,EAAE;YACf,MAAM,EAAE,IAAI,CAAC,OAAO;YACpB,OAAO,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE;SAC3B,CAAC,CAAC;QACH,IAAI,MAAM,CAAC,iBAAiB,EAAE,CAAC;YAC7B,0EAA0E;YAC1E,+DAA+D;YAC/D,MAAM,IAAI,cAAc,CACtB,wCAAwC,IAAI,CAAC,SAAS,CACpD,MAAM,CAAC,iBAAiB,CAAC,MAAM,CAAC,KAAK,CACtC,EAAE,CACJ,CAAC;QACJ,CAAC;QAED,MAAM,MAAM,GAAG,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC;QACzC,mEAAmE;QACnE,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC;QACtD,OAAO,MAAM,CAAC;IAChB,CAAC;CACF;AAED,SAAS,SAAS,CAAC,GAAY;IAC7B,OAAO,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;AAC1D,CAAC"}
@@ -0,0 +1,169 @@
1
+ import { type UcpClientConfig } from '../config.js';
2
+ /** A UCP response envelope (`ucp` meta + capability payload). */
3
+ export type UcpResponse = {
4
+ ucp?: {
5
+ status?: string;
6
+ [k: string]: unknown;
7
+ };
8
+ messages?: Array<{
9
+ code?: string;
10
+ content?: string;
11
+ }>;
12
+ [k: string]: unknown;
13
+ };
14
+ /**
15
+ * Tool behavior hints as advertised by an MCP `tools/list` (a structural subset
16
+ * of the MCP SDK's `ToolAnnotations`). Kept local so the SDK carries no
17
+ * dependency on `@modelcontextprotocol/sdk`.
18
+ */
19
+ export type ToolAnnotations = {
20
+ title?: string;
21
+ readOnlyHint?: boolean;
22
+ destructiveHint?: boolean;
23
+ idempotentHint?: boolean;
24
+ openWorldHint?: boolean;
25
+ [k: string]: unknown;
26
+ };
27
+ /** A buyer-account tool descriptor as advertised by the API's `tools/list`. */
28
+ export type AccountToolSpec = {
29
+ name: string;
30
+ description?: string;
31
+ inputSchema?: Record<string, unknown>;
32
+ annotations?: ToolAnnotations;
33
+ };
34
+ /** Thrown for transport / JSON-RPC / HTTP failures (not UCP business outcomes). */
35
+ export declare class UcpClientError extends Error {
36
+ }
37
+ /**
38
+ * Thrown when the API rejects a buyer-bound call with a 401 (expired/invalid
39
+ * bearer). Carries the API's `WWW-Authenticate` challenge so a host can relay it
40
+ * verbatim to the agent, which then runs its refresh_token grant and retries —
41
+ * instead of forcing the buyer through full re-consent.
42
+ */
43
+ export declare class UcpAuthError extends UcpClientError {
44
+ readonly wwwAuthenticate: string;
45
+ constructor(wwwAuthenticate: string);
46
+ }
47
+ type FetchLike = typeof fetch;
48
+ /** Which credential the API call authenticates with. */
49
+ export type UcpAuthMode = 'platform' | 'buyer';
50
+ export interface StoreToolOptions {
51
+ /** State-changing tools require an idempotency key in `meta`. */
52
+ idempotent?: boolean;
53
+ /** Override the generated idempotency key (used for retry safety). */
54
+ idempotencyKey?: string;
55
+ /**
56
+ * Credential to authenticate with. `platform` (default) sends the cross-store
57
+ * `X-API-Key`. `buyer` forwards the session's OAuth bearer instead (and omits
58
+ * the API key), so the API resolves the buyer account and enforces their
59
+ * STORED agent authorization + caps. Buyer-bound calls (crypto
60
+ * prepare/complete) require {@link UcpClientOptions.buyerToken}.
61
+ */
62
+ auth?: UcpAuthMode;
63
+ }
64
+ export interface UcpClientOptions {
65
+ /**
66
+ * Inject a `fetch` implementation (for tests or a custom agent/proxy). When
67
+ * omitted the global `fetch` is used (Node 18+ / browsers).
68
+ */
69
+ fetch?: FetchLike;
70
+ /**
71
+ * The current session's buyer OAuth bearer (forwarded to the API on
72
+ * `auth: 'buyer'` calls). Absent for anonymous/unauthenticated sessions.
73
+ */
74
+ buyerToken?: string;
75
+ /**
76
+ * Invoked with the API's `WWW-Authenticate` challenge when a buyer-bound call
77
+ * is rejected with a 401, so the surrounding request can be answered with that
78
+ * 401 + header (triggering the agent's silent refresh).
79
+ */
80
+ onAuthChallenge?: (wwwAuthenticate: string) => void;
81
+ }
82
+ /**
83
+ * Talks to the Artos UCP API two ways:
84
+ * - global catalog search over REST (`POST /catalog/search`), the cross-store
85
+ * vendor extension; and
86
+ * - per-store shopping ops over the UCP MCP JSON-RPC transport
87
+ * (`POST /s/:slug/mcp`), routed by the seller slug carried on search results.
88
+ *
89
+ * Every request carries the UCP transport preamble (`UCP-Agent` with the agent
90
+ * profile, `Request-Id`) and the platform API key (which authenticates at any
91
+ * store). State-changing MCP calls also carry `meta["idempotency-key"]`.
92
+ */
93
+ export declare class UcpClient {
94
+ private readonly cfg;
95
+ private readonly fetchImpl;
96
+ private readonly buyerToken?;
97
+ private readonly onAuthChallenge?;
98
+ constructor(cfg: UcpClientConfig, options?: UcpClientOptions);
99
+ /**
100
+ * Raises a {@link UcpAuthError} (and notifies the auth-challenge sink) when
101
+ * the API rejected the call with a 401, preserving the `WWW-Authenticate`
102
+ * header so a host can relay it for a silent refresh. No-op otherwise.
103
+ */
104
+ private relayAuthError;
105
+ /** True when the session carries a buyer OAuth bearer (buyer is signed in). */
106
+ hasBuyerToken(): boolean;
107
+ /** `ucp-version="...", profile="..."` per the UCP-Agent header grammar. */
108
+ ucpAgentHeader(): string;
109
+ private baseHeaders;
110
+ /** Cross-store catalog search. `catalog` is the UCP search request body. */
111
+ globalSearch(catalog: {
112
+ query?: string;
113
+ filters?: Record<string, unknown>;
114
+ sort?: string;
115
+ pagination?: Record<string, unknown>;
116
+ context?: Record<string, unknown>;
117
+ }): Promise<UcpResponse>;
118
+ /**
119
+ * Calls a read-only global (cross-store) catalog tool over the platform MCP
120
+ * JSON-RPC transport (`POST /mcp`) and returns the `structuredContent` payload
121
+ * (itself a UCP response, possibly a business-error envelope). Unlike
122
+ * {@link callStoreTool} there is no store slug and no idempotency key (these
123
+ * tools never mutate state); the server still requires the agent profile in
124
+ * `meta` and the request DTO wrapped under `catalog`.
125
+ */
126
+ callGlobalTool(name: 'search_catalog' | 'lookup_catalog' | 'get_product', catalog: Record<string, unknown>): Promise<UcpResponse>;
127
+ /**
128
+ * Calls a UCP MCP tool at a specific store and returns the `structuredContent`
129
+ * payload (itself a UCP response, which may be a business-error envelope the
130
+ * caller can inspect via `ucp.status`).
131
+ */
132
+ callStoreTool(slug: string, name: string, args: Record<string, unknown>, opts?: StoreToolOptions): Promise<UcpResponse>;
133
+ /**
134
+ * Lists the buyer-account tools the API advertises (`POST /account/mcp`,
135
+ * `tools/list`). First-party + buyer-scoped, so this sends only the buyer
136
+ * bearer. Throws {@link UcpAuthError} on 401.
137
+ */
138
+ listAccountTools(): Promise<AccountToolSpec[]>;
139
+ /**
140
+ * Calls a buyer-account tool (`POST /account/mcp`, `tools/call`) with the
141
+ * buyer bearer and returns the `structuredContent` payload (a plain JSON
142
+ * resource, possibly a UCP business-error envelope). Throws
143
+ * {@link UcpAuthError} on 401 so a host can relay the refresh challenge.
144
+ */
145
+ callAccountTool(name: string, args: Record<string, unknown>): Promise<Record<string, unknown>>;
146
+ /**
147
+ * Fetches a store's UCP profile (`GET /s/:slug/.well-known/ucp`) and returns
148
+ * its published `signing_keys` (JWKs), used to verify the store's
149
+ * `merchant_authorization` before completing a checkout. Returns an empty
150
+ * array when the profile is unreachable or publishes no keys.
151
+ */
152
+ fetchStoreProfile(slug: string): Promise<Record<string, unknown>[]>;
153
+ /**
154
+ * Fetches an image and returns it as a base64 block (`{ type, data, mimeType }`),
155
+ * which hosts like Claude render inline natively. Returns null on any failure
156
+ * or if the image is too large to embed. Images are served by the Artos API
157
+ * itself, so no auth is needed.
158
+ */
159
+ fetchImage(url: string, maxBytes?: number): Promise<{
160
+ type: 'image';
161
+ data: string;
162
+ mimeType: string;
163
+ } | null>;
164
+ }
165
+ /** First message content (or undefined) from a UCP envelope. */
166
+ export declare function messageOf(body: UcpResponse): string | undefined;
167
+ /** True when a UCP response is a business-error envelope. */
168
+ export declare function isUcpError(body: UcpResponse): boolean;
169
+ export {};
@@ -0,0 +1,269 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { UCP_VERSION } from '../config.js';
3
+ /** Thrown for transport / JSON-RPC / HTTP failures (not UCP business outcomes). */
4
+ export class UcpClientError extends Error {
5
+ }
6
+ /** The default RFC 6750 challenge when the API omits its own. */
7
+ const DEFAULT_BEARER_CHALLENGE = 'Bearer error="invalid_token"';
8
+ /**
9
+ * Thrown when the API rejects a buyer-bound call with a 401 (expired/invalid
10
+ * bearer). Carries the API's `WWW-Authenticate` challenge so a host can relay it
11
+ * verbatim to the agent, which then runs its refresh_token grant and retries —
12
+ * instead of forcing the buyer through full re-consent.
13
+ */
14
+ export class UcpAuthError extends UcpClientError {
15
+ wwwAuthenticate;
16
+ constructor(wwwAuthenticate) {
17
+ super('Buyer authentication required (401).');
18
+ this.wwwAuthenticate = wwwAuthenticate;
19
+ this.name = 'UcpAuthError';
20
+ }
21
+ }
22
+ /**
23
+ * Talks to the Artos UCP API two ways:
24
+ * - global catalog search over REST (`POST /catalog/search`), the cross-store
25
+ * vendor extension; and
26
+ * - per-store shopping ops over the UCP MCP JSON-RPC transport
27
+ * (`POST /s/:slug/mcp`), routed by the seller slug carried on search results.
28
+ *
29
+ * Every request carries the UCP transport preamble (`UCP-Agent` with the agent
30
+ * profile, `Request-Id`) and the platform API key (which authenticates at any
31
+ * store). State-changing MCP calls also carry `meta["idempotency-key"]`.
32
+ */
33
+ export class UcpClient {
34
+ cfg;
35
+ fetchImpl;
36
+ buyerToken;
37
+ onAuthChallenge;
38
+ constructor(cfg, options = {}) {
39
+ this.cfg = cfg;
40
+ this.fetchImpl = options.fetch ?? fetch;
41
+ this.buyerToken = options.buyerToken;
42
+ this.onAuthChallenge = options.onAuthChallenge;
43
+ }
44
+ /**
45
+ * Raises a {@link UcpAuthError} (and notifies the auth-challenge sink) when
46
+ * the API rejected the call with a 401, preserving the `WWW-Authenticate`
47
+ * header so a host can relay it for a silent refresh. No-op otherwise.
48
+ */
49
+ relayAuthError(res) {
50
+ if (res.status !== 401) {
51
+ return;
52
+ }
53
+ const challenge = res.headers?.get('www-authenticate') ?? DEFAULT_BEARER_CHALLENGE;
54
+ this.onAuthChallenge?.(challenge);
55
+ throw new UcpAuthError(challenge);
56
+ }
57
+ /** True when the session carries a buyer OAuth bearer (buyer is signed in). */
58
+ hasBuyerToken() {
59
+ return Boolean(this.buyerToken);
60
+ }
61
+ /** `ucp-version="...", profile="..."` per the UCP-Agent header grammar. */
62
+ ucpAgentHeader() {
63
+ return `ucp-version="${UCP_VERSION}", profile="${this.cfg.platformProfileUrl}"`;
64
+ }
65
+ baseHeaders(auth = 'platform') {
66
+ const headers = {
67
+ 'Content-Type': 'application/json',
68
+ Accept: 'application/json',
69
+ 'UCP-Agent': this.ucpAgentHeader(),
70
+ 'Request-Id': randomUUID(),
71
+ };
72
+ // The API's identity guard prefers X-API-Key over a bearer, so buyer-bound
73
+ // calls must send ONLY the bearer for the buyer account to be resolved.
74
+ if (auth === 'buyer' && this.buyerToken) {
75
+ headers.Authorization = `Bearer ${this.buyerToken}`;
76
+ }
77
+ else {
78
+ headers['X-API-Key'] = this.cfg.platformApiKey;
79
+ }
80
+ return headers;
81
+ }
82
+ /** Cross-store catalog search. `catalog` is the UCP search request body. */
83
+ async globalSearch(catalog) {
84
+ const res = await this.fetchImpl(`${this.cfg.artosBaseUrl}/catalog/search`, {
85
+ method: 'POST',
86
+ headers: this.baseHeaders(),
87
+ body: JSON.stringify(catalog),
88
+ });
89
+ const body = (await res.json());
90
+ if (!res.ok || body.ucp?.status === 'error') {
91
+ throw new UcpClientError(messageOf(body) ?? `Search failed (${res.status}).`);
92
+ }
93
+ return body;
94
+ }
95
+ /**
96
+ * Calls a read-only global (cross-store) catalog tool over the platform MCP
97
+ * JSON-RPC transport (`POST /mcp`) and returns the `structuredContent` payload
98
+ * (itself a UCP response, possibly a business-error envelope). Unlike
99
+ * {@link callStoreTool} there is no store slug and no idempotency key (these
100
+ * tools never mutate state); the server still requires the agent profile in
101
+ * `meta` and the request DTO wrapped under `catalog`.
102
+ */
103
+ async callGlobalTool(name, catalog) {
104
+ const meta = { 'ucp-agent': { profile: this.cfg.platformProfileUrl } };
105
+ const res = await this.fetchImpl(`${this.cfg.artosBaseUrl}/mcp`, {
106
+ method: 'POST',
107
+ headers: this.baseHeaders(),
108
+ body: JSON.stringify({
109
+ jsonrpc: '2.0',
110
+ id: randomUUID(),
111
+ method: 'tools/call',
112
+ params: { name, arguments: { catalog, meta } },
113
+ }),
114
+ });
115
+ if (!res.ok) {
116
+ this.relayAuthError(res);
117
+ throw new UcpClientError(`MCP transport error (${res.status}).`);
118
+ }
119
+ const body = (await res.json());
120
+ if (body.error) {
121
+ throw new UcpClientError(body.error.message ?? `MCP error ${body.error.code ?? ''}`.trim());
122
+ }
123
+ const structured = body.result?.structuredContent;
124
+ if (!structured) {
125
+ throw new UcpClientError('MCP response missing structuredContent.');
126
+ }
127
+ return structured;
128
+ }
129
+ /**
130
+ * Calls a UCP MCP tool at a specific store and returns the `structuredContent`
131
+ * payload (itself a UCP response, which may be a business-error envelope the
132
+ * caller can inspect via `ucp.status`).
133
+ */
134
+ async callStoreTool(slug, name, args, opts = {}) {
135
+ const meta = {
136
+ 'ucp-agent': { profile: this.cfg.platformProfileUrl },
137
+ };
138
+ if (opts.idempotent) {
139
+ meta['idempotency-key'] = opts.idempotencyKey ?? randomUUID();
140
+ }
141
+ const res = await this.fetchImpl(`${this.cfg.artosBaseUrl}/s/${slug}/mcp`, {
142
+ method: 'POST',
143
+ headers: this.baseHeaders(opts.auth),
144
+ body: JSON.stringify({
145
+ jsonrpc: '2.0',
146
+ id: randomUUID(),
147
+ method: 'tools/call',
148
+ params: { name, arguments: { ...args, meta } },
149
+ }),
150
+ });
151
+ if (!res.ok) {
152
+ this.relayAuthError(res);
153
+ throw new UcpClientError(`MCP transport error (${res.status}).`);
154
+ }
155
+ const body = (await res.json());
156
+ if (body.error) {
157
+ throw new UcpClientError(body.error.message ?? `MCP error ${body.error.code ?? ''}`.trim());
158
+ }
159
+ const structured = body.result?.structuredContent;
160
+ if (!structured) {
161
+ throw new UcpClientError('MCP response missing structuredContent.');
162
+ }
163
+ return structured;
164
+ }
165
+ /**
166
+ * Lists the buyer-account tools the API advertises (`POST /account/mcp`,
167
+ * `tools/list`). First-party + buyer-scoped, so this sends only the buyer
168
+ * bearer. Throws {@link UcpAuthError} on 401.
169
+ */
170
+ async listAccountTools() {
171
+ const res = await this.fetchImpl(`${this.cfg.artosBaseUrl}/account/mcp`, {
172
+ method: 'POST',
173
+ headers: this.baseHeaders('buyer'),
174
+ body: JSON.stringify({
175
+ jsonrpc: '2.0',
176
+ id: randomUUID(),
177
+ method: 'tools/list',
178
+ }),
179
+ });
180
+ if (!res.ok) {
181
+ this.relayAuthError(res);
182
+ throw new UcpClientError(`Account MCP transport error (${res.status}).`);
183
+ }
184
+ const body = (await res.json());
185
+ if (body.error) {
186
+ throw new UcpClientError(body.error.message ??
187
+ `Account MCP error ${body.error.code ?? ''}`.trim());
188
+ }
189
+ return body.result?.tools ?? [];
190
+ }
191
+ /**
192
+ * Calls a buyer-account tool (`POST /account/mcp`, `tools/call`) with the
193
+ * buyer bearer and returns the `structuredContent` payload (a plain JSON
194
+ * resource, possibly a UCP business-error envelope). Throws
195
+ * {@link UcpAuthError} on 401 so a host can relay the refresh challenge.
196
+ */
197
+ async callAccountTool(name, args) {
198
+ const res = await this.fetchImpl(`${this.cfg.artosBaseUrl}/account/mcp`, {
199
+ method: 'POST',
200
+ headers: this.baseHeaders('buyer'),
201
+ body: JSON.stringify({
202
+ jsonrpc: '2.0',
203
+ id: randomUUID(),
204
+ method: 'tools/call',
205
+ params: { name, arguments: args },
206
+ }),
207
+ });
208
+ if (!res.ok) {
209
+ this.relayAuthError(res);
210
+ throw new UcpClientError(`Account MCP transport error (${res.status}).`);
211
+ }
212
+ const body = (await res.json());
213
+ if (body.error) {
214
+ throw new UcpClientError(body.error.message ??
215
+ `Account MCP error ${body.error.code ?? ''}`.trim());
216
+ }
217
+ const structured = body.result?.structuredContent;
218
+ if (!structured) {
219
+ throw new UcpClientError('Account MCP response missing structuredContent.');
220
+ }
221
+ return structured;
222
+ }
223
+ /**
224
+ * Fetches a store's UCP profile (`GET /s/:slug/.well-known/ucp`) and returns
225
+ * its published `signing_keys` (JWKs), used to verify the store's
226
+ * `merchant_authorization` before completing a checkout. Returns an empty
227
+ * array when the profile is unreachable or publishes no keys.
228
+ */
229
+ async fetchStoreProfile(slug) {
230
+ const res = await this.fetchImpl(`${this.cfg.artosBaseUrl}/s/${slug}/.well-known/ucp`, { method: 'GET', headers: this.baseHeaders() });
231
+ if (!res.ok) {
232
+ throw new UcpClientError(`Store profile fetch failed (${res.status}).`);
233
+ }
234
+ const body = (await res.json());
235
+ return body.signing_keys ?? [];
236
+ }
237
+ /**
238
+ * Fetches an image and returns it as a base64 block (`{ type, data, mimeType }`),
239
+ * which hosts like Claude render inline natively. Returns null on any failure
240
+ * or if the image is too large to embed. Images are served by the Artos API
241
+ * itself, so no auth is needed.
242
+ */
243
+ async fetchImage(url, maxBytes = 4_000_000) {
244
+ try {
245
+ const res = await this.fetchImpl(url);
246
+ if (!res.ok)
247
+ return null;
248
+ const mimeType = (res.headers.get('content-type') ?? 'image/webp').split(';')[0];
249
+ if (!mimeType.startsWith('image/'))
250
+ return null;
251
+ const buf = Buffer.from(await res.arrayBuffer());
252
+ if (buf.byteLength === 0 || buf.byteLength > maxBytes)
253
+ return null;
254
+ return { type: 'image', data: buf.toString('base64'), mimeType };
255
+ }
256
+ catch {
257
+ return null;
258
+ }
259
+ }
260
+ }
261
+ /** First message content (or undefined) from a UCP envelope. */
262
+ export function messageOf(body) {
263
+ return body.messages?.find((m) => m.content)?.content;
264
+ }
265
+ /** True when a UCP response is a business-error envelope. */
266
+ export function isUcpError(body) {
267
+ return body.ucp?.status === 'error';
268
+ }
269
+ //# sourceMappingURL=client.js.map