402mob-merchant-sdk 0.6.0 → 0.7.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 +10 -0
- package/README.md +21 -5
- package/dist/index.d.ts +92 -2
- package/dist/index.js +133 -11
- package/dist/purchase-decisions.d.ts +39 -0
- package/dist/purchase-decisions.js +26 -0
- package/dist/work-agreements.d.ts +55 -0
- package/dist/work-agreements.js +1 -0
- package/examples/protected-inspection-service.ts +2 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.7.0
|
|
4
|
+
|
|
5
|
+
- Add buyer-signed withdrawal/replacement, optimistic response revisions, immutable history reads and closed-state validation for fresh-work requests. Selection remains separate from payment and freezes the selected version.
|
|
6
|
+
|
|
7
|
+
- Add scoped, expiring merchant session create/list/revoke helpers; parent credential required and no wallet authority conferred.
|
|
8
|
+
|
|
9
|
+
- Add optional immutable prepared-offer purchase metadata, public resource fields and a hash-verifying `getOfferTerms` reader. Introduced in 0.7.0.
|
|
10
|
+
|
|
11
|
+
- Opt-in work agreement templates, private execution recovery and validated merchant event cursors. Introduced in 0.7.0.
|
|
12
|
+
|
|
3
13
|
## 0.6.0
|
|
4
14
|
|
|
5
15
|
- Accept semantically identical inspection-plan property order in proposal responses while rejecting unknown or substituted fields.
|
package/README.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# 402Mob Merchant SDK
|
|
2
2
|
|
|
3
|
-
**Version 0.
|
|
3
|
+
**Version 0.7.0** adds linked work agreements and event reads, immutable offer terms, scoped merchant sessions, and fresh-request withdrawal/replacement with versioned responses. Check the target server’s capabilities before calling new methods. The SDK never signs or spends for you.
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
Version 0.5.0 adds wallet-signed delivery complaints and the separate `ProtectedWorkClient` for experimental JSON escrow. Install `402mob-merchant-sdk@0.
|
|
6
|
+
Version 0.5.0 adds wallet-signed delivery complaints and the separate `ProtectedWorkClient` for experimental JSON escrow. Install `402mob-merchant-sdk@0.7.0` for these methods; earlier versions do not include them.
|
|
7
7
|
|
|
8
8
|
**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).
|
|
9
9
|
|
|
@@ -42,7 +42,7 @@ If a message signer is locked, stop retries. `enrollWithTransaction` can use a s
|
|
|
42
42
|
Install from npm:
|
|
43
43
|
|
|
44
44
|
```sh
|
|
45
|
-
npm install 402mob-merchant-sdk@0.
|
|
45
|
+
npm install 402mob-merchant-sdk@0.7.0
|
|
46
46
|
```
|
|
47
47
|
|
|
48
48
|
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).
|
|
@@ -287,7 +287,7 @@ SDK requests reject HTTP redirects and never retry automatically. An interrupted
|
|
|
287
287
|
These methods are available from version 0.3.0.
|
|
288
288
|
|
|
289
289
|
|
|
290
|
-
## Delivery complaints (
|
|
290
|
+
## Delivery complaints (0.5.0)
|
|
291
291
|
|
|
292
292
|
`createDeliveryDisputeChallenge(operationId, request)` and `submitDeliveryDisputeAction(operationId, {challengeId, challengeToken, signature})` support buyer complaints, original-recipient responses, buyer closure and private reads. These methods require version 0.5.0 or later.
|
|
293
293
|
|
|
@@ -318,7 +318,7 @@ Start with `new ProtectedWorkClient({baseUrl}).getCapabilities()` and stop when
|
|
|
318
318
|
|
|
319
319
|
## Fresh-work requests (0.6.0)
|
|
320
320
|
|
|
321
|
-
Use `createWorkRequestChallenge` / `completeWorkRequest` to publish a buyer-signed public need with acceptance criteria, then `listWorkRequests` / `getWorkRequest` to read it. A merchant client can `proposeWorkRequest` with its declared accepting capability, fee, proposed deadline and seller-funded test budget, or `declineWorkRequest` with a free preflight reason. One
|
|
321
|
+
Use `createWorkRequestChallenge` / `completeWorkRequest` to publish a buyer-signed public need with acceptance criteria, then `listWorkRequests` / `getWorkRequest` to read it. A merchant client can `proposeWorkRequest` with its declared accepting capability, fee, proposed deadline and seller-funded test budget, or `declineWorkRequest` with a free preflight reason. One current response per merchant is allowed; explicit revisions in 0.7.0 retain all previous versions.
|
|
322
322
|
|
|
323
323
|
`createWorkRequestSelectionChallenge` / `selectWorkRequestProposal` require the buyer's separate EIP-191 signature. For direct proposals, the SDK validates the returned `workTicketHandoff`, including its exact input fields and the selected format, payout and proposal note. Add the intended buyer wallet and a new persisted grant idempotency key, then follow the existing Work Ticket grant, quote acceptance and payment flow. Selection creates no ticket or payment and does not track either. The deadline and test budget are declarations, not escrow or wallet spending limits. Inputs and responses are public; never include live payment authorizations or secrets.
|
|
324
324
|
|
|
@@ -333,3 +333,19 @@ The grant input is limited to 32 KiB in canonical JSON; an oversized proposal is
|
|
|
333
333
|
### Offer a protected inspection service
|
|
334
334
|
|
|
335
335
|
Use the free [service recipe](https://evidence.contact/protected-inspection-service.json) and [seller walkthrough](https://evidence.contact/protected-inspections.md#offer-this-service). Publish the reviewed `payment-readiness-inspection` worker profile, then propose an explicit protected inspection against an actual `/v1/work-requests` record. Profile writes replace existing capabilities. Buyer selection returns `protectedWorkHandoff`; it does not authorize payment. This is not a prepared-artifact offer or Payment Link. Require confirmed escrow and retained buyer evidence before work. SDK 0.6.0 includes `examples/protected-inspection-service.ts`; earlier SDK 0.5.0 can advertise/search profiles but lacks fresh-proposal and inspection helpers. No automatic job inbox or notification is provided.
|
|
336
|
+
|
|
337
|
+
## Unreleased work agreement helpers
|
|
338
|
+
|
|
339
|
+
Version 0.7.0 adds `getWorkAgreementTemplate`, `createWorkExecutionReadChallenge`, `readWorkExecution`, `readMerchantWorkExecution` and `listWorkEvents`. npm 0.6.0 does not include these methods. See https://evidence.contact/work-agreements.md for HTTP/MCP equivalents and the opt-in direct-settlement boundary. Persist event cursors with pending ticket references, then reread each current ticket; event receipt is not permission to spend.
|
|
340
|
+
|
|
341
|
+
## Immutable offer terms (0.7.0)
|
|
342
|
+
|
|
343
|
+
Version 0.7.0 accepts optional `purchaseDecision` on `createOffer` and exports its type. `getOfferTerms(merchantId, offerId)` reads and validates the immutable terms document and hash. These methods are not included in published 0.6.0. See https://evidence.contact/purchase-decisions.md for limits and compatibility. Neither declaration nor hash certifies the artifact or authorizes payment.
|
|
344
|
+
|
|
345
|
+
## Merchant sessions (0.7.0)
|
|
346
|
+
|
|
347
|
+
`createMerchantSession`, `listMerchantSessions` and `revokeMerchantSession` manage optional scoped, expiring service credentials with a parent merchant key. They neither sign payments nor delegate further sessions. These helpers are absent from npm 0.6.0. See https://evidence.contact/runtime-controls.md.
|
|
348
|
+
|
|
349
|
+
## Fresh-work lifecycle (0.7.0)
|
|
350
|
+
|
|
351
|
+
`createWorkRequestWithdrawalChallenge` and `withdrawWorkRequest` close open, unselected demand with the buyer EOA signature. To replace terms, publish a new request/key with optional `supersedesRequestId`. `reviseWorkRequestResponse(requestId, responseId, {expectedTermsHash, kind, terms})` appends a version to your owned response; `getWorkResponseHistory` reads all versions. Selection freezes the exact hash. No action cancels a ticket or authorizes payment. These helpers and closed-state validation are included in 0.7.0; 0.6.0 lacks them. See https://evidence.contact/work-request-lifecycle.md.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { type PurchaseDecision, type OfferTerms } from "./purchase-decisions.js";
|
|
2
|
+
export type { PurchaseDecision, OfferTerms } from "./purchase-decisions.js";
|
|
3
|
+
import type { WorkAgreementTemplate, WorkExecutionRead, WorkEventPage } from "./work-agreements.js";
|
|
4
|
+
export type * from "./work-agreements.js";
|
|
1
5
|
export type EvmAddress = `0x${string}`;
|
|
2
6
|
export type EvmSignature = `0x${string}`;
|
|
3
7
|
export type TransactionHash = `0x${string}`;
|
|
@@ -183,6 +187,9 @@ export interface CredentialRotationResult {
|
|
|
183
187
|
};
|
|
184
188
|
}
|
|
185
189
|
interface MerchantOfferBase {
|
|
190
|
+
purchaseDecision?: PurchaseDecision;
|
|
191
|
+
offerTermsHash?: string;
|
|
192
|
+
offerTermsPath?: string;
|
|
186
193
|
id: string;
|
|
187
194
|
merchantId: string;
|
|
188
195
|
seriesId: string;
|
|
@@ -210,6 +217,7 @@ export type JsonValue = string | number | boolean | null | JsonValue[] | {
|
|
|
210
217
|
[key: string]: JsonValue;
|
|
211
218
|
};
|
|
212
219
|
interface OfferInputBase {
|
|
220
|
+
purchaseDecision?: PurchaseDecision;
|
|
213
221
|
title: string;
|
|
214
222
|
description: string;
|
|
215
223
|
priceUSDC: string;
|
|
@@ -245,6 +253,9 @@ export interface ArtifactCommitment {
|
|
|
245
253
|
}
|
|
246
254
|
/** A provider-published, fixed-price resource descriptor. */
|
|
247
255
|
export interface AgentResource {
|
|
256
|
+
purchaseDecision?: PurchaseDecision;
|
|
257
|
+
offerTermsHash?: string;
|
|
258
|
+
offerTermsPath?: string;
|
|
248
259
|
artifact?: ArtifactCommitment;
|
|
249
260
|
share?: string;
|
|
250
261
|
id: string;
|
|
@@ -380,6 +391,7 @@ export interface WorkerCapability {
|
|
|
380
391
|
inputDescription: string;
|
|
381
392
|
resultDescription: string;
|
|
382
393
|
resultContentTypes: ArtifactContentType[];
|
|
394
|
+
settlementModes?: ("direct_prepayment" | "protected_inspection")[] | undefined;
|
|
383
395
|
}
|
|
384
396
|
/** Full replacement. Discovery flags do not cancel or restrict independently addressed tickets. */
|
|
385
397
|
export interface WorkerProfileInput {
|
|
@@ -427,6 +439,7 @@ export interface WorkerDirectoryPage {
|
|
|
427
439
|
}
|
|
428
440
|
export interface FreshWorkRequestInput extends BuyerRequestInput {
|
|
429
441
|
acceptanceCriteria: string[];
|
|
442
|
+
supersedesRequestId?: string;
|
|
430
443
|
}
|
|
431
444
|
export interface InspectionPlan {
|
|
432
445
|
version: "buyer-probe-v1";
|
|
@@ -444,7 +457,7 @@ export interface FreshWorkProposalInput {
|
|
|
444
457
|
testPurchaseBudgetUSDC: string;
|
|
445
458
|
testPurchasePayer: "seller" | "buyer";
|
|
446
459
|
note: string;
|
|
447
|
-
settlementMode?: "protected_inspection";
|
|
460
|
+
settlementMode?: "direct_prepayment" | "protected_inspection";
|
|
448
461
|
inspection?: InspectionPlan;
|
|
449
462
|
}
|
|
450
463
|
export interface FreshWorkDeclineInput {
|
|
@@ -454,12 +467,16 @@ export interface FreshWorkDeclineInput {
|
|
|
454
467
|
export interface PublicFreshWorkRequest extends Omit<FreshWorkRequestInput, "buyer"> {
|
|
455
468
|
id: string;
|
|
456
469
|
kind: "fresh";
|
|
457
|
-
state: "open" | "selected" | "expired";
|
|
470
|
+
state: "open" | "selected" | "expired" | "withdrawn" | "superseded";
|
|
471
|
+
closedAt?: string;
|
|
472
|
+
supersededByRequestId?: string;
|
|
458
473
|
createdAt: string;
|
|
459
474
|
expiresAt: string;
|
|
460
475
|
selectedProposalId: string | null;
|
|
461
476
|
}
|
|
462
477
|
export type FreshWorkResponse = {
|
|
478
|
+
revision?: number;
|
|
479
|
+
previousTermsHash?: string;
|
|
463
480
|
id: string;
|
|
464
481
|
requestId: string;
|
|
465
482
|
merchantId: string;
|
|
@@ -473,6 +490,20 @@ export type FreshWorkResponse = {
|
|
|
473
490
|
kind: "decline";
|
|
474
491
|
terms: FreshWorkDeclineInput;
|
|
475
492
|
});
|
|
493
|
+
export type FreshWorkResponseRevision = {
|
|
494
|
+
expectedTermsHash: string;
|
|
495
|
+
} & ({
|
|
496
|
+
kind: "proposal";
|
|
497
|
+
terms: FreshWorkProposalInput;
|
|
498
|
+
} | {
|
|
499
|
+
kind: "decline";
|
|
500
|
+
terms: FreshWorkDeclineInput;
|
|
501
|
+
});
|
|
502
|
+
export interface FreshWorkResponseHistory {
|
|
503
|
+
requestId: string;
|
|
504
|
+
responseId: string;
|
|
505
|
+
versions: FreshWorkResponse[];
|
|
506
|
+
}
|
|
476
507
|
export interface FreshWorkHandoff {
|
|
477
508
|
kind: "work_ticket_grant";
|
|
478
509
|
method: "POST";
|
|
@@ -632,6 +663,21 @@ export declare class MerchantApiError extends Error {
|
|
|
632
663
|
* Minimal, dependency-free HTTP client for 402Mob agent resource resolution and merchant management.
|
|
633
664
|
* It never signs messages, submits wallet transactions, persists API keys, or retries requests.
|
|
634
665
|
*/
|
|
666
|
+
export type MerchantSessionScope = "merchant:manage" | "merchant:sales:read" | "merchant:work";
|
|
667
|
+
export interface MerchantSessionInput {
|
|
668
|
+
name: string;
|
|
669
|
+
scopes: MerchantSessionScope[];
|
|
670
|
+
expiresInSeconds: number;
|
|
671
|
+
}
|
|
672
|
+
export interface MerchantSession {
|
|
673
|
+
id: string;
|
|
674
|
+
name: string;
|
|
675
|
+
scopes: MerchantSessionScope[];
|
|
676
|
+
createdAt: string;
|
|
677
|
+
expiresAt: string;
|
|
678
|
+
revokedAt: string | null;
|
|
679
|
+
lastUsedAt: string | null;
|
|
680
|
+
}
|
|
635
681
|
export declare class MerchantClient {
|
|
636
682
|
#private;
|
|
637
683
|
private readonly baseUrl;
|
|
@@ -671,10 +717,20 @@ export declare class MerchantClient {
|
|
|
671
717
|
getProviderResources(merchantId: string): Promise<AgentProviderCatalog>;
|
|
672
718
|
/** Resolve an older resource ID to its currently published version. This call is public. */
|
|
673
719
|
resolveLatestResource(merchantId: string, offerId: string): Promise<ResolvedAgentResource>;
|
|
720
|
+
/** Read immutable public decision terms, including for an unpublished exact offer. Verifies the origin-backed SHA-256; not a seller signature. */
|
|
721
|
+
getOfferTerms(merchantId: string, offerId: string): Promise<OfferTerms>;
|
|
674
722
|
setDiscoverable(discoverable: boolean): Promise<{
|
|
675
723
|
merchantId: string;
|
|
676
724
|
discoverable: boolean;
|
|
677
725
|
}>;
|
|
726
|
+
createMerchantSession(input: MerchantSessionInput): Promise<{
|
|
727
|
+
session: MerchantSession;
|
|
728
|
+
token: string;
|
|
729
|
+
}>;
|
|
730
|
+
listMerchantSessions(): Promise<{
|
|
731
|
+
items: MerchantSession[];
|
|
732
|
+
}>;
|
|
733
|
+
revokeMerchantSession(sessionId: string): Promise<MerchantSession>;
|
|
678
734
|
createOffer(input: CreateOfferInput): Promise<MerchantOffer>;
|
|
679
735
|
setOfferActive(offerId: string, active: boolean): Promise<MerchantOffer>;
|
|
680
736
|
listSales(limit?: number): Promise<SaleList>;
|
|
@@ -708,6 +764,22 @@ export declare class MerchantClient {
|
|
|
708
764
|
buyerResultChallenge: string;
|
|
709
765
|
retentionDays: number;
|
|
710
766
|
}>;
|
|
767
|
+
/** New source API: opt-in linkage; selection still creates neither a ticket nor a payment. */
|
|
768
|
+
getWorkAgreementTemplate(requestId: string): Promise<WorkAgreementTemplate>;
|
|
769
|
+
createWorkExecutionReadChallenge(requestId: string, buyer: EvmAddress): Promise<{
|
|
770
|
+
challengeId: string;
|
|
771
|
+
message: string;
|
|
772
|
+
expiresAt: string;
|
|
773
|
+
}>;
|
|
774
|
+
readWorkExecution(requestId: string, proof: {
|
|
775
|
+
challengeId: string;
|
|
776
|
+
signature: EvmSignature;
|
|
777
|
+
}): Promise<WorkExecutionRead>;
|
|
778
|
+
readMerchantWorkExecution(requestId: string): Promise<WorkExecutionRead>;
|
|
779
|
+
listWorkEvents(input?: {
|
|
780
|
+
after?: number;
|
|
781
|
+
limit?: number;
|
|
782
|
+
}): Promise<WorkEventPage>;
|
|
711
783
|
/** Free public demand. Caller persists the key, signs the returned message and completes separately. */
|
|
712
784
|
createWorkRequestChallenge(input: FreshWorkRequestInput, idempotencyKey: string): Promise<BuyerRequestChallenge>;
|
|
713
785
|
completeWorkRequest(input: FreshWorkRequestInput & {
|
|
@@ -729,6 +801,24 @@ export declare class MerchantClient {
|
|
|
729
801
|
response: FreshWorkResponse;
|
|
730
802
|
replay: boolean;
|
|
731
803
|
}>;
|
|
804
|
+
createWorkRequestWithdrawalChallenge(id: string, input: {
|
|
805
|
+
buyer: EvmAddress;
|
|
806
|
+
}): Promise<{
|
|
807
|
+
challengeId: string;
|
|
808
|
+
message: string;
|
|
809
|
+
expiresAt: string;
|
|
810
|
+
}>;
|
|
811
|
+
withdrawWorkRequest(id: string, input: {
|
|
812
|
+
challengeId: string;
|
|
813
|
+
signature: EvmSignature;
|
|
814
|
+
}): Promise<FreshWorkDetail & {
|
|
815
|
+
replay: boolean;
|
|
816
|
+
}>;
|
|
817
|
+
reviseWorkRequestResponse(id: string, responseId: string, input: FreshWorkResponseRevision): Promise<{
|
|
818
|
+
response: FreshWorkResponse;
|
|
819
|
+
replay: boolean;
|
|
820
|
+
}>;
|
|
821
|
+
getWorkResponseHistory(id: string, responseId: string): Promise<FreshWorkResponseHistory>;
|
|
732
822
|
createWorkRequestSelectionChallenge(id: string, input: {
|
|
733
823
|
buyer: EvmAddress;
|
|
734
824
|
proposalId: string;
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { validPurchaseDecision, decisionCanonical } from "./purchase-decisions.js";
|
|
1
2
|
function requireEip191Signature(signature) {
|
|
2
3
|
if (!/^0x[0-9a-fA-F]{130}$/.test(signature)) {
|
|
3
4
|
throw new TypeError("signature must be a 65-byte EIP-191 signature encoded as 0x-prefixed hex.");
|
|
@@ -20,10 +21,11 @@ export class MerchantApiError extends Error {
|
|
|
20
21
|
this.name = "MerchantApiError";
|
|
21
22
|
}
|
|
22
23
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
function validSession(v) {
|
|
25
|
+
if (!isRecord(v) || !freshExactKeys(v, ["id", "name", "scopes", "createdAt", "expiresAt", "revokedAt", "lastUsedAt"]) || !uuid(v.id) || !publicText(v.name, 64) || !Array.isArray(v.scopes) || v.scopes.length < 1 || v.scopes.length > 3 || new Set(v.scopes).size !== v.scopes.length || !v.scopes.every(s => ["merchant:manage", "merchant:sales:read", "merchant:work"].includes(String(s))))
|
|
26
|
+
return false;
|
|
27
|
+
return [v.createdAt, v.expiresAt].every(x => typeof x === "string" && Number.isFinite(Date.parse(x))) && [v.revokedAt, v.lastUsedAt].every(x => x === null || typeof x === "string" && Number.isFinite(Date.parse(x))) && Date.parse(String(v.expiresAt)) > Date.parse(String(v.createdAt));
|
|
28
|
+
}
|
|
27
29
|
export class MerchantClient {
|
|
28
30
|
baseUrl;
|
|
29
31
|
#apiKey;
|
|
@@ -54,7 +56,7 @@ export class MerchantClient {
|
|
|
54
56
|
}
|
|
55
57
|
/** Full replacement, with separate worker-discovery opt-in. No artifact or wallet action. */
|
|
56
58
|
setWorkerProfile(input) {
|
|
57
|
-
if (!isWorkerProfileInput(input) || !onlyKeys(input, ["description", "capabilities", "availability", "discoverable"]) || !input.capabilities.every(cap => onlyKeys(cap, ["id", "title", "description", "inputDescription", "resultDescription", "resultContentTypes"])))
|
|
59
|
+
if (!isWorkerProfileInput(input) || !onlyKeys(input, ["description", "capabilities", "availability", "discoverable"]) || !input.capabilities.every(cap => onlyKeys(cap, ["id", "title", "description", "inputDescription", "resultDescription", "resultContentTypes", "settlementModes"])))
|
|
58
60
|
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.");
|
|
59
61
|
const body = normalizedWorkerProfile(input);
|
|
60
62
|
if (new TextEncoder().encode(JSON.stringify(body)).byteLength > 64 * 1024)
|
|
@@ -190,9 +192,35 @@ export class MerchantClient {
|
|
|
190
192
|
}
|
|
191
193
|
return payload;
|
|
192
194
|
}
|
|
195
|
+
/** Read immutable public decision terms, including for an unpublished exact offer. Verifies the origin-backed SHA-256; not a seller signature. */
|
|
196
|
+
async getOfferTerms(merchantId, offerId) {
|
|
197
|
+
const v = await this.request("GET", `/v1/merchants/${checkedWorkTicketId(merchantId)}/offers/${checkedWorkTicketId(offerId)}/terms`);
|
|
198
|
+
if (!isRecord(v) || Object.keys(v).length !== 14 || !publicText(v.title, 120) || !publicText(v.description, 300) || typeof v.version !== "string" || !/^(0|[1-9][0-9]{0,8})\.(0|[1-9][0-9]{0,8})\.(0|[1-9][0-9]{0,8})$/.test(v.version) || v.schema !== "402mob.offer-terms.v1" || v.merchantId !== merchantId || v.offerId !== offerId || !validPurchaseDecision(v.purchaseDecision) || !digest(v.hash) || !address(v.payee) || v.network !== "eip155:8453" || v.asset !== baseUsdc || !atomicAmount(v.amountAtomic) || !["application/json", "text/plain"].includes(v.contentType) || !isRecord(v.artifact) || v.artifact.algorithm !== "sha256-private-proof-v1" || !digest(v.artifact.commitment) || !Number.isSafeInteger(v.artifact.sizeUpperBound) || v.artifact.sizeUpperBound < 1024 || v.artifact.sizeUpperBound > 49152 || v.artifact.sizeUpperBound % 1024)
|
|
199
|
+
throw invalidPublicResponse();
|
|
200
|
+
const { hash, ...document } = v;
|
|
201
|
+
const nodeCryptoModule = "node:crypto";
|
|
202
|
+
const subtle = globalThis.crypto?.subtle ?? (await import(nodeCryptoModule)).webcrypto.subtle;
|
|
203
|
+
const bytes = await subtle.digest("SHA-256", new TextEncoder().encode(decisionCanonical(document)));
|
|
204
|
+
const expected = Array.from(new Uint8Array(bytes), x => x.toString(16).padStart(2, "0")).join("");
|
|
205
|
+
if (hash !== expected)
|
|
206
|
+
throw invalidPublicResponse();
|
|
207
|
+
return v;
|
|
208
|
+
}
|
|
193
209
|
setDiscoverable(discoverable) {
|
|
194
210
|
return this.request("PUT", "/v1/agent/merchant/discovery", { discoverable }, true);
|
|
195
211
|
}
|
|
212
|
+
async createMerchantSession(input) {
|
|
213
|
+
if (!isRecord(input) || Object.keys(input).some(k => !["name", "scopes", "expiresInSeconds"].includes(k)) || !publicText(input.name, 64) || !Array.isArray(input.scopes) || input.scopes.length < 1 || input.scopes.length > 3 || new Set(input.scopes).size !== input.scopes.length || !input.scopes.every(s => ["merchant:manage", "merchant:sales:read", "merchant:work"].includes(s)) || !Number.isInteger(input.expiresInSeconds) || input.expiresInSeconds < 60 || input.expiresInSeconds > 86400)
|
|
214
|
+
throw new TypeError("Provide a name, distinct merchant scopes and expiry of 60–86400 seconds.");
|
|
215
|
+
const v = await this.request("POST", "/v1/agent/merchant/sessions", input, true);
|
|
216
|
+
if (!isRecord(v) || !validSession(v.session) || v.session.name !== input.name.trim() || [...v.session.scopes].sort().join() !== [...input.scopes].sort().join() || Date.parse(v.session.expiresAt) - Date.parse(v.session.createdAt) !== input.expiresInSeconds * 1000 || typeof v.token !== "string" || !/^402mob_session_[A-Za-z0-9_-]{43}$/.test(v.token))
|
|
217
|
+
throw invalidPublicResponse();
|
|
218
|
+
return v;
|
|
219
|
+
}
|
|
220
|
+
async listMerchantSessions() { const v = await this.request("GET", "/v1/agent/merchant/sessions", undefined, true); if (!isRecord(v) || !Array.isArray(v.items) || v.items.length > 1000 || !v.items.every(validSession) || new Set(v.items.map(x => x.id)).size !== v.items.length)
|
|
221
|
+
throw invalidPublicResponse(); return v; }
|
|
222
|
+
async revokeMerchantSession(sessionId) { const v = await this.request("DELETE", `/v1/agent/merchant/sessions/${checkedWorkTicketId(sessionId)}`, undefined, true); if (!validSession(v) || v.id !== sessionId || v.revokedAt === null)
|
|
223
|
+
throw invalidPublicResponse(); return v; }
|
|
196
224
|
createOffer(input) {
|
|
197
225
|
return this.request("POST", "/v1/agent/merchant/offers", input, true);
|
|
198
226
|
}
|
|
@@ -249,6 +277,28 @@ export class MerchantClient {
|
|
|
249
277
|
submitWorkTicketResult(ticketId, input) {
|
|
250
278
|
return this.request("POST", `/v1/agent/work-tickets/${checkedWorkTicketId(ticketId)}/results`, input, true);
|
|
251
279
|
}
|
|
280
|
+
/** New source API: opt-in linkage; selection still creates neither a ticket nor a payment. */
|
|
281
|
+
getWorkAgreementTemplate(requestId) { return this.request("GET", `/v1/work-requests/${checkedWorkTicketId(requestId)}/agreement-template`); }
|
|
282
|
+
createWorkExecutionReadChallenge(requestId, buyer) { return this.request("POST", `/v1/work-requests/${checkedWorkTicketId(requestId)}/execution-challenges`, { buyer }); }
|
|
283
|
+
readWorkExecution(requestId, proof) { return this.request("POST", `/v1/work-requests/${checkedWorkTicketId(requestId)}/execution`, { ...proof, signature: requireEip191Signature(proof.signature) }); }
|
|
284
|
+
readMerchantWorkExecution(requestId) { return this.request("GET", `/v1/agent/work-requests/${checkedWorkTicketId(requestId)}/execution`, undefined, true); }
|
|
285
|
+
async listWorkEvents(input = {}) {
|
|
286
|
+
const after = input.after ?? 0, limit = input.limit ?? 50;
|
|
287
|
+
if (!Number.isSafeInteger(after) || after < 0 || !Number.isInteger(limit) || limit < 1 || limit > 100)
|
|
288
|
+
throw new TypeError("after must be a nonnegative safe integer; limit must be 1–100.");
|
|
289
|
+
const page = await this.request("GET", `/v1/agent/work-events?after=${after}&limit=${limit}`, undefined, true);
|
|
290
|
+
if (!uuid(page.merchantId) || !Array.isArray(page.items) || page.items.length > limit || typeof page.hasMore !== "boolean")
|
|
291
|
+
throw invalidPublicResponse();
|
|
292
|
+
let cursor = after;
|
|
293
|
+
for (const event of page.items) {
|
|
294
|
+
if (!isRecord(event) || !Number.isSafeInteger(event.sequence) || event.sequence <= cursor || !uuid(event.ticketId) || !workTicketStates.includes(event.state) || !timestamp(event.occurredAt))
|
|
295
|
+
throw invalidPublicResponse();
|
|
296
|
+
cursor = event.sequence;
|
|
297
|
+
}
|
|
298
|
+
if (page.nextCursor !== cursor || page.hasMore && page.items.length === 0)
|
|
299
|
+
throw invalidPublicResponse();
|
|
300
|
+
return page;
|
|
301
|
+
}
|
|
252
302
|
/** Free public demand. Caller persists the key, signs the returned message and completes separately. */
|
|
253
303
|
createWorkRequestChallenge(input, idempotencyKey) { return this.request("POST", "/v1/work-requests/challenges", input, false, { "Idempotency-Key": idempotencyKey }); }
|
|
254
304
|
completeWorkRequest(input, idempotencyKey) { return this.request("POST", "/v1/work-requests", input, false, { "Idempotency-Key": idempotencyKey }); }
|
|
@@ -265,6 +315,10 @@ export class MerchantClient {
|
|
|
265
315
|
getWorkRequest(id) { return this.request("GET", `/v1/work-requests/${encodeURIComponent(id)}`); }
|
|
266
316
|
proposeWorkRequest(id, input) { return this.request("POST", `/v1/agent/work-requests/${encodeURIComponent(id)}/proposals`, input, true); }
|
|
267
317
|
declineWorkRequest(id, input) { return this.request("POST", `/v1/agent/work-requests/${encodeURIComponent(id)}/declines`, input, true); }
|
|
318
|
+
createWorkRequestWithdrawalChallenge(id, input) { return this.request("POST", `/v1/work-requests/${encodeURIComponent(id)}/withdrawal-challenges`, input); }
|
|
319
|
+
withdrawWorkRequest(id, input) { return this.request("POST", `/v1/work-requests/${encodeURIComponent(id)}/withdrawals`, input); }
|
|
320
|
+
reviseWorkRequestResponse(id, responseId, input) { return this.request("POST", `/v1/agent/work-requests/${encodeURIComponent(id)}/responses/${encodeURIComponent(responseId)}/revisions`, input, true); }
|
|
321
|
+
getWorkResponseHistory(id, responseId) { return this.request("GET", `/v1/work-requests/${encodeURIComponent(id)}/responses/${encodeURIComponent(responseId)}/history`); }
|
|
268
322
|
createWorkRequestSelectionChallenge(id, input) { return this.request("POST", `/v1/work-requests/${encodeURIComponent(id)}/selection-challenges`, input); }
|
|
269
323
|
selectWorkRequestProposal(id, input) { return this.request("POST", `/v1/work-requests/${encodeURIComponent(id)}/selections`, input); }
|
|
270
324
|
createBuyerRequestChallenge(input, idempotencyKey) {
|
|
@@ -363,6 +417,14 @@ export class MerchantClient {
|
|
|
363
417
|
if (method === "PUT" && JSON.stringify(normalizedWorkerProfile(payload.workerProfile)) !== JSON.stringify(body))
|
|
364
418
|
throw invalidResponse();
|
|
365
419
|
}
|
|
420
|
+
if (path.endsWith("/execution-challenges")) {
|
|
421
|
+
if (!isRecord(body) || !address(body.buyer))
|
|
422
|
+
throw invalidResponse();
|
|
423
|
+
const id = path.split("/")[3];
|
|
424
|
+
const expected = ["402Mob private work execution read", `Origin: ${new URL(this.baseUrl).origin}`, `Request: ${id}`, `Buyer: ${body.buyer.toLowerCase()}`, `Challenge: ${payload.challengeId}`, `Expires: ${payload.expiresAt}`, "Read the linked execution only. This authorizes no grant, payment or result acceptance."].join("\n");
|
|
425
|
+
if (payload.message !== expected)
|
|
426
|
+
throw invalidResponse();
|
|
427
|
+
}
|
|
366
428
|
if ((path.startsWith("/v1/work-requests") || path.startsWith("/v1/agent/work-requests/")) && !isFreshWorkResponse(method, path, payload, body))
|
|
367
429
|
throw invalidResponse();
|
|
368
430
|
if ((path.startsWith("/v1/buyer-requests") || path.startsWith("/v1/agent/buyer-requests/")) && !isBuyerRequestResponse(method, path, payload, body))
|
|
@@ -402,14 +464,14 @@ function validWorkerCapabilityId(value) { return typeof value === "string" && /^
|
|
|
402
464
|
function onlyKeys(value, keys) { return Object.keys(value).every(key => keys.includes(key)); }
|
|
403
465
|
function boundedWorkerText(value, max) { return typeof value === "string" && value.trim().length > 0 && Array.from(value.trim()).length <= max; }
|
|
404
466
|
function normalizedWorkerProfile(value) {
|
|
405
|
-
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 };
|
|
467
|
+
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], ...(cap.settlementModes ? { settlementModes: [...cap.settlementModes] } : {}) })), availability: value.availability, discoverable: value.discoverable };
|
|
406
468
|
}
|
|
407
469
|
function isWorkerProfileInput(value) {
|
|
408
470
|
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)
|
|
409
471
|
return false;
|
|
410
472
|
if (new Set(value.capabilities.map(c => isRecord(c) ? c.id : null)).size !== value.capabilities.length)
|
|
411
473
|
return false;
|
|
412
|
-
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));
|
|
474
|
+
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) && (c.settlementModes === undefined || Array.isArray(c.settlementModes) && c.settlementModes.length >= 1 && c.settlementModes.length <= 2 && new Set(c.settlementModes).size === c.settlementModes.length && c.settlementModes.every(mode => mode === "direct_prepayment" || mode === "protected_inspection")));
|
|
413
475
|
}
|
|
414
476
|
function isWorkerProfileView(value) {
|
|
415
477
|
if (!isRecord(value) || !isRecord(value.merchant) || !uuid(value.merchant.id) || !publicText(value.merchant.name, 100) || !address(value.merchant.payoutAddress) || !isWorkerProfileInput(value.workerProfile))
|
|
@@ -486,7 +548,8 @@ function isAgentResource(value) {
|
|
|
486
548
|
return false;
|
|
487
549
|
const amountAtomic = value.amountAtomic;
|
|
488
550
|
const amountUSDC = value.amountUSDC;
|
|
489
|
-
return (value.
|
|
551
|
+
return (value.purchaseDecision === undefined ? value.offerTermsHash === undefined && value.offerTermsPath === undefined : validPurchaseDecision(value.purchaseDecision) && digest(value.offerTermsHash) && typeof value.offerTermsPath === "string" && value.offerTermsPath.endsWith(`/offers/${value.id}/terms`)) &&
|
|
552
|
+
(value.share === undefined || nonempty(value.share)) &&
|
|
490
553
|
(value.artifact === undefined || (isRecord(value.artifact) && value.artifact.algorithm === "sha256-private-proof-v1" &&
|
|
491
554
|
typeof value.artifact.commitment === "string" && /^[a-f0-9]{64}$/.test(value.artifact.commitment) &&
|
|
492
555
|
Number.isSafeInteger(value.artifact.sizeUpperBound) && Number(value.artifact.sizeUpperBound) >= 1024 &&
|
|
@@ -649,7 +712,21 @@ export { ProtectedWorkClient, ProtectedWorkApiError } from "./protected-work.js"
|
|
|
649
712
|
function isFreshRequest(v) {
|
|
650
713
|
if (!isRecord(v) || Object.hasOwn(v, "buyer"))
|
|
651
714
|
return false;
|
|
652
|
-
|
|
715
|
+
if (v.supersedesRequestId !== undefined && (!uuid(v.supersedesRequestId) || v.supersedesRequestId === v.id))
|
|
716
|
+
return false;
|
|
717
|
+
if (v.state === "withdrawn" || v.state === "superseded") {
|
|
718
|
+
if (!timestamp(v.closedAt))
|
|
719
|
+
return false;
|
|
720
|
+
}
|
|
721
|
+
else if (v.closedAt !== undefined)
|
|
722
|
+
return false;
|
|
723
|
+
if (v.state === "superseded") {
|
|
724
|
+
if (!uuid(v.supersededByRequestId) || v.supersededByRequestId === v.id)
|
|
725
|
+
return false;
|
|
726
|
+
}
|
|
727
|
+
else if (v.supersededByRequestId !== undefined)
|
|
728
|
+
return false;
|
|
729
|
+
return uuid(v.id) && v.kind === "fresh" && publicText(v.title, 100) && publicText(v.description, 2000) && usdcAtomic(v.maxPriceUSDC) !== undefined && Number(usdcAtomic(v.maxPriceUSDC)) > 0 && artifactType(v.contentType) && Array.isArray(v.acceptanceCriteria) && v.acceptanceCriteria.length >= 1 && v.acceptanceCriteria.length <= 10 && v.acceptanceCriteria.every(x => publicText(x, 300)) && Number.isInteger(v.expiresInSeconds) && Number(v.expiresInSeconds) >= 60 && Number(v.expiresInSeconds) <= 86400 && timestamp(v.createdAt) && timestamp(v.expiresAt) && ["open", "selected", "expired", "withdrawn", "superseded"].includes(String(v.state)) && (v.state === "selected" ? uuid(v.selectedProposalId) : v.selectedProposalId === null);
|
|
653
730
|
}
|
|
654
731
|
function isInspectionPlan(v) {
|
|
655
732
|
if (!isRecord(v) || !freshExactKeys(v, ["version", "targetUrl", "method", "paymentRecipient", "maxTestAmountAtomic", "maxPaidAttempts"]) || v.version !== "buyer-probe-v1" || v.method !== "GET" || !address(v.paymentRecipient) || v.maxPaidAttempts !== 1 || typeof v.maxTestAmountAtomic !== "string" || !/^(?:0|[1-9][0-9]{0,5}|1000000)$/.test(v.maxTestAmountAtomic) || typeof v.targetUrl !== "string" || v.targetUrl.length > 2048)
|
|
@@ -666,10 +743,12 @@ function sameInspectionPlan(a, b) { return isInspectionPlan(a) && isInspectionPl
|
|
|
666
743
|
function isFreshProposal(v) {
|
|
667
744
|
if (!isRecord(v) || !uuid(v.id) || !uuid(v.requestId) || !uuid(v.merchantId) || !address(v.payoutAddress) || !digest(v.termsHash) || !timestamp(v.createdAt) || !isRecord(v.terms) || !publicText(v.terms.note, 300))
|
|
668
745
|
return false;
|
|
746
|
+
if (v.revision === undefined ? v.previousTermsHash !== undefined : !Number.isInteger(v.revision) || Number(v.revision) < 2 || Number(v.revision) > 20 || !digest(v.previousTermsHash) || v.previousTermsHash === v.termsHash)
|
|
747
|
+
return false;
|
|
669
748
|
const t = v.terms;
|
|
670
749
|
if (v.kind === "decline")
|
|
671
750
|
return ["endpoint_unavailable", "unsupported_payment", "scope_unsupported", "capacity_unavailable", "other"].includes(String(t.reason));
|
|
672
|
-
return v.kind === "proposal" && typeof t.capability === "string" && /^[a-z0-9][a-z0-9-]{1,62}[a-z0-9]$/.test(t.capability) && publicText(t.deliverable, 300) && usdcAtomic(t.priceUSDC) !== undefined && Number(usdcAtomic(t.priceUSDC)) > 0 && timestamp(t.deliveryDeadline) && usdcAtomic(t.testPurchaseBudgetUSDC) !== undefined && (t.settlementMode === "protected_inspection" ? t.testPurchasePayer === "buyer" && isInspectionPlan(t.inspection) && t.inspection.maxTestAmountAtomic === usdcAtomic(t.testPurchaseBudgetUSDC) : t.settlementMode === undefined && t.inspection === undefined && t.testPurchasePayer === "seller");
|
|
751
|
+
return v.kind === "proposal" && typeof t.capability === "string" && /^[a-z0-9][a-z0-9-]{1,62}[a-z0-9]$/.test(t.capability) && publicText(t.deliverable, 300) && usdcAtomic(t.priceUSDC) !== undefined && Number(usdcAtomic(t.priceUSDC)) > 0 && timestamp(t.deliveryDeadline) && usdcAtomic(t.testPurchaseBudgetUSDC) !== undefined && (t.settlementMode === "protected_inspection" ? t.testPurchasePayer === "buyer" && isInspectionPlan(t.inspection) && t.inspection.maxTestAmountAtomic === usdcAtomic(t.testPurchaseBudgetUSDC) : (t.settlementMode === undefined || t.settlementMode === "direct_prepayment") && t.inspection === undefined && t.testPurchasePayer === "seller");
|
|
673
752
|
}
|
|
674
753
|
function freshExactKeys(value, keys) {
|
|
675
754
|
return Object.keys(value).length === keys.length && keys.every(k => Object.hasOwn(value, k));
|
|
@@ -700,13 +779,36 @@ function isFreshDetail(v, id) {
|
|
|
700
779
|
}
|
|
701
780
|
function isFreshWorkResponse(method, path, v, body) {
|
|
702
781
|
const args = isRecord(body) ? body : {}, id = path.match(/^\/v1\/(?:agent\/)?work-requests\/([^/?]+)/)?.[1];
|
|
782
|
+
if (path.endsWith("/agreement-template"))
|
|
783
|
+
return validAgreementTemplate(v, id);
|
|
784
|
+
if (path.endsWith("/execution-challenges"))
|
|
785
|
+
return uuid(v.challengeId) && nonempty(v.message) && timestamp(v.expiresAt);
|
|
786
|
+
if (path.endsWith("/execution"))
|
|
787
|
+
return validWorkExecution(v, id);
|
|
703
788
|
const challenge = () => uuid(v.challengeId) && nonempty(v.message) && timestamp(v.expiresAt);
|
|
704
789
|
if (path === "/v1/work-requests/challenges")
|
|
705
790
|
return challenge() && timestamp(v.requestExpiresAt) && digest(v.policyHash) && Date.parse(String(v.expiresAt)) <= Date.parse(String(v.requestExpiresAt));
|
|
706
791
|
if (path === "/v1/work-requests" && method === "POST")
|
|
707
|
-
return isFreshDetail(v) && typeof v.replay === "boolean" && v.request.title === String(args.title).trim() && v.request.description === String(args.description).trim() && v.request.contentType === args.contentType && v.request.expiresInSeconds === args.expiresInSeconds && usdcAtomic(v.request.maxPriceUSDC) === usdcAtomic(args.maxPriceUSDC) && JSON.stringify(v.request.acceptanceCriteria) === JSON.stringify(args.acceptanceCriteria?.map(x => typeof x === 'string' ? x.trim() : x));
|
|
792
|
+
return isFreshDetail(v) && typeof v.replay === "boolean" && v.request.supersedesRequestId === args.supersedesRequestId && v.request.title === String(args.title).trim() && v.request.description === String(args.description).trim() && v.request.contentType === args.contentType && v.request.expiresInSeconds === args.expiresInSeconds && usdcAtomic(v.request.maxPriceUSDC) === usdcAtomic(args.maxPriceUSDC) && JSON.stringify(v.request.acceptanceCriteria) === JSON.stringify(args.acceptanceCriteria?.map(x => typeof x === 'string' ? x.trim() : x));
|
|
708
793
|
if (path.startsWith("/v1/work-requests?") || path === "/v1/work-requests" && method === "GET")
|
|
709
794
|
return Array.isArray(v.items) && v.items.length <= 50 && v.items.every(isFreshRequest) && (v.nextCursor === null || Number.isSafeInteger(v.nextCursor) && Number(v.nextCursor) >= 0);
|
|
795
|
+
if (path.endsWith("/withdrawal-challenges"))
|
|
796
|
+
return challenge();
|
|
797
|
+
if (path.endsWith("/withdrawals"))
|
|
798
|
+
return isFreshDetail(v, id) && v.request.state === "withdrawn" && typeof v.replay === "boolean";
|
|
799
|
+
const responseId = path.match(/\/responses\/([^/]+)\/(?:history|revisions)$/)?.[1];
|
|
800
|
+
if (path.endsWith("/history")) {
|
|
801
|
+
if (v.requestId !== id || v.responseId !== responseId || !Array.isArray(v.versions) || v.versions.length < 1 || v.versions.length > 20 || !v.versions.every(isFreshProposal))
|
|
802
|
+
return false;
|
|
803
|
+
const versions = v.versions;
|
|
804
|
+
return versions.every((p, i) => p.id === responseId && p.requestId === id && p.merchantId === versions[0].merchantId && (i === 0 ? p.revision === undefined && p.previousTermsHash === undefined : p.revision === i + 1 && p.previousTermsHash === versions[i - 1].termsHash));
|
|
805
|
+
}
|
|
806
|
+
if (path.endsWith("/revisions")) {
|
|
807
|
+
if (!isFreshProposal(v.response) || v.response.id !== responseId || v.response.requestId !== id || v.response.kind !== args.kind || v.response.previousTermsHash !== args.expectedTermsHash || !isRecord(args.terms) || typeof v.replay !== "boolean")
|
|
808
|
+
return false;
|
|
809
|
+
const terms = v.response.terms;
|
|
810
|
+
return Object.keys(terms).length === Object.keys(args.terms).length && Object.entries(args.terms).every(([k, x]) => k === "inspection" ? sameInspectionPlan(terms[k], x) : ["priceUSDC", "testPurchaseBudgetUSDC"].includes(k) ? usdcAtomic(terms[k]) === usdcAtomic(x) : JSON.stringify(terms[k]) === JSON.stringify(typeof x === 'string' ? x.trim() : x));
|
|
811
|
+
}
|
|
710
812
|
if (path.endsWith("/selection-challenges"))
|
|
711
813
|
return challenge() && isFreshProposal(v.proposal) && v.proposal.kind === "proposal" && v.proposal.requestId === id && v.proposal.id === args.proposalId;
|
|
712
814
|
if (path.endsWith("/proposals") || path.endsWith("/declines")) {
|
|
@@ -718,3 +820,23 @@ function isFreshWorkResponse(method, path, v, body) {
|
|
|
718
820
|
return isFreshDetail(v, id) && (method === "GET" || v.request.state === "selected" && typeof v.replay === "boolean");
|
|
719
821
|
}
|
|
720
822
|
export { ProtectedInspectionClient, ProtectedInspectionApiError } from "./protected-inspection.js";
|
|
823
|
+
function validAgreementReference(v, id) {
|
|
824
|
+
return isRecord(v) && freshExactKeys(v, ["protocol", "requestId", "proposalId", "proposalTermsHash", "settlementMode"]) && v.protocol === "402mob.work-agreement.v1" && uuid(v.requestId) && (!id || v.requestId === id) && uuid(v.proposalId) && digest(v.proposalTermsHash) && v.settlementMode === "direct_prepayment";
|
|
825
|
+
}
|
|
826
|
+
function validAgreementTemplate(v, id) {
|
|
827
|
+
if (!validAgreementReference(v.agreement, id) || !isRecord(v.grant) || !isRecord(v.grant.input) || !validAgreementReference(v.grant.agreement, id) || stableJson(v.agreement) !== stableJson(v.grant.agreement))
|
|
828
|
+
return false;
|
|
829
|
+
const g = v.grant, i = g.input;
|
|
830
|
+
return freshExactKeys(v, ["agreement", "grant", "tracksTicketCreation", "paymentAuthorized", "grantSignatureRequired", "quoteAcceptanceRequired"]) && freshExactKeys(g, ["merchantId", "capability", "input", "maxPriceUSDC", "expiresInSeconds", "agreement"]) && uuid(g.merchantId) && typeof g.capability === "string" && /^[a-z0-9][a-z0-9-]{1,62}[a-z0-9]$/.test(g.capability) && usdcAtomic(g.maxPriceUSDC) !== undefined && BigInt(usdcAtomic(g.maxPriceUSDC)) > 0n && g.expiresInSeconds === 3600 && i.workRequestId === v.agreement.requestId && i.proposalId === v.agreement.proposalId && i.proposalTermsHash === v.agreement.proposalTermsHash && artifactType(i.contentType) && address(i.payoutAddress) && timestamp(i.deliveryDeadline) && publicText(i.deliverable, 300) && i.testPurchasePayer === "seller" && v.tracksTicketCreation === true && v.paymentAuthorized === false && v.grantSignatureRequired === true && v.quoteAcceptanceRequired === true;
|
|
831
|
+
}
|
|
832
|
+
function validWorkExecution(v, id) {
|
|
833
|
+
if (!freshExactKeys(v, ["agreement", "execution"]))
|
|
834
|
+
return false;
|
|
835
|
+
if (v.agreement === null)
|
|
836
|
+
return v.execution === null;
|
|
837
|
+
if (!isRecord(v.agreement) || !isRecord(v.execution))
|
|
838
|
+
return false;
|
|
839
|
+
const a = v.agreement, e = v.execution;
|
|
840
|
+
const reference = { protocol: a.protocol, requestId: a.requestId, proposalId: a.proposalId, proposalTermsHash: a.proposalTermsHash, settlementMode: a.settlementMode };
|
|
841
|
+
return validAgreementReference(reference, id) && uuid(a.ticketId) && uuid(a.merchantId) && address(a.buyer) && address(a.payTo) && atomicAmount(a.amountAtomic) && publicText(a.deliverable, 300) && artifactType(a.contentType) && timestamp(a.deliveryDeadline) && timestamp(a.createdAt) && e.ticketId === a.ticketId && workTicketStates.includes(e.state) && (e.paymentOperationId === null || uuid(e.paymentOperationId)) && (e.paymentState === null || ["executing", "staged", "settling", "pending", "settled", "failed", "expired"].includes(String(e.paymentState))) && ((e.paymentOperationId === null) === (e.paymentState === null)) && typeof e.resultAvailable === "boolean" && (!e.resultAvailable || digest(e.resultHash)) && (e.resultHash === null || digest(e.resultHash)) && (e.buyerVerdict === null || e.buyerVerdict === "accepted" || e.buyerVerdict === "disputed") && e.deliveryDeadline === a.deliveryDeadline && typeof e.deadlinePassed === "boolean" && (e.retentionExpiresAt === null || timestamp(e.retentionExpiresAt));
|
|
842
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export interface PurchaseDecision {
|
|
2
|
+
protocol: '402mob.purchase-decision.v1';
|
|
3
|
+
intendedUse: string;
|
|
4
|
+
resultSchema: {
|
|
5
|
+
uri: string;
|
|
6
|
+
version: string;
|
|
7
|
+
sha256: string;
|
|
8
|
+
} | null;
|
|
9
|
+
coverage: {
|
|
10
|
+
asOf: string;
|
|
11
|
+
from?: string;
|
|
12
|
+
through?: string;
|
|
13
|
+
};
|
|
14
|
+
limitations: string[];
|
|
15
|
+
reuseTerms: string;
|
|
16
|
+
publicSample?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface OfferTerms {
|
|
19
|
+
schema: '402mob.offer-terms.v1';
|
|
20
|
+
merchantId: string;
|
|
21
|
+
offerId: string;
|
|
22
|
+
version: string;
|
|
23
|
+
title: string;
|
|
24
|
+
description: string;
|
|
25
|
+
payee: string;
|
|
26
|
+
network: 'eip155:8453';
|
|
27
|
+
asset: string;
|
|
28
|
+
amountAtomic: string;
|
|
29
|
+
contentType: 'application/json' | 'text/plain';
|
|
30
|
+
artifact: {
|
|
31
|
+
algorithm: 'sha256-private-proof-v1';
|
|
32
|
+
commitment: string;
|
|
33
|
+
sizeUpperBound: number;
|
|
34
|
+
};
|
|
35
|
+
purchaseDecision: PurchaseDecision;
|
|
36
|
+
hash: string;
|
|
37
|
+
}
|
|
38
|
+
export declare const decisionCanonical: (v: unknown) => string;
|
|
39
|
+
export declare function validPurchaseDecision(v: unknown): v is PurchaseDecision;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const record = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
2
|
+
const keys = (v, required, optional = []) => record(v) && required.every(k => Object.hasOwn(v, k)) && Object.keys(v).every(k => required.includes(k) || optional.includes(k));
|
|
3
|
+
const text = (v, max) => typeof v === 'string' && v.trim() === v && v.length > 0 && Array.from(v).length <= max;
|
|
4
|
+
const date = (v) => typeof v === 'string' && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/.test(v) && Number.isFinite(Date.parse(v)) && new Date(v).toISOString().slice(0, 19) === v.slice(0, 19);
|
|
5
|
+
export const decisionCanonical = (v) => v === null || typeof v !== 'object' ? JSON.stringify(v) : Array.isArray(v) ? '[' + v.map(decisionCanonical).join(',') + ']' : '{' + Object.keys(v).sort().map(k => JSON.stringify(k) + ':' + decisionCanonical(v[k])).join(',') + '}';
|
|
6
|
+
export function validPurchaseDecision(v) {
|
|
7
|
+
if (!keys(v, ['protocol', 'intendedUse', 'resultSchema', 'coverage', 'limitations', 'reuseTerms'], ['publicSample']) || v.protocol !== '402mob.purchase-decision.v1' || !text(v.intendedUse, 500) || !text(v.reuseTerms, 3000) || !Array.isArray(v.limitations) || v.limitations.length < 1 || v.limitations.length > 8 || !v.limitations.every(x => text(x, 300)) || Object.hasOwn(v, 'publicSample') && !text(v.publicSample, 1024))
|
|
8
|
+
return false;
|
|
9
|
+
if (v.resultSchema !== null) {
|
|
10
|
+
const r = v.resultSchema;
|
|
11
|
+
if (!keys(r, ['uri', 'version', 'sha256']) || typeof r.uri !== 'string' || r.uri.length > 2048 || typeof r.version !== 'string' || !/^(0|[1-9][0-9]{0,8})\.(0|[1-9][0-9]{0,8})\.(0|[1-9][0-9]{0,8})$/.test(r.version) || typeof r.sha256 !== 'string' || !/^[a-f0-9]{64}$/.test(r.sha256))
|
|
12
|
+
return false;
|
|
13
|
+
try {
|
|
14
|
+
const u = new URL(r.uri);
|
|
15
|
+
if (!r.uri.startsWith('https://') || /[\s#]/u.test(r.uri) || u.protocol !== 'https:' || u.username || u.password || u.hash)
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
const c = v.coverage;
|
|
23
|
+
if (!keys(c, ['asOf'], ['from', 'through']) || !date(c.asOf) || (c.from === undefined) !== (c.through === undefined) || c.from !== undefined && (!date(c.from) || !date(c.through) || Date.parse(c.from) > Date.parse(c.through)))
|
|
24
|
+
return false;
|
|
25
|
+
return new TextEncoder().encode(decisionCanonical(v)).length <= 8192;
|
|
26
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { ArtifactContentType, EvmAddress, FreshWorkHandoff, WorkTicketState } from './index.js';
|
|
2
|
+
export interface WorkAgreementReference {
|
|
3
|
+
protocol: '402mob.work-agreement.v1';
|
|
4
|
+
requestId: string;
|
|
5
|
+
proposalId: string;
|
|
6
|
+
proposalTermsHash: string;
|
|
7
|
+
settlementMode: 'direct_prepayment';
|
|
8
|
+
}
|
|
9
|
+
export interface WorkAgreementTemplate {
|
|
10
|
+
agreement: WorkAgreementReference;
|
|
11
|
+
grant: FreshWorkHandoff['body'] & {
|
|
12
|
+
agreement: WorkAgreementReference;
|
|
13
|
+
};
|
|
14
|
+
tracksTicketCreation: true;
|
|
15
|
+
paymentAuthorized: false;
|
|
16
|
+
grantSignatureRequired: true;
|
|
17
|
+
quoteAcceptanceRequired: true;
|
|
18
|
+
}
|
|
19
|
+
export interface WorkAgreementRecord extends WorkAgreementReference {
|
|
20
|
+
ticketId: string;
|
|
21
|
+
buyer: EvmAddress;
|
|
22
|
+
merchantId: string;
|
|
23
|
+
payTo: EvmAddress;
|
|
24
|
+
amountAtomic: string;
|
|
25
|
+
deliverable: string;
|
|
26
|
+
contentType: ArtifactContentType;
|
|
27
|
+
deliveryDeadline: string;
|
|
28
|
+
createdAt: string;
|
|
29
|
+
}
|
|
30
|
+
export interface WorkExecutionRead {
|
|
31
|
+
agreement: WorkAgreementRecord | null;
|
|
32
|
+
execution: {
|
|
33
|
+
ticketId: string;
|
|
34
|
+
state: WorkTicketState;
|
|
35
|
+
paymentOperationId: string | null;
|
|
36
|
+
paymentState: 'executing' | 'staged' | 'settling' | 'pending' | 'settled' | 'failed' | 'expired' | null;
|
|
37
|
+
resultAvailable: boolean;
|
|
38
|
+
resultHash: string | null;
|
|
39
|
+
buyerVerdict: 'accepted' | 'disputed' | null;
|
|
40
|
+
deliveryDeadline: string;
|
|
41
|
+
deadlinePassed: boolean;
|
|
42
|
+
retentionExpiresAt: string | null;
|
|
43
|
+
} | null;
|
|
44
|
+
}
|
|
45
|
+
export interface WorkEventPage {
|
|
46
|
+
merchantId: string;
|
|
47
|
+
items: Array<{
|
|
48
|
+
sequence: number;
|
|
49
|
+
ticketId: string;
|
|
50
|
+
state: WorkTicketState;
|
|
51
|
+
occurredAt: string;
|
|
52
|
+
}>;
|
|
53
|
+
nextCursor: number;
|
|
54
|
+
hasMore: boolean;
|
|
55
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import {MerchantClient,type WorkerProfileInput,type FreshWorkProposalInput,type EvmAddress} from '../dist/index.js';
|
|
2
2
|
|
|
3
|
-
//
|
|
3
|
+
// Unreleased source SDK example with explicit settlementModes. No side effects on import and no signing or payment.
|
|
4
4
|
export const inspectionCapabilityId='payment-readiness-inspection';
|
|
5
5
|
export const inspectionWorkerProfile:WorkerProfileInput={
|
|
6
6
|
description:'Inspect buyer-signed endpoint observations and verify claimed Base USDC test payments under the protected inspection policy.',
|
|
7
7
|
availability:'accepting',discoverable:true,
|
|
8
|
-
capabilities:[{id:inspectionCapabilityId,title:'Payment-readiness evidence inspection',description:'Classify one bounded buyer probe. Negative and unresolved findings can be valid delivery. No independent HTTP truth or general audit certification.',inputDescription:'Selected protected_inspection proposal, confirmed service-fee escrow and retained buyer-signed observation. The buyer runs and funds any endpoint test.',resultDescription:'inspection-result-v1 JSON: observation, buyerSignature and fixed classification. Passing evidence is subject to escrow review and appeal.',resultContentTypes:['application/json']}]
|
|
8
|
+
capabilities:[{id:inspectionCapabilityId,title:'Payment-readiness evidence inspection',description:'Classify one bounded buyer probe. Negative and unresolved findings can be valid delivery. No independent HTTP truth or general audit certification.',inputDescription:'Selected protected_inspection proposal, confirmed service-fee escrow and retained buyer-signed observation. The buyer runs and funds any endpoint test.',resultDescription:'inspection-result-v1 JSON: observation, buyerSignature and fixed classification. Passing evidence is subject to escrow review and appeal.',settlementModes:['protected_inspection'],resultContentTypes:['application/json']}]
|
|
9
9
|
};
|
|
10
10
|
|
|
11
11
|
// Review real capacity and discovery consent first. Profile PUT fully replaces
|