@haven_ai/signer 0.1.32-alpha.0 → 0.1.34-alpha.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/README.md CHANGED
@@ -5,7 +5,7 @@ that's very nearly all it does. It pairs with the hosted, keyless
5
5
  `@haven_ai/mcp-server`: the hosted server identifies the agent, constructs
6
6
  unsigned payloads and relays signatures; this one signs. **The delegate key
7
7
  never leaves this process** — it is not part of any request or response, and
8
- only signatures (and the standard x402 `X-PAYMENT` header) ever come out.
8
+ only signatures (and the standard x402 merchant payment header) ever come out.
9
9
 
10
10
  Design: [`docs/architecture/07-edge-signer.md`](../../docs/architecture/07-edge-signer.md).
11
11
  Contract: [`docs/architecture/06-hosted-mcp-connect-flow.md`](../../docs/architecture/06-hosted-mcp-connect-flow.md).
@@ -44,7 +44,7 @@ It exposes four stdio MCP tools, all sign-only:
44
44
  |---|---|---|
45
45
  | `haven_sign` | Sign one payment. Preferred form is `{ payment_id }` alone — the signer fetches the exact payload itself. Signs an EIP-712 typed-data payload on the delegation rail (a redemption, or an erc7710 settlement child), or a bare `payload_hash` on a v1 context; for the EIP-3009 x402 bridge it also records the funding context and returns a binding | `{ signature }` or `{ signature, x402_binding }` |
46
46
  | `haven_sign_x402` | One-shot x402: funding signature **and** the merchant header in a single local call (`haven_sign` + `haven_x402_sign_header`). `{ payment_id }` alone is the preferred call | `{ signature, x402_binding, payment_header, accepted }` |
47
- | `haven_x402_sign_header` | Build + sign the EIP-3009 `X-PAYMENT` header, only when the fresh merchant `payment_required` matches the recorded `x402_binding` | `{ payment_header, accepted }` |
47
+ | `haven_x402_sign_header` | Build + sign the EIP-3009 merchant payment header, only when the fresh merchant `payment_required` matches the recorded `x402_binding` | `{ payment_header, accepted }` |
48
48
  | `haven_sign_sweep_delegate` | Sign a Haven-prepared gasless EIP-3009 sweep that recovers stranded funds from the delegate wallet back to your own account. Never broadcasts | `{ signature }` |
49
49
 
50
50
  The `initialize` handshake advertises which binding versions this signer
@@ -105,7 +105,9 @@ no delegate hot balance and no `haven_x402_sign_header` step:
105
105
  hosted: haven_pay_x402_quote -> settlement child + settlement_scheme: erc7710
106
106
  local: haven_sign { payment_id } -> child signature (caveats verified locally)
107
107
  hosted: haven_submit { settlement_scheme: "erc7710" } -> payment_header
108
- agent: retry merchant with X-PAYMENT
108
+ agent: retry merchant, setting PAYMENT-SIGNATURE ONLY (never X-PAYMENT:
109
+ this header carries a delegation chain and duplicating it is
110
+ refused with HTTP 431)
109
111
  ```
110
112
 
111
113
  x402 — **EIP-3009 bridge**, the fallback for merchants without facilitator-side
@@ -116,8 +118,9 @@ bounded funding leg:
116
118
  hosted: haven_pay_x402_quote -> { payment_id, payload_hash, x402.expected }
117
119
  local: haven_sign + expected -> funding signature + x402_binding
118
120
  hosted: haven_submit -> funds account -> delegate EOA
119
- local: haven_x402_sign_header -> X-PAYMENT header only if binding matches
120
- agent: retry merchant with X-PAYMENT
121
+ local: haven_x402_sign_header -> payment header only if binding matches
122
+ agent: retry merchant, setting BOTH PAYMENT-SIGNATURE + X-PAYMENT
123
+ (both names are correct HERE — the bridged header is small)
121
124
  ```
122
125
 
123
126
  On the bridge, pass `x402.expected` from the hosted quote unchanged into the
@@ -129,7 +132,7 @@ challenge has a different amount, merchant recipient, resource URL, token asset
129
132
  or network than the recorded funding intent, refuses an expired window, and
130
133
  consumes the binding after one header.
131
134
 
132
- The `X-PAYMENT` header's validity window starts when it is signed, not when
135
+ The merchant payment header's validity window starts when it is signed, not when
133
136
  funding confirms — so relay it promptly.
134
137
 
135
138
  ## What the signer refuses to sign
package/dist/cli.cjs CHANGED
@@ -184,6 +184,15 @@ function createEdgeSigner(delegateKey, options = {}) {
184
184
  );
185
185
  }
186
186
  const x402Bindings = /* @__PURE__ */ new Map();
187
+ const retiredX402Bindings = /* @__PURE__ */ new Map();
188
+ const RETIRED_BINDING_MEMORY = 64;
189
+ function retireX402Binding(id, reason) {
190
+ retiredX402Bindings.set(id, reason);
191
+ if (retiredX402Bindings.size > RETIRED_BINDING_MEMORY) {
192
+ const oldest = retiredX402Bindings.keys().next();
193
+ if (!oldest.done) retiredX402Bindings.delete(oldest.value);
194
+ }
195
+ }
187
196
  function signAndVerify(hash) {
188
197
  const signature = sdk.signHash(delegateKey, hash);
189
198
  if (!sdk.verifySignature(hash, signature, delegateAddress)) {
@@ -249,14 +258,26 @@ function createEdgeSigner(delegateKey, options = {}) {
249
258
  async buildX402PaymentHeader(paymentRequired, x402Binding) {
250
259
  const expected = x402Bindings.get(x402Binding);
251
260
  if (!expected) {
261
+ const retired = retiredX402Bindings.get(x402Binding);
262
+ if (retired === "header_built") {
263
+ throw new sdk.HavenSigningError(
264
+ "This x402 binding was already used to build a merchant header. Bindings are single-use. If you called haven_sign_x402, it already returned the payment_header \u2014 retry the merchant with THAT header instead of building another; haven_x402_sign_header is the follow-up to haven_sign, not to haven_sign_x402. If you no longer have the header, re-run the quote tool with the same idempotency_key."
265
+ );
266
+ }
267
+ if (retired === "window_expired") {
268
+ throw new sdk.HavenSigningError(
269
+ "This x402 binding was retired because its payment window closed before a merchant header could be built \u2014 no header exists to retry with. Re-run the quote tool with the same idempotency_key to get a fresh window, then sign again."
270
+ );
271
+ }
252
272
  throw new sdk.HavenSigningError(
253
- "x402 funding binding is required before signing a merchant header. Sign the hosted funding hash with x402_expected first."
273
+ "x402 funding binding is required before signing a merchant header. Sign the hosted funding hash with x402_expected first (haven_sign returns a binding this tool can use). A binding is also lost when the signer process restarts, since bindings live in memory only \u2014 re-sign to mint a fresh one."
254
274
  );
255
275
  }
256
276
  try {
257
277
  assertX402PaymentWindowOpen(expected);
258
278
  } catch (err) {
259
279
  x402Bindings.delete(x402Binding);
280
+ retireX402Binding(x402Binding, "window_expired");
260
281
  throw err;
261
282
  }
262
283
  const option = sdk.selectStandardPaymentOption(paymentRequired.accepts);
@@ -276,18 +297,18 @@ function createEdgeSigner(delegateKey, options = {}) {
276
297
  );
277
298
  if (paymentRequired.x402Version < 2) {
278
299
  x402Bindings.delete(x402Binding);
300
+ retireX402Binding(x402Binding, "header_built");
279
301
  return { paymentHeader: header, accepted: option };
280
302
  }
281
303
  try {
282
304
  const payment = sdk.decodeBase64Json(header);
283
- const wrapped = sdk.encodeBase64Json({
284
- x402Version: paymentRequired.x402Version,
285
- accepted: option,
286
- payload: payment.payload
287
- });
305
+ const wrapped = sdk.encodeBase64Json(
306
+ sdk.x402V2PaymentEnvelope(paymentRequired, option, payment.payload)
307
+ );
288
308
  return { paymentHeader: wrapped, accepted: option };
289
309
  } finally {
290
310
  x402Bindings.delete(x402Binding);
311
+ retireX402Binding(x402Binding, "header_built");
291
312
  }
292
313
  },
293
314
  async signSweepAuthorization({
@@ -392,7 +413,7 @@ function assertSupportedBindingVersion(received, supported, context) {
392
413
  const ceiling = outOfDate ? `This signer is out of date: it supports ${context} versions up to ${highest}, and Haven sent version ${received}. Update @haven_ai/signer \u2014 rerun the Haven connector (\`npx @haven_ai/connect@alpha\`), which reinstalls the pinned MCP runtime.` : `Unsupported ${context} version ${received}: this signer supports ${supported.join(", ")}.`;
393
414
  const fallback = outOfDate ? sdk.SIGNER_UPDATE_FALLBACK : `This ${context} version (${received}) is older than what this signer enforces (${supported.join(", ")}) \u2014 updating @haven_ai/signer will not restore it. Nothing was signed or spent; stop and tell the user rather than retrying.`;
394
415
  throw new sdk.HavenUnsupportedSignerVersionError(
395
- `${ceiling} Nothing was signed. Do not rewrite the version field to a supported value: it is part of the Haven-signed binding message, so changing it invalidates the signature and would misrepresent what Haven authorised.`,
416
+ `${ceiling} Nothing was signed. Do not rewrite the version field to a supported value: it is part of the Haven-signed binding message, so changing it invalidates the signature and would misrepresent what Haven declared.`,
396
417
  code,
397
418
  supported,
398
419
  received,
@@ -689,19 +710,23 @@ var SIGN_DESCRIPTION = [
689
710
  "relay the returned signature via mcp__haven__haven_submit."
690
711
  ].join(" ");
691
712
  var X402_SIGN_HEADER_DESCRIPTION = [
692
- "Build and sign the EIP-3009 X-PAYMENT header for the merchant leg of an x402 payment.",
713
+ "Build and sign the EIP-3009 merchant payment header for the merchant leg of an x402 payment.",
693
714
  "The delegate key stays local \u2014 only the signed header crosses any boundary.",
694
715
  "Pass the payment_required from the original merchant 402 response and the x402_binding",
695
- "returned by haven_sign. The signer validates the merchant, amount, resource, asset, and",
716
+ "returned by haven_sign. It must be haven_sign \u2014 NOT haven_sign_x402, which is a",
717
+ "one-shot that builds the header itself and spends its own binding doing so. If you called",
718
+ "haven_sign_x402, its result already carries payment_header; retry the merchant with that and",
719
+ "do not call this tool. The signer validates the merchant, amount, resource, asset, and",
696
720
  "network against the recorded funding context before signing, checks expires_at when present,",
697
721
  "and rejects mismatches or expired payment windows.",
698
- "Returns { payment_header, accepted }. Set X-PAYMENT: <payment_header> on your retry to the",
699
- "merchant. Only call after haven_submit has confirmed the funding step (nextAction=none or",
722
+ "Returns { payment_header, accepted }. On your retry set BOTH PAYMENT-SIGNATURE (x402 v2) and",
723
+ "X-PAYMENT (v1) to <payment_header>; a strict v2 merchant reads only the first.",
724
+ "Only call after haven_submit has confirmed the funding step (nextAction=none or",
700
725
  "the funding tx has a confirmed status). Next for paid MCP tools: call mcp__haven__haven_complete_mcp_tool."
701
726
  ].join(" ");
702
727
  var SIGN_X402_DESCRIPTION = [
703
728
  "One-shot x402 signing for the fast 3-call flow: sign the funding hash AND build the EIP-3009",
704
- "X-PAYMENT header in a single local call (equivalent to haven_sign followed by",
729
+ "merchant payment header in a single local call (equivalent to haven_sign followed by",
705
730
  "haven_x402_sign_header). The delegate key never leaves this process. From the haven_pay_mcp_tool",
706
731
  "result pass JUST payment_id \u2014 PREFERRED (#1263, #1355): this signer fetches the exact signing",
707
732
  "payload, expected context, and merchant payment_required from Haven itself, so nothing bulky",
@@ -716,7 +741,13 @@ var SIGN_X402_DESCRIPTION = [
716
741
  "mcp__haven__haven_settle_mcp_tool to fund and settle in one hosted call. The header is built now (before",
717
742
  "funding confirms), so its short validity window starts here \u2014 call mcp__haven__haven_settle_mcp_tool promptly,",
718
743
  "and re-run mcp__haven__haven_pay_mcp_tool with the same idempotency_key if a tool returns PAYMENT_WINDOW_EXPIRED.",
719
- "Next: call mcp__haven__haven_settle_mcp_tool."
744
+ "The returned x402_binding is ALREADY SPENT \u2014 this tool consumed it building payment_header \u2014",
745
+ "so never pass it to mcp__haven-signer__haven_x402_sign_header; that tool is the follow-up to",
746
+ "haven_sign, not to this one. payment_header IS the header to use.",
747
+ "Next: for a paid MCP tool, call mcp__haven__haven_settle_mcp_tool. For a direct plain-HTTP x402",
748
+ "merchant (the haven_pay_x402_quote path), relay signature via mcp__haven__haven_submit and then",
749
+ "retry the original merchant URL YOURSELF, setting BOTH PAYMENT-SIGNATURE (x402 v2) and",
750
+ "X-PAYMENT (v1) to payment_header \u2014 Haven never contacts that merchant."
720
751
  ].join(" ");
721
752
  var SIGN_SWEEP_DELEGATE_DESCRIPTION = [
722
753
  "Sign a Haven-prepared gasless USDC sweep that recovers stranded funds from the delegate",
@@ -1206,7 +1237,7 @@ async function warnIfCredentialFilePermissive(path, log = (message) => process.s
1206
1237
 
1207
1238
  // src/server.ts
1208
1239
  var SIGNER_NAME = "@haven_ai/signer";
1209
- var SIGNER_VERSION = "0.1.32-alpha.0";
1240
+ var SIGNER_VERSION = "0.1.34-alpha.0";
1210
1241
  async function resolveSignerRuntime(options = {}) {
1211
1242
  assertSupportedNodeVersion(options.nodeVersion);
1212
1243
  if (options.delegateKey) {