@haven_ai/signer 0.0.0-dev.202609031523.fd49e1a

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 ADDED
@@ -0,0 +1,240 @@
1
+ # @haven_ai/signer
2
+
3
+ The Haven **edge signer**. It holds the delegate key locally and signs — and
4
+ that's very nearly all it does. It pairs with the hosted, keyless
5
+ `@haven_ai/mcp-server`: the hosted server identifies the agent, constructs
6
+ unsigned payloads and relays signatures; this one signs. **The delegate key
7
+ never leaves this process** — it is not part of any request or response, and
8
+ only signatures (and the standard x402 merchant payment header) ever come out.
9
+
10
+ Design: [`docs/architecture/07-edge-signer.md`](../../docs/architecture/07-edge-signer.md).
11
+ Contract: [`docs/architecture/06-hosted-mcp-connect-flow.md`](../../docs/architecture/06-hosted-mcp-connect-flow.md).
12
+
13
+ Requires **Node >= 22**; the signer refuses to start on anything older, before
14
+ it reads a key.
15
+
16
+ ## Two ways to use it
17
+
18
+ **As a local MCP signer** (for Claude Desktop / Code / Cursor) — run it
19
+ alongside the hosted Haven connection. The supported install is the connector
20
+ the Haven dashboard hands out, which writes the MCP config and pins the
21
+ runtime:
22
+
23
+ ```sh
24
+ npx @haven_ai/connect@alpha
25
+ ```
26
+
27
+ `@alpha` is the production channel and is the right answer unless your
28
+ dashboard hands you a different one — **run the command that dashboard shows
29
+ you**, which names the npm dist-tag that backend is paired with. This signer's
30
+ own messages do the same: since [#2423](https://github.com/d-hinders/Haven-AI/issues/2423)
31
+ every "rerun the connector" hint it prints names the channel THIS build was
32
+ published under, so a build installed from a non-production channel tells you
33
+ to reinstall from that same channel rather than sending you to production.
34
+
35
+ Rerunning it is also the documented fix for a signer that has fallen behind the
36
+ backend's expected-context version. To run the signer directly:
37
+
38
+ ```sh
39
+ HAVEN_DELEGATE_KEY=0x... npx @haven_ai/signer@alpha
40
+ # or
41
+ npx @haven_ai/signer@alpha --credentials /path/to/haven-agent.json
42
+ ```
43
+
44
+ On first launch, the signer prints the delegate address, any wallet/network
45
+ metadata found in the credential file, and the sign-only tool list. It refuses
46
+ to start until acknowledged with either `HAVEN_SIGNER_ACK=<hash>` or
47
+ `npx @haven_ai/signer@alpha --credentials /path/to/haven-agent.json --ack`.
48
+
49
+ It exposes four stdio MCP tools, all sign-only:
50
+
51
+ | Tool | Does | Emits |
52
+ |---|---|---|
53
+ | `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 }` |
54
+ | `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 }` |
55
+ | `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 }` |
56
+ | `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 }` |
57
+
58
+ The `initialize` handshake advertises which binding versions this signer
59
+ understands, under `capabilities.experimental['haven/signer-compatibility']`
60
+ and in the MCP `instructions` string. Both are **derived** from
61
+ `SUPPORTED_X402_EXPECTED_VERSIONS` / `SUPPORTED_SWEEP_BINDING_VERSIONS` in
62
+ `src/core.ts` — the same constants the signing path enforces — so this README
63
+ deliberately does not restate the numbers. Read them from the handshake, or
64
+ from those constants.
65
+
66
+ **As a library** (for SDK / autonomous agents):
67
+
68
+ ```ts
69
+ import { createEdgeSigner } from '@haven_ai/signer'
70
+
71
+ const signer = createEdgeSigner(process.env.HAVEN_DELEGATE_KEY!)
72
+
73
+ // Delegation-rail direct payment: sign the EIP-712 typed data the account
74
+ // validates — not the bare ERC-4337 hash.
75
+ const signature = await signer.signDelegationTypedData(typedData)
76
+
77
+ // x402, EIP-3009 bridge: sign the funding leg, then the merchant header.
78
+ const funding = await signer.signX402FundingTypedData(fundingTypedData, expected)
79
+ const { paymentHeader } = await signer.buildX402PaymentHeader(
80
+ paymentRequired,
81
+ funding.x402Binding,
82
+ )
83
+ ```
84
+
85
+ The signer also exposes `signPaymentHash(hash)` (raw ECDSA over a legacy
86
+ AllowanceModule funding/transfer hash) and `signX402FundingHash(hash, expected)`
87
+ for v1 contexts, and `signSweepAuthorization(input)` for the gasless sweep. All
88
+ six are methods on the object `createEdgeSigner` returns, not standalone
89
+ exports.
90
+
91
+ ## Orchestration
92
+
93
+ Direct payment:
94
+
95
+ ```
96
+ hosted: haven_pay -> { payment_id, payload to sign }
97
+ local: haven_sign -> { signature }
98
+ hosted: haven_submit -> { status, tx_hash }
99
+ ```
100
+
101
+ On the delegation rail the payload is the EIP-712 typed data the account
102
+ validates, not the bare ERC-4337 hash. Note the trust-model asymmetry: this
103
+ direct leg has no Haven-signed expected context to verify against, so the
104
+ authority boundary is the account's on-chain caveat enforcers rather than a
105
+ client-side gate — unlike the x402 legs below.
106
+
107
+ x402 — **erc7710 direct settlement**, the preferred scheme when the account is
108
+ on the delegation rail and the merchant advertises
109
+ `extra.assetTransferMethod: "erc7710"`. There is **no funding leg**, so there is
110
+ no delegate hot balance and no `haven_x402_sign_header` step:
111
+
112
+ ```
113
+ hosted: haven_pay_x402_quote -> settlement child + settlement_scheme: erc7710
114
+ local: haven_sign { payment_id } -> child signature (caveats verified locally)
115
+ hosted: haven_submit { settlement_scheme: "erc7710" } -> payment_header
116
+ agent: retry merchant, setting PAYMENT-SIGNATURE ONLY (never X-PAYMENT:
117
+ this header carries a delegation chain and duplicating it is
118
+ refused with HTTP 431)
119
+ ```
120
+
121
+ x402 — **EIP-3009 bridge**, the fallback for merchants without facilitator-side
122
+ erc7710 support (still most of them). Two local delegate signatures, and a
123
+ bounded funding leg:
124
+
125
+ ```
126
+ hosted: haven_pay_x402_quote -> { payment_id, payload_hash, x402.expected }
127
+ local: haven_sign + expected -> funding signature + x402_binding
128
+ hosted: haven_submit -> funds account -> delegate EOA
129
+ local: haven_x402_sign_header -> payment header only if binding matches
130
+ agent: retry merchant, setting BOTH PAYMENT-SIGNATURE + X-PAYMENT
131
+ (both names are correct HERE — the bridged header is small)
132
+ ```
133
+
134
+ On the bridge, pass `x402.expected` from the hosted quote unchanged into the
135
+ local `haven_sign` call, or just pass `{ payment_id }` and let the signer fetch
136
+ it. The signer records that context and returns a process-local `x402_binding`;
137
+ pass that binding into `haven_x402_sign_header` after `haven_submit` confirms.
138
+ The signer refuses to authorize the merchant header when the fresh merchant
139
+ challenge has a different amount, merchant recipient, resource URL, token asset
140
+ or network than the recorded funding intent, refuses an expired window, and
141
+ consumes the binding after one header.
142
+
143
+ The merchant payment header's validity window starts when it is signed, not when
144
+ funding confirms — so relay it promptly.
145
+
146
+ ## What the signer refuses to sign
147
+
148
+ These are local, independent checks. They do not trust Haven's assertion about
149
+ what a payload means; they re-derive it.
150
+
151
+ - **Unauthenticated context.** The expected context must carry Haven's `auth`
152
+ signature over it. Configure `HAVEN_X402_BINDING_SIGNER` (or
153
+ `x402_binding_signer` in the credential file) so the signer can reject
154
+ locally invented or tampered contexts before signing anything.
155
+ - **Wrong signing mode.** The *context* selects the path, never the caller's
156
+ arguments: a context that commits to a typed-data digest requires the typed
157
+ data, one that does not requires the bare hash. A mismatch is refused rather
158
+ than signed into an on-chain failure.
159
+ - **Another agent's quote.** A context naming a `payer_delegate` that is not
160
+ this signer's own delegate is refused.
161
+ - **An unbound delegation payload.** Typed data with `primaryType: "Delegation"`
162
+ is never raw-signed without a context binding it.
163
+ - **An erc7710 settlement child whose caveats do not match what Haven declared.**
164
+ The signer re-derives the child's meaning from its own pinned
165
+ `DelegationManager` and caveat-enforcer addresses (cross-checked against
166
+ `@metamask/smart-accounts-kit` by a test, never fetched from Haven, which
167
+ would make the check circular): the payee pin, the exact token and amount,
168
+ the chain, and a settlement window bounded at 600 seconds. Extra caveats are
169
+ allowed — top-level caveats are AND-ed during redemption, so an unrecognised
170
+ one can only add a constraint.
171
+ - **A binding version it does not understand.** The refusal is machine-readable
172
+ — `code`, `supported_versions`, `received_version`, `fallback` — and names
173
+ updating the signer as the fix.
174
+ - **A sweep that does not move funds out of this delegate's own key** — the
175
+ `from` check is unconditional. The **destination** check is not, and this is
176
+ the one asymmetry in this list: the signer compares the sweep's `to` against
177
+ the account address **only when the local credential records one**
178
+ (`safe_address`). Run with `HAVEN_DELEGATE_KEY` alone — or with a credential
179
+ whose `safe_address` is absent — and there is no local value to compare
180
+ against, so the destination is authenticated by Haven's binding signature and
181
+ the token/chain canonicality check, but not independently re-derived. Prefer
182
+ a credential file that carries the account address.
183
+
184
+ ## Custody
185
+
186
+ The delegate key is read from `HAVEN_DELEGATE_KEY` or a `--credentials` file's
187
+ `delegate_key` (with a permissive-file warning). It stays in this process, and
188
+ is never transmitted.
189
+
190
+ **The signer makes at most one kind of network call, on one path, and it is a
191
+ read.** Since [#1263](https://github.com/d-hinders/Haven-AI/issues/1263) the
192
+ `{ payment_id }` form of `haven_sign` and `haven_sign_x402` performs an
193
+ authenticated, read-only `GET /x402/:payment_id/sign-context` against Haven, so
194
+ that agents never have to relay multi-KB EIP-712 payloads through a model's
195
+ context window. **Only the Bearer API key goes out; the delegate key is never
196
+ part of that request or its response.** Nothing else in the package reaches the
197
+ network: `haven_x402_sign_header` and `haven_sign_sweep_delegate` never fetch,
198
+ the library surface above (`createEdgeSigner` and its six signing methods, over
199
+ the network-free `src/core.ts`) never fetches, and passing the payload as
200
+ `typed_data_b64` instead of `payment_id` keeps even the two fetching tools
201
+ offline. It never relays, submits, or broadcasts.
202
+
203
+ The fetch needs `api_url` and `api_key` from an `identity.json` sitting **next
204
+ to the signer credential file** — the signer's own credential still needs no
205
+ `api_key`. So the network call is a property of how you start the signer, not
206
+ just of which tool you call: run it from `HAVEN_DELEGATE_KEY` alone and there is
207
+ no credential file, hence no directory to find an `identity.json` in, so the
208
+ `{ payment_id }` form refuses with a message naming the `typed_data_b64`
209
+ fallback rather than reaching out — and the process makes no network calls at
210
+ all. Egress is needed by `--credentials` / `HAVEN_CREDENTIALS` runs that use the
211
+ preferred `{ payment_id }` call, which is what the connector install above
212
+ sets up.
213
+
214
+ Fetched bytes are treated as untrusted input exactly like a tool argument: the
215
+ same digest re-derivation and Haven-binding verification apply, because what
216
+ makes them safe is the verification, not where they came from.
217
+
218
+ Connect Agent 2 may create the signer credential file locally during setup. In
219
+ that flow Haven receives the public signing address, proof, API-key hash/prefix,
220
+ and install status only; the plaintext API key and delegate key stay in local
221
+ protected storage/runtime config.
222
+
223
+ ## Local audit
224
+
225
+ Every MCP signing operation appends a JSONL row locally. File-backed runs write
226
+ next to the credential as `<credential>.signer-audit.jsonl`; env-only runs use
227
+ `~/.haven/signer-audit.jsonl`. Rows include timestamp, tool, payload hash and
228
+ delegate address, plus the account address and chain id when the credential
229
+ carries them. They never include the delegate key, the signature, or the x402
230
+ payment header.
231
+
232
+ ## Hot-wallet minimization
233
+
234
+ This applies to the **EIP-3009 bridge only** — the erc7710 path above has no
235
+ funding leg and no delegate balance to strand. On the bridge, the account
236
+ briefly funds the delegate EOA before the merchant settles the EIP-3009
237
+ authorization. Keep delegate balances transient: keep budgets small and
238
+ period-bound, retry the original merchant session only after funding confirms,
239
+ and sweep stranded delegate balances (`haven_sign_sweep_delegate`) when the
240
+ merchant retry fails or does not settle before authorization expiry.