@haven_ai/signer 0.0.0-dev.202609232125.72f3d20 → 0.0.0-dev.202609241802.ea320de
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 +76 -25
- package/dist/cli.cjs +183 -34
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +184 -35
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +363 -212
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +114 -103
- package/dist/index.d.ts +114 -103
- package/dist/index.js +364 -213
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -70,7 +70,8 @@ The `initialize` handshake advertises which binding versions this signer
|
|
|
70
70
|
understands, under `capabilities.experimental['haven/signer-compatibility']`
|
|
71
71
|
and in the MCP `instructions` string. Both are **derived** from
|
|
72
72
|
`SUPPORTED_X402_EXPECTED_VERSIONS` / `SUPPORTED_SWEEP_BINDING_VERSIONS` in
|
|
73
|
-
`src/core.ts`
|
|
73
|
+
`src/core.ts` and `SUPPORTED_DIRECT_SIGN_CONTEXT_VERSIONS` in
|
|
74
|
+
`src/sign-context.ts` (#3271) — the same constants the signing path enforces — so this README
|
|
74
75
|
deliberately does not restate the numbers. Read them from the handshake, or
|
|
75
76
|
from those constants.
|
|
76
77
|
|
|
@@ -102,6 +103,27 @@ verifies something before it signs, and `haven_sign` called with a bare
|
|
|
102
103
|
`payload_hash` answers `BARE_HASH_REFUSED` with a typed next step instead of a
|
|
103
104
|
signature.
|
|
104
105
|
|
|
106
|
+
**Direct payments (#3271).** A direct payment (`POST /payments`, surfaced as
|
|
107
|
+
`haven_send` / `haven_pay`) is signed as the account's EIP-712
|
|
108
|
+
`PackedUserOperation`, and Haven returns both that typed data and
|
|
109
|
+
`payload_hash` — the ERC-4337 v0.7 UserOperation hash of the same operation.
|
|
110
|
+
Before `haven_sign` signs one, it recomputes that hash from the typed data's
|
|
111
|
+
own domain, types and message and refuses (`USEROP_BINDING_MISMATCH`) unless
|
|
112
|
+
it equals `payload_hash` exactly, in the HybridDeleGator domain of the typed
|
|
113
|
+
data's own sender, against the v0.7 EntryPoint. This is a **corruption**
|
|
114
|
+
check: the caller supplies both values, so it proves they describe the same
|
|
115
|
+
operation, never that Haven prepared it — provenance is `payment_id` (below).
|
|
116
|
+
It runs whether the typed data arrived as a tool argument or by the
|
|
117
|
+
`payment_id` fetch; a fetched direct context that is not a
|
|
118
|
+
`PackedUserOperation` at all is refused by the same check. The x402 EIP-3009
|
|
119
|
+
bridge's funding leg keeps its own digest check against the Haven-signed
|
|
120
|
+
expected context in this signer (the SDK's `signForData` runs this binding
|
|
121
|
+
check there too). The check exists because this typed data is
|
|
122
|
+
multi-KB and can reach the signer through a language model relaying it by
|
|
123
|
+
hand: one corrupted character used to produce a valid-looking signature over
|
|
124
|
+
the wrong digest, surfacing only as an opaque `AA24 signature error` from the
|
|
125
|
+
bundler, after the signature was already produced.
|
|
126
|
+
|
|
105
127
|
## Startup, CLI options and the consent screen (#3173)
|
|
106
128
|
|
|
107
129
|
**Startup cost.** The signer loads `@haven_ai/sdk/edge` — the ethers-free
|
|
@@ -195,6 +217,10 @@ funding confirms — so relay it promptly.
|
|
|
195
217
|
|
|
196
218
|
## What the signer refuses to sign
|
|
197
219
|
|
|
220
|
+
(`USEROP_BINDING_MISMATCH`, the #3271 direct-payment refusal, is described
|
|
221
|
+
under [Two ways to use it](#two-ways-to-use-it) and in the
|
|
222
|
+
[refusal table](#sign-context-refusal-codes).)
|
|
223
|
+
|
|
198
224
|
These are local, independent checks. They do not trust Haven's assertion about
|
|
199
225
|
what a payload means; they re-derive it.
|
|
200
226
|
|
|
@@ -239,7 +265,8 @@ what a payload means; they re-derive it.
|
|
|
239
265
|
## Sign-context refusal codes
|
|
240
266
|
|
|
241
267
|
`{ payment_id }` calls (`haven_sign` / `haven_sign_x402`) fetch the exact
|
|
242
|
-
signing payload from Haven (`GET /x402/:id/sign-context`,
|
|
268
|
+
signing payload from Haven (`GET /x402/:id/sign-context`, and for a direct
|
|
269
|
+
payment `GET /payments/:id/sign-context` — see the #3271 paragraph below and
|
|
243
270
|
[Custody](#custody)). Every refusal on that fetch is a `HavenSignContextError`
|
|
244
271
|
(#3001) — a `HavenSigningError` subclass, so `instanceof HavenSigningError`
|
|
245
272
|
still holds everywhere it did before, but structured like the version-mismatch
|
|
@@ -252,24 +279,44 @@ refusal class — `fallback`, `retry_with_new_quote`, `http_status`,
|
|
|
252
279
|
names); every other row carries `next_tool_omitted_reason` with the exact
|
|
253
280
|
remedy. `message` is unchanged.
|
|
254
281
|
|
|
282
|
+
**#3271: `haven_sign` (never `haven_sign_x402`) has one escape from this
|
|
283
|
+
table.** When the x402 fetch answers `SIGN_CONTEXT_REFUSED` with
|
|
284
|
+
`http_status: 409` and `backend_error_code: 'sign_context_unavailable'` — this
|
|
285
|
+
`payment_id` names a direct payment, not an x402 intent — `haven_sign` fetches
|
|
286
|
+
`GET /payments/:id/sign-context` instead, same auth header, timeout and
|
|
287
|
+
refusal structuring. That second fetch's own refusals reuse the codes in the
|
|
288
|
+
table below with direct-payment remedies: no quote to re-run, and the
|
|
289
|
+
`typed_data_b64` relay from the `haven_send` / `haven_pay` result is the
|
|
290
|
+
fallback. A 409 `sign_context_unavailable` from the direct route too (an
|
|
291
|
+
x402 row the x402 route could not serve, or a direct row with no stored
|
|
292
|
+
signing payload) surfaces the x402 route's own refusal instead. `haven_sign_x402` never takes this branch: a direct payment
|
|
293
|
+
carries no x402 context to fund a merchant retry with, so it surfaces the
|
|
294
|
+
409 unchanged.
|
|
295
|
+
|
|
255
296
|
| `code` | When | `next_action` | `fallback` | extra |
|
|
256
297
|
|---|---|---|---|---|
|
|
257
298
|
| `SIGN_CONTEXT_TIMEOUT` | The fetch (or its body read) did not finish within `SIGN_CONTEXT_TIMEOUT_MS` | `stop_and_tell_user` | `typed_data_b64` | — |
|
|
258
299
|
| `SIGN_CONTEXT_UNREACHABLE` | The fetch failed before any response (DNS, connection refused, TLS, …) | `stop_and_tell_user` | `typed_data_b64` | — |
|
|
259
|
-
| `SIGN_CONTEXT_MALFORMED` | The response body was missing `sign_data.typed_data`
|
|
260
|
-
| `SIGN_CONTEXT_REFUSED` (410
|
|
261
|
-
| `SIGN_CONTEXT_REFUSED` (
|
|
300
|
+
| `SIGN_CONTEXT_MALFORMED` | The response body was missing `sign_data.typed_data` / `x402_expected` (a pre-#1263 backend), or — on the direct-payment fetch — an unsupported `direct_sign_context_version` or a `signature_scheme` other than `eip712_userop` | `stop_and_tell_user` | `typed_data_b64` | — |
|
|
301
|
+
| `SIGN_CONTEXT_REFUSED` (x402: 410 or `expired`; direct: `expired` only) | x402: the quote's window closed. Direct: the payment's window closed — call `haven_send` / `haven_pay` again with the same `idempotency_key` | `payment_window_expired` | — | x402 only: `retry_with_new_quote: true`; both: `http_status`, and `backend_error_code: 'expired'` when the backend sent one |
|
|
302
|
+
| `SIGN_CONTEXT_REFUSED` (404, direct fetch) | The `payment_id` is not this agent's, or the backend predates #3271 and has no direct route | `stop_and_tell_user` | `typed_data_b64` | `http_status`, `backend_error_code` |
|
|
303
|
+
| `SIGN_CONTEXT_REFUSED` (other) | Unknown `payment_id` (404, x402 fetch), `already_executed` / `not_signable` (409), a bare 410 retired-rail tombstone (direct fetch) — or `sign_context_unavailable` (409): always on `haven_sign_x402`; on `haven_sign` only when neither route can serve the row | `stop_and_tell_user` | — | `http_status`, `backend_error_code` |
|
|
304
|
+
| `USEROP_BINDING_MISMATCH` | A direct payment's `PackedUserOperation` typed data (from the direct fetch, or a tool argument) does not recompute to its own `payload_hash`, or a fetched direct context is not a `PackedUserOperation` — see [Two ways to use it](#two-ways-to-use-it) above | `stop_and_tell_user` | — | no `http_status` — this is a local recomputation, not a backend refusal |
|
|
262
305
|
|
|
263
306
|
`fallback: 'typed_data_b64'` appears only where signing OTHER bytes is a
|
|
264
|
-
remedy — a transport failure or a body this signer could not read
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
`
|
|
307
|
+
remedy — a transport failure or a body this signer could not read, plus one
|
|
308
|
+
backend refusal: a 404 from the direct-payment fetch (an older backend with
|
|
309
|
+
no direct route, #3271). For x402 it is **not** in the default quote result
|
|
310
|
+
since #1272: obtain it by re-running the SAME quote tool with the SAME
|
|
311
|
+
`idempotency_key` plus `include_signing_payload: true`, then pass
|
|
312
|
+
`typed_data_b64` (plus `payload_hash` / `x402_expected`) instead of
|
|
313
|
+
`payment_id`. A direct payment's `haven_send` / `haven_pay` result always
|
|
314
|
+
carries `payload_hash` + `typed_data_b64`. Any other backend REFUSAL
|
|
269
315
|
carries no fallback: an expired, executed or unsignable intent cannot be
|
|
270
|
-
rescued by re-signing its bytes — an expired one is re-quoted (the same
|
|
316
|
+
rescued by re-signing its bytes — an expired x402 one is re-quoted (the same
|
|
271
317
|
`payment_window_expired` + `retry_with_new_quote` the signer emits for
|
|
272
|
-
`PAYMENT_WINDOW_EXPIRED`),
|
|
318
|
+
`PAYMENT_WINDOW_EXPIRED`), an expired direct one is re-sent with the same
|
|
319
|
+
`idempotency_key` (no `retry_with_new_quote`), the rest stop. These codes are signer-local,
|
|
273
320
|
not part of `@haven_ai/sdk`'s `AgentPaymentFailureCode` taxonomy, since they
|
|
274
321
|
describe a local fetch failure, not a payment-domain outcome, and never reach
|
|
275
322
|
the backend's REST/OpenAPI surface — only this package's MCP tool responses.
|
|
@@ -280,19 +327,23 @@ The delegate key is read from `HAVEN_DELEGATE_KEY` or a `--credentials` file's
|
|
|
280
327
|
`delegate_key` (with a permissive-file warning). It stays in this process, and
|
|
281
328
|
is never transmitted.
|
|
282
329
|
|
|
283
|
-
**The signer makes at most
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
`
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
330
|
+
**The signer makes at most two kinds of network call, both reads.** Since
|
|
331
|
+
[#1263](https://github.com/d-hinders/Haven-AI/issues/1263) the `{ payment_id }`
|
|
332
|
+
form of `haven_sign` and `haven_sign_x402` performs an authenticated,
|
|
333
|
+
read-only `GET /x402/:payment_id/sign-context` against Haven, so that agents
|
|
334
|
+
never have to relay multi-KB EIP-712 payloads through a model's context
|
|
335
|
+
window. Since #3271, `haven_sign` (never `haven_sign_x402`) falls back to a
|
|
336
|
+
second read — `GET /payments/:payment_id/sign-context` — only when that first
|
|
337
|
+
fetch answers the backend's 409 `sign_context_unavailable`, i.e. this
|
|
338
|
+
`payment_id` names a direct payment rather than an x402 intent. **Only the
|
|
339
|
+
Bearer API key goes out on either read; the delegate key is never part of
|
|
340
|
+
either request or response.** Since #2985 both reads are bounded: each aborts
|
|
341
|
+
after `SIGN_CONTEXT_TIMEOUT_MS` (15 s) and reports a `HavenSignContextError`
|
|
342
|
+
naming the timeout and the `typed_data_b64` fallback, so a hung backend cannot
|
|
343
|
+
hang the signer — and the agent — past the funding window. Every refusal on
|
|
344
|
+
either fetch (timeout, unreachable host, a non-ok backend response, a
|
|
345
|
+
malformed body) is structured the same way, not just prose — see
|
|
346
|
+
[Sign-context refusal codes](#sign-context-refusal-codes) below.
|
|
296
347
|
Nothing else in the package reaches the
|
|
297
348
|
network: `haven_x402_sign_header` and `haven_sign_sweep_delegate` never fetch,
|
|
298
349
|
the library surface above (`createEdgeSigner` and its six signing methods, over
|
package/dist/cli.cjs
CHANGED
|
@@ -289,6 +289,7 @@ function stableStringify(value) {
|
|
|
289
289
|
const object = value;
|
|
290
290
|
return `{${Object.keys(object).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(object[key])}`).join(",")}}`;
|
|
291
291
|
}
|
|
292
|
+
var DIRECT_RELAY_FALLBACK = "call haven_sign with payload_hash and typed_data_b64 from the haven_send / haven_pay result, passed through unchanged";
|
|
292
293
|
var HavenSignContextError = class extends edge.HavenSigningError {
|
|
293
294
|
/**
|
|
294
295
|
* #3001 / #3010 review: the recovery path, when signing OTHER bytes is
|
|
@@ -296,24 +297,31 @@ var HavenSignContextError = class extends edge.HavenSigningError {
|
|
|
296
297
|
* body this signer could not read. `typed_data_b64` is NOT in the default
|
|
297
298
|
* quote result since #1272: obtain it by re-running the SAME quote tool
|
|
298
299
|
* with the SAME idempotency_key plus `include_signing_payload: true`.
|
|
299
|
-
* Absent on a backend REFUSAL
|
|
300
|
-
* cannot be rescued by re-signing its bytes
|
|
300
|
+
* Absent on a backend REFUSAL (an expired, executed or unsignable intent
|
|
301
|
+
* cannot be rescued by re-signing its bytes) — with one #3271 exception: a
|
|
302
|
+
* 404 from the DIRECT fetch (an older backend with no direct route), where
|
|
303
|
+
* the `haven_send` / `haven_pay` result's relay fields still work.
|
|
301
304
|
*/
|
|
302
305
|
fallback;
|
|
303
306
|
/**
|
|
304
307
|
* #3001: `AgentPaymentNextAction` values the signer already emits — the
|
|
305
308
|
* version-mismatch refusal's `stop_and_tell_user` for the classes where
|
|
306
309
|
* retrying the same call cannot help, and `payment_window_expired` (with
|
|
307
|
-
* `retry_with_new_quote`) for the
|
|
308
|
-
* plain `HavenError` branch already does for that code.
|
|
310
|
+
* `retry_with_new_quote`) for the x402 fetch's 410 / `expired`, exactly as
|
|
311
|
+
* the plain `HavenError` branch already does for that code. On the direct
|
|
312
|
+
* fetch (#3271) only `error_code: 'expired'` means an expired window, with
|
|
313
|
+
* no `retry_with_new_quote`; a bare 410 there is a retired-rail tombstone.
|
|
309
314
|
*/
|
|
310
315
|
next_action;
|
|
311
316
|
/**
|
|
312
317
|
* #3103 (epic #3105, decisions 1 and 3): the typed next step beside the
|
|
313
318
|
* action. A refusal Haven made (`SIGN_CONTEXT_REFUSED`, not expired) names
|
|
314
|
-
* the hosted status read with the payment id
|
|
319
|
+
* the hosted status read with the payment id — except a 404 from the direct
|
|
320
|
+
* fetch (#3271), which names none and points at the `typed_data_b64`
|
|
321
|
+
* relay; a transport failure or a
|
|
315
322
|
* malformed body names no tool — the remedy is re-running the SAME quote
|
|
316
|
-
* tool with `include_signing_payload: true`
|
|
323
|
+
* tool with `include_signing_payload: true` (x402), or the payment
|
|
324
|
+
* result's `typed_data_b64` relay (direct, #3271) — and an expired window names
|
|
317
325
|
* none either (which quote tool depends on the flow). Never null: a step
|
|
318
326
|
* with no tool carries `next_tool_omitted_reason`. Additive: the class is
|
|
319
327
|
* not exported from the package index — the published surface is the
|
|
@@ -331,7 +339,7 @@ var HavenSignContextError = class extends edge.HavenSigningError {
|
|
|
331
339
|
http_status;
|
|
332
340
|
/** Present only for `SIGN_CONTEXT_REFUSED`: the backend's own `error_code`. */
|
|
333
341
|
backend_error_code;
|
|
334
|
-
constructor(message, code, refusal, paymentId) {
|
|
342
|
+
constructor(message, code, refusal, paymentId, flow = "x402") {
|
|
335
343
|
super(message);
|
|
336
344
|
this.code = code;
|
|
337
345
|
this.name = "HavenSignContextError";
|
|
@@ -339,7 +347,22 @@ var HavenSignContextError = class extends edge.HavenSigningError {
|
|
|
339
347
|
if (code === "SIGN_CONTEXT_REFUSED") {
|
|
340
348
|
this.http_status = refusal?.httpStatus;
|
|
341
349
|
this.backend_error_code = refusal?.errorCode;
|
|
342
|
-
if (
|
|
350
|
+
if (flow === "direct" && refusal?.httpStatus === 404) {
|
|
351
|
+
this.fallback = "typed_data_b64";
|
|
352
|
+
this.next_action = edge.AgentPaymentNextAction.StopAndTellUser;
|
|
353
|
+
step = signerRefusalStep({
|
|
354
|
+
nextAction: edge.AgentPaymentNextAction.StopAndTellUser,
|
|
355
|
+
nextTool: null,
|
|
356
|
+
nextToolOmittedReason: DIRECT_RELAY_FALLBACK
|
|
357
|
+
});
|
|
358
|
+
} else if (flow === "direct" && refusal?.errorCode === "expired") {
|
|
359
|
+
this.next_action = edge.AgentPaymentNextAction.PaymentWindowExpired;
|
|
360
|
+
step = signerRefusalStep({
|
|
361
|
+
nextAction: edge.AgentPaymentNextAction.PaymentWindowExpired,
|
|
362
|
+
nextTool: null,
|
|
363
|
+
nextToolOmittedReason: "call haven_send / haven_pay again with the same idempotency_key \u2014 the expired payment frees it"
|
|
364
|
+
});
|
|
365
|
+
} else if (flow === "x402" && (refusal?.httpStatus === 410 || refusal?.errorCode === "expired")) {
|
|
343
366
|
this.next_action = edge.AgentPaymentNextAction.PaymentWindowExpired;
|
|
344
367
|
this.retry_with_new_quote = true;
|
|
345
368
|
step = signerRefusalStep({
|
|
@@ -361,7 +384,7 @@ var HavenSignContextError = class extends edge.HavenSigningError {
|
|
|
361
384
|
step = signerRefusalStep({
|
|
362
385
|
nextAction: edge.AgentPaymentNextAction.StopAndTellUser,
|
|
363
386
|
nextTool: null,
|
|
364
|
-
nextToolOmittedReason: "re-run the SAME hosted quote tool with the same idempotency_key and include_signing_payload: true, then pass its typed_data_b64 to this signer"
|
|
387
|
+
nextToolOmittedReason: flow === "direct" ? DIRECT_RELAY_FALLBACK : "re-run the SAME hosted quote tool with the same idempotency_key and include_signing_payload: true, then pass its typed_data_b64 to this signer"
|
|
365
388
|
});
|
|
366
389
|
}
|
|
367
390
|
Object.assign(this, nextStepWireFields(step));
|
|
@@ -443,8 +466,89 @@ async function fetchX402SignContext(identity, paymentId, fetchImpl = fetch, time
|
|
|
443
466
|
paymentRequired: paymentRequired && typeof paymentRequired === "object" && !Array.isArray(paymentRequired) ? paymentRequired : null
|
|
444
467
|
};
|
|
445
468
|
}
|
|
469
|
+
var SUPPORTED_DIRECT_SIGN_CONTEXT_VERSIONS = [edge.DIRECT_SIGN_CONTEXT_VERSION];
|
|
470
|
+
async function fetchDirectSignContext(identity, paymentId, fetchImpl = fetch, timeoutMs = SIGN_CONTEXT_TIMEOUT_MS) {
|
|
471
|
+
let response;
|
|
472
|
+
try {
|
|
473
|
+
response = await fetchImpl(
|
|
474
|
+
`${identity.apiUrl}/payments/${encodeURIComponent(paymentId)}/sign-context`,
|
|
475
|
+
{
|
|
476
|
+
headers: { Authorization: `Bearer ${identity.apiKey}` },
|
|
477
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
478
|
+
}
|
|
479
|
+
);
|
|
480
|
+
} catch (err) {
|
|
481
|
+
const timedOut = err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError");
|
|
482
|
+
throw new HavenSignContextError(
|
|
483
|
+
timedOut ? `Haven did not answer the direct-payment signing-context fetch for ${paymentId} within ${timeoutMs} ms. Retry, or pass typed_data_b64 from the payment result instead.` : `Could not reach Haven to fetch the direct-payment signing context for ${paymentId}: ${err instanceof Error ? err.message : String(err)}. Retry, or pass typed_data_b64 from the payment result instead.`,
|
|
484
|
+
timedOut ? "SIGN_CONTEXT_TIMEOUT" : "SIGN_CONTEXT_UNREACHABLE",
|
|
485
|
+
void 0,
|
|
486
|
+
paymentId,
|
|
487
|
+
"direct"
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
let body;
|
|
491
|
+
try {
|
|
492
|
+
body = await response.json();
|
|
493
|
+
} catch (err) {
|
|
494
|
+
if (err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError")) {
|
|
495
|
+
throw new HavenSignContextError(
|
|
496
|
+
`Haven did not finish sending the direct-payment signing context for ${paymentId} within ${timeoutMs} ms. Retry, or pass typed_data_b64 from the payment result instead.`,
|
|
497
|
+
"SIGN_CONTEXT_TIMEOUT",
|
|
498
|
+
void 0,
|
|
499
|
+
paymentId,
|
|
500
|
+
"direct"
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
body = {};
|
|
504
|
+
}
|
|
505
|
+
if (!response.ok) {
|
|
506
|
+
const detail = typeof body.error === "string" ? body.error : `HTTP ${response.status}`;
|
|
507
|
+
throw new HavenSignContextError(
|
|
508
|
+
`Haven refused the direct-payment signing-context fetch for ${paymentId}: ${detail}` + (response.status === 404 ? " \u2014 either the payment_id is not from this agent\u2019s own POST /payments call, or this Haven backend predates #3271. Pass payload_hash and typed_data_b64 from the haven_send / haven_pay result, unchanged, instead." : body.error_code === "expired" ? " The payment window has expired; call haven_send / haven_pay again with the same idempotency_key." : ""),
|
|
509
|
+
"SIGN_CONTEXT_REFUSED",
|
|
510
|
+
{ httpStatus: response.status, errorCode: typeof body.error_code === "string" ? body.error_code : void 0 },
|
|
511
|
+
paymentId,
|
|
512
|
+
"direct"
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
const signData = body.sign_data;
|
|
516
|
+
const version = body.direct_sign_context_version;
|
|
517
|
+
if (!signData || typeof signData.hash !== "string" || !signData.typed_data || typeof signData.typed_data !== "object" || signData.signature_scheme !== "eip712_userop" || typeof version !== "number" || !SUPPORTED_DIRECT_SIGN_CONTEXT_VERSIONS.includes(version)) {
|
|
518
|
+
throw new HavenSignContextError(
|
|
519
|
+
typeof version === "number" && !SUPPORTED_DIRECT_SIGN_CONTEXT_VERSIONS.includes(version) ? `The Haven direct-payment sign-context response is version ${version}, which this signer does not support (supported: ${SUPPORTED_DIRECT_SIGN_CONTEXT_VERSIONS.join(", ")}). Update @haven_ai/signer.` : "The Haven direct-payment sign-context response is missing sign_data.typed_data, or its signature_scheme is not 'eip712_userop' \u2014 the backend may predate #3271. Pass typed_data_b64 from the payment result instead.",
|
|
520
|
+
"SIGN_CONTEXT_MALFORMED",
|
|
521
|
+
void 0,
|
|
522
|
+
paymentId,
|
|
523
|
+
"direct"
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
return {
|
|
527
|
+
paymentId: String(body.payment_id ?? paymentId),
|
|
528
|
+
payloadHash: signData.hash,
|
|
529
|
+
typedData: signData.typed_data,
|
|
530
|
+
status: typeof body.status === "string" ? body.status : "pending_signature",
|
|
531
|
+
expiresAt: typeof body.expires_at === "string" ? body.expires_at : void 0
|
|
532
|
+
};
|
|
533
|
+
}
|
|
446
534
|
|
|
447
535
|
// src/tools.ts
|
|
536
|
+
var USEROP_BINDING_MISMATCH = "USEROP_BINDING_MISMATCH";
|
|
537
|
+
var HavenUserOpBindingRefusedError = class extends edge.HavenSigningError {
|
|
538
|
+
next_action = edge.AgentPaymentNextAction.StopAndTellUser;
|
|
539
|
+
step;
|
|
540
|
+
/** `message`: the SDK's `HavenUserOpBindingError.message` verbatim — it already names the mismatch and the remedy. */
|
|
541
|
+
constructor(message) {
|
|
542
|
+
super(message);
|
|
543
|
+
this.code = USEROP_BINDING_MISMATCH;
|
|
544
|
+
this.name = "HavenUserOpBindingRefusedError";
|
|
545
|
+
this.step = signerRefusalStep({
|
|
546
|
+
nextAction: edge.AgentPaymentNextAction.StopAndTellUser,
|
|
547
|
+
nextTool: null,
|
|
548
|
+
nextToolOmittedReason: "call haven_sign again with payment_id alone (preferred), or with typed_data copied unchanged from the payment result"
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
};
|
|
448
552
|
var sweepAuthorizationSchema = v3.z.object({
|
|
449
553
|
from: v3.z.string().regex(/^0x[0-9a-fA-F]{40}$/, "from must be a 0x address"),
|
|
450
554
|
to: v3.z.string().regex(/^0x[0-9a-fA-F]{40}$/, "to must be a 0x address"),
|
|
@@ -565,8 +669,13 @@ var SIGN_DESCRIPTION = [
|
|
|
565
669
|
"and returns { signature, x402_binding }. x402_expected includes expires_at; sign before that",
|
|
566
670
|
"window closes. DELEGATION-RAIL x402 accounts (#1263): pass payment_id ALONE (preferred) \u2014 this",
|
|
567
671
|
"signer fetches the exact signing payload and expected context from Haven itself, so nothing",
|
|
568
|
-
"bulky ever crosses your context.
|
|
569
|
-
"
|
|
672
|
+
"bulky ever crosses your context. DIRECT payments (#3271, haven_send / haven_pay): payment_id",
|
|
673
|
+
"ALONE also works here \u2014 the signer fetches the exact PackedUserOperation typed data and its",
|
|
674
|
+
"payload_hash from Haven and checks them against each other before signing, so a payload corrupted",
|
|
675
|
+
"in transit is refused (USEROP_BINDING_MISMATCH) instead of producing a bad signature. Fallback for",
|
|
676
|
+
"either flow: pass typed_data_b64 through UNCHANGED (never re-type the nested typed_data JSON); the",
|
|
677
|
+
"account validates that EIP-712 payload, not payload_hash. On a direct payment the same binding",
|
|
678
|
+
"check runs on the relayed payload too.",
|
|
570
679
|
"Next: call mcp__haven__haven_submit with signature, then pass x402_binding",
|
|
571
680
|
"to mcp__haven-signer__haven_x402_sign_header. A bare payload_hash with no payment_id, typed_data",
|
|
572
681
|
"or x402_expected is REFUSED (BARE_HASH_REFUSED): a hash carries nothing this signer can verify."
|
|
@@ -679,7 +788,14 @@ function resolveTypedData(args) {
|
|
|
679
788
|
}
|
|
680
789
|
}
|
|
681
790
|
function createToolHandlers(signer, options = {}) {
|
|
682
|
-
|
|
791
|
+
function checkPayloadHashMatch(suppliedHash, fetchedHash, paymentId) {
|
|
792
|
+
if (suppliedHash && suppliedHash.toLowerCase() !== fetchedHash.toLowerCase()) {
|
|
793
|
+
throw new edge.HavenSigningError(
|
|
794
|
+
`The supplied payload_hash does not match the signing context Haven serves for payment ${paymentId}. Pass payment_id alone, or check which quote the hash came from.`
|
|
795
|
+
);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
async function resolveSignContext(args, opts) {
|
|
683
799
|
if (!args.payment_id) return null;
|
|
684
800
|
const identity = await options.signContext?.loadIdentity() ?? null;
|
|
685
801
|
if (!identity) {
|
|
@@ -687,30 +803,39 @@ function createToolHandlers(signer, options = {}) {
|
|
|
687
803
|
`payment_id signing needs the agent identity (identity.json next to the signer credentials), which this signer could not load. Re-run \`${edge.connectorRerunCommand()}\` to restore it, or pass typed_data_b64 from the quote result instead.`
|
|
688
804
|
);
|
|
689
805
|
}
|
|
690
|
-
|
|
691
|
-
identity,
|
|
692
|
-
args.payment_id
|
|
693
|
-
|
|
694
|
-
)
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
806
|
+
try {
|
|
807
|
+
const ctx = await fetchX402SignContext(identity, args.payment_id, options.signContext?.fetchImpl);
|
|
808
|
+
checkPayloadHashMatch(args.payload_hash, ctx.payloadHash, args.payment_id);
|
|
809
|
+
return { kind: "x402", ctx };
|
|
810
|
+
} catch (err) {
|
|
811
|
+
if (opts.allowDirectFallback && err instanceof HavenSignContextError && err.http_status === 409 && err.backend_error_code === "sign_context_unavailable") {
|
|
812
|
+
let ctx;
|
|
813
|
+
try {
|
|
814
|
+
ctx = await fetchDirectSignContext(identity, args.payment_id, options.signContext?.fetchImpl);
|
|
815
|
+
} catch (directErr) {
|
|
816
|
+
if (directErr instanceof HavenSignContextError && directErr.http_status === 409 && directErr.backend_error_code === "sign_context_unavailable") {
|
|
817
|
+
throw err;
|
|
818
|
+
}
|
|
819
|
+
throw directErr;
|
|
820
|
+
}
|
|
821
|
+
checkPayloadHashMatch(args.payload_hash, ctx.payloadHash, args.payment_id);
|
|
822
|
+
return { kind: "direct", ctx };
|
|
823
|
+
}
|
|
824
|
+
throw err;
|
|
699
825
|
}
|
|
700
|
-
return ctx;
|
|
701
826
|
}
|
|
702
827
|
return {
|
|
703
828
|
haven_sign: async (input) => runTool(async () => {
|
|
704
829
|
const args = parse("haven_sign", coerceX402Expected(input));
|
|
705
|
-
const
|
|
706
|
-
const typedData =
|
|
707
|
-
const payloadHash =
|
|
830
|
+
const resolved = await resolveSignContext(args, { allowDirectFallback: true });
|
|
831
|
+
const typedData = resolved?.ctx.typedData ?? resolveTypedData(args);
|
|
832
|
+
const payloadHash = resolved?.ctx.payloadHash ?? args.payload_hash;
|
|
708
833
|
if (!payloadHash) {
|
|
709
834
|
throw new edge.HavenSigningError(
|
|
710
|
-
"Pass payment_id (preferred for delegation-rail x402) or payload_hash."
|
|
835
|
+
"Pass payment_id (preferred for delegation-rail x402 or direct payments) or payload_hash."
|
|
711
836
|
);
|
|
712
837
|
}
|
|
713
|
-
const expectedRaw =
|
|
838
|
+
const expectedRaw = resolved?.kind === "x402" ? parseFetchedExpected(resolved.ctx) : resolved?.kind === "direct" ? null : args.x402_expected;
|
|
714
839
|
const x402Expected = expectedRaw ? toExpectedX402(expectedRaw) : null;
|
|
715
840
|
const result = x402Expected ? await signFundingLeg(signer, x402Expected, payloadHash, typedData) : null;
|
|
716
841
|
if (!result) {
|
|
@@ -720,6 +845,16 @@ function createToolHandlers(signer, options = {}) {
|
|
|
720
845
|
"Refusing to sign a delegation payload with no expected context. This typed data is a DELEGATION \u2014 signing it grants a third party authority to move funds \u2014 so it is only ever signed against a Haven-signed context that the caveats are verified against. Call this tool with { payment_id } instead (the signer then fetches and verifies the context itself), or use haven_sign_x402."
|
|
721
846
|
);
|
|
722
847
|
}
|
|
848
|
+
if (resolved?.kind === "direct" || edge.isPackedUserOperationTypedData(typedData)) {
|
|
849
|
+
try {
|
|
850
|
+
edge.assertUserOpTypedDataBinding(typedData, payloadHash);
|
|
851
|
+
} catch (err) {
|
|
852
|
+
if (err instanceof edge.HavenUserOpBindingError) {
|
|
853
|
+
throw new HavenUserOpBindingRefusedError(err.message);
|
|
854
|
+
}
|
|
855
|
+
throw err;
|
|
856
|
+
}
|
|
857
|
+
}
|
|
723
858
|
const signature = await signer.signDelegationTypedData(typedData);
|
|
724
859
|
await auditSigning("haven_sign", payloadHash);
|
|
725
860
|
return { signature };
|
|
@@ -743,7 +878,8 @@ function createToolHandlers(signer, options = {}) {
|
|
|
743
878
|
}),
|
|
744
879
|
haven_sign_x402: async (input) => runTool(async () => {
|
|
745
880
|
const args = parse("haven_sign_x402", coerceX402Expected(coercePaymentRequired(input)));
|
|
746
|
-
const
|
|
881
|
+
const resolved = await resolveSignContext(args, { allowDirectFallback: false });
|
|
882
|
+
const fetched = resolved?.kind === "x402" ? resolved.ctx : void 0;
|
|
747
883
|
const payloadHash = fetched?.payloadHash ?? args.payload_hash;
|
|
748
884
|
const expectedRaw = fetched ? parseFetchedExpected(fetched) : args.x402_expected;
|
|
749
885
|
if (!payloadHash || !expectedRaw) {
|
|
@@ -883,6 +1019,15 @@ function normalizeError(err) {
|
|
|
883
1019
|
...nextStepWireFields(err.step)
|
|
884
1020
|
};
|
|
885
1021
|
}
|
|
1022
|
+
if (err instanceof HavenUserOpBindingRefusedError) {
|
|
1023
|
+
return {
|
|
1024
|
+
success: false,
|
|
1025
|
+
code: err.code,
|
|
1026
|
+
message: err.message,
|
|
1027
|
+
next_action: err.next_action,
|
|
1028
|
+
...nextStepWireFields(err.step)
|
|
1029
|
+
};
|
|
1030
|
+
}
|
|
886
1031
|
if (err instanceof HavenSignContextError) {
|
|
887
1032
|
return {
|
|
888
1033
|
success: false,
|
|
@@ -1467,7 +1612,8 @@ var SIGNER_CAPABILITY_KEY = "haven/signer-compatibility";
|
|
|
1467
1612
|
function signerCompatibility() {
|
|
1468
1613
|
return {
|
|
1469
1614
|
x402_expected_context_versions: [...SUPPORTED_X402_EXPECTED_VERSIONS],
|
|
1470
|
-
sweep_binding_versions: [...SUPPORTED_SWEEP_BINDING_VERSIONS]
|
|
1615
|
+
sweep_binding_versions: [...SUPPORTED_SWEEP_BINDING_VERSIONS],
|
|
1616
|
+
direct_sign_context_versions: [...SUPPORTED_DIRECT_SIGN_CONTEXT_VERSIONS]
|
|
1471
1617
|
};
|
|
1472
1618
|
}
|
|
1473
1619
|
function signerCapabilityAdvertisement() {
|
|
@@ -1478,12 +1624,14 @@ function signerInstructions() {
|
|
|
1478
1624
|
return [
|
|
1479
1625
|
"Haven edge signer: sign-only tools bound to the local delegate key. It never emits the",
|
|
1480
1626
|
"key. Its one network capability is an authenticated READ of a signing context from",
|
|
1481
|
-
"Haven by payment_id \u2014 pass payment_id to haven_sign
|
|
1482
|
-
"delegation-rail x402) instead of relaying bulky
|
|
1627
|
+
"Haven by payment_id \u2014 pass payment_id to haven_sign (preferred for both a direct payment,",
|
|
1628
|
+
"#3271, and delegation-rail x402) or haven_sign_x402 (x402 only) instead of relaying bulky",
|
|
1629
|
+
"typed-data payloads yourself.",
|
|
1483
1630
|
"",
|
|
1484
1631
|
"Version compatibility (check this BEFORE signing, not after):",
|
|
1485
1632
|
`- x402 expected-context versions supported: ${compatibility.x402_expected_context_versions.join(", ")}`,
|
|
1486
1633
|
`- sweep authorization binding versions supported: ${compatibility.sweep_binding_versions.join(", ")}`,
|
|
1634
|
+
`- direct-payment (haven_send / haven_pay) sign-context versions supported: ${compatibility.direct_sign_context_versions.join(", ")} \u2014 pass payment_id alone to haven_sign; this signer fetches the exact bytes`,
|
|
1487
1635
|
"",
|
|
1488
1636
|
"Haven quote and prepare results report the expected-context version they will emit",
|
|
1489
1637
|
"(signer_compatibility.x402_expected_context_version). If that version is not in the list",
|
|
@@ -1601,7 +1749,7 @@ async function warnIfCredentialFilePermissive(path, log = (message) => process.s
|
|
|
1601
1749
|
|
|
1602
1750
|
// src/server.ts
|
|
1603
1751
|
var SIGNER_NAME = "@haven_ai/signer";
|
|
1604
|
-
var SIGNER_VERSION = "0.0.0-dev.
|
|
1752
|
+
var SIGNER_VERSION = "0.0.0-dev.202609241802.ea320de";
|
|
1605
1753
|
async function resolveSignerRuntime(options = {}) {
|
|
1606
1754
|
assertSupportedNodeVersion(options.nodeVersion);
|
|
1607
1755
|
if (options.delegateKey) {
|
|
@@ -1639,8 +1787,9 @@ function buildSignerMcpServer(signer, options = {}) {
|
|
|
1639
1787
|
accountAddress: options.credentials?.accountAddress,
|
|
1640
1788
|
chainId: options.credentials?.chainId
|
|
1641
1789
|
},
|
|
1642
|
-
// #1263: the payment_id signing path — the ONLY network
|
|
1643
|
-
//
|
|
1790
|
+
// #1263: the payment_id signing path — the ONLY network path this server
|
|
1791
|
+
// has: up to two authenticated reads of a signing context from Haven (x402,
|
|
1792
|
+
// then direct for haven_sign, #3271), using
|
|
1644
1793
|
// the agent identity the connector stores next to the signer credential.
|
|
1645
1794
|
// The signer CORE stays network-free; fetched bytes still pass the same
|
|
1646
1795
|
// binding verification + digest re-derivation as tool-argument bytes.
|