@haven_ai/signer 0.0.0-dev.202609242134.f497e3b → 0.0.0-dev.202609250737.3bd5a51

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
@@ -61,7 +61,7 @@ It exposes four stdio MCP tools, all sign-only:
61
61
 
62
62
  | Tool | Does | Emits |
63
63
  |---|---|---|
64
- | `haven_sign` | Sign one payment. Preferred form is `{ payment_id }` alone — the signer fetches the exact payload itself. Signs only Haven-prepared payloads (#3272): a direct-payment `PackedUserOperation` from this signer's own delegate account whose only call redeems a delegation made to that account, an erc7710 settlement child or EIP-3009 funding leg against a Haven-signed context (which it records and binds); other typed data is refused (`TYPED_DATA_NOT_ALLOWED`) | `{ signature }` or `{ signature, x402_binding }` |
64
+ | `haven_sign` | Sign one payment. Preferred form is `{ payment_id }` alone — the signer fetches the exact payload itself. Signs only Haven-prepared payloads (#3272, #3281): a direct-payment `PackedUserOperation` from this signer's own delegate account whose only call redeems a delegation made to that account, or — against a Haven-signed context, which it records and binds — an EIP-3009 funding leg of that same shape paying this signer's own delegate EOA, or an erc7710 settlement child from this signer's own account; other typed data is refused (`TYPED_DATA_NOT_ALLOWED`) | `{ signature }` or `{ signature, x402_binding }` |
65
65
  | `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 }` |
66
66
  | `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 }` |
67
67
  | `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 }` |
@@ -105,8 +105,11 @@ it signs whatever typed data it is handed. The allowlist lives in the MCP tool
105
105
  layer: `haven_sign` signs typed data only when it is a bound direct-payment
106
106
  `PackedUserOperation` (below) or an x402 payload against a Haven-signed context,
107
107
  refuses anything else with `TYPED_DATA_NOT_ALLOWED`, and answers a bare
108
- `payload_hash` with `BARE_HASH_REFUSED`. An embedder that calls the core
109
- directly owns that check itself.
108
+ `payload_hash` with `BARE_HASH_REFUSED`. An embedder that calls
109
+ `signDelegationTypedData` directly owns that check itself. The core's
110
+ `signX402FundingTypedData` is not a verbatim primitive: since #3281 it runs
111
+ the x402 shape checks itself (a guarded funding leg or a verified settlement
112
+ child only), whoever calls it.
110
113
 
111
114
  **The unbound-branch allowlist (#3272).** Without an x402 context, `haven_sign`
112
115
  signs typed data only when ALL of these hold; otherwise it refuses, with no
@@ -144,9 +147,13 @@ operation, never that Haven prepared it — provenance is `payment_id` (below).
144
147
  It runs whether the typed data arrived as a tool argument or by the
145
148
  `payment_id` fetch; a fetched direct context that is not a
146
149
  `PackedUserOperation` at all is refused by the same check. The x402 EIP-3009
147
- bridge's funding leg keeps its own digest check against the Haven-signed
148
- expected context in this signer (the SDK's `signForData` runs this binding
149
- check there too). The check exists because this typed data is
150
+ bridge's funding leg is checked against the Haven-signed expected context AND,
151
+ since #3281, by this same binding check against the declared UserOp hash, the
152
+ direct-payment allowlist and a recipient pin (a transfer of the quoted amount
153
+ to this signer's own delegate EOA). A settlement child must be re-delegated
154
+ from this signer's own account, never a ROOT grant. Anything else on the x402
155
+ arm is `TYPED_DATA_NOT_ALLOWED`, however validly Haven's binding key declared
156
+ it. The check exists because this typed data is
150
157
  multi-KB and can reach the signer through a language model relaying it by
151
158
  hand: one corrupted character used to produce a valid-looking signature over
152
159
  the wrong digest, surfacing only as an opaque `AA24 signature error` from the
@@ -431,7 +438,10 @@ audit write (disk full, read-only, two appends racing at the bound) is
431
438
  reported on stderr and never fails the signing call that already produced
432
439
  its signature — so the consent block's
433
440
  "appended for every signing operation" is a best-effort promise since #3172,
434
- kept unchanged in text because editing it moves the consent hash. The
441
+ kept unchanged in text (editing consent copy does NOT move the consent hash —
442
+ that covers identity, tool names and the surface version, as the consent
443
+ screen section above already says; #3279 rewords the block without
444
+ re-prompting anyone). The
435
445
  credential check itself is unchanged in wording and still judges a symlinked
436
446
  credential by its target. The
437
447
  `payload_hash` argument itself is bounded on the tool schema to a 32-byte hash
package/dist/cli.cjs CHANGED
@@ -619,12 +619,13 @@ var SIGN_X402_DESCRIPTION = [
619
619
  ].join(" ");
620
620
  var SIGN_SWEEP_DELEGATE_DESCRIPTION = [
621
621
  "Sign a Haven-prepared gasless USDC sweep that recovers stranded funds from the delegate",
622
- "wallet back to your Haven wallet. The delegate key never leaves this process and this tool",
623
- "never broadcasts \u2014 it returns only an EIP-3009 signature that Haven's relayer submits and",
622
+ "wallet back to the agent's account (Haven wallet). The delegate key never leaves this process and this",
623
+ "tool never broadcasts \u2014 it returns only an EIP-3009 signature that Haven's relayer submits and",
624
624
  "pays gas for. Pass the authorization and expected_auth returned by the hosted",
625
- "haven_sweep_delegate tool. The signer verifies Haven authored the authorization and that it",
626
- "pays out to your own Safe before signing, then returns { signature } to hand back to",
627
- "mcp__haven__haven_sweep_delegate to complete recovery."
625
+ "haven_sweep_delegate tool. The signer verifies Haven authored the authorization; the",
626
+ "destination is checked against the agent's account (Haven wallet) from the local credential",
627
+ "when the credential carries one, and otherwise rests on Haven's binding signature. Then it",
628
+ "returns { signature } to hand back to mcp__haven__haven_sweep_delegate to complete recovery."
628
629
  ].join(" ");
629
630
  var toolDescriptions = {
630
631
  haven_sign: SIGN_DESCRIPTION,
@@ -633,7 +634,17 @@ var toolDescriptions = {
633
634
  haven_sign_sweep_delegate: SIGN_SWEEP_DELEGATE_DESCRIPTION
634
635
  };
635
636
  async function signFundingLeg(signer, expected, typedData) {
636
- return signer.signX402FundingTypedData(typedData, expected);
637
+ try {
638
+ return await signer.signX402FundingTypedData(typedData, expected);
639
+ } catch (err) {
640
+ if (err instanceof edge.HavenTypedDataRefusedError && !(err instanceof HavenTypedDataNotAllowedError)) {
641
+ throw new HavenTypedDataNotAllowedError(err.message);
642
+ }
643
+ if (err instanceof edge.HavenUserOpBindingError) {
644
+ throw new HavenUserOpBindingRefusedError(err.message);
645
+ }
646
+ throw err;
647
+ }
637
648
  }
638
649
  function toExpectedX402(raw) {
639
650
  return {
@@ -809,7 +820,8 @@ function createToolHandlers(signer, options = {}) {
809
820
  const result = await signer.signSweepAuthorization({
810
821
  authorization: args.authorization,
811
822
  expectedAuth: args.expected_auth,
812
- // Cross-check `to` against the Safe in the local credential when present.
823
+ // Cross-check `to` against the agent's account (Haven wallet) from the
824
+ // local credential, when the credential carries one.
813
825
  expectedSafe: options.audit?.accountAddress
814
826
  });
815
827
  await auditSigning(
@@ -1028,8 +1040,8 @@ function renderSignerConsentBlock(input, hash) {
1028
1040
  lines.push("payload from Haven, authenticated with the agent credential stored next to");
1029
1041
  lines.push("this signer's key file - it never sends the key, a signature, or anything");
1030
1042
  lines.push("else outbound, and it cannot show a live allowance summary.");
1031
- lines.push("On-chain Safe rules remain the real spend gate, and the wallet owner can");
1032
- lines.push("pause or revoke agent authority outside this signer.");
1043
+ lines.push("The agent's signed budget delegation is the real spend gate, and the wallet");
1044
+ lines.push("owner can pause or revoke agent authority outside this signer.");
1033
1045
  lines.push("");
1034
1046
  lines.push("Tools this signer will expose to your agent runtime (one line each; the");
1035
1047
  lines.push("full agent-facing descriptions are what the runtime sees):");
@@ -1220,7 +1232,7 @@ function createEdgeSigner(delegateKey, options = {}) {
1220
1232
  `Refusing to sign the x402 settlement child: this signer cannot map the network '${expected.network}' to a chain id, so it cannot check which chain the child is scoped to. Update @haven_ai/signer.`
1221
1233
  );
1222
1234
  }
1223
- edge.verifySettlementChild(typedData, {
1235
+ edge.assertOwnSettlementChild(typedData, {
1224
1236
  merchantTo: expected.merchantTo,
1225
1237
  amount: expected.amount,
1226
1238
  asset: expected.asset,
@@ -1230,7 +1242,21 @@ function createEdgeSigner(delegateKey, options = {}) {
1230
1242
  // signed another" class this file exists to catch (#1455 review).
1231
1243
  chainId: settlementChainId,
1232
1244
  expiresAt: expected.expiresAt
1245
+ }, delegateAddress);
1246
+ } else if (edge.isPackedUserOperationTypedData(typedData)) {
1247
+ edge.assertUserOpTypedDataBinding(typedData, expected.payloadHash);
1248
+ edge.assertBoundDirectPaymentUserOp(typedData, delegateAddress);
1249
+ edge.assertFundingLegPaysDelegate(typedData, {
1250
+ delegateAddress,
1251
+ asset: expected.asset,
1252
+ amount: expected.amount
1233
1253
  });
1254
+ } else {
1255
+ throw new edge.HavenTypedDataRefusedError(
1256
+ `This signer's x402 arm signs only a funding-leg PackedUserOperation or an erc7710 settlement Delegation; this typed data is '${String(
1257
+ typedData.primaryType
1258
+ )}'. Refusing, even though its digest is bound by the expected context.`
1259
+ );
1234
1260
  }
1235
1261
  const account = accounts.privateKeyToAccount(delegateKey);
1236
1262
  const signature = await account.signTypedData(
@@ -1314,7 +1340,7 @@ function createEdgeSigner(delegateKey, options = {}) {
1314
1340
  }
1315
1341
  if (expectedSafe && !sameAddress(authorization.to, expectedSafe)) {
1316
1342
  throw new edge.HavenSigningError(
1317
- "Sweep authorization `to` does not match the Safe in the local credential."
1343
+ "Sweep authorization `to` does not match the agent's account (Haven wallet) in the local credential."
1318
1344
  );
1319
1345
  }
1320
1346
  const typedData = edge.buildSweepTypedData(authorization);
@@ -1637,7 +1663,7 @@ async function warnIfCredentialFilePermissive(path, log = (message) => process.s
1637
1663
 
1638
1664
  // src/server.ts
1639
1665
  var SIGNER_NAME = "@haven_ai/signer";
1640
- var SIGNER_VERSION = "0.0.0-dev.202609242134.f497e3b";
1666
+ var SIGNER_VERSION = "0.0.0-dev.202609250737.3bd5a51";
1641
1667
  async function resolveSignerRuntime(options = {}) {
1642
1668
  assertSupportedNodeVersion(options.nodeVersion);
1643
1669
  if (options.delegateKey) {