402mob-merchant-sdk 0.4.2 → 0.6.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 +24 -1
- package/README.md +53 -2
- package/dist/index.d.ts +264 -1
- package/dist/index.js +239 -0
- package/dist/protected-inspection-schemas.d.ts +2 -0
- package/dist/protected-inspection-schemas.js +3117 -0
- package/dist/protected-inspection-types.d.ts +623 -0
- package/dist/protected-inspection-types.js +1 -0
- package/dist/protected-inspection.d.ts +42 -0
- package/dist/protected-inspection.js +197 -0
- package/dist/protected-work-schemas.d.ts +2 -0
- package/dist/protected-work-schemas.js +2558 -0
- package/dist/protected-work-types.d.ts +524 -0
- package/dist/protected-work-types.js +1 -0
- package/dist/protected-work.d.ts +40 -0
- package/dist/protected-work.js +187 -0
- package/examples/protected-inspection-service.ts +31 -0
- package/examples/protected-work.ts +21 -0
- package/examples/respond-to-work-request.ts +21 -0
- package/package.json +1 -1
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.");
|
|
@@ -249,6 +250,23 @@ export class MerchantClient {
|
|
|
249
250
|
return this.request("POST", `/v1/agent/work-tickets/${checkedWorkTicketId(ticketId)}/results`, input, true);
|
|
250
251
|
}
|
|
251
252
|
/** Free public demand. Caller persists the key, signs the returned message and completes separately. */
|
|
253
|
+
createWorkRequestChallenge(input, idempotencyKey) { return this.request("POST", "/v1/work-requests/challenges", input, false, { "Idempotency-Key": idempotencyKey }); }
|
|
254
|
+
completeWorkRequest(input, idempotencyKey) { return this.request("POST", "/v1/work-requests", input, false, { "Idempotency-Key": idempotencyKey }); }
|
|
255
|
+
listWorkRequests(input = {}) {
|
|
256
|
+
const q = new URLSearchParams();
|
|
257
|
+
if (input.query !== undefined)
|
|
258
|
+
q.set("q", input.query);
|
|
259
|
+
if (input.limit !== undefined)
|
|
260
|
+
q.set("limit", String(input.limit));
|
|
261
|
+
if (input.cursor !== undefined)
|
|
262
|
+
q.set("cursor", String(input.cursor));
|
|
263
|
+
return this.request("GET", "/v1/work-requests?" + q);
|
|
264
|
+
}
|
|
265
|
+
getWorkRequest(id) { return this.request("GET", `/v1/work-requests/${encodeURIComponent(id)}`); }
|
|
266
|
+
proposeWorkRequest(id, input) { return this.request("POST", `/v1/agent/work-requests/${encodeURIComponent(id)}/proposals`, input, true); }
|
|
267
|
+
declineWorkRequest(id, input) { return this.request("POST", `/v1/agent/work-requests/${encodeURIComponent(id)}/declines`, input, true); }
|
|
268
|
+
createWorkRequestSelectionChallenge(id, input) { return this.request("POST", `/v1/work-requests/${encodeURIComponent(id)}/selection-challenges`, input); }
|
|
269
|
+
selectWorkRequestProposal(id, input) { return this.request("POST", `/v1/work-requests/${encodeURIComponent(id)}/selections`, input); }
|
|
252
270
|
createBuyerRequestChallenge(input, idempotencyKey) {
|
|
253
271
|
return this.request("POST", "/v1/buyer-requests/challenges", input, false, { "Idempotency-Key": buyerRequestKey(idempotencyKey) });
|
|
254
272
|
}
|
|
@@ -274,6 +292,18 @@ export class MerchantClient {
|
|
|
274
292
|
selectBuyerRequestOffer(requestId, input) {
|
|
275
293
|
return this.request("POST", `/v1/buyer-requests/${checkedWorkTicketId(requestId)}/selections`, { ...input, signature: requireEip191Signature(input.signature) });
|
|
276
294
|
}
|
|
295
|
+
/** Free, wallet-signed complaint/response/closure/read. No bearer key or payment is sent. */
|
|
296
|
+
createDeliveryDisputeChallenge(operationId, input) {
|
|
297
|
+
if (!isDisputeRequest(input))
|
|
298
|
+
throw new TypeError("Provide a participant wallet, supported action, current revision and bounded statement/evidence hashes.");
|
|
299
|
+
return this.request("POST", `/v1/operations/${checkedWorkTicketId(operationId)}/dispute-challenges`, input);
|
|
300
|
+
}
|
|
301
|
+
/** Retry the same signed write after interruption; private reads require a fresh challenge. */
|
|
302
|
+
submitDeliveryDisputeAction(operationId, input) {
|
|
303
|
+
if (!digest(input.challengeId) || !disputeToken(input.challengeToken))
|
|
304
|
+
throw new TypeError("Return the exact challengeId and challengeToken from the challenge response.");
|
|
305
|
+
return this.request("POST", `/v1/operations/${checkedWorkTicketId(operationId)}/dispute-actions`, { ...input, signature: requireEip191Signature(input.signature) });
|
|
306
|
+
}
|
|
277
307
|
async request(method, path, body, authenticated = false, extraHeaders = {}) {
|
|
278
308
|
if (authenticated && !this.#apiKey) {
|
|
279
309
|
throw new Error("This request requires a merchant API key. Use client.withApiKey(apiKey).");
|
|
@@ -305,6 +335,17 @@ export class MerchantClient {
|
|
|
305
335
|
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
336
|
if (!isRecord(payload))
|
|
307
337
|
throw invalidResponse();
|
|
338
|
+
if (path.endsWith("/dispute-challenges")) {
|
|
339
|
+
const expected = path.replace(/dispute-challenges$/, "dispute-actions"), id = path.split("/")[3];
|
|
340
|
+
if (!digest(payload.challengeId) || !disputeToken(payload.challengeToken) || !nonempty(payload.message) || !timestamp(payload.expiresAt) || payload.submit !== expected || !isDisputeRequest(body))
|
|
341
|
+
throw invalidResponse();
|
|
342
|
+
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() } } : {}) };
|
|
343
|
+
const fields = disputeMessage(payload.message);
|
|
344
|
+
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))
|
|
345
|
+
throw invalidResponse();
|
|
346
|
+
}
|
|
347
|
+
if (path.endsWith("/dispute-actions") && !isDisputeResponse(payload, path.split("/")[3], body, new URL(this.baseUrl).origin))
|
|
348
|
+
throw invalidResponse();
|
|
308
349
|
if (path.startsWith("/v1/worker-directory?")) {
|
|
309
350
|
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
351
|
throw invalidResponse();
|
|
@@ -322,6 +363,8 @@ export class MerchantClient {
|
|
|
322
363
|
if (method === "PUT" && JSON.stringify(normalizedWorkerProfile(payload.workerProfile)) !== JSON.stringify(body))
|
|
323
364
|
throw invalidResponse();
|
|
324
365
|
}
|
|
366
|
+
if ((path.startsWith("/v1/work-requests") || path.startsWith("/v1/agent/work-requests/")) && !isFreshWorkResponse(method, path, payload, body))
|
|
367
|
+
throw invalidResponse();
|
|
325
368
|
if ((path.startsWith("/v1/buyer-requests") || path.startsWith("/v1/agent/buyer-requests/")) && !isBuyerRequestResponse(method, path, payload, body))
|
|
326
369
|
throw invalidResponse();
|
|
327
370
|
if (path === "/v1/merchants/enrollment-challenges" || path === "/v1/merchants/credential-challenges") {
|
|
@@ -479,3 +522,199 @@ function checkedWorkTicketId(id) {
|
|
|
479
522
|
}
|
|
480
523
|
return id;
|
|
481
524
|
}
|
|
525
|
+
function isDisputeRequest(v) {
|
|
526
|
+
if (!isRecord(v) || !address(v.wallet))
|
|
527
|
+
return false;
|
|
528
|
+
if (v.action === "read")
|
|
529
|
+
return onlyKeys(v, ["wallet", "action"]);
|
|
530
|
+
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))
|
|
531
|
+
return false;
|
|
532
|
+
const common = ["wallet", "action", "expectedRevision", "binding", "detail", "evidenceHashes"];
|
|
533
|
+
if (v.action === "open")
|
|
534
|
+
return v.expectedRevision === 0 && ["not_delivered", "unreadable", "terms_mismatch"].includes(String(v.reason)) && onlyKeys(v, [...common, "reason"]);
|
|
535
|
+
if (v.action === "close")
|
|
536
|
+
return ["resolved", "unresolved"].includes(String(v.outcome)) && onlyKeys(v, [...common, "outcome"]);
|
|
537
|
+
return onlyKeys(v, common);
|
|
538
|
+
}
|
|
539
|
+
function disputeToken(v) { return typeof v === "string" && /^[A-Za-z0-9_-]{40,40000}$/.test(v); }
|
|
540
|
+
function stableJson(v) {
|
|
541
|
+
if (Array.isArray(v))
|
|
542
|
+
return `[${v.map(stableJson).join(",")}]`;
|
|
543
|
+
if (isRecord(v))
|
|
544
|
+
return `{${Object.keys(v).sort().map(k => JSON.stringify(k) + ":" + stableJson(v[k])).join(",")}}`;
|
|
545
|
+
return JSON.stringify(v);
|
|
546
|
+
}
|
|
547
|
+
function disputeMessage(v) {
|
|
548
|
+
if (typeof v !== "string")
|
|
549
|
+
return;
|
|
550
|
+
const lines = v.split("\n");
|
|
551
|
+
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: "))
|
|
552
|
+
return;
|
|
553
|
+
try {
|
|
554
|
+
const binding = JSON.parse(lines[4].slice(9));
|
|
555
|
+
if (binding !== null && !isRecord(binding))
|
|
556
|
+
return;
|
|
557
|
+
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 };
|
|
558
|
+
}
|
|
559
|
+
catch {
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
function validDisputeBinding(b) {
|
|
564
|
+
return isRecord(b) && onlyKeys(b, ["requestHash", "workTicketId", "quoteTermsHash", "resultHashAtOpen", "transaction", "amountAtomic", "asset", "network"]) && digest(b.requestHash) &&
|
|
565
|
+
((b.workTicketId === null && b.quoteTermsHash === null) || (uuid(b.workTicketId) && digest(b.quoteTermsHash))) && (b.resultHashAtOpen === null || digest(b.resultHashAtOpen)) &&
|
|
566
|
+
(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";
|
|
567
|
+
}
|
|
568
|
+
function validMessageBinding(b) {
|
|
569
|
+
const { payer, recipient, ...rest } = b;
|
|
570
|
+
return address(payer) && address(recipient) && validDisputeBinding(rest);
|
|
571
|
+
}
|
|
572
|
+
function isDisputeResponse(v, operationId, proof, origin) {
|
|
573
|
+
if (v.protocol !== "402mob.delivery-dispute.v1" || v.operationId !== operationId || !isRecord(proof) || typeof v.replay !== "boolean")
|
|
574
|
+
return false;
|
|
575
|
+
if (v.kind === "acknowledgment") {
|
|
576
|
+
const r = v.receipt;
|
|
577
|
+
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);
|
|
578
|
+
}
|
|
579
|
+
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))
|
|
580
|
+
return false;
|
|
581
|
+
if (!isRecord(v.binding) || !validMessageBinding(v.binding) || v.binding.transaction !== v.payment.transaction || v.binding.resultHashAtOpen !== v.delivery.resultHash)
|
|
582
|
+
return false;
|
|
583
|
+
if (v.payment.state === "settled" && v.payment.transaction === null)
|
|
584
|
+
return false;
|
|
585
|
+
const expectedPayment = { executing: "unconfirmed", staged: "unconfirmed", settling: "pending", pending: "pending", settled: "settled", failed: "failed" };
|
|
586
|
+
if (v.operationState !== "expired" && v.payment.state !== expectedPayment[String(v.operationState)])
|
|
587
|
+
return false;
|
|
588
|
+
if (v.operationState === "expired" && !["settled", "unconfirmed"].includes(String(v.payment.state)))
|
|
589
|
+
return false;
|
|
590
|
+
if (v.delivery.resultAvailable && (v.delivery.resultHash === null || v.payment.state !== "settled"))
|
|
591
|
+
return false;
|
|
592
|
+
if (v.binding.workTicketId === null && v.delivery.resultAvailable && v.operationState !== "settled")
|
|
593
|
+
return false;
|
|
594
|
+
if (v.dispute === null)
|
|
595
|
+
return !v.responseOverdue && !v.buyerReviewOverdue;
|
|
596
|
+
const d = v.dispute;
|
|
597
|
+
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)
|
|
598
|
+
return false;
|
|
599
|
+
const currentBinding = v.binding, caseBinding = d.binding;
|
|
600
|
+
const immutable = ["requestHash", "workTicketId", "quoteTermsHash", "transaction", "amountAtomic", "asset", "network"];
|
|
601
|
+
if (v.binding.payer !== d.buyer || v.binding.recipient !== d.seller || immutable.some(k => currentBinding[k] !== caseBinding[k]))
|
|
602
|
+
return false;
|
|
603
|
+
if (Date.parse(d.expiresAt) !== Date.parse(d.openedAt) + 2592000000 || Date.parse(d.responseDueAt) !== Date.parse(d.openedAt) + 172800000)
|
|
604
|
+
return false;
|
|
605
|
+
let closed, firstResponse, lastTime = Date.parse(d.openedAt);
|
|
606
|
+
const seen = new Set();
|
|
607
|
+
for (let i = 0; i < d.events.length; i++) {
|
|
608
|
+
const e = d.events[i];
|
|
609
|
+
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))
|
|
610
|
+
return false;
|
|
611
|
+
seen.add(e.challengeId);
|
|
612
|
+
const at = Date.parse(e.createdAt);
|
|
613
|
+
if (at < lastTime || at >= Date.parse(d.expiresAt))
|
|
614
|
+
return false;
|
|
615
|
+
lastTime = at;
|
|
616
|
+
const m = disputeMessage(e.message);
|
|
617
|
+
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))
|
|
618
|
+
return false;
|
|
619
|
+
if (i === 0) {
|
|
620
|
+
const { payer: _payer, recipient: _recipient, ...binding } = m.binding;
|
|
621
|
+
if (e.action !== "open" || e.wallet.toLowerCase() !== d.buyer.toLowerCase() || e.createdAt !== d.openedAt || stableJson(binding) !== stableJson(d.binding))
|
|
622
|
+
return false;
|
|
623
|
+
}
|
|
624
|
+
else if (e.action === "respond") {
|
|
625
|
+
if (e.wallet.toLowerCase() !== d.seller.toLowerCase())
|
|
626
|
+
return false;
|
|
627
|
+
if (!closed)
|
|
628
|
+
firstResponse ??= e;
|
|
629
|
+
}
|
|
630
|
+
else if (e.action === "close") {
|
|
631
|
+
if (closed || e.wallet.toLowerCase() !== d.buyer.toLowerCase())
|
|
632
|
+
return false;
|
|
633
|
+
closed = e;
|
|
634
|
+
}
|
|
635
|
+
else
|
|
636
|
+
return false;
|
|
637
|
+
}
|
|
638
|
+
if (d.status !== (closed ? "closed" : firstResponse ? "responded" : "open"))
|
|
639
|
+
return false;
|
|
640
|
+
if (d.outcome !== (closed ? closed.statement.outcome : null))
|
|
641
|
+
return false;
|
|
642
|
+
if (d.buyerReviewDueAt !== (firstResponse ? new Date(Date.parse(firstResponse.createdAt) + 604800000).toISOString() : null))
|
|
643
|
+
return false;
|
|
644
|
+
if ((d.status !== "open" && v.responseOverdue) || (d.status !== "responded" && v.buyerReviewOverdue))
|
|
645
|
+
return false;
|
|
646
|
+
return true;
|
|
647
|
+
}
|
|
648
|
+
export { ProtectedWorkClient, ProtectedWorkApiError } from "./protected-work.js";
|
|
649
|
+
function isFreshRequest(v) {
|
|
650
|
+
if (!isRecord(v) || Object.hasOwn(v, "buyer"))
|
|
651
|
+
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);
|
|
653
|
+
}
|
|
654
|
+
function isInspectionPlan(v) {
|
|
655
|
+
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)
|
|
656
|
+
return false;
|
|
657
|
+
try {
|
|
658
|
+
const u = new URL(v.targetUrl);
|
|
659
|
+
return u.protocol === "https:" && !u.username && !u.password && !u.hash && (!u.port || u.port === "443") && u.href === v.targetUrl;
|
|
660
|
+
}
|
|
661
|
+
catch {
|
|
662
|
+
return false;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
function sameInspectionPlan(a, b) { return isInspectionPlan(a) && isInspectionPlan(b) && Object.entries(a).every(([key, value]) => value === b[key]); }
|
|
666
|
+
function isFreshProposal(v) {
|
|
667
|
+
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
|
+
return false;
|
|
669
|
+
const t = v.terms;
|
|
670
|
+
if (v.kind === "decline")
|
|
671
|
+
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");
|
|
673
|
+
}
|
|
674
|
+
function freshExactKeys(value, keys) {
|
|
675
|
+
return Object.keys(value).length === keys.length && keys.every(k => Object.hasOwn(value, k));
|
|
676
|
+
}
|
|
677
|
+
function isFreshDetail(v, id) {
|
|
678
|
+
if (!isRecord(v) || !isFreshRequest(v.request) || id && v.request.id !== id || !Array.isArray(v.responses) || v.responses.length > 20 || !v.responses.every(isFreshProposal))
|
|
679
|
+
return false;
|
|
680
|
+
const r = v.request, responses = v.responses;
|
|
681
|
+
if (new Set(responses.map(p => p.merchantId)).size !== responses.length || new Set(responses.map(p => p.id)).size !== responses.length || responses.some(p => p.requestId !== r.id || p.kind === "proposal" && BigInt(usdcAtomic(p.terms.priceUSDC)) > BigInt(usdcAtomic(r.maxPriceUSDC))))
|
|
682
|
+
return false;
|
|
683
|
+
if (r.state !== "selected")
|
|
684
|
+
return v.workTicketHandoff === null && v.protectedWorkHandoff === null;
|
|
685
|
+
const p = responses.find(p => p.id === r.selectedProposalId), h = v.workTicketHandoff;
|
|
686
|
+
if (p?.kind === "proposal" && p.terms.settlementMode === "protected_inspection") {
|
|
687
|
+
const x = v.protectedWorkHandoff;
|
|
688
|
+
if (h !== null || !isRecord(x) || x.kind !== "protected_inspection" || x.path !== "/v1/protected-inspections/prepare" || !isRecord(x.task))
|
|
689
|
+
return false;
|
|
690
|
+
return r.contentType === "application/json" && x.seller === p.payoutAddress && x.amountAtomic === usdcAtomic(p.terms.priceUSDC) && x.deliveryDeadline === Math.floor(Date.parse(p.terms.deliveryDeadline) / 1000) && x.fundingRequired === true && x.buyerProbeRequired === true && x.paymentAuthorized === false && x.task.version === "funded-inspection-v1" && x.task.requestId === r.id && x.task.proposalId === p.id && x.task.proposalTermsHash === p.termsHash && sameInspectionPlan(x.task.plan, p.terms.inspection) && freshExactKeys(x, ["kind", "path", "task", "seller", "amountAtomic", "deliveryDeadline", "fundingRequired", "buyerProbeRequired", "paymentAuthorized"]) && freshExactKeys(x.task, ["version", "requestId", "proposalId", "proposalTermsHash", "plan"]);
|
|
691
|
+
}
|
|
692
|
+
if (v.protectedWorkHandoff !== null)
|
|
693
|
+
return false;
|
|
694
|
+
if (!p || p.kind !== "proposal" || !isRecord(h) || h.kind !== "work_ticket_grant" || h.method !== "POST" || h.path !== "/v1/work-tickets/challenges" || h.buyerWalletRequired !== true || h.grantSignatureRequired !== true || h.paymentAuthorized !== false || h.quoteAcceptanceRequired !== true || h.tracksTicketCreation !== false || !isRecord(h.body) || !isRecord(h.body.input))
|
|
695
|
+
return false;
|
|
696
|
+
const b = h.body, i = b.input;
|
|
697
|
+
if (!freshExactKeys(h, ["kind", "method", "path", "body", "buyerWalletRequired", "grantSignatureRequired", "paymentAuthorized", "quoteAcceptanceRequired", "tracksTicketCreation"]) || !freshExactKeys(b, ["merchantId", "capability", "input", "maxPriceUSDC", "expiresInSeconds"]) || !freshExactKeys(i, ["workRequestId", "proposalId", "proposalTermsHash", "title", "scope", "contentType", "acceptanceCriteria", "payoutAddress", "deliverable", "proposalNote", "deliveryDeadline", "testPurchaseBudgetUSDC", "testPurchasePayer"]))
|
|
698
|
+
return false;
|
|
699
|
+
return i.title === r.title && i.contentType === r.contentType && i.payoutAddress === p.payoutAddress && i.proposalNote === p.terms.note && b.merchantId === p.merchantId && b.capability === p.terms.capability && b.maxPriceUSDC === p.terms.priceUSDC && b.expiresInSeconds === 3600 && !Object.hasOwn(b, "buyer") && i.workRequestId === r.id && i.proposalId === p.id && i.proposalTermsHash === p.termsHash && i.scope === r.description && JSON.stringify(i.acceptanceCriteria) === JSON.stringify(r.acceptanceCriteria) && i.deliverable === p.terms.deliverable && i.deliveryDeadline === p.terms.deliveryDeadline && i.testPurchaseBudgetUSDC === p.terms.testPurchaseBudgetUSDC && i.testPurchasePayer === "seller";
|
|
700
|
+
}
|
|
701
|
+
function isFreshWorkResponse(method, path, v, body) {
|
|
702
|
+
const args = isRecord(body) ? body : {}, id = path.match(/^\/v1\/(?:agent\/)?work-requests\/([^/?]+)/)?.[1];
|
|
703
|
+
const challenge = () => uuid(v.challengeId) && nonempty(v.message) && timestamp(v.expiresAt);
|
|
704
|
+
if (path === "/v1/work-requests/challenges")
|
|
705
|
+
return challenge() && timestamp(v.requestExpiresAt) && digest(v.policyHash) && Date.parse(String(v.expiresAt)) <= Date.parse(String(v.requestExpiresAt));
|
|
706
|
+
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));
|
|
708
|
+
if (path.startsWith("/v1/work-requests?") || path === "/v1/work-requests" && method === "GET")
|
|
709
|
+
return Array.isArray(v.items) && v.items.length <= 50 && v.items.every(isFreshRequest) && (v.nextCursor === null || Number.isSafeInteger(v.nextCursor) && Number(v.nextCursor) >= 0);
|
|
710
|
+
if (path.endsWith("/selection-challenges"))
|
|
711
|
+
return challenge() && isFreshProposal(v.proposal) && v.proposal.kind === "proposal" && v.proposal.requestId === id && v.proposal.id === args.proposalId;
|
|
712
|
+
if (path.endsWith("/proposals") || path.endsWith("/declines")) {
|
|
713
|
+
if (!isFreshProposal(v.response) || v.response.requestId !== id || v.response.kind !== (path.endsWith("/proposals") ? "proposal" : "decline") || typeof v.replay !== "boolean")
|
|
714
|
+
return false;
|
|
715
|
+
const terms = v.response.terms;
|
|
716
|
+
return Object.entries(args).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));
|
|
717
|
+
}
|
|
718
|
+
return isFreshDetail(v, id) && (method === "GET" || v.request.state === "selected" && typeof v.replay === "boolean");
|
|
719
|
+
}
|
|
720
|
+
export { ProtectedInspectionClient, ProtectedInspectionApiError } from "./protected-inspection.js";
|