@artos-commerce/ucp-client 0.1.1 → 0.2.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.
package/README.md CHANGED
@@ -1,16 +1,17 @@
1
1
  # @artos-commerce/ucp-client
2
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.
3
+ **Build-track Direct UCP client for Artos.** A typed UCP transport, AP2 mandate
4
+ signing, checkout orchestration, buyer-account tools, and buyer OAuth so you can
5
+ build your own commerce-grade UCP client against `api.artos.sh` — without
6
+ hand-rolling the JSON-RPC envelope, the canonical-JSON signing bytes, the AP2
7
+ mandates, or the payment-rail rules.
7
8
 
8
9
  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.
10
+ want an MCP endpoint for AI agents, point them at the hosted **Connect** bridge
11
+ (`https://agent.artos.sh/mcp`) and skip this package. Reach for
12
+ `@artos-commerce/ucp-client` when you are building your **own**
13
+ client/integration (the **Build** track) and want the hard parts — signing,
14
+ verification, and rail selection — done correctly for you.
14
15
 
15
16
  ## What it does for you
16
17
 
@@ -25,6 +26,12 @@ want an MCP endpoint for AI agents, point them at the hosted bridge
25
26
  - **Checkout orchestration** (`createCheckoutHandlers`): a single
26
27
  `confirmPurchase` re-prices, verifies, mints the mandate, routes to the
27
28
  `$0` / card / crypto rail, and returns a structured `CheckoutOutcome`.
29
+ - **Buyer-account tools** (`createAccountHandlers`): typed wrappers over the
30
+ buyer's account MCP (`get_buyer_context`, `list_my_orders`, wallet balances,
31
+ coupons, …) — all buyer-bound.
32
+ - **Buyer OAuth** (`OAuthClient` + `createPkce`): discover the Authorization
33
+ Server, build the PKCE authorize URL, and exchange/refresh tokens so a
34
+ Build-track agent can run buyer sign-in itself.
28
35
 
29
36
  ## Install
30
37
 
@@ -118,6 +125,46 @@ switch (outcome.status) {
118
125
  throw `UcpClientError`; an expired buyer bearer throws `UcpAuthError` carrying
119
126
  the `WWW-Authenticate` challenge.
120
127
 
128
+ ### Buyer sign-in (OAuth) and account tools
129
+
130
+ A Build-track agent can run the buyer-OAuth dance itself instead of relying on
131
+ the hosted Connect bridge:
132
+
133
+ ```ts
134
+ import { OAuthClient, createPkce } from '@artos-commerce/ucp-client';
135
+
136
+ const oauth = new OAuthClient({ artosBaseUrl: 'https://api.artos.sh' });
137
+ const pkce = createPkce();
138
+
139
+ // 1. Send the buyer here to sign in + consent (offline_access by default).
140
+ const { url, state } = await oauth.authorizeUrl({
141
+ clientId: 'https://you.example/well-known/oauth-client.json',
142
+ redirectUri: 'https://you.example/oauth/callback',
143
+ codeChallenge: pkce.challenge,
144
+ });
145
+
146
+ // 2. On the redirect (verify `state`), exchange the code for tokens.
147
+ const tokens = await oauth.exchangeCode({
148
+ code,
149
+ codeVerifier: pkce.verifier,
150
+ clientId: 'https://you.example/well-known/oauth-client.json',
151
+ redirectUri: 'https://you.example/oauth/callback',
152
+ });
153
+
154
+ // 3. Later, silently renew with the rotating refresh token.
155
+ const fresh = await oauth.refresh({ refreshToken: tokens.refreshToken! });
156
+ ```
157
+
158
+ With `tokens.accessToken` as the `buyerToken`, the account tools are typed:
159
+
160
+ ```ts
161
+ import { createAccountHandlers } from '@artos-commerce/ucp-client';
162
+
163
+ const account = createAccountHandlers({ client }); // client carries buyerToken
164
+ const me = await account.getBuyerContext();
165
+ const orders = await account.listOrders({ limit: 10 });
166
+ ```
167
+
121
168
  ## The two-credential auth model
122
169
 
123
170
  The Artos API's identity guard prefers `X-API-Key` over a bearer. `UcpClient`
@@ -139,12 +186,14 @@ Never send both for a buyer-bound call.
139
186
  - `Ap2Signer` — `mintCheckoutMandate`, `mintPaymentMandate`.
140
187
  - `canonicalJson`, `verifyDetachedJws`, `verifyMerchantAuthorization`.
141
188
  - `createCheckoutHandlers` — read tools + `confirmPurchase`.
189
+ - `createAccountHandlers` — typed buyer-account tools (buyer-bound).
190
+ - `OAuthClient`, `createPkce`, `DEFAULT_BUYER_SCOPE` — buyer OAuth (PKCE).
142
191
  - `SuiSigner`, `resolveCryptoDeps` (crypto rail; need `@mysten/sui`).
143
192
  - Helpers: `toMinorUnits`, `normalizeSearchFilters`, `resolveRail`,
144
193
  `grandTotal`, `currencyOf`, `asAllowanceId`, `isUcpError`, `messageOf`.
145
194
 
146
195
  Subpath entries are also published: `@artos-commerce/ucp-client/ucp`,
147
- `/ap2`, `/checkout`, `/sui`, `/crypto`.
196
+ `/ap2`, `/checkout`, `/account`, `/oauth`, `/sui`, `/crypto`.
148
197
 
149
198
  ## Security notes
150
199
 
@@ -0,0 +1,43 @@
1
+ import type { AccountToolSpec, UcpClient } from '../ucp/client.js';
2
+ /** Dependencies the account handlers operate over (injectable for tests). */
3
+ export interface AccountDeps {
4
+ client: UcpClient;
5
+ }
6
+ /**
7
+ * Typed convenience wrappers over the buyer-account MCP tools (`POST
8
+ * /account/mcp`). Every call is **buyer-bound**: the {@link UcpClient} must carry
9
+ * a buyer OAuth bearer (`buyerToken`) or the API answers 401 (relayed as a
10
+ * {@link import('../ucp/client.js').UcpAuthError}). The buyer is always resolved
11
+ * server-side from the bearer — never passed in args. Each method returns the
12
+ * tool's `structuredContent` payload (a plain JSON resource, possibly a UCP
13
+ * business-error envelope inspectable via `isUcpError`).
14
+ *
15
+ * Mirrors {@link import('../checkout/handlers.js').createCheckoutHandlers}: pure
16
+ * handlers over an injected client, so a host unit-tests them with a fake.
17
+ */
18
+ export declare function createAccountHandlers(deps: AccountDeps): {
19
+ /** Lists the buyer-account tools the API advertises (`tools/list`). */
20
+ listTools(): Promise<AccountToolSpec[]>;
21
+ /** The buyer's profile + session context. */
22
+ getBuyerContext(): Promise<Record<string, unknown>>;
23
+ /** The buyer's saved shipping/billing addresses. */
24
+ listAddresses(): Promise<Record<string, unknown>>;
25
+ /** The buyer's orders across all stores. */
26
+ listOrders(args?: Record<string, unknown>): Promise<Record<string, unknown>>;
27
+ /** A single order by id. */
28
+ getOrder(id: string): Promise<Record<string, unknown>>;
29
+ /** The buyer's returns. */
30
+ listReturns(): Promise<Record<string, unknown>>;
31
+ /** The buyer's on-chain wallet balances. */
32
+ getWalletBalances(): Promise<Record<string, unknown>>;
33
+ /** The buyer's available coupons. */
34
+ listCoupons(): Promise<Record<string, unknown>>;
35
+ /** The buyer's gift cards. */
36
+ listGifts(): Promise<Record<string, unknown>>;
37
+ /** Claims a gift by code. */
38
+ claimGift(args: Record<string, unknown>): Promise<Record<string, unknown>>;
39
+ /** Escape hatch: call any account tool by name (future-proof). */
40
+ call(name: string, args?: Record<string, unknown>): Promise<Record<string, unknown>>;
41
+ };
42
+ /** The handler object returned by {@link createAccountHandlers}. */
43
+ export type AccountHandlers = ReturnType<typeof createAccountHandlers>;
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Typed convenience wrappers over the buyer-account MCP tools (`POST
3
+ * /account/mcp`). Every call is **buyer-bound**: the {@link UcpClient} must carry
4
+ * a buyer OAuth bearer (`buyerToken`) or the API answers 401 (relayed as a
5
+ * {@link import('../ucp/client.js').UcpAuthError}). The buyer is always resolved
6
+ * server-side from the bearer — never passed in args. Each method returns the
7
+ * tool's `structuredContent` payload (a plain JSON resource, possibly a UCP
8
+ * business-error envelope inspectable via `isUcpError`).
9
+ *
10
+ * Mirrors {@link import('../checkout/handlers.js').createCheckoutHandlers}: pure
11
+ * handlers over an injected client, so a host unit-tests them with a fake.
12
+ */
13
+ export function createAccountHandlers(deps) {
14
+ const { client } = deps;
15
+ return {
16
+ /** Lists the buyer-account tools the API advertises (`tools/list`). */
17
+ listTools() {
18
+ return client.listAccountTools();
19
+ },
20
+ /** The buyer's profile + session context. */
21
+ getBuyerContext() {
22
+ return client.callAccountTool('get_buyer_context', {});
23
+ },
24
+ /** The buyer's saved shipping/billing addresses. */
25
+ listAddresses() {
26
+ return client.callAccountTool('list_my_addresses', {});
27
+ },
28
+ /** The buyer's orders across all stores. */
29
+ listOrders(args = {}) {
30
+ return client.callAccountTool('list_my_orders', args);
31
+ },
32
+ /** A single order by id. */
33
+ getOrder(id) {
34
+ return client.callAccountTool('get_my_order', { id });
35
+ },
36
+ /** The buyer's returns. */
37
+ listReturns() {
38
+ return client.callAccountTool('list_my_returns', {});
39
+ },
40
+ /** The buyer's on-chain wallet balances. */
41
+ getWalletBalances() {
42
+ return client.callAccountTool('get_wallet_balances', {});
43
+ },
44
+ /** The buyer's available coupons. */
45
+ listCoupons() {
46
+ return client.callAccountTool('list_my_coupons', {});
47
+ },
48
+ /** The buyer's gift cards. */
49
+ listGifts() {
50
+ return client.callAccountTool('list_my_gifts', {});
51
+ },
52
+ /** Claims a gift by code. */
53
+ claimGift(args) {
54
+ return client.callAccountTool('claim_gift', args);
55
+ },
56
+ /** Escape hatch: call any account tool by name (future-proof). */
57
+ call(name, args = {}) {
58
+ return client.callAccountTool(name, args);
59
+ },
60
+ };
61
+ }
62
+ //# sourceMappingURL=handlers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handlers.js","sourceRoot":"","sources":["../../src/account/handlers.ts"],"names":[],"mappings":"AAOA;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,qBAAqB,CAAC,IAAiB;IACrD,MAAM,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IAExB,OAAO;QACL,uEAAuE;QACvE,SAAS;YACP,OAAO,MAAM,CAAC,gBAAgB,EAAE,CAAC;QACnC,CAAC;QAED,6CAA6C;QAC7C,eAAe;YACb,OAAO,MAAM,CAAC,eAAe,CAAC,mBAAmB,EAAE,EAAE,CAAC,CAAC;QACzD,CAAC;QAED,oDAAoD;QACpD,aAAa;YACX,OAAO,MAAM,CAAC,eAAe,CAAC,mBAAmB,EAAE,EAAE,CAAC,CAAC;QACzD,CAAC;QAED,4CAA4C;QAC5C,UAAU,CACR,OAAgC,EAAE;YAElC,OAAO,MAAM,CAAC,eAAe,CAAC,gBAAgB,EAAE,IAAI,CAAC,CAAC;QACxD,CAAC;QAED,4BAA4B;QAC5B,QAAQ,CAAC,EAAU;YACjB,OAAO,MAAM,CAAC,eAAe,CAAC,cAAc,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC;QACxD,CAAC;QAED,2BAA2B;QAC3B,WAAW;YACT,OAAO,MAAM,CAAC,eAAe,CAAC,iBAAiB,EAAE,EAAE,CAAC,CAAC;QACvD,CAAC;QAED,4CAA4C;QAC5C,iBAAiB;YACf,OAAO,MAAM,CAAC,eAAe,CAAC,qBAAqB,EAAE,EAAE,CAAC,CAAC;QAC3D,CAAC;QAED,qCAAqC;QACrC,WAAW;YACT,OAAO,MAAM,CAAC,eAAe,CAAC,iBAAiB,EAAE,EAAE,CAAC,CAAC;QACvD,CAAC;QAED,8BAA8B;QAC9B,SAAS;YACP,OAAO,MAAM,CAAC,eAAe,CAAC,eAAe,EAAE,EAAE,CAAC,CAAC;QACrD,CAAC;QAED,6BAA6B;QAC7B,SAAS,CAAC,IAA6B;YACrC,OAAO,MAAM,CAAC,eAAe,CAAC,YAAY,EAAE,IAAI,CAAC,CAAC;QACpD,CAAC;QAED,kEAAkE;QAClE,IAAI,CACF,IAAY,EACZ,OAAgC,EAAE;YAElC,OAAO,MAAM,CAAC,eAAe,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QAC5C,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Sorted-key canonical JSON serialization, byte-for-byte identical to the
3
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
4
+ * copies in artos-agent / artos-my / artos-storefront. Used to recompute
5
5
  * the bytes a store's `merchant_authorization` (and an AP2 mandate) is signed
6
6
  * over, so a signature verifies across every repo.
7
7
  *
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Sorted-key canonical JSON serialization, byte-for-byte identical to the
3
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
4
+ * copies in artos-agent / artos-my / artos-storefront. Used to recompute
5
5
  * the bytes a store's `merchant_authorization` (and an AP2 mandate) is signed
6
6
  * over, so a signature verifies across every repo.
7
7
  *
package/dist/index.d.ts CHANGED
@@ -2,5 +2,7 @@ export { UCP_VERSION, normalizeBaseUrl, assertEcP256PrivateJwk, parseSuiNetwork,
2
2
  export { UcpClient, UcpClientError, UcpAuthError, messageOf, isUcpError, type UcpResponse, type UcpClientOptions, type UcpAuthMode, type StoreToolOptions, type AccountToolSpec, type ToolAnnotations, } from './ucp/client.js';
3
3
  export { canonicalJson, verifyDetachedJws, verifyMerchantAuthorization, Ap2Signer, type CheckoutMandateClaims, type PaymentMandateClaims, } from './ap2/index.js';
4
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 { createAccountHandlers, type AccountHandlers, type AccountDeps, } from './account/handlers.js';
6
+ export { OAuthClient, OAuthClientError, createPkce, DEFAULT_BUYER_SCOPE, type AuthorizationServerMetadata, type OAuthTokens, type Pkce, type OAuthClientConfig, type OAuthClientOptions, } from './oauth/client.js';
5
7
  export { resolveCryptoDeps, type CryptoDeps } from './crypto/deps.js';
6
8
  export { SuiSigner, SuiSignerError } from './sui/signer.js';
package/dist/index.js CHANGED
@@ -6,6 +6,10 @@ export { UcpClient, UcpClientError, UcpAuthError, messageOf, isUcpError, } from
6
6
  export { canonicalJson, verifyDetachedJws, verifyMerchantAuthorization, Ap2Signer, } from './ap2/index.js';
7
7
  // Checkout orchestration
8
8
  export { createCheckoutHandlers, toMinorUnits, normalizeSearchFilters, availableRails, resolveRail, railKind, grandTotal, currencyOf, asAllowanceId, } from './checkout/index.js';
9
+ // Buyer-account tools
10
+ export { createAccountHandlers, } from './account/handlers.js';
11
+ // Buyer OAuth (PKCE authorize + token exchange/refresh)
12
+ export { OAuthClient, OAuthClientError, createPkce, DEFAULT_BUYER_SCOPE, } from './oauth/client.js';
9
13
  // Crypto rail (requires the optional `@mysten/sui` peer dependency)
10
14
  export { resolveCryptoDeps } from './crypto/deps.js';
11
15
  export { SuiSigner, SuiSignerError } from './sui/signer.js';
package/dist/index.js.map CHANGED
@@ -1 +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"}
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,sBAAsB;AACtB,OAAO,EACL,qBAAqB,GAGtB,MAAM,uBAAuB,CAAC;AAE/B,wDAAwD;AACxD,OAAO,EACL,WAAW,EACX,gBAAgB,EAChB,UAAU,EACV,mBAAmB,GAMpB,MAAM,mBAAmB,CAAC;AAE3B,oEAAoE;AACpE,OAAO,EAAE,iBAAiB,EAAmB,MAAM,kBAAkB,CAAC;AACtE,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC"}
@@ -0,0 +1,96 @@
1
+ /**
2
+ * The buyer-auth scopes a Build-track agent should request: `purchase:complete`
3
+ * to place orders, and `offline_access` so the API mints a rotating refresh
4
+ * token (without it the buyer must re-consent every time the access token
5
+ * expires).
6
+ */
7
+ export declare const DEFAULT_BUYER_SCOPE = "purchase:complete offline_access";
8
+ /** RFC 8414 Authorization Server metadata (the fields this client uses). */
9
+ export interface AuthorizationServerMetadata {
10
+ issuer: string;
11
+ authorization_endpoint: string;
12
+ token_endpoint: string;
13
+ scopes_supported?: string[];
14
+ code_challenge_methods_supported?: string[];
15
+ [k: string]: unknown;
16
+ }
17
+ /** A normalized OAuth token response. */
18
+ export interface OAuthTokens {
19
+ accessToken: string;
20
+ tokenType: string;
21
+ /** Access-token lifetime in seconds. */
22
+ expiresIn: number;
23
+ /** Present only when `offline_access` was granted. */
24
+ refreshToken?: string;
25
+ scope?: string;
26
+ }
27
+ /** A PKCE (RFC 7636) verifier/challenge pair (S256). */
28
+ export interface Pkce {
29
+ verifier: string;
30
+ challenge: string;
31
+ method: 'S256';
32
+ }
33
+ /** Raised for OAuth discovery / token errors (carries the RFC 6749 `error`). */
34
+ export declare class OAuthClientError extends Error {
35
+ readonly error: string;
36
+ constructor(error: string, description?: string);
37
+ }
38
+ type FetchLike = typeof fetch;
39
+ /**
40
+ * Generates a PKCE pair: a high-entropy `verifier` and its S256 `challenge`. Send
41
+ * the challenge on the authorize request; keep the verifier to redeem the code.
42
+ */
43
+ export declare function createPkce(): Pkce;
44
+ export interface OAuthClientConfig {
45
+ /** Artos API base URL / OAuth issuer origin, e.g. `https://api.artos.sh`. */
46
+ artosBaseUrl: string;
47
+ }
48
+ export interface OAuthClientOptions {
49
+ fetch?: FetchLike;
50
+ }
51
+ /**
52
+ * Drives the Artos buyer-OAuth flow (RFC 6749/7636/8414) for a **Build-track**
53
+ * agent that runs sign-in itself (instead of relying on the hosted Connect
54
+ * bridge / Claude). It discovers the Authorization Server metadata, builds the
55
+ * PKCE authorize URL, and exchanges/refreshes tokens against the token endpoint.
56
+ * AS metadata is fetched once and cached for the client's lifetime.
57
+ */
58
+ export declare class OAuthClient {
59
+ private readonly fetchImpl;
60
+ private readonly baseUrl;
61
+ private cached?;
62
+ constructor(cfg: OAuthClientConfig, options?: OAuthClientOptions);
63
+ /** Fetches RFC 8414 AS metadata (`/.well-known/oauth-authorization-server`). */
64
+ discover(): Promise<AuthorizationServerMetadata>;
65
+ /** Cached AS metadata (discovered on first use). */
66
+ metadata(): Promise<AuthorizationServerMetadata>;
67
+ /**
68
+ * Builds the PKCE authorize URL the buyer opens to sign in + consent. Returns
69
+ * the URL and the `state` (verify it on the redirect to prevent CSRF). Pair
70
+ * with a {@link createPkce} `challenge`; keep the matching `verifier`.
71
+ */
72
+ authorizeUrl(params: {
73
+ clientId: string;
74
+ redirectUri: string;
75
+ codeChallenge: string;
76
+ scope?: string;
77
+ state?: string;
78
+ }): Promise<{
79
+ url: string;
80
+ state: string;
81
+ }>;
82
+ /** Redeems an authorization code (+ PKCE verifier) for tokens. */
83
+ exchangeCode(params: {
84
+ code: string;
85
+ codeVerifier: string;
86
+ clientId: string;
87
+ redirectUri: string;
88
+ }): Promise<OAuthTokens>;
89
+ /** Exchanges a refresh token for a fresh access token (rotates the refresh). */
90
+ refresh(params: {
91
+ refreshToken: string;
92
+ clientId?: string;
93
+ }): Promise<OAuthTokens>;
94
+ private tokenRequest;
95
+ }
96
+ export {};
@@ -0,0 +1,123 @@
1
+ import { createHash, randomBytes, randomUUID } from 'node:crypto';
2
+ import { normalizeBaseUrl } from '../config.js';
3
+ /**
4
+ * The buyer-auth scopes a Build-track agent should request: `purchase:complete`
5
+ * to place orders, and `offline_access` so the API mints a rotating refresh
6
+ * token (without it the buyer must re-consent every time the access token
7
+ * expires).
8
+ */
9
+ export const DEFAULT_BUYER_SCOPE = 'purchase:complete offline_access';
10
+ /** Raised for OAuth discovery / token errors (carries the RFC 6749 `error`). */
11
+ export class OAuthClientError extends Error {
12
+ error;
13
+ constructor(error, description) {
14
+ super(description ?? error);
15
+ this.error = error;
16
+ this.name = 'OAuthClientError';
17
+ }
18
+ }
19
+ function base64url(buf) {
20
+ return buf.toString('base64url');
21
+ }
22
+ /**
23
+ * Generates a PKCE pair: a high-entropy `verifier` and its S256 `challenge`. Send
24
+ * the challenge on the authorize request; keep the verifier to redeem the code.
25
+ */
26
+ export function createPkce() {
27
+ const verifier = base64url(randomBytes(32));
28
+ const challenge = base64url(createHash('sha256').update(verifier).digest());
29
+ return { verifier, challenge, method: 'S256' };
30
+ }
31
+ /**
32
+ * Drives the Artos buyer-OAuth flow (RFC 6749/7636/8414) for a **Build-track**
33
+ * agent that runs sign-in itself (instead of relying on the hosted Connect
34
+ * bridge / Claude). It discovers the Authorization Server metadata, builds the
35
+ * PKCE authorize URL, and exchanges/refreshes tokens against the token endpoint.
36
+ * AS metadata is fetched once and cached for the client's lifetime.
37
+ */
38
+ export class OAuthClient {
39
+ fetchImpl;
40
+ baseUrl;
41
+ cached;
42
+ constructor(cfg, options = {}) {
43
+ this.baseUrl = normalizeBaseUrl(cfg.artosBaseUrl);
44
+ this.fetchImpl = options.fetch ?? fetch;
45
+ }
46
+ /** Fetches RFC 8414 AS metadata (`/.well-known/oauth-authorization-server`). */
47
+ async discover() {
48
+ const res = await this.fetchImpl(`${this.baseUrl}/.well-known/oauth-authorization-server`, { headers: { Accept: 'application/json' } });
49
+ if (!res.ok) {
50
+ throw new OAuthClientError('discovery_failed', `Authorization Server metadata fetch failed (${res.status}).`);
51
+ }
52
+ const meta = (await res.json());
53
+ if (!meta.authorization_endpoint || !meta.token_endpoint) {
54
+ throw new OAuthClientError('discovery_failed', 'Authorization Server metadata is missing authorize/token endpoints.');
55
+ }
56
+ return meta;
57
+ }
58
+ /** Cached AS metadata (discovered on first use). */
59
+ async metadata() {
60
+ return (this.cached ??= await this.discover());
61
+ }
62
+ /**
63
+ * Builds the PKCE authorize URL the buyer opens to sign in + consent. Returns
64
+ * the URL and the `state` (verify it on the redirect to prevent CSRF). Pair
65
+ * with a {@link createPkce} `challenge`; keep the matching `verifier`.
66
+ */
67
+ async authorizeUrl(params) {
68
+ const meta = await this.metadata();
69
+ const state = params.state ?? randomUUID();
70
+ const url = new URL(meta.authorization_endpoint);
71
+ url.searchParams.set('response_type', 'code');
72
+ url.searchParams.set('client_id', params.clientId);
73
+ url.searchParams.set('redirect_uri', params.redirectUri);
74
+ url.searchParams.set('code_challenge', params.codeChallenge);
75
+ url.searchParams.set('code_challenge_method', 'S256');
76
+ url.searchParams.set('scope', params.scope ?? DEFAULT_BUYER_SCOPE);
77
+ url.searchParams.set('state', state);
78
+ return { url: url.toString(), state };
79
+ }
80
+ /** Redeems an authorization code (+ PKCE verifier) for tokens. */
81
+ async exchangeCode(params) {
82
+ return this.tokenRequest({
83
+ grant_type: 'authorization_code',
84
+ code: params.code,
85
+ code_verifier: params.codeVerifier,
86
+ client_id: params.clientId,
87
+ redirect_uri: params.redirectUri,
88
+ });
89
+ }
90
+ /** Exchanges a refresh token for a fresh access token (rotates the refresh). */
91
+ async refresh(params) {
92
+ const body = {
93
+ grant_type: 'refresh_token',
94
+ refresh_token: params.refreshToken,
95
+ };
96
+ if (params.clientId)
97
+ body.client_id = params.clientId;
98
+ return this.tokenRequest(body);
99
+ }
100
+ async tokenRequest(body) {
101
+ const meta = await this.metadata();
102
+ const res = await this.fetchImpl(meta.token_endpoint, {
103
+ method: 'POST',
104
+ headers: {
105
+ 'Content-Type': 'application/x-www-form-urlencoded',
106
+ Accept: 'application/json',
107
+ },
108
+ body: new URLSearchParams(body).toString(),
109
+ });
110
+ const json = (await res.json());
111
+ if (!res.ok || !json.access_token) {
112
+ throw new OAuthClientError(json.error ?? 'token_error', json.error_description ?? `Token request failed (${res.status}).`);
113
+ }
114
+ return {
115
+ accessToken: json.access_token,
116
+ tokenType: json.token_type ?? 'Bearer',
117
+ expiresIn: json.expires_in ?? 0,
118
+ refreshToken: json.refresh_token,
119
+ scope: json.scope,
120
+ };
121
+ }
122
+ }
123
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/oauth/client.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@artos-commerce/ucp-client",
3
- "version": "0.1.1",
4
- "description": "Path B Direct UCP client for Artos: typed UCP transport, AP2 mandate signing, and checkout orchestration so you can build your own commerce-grade UCP client without hand-rolling the envelope, signing, and credential rules.",
3
+ "version": "0.2.0",
4
+ "description": "Build-track Direct UCP client for Artos: typed UCP transport, AP2 mandate signing, checkout orchestration, buyer-account tools, and buyer OAuth so you can build your own commerce-grade UCP client without hand-rolling the envelope, signing, and credential rules.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "engines": {
@@ -17,6 +17,8 @@
17
17
  "./ucp": "./dist/ucp/client.js",
18
18
  "./ap2": "./dist/ap2/index.js",
19
19
  "./checkout": "./dist/checkout/index.js",
20
+ "./account": "./dist/account/handlers.js",
21
+ "./oauth": "./dist/oauth/client.js",
20
22
  "./sui": "./dist/sui/signer.js",
21
23
  "./crypto": "./dist/crypto/deps.js"
22
24
  },