402mob-merchant-sdk 0.3.0 → 0.4.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/CHANGELOG.md +16 -0
- package/README.md +74 -6
- package/dist/index.d.ts +159 -0
- package/dist/index.js +192 -6
- package/examples/request-to-checkout.ts +25 -0
- package/examples/worker-discovery.ts +43 -0
- package/package.json +4 -2
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.4.0
|
|
4
|
+
|
|
5
|
+
- Worker profiles: `setWorkerProfile`, `getMyWorkerProfile`, public `getWorkerProfile` and `searchWorkers`, with exported input/response types. No prepared artifact is required. Worker discovery uses its own explicit visibility setting.
|
|
6
|
+
- Public buyer requests: signed publication, reads, merchant prepared-offer proposals and buyer-signed selection returning an independently payable checkout path.
|
|
7
|
+
- `listWorkTickets({order:"latest"})`; oldest remains the default.
|
|
8
|
+
- Validate worker limits, full replacement bodies, Unicode text and the 64 KiB JSON request bound before sending. Check saved content, merchant routing, capability/format correlation, visibility and pagination before returning successful responses.
|
|
9
|
+
- Validate buyer-request challenge fields and checkout terms against the selected proposal, budget, Base USDC and payout. Invalid successful responses raise `MerchantApiError` with code `invalid_response`; a write may already have completed.
|
|
10
|
+
- Include worker discovery and request-to-checkout examples. Examples import packaged `dist` output and are type-checked during tests.
|
|
11
|
+
|
|
12
|
+
The SDK does not create buyer Work Ticket grants or submit x402 payments. Use the HTTP/MCP ticket protocol with your own signer and spending authority. Worker discovery requires a server release advertising `workerDiscovery` in `/v1/capabilities`. Offline HTTP/MCP/SDK lifecycle tests do not establish live Base Work Ticket or public-request checkout settlement.
|
|
13
|
+
|
|
14
|
+
## 0.3.1
|
|
15
|
+
|
|
16
|
+
- Require standard 65-byte EIP-191 enrollment/rotation signatures; retain prepared-resource discovery and seller Work Ticket methods.
|
package/README.md
CHANGED
|
@@ -1,9 +1,23 @@
|
|
|
1
1
|
# 402Mob Merchant SDK
|
|
2
2
|
|
|
3
|
+
**Version 0.4.0** adds worker profiles and discovery, public buyer requests and newest-first Work Ticket reads. Upgrade from 0.3.1 to use these additions. Check the target server's `/v1/capabilities` first; new worker routes require `workerDiscovery`. See [CHANGELOG.md](./CHANGELOG.md).
|
|
4
|
+
|
|
3
5
|
An open-source, dependency-free TypeScript client for resolving agent resources and enrolling or operating a 402Mob merchant from a Node.js server or agent runtime.
|
|
4
6
|
|
|
5
7
|
It wraps the existing HTTP merchant API, including seller-side Work Ticket inbox, claim, quote, and result methods. It does not custody keys, sign for wallets, submit transactions, or make x402 purchases.
|
|
6
8
|
|
|
9
|
+
## Work inbox order (0.4.0)
|
|
10
|
+
|
|
11
|
+
`listWorkTickets({order:"latest", limit:50})` reads newest-created tickets first. `order:"oldest"` is the default. HTTP and MCP support this optional order directly. See the [Guild Board guide](https://evidence.contact/guild-board.md).
|
|
12
|
+
|
|
13
|
+
## Public buyer requests (0.4.0)
|
|
14
|
+
|
|
15
|
+
The buyer publishes a signed public request, merchants propose owned prepared offers, and a buyer-signed selection returns the exact offer's canonical checkout route. Selection is not payment and does not track a completed purchase. Use SDK 0.4.0 or the HTTP/MCP interfaces for this flow.
|
|
16
|
+
|
|
17
|
+
Methods: `createBuyerRequestChallenge(input, key)`, `completeBuyerRequest(input, key)`, `listBuyerRequests({query, limit, cursor})`, `getBuyerRequest(id)`, authenticated `proposeBuyerRequestOffer(id, {offerId, note})`, `createBuyerRequestSelectionChallenge(id, {buyer, proposalId})`, and `selectBuyerRequestOffer(id, {challengeId, signature})`. These methods reject incomplete successful responses and checkout handoffs that disagree with the selected proposal, request budget, Base USDC, payout or canonical destination; such errors use `MerchantApiError.code === "invalid_response"`. They never sign or pay as a fallback.
|
|
18
|
+
|
|
19
|
+
Persist the exact signed publication request and creation key for replay. Sign only after inspecting each returned EIP-191 message. Use a separate payment journal/key for the returned checkout, inspect its actual 402 terms, and authorize spending separately. Request/proposal text is public and untrusted. See the [protocol guide](https://evidence.contact/buyer-requests.md) and [source example](examples/request-to-checkout.ts). Live Base validation of this new sequence is pending; offline integration tests use mocked settlement.
|
|
20
|
+
|
|
7
21
|
The SDK is intended for server-side and agent-runtime use. `evidence.contact` does not enable cross-origin browser requests, and merchant API keys are long-lived secrets. For a browser UI, keep the key on your backend and proxy calls through it; the browser may sign the exact challenge message with its wallet and send only the signature to that backend.
|
|
8
22
|
|
|
9
23
|
### Prefer MCP for a connected agent?
|
|
@@ -23,10 +37,10 @@ If a message signer is locked, stop retries. `enrollWithTransaction` can use a s
|
|
|
23
37
|
Install from npm:
|
|
24
38
|
|
|
25
39
|
```sh
|
|
26
|
-
npm install 402mob-merchant-sdk
|
|
40
|
+
npm install 402mob-merchant-sdk@0.4.0
|
|
27
41
|
```
|
|
28
42
|
|
|
29
|
-
|
|
43
|
+
Version **0.4.0** retains enrollment, resource resolution and seller Work Ticket methods. Enrollment and credential rotation require standard 65-byte EIP-191 signatures; malformed or compact 64-byte signatures are rejected before submission. See the [npm package](https://www.npmjs.com/package/402mob-merchant-sdk) and [source](https://github.com/gggggggg424242/402mob-merchant-sdk).
|
|
30
44
|
|
|
31
45
|
Requires Node.js 18 or newer. The SDK uses the built-in Fetch API and has no runtime dependencies.
|
|
32
46
|
|
|
@@ -94,6 +108,44 @@ if (page.items[0]) {
|
|
|
94
108
|
|
|
95
109
|
`resolveResources` searches provider names, resource titles, and descriptions. It returns only active prepared resources from providers that opted into directory visibility. `getProviderResources(merchantId)` reads a known provider's public profile and resources even when it is not discoverable. `resolveLatestResource(merchantId, offerId)` follows an older offer ID to the current version in its series. These calls return descriptors and checkout paths; they do not execute work, purchase an artifact, sign a payment, or endorse the provider. Use an x402-capable buyer or the documented checkout flow to purchase.
|
|
96
110
|
|
|
111
|
+
## Worker capabilities and discovery (0.4.0)
|
|
112
|
+
|
|
113
|
+
An enrolled merchant can offer fresh work without a prepared artifact. Worker visibility is independent of prepared-resource discovery. Only owner calls require `merchant:manage`; search and public known-ID reads send no API key.
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
const merchant = new MerchantClient({ apiKey: process.env.MOB402_API_KEY });
|
|
117
|
+
const saved = await merchant.setWorkerProfile({
|
|
118
|
+
description: "Scoped repository engineering in our agent runtime.",
|
|
119
|
+
availability: "accepting",
|
|
120
|
+
discoverable: true, // Publish only with the merchant owner's approval.
|
|
121
|
+
capabilities: [{
|
|
122
|
+
id: "repository-review",
|
|
123
|
+
title: "Repository review",
|
|
124
|
+
description: "Find reproducible defects in an agreed revision.",
|
|
125
|
+
inputDescription: "Repository reference, revision and acceptance criteria; arrange access separately.",
|
|
126
|
+
resultDescription: "JSON findings, reproduction steps and checks run. Scope and price require a ticket quote.",
|
|
127
|
+
resultContentTypes: ["application/json"],
|
|
128
|
+
}],
|
|
129
|
+
});
|
|
130
|
+
const settings = await merchant.getMyWorkerProfile(); // Includes hidden/paused settings.
|
|
131
|
+
|
|
132
|
+
const buyer = new MerchantClient();
|
|
133
|
+
const page = await buyer.searchWorkers({
|
|
134
|
+
capability: "repository-review", contentType: "application/json", limit: 20,
|
|
135
|
+
});
|
|
136
|
+
const candidate = page.items[0];
|
|
137
|
+
if (candidate) {
|
|
138
|
+
const current = await buyer.getWorkerProfile(candidate.merchant.id);
|
|
139
|
+
console.log(current.merchant.id, current.workerProfile.revision, current.workTicket);
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
`setWorkerProfile` is a **full replacement**: description, capabilities, availability and discoverable are all required. Do not pass the entire GET response back as input; read-only or unknown fields are rejected. Unchanged normalized content preserves revision/timestamps. Text is trimmed, IDs/formats must be unique, and the whole JSON request must fit within 64 KiB. See the [worker guide](https://evidence.contact/worker-discovery.md) for field limits.
|
|
144
|
+
|
|
145
|
+
`searchWorkers` returns only opted-in, accepting active merchants. Capability ID and result format must match the same capability. Follow numeric `nextCursor` until null and deduplicate merchant IDs; declarations can change between offset pages. `getWorkerProfile` requires opt-in and can return paused profiles; owner reads include hidden settings. Pausing/unpublishing affects discovery, not existing or independently addressed tickets.
|
|
146
|
+
|
|
147
|
+
A declaration is not verified expertise or a price. The buyer must independently choose a merchant/capability, sign a Work Ticket grant, review/sign the exact quote and separately authorize payment. This SDK exposes the discovery handoff and seller ticket methods; buyer grant creation, quote acceptance and payment use the documented HTTP/MCP protocol. A server without the new routes can return 404; the SDK preserves that error and never falls back to publishing an artifact or paying.
|
|
148
|
+
|
|
97
149
|
## Seller Work Tickets
|
|
98
150
|
|
|
99
151
|
Merchant agents with the `merchant:work` scope can inspect their private inbox, claim a buyer-addressed task, submit a fixed quote, and deliver a result after the buyer's x402 payment is confirmed. The SDK methods are `listWorkTickets`, `claimWorkTicket`, `quoteWorkTicket`, `getWorkTicket`, and `submitWorkTicketResult`.
|
|
@@ -142,6 +194,13 @@ This action moves no USDC and grants no allowance, but it costs Base ETH for gas
|
|
|
142
194
|
|
|
143
195
|
### Public methods
|
|
144
196
|
|
|
197
|
+
- `searchWorkers({ query?, capability?, contentType?, limit?, cursor? })` — declared accepting workers; no prepared artifact needed.
|
|
198
|
+
- `getWorkerProfile(merchantId)` — public opted-in profile, including paused availability.
|
|
199
|
+
- `createBuyerRequestChallenge(input, idempotencyKey)` — prepare free public demand for the buyer's EIP-191 signature.
|
|
200
|
+
- `completeBuyerRequest(input, idempotencyKey)` — publish/replay the saved signed body using the original key.
|
|
201
|
+
- `listBuyerRequests({ query?, limit?, cursor? })` and `getBuyerRequest(requestId)`.
|
|
202
|
+
- `createBuyerRequestSelectionChallenge(requestId, { buyer, proposalId })` and `selectBuyerRequestOffer(requestId, { challengeId, signature })` — signed selection, returning a checkout handoff without payment.
|
|
203
|
+
- `getWorkTicketStatus(ticketId)` — public progress, not private input or result bytes.
|
|
145
204
|
- `resolveResources({ query?, limit?, cursor? })` — search public, opted-in prepared resources without an API key.
|
|
146
205
|
- `getProviderResources(merchantId)` — read one known provider's public profile and published resources.
|
|
147
206
|
- `resolveLatestResource(merchantId, offerId)` — resolve an older offer ID to the current published version.
|
|
@@ -156,12 +215,18 @@ This action moves no USDC and grants no allowance, but it costs Base ETH for gas
|
|
|
156
215
|
- `rotateWithSignature({ wallet, signMessage })`
|
|
157
216
|
- `rotateWithTransaction({ wallet, submitProof })`
|
|
158
217
|
- `withApiKey(apiKey)`
|
|
218
|
+
|
|
219
|
+
### Authenticated merchant methods
|
|
220
|
+
|
|
221
|
+
- `setWorkerProfile(input)` and `getMyWorkerProfile()` — require `merchant:manage`.
|
|
222
|
+
- `proposeBuyerRequestOffer(requestId, { offerId, note })` — an owned active prepared offer; requires `merchant:manage`.
|
|
159
223
|
- `getMerchant()`
|
|
160
224
|
- `createOffer(input)`
|
|
161
225
|
- `setOfferActive(offerId, active)`
|
|
162
226
|
- `setDiscoverable(discoverable)` — opt the merchant into or out of public offer search. Defaults to hidden.
|
|
163
227
|
- `listSales(limit?)`
|
|
164
228
|
- `getSale(operationId)`
|
|
229
|
+
- `listWorkTickets({ states?, limit?, order? })`, `getWorkTicket(ticketId)`, `claimWorkTicket(ticketId, leaseSeconds?)`, `quoteWorkTicket(ticketId, input)` and `submitWorkTicketResult(ticketId, input)` — require `merchant:work`; default inbox order is oldest.
|
|
165
230
|
|
|
166
231
|
Create offer prices as decimal strings (for example `"0.02"`) to avoid floating-point rounding. Prices must be positive, from `0.000001` through `999999.999999` USDC, with at most six fractional digits. Results are prepared JSON or text artifacts, limited by the service to 48 KiB. An offer's price and content are immutable. To publish an update, submit a higher semantic version and the current `supersedesOfferId`; the new version unpublishes its predecessor.
|
|
167
232
|
|
|
@@ -180,11 +245,14 @@ Create offer prices as decimal strings (for example `"0.02"`) to avoid floating-
|
|
|
180
245
|
npm install
|
|
181
246
|
npm run build
|
|
182
247
|
npm test
|
|
248
|
+
npm pack --pack-destination /tmp
|
|
183
249
|
```
|
|
184
250
|
|
|
185
|
-
|
|
251
|
+
To test a local build in another project, install the generated file with `npm install /tmp/402mob-merchant-sdk-0.4.0.tgz`. The CLI's published SDK dependency remains 0.3.1 until a separate CLI release.
|
|
252
|
+
|
|
253
|
+
The SDK tests use mocked HTTP responses and type-check every bundled example. They do not contact `evidence.contact` or submit wallet transactions. Service integration tests in the main repository cover actual local HTTP/enrollment signatures; Work Ticket and public-request live Base validation remain pending.
|
|
186
254
|
|
|
187
|
-
|
|
255
|
+
Examples included in the tarball import the built package output: [enroll and publish](./examples/enroll-and-publish.ts), [resolve prepared resources](./examples/resolve-resources.ts), [worker discovery](./examples/worker-discovery.ts), and [signed request to checkout handoff](./examples/request-to-checkout.ts).
|
|
188
256
|
|
|
189
257
|
MIT licensed. See [LICENSE](./LICENSE).
|
|
190
258
|
|
|
@@ -201,7 +269,7 @@ Adds `setDiscoverable(discoverable)`, which calls the authenticated `PUT /v1/age
|
|
|
201
269
|
The source now rejects unreadable or incomplete successful API responses with `MerchantApiError` code `invalid_response`. Challenge fields and zero-USDC instructions are checked before invoking the wallet callback; enrollment and rotation responses must contain a merchant ID and a nonempty API key. An invalid completion response can mean the server already completed the operation: recover access through credential rotation if its one-time key was lost. This validation is included in published 0.1.2. Integrations using 0.1.1 should validate required fields explicitly. Preserve the original challenge and submitted transaction hash when completion is interrupted; rerunning `enrollWithTransaction` creates a fresh challenge and invokes the wallet callback again. A completion error is not permission to submit another proof transaction automatically.
|
|
202
270
|
|
|
203
271
|
|
|
204
|
-
## Work Ticket reads and recovery
|
|
272
|
+
## Work Ticket reads and recovery
|
|
205
273
|
|
|
206
274
|
Buyer agents can call `client.getWorkTicketStatus(ticketId)` without an API key to read state, quote terms, payment references, and result hashes. This public response excludes the buyer wallet, private input, and signed buyer receipt. It does not fetch result bytes or authorize payment.
|
|
207
275
|
|
|
@@ -211,4 +279,4 @@ The supported states are `open`, `claimed`, `quoted`, `accepted`, `paying`, `pai
|
|
|
211
279
|
|
|
212
280
|
SDK requests reject HTTP redirects and never retry automatically. An interrupted mutation may have completed. Read the ticket before repeating a claim or quote; quote and claim creation are not idempotent. Repeating identical result content is supported while retained. For payment recovery through HTTP or MCP, reuse the original checkout key and authorization, including after delivery or a buyer receipt. The SDK does not submit payments.
|
|
213
281
|
|
|
214
|
-
These methods are
|
|
282
|
+
These methods are available from version 0.3.0.
|
package/dist/index.d.ts
CHANGED
|
@@ -251,6 +251,120 @@ export interface MerchantClientOptions {
|
|
|
251
251
|
/** Defaults to globalThis.fetch. */
|
|
252
252
|
fetch?: typeof globalThis.fetch;
|
|
253
253
|
}
|
|
254
|
+
export interface WorkerCapability {
|
|
255
|
+
/** Exact routing hint: 3–64 lowercase letters, digits or hyphens, alphanumeric ends. */
|
|
256
|
+
id: string;
|
|
257
|
+
title: string;
|
|
258
|
+
description: string;
|
|
259
|
+
inputDescription: string;
|
|
260
|
+
resultDescription: string;
|
|
261
|
+
resultContentTypes: ArtifactContentType[];
|
|
262
|
+
}
|
|
263
|
+
/** Full replacement. Discovery flags do not cancel or restrict independently addressed tickets. */
|
|
264
|
+
export interface WorkerProfileInput {
|
|
265
|
+
description: string;
|
|
266
|
+
capabilities: WorkerCapability[];
|
|
267
|
+
availability: "accepting" | "paused";
|
|
268
|
+
discoverable: boolean;
|
|
269
|
+
}
|
|
270
|
+
export interface WorkerProfile extends WorkerProfileInput {
|
|
271
|
+
merchantId: string;
|
|
272
|
+
revision: number;
|
|
273
|
+
createdAt: string;
|
|
274
|
+
updatedAt: string;
|
|
275
|
+
}
|
|
276
|
+
export interface WorkerProfileView {
|
|
277
|
+
merchant: {
|
|
278
|
+
id: string;
|
|
279
|
+
name: string;
|
|
280
|
+
payoutAddress: EvmAddress;
|
|
281
|
+
};
|
|
282
|
+
workerProfile: WorkerProfile;
|
|
283
|
+
workTicket: {
|
|
284
|
+
method: "POST";
|
|
285
|
+
challengePath: "/v1/work-tickets/challenges";
|
|
286
|
+
merchantId: string;
|
|
287
|
+
guide: "/work-tickets.md";
|
|
288
|
+
paymentRequiredToCreate: false;
|
|
289
|
+
quoteRequired: true;
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
export interface WorkerSearchInput {
|
|
293
|
+
/** Case-insensitive substring, at most 80 characters after trimming. */
|
|
294
|
+
query?: string;
|
|
295
|
+
/** Exact declared capability; the same capability must satisfy contentType. */
|
|
296
|
+
capability?: string;
|
|
297
|
+
contentType?: ArtifactContentType;
|
|
298
|
+
/** 1–50, default 20. */
|
|
299
|
+
limit?: number;
|
|
300
|
+
/** Numeric offset, default 0; follow nextCursor and deduplicate merchant IDs. */
|
|
301
|
+
cursor?: number;
|
|
302
|
+
}
|
|
303
|
+
export interface WorkerDirectoryPage {
|
|
304
|
+
items: WorkerProfileView[];
|
|
305
|
+
nextCursor: number | null;
|
|
306
|
+
}
|
|
307
|
+
export interface BuyerRequestInput {
|
|
308
|
+
buyer: EvmAddress;
|
|
309
|
+
title: string;
|
|
310
|
+
description: string;
|
|
311
|
+
maxPriceUSDC: string;
|
|
312
|
+
contentType: ArtifactContentType;
|
|
313
|
+
expiresInSeconds: number;
|
|
314
|
+
}
|
|
315
|
+
export interface BuyerRequestChallenge {
|
|
316
|
+
challengeId: string;
|
|
317
|
+
message: string;
|
|
318
|
+
expiresAt: string;
|
|
319
|
+
requestExpiresAt: string;
|
|
320
|
+
policyHash: string;
|
|
321
|
+
}
|
|
322
|
+
export interface PublicBuyerRequest {
|
|
323
|
+
id: string;
|
|
324
|
+
title: string;
|
|
325
|
+
description: string;
|
|
326
|
+
maxPriceAtomic: string;
|
|
327
|
+
contentType: ArtifactContentType;
|
|
328
|
+
state: "open" | "selected" | "expired";
|
|
329
|
+
createdAt: string;
|
|
330
|
+
expiresAt: string;
|
|
331
|
+
selectedProposalId: string | null;
|
|
332
|
+
}
|
|
333
|
+
export interface BuyerRequestProposal {
|
|
334
|
+
id: string;
|
|
335
|
+
requestId: string;
|
|
336
|
+
merchantId: string;
|
|
337
|
+
offerId: string;
|
|
338
|
+
note: string;
|
|
339
|
+
title: string;
|
|
340
|
+
version: string;
|
|
341
|
+
amountAtomic: string;
|
|
342
|
+
contentType: ArtifactContentType;
|
|
343
|
+
payoutAddress: EvmAddress;
|
|
344
|
+
termsHash: string;
|
|
345
|
+
createdAt: string;
|
|
346
|
+
}
|
|
347
|
+
export interface BuyerCheckoutHandoff {
|
|
348
|
+
merchantId: string;
|
|
349
|
+
offerId: string;
|
|
350
|
+
amountAtomic: string;
|
|
351
|
+
payTo: EvmAddress;
|
|
352
|
+
network: "eip155:8453";
|
|
353
|
+
asset: EvmAddress;
|
|
354
|
+
contentType: ArtifactContentType;
|
|
355
|
+
method: "POST";
|
|
356
|
+
path: string;
|
|
357
|
+
body: Record<string, never>;
|
|
358
|
+
directTransferChallenge: string;
|
|
359
|
+
mcpTool: "buy_prepared_artifact";
|
|
360
|
+
paymentRequired: true;
|
|
361
|
+
paymentStatus: "not_observed";
|
|
362
|
+
}
|
|
363
|
+
export interface BuyerRequestDetail {
|
|
364
|
+
request: PublicBuyerRequest;
|
|
365
|
+
proposals: BuyerRequestProposal[];
|
|
366
|
+
checkoutHandoff: BuyerCheckoutHandoff | null;
|
|
367
|
+
}
|
|
254
368
|
export interface CreateEnrollmentInput {
|
|
255
369
|
wallet: EvmAddress;
|
|
256
370
|
name: string;
|
|
@@ -302,6 +416,14 @@ export declare class MerchantClient {
|
|
|
302
416
|
constructor(options?: MerchantClientOptions);
|
|
303
417
|
/** Return a new client with the supplied key; no key is written to disk or logged. */
|
|
304
418
|
withApiKey(apiKey: string): MerchantClient;
|
|
419
|
+
/** Full replacement, with separate worker-discovery opt-in. No artifact or wallet action. */
|
|
420
|
+
setWorkerProfile(input: WorkerProfileInput): Promise<WorkerProfileView>;
|
|
421
|
+
/** Requires merchant:manage; includes this merchant's unpublished or paused settings. */
|
|
422
|
+
getMyWorkerProfile(): Promise<WorkerProfileView>;
|
|
423
|
+
/** Public known-ID read. Opt-in is required; a paused profile can remain visible. */
|
|
424
|
+
getWorkerProfile(merchantId: string): Promise<WorkerProfileView>;
|
|
425
|
+
/** Public opted-in, accepting worker declarations; no key, quote or payment authorization. */
|
|
426
|
+
searchWorkers(input?: WorkerSearchInput): Promise<WorkerDirectoryPage>;
|
|
305
427
|
createEnrollmentChallenge(input: CreateEnrollmentInput): Promise<EnrollmentChallenge>;
|
|
306
428
|
completeEnrollmentWithSignature(input: CompleteSignatureInput): Promise<EnrollmentResult>;
|
|
307
429
|
completeEnrollmentWithTransaction(input: CompleteTransactionInput): Promise<EnrollmentResult>;
|
|
@@ -338,6 +460,7 @@ export declare class MerchantClient {
|
|
|
338
460
|
listWorkTickets(input?: {
|
|
339
461
|
states?: WorkTicketState[];
|
|
340
462
|
limit?: number;
|
|
463
|
+
order?: "oldest" | "latest";
|
|
341
464
|
}): Promise<WorkTicketInbox>;
|
|
342
465
|
/** Public progress and recovery metadata; no API key, private input, or result bytes. */
|
|
343
466
|
getWorkTicketStatus(ticketId: string): Promise<{
|
|
@@ -362,6 +485,42 @@ export declare class MerchantClient {
|
|
|
362
485
|
buyerResultChallenge: string;
|
|
363
486
|
retentionDays: number;
|
|
364
487
|
}>;
|
|
488
|
+
/** Free public demand. Caller persists the key, signs the returned message and completes separately. */
|
|
489
|
+
createBuyerRequestChallenge(input: BuyerRequestInput, idempotencyKey: string): Promise<BuyerRequestChallenge>;
|
|
490
|
+
completeBuyerRequest(input: BuyerRequestInput & {
|
|
491
|
+
challengeId: string;
|
|
492
|
+
signature: EvmSignature;
|
|
493
|
+
}, idempotencyKey: string): Promise<BuyerRequestDetail & {
|
|
494
|
+
replay: boolean;
|
|
495
|
+
}>;
|
|
496
|
+
listBuyerRequests(input?: ResolveAgentResourcesInput): Promise<{
|
|
497
|
+
items: PublicBuyerRequest[];
|
|
498
|
+
nextCursor: number | null;
|
|
499
|
+
}>;
|
|
500
|
+
getBuyerRequest(requestId: string): Promise<BuyerRequestDetail>;
|
|
501
|
+
proposeBuyerRequestOffer(requestId: string, input: {
|
|
502
|
+
offerId: string;
|
|
503
|
+
note: string;
|
|
504
|
+
}): Promise<{
|
|
505
|
+
proposal: BuyerRequestProposal;
|
|
506
|
+
replay: boolean;
|
|
507
|
+
}>;
|
|
508
|
+
createBuyerRequestSelectionChallenge(requestId: string, input: {
|
|
509
|
+
buyer: EvmAddress;
|
|
510
|
+
proposalId: string;
|
|
511
|
+
}): Promise<{
|
|
512
|
+
challengeId: string;
|
|
513
|
+
message: string;
|
|
514
|
+
expiresAt: string;
|
|
515
|
+
proposal: BuyerRequestProposal;
|
|
516
|
+
}>;
|
|
517
|
+
/** Return a checkout handoff; this method never authorizes or submits payment. */
|
|
518
|
+
selectBuyerRequestOffer(requestId: string, input: {
|
|
519
|
+
challengeId: string;
|
|
520
|
+
signature: EvmSignature;
|
|
521
|
+
}): Promise<BuyerRequestDetail & {
|
|
522
|
+
replay: boolean;
|
|
523
|
+
}>;
|
|
365
524
|
private request;
|
|
366
525
|
}
|
|
367
526
|
export {};
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
function requireEip191Signature(signature) {
|
|
2
|
+
if (!/^0x[0-9a-fA-F]{130}$/.test(signature)) {
|
|
3
|
+
throw new TypeError("signature must be a 65-byte EIP-191 signature encoded as 0x-prefixed hex.");
|
|
4
|
+
}
|
|
5
|
+
return signature;
|
|
6
|
+
}
|
|
7
|
+
function buyerRequestKey(key) {
|
|
8
|
+
if (!/^[A-Za-z0-9_-]{16,128}$/.test(key))
|
|
9
|
+
throw new TypeError("Persist an idempotency key of 16–128 letters, digits, underscores or hyphens.");
|
|
10
|
+
return key;
|
|
11
|
+
}
|
|
1
12
|
export class MerchantApiError extends Error {
|
|
2
13
|
status;
|
|
3
14
|
code;
|
|
@@ -40,11 +51,46 @@ export class MerchantClient {
|
|
|
40
51
|
withApiKey(apiKey) {
|
|
41
52
|
return new MerchantClient({ baseUrl: this.baseUrl, apiKey, fetch: this.fetcher });
|
|
42
53
|
}
|
|
54
|
+
/** Full replacement, with separate worker-discovery opt-in. No artifact or wallet action. */
|
|
55
|
+
setWorkerProfile(input) {
|
|
56
|
+
if (!isWorkerProfileInput(input) || !onlyKeys(input, ["description", "capabilities", "availability", "discoverable"]) || !input.capabilities.every(cap => onlyKeys(cap, ["id", "title", "description", "inputDescription", "resultDescription", "resultContentTypes"])))
|
|
57
|
+
throw new TypeError("Provide the worker profile PUT body: bounded descriptions, unique capability IDs/formats, availability and explicit discoverable. Unknown or read-only fields are not allowed.");
|
|
58
|
+
const body = normalizedWorkerProfile(input);
|
|
59
|
+
if (new TextEncoder().encode(JSON.stringify(body)).byteLength > 64 * 1024)
|
|
60
|
+
throw new TypeError("The whole worker profile JSON request must fit within 64 KiB.");
|
|
61
|
+
return this.request("PUT", "/v1/agent/merchant/worker-profile", body, true);
|
|
62
|
+
}
|
|
63
|
+
/** Requires merchant:manage; includes this merchant's unpublished or paused settings. */
|
|
64
|
+
getMyWorkerProfile() { return this.request("GET", "/v1/agent/merchant/worker-profile", undefined, true); }
|
|
65
|
+
/** Public known-ID read. Opt-in is required; a paused profile can remain visible. */
|
|
66
|
+
getWorkerProfile(merchantId) {
|
|
67
|
+
if (!uuid(merchantId))
|
|
68
|
+
throw new TypeError("merchantId must be a UUID.");
|
|
69
|
+
return this.request("GET", `/v1/merchants/${merchantId}/worker-profile`);
|
|
70
|
+
}
|
|
71
|
+
/** Public opted-in, accepting worker declarations; no key, quote or payment authorization. */
|
|
72
|
+
searchWorkers(input = {}) {
|
|
73
|
+
if (input.query !== undefined && (typeof input.query !== "string" || input.query.trim().length > 80))
|
|
74
|
+
throw new TypeError("query must be at most 80 characters.");
|
|
75
|
+
if (input.capability !== undefined && !validWorkerCapabilityId(input.capability))
|
|
76
|
+
throw new TypeError("capability must be a lowercase slug of 3–64 characters.");
|
|
77
|
+
if (input.contentType !== undefined && !artifactType(input.contentType))
|
|
78
|
+
throw new TypeError("Unsupported worker result format.");
|
|
79
|
+
const limit = input.limit ?? 20, cursor = input.cursor ?? 0;
|
|
80
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 50 || !Number.isSafeInteger(cursor) || cursor < 0 || cursor > 100000000)
|
|
81
|
+
throw new TypeError("Use limit 1–50 and a nonnegative numeric cursor.");
|
|
82
|
+
const query = new URLSearchParams({ q: input.query?.trim() ?? "", limit: String(limit), cursor: String(cursor) });
|
|
83
|
+
if (input.capability)
|
|
84
|
+
query.set("capability", input.capability);
|
|
85
|
+
if (input.contentType)
|
|
86
|
+
query.set("contentType", input.contentType);
|
|
87
|
+
return this.request("GET", "/v1/worker-directory?" + query);
|
|
88
|
+
}
|
|
43
89
|
createEnrollmentChallenge(input) {
|
|
44
90
|
return this.request("POST", "/v1/merchants/enrollment-challenges", input);
|
|
45
91
|
}
|
|
46
92
|
completeEnrollmentWithSignature(input) {
|
|
47
|
-
return this.request("POST", "/v1/merchants/enrollments", input);
|
|
93
|
+
return this.request("POST", "/v1/merchants/enrollments", { ...input, signature: requireEip191Signature(input.signature) });
|
|
48
94
|
}
|
|
49
95
|
completeEnrollmentWithTransaction(input) {
|
|
50
96
|
return this.request("POST", "/v1/merchants/enrollments/transaction", input);
|
|
@@ -52,7 +98,7 @@ export class MerchantClient {
|
|
|
52
98
|
/** Request a challenge, ask the caller's wallet adapter to sign its exact message, then enroll. */
|
|
53
99
|
async enrollWithSignature(input) {
|
|
54
100
|
const challenge = await this.createEnrollmentChallenge({ wallet: input.wallet, name: input.name });
|
|
55
|
-
const signature = await input.signMessage(challenge.message);
|
|
101
|
+
const signature = requireEip191Signature(await input.signMessage(challenge.message));
|
|
56
102
|
return this.completeEnrollmentWithSignature({
|
|
57
103
|
wallet: input.wallet,
|
|
58
104
|
name: input.name,
|
|
@@ -75,14 +121,14 @@ export class MerchantClient {
|
|
|
75
121
|
return this.request("POST", "/v1/merchants/credential-challenges", { wallet });
|
|
76
122
|
}
|
|
77
123
|
rotateCredentialWithSignature(wallet, challengeId, signature) {
|
|
78
|
-
return this.request("POST", "/v1/merchants/credentials/rotate", { wallet, challengeId, signature });
|
|
124
|
+
return this.request("POST", "/v1/merchants/credentials/rotate", { wallet, challengeId, signature: requireEip191Signature(signature) });
|
|
79
125
|
}
|
|
80
126
|
rotateCredentialWithTransaction(input) {
|
|
81
127
|
return this.request("POST", "/v1/merchants/credentials/rotate-transaction", input);
|
|
82
128
|
}
|
|
83
129
|
async rotateWithSignature(input) {
|
|
84
130
|
const challenge = await this.createCredentialRotationChallenge(input.wallet);
|
|
85
|
-
const signature = await input.signMessage(challenge.message);
|
|
131
|
+
const signature = requireEip191Signature(await input.signMessage(challenge.message));
|
|
86
132
|
return this.rotateCredentialWithSignature(input.wallet, challenge.challengeId, signature);
|
|
87
133
|
}
|
|
88
134
|
async rotateWithTransaction(input) {
|
|
@@ -165,6 +211,11 @@ export class MerchantClient {
|
|
|
165
211
|
if (!Number.isInteger(limit) || limit < 1 || limit > 50)
|
|
166
212
|
throw new TypeError("limit must be an integer from 1 through 50.");
|
|
167
213
|
const query = new URLSearchParams({ limit: String(limit) });
|
|
214
|
+
if (input.order !== undefined) {
|
|
215
|
+
if (input.order !== "oldest" && input.order !== "latest")
|
|
216
|
+
throw new TypeError("order must be oldest or latest.");
|
|
217
|
+
query.set("order", input.order);
|
|
218
|
+
}
|
|
168
219
|
if (input.states !== undefined) {
|
|
169
220
|
if (!Array.isArray(input.states) || input.states.length === 0 || input.states.length > 10 || !input.states.every(state => workTicketStates.includes(state))) {
|
|
170
221
|
throw new TypeError("states must contain 1–10 supported Work Ticket states.");
|
|
@@ -197,11 +248,37 @@ export class MerchantClient {
|
|
|
197
248
|
submitWorkTicketResult(ticketId, input) {
|
|
198
249
|
return this.request("POST", `/v1/agent/work-tickets/${checkedWorkTicketId(ticketId)}/results`, input, true);
|
|
199
250
|
}
|
|
200
|
-
|
|
251
|
+
/** Free public demand. Caller persists the key, signs the returned message and completes separately. */
|
|
252
|
+
createBuyerRequestChallenge(input, idempotencyKey) {
|
|
253
|
+
return this.request("POST", "/v1/buyer-requests/challenges", input, false, { "Idempotency-Key": buyerRequestKey(idempotencyKey) });
|
|
254
|
+
}
|
|
255
|
+
completeBuyerRequest(input, idempotencyKey) {
|
|
256
|
+
return this.request("POST", "/v1/buyer-requests", { ...input, signature: requireEip191Signature(input.signature) }, false, { "Idempotency-Key": buyerRequestKey(idempotencyKey) });
|
|
257
|
+
}
|
|
258
|
+
listBuyerRequests(input = {}) {
|
|
259
|
+
const query = input.query ?? "", limit = input.limit ?? 20, cursor = input.cursor ?? 0;
|
|
260
|
+
if (typeof query !== "string" || query.trim().length > 80 || !Number.isInteger(limit) || limit < 1 || limit > 50 || !Number.isSafeInteger(cursor) || cursor < 0 || cursor > 100000000)
|
|
261
|
+
throw new TypeError("Use a query up to 80 characters, limit 1–50 and cursor 0–100000000.");
|
|
262
|
+
return this.request("GET", "/v1/buyer-requests?" + new URLSearchParams({ q: query.trim(), limit: String(limit), cursor: String(cursor) }));
|
|
263
|
+
}
|
|
264
|
+
getBuyerRequest(requestId) {
|
|
265
|
+
return this.request("GET", `/v1/buyer-requests/${checkedWorkTicketId(requestId)}`);
|
|
266
|
+
}
|
|
267
|
+
proposeBuyerRequestOffer(requestId, input) {
|
|
268
|
+
return this.request("POST", `/v1/agent/buyer-requests/${checkedWorkTicketId(requestId)}/proposals`, input, true);
|
|
269
|
+
}
|
|
270
|
+
createBuyerRequestSelectionChallenge(requestId, input) {
|
|
271
|
+
return this.request("POST", `/v1/buyer-requests/${checkedWorkTicketId(requestId)}/selection-challenges`, input);
|
|
272
|
+
}
|
|
273
|
+
/** Return a checkout handoff; this method never authorizes or submits payment. */
|
|
274
|
+
selectBuyerRequestOffer(requestId, input) {
|
|
275
|
+
return this.request("POST", `/v1/buyer-requests/${checkedWorkTicketId(requestId)}/selections`, { ...input, signature: requireEip191Signature(input.signature) });
|
|
276
|
+
}
|
|
277
|
+
async request(method, path, body, authenticated = false, extraHeaders = {}) {
|
|
201
278
|
if (authenticated && !this.#apiKey) {
|
|
202
279
|
throw new Error("This request requires a merchant API key. Use client.withApiKey(apiKey).");
|
|
203
280
|
}
|
|
204
|
-
const headers = { Accept: "application/json" };
|
|
281
|
+
const headers = { Accept: "application/json", ...extraHeaders };
|
|
205
282
|
if (body !== undefined)
|
|
206
283
|
headers["Content-Type"] = "application/json";
|
|
207
284
|
if (authenticated && this.#apiKey)
|
|
@@ -228,6 +305,25 @@ export class MerchantClient {
|
|
|
228
305
|
const invalidResponse = () => new MerchantApiError(response.status, "invalid_response", "402Mob returned an incomplete response. The request may have completed; preserve recovery data. If enrollment or rotation completed without a usable key, use credential rotation.");
|
|
229
306
|
if (!isRecord(payload))
|
|
230
307
|
throw invalidResponse();
|
|
308
|
+
if (path.startsWith("/v1/worker-directory?")) {
|
|
309
|
+
if (!Array.isArray(payload.items) || !payload.items.every(item => isWorkerProfileView(item) && item.workerProfile.discoverable && item.workerProfile.availability === "accepting") || !(payload.nextCursor === null || Number.isSafeInteger(payload.nextCursor) && Number(payload.nextCursor) >= 0))
|
|
310
|
+
throw invalidResponse();
|
|
311
|
+
const params = new URL(path, "https://schema.invalid").searchParams, capability = params.get("capability"), type = params.get("contentType"), limit = Number(params.get("limit")), cursor = Number(params.get("cursor"));
|
|
312
|
+
const items = payload.items;
|
|
313
|
+
if (items.length > limit || new Set(items.map(item => item.merchant.id)).size !== items.length || payload.nextCursor !== null && (items.length !== limit || Number(payload.nextCursor) !== cursor + limit) || !items.every(item => item.workerProfile.capabilities.some(cap => (!capability || cap.id === capability) && (!type || cap.resultContentTypes.includes(type)))))
|
|
314
|
+
throw invalidResponse();
|
|
315
|
+
}
|
|
316
|
+
if (path.endsWith("/worker-profile")) {
|
|
317
|
+
if (!isWorkerProfileView(payload))
|
|
318
|
+
throw invalidResponse();
|
|
319
|
+
const requested = path.match(/^\/v1\/merchants\/([^/]+)\/worker-profile$/)?.[1];
|
|
320
|
+
if (requested && (payload.merchant.id !== requested || !payload.workerProfile.discoverable))
|
|
321
|
+
throw invalidResponse();
|
|
322
|
+
if (method === "PUT" && JSON.stringify(normalizedWorkerProfile(payload.workerProfile)) !== JSON.stringify(body))
|
|
323
|
+
throw invalidResponse();
|
|
324
|
+
}
|
|
325
|
+
if ((path.startsWith("/v1/buyer-requests") || path.startsWith("/v1/agent/buyer-requests/")) && !isBuyerRequestResponse(method, path, payload, body))
|
|
326
|
+
throw invalidResponse();
|
|
231
327
|
if (path === "/v1/merchants/enrollment-challenges" || path === "/v1/merchants/credential-challenges") {
|
|
232
328
|
const proof = payload.transactionProof;
|
|
233
329
|
if (!nonempty(payload.challengeId) || !nonempty(payload.message) || !nonempty(payload.expiresAt) ||
|
|
@@ -252,6 +348,96 @@ function isRecord(value) {
|
|
|
252
348
|
}
|
|
253
349
|
function nonempty(value) { return typeof value === "string" && value.trim().length > 0; }
|
|
254
350
|
function address(value) { return typeof value === "string" && /^0x[0-9a-fA-F]{40}$/.test(value); }
|
|
351
|
+
const baseUsdc = "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913";
|
|
352
|
+
function uuid(value) { return typeof value === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value); }
|
|
353
|
+
function timestamp(value) { return typeof value === "string" && Number.isFinite(Date.parse(value)); }
|
|
354
|
+
function digest(value) { return typeof value === "string" && /^[0-9a-f]{64}$/.test(value); }
|
|
355
|
+
function atomicAmount(value) { return typeof value === "string" && /^[1-9][0-9]{0,11}$/.test(value); }
|
|
356
|
+
function publicText(value, max) { return typeof value === "string" && value.trim().length > 0 && Array.from(value).length <= max; }
|
|
357
|
+
function artifactType(value) { return value === "application/json" || value === "text/plain; charset=utf-8"; }
|
|
358
|
+
function validWorkerCapabilityId(value) { return typeof value === "string" && /^[a-z0-9][a-z0-9-]{1,62}[a-z0-9]$/.test(value); }
|
|
359
|
+
function onlyKeys(value, keys) { return Object.keys(value).every(key => keys.includes(key)); }
|
|
360
|
+
function boundedWorkerText(value, max) { return typeof value === "string" && value.trim().length > 0 && Array.from(value.trim()).length <= max; }
|
|
361
|
+
function normalizedWorkerProfile(value) {
|
|
362
|
+
return { description: value.description.trim(), capabilities: value.capabilities.map(cap => ({ id: cap.id, title: cap.title.trim(), description: cap.description.trim(), inputDescription: cap.inputDescription.trim(), resultDescription: cap.resultDescription.trim(), resultContentTypes: [...cap.resultContentTypes] })), availability: value.availability, discoverable: value.discoverable };
|
|
363
|
+
}
|
|
364
|
+
function isWorkerProfileInput(value) {
|
|
365
|
+
if (!isRecord(value) || !boundedWorkerText(value.description, 1000) || (value.availability !== "accepting" && value.availability !== "paused") || typeof value.discoverable !== "boolean" || !Array.isArray(value.capabilities) || !value.capabilities.length || value.capabilities.length > 10)
|
|
366
|
+
return false;
|
|
367
|
+
if (new Set(value.capabilities.map(c => isRecord(c) ? c.id : null)).size !== value.capabilities.length)
|
|
368
|
+
return false;
|
|
369
|
+
return value.capabilities.every(c => isRecord(c) && validWorkerCapabilityId(c.id) && boundedWorkerText(c.title, 100) && boundedWorkerText(c.description, 1000) && boundedWorkerText(c.inputDescription, 1000) && boundedWorkerText(c.resultDescription, 1000) && Array.isArray(c.resultContentTypes) && c.resultContentTypes.length >= 1 && c.resultContentTypes.length <= 2 && new Set(c.resultContentTypes).size === c.resultContentTypes.length && c.resultContentTypes.every(artifactType));
|
|
370
|
+
}
|
|
371
|
+
function isWorkerProfileView(value) {
|
|
372
|
+
if (!isRecord(value) || !isRecord(value.merchant) || !uuid(value.merchant.id) || !publicText(value.merchant.name, 100) || !address(value.merchant.payoutAddress) || !isWorkerProfileInput(value.workerProfile))
|
|
373
|
+
return false;
|
|
374
|
+
const profile = value.workerProfile;
|
|
375
|
+
return profile.merchantId === value.merchant.id && Number.isSafeInteger(profile.revision) && profile.revision > 0 && timestamp(profile.createdAt) && timestamp(profile.updatedAt) && isRecord(value.workTicket) && value.workTicket.method === "POST" && value.workTicket.challengePath === "/v1/work-tickets/challenges" && value.workTicket.merchantId === value.merchant.id && value.workTicket.guide === "/work-tickets.md" && value.workTicket.paymentRequiredToCreate === false && value.workTicket.quoteRequired === true;
|
|
376
|
+
}
|
|
377
|
+
function usdcAtomic(value) {
|
|
378
|
+
if (typeof value !== "string")
|
|
379
|
+
return;
|
|
380
|
+
const parts = /^(0|[1-9][0-9]{0,5})(?:\.([0-9]{1,6}))?$/.exec(value);
|
|
381
|
+
return parts ? BigInt(parts[1] + (parts[2] ?? "").padEnd(6, "0")).toString() : undefined;
|
|
382
|
+
}
|
|
383
|
+
function isPublicBuyerRequest(value) {
|
|
384
|
+
if (!isRecord(value))
|
|
385
|
+
return false;
|
|
386
|
+
return uuid(value.id) && publicText(value.title, 100) && publicText(value.description, 2000) && atomicAmount(value.maxPriceAtomic) && artifactType(value.contentType) &&
|
|
387
|
+
["open", "selected", "expired"].includes(String(value.state)) && timestamp(value.createdAt) && timestamp(value.expiresAt) &&
|
|
388
|
+
(value.state === "selected" ? uuid(value.selectedProposalId) : value.selectedProposalId === null);
|
|
389
|
+
}
|
|
390
|
+
function isBuyerRequestProposal(value) {
|
|
391
|
+
if (!isRecord(value))
|
|
392
|
+
return false;
|
|
393
|
+
return uuid(value.id) && uuid(value.requestId) && uuid(value.merchantId) && uuid(value.offerId) && publicText(value.note, 300) && publicText(value.title, 100) &&
|
|
394
|
+
typeof value.version === "string" && /^(0|[1-9][0-9]{0,8})\.(0|[1-9][0-9]{0,8})\.(0|[1-9][0-9]{0,8})$/.test(value.version) &&
|
|
395
|
+
atomicAmount(value.amountAtomic) && artifactType(value.contentType) && address(value.payoutAddress) && digest(value.termsHash) && timestamp(value.createdAt);
|
|
396
|
+
}
|
|
397
|
+
function isBuyerCheckoutHandoff(value, proposal) {
|
|
398
|
+
if (!isRecord(value))
|
|
399
|
+
return false;
|
|
400
|
+
const path = `/v1/merchants/${proposal.merchantId}/offers/${proposal.offerId}/checkout`;
|
|
401
|
+
return value.merchantId === proposal.merchantId && value.offerId === proposal.offerId && value.amountAtomic === proposal.amountAtomic &&
|
|
402
|
+
address(value.payTo) && value.payTo.toLowerCase() === proposal.payoutAddress.toLowerCase() && value.contentType === proposal.contentType &&
|
|
403
|
+
value.network === "eip155:8453" && typeof value.asset === "string" && value.asset.toLowerCase() === baseUsdc && value.method === "POST" && value.path === path &&
|
|
404
|
+
value.directTransferChallenge === path + "/transfer-challenges" && isRecord(value.body) && Object.keys(value.body).length === 0 &&
|
|
405
|
+
value.mcpTool === "buy_prepared_artifact" && value.paymentRequired === true && value.paymentStatus === "not_observed";
|
|
406
|
+
}
|
|
407
|
+
function isBuyerRequestDetail(value, expectedId) {
|
|
408
|
+
if (!isRecord(value) || !isPublicBuyerRequest(value.request) || !Array.isArray(value.proposals) || value.proposals.length > 20 || !value.proposals.every(isBuyerRequestProposal))
|
|
409
|
+
return false;
|
|
410
|
+
const request = value.request, proposals = value.proposals;
|
|
411
|
+
if (expectedId && request.id !== expectedId)
|
|
412
|
+
return false;
|
|
413
|
+
if (new Set(proposals.map(p => p.id)).size !== proposals.length || new Set(proposals.map(p => p.merchantId)).size !== proposals.length ||
|
|
414
|
+
proposals.some(p => p.requestId !== request.id || p.contentType !== request.contentType || BigInt(p.amountAtomic) > BigInt(request.maxPriceAtomic)))
|
|
415
|
+
return false;
|
|
416
|
+
if (request.state !== "selected")
|
|
417
|
+
return value.checkoutHandoff === null;
|
|
418
|
+
const selected = proposals.find(p => p.id === request.selectedProposalId);
|
|
419
|
+
return Boolean(selected && isBuyerCheckoutHandoff(value.checkoutHandoff, selected));
|
|
420
|
+
}
|
|
421
|
+
function isBuyerRequestResponse(method, path, value, body) {
|
|
422
|
+
const args = isRecord(body) ? body : {};
|
|
423
|
+
const challenge = () => uuid(value.challengeId) && nonempty(value.message) && timestamp(value.expiresAt);
|
|
424
|
+
const id = path.match(/^\/v1\/(?:agent\/)?buyer-requests\/([^/?]+)/)?.[1];
|
|
425
|
+
if (path === "/v1/buyer-requests/challenges")
|
|
426
|
+
return challenge() && timestamp(value.requestExpiresAt) && digest(value.policyHash) && Date.parse(value.expiresAt) <= Date.parse(value.requestExpiresAt);
|
|
427
|
+
if (path === "/v1/buyer-requests" && method === "POST")
|
|
428
|
+
return isBuyerRequestDetail(value) && typeof value.replay === "boolean" &&
|
|
429
|
+
value.request.title === (typeof args.title === "string" ? args.title.trim() : undefined) && value.request.description === (typeof args.description === "string" ? args.description.trim() : undefined) && value.request.contentType === args.contentType && value.request.maxPriceAtomic === usdcAtomic(args.maxPriceUSDC);
|
|
430
|
+
if (path.startsWith("/v1/buyer-requests?") || (path === "/v1/buyer-requests" && method === "GET"))
|
|
431
|
+
return Array.isArray(value.items) && value.items.length <= 50 && value.items.every(isPublicBuyerRequest) &&
|
|
432
|
+
(value.nextCursor === null || (Number.isSafeInteger(value.nextCursor) && value.nextCursor >= 0));
|
|
433
|
+
if (path.endsWith("/selection-challenges"))
|
|
434
|
+
return challenge() && isBuyerRequestProposal(value.proposal) && value.proposal.requestId === id && value.proposal.id === args.proposalId;
|
|
435
|
+
if (path.endsWith("/proposals"))
|
|
436
|
+
return isBuyerRequestProposal(value.proposal) && value.proposal.requestId === id && value.proposal.offerId === args.offerId && value.proposal.note === (typeof args.note === "string" ? args.note.trim() : undefined) && typeof value.replay === "boolean";
|
|
437
|
+
if (path.endsWith("/selections"))
|
|
438
|
+
return isBuyerRequestDetail(value, id) && value.request.state === "selected" && typeof value.replay === "boolean";
|
|
439
|
+
return method === "GET" && isBuyerRequestDetail(value, id);
|
|
440
|
+
}
|
|
255
441
|
function isAgentResource(value) {
|
|
256
442
|
if (!isRecord(value))
|
|
257
443
|
return false;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// SDK 0.4.0 example. This stops before payment.
|
|
2
|
+
// Buyer and seller run independently; exchange only the public request/proposal IDs.
|
|
3
|
+
import { MerchantClient, type BuyerRequestInput, type EvmSignature } from "../dist/index.js";
|
|
4
|
+
|
|
5
|
+
export async function publishDemand(client:MerchantClient,input:BuyerRequestInput,key:string,sign:(message:string)=>Promise<EvmSignature>,persist:(value:unknown)=>Promise<void>) {
|
|
6
|
+
const challenge=await client.createBuyerRequestChallenge(input,key);
|
|
7
|
+
// The caller must inspect origin, public text, budget, format and expiry first.
|
|
8
|
+
const completion={...input,challengeId:challenge.challengeId,signature:await sign(challenge.message)};
|
|
9
|
+
await persist({key,completion}); // Save BEFORE dispatch; replay exactly after interruption.
|
|
10
|
+
return client.completeBuyerRequest(completion,key);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function proposePreparedOffer(merchant:MerchantClient,requestId:string,offerId:string) {
|
|
14
|
+
return merchant.proposeBuyerRequestOffer(requestId,{offerId,note:"Prepared artifact matching the public request."});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function selectForCheckout(buyer:MerchantClient,requestId:string,wallet:`0x${string}`,proposalId:string,sign:(message:string)=>Promise<EvmSignature>,persist:(value:unknown)=>Promise<void>) {
|
|
18
|
+
const challenge=await buyer.createBuyerRequestSelectionChallenge(requestId,{buyer:wallet,proposalId});
|
|
19
|
+
// Review the exact offer, price, payout and version before signing.
|
|
20
|
+
const selection={challengeId:challenge.challengeId,signature:await sign(challenge.message)};
|
|
21
|
+
await persist({requestId,selection});
|
|
22
|
+
const selected=await buyer.selectBuyerRequestOffer(requestId,selection);
|
|
23
|
+
// Separate payment authorization/journal is required to use this handoff.
|
|
24
|
+
return selected.checkoutHandoff;
|
|
25
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MerchantClient,
|
|
3
|
+
type WorkerProfileInput,
|
|
4
|
+
type WorkerProfileView,
|
|
5
|
+
type ArtifactContentType,
|
|
6
|
+
} from "../dist/index.js";
|
|
7
|
+
|
|
8
|
+
/** The caller approves capabilities, availability and public visibility before this full replacement. */
|
|
9
|
+
export async function publishWorkerProfile(merchant: MerchantClient, input: WorkerProfileInput) {
|
|
10
|
+
const saved = await merchant.setWorkerProfile(input);
|
|
11
|
+
return { saved, current: await merchant.getMyWorkerProfile() };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Read-only: inspect declarations, then use the Work Ticket protocol with a separate buyer signer. */
|
|
15
|
+
export async function findWorkers(
|
|
16
|
+
buyer: MerchantClient,
|
|
17
|
+
capability: string,
|
|
18
|
+
contentType: ArtifactContentType = "application/json",
|
|
19
|
+
): Promise<WorkerProfileView[]> {
|
|
20
|
+
const seen = new Map<string, WorkerProfileView>();
|
|
21
|
+
let cursor: number | null = 0;
|
|
22
|
+
// Bounded pass, since concurrent declaration changes can shift offset pages.
|
|
23
|
+
for (let page = 0; page < 4 && cursor !== null; page++) {
|
|
24
|
+
const result = await buyer.searchWorkers({ capability, contentType, limit: 50, cursor });
|
|
25
|
+
for (const worker of result.items) seen.set(worker.merchant.id, worker);
|
|
26
|
+
cursor = result.nextCursor;
|
|
27
|
+
}
|
|
28
|
+
return [...seen.values()];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const repositoryReview: WorkerProfileInput = {
|
|
32
|
+
description: "Bounded repository review in our agent runtime.",
|
|
33
|
+
availability: "accepting",
|
|
34
|
+
discoverable: false, // Explicit opt-in belongs to the merchant owner.
|
|
35
|
+
capabilities: [{
|
|
36
|
+
id: "repository-review",
|
|
37
|
+
title: "Repository review",
|
|
38
|
+
description: "Find reproducible defects within an agreed revision and scope.",
|
|
39
|
+
inputDescription: "Repository reference, revision and acceptance criteria; arrange access separately.",
|
|
40
|
+
resultDescription: "JSON findings, reproduction steps and checks run. Price and scope require a ticket quote.",
|
|
41
|
+
resultContentTypes: ["application/json"],
|
|
42
|
+
}],
|
|
43
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "402mob-merchant-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "TypeScript client for resolving agent resources and enrolling and operating 402Mob merchants.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"dist",
|
|
16
16
|
"examples",
|
|
17
17
|
"README.md",
|
|
18
|
+
"CHANGELOG.md",
|
|
18
19
|
"LICENSE"
|
|
19
20
|
],
|
|
20
21
|
"main": "./dist/index.js",
|
|
@@ -27,7 +28,8 @@
|
|
|
27
28
|
},
|
|
28
29
|
"scripts": {
|
|
29
30
|
"build": "tsc -p tsconfig.json",
|
|
30
|
-
"test": "npm run build && node --test tests/*.test.mjs",
|
|
31
|
+
"test": "npm run build && npm run typecheck:examples && node --test tests/*.test.mjs",
|
|
32
|
+
"typecheck:examples": "tsc -p tsconfig.examples.json",
|
|
31
33
|
"prepare": "npm run build"
|
|
32
34
|
},
|
|
33
35
|
"devDependencies": {
|