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 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.6.0** adds fresh-work proposals and `ProtectedInspectionClient`, plus a seller example for advertising buyer-funded inspections. Use `402mob-merchant-sdk@0.6.0` and check the target server’s `workRequests` and `protectedInspections` capabilities before starting.
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.0` for these methods; earlier versions do not include them.
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.4.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 (unreleased source)
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 immutable response per merchant is allowed.
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
- * Minimal, dependency-free HTTP client for 402Mob agent resource resolution and merchant management.
25
- * It never signs messages, submits wallet transactions, persists API keys, or retries requests.
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.share === undefined || nonempty(value.share)) &&
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
- 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"].includes(String(v.state)) && (v.state === "selected" ? uuid(v.selectedProposalId) : v.selectedProposalId === null);
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
- // Source SDK 0.6.0 example. No side effects on import and no signing or payment.
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "402mob-merchant-sdk",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "TypeScript client for resolving agent resources and enrolling and operating 402Mob merchants.",
5
5
  "license": "MIT",
6
6
  "repository": {