@haven_ai/sdk 0.0.0-dev.202609191954.6adcc6b → 0.0.0-dev.202609192153.41dae78

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/README.md CHANGED
@@ -579,6 +579,18 @@ original `tools/call`, setting BOTH `PAYMENT-SIGNATURE` (x402 v2) and `X-PAYMENT
579
579
  user intent so fresh merchant quotes or sessions do not become duplicate Haven
580
580
  payments.
581
581
 
582
+ If the merchant's session is gone by the time the paid retry arrives (it
583
+ expired, or the merchant restarted), a merchant that answers HTTP 404 with
584
+ JSON-RPC `-32001` **and** `error.data = { settled: false, next_action:
585
+ 'reinitialize_then_retry_same_payment_header' }` is stating its own guarantee
586
+ that nothing was settled; since #3171 `fetch()`, `payX402Quote()`,
587
+ `resumeX402Payment()` and `completeX402MerchantCall()` act on exactly that
588
+ shape — re-initialize once and resend the SAME payment header on the new
589
+ session. A bare `-32001` without the data, any other 404, a failed
590
+ re-initialize, or a second 404 is returned to you unchanged and, after
591
+ funding, recorded as a rejection as before: the SDK never resends on an
592
+ inference, only on the merchant's stated guarantee.
593
+
582
594
  See [`examples/mcp-x402-sse.ts`](./examples/mcp-x402-sse.ts) for a complete
583
595
  MCP flow with initialize, `mcp-session-id`, JSON-RPC `tools/call`, quote
584
596
  inspection, saved resume state, and final retry.
package/dist/index.cjs CHANGED
@@ -1494,6 +1494,7 @@ var MCP_CLIENT_INFO = { name: "haven-sdk", version: "1" };
1494
1494
  var MCP_X402_PAYMENT_META_KEY = "x402/payment";
1495
1495
  var MCP_X402_PAYMENT_RESPONSE_META_KEY = "x402/payment-response";
1496
1496
  var X402_RETRY_REJECTED_STATUS = 402;
1497
+ var SESSION_NOT_FOUND_NEXT_ACTION = "reinitialize_then_retry_same_payment_header";
1497
1498
  var McpMerchantTransport = class {
1498
1499
  merchantTimeout;
1499
1500
  fetchImpl;
@@ -1654,6 +1655,29 @@ var McpMerchantTransport = class {
1654
1655
  }
1655
1656
  return this.fetch(url, withMcpPaymentMeta({ ...init, headers }, paymentHeader));
1656
1657
  }
1658
+ /**
1659
+ * #3171: deliver the payment; if the merchant answers its unknown-session
1660
+ * 404 WITH the settled-nothing recovery data, re-initialize once and resend
1661
+ * the SAME header on the new session. The merchant's own guarantee (#1578:
1662
+ * the session guard runs before its payment gate) is what makes the resend
1663
+ * safe — a bare -32001, or any other 404, is returned to the caller
1664
+ * untouched (body re-wrapped, since reading it consumed the stream). One
1665
+ * recovery only: a second 404 is the answer.
1666
+ */
1667
+ async deliverPaymentRecoveringSession(url, init, paymentHeader, reinitialize) {
1668
+ const first = await this.deliverPayment(url, init, paymentHeader);
1669
+ if (first.status !== 404) return first;
1670
+ let text;
1671
+ try {
1672
+ text = await first.clone().text();
1673
+ } catch {
1674
+ return first;
1675
+ }
1676
+ if (!sessionNotFoundRecovery(text)) return first;
1677
+ const sessionId = await reinitialize();
1678
+ if (!sessionId) return first;
1679
+ return this.deliverPayment(url, this.withSessionHeaders(init, sessionId), paymentHeader);
1680
+ }
1657
1681
  /**
1658
1682
  * #3118: read a native MCP payment-required tool result from a response
1659
1683
  * WITHOUT consuming it — JSON or SSE framed. `undefined` for anything that
@@ -1780,6 +1804,24 @@ function parseJsonRpcToolsCall(body) {
1780
1804
  function isJsonRpcToolsCallBody(body) {
1781
1805
  return parseJsonRpcToolsCall(body) !== void 0;
1782
1806
  }
1807
+ function sessionNotFoundRecovery(body) {
1808
+ let parsed;
1809
+ try {
1810
+ parsed = JSON.parse(body);
1811
+ } catch {
1812
+ return void 0;
1813
+ }
1814
+ if (!isRecord(parsed) || !isRecord(parsed.error)) return void 0;
1815
+ if (parsed.error.code !== -32001) return void 0;
1816
+ const data = parsed.error.data;
1817
+ if (!isRecord(data)) return void 0;
1818
+ if (data.settled !== false || data.next_action !== SESSION_NOT_FOUND_NEXT_ACTION) return void 0;
1819
+ return {
1820
+ reason: typeof data.reason === "string" ? data.reason : "session_expired",
1821
+ settled: false,
1822
+ next_action: SESSION_NOT_FOUND_NEXT_ACTION
1823
+ };
1824
+ }
1783
1825
  function isRecord(value) {
1784
1826
  return typeof value === "object" && value !== null && !Array.isArray(value);
1785
1827
  }
@@ -2931,10 +2973,11 @@ var MerchantCompletion = class {
2931
2973
  if (!receipt.paymentHeader) {
2932
2974
  throw new HavenApiError("No x402 payment header was returned for payment retry", 500);
2933
2975
  }
2934
- const retryResponse = await this.merchantTransport.deliverPayment(
2976
+ const retryResponse = await this.merchantTransport.deliverPaymentRecoveringSession(
2935
2977
  url,
2936
2978
  initialInit,
2937
- receipt.paymentHeader
2979
+ receipt.paymentHeader,
2980
+ () => this.merchantTransport.initialize(url, initialInit)
2938
2981
  );
2939
2982
  const toolResult = retryResponse.ok ? await this.merchantTransport.readToolResult(retryResponse) : void 0;
2940
2983
  const inBandChallenge = toolResult ? extractMcpPaymentRequired(toolResult) : void 0;
@@ -4265,7 +4308,12 @@ var HavenClient = class {
4265
4308
  }
4266
4309
  let requestInit = withX402Wallet(input.init, x402Wallet) ?? {};
4267
4310
  if (mcpSessionId) requestInit = this.merchantTransport.withSessionHeaders(requestInit, mcpSessionId);
4268
- const response = await this.merchantTransport.deliverPayment(input.url, requestInit, input.paymentHeader);
4311
+ const response = await this.merchantTransport.deliverPaymentRecoveringSession(
4312
+ input.url,
4313
+ requestInit,
4314
+ input.paymentHeader,
4315
+ () => this.merchantTransport.initialize(input.url, input.init, x402Wallet)
4316
+ );
4269
4317
  const surfaced = await this.merchantTransport.surfaceResult(response);
4270
4318
  const text = await surfaced.text();
4271
4319
  let body;