402mob-merchant-sdk 0.4.1 → 0.5.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,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.0
4
+
5
+ - Add `ProtectedWorkClient` for the separate experimental JSON escrow HTTP API, generated request/response types and schema checks. Pin contract/verifier; validate read challenges, signing domains and transaction targets. No signing, broadcasting, automatic retry or replica management. Includes protocol integration tests and an example. Earlier versions do not include these methods.
6
+
7
+ - Require an authenticated-read binding for dispute writes. Reject unexpected signing origins/consent, contradictory payment and delivery states, and immutable binding drift across evidence events. Challenge issuance reveals no operation or case state.
8
+
9
+ - Require stateless dispute challenge tokens; distinguish write acknowledgments from authenticated evidence reads. Validate operation, statement, binding and lifecycle consistency; preserve settlement status and post-closure seller evidence.
10
+
11
+ - Add wallet-signed delivery complaint, seller response, buyer closure and evidence-read methods and types. No automatic payment or refund.
12
+
13
+ ## 0.4.2 — published
14
+
15
+ - Add optional `artifact` commitment and `share` fields to resolved resources. Validate supplied commitments and preserve compatibility with older servers that omit them.
16
+
3
17
  ## 0.4.1
4
18
 
5
19
  - Documentation-only release. The packaged README no longer states that the CLI's published SDK dependency remains 0.3.1; it states that published CLI 0.2.0 pins SDK 0.4.0. No code changes: `dist` is byte-identical to 0.4.0.
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # 402Mob Merchant SDK
2
2
 
3
+ Version 0.5.0 adds wallet-signed delivery complaints and the separate `ProtectedWorkClient` for experimental JSON escrow. Install `402mob-merchant-sdk@0.5.0` for these methods; earlier versions do not include them.
4
+
3
5
  **Version 0.4.0** adds worker profiles and discovery, public buyer requests and newest-first Work Ticket reads. Upgrade from 0.3.1 to use these additions. Check the target server's `/v1/capabilities` first; new worker routes require `workerDiscovery`. See [CHANGELOG.md](./CHANGELOG.md).
4
6
 
5
7
  An open-source, dependency-free TypeScript client for resolving agent resources and enrolling or operating a 402Mob merchant from a Node.js server or agent runtime.
@@ -280,3 +282,33 @@ The supported states are `open`, `claimed`, `quoted`, `accepted`, `paying`, `pai
280
282
  SDK requests reject HTTP redirects and never retry automatically. An interrupted mutation may have completed. Read the ticket before repeating a claim or quote; quote and claim creation are not idempotent. Repeating identical result content is supported while retained. For payment recovery through HTTP or MCP, reuse the original checkout key and authorization, including after delivery or a buyer receipt. The SDK does not submit payments.
281
283
 
282
284
  These methods are available from version 0.3.0.
285
+
286
+
287
+ ## Delivery complaints (unreleased source)
288
+
289
+ `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.
290
+
291
+ ```ts
292
+ const challenge = await client.createDeliveryDisputeChallenge(operationId, {
293
+ wallet: account.address, action: "read"
294
+ });
295
+ const signature = await account.signMessage({ message: challenge.message });
296
+ const response = await client.submitDeliveryDisputeAction(operationId, {
297
+ challengeId: challenge.challengeId, challengeToken: challenge.challengeToken, signature
298
+ });
299
+ if (response.kind === "evidence") {
300
+ // Inspect response.dispute and response.payment.
301
+ }
302
+ ```
303
+
304
+ No API key or payment is sent. Sign with the original payer or recipient wallet; keep proofs private. Retry exact signed writes after interruption; reads need fresh challenges. This records statements, not arbitration or refunds. See [the complete workflow](https://evidence.contact/delivery-disputes.md).
305
+
306
+ Dispute writes (including retries) return `kind: "acknowledgment"` and the original action receipt only. Fresh wallet-signed reads return `kind: "evidence"` with the current case. Return the opaque `challengeToken` unchanged; it is required for submission. Buyer closure preserves the seller’s right to append evidence. Settlement status is independent of operation/result expiry. SDK checks enforce structural and lifecycle consistency, not cryptographic verification of exported signatures.
307
+
308
+ Dispute writes require `binding` from a fresh signed read. Read challenges contain no private operation data and do not confirm that an operation or case exists. Check the returned origin and consent text before signing; `dispute_binding_conflict` requires a new read and signed statement. The SDK rejects origin/consent changes, contradictory delivery/payment states and inconsistent immutable bindings across events.
309
+
310
+ ## Protected work (0.5.0)
311
+
312
+ `ProtectedWorkClient` covers all primary protected-work HTTP endpoints with generated OpenAPI types and request/response validation. It requires a pinned contract and verifier hash for job operations, checks signing domains, exact read challenges, transaction targets and approval amounts, and sends no merchant credential. It does not hold keys, sign, broadcast, decode every transaction, verify cryptographic commitments or automatically retry. A configured server may relay signed actions; funding is never relayed.
313
+
314
+ Start with `new ProtectedWorkClient({baseUrl}).getCapabilities()` and stop when `configured` is false. The isolated Base test does not activate production. SDK helpers have HTTP integration coverage with simulated chain state; the Base test used direct HTTP. Use version 0.5.0 for these helpers. See [the full client guide](https://evidence.contact/merchant-sdk.md#protected-json-work-source-050), [protocol](https://evidence.contact/protected-work.md) and [source examples](examples/protected-work.ts). Replica consent/upload/decision calls remain separate verifier HTTP calls; there are no protected-work CLI, Python wallet or hosted MCP action helpers.
package/dist/index.d.ts CHANGED
@@ -4,6 +4,119 @@ export type TransactionHash = `0x${string}`;
4
4
  export type ProofMethod = "approve_zero" | "transfer_zero";
5
5
  export type ArtifactContentType = "application/json" | "text/plain; charset=utf-8";
6
6
  export type WorkTicketState = "open" | "claimed" | "quoted" | "accepted" | "paying" | "paid" | "delivered" | "accepted_by_buyer" | "disputed" | "expired";
7
+ export interface DeliveryDisputeBinding {
8
+ payer: EvmAddress;
9
+ recipient: EvmAddress;
10
+ requestHash: string;
11
+ workTicketId: string | null;
12
+ quoteTermsHash: string | null;
13
+ resultHashAtOpen: string | null;
14
+ transaction: string | null;
15
+ amountAtomic: string;
16
+ asset: EvmAddress;
17
+ network: "eip155:8453";
18
+ }
19
+ export type DeliveryDisputeRequest = {
20
+ wallet: EvmAddress;
21
+ action: "open";
22
+ expectedRevision: 0;
23
+ binding: DeliveryDisputeBinding;
24
+ reason: "not_delivered" | "unreadable" | "terms_mismatch";
25
+ detail: string;
26
+ evidenceHashes: string[];
27
+ } | {
28
+ wallet: EvmAddress;
29
+ action: "respond";
30
+ expectedRevision: number;
31
+ binding: DeliveryDisputeBinding;
32
+ detail: string;
33
+ evidenceHashes: string[];
34
+ } | {
35
+ wallet: EvmAddress;
36
+ action: "close";
37
+ expectedRevision: number;
38
+ binding: DeliveryDisputeBinding;
39
+ outcome: "resolved" | "unresolved";
40
+ detail: string;
41
+ evidenceHashes: string[];
42
+ } | {
43
+ wallet: EvmAddress;
44
+ action: "read";
45
+ };
46
+ export interface DeliveryDisputeChallenge {
47
+ challengeToken: string;
48
+ challengeId: string;
49
+ message: string;
50
+ expiresAt: string;
51
+ submit: string;
52
+ }
53
+ export interface DeliveryDisputeEvent {
54
+ revision: number;
55
+ action: "open" | "respond" | "close";
56
+ wallet: string;
57
+ createdAt: string;
58
+ statement: Exclude<DeliveryDisputeRequest, {
59
+ action: "read";
60
+ }>;
61
+ message: string;
62
+ signature: string;
63
+ challengeId: string;
64
+ }
65
+ export interface DeliveryDisputeEvidence {
66
+ protocol: "402mob.delivery-dispute.v1";
67
+ kind: "evidence";
68
+ operationId: string;
69
+ operationState: string;
70
+ binding: DeliveryDisputeBinding;
71
+ dispute: null | {
72
+ operationId: string;
73
+ buyer: string;
74
+ seller: string;
75
+ status: "open" | "responded" | "closed";
76
+ outcome: "resolved" | "unresolved" | null;
77
+ revision: number;
78
+ openedAt: string;
79
+ expiresAt: string;
80
+ responseDueAt: string;
81
+ buyerReviewDueAt: string | null;
82
+ binding: {
83
+ requestHash: string;
84
+ workTicketId: string | null;
85
+ quoteTermsHash: string | null;
86
+ resultHashAtOpen: string | null;
87
+ transaction: string | null;
88
+ amountAtomic: string;
89
+ asset: string;
90
+ network: string;
91
+ };
92
+ events: DeliveryDisputeEvent[];
93
+ };
94
+ payment: {
95
+ state: "settled" | "pending" | "failed" | "unconfirmed";
96
+ transaction: string | null;
97
+ };
98
+ delivery: {
99
+ resultHash: string | null;
100
+ resultAvailable: boolean;
101
+ };
102
+ responseOverdue: boolean;
103
+ buyerReviewOverdue: boolean;
104
+ scope: string;
105
+ replay: boolean;
106
+ }
107
+ export interface DeliveryDisputeAcknowledgment {
108
+ protocol: "402mob.delivery-dispute.v1";
109
+ kind: "acknowledgment";
110
+ operationId: string;
111
+ receipt: {
112
+ challengeId: string;
113
+ action: "open" | "respond" | "close";
114
+ revision: number;
115
+ createdAt: string;
116
+ };
117
+ replay: boolean;
118
+ }
119
+ export type DeliveryDisputeResponse = DeliveryDisputeEvidence | DeliveryDisputeAcknowledgment;
7
120
  export interface EnrollmentChallenge {
8
121
  challengeId: string;
9
122
  message: string;
@@ -124,8 +237,16 @@ export interface AgentResourceProvider {
124
237
  payoutAddress?: EvmAddress;
125
238
  createdAt?: string;
126
239
  }
240
+ /** Public commitment; the private verifier is returned only with paid delivery. */
241
+ export interface ArtifactCommitment {
242
+ algorithm: "sha256-private-proof-v1";
243
+ commitment: string;
244
+ sizeUpperBound: number;
245
+ }
127
246
  /** A provider-published, fixed-price resource descriptor. */
128
247
  export interface AgentResource {
248
+ artifact?: ArtifactCommitment;
249
+ share?: string;
129
250
  id: string;
130
251
  seriesId: string;
131
252
  version: string;
@@ -521,6 +642,16 @@ export declare class MerchantClient {
521
642
  }): Promise<BuyerRequestDetail & {
522
643
  replay: boolean;
523
644
  }>;
645
+ /** Free, wallet-signed complaint/response/closure/read. No bearer key or payment is sent. */
646
+ createDeliveryDisputeChallenge(operationId: string, input: DeliveryDisputeRequest): Promise<DeliveryDisputeChallenge>;
647
+ /** Retry the same signed write after interruption; private reads require a fresh challenge. */
648
+ submitDeliveryDisputeAction(operationId: string, input: {
649
+ challengeId: string;
650
+ challengeToken: string;
651
+ signature: EvmSignature;
652
+ }): Promise<DeliveryDisputeResponse>;
524
653
  private request;
525
654
  }
526
- export {};
655
+ export { ProtectedWorkClient, ProtectedWorkApiError } from "./protected-work.js";
656
+ export type { ProtectedWorkClientOptions } from "./protected-work.js";
657
+ export type * from "./protected-work-types.js";
package/dist/index.js CHANGED
@@ -4,6 +4,7 @@ function requireEip191Signature(signature) {
4
4
  }
5
5
  return signature;
6
6
  }
7
+ const disputeConsent = "This signature authorizes only this statement or private read. It does not authorize payment, refund or adjudication. Response targets: seller 48 hours after opening; buyer 7 days after first response. Silence is not acceptance.";
7
8
  function buyerRequestKey(key) {
8
9
  if (!/^[A-Za-z0-9_-]{16,128}$/.test(key))
9
10
  throw new TypeError("Persist an idempotency key of 16–128 letters, digits, underscores or hyphens.");
@@ -274,6 +275,18 @@ export class MerchantClient {
274
275
  selectBuyerRequestOffer(requestId, input) {
275
276
  return this.request("POST", `/v1/buyer-requests/${checkedWorkTicketId(requestId)}/selections`, { ...input, signature: requireEip191Signature(input.signature) });
276
277
  }
278
+ /** Free, wallet-signed complaint/response/closure/read. No bearer key or payment is sent. */
279
+ createDeliveryDisputeChallenge(operationId, input) {
280
+ if (!isDisputeRequest(input))
281
+ throw new TypeError("Provide a participant wallet, supported action, current revision and bounded statement/evidence hashes.");
282
+ return this.request("POST", `/v1/operations/${checkedWorkTicketId(operationId)}/dispute-challenges`, input);
283
+ }
284
+ /** Retry the same signed write after interruption; private reads require a fresh challenge. */
285
+ submitDeliveryDisputeAction(operationId, input) {
286
+ if (!digest(input.challengeId) || !disputeToken(input.challengeToken))
287
+ throw new TypeError("Return the exact challengeId and challengeToken from the challenge response.");
288
+ return this.request("POST", `/v1/operations/${checkedWorkTicketId(operationId)}/dispute-actions`, { ...input, signature: requireEip191Signature(input.signature) });
289
+ }
277
290
  async request(method, path, body, authenticated = false, extraHeaders = {}) {
278
291
  if (authenticated && !this.#apiKey) {
279
292
  throw new Error("This request requires a merchant API key. Use client.withApiKey(apiKey).");
@@ -305,6 +318,17 @@ export class MerchantClient {
305
318
  const invalidResponse = () => new MerchantApiError(response.status, "invalid_response", "402Mob returned an incomplete response. The request may have completed; preserve recovery data. If enrollment or rotation completed without a usable key, use credential rotation.");
306
319
  if (!isRecord(payload))
307
320
  throw invalidResponse();
321
+ if (path.endsWith("/dispute-challenges")) {
322
+ const expected = path.replace(/dispute-challenges$/, "dispute-actions"), id = path.split("/")[3];
323
+ if (!digest(payload.challengeId) || !disputeToken(payload.challengeToken) || !nonempty(payload.message) || !timestamp(payload.expiresAt) || payload.submit !== expected || !isDisputeRequest(body))
324
+ throw invalidResponse();
325
+ const statement = { ...body, wallet: body.wallet.toLowerCase(), ...(body.action !== "read" ? { detail: body.detail.trim(), binding: { ...body.binding, payer: body.binding.payer.toLowerCase(), recipient: body.binding.recipient.toLowerCase(), asset: body.binding.asset.toLowerCase() } } : {}) };
326
+ const fields = disputeMessage(payload.message);
327
+ if (!fields || fields.origin !== new URL(this.baseUrl).origin || fields.operation !== id || fields.nonce !== payload.challengeId || fields.expires !== payload.expiresAt || stableJson(fields.statement) !== stableJson(statement) || stableJson(fields.binding) !== stableJson(body.action === "read" ? null : statement.binding))
328
+ throw invalidResponse();
329
+ }
330
+ if (path.endsWith("/dispute-actions") && !isDisputeResponse(payload, path.split("/")[3], body, new URL(this.baseUrl).origin))
331
+ throw invalidResponse();
308
332
  if (path.startsWith("/v1/worker-directory?")) {
309
333
  if (!Array.isArray(payload.items) || !payload.items.every(item => isWorkerProfileView(item) && item.workerProfile.discoverable && item.workerProfile.availability === "accepting") || !(payload.nextCursor === null || Number.isSafeInteger(payload.nextCursor) && Number(payload.nextCursor) >= 0))
310
334
  throw invalidResponse();
@@ -443,7 +467,12 @@ function isAgentResource(value) {
443
467
  return false;
444
468
  const amountAtomic = value.amountAtomic;
445
469
  const amountUSDC = value.amountUSDC;
446
- return nonempty(value.id) && nonempty(value.seriesId) && nonempty(value.version) &&
470
+ return (value.share === undefined || nonempty(value.share)) &&
471
+ (value.artifact === undefined || (isRecord(value.artifact) && value.artifact.algorithm === "sha256-private-proof-v1" &&
472
+ typeof value.artifact.commitment === "string" && /^[a-f0-9]{64}$/.test(value.artifact.commitment) &&
473
+ Number.isSafeInteger(value.artifact.sizeUpperBound) && Number(value.artifact.sizeUpperBound) >= 1024 &&
474
+ Number(value.artifact.sizeUpperBound) <= 49152 && Number(value.artifact.sizeUpperBound) % 1024 === 0)) &&
475
+ nonempty(value.id) && nonempty(value.seriesId) && nonempty(value.version) &&
447
476
  nonempty(value.title) && typeof value.description === "string" &&
448
477
  typeof amountAtomic === "string" && /^[1-9][0-9]*$/.test(amountAtomic) &&
449
478
  typeof amountUSDC === "string" && /^(0|[1-9][0-9]*)(\.[0-9]{1,6})?$/.test(amountUSDC) &&
@@ -474,3 +503,127 @@ function checkedWorkTicketId(id) {
474
503
  }
475
504
  return id;
476
505
  }
506
+ function isDisputeRequest(v) {
507
+ if (!isRecord(v) || !address(v.wallet))
508
+ return false;
509
+ if (v.action === "read")
510
+ return onlyKeys(v, ["wallet", "action"]);
511
+ if (!["open", "respond", "close"].includes(String(v.action)) || !Number.isInteger(v.expectedRevision) || Number(v.expectedRevision) < 0 || Number(v.expectedRevision) > 32 || !boundedWorkerText(v.detail, 2000) || !isRecord(v.binding) || !validMessageBinding(v.binding) || !Array.isArray(v.evidenceHashes) || v.evidenceHashes.length > 8 || !v.evidenceHashes.every(digest))
512
+ return false;
513
+ const common = ["wallet", "action", "expectedRevision", "binding", "detail", "evidenceHashes"];
514
+ if (v.action === "open")
515
+ return v.expectedRevision === 0 && ["not_delivered", "unreadable", "terms_mismatch"].includes(String(v.reason)) && onlyKeys(v, [...common, "reason"]);
516
+ if (v.action === "close")
517
+ return ["resolved", "unresolved"].includes(String(v.outcome)) && onlyKeys(v, [...common, "outcome"]);
518
+ return onlyKeys(v, common);
519
+ }
520
+ function disputeToken(v) { return typeof v === "string" && /^[A-Za-z0-9_-]{40,40000}$/.test(v); }
521
+ function stableJson(v) {
522
+ if (Array.isArray(v))
523
+ return `[${v.map(stableJson).join(",")}]`;
524
+ if (isRecord(v))
525
+ return `{${Object.keys(v).sort().map(k => JSON.stringify(k) + ":" + stableJson(v[k])).join(",")}}`;
526
+ return JSON.stringify(v);
527
+ }
528
+ function disputeMessage(v) {
529
+ if (typeof v !== "string")
530
+ return;
531
+ const lines = v.split("\n");
532
+ if (lines.length !== 8 || lines[7] !== disputeConsent || lines[0] !== "402Mob delivery dispute authorization" || !lines[1]?.startsWith("Origin: ") || !lines[2]?.startsWith("Operation: ") || !lines[3]?.startsWith("Statement: ") || !lines[4]?.startsWith("Binding: ") || !lines[5]?.startsWith("Nonce: ") || !lines[6]?.startsWith("Expires: "))
533
+ return;
534
+ try {
535
+ const binding = JSON.parse(lines[4].slice(9));
536
+ if (binding !== null && !isRecord(binding))
537
+ return;
538
+ return { origin: lines[1].slice(8), operation: lines[2].slice(11), nonce: lines[5].slice(7), expires: lines[6].slice(9), statement: JSON.parse(lines[3].slice(11)), binding };
539
+ }
540
+ catch {
541
+ return;
542
+ }
543
+ }
544
+ function validDisputeBinding(b) {
545
+ return isRecord(b) && onlyKeys(b, ["requestHash", "workTicketId", "quoteTermsHash", "resultHashAtOpen", "transaction", "amountAtomic", "asset", "network"]) && digest(b.requestHash) &&
546
+ ((b.workTicketId === null && b.quoteTermsHash === null) || (uuid(b.workTicketId) && digest(b.quoteTermsHash))) && (b.resultHashAtOpen === null || digest(b.resultHashAtOpen)) &&
547
+ (b.transaction === null || typeof b.transaction === "string" && /^0x[0-9a-fA-F]{64}$/.test(b.transaction)) && atomicAmount(b.amountAtomic) && address(b.asset) && b.asset.toLowerCase() === baseUsdc && b.network === "eip155:8453";
548
+ }
549
+ function validMessageBinding(b) {
550
+ const { payer, recipient, ...rest } = b;
551
+ return address(payer) && address(recipient) && validDisputeBinding(rest);
552
+ }
553
+ function isDisputeResponse(v, operationId, proof, origin) {
554
+ if (v.protocol !== "402mob.delivery-dispute.v1" || v.operationId !== operationId || !isRecord(proof) || typeof v.replay !== "boolean")
555
+ return false;
556
+ if (v.kind === "acknowledgment") {
557
+ const r = v.receipt;
558
+ return onlyKeys(v, ["protocol", "kind", "operationId", "receipt", "replay"]) && isRecord(r) && onlyKeys(r, ["challengeId", "action", "revision", "createdAt"]) && r.challengeId === proof.challengeId && ["open", "respond", "close"].includes(String(r.action)) && Number.isInteger(r.revision) && Number(r.revision) >= 1 && Number(r.revision) <= 32 && timestamp(r.createdAt);
559
+ }
560
+ if (v.kind !== "evidence" || v.replay !== false || !onlyKeys(v, ["protocol", "kind", "operationId", "operationState", "binding", "dispute", "payment", "delivery", "responseOverdue", "buyerReviewOverdue", "scope", "replay"]) || !["executing", "staged", "settling", "pending", "settled", "failed", "expired"].includes(String(v.operationState)) || !isRecord(v.payment) || !onlyKeys(v.payment, ["state", "transaction"]) || !["settled", "pending", "failed", "unconfirmed"].includes(String(v.payment.state)) || !(v.payment.transaction === null || typeof v.payment.transaction === "string" && /^0x[0-9a-fA-F]{64}$/.test(v.payment.transaction)) || !isRecord(v.delivery) || !onlyKeys(v.delivery, ["resultHash", "resultAvailable"]) || typeof v.delivery.resultAvailable !== "boolean" || !(v.delivery.resultHash === null || digest(v.delivery.resultHash)) || typeof v.responseOverdue !== "boolean" || typeof v.buyerReviewOverdue !== "boolean" || !nonempty(v.scope))
561
+ return false;
562
+ if (!isRecord(v.binding) || !validMessageBinding(v.binding) || v.binding.transaction !== v.payment.transaction || v.binding.resultHashAtOpen !== v.delivery.resultHash)
563
+ return false;
564
+ if (v.payment.state === "settled" && v.payment.transaction === null)
565
+ return false;
566
+ const expectedPayment = { executing: "unconfirmed", staged: "unconfirmed", settling: "pending", pending: "pending", settled: "settled", failed: "failed" };
567
+ if (v.operationState !== "expired" && v.payment.state !== expectedPayment[String(v.operationState)])
568
+ return false;
569
+ if (v.operationState === "expired" && !["settled", "unconfirmed"].includes(String(v.payment.state)))
570
+ return false;
571
+ if (v.delivery.resultAvailable && (v.delivery.resultHash === null || v.payment.state !== "settled"))
572
+ return false;
573
+ if (v.binding.workTicketId === null && v.delivery.resultAvailable && v.operationState !== "settled")
574
+ return false;
575
+ if (v.dispute === null)
576
+ return !v.responseOverdue && !v.buyerReviewOverdue;
577
+ const d = v.dispute;
578
+ if (!isRecord(d) || d.operationId !== operationId || !address(d.buyer) || !address(d.seller) || !["open", "responded", "closed"].includes(String(d.status)) || !(d.outcome === null || ["resolved", "unresolved"].includes(String(d.outcome))) || !Number.isInteger(d.revision) || Number(d.revision) < 1 || Number(d.revision) > 32 || !timestamp(d.openedAt) || !timestamp(d.expiresAt) || !timestamp(d.responseDueAt) || !(d.buyerReviewDueAt === null || timestamp(d.buyerReviewDueAt)) || !validDisputeBinding(d.binding) || !Array.isArray(d.events) || d.events.length !== d.revision || v.payment.state !== "settled" || d.binding.transaction !== v.payment.transaction)
579
+ return false;
580
+ const currentBinding = v.binding, caseBinding = d.binding;
581
+ const immutable = ["requestHash", "workTicketId", "quoteTermsHash", "transaction", "amountAtomic", "asset", "network"];
582
+ if (v.binding.payer !== d.buyer || v.binding.recipient !== d.seller || immutable.some(k => currentBinding[k] !== caseBinding[k]))
583
+ return false;
584
+ if (Date.parse(d.expiresAt) !== Date.parse(d.openedAt) + 2592000000 || Date.parse(d.responseDueAt) !== Date.parse(d.openedAt) + 172800000)
585
+ return false;
586
+ let closed, firstResponse, lastTime = Date.parse(d.openedAt);
587
+ const seen = new Set();
588
+ for (let i = 0; i < d.events.length; i++) {
589
+ const e = d.events[i];
590
+ if (!isRecord(e) || e.revision !== i + 1 || !address(e.wallet) || !timestamp(e.createdAt) || !isDisputeRequest(e.statement) || e.statement.action === "read" || e.statement.action !== e.action || e.statement.expectedRevision !== i || e.wallet.toLowerCase() !== e.statement.wallet.toLowerCase() || typeof e.signature !== "string" || !/^0x[0-9a-fA-F]{130}$/.test(e.signature) || !digest(e.challengeId) || seen.has(e.challengeId))
591
+ return false;
592
+ seen.add(e.challengeId);
593
+ const at = Date.parse(e.createdAt);
594
+ if (at < lastTime || at >= Date.parse(d.expiresAt))
595
+ return false;
596
+ lastTime = at;
597
+ const m = disputeMessage(e.message);
598
+ if (!m || m.origin !== origin || m.operation !== operationId || m.nonce !== e.challengeId || !timestamp(m.expires) || !isRecord(m.binding) || !validMessageBinding(m.binding) || m.binding.payer !== d.buyer || m.binding.recipient !== d.seller || m.binding.requestHash !== d.binding.requestHash || m.binding.quoteTermsHash !== d.binding.quoteTermsHash || immutable.some(k => m.binding[k] !== caseBinding[k]) || stableJson(m.binding) !== stableJson(e.statement.binding) || stableJson(m.statement) !== stableJson(e.statement))
599
+ return false;
600
+ if (i === 0) {
601
+ const { payer: _payer, recipient: _recipient, ...binding } = m.binding;
602
+ if (e.action !== "open" || e.wallet.toLowerCase() !== d.buyer.toLowerCase() || e.createdAt !== d.openedAt || stableJson(binding) !== stableJson(d.binding))
603
+ return false;
604
+ }
605
+ else if (e.action === "respond") {
606
+ if (e.wallet.toLowerCase() !== d.seller.toLowerCase())
607
+ return false;
608
+ if (!closed)
609
+ firstResponse ??= e;
610
+ }
611
+ else if (e.action === "close") {
612
+ if (closed || e.wallet.toLowerCase() !== d.buyer.toLowerCase())
613
+ return false;
614
+ closed = e;
615
+ }
616
+ else
617
+ return false;
618
+ }
619
+ if (d.status !== (closed ? "closed" : firstResponse ? "responded" : "open"))
620
+ return false;
621
+ if (d.outcome !== (closed ? closed.statement.outcome : null))
622
+ return false;
623
+ if (d.buyerReviewDueAt !== (firstResponse ? new Date(Date.parse(firstResponse.createdAt) + 604800000).toISOString() : null))
624
+ return false;
625
+ if ((d.status !== "open" && v.responseOverdue) || (d.status !== "responded" && v.buyerReviewOverdue))
626
+ return false;
627
+ return true;
628
+ }
629
+ export { ProtectedWorkClient, ProtectedWorkApiError } from "./protected-work.js";
@@ -0,0 +1,2 @@
1
+ export declare const protectedSchemaHash = "5c69849fb52f35cf29367043b58156d274955a79ef7ee52f0c78d6b4561e1b40";
2
+ export declare const protectedSchemas: Record<string, unknown>;