@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 +59 -10
- package/dist/account/handlers.d.ts +43 -0
- package/dist/account/handlers.js +62 -0
- package/dist/account/handlers.js.map +1 -0
- package/dist/ap2/canonical-json.d.ts +1 -1
- package/dist/ap2/canonical-json.js +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/oauth/client.d.ts +96 -0
- package/dist/oauth/client.js +123 -0
- package/dist/oauth/client.js.map +1 -0
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
# @artos-commerce/ucp-client
|
|
2
2
|
|
|
3
|
-
**
|
|
4
|
-
signing,
|
|
5
|
-
client against `api.artos.sh` — without
|
|
6
|
-
canonical-JSON signing bytes, the AP2
|
|
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
|
|
11
|
-
`@artos-commerce/ucp-client` when you are building your **own**
|
|
12
|
-
(**
|
|
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-
|
|
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-
|
|
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.
|
|
4
|
-
"description": "
|
|
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
|
},
|