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