@haven_ai/signer 0.2.0-alpha.0 → 0.3.0-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 +45 -3
- package/dist/cli.cjs +107 -19
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +107 -19
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +107 -19
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +29 -23
- package/dist/index.d.ts +29 -23
- package/dist/index.js +107 -19
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -187,14 +187,49 @@ what a payload means; they re-derive it.
|
|
|
187
187
|
the one asymmetry in this list: the signer compares the sweep's `to` against
|
|
188
188
|
the account address **only when the local credential records one**
|
|
189
189
|
(`account_address`, or the pre-#2908 `safe_address` / `safeAddress`, which
|
|
190
|
-
are read permanently; from the environment, `HAVEN_ACCOUNT_ADDRESS
|
|
191
|
-
|
|
190
|
+
are read permanently; from the environment, `HAVEN_ACCOUNT_ADDRESS` only —
|
|
191
|
+
`HAVEN_WALLET_ADDRESS` and `HAVEN_SAFE_ADDRESS` were removed by #2914, so a
|
|
192
|
+
machine still configured through either records no account address and lands
|
|
193
|
+
in exactly the degraded case this paragraph describes). Run with
|
|
192
194
|
`HAVEN_DELEGATE_KEY` alone — or with a credential
|
|
193
195
|
whose account address is absent — and there is no local value to compare
|
|
194
196
|
against, so the destination is authenticated by Haven's binding signature and
|
|
195
197
|
the token/chain canonicality check, but not independently re-derived. Prefer
|
|
196
198
|
a credential file that carries the account address.
|
|
197
199
|
|
|
200
|
+
## Sign-context refusal codes
|
|
201
|
+
|
|
202
|
+
`{ payment_id }` calls (`haven_sign` / `haven_sign_x402`) fetch the exact
|
|
203
|
+
signing payload from Haven (`GET /x402/:id/sign-context`, see
|
|
204
|
+
[Custody](#custody)). Every refusal on that fetch is a `HavenSignContextError`
|
|
205
|
+
(#3001) — a `HavenSigningError` subclass, so `instanceof HavenSigningError`
|
|
206
|
+
still holds everywhere it did before, but structured like the version-mismatch
|
|
207
|
+
refusal below rather than prose alone: `code`, `next_action`, and — per
|
|
208
|
+
refusal class — `fallback`, `retry_with_new_quote`, `http_status`,
|
|
209
|
+
`backend_error_code`. `message` is unchanged.
|
|
210
|
+
|
|
211
|
+
| `code` | When | `next_action` | `fallback` | extra |
|
|
212
|
+
|---|---|---|---|---|
|
|
213
|
+
| `SIGN_CONTEXT_TIMEOUT` | The fetch (or its body read) did not finish within `SIGN_CONTEXT_TIMEOUT_MS` | `stop_and_tell_user` | `typed_data_b64` | — |
|
|
214
|
+
| `SIGN_CONTEXT_UNREACHABLE` | The fetch failed before any response (DNS, connection refused, TLS, …) | `stop_and_tell_user` | `typed_data_b64` | — |
|
|
215
|
+
| `SIGN_CONTEXT_MALFORMED` | The response body was missing `sign_data.typed_data` or `x402_expected` (a pre-#1263 backend) | `stop_and_tell_user` | `typed_data_b64` | — |
|
|
216
|
+
| `SIGN_CONTEXT_REFUSED` (410 / `expired`) | The quote's window closed | `payment_window_expired` | — | `retry_with_new_quote: true`, `http_status`, `backend_error_code: 'expired'` |
|
|
217
|
+
| `SIGN_CONTEXT_REFUSED` (other) | Unknown `payment_id` (404), `already_executed` / `not_signable` / `sign_context_unavailable` (409) | `stop_and_tell_user` | — | `http_status`, `backend_error_code` |
|
|
218
|
+
|
|
219
|
+
`fallback: 'typed_data_b64'` appears only where signing OTHER bytes is a
|
|
220
|
+
remedy — a transport failure or a body this signer could not read. It is
|
|
221
|
+
**not** in the default quote result since #1272: obtain it by re-running the
|
|
222
|
+
SAME quote tool with the SAME `idempotency_key` plus
|
|
223
|
+
`include_signing_payload: true`, then pass `typed_data_b64` (plus
|
|
224
|
+
`payload_hash` / `x402_expected`) instead of `payment_id`. A backend REFUSAL
|
|
225
|
+
carries no fallback: an expired, executed or unsignable intent cannot be
|
|
226
|
+
rescued by re-signing its bytes — an expired one is re-quoted (the same
|
|
227
|
+
`payment_window_expired` + `retry_with_new_quote` the signer emits for
|
|
228
|
+
`PAYMENT_WINDOW_EXPIRED`), the rest stop. These codes are signer-local,
|
|
229
|
+
not part of `@haven_ai/sdk`'s `AgentPaymentFailureCode` taxonomy, since they
|
|
230
|
+
describe a local fetch failure, not a payment-domain outcome, and never reach
|
|
231
|
+
the backend's REST/OpenAPI surface — only this package's MCP tool responses.
|
|
232
|
+
|
|
198
233
|
## Custody
|
|
199
234
|
|
|
200
235
|
The delegate key is read from `HAVEN_DELEGATE_KEY` or a `--credentials` file's
|
|
@@ -207,7 +242,14 @@ read.** Since [#1263](https://github.com/d-hinders/Haven-AI/issues/1263) the
|
|
|
207
242
|
authenticated, read-only `GET /x402/:payment_id/sign-context` against Haven, so
|
|
208
243
|
that agents never have to relay multi-KB EIP-712 payloads through a model's
|
|
209
244
|
context window. **Only the Bearer API key goes out; the delegate key is never
|
|
210
|
-
part of that request or its response.**
|
|
245
|
+
part of that request or its response.** Since #2985 that read is bounded:
|
|
246
|
+
it aborts after `SIGN_CONTEXT_TIMEOUT_MS` (15 s) and reports a
|
|
247
|
+
`HavenSignContextError` naming the timeout and the `typed_data_b64` fallback,
|
|
248
|
+
so a hung backend cannot hang the signer — and the agent — past the funding
|
|
249
|
+
window. Every refusal on this fetch (timeout, unreachable host, a non-ok
|
|
250
|
+
backend response, a malformed body) is structured the same way, not just
|
|
251
|
+
prose — see [Sign-context refusal codes](#sign-context-refusal-codes) below.
|
|
252
|
+
Nothing else in the package reaches the
|
|
211
253
|
network: `haven_x402_sign_header` and `haven_sign_sweep_delegate` never fetch,
|
|
212
254
|
the library surface above (`createEdgeSigner` and its six signing methods, over
|
|
213
255
|
the network-free `src/core.ts`) never fetches, and passing the payload as
|
package/dist/cli.cjs
CHANGED
|
@@ -30,7 +30,7 @@ function createSigningAuditEntry(tool, payloadHash, context, now = /* @__PURE__
|
|
|
30
30
|
payload_hash: payloadHash,
|
|
31
31
|
delegate_address: context.delegateAddress
|
|
32
32
|
};
|
|
33
|
-
if (context.
|
|
33
|
+
if (context.accountAddress) entry.safe_address = context.accountAddress;
|
|
34
34
|
if (typeof context.chainId === "number") entry.chain_id = context.chainId;
|
|
35
35
|
return entry;
|
|
36
36
|
}
|
|
@@ -534,6 +534,49 @@ function signerInstructions() {
|
|
|
534
534
|
"fallback fields alongside the message, so you can branch on it directly."
|
|
535
535
|
].join("\n");
|
|
536
536
|
}
|
|
537
|
+
var HavenSignContextError = class extends sdk.HavenSigningError {
|
|
538
|
+
/**
|
|
539
|
+
* #3001 / #3010 review: the recovery path, when signing OTHER bytes is
|
|
540
|
+
* actually a remedy — a transport failure (timeout, unreachable) or a
|
|
541
|
+
* body this signer could not read. `typed_data_b64` is NOT in the default
|
|
542
|
+
* quote result since #1272: obtain it by re-running the SAME quote tool
|
|
543
|
+
* with the SAME idempotency_key plus `include_signing_payload: true`.
|
|
544
|
+
* Absent on a backend REFUSAL: an expired, executed or unsignable intent
|
|
545
|
+
* cannot be rescued by re-signing its bytes.
|
|
546
|
+
*/
|
|
547
|
+
fallback;
|
|
548
|
+
/**
|
|
549
|
+
* #3001: `AgentPaymentNextAction` values the signer already emits — the
|
|
550
|
+
* version-mismatch refusal's `stop_and_tell_user` for the classes where
|
|
551
|
+
* retrying the same call cannot help, and `payment_window_expired` (with
|
|
552
|
+
* `retry_with_new_quote`) for the backend's 410 `expired`, exactly as the
|
|
553
|
+
* plain `HavenError` branch already does for that code.
|
|
554
|
+
*/
|
|
555
|
+
next_action;
|
|
556
|
+
retry_with_new_quote;
|
|
557
|
+
/** Present only for `SIGN_CONTEXT_REFUSED` (the backend's HTTP status). */
|
|
558
|
+
http_status;
|
|
559
|
+
/** Present only for `SIGN_CONTEXT_REFUSED`: the backend's own `error_code`. */
|
|
560
|
+
backend_error_code;
|
|
561
|
+
constructor(message, code, refusal) {
|
|
562
|
+
super(message);
|
|
563
|
+
this.code = code;
|
|
564
|
+
this.name = "HavenSignContextError";
|
|
565
|
+
if (code === "SIGN_CONTEXT_REFUSED") {
|
|
566
|
+
this.http_status = refusal?.httpStatus;
|
|
567
|
+
this.backend_error_code = refusal?.errorCode;
|
|
568
|
+
if (refusal?.httpStatus === 410 || refusal?.errorCode === "expired") {
|
|
569
|
+
this.next_action = sdk.AgentPaymentNextAction.PaymentWindowExpired;
|
|
570
|
+
this.retry_with_new_quote = true;
|
|
571
|
+
} else {
|
|
572
|
+
this.next_action = sdk.AgentPaymentNextAction.StopAndTellUser;
|
|
573
|
+
}
|
|
574
|
+
} else {
|
|
575
|
+
this.fallback = "typed_data_b64";
|
|
576
|
+
this.next_action = sdk.AgentPaymentNextAction.StopAndTellUser;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
};
|
|
537
580
|
async function loadHavenIdentity(credentialsPath) {
|
|
538
581
|
if (!credentialsPath) return null;
|
|
539
582
|
try {
|
|
@@ -548,30 +591,50 @@ async function loadHavenIdentity(credentialsPath) {
|
|
|
548
591
|
return null;
|
|
549
592
|
}
|
|
550
593
|
}
|
|
551
|
-
|
|
594
|
+
var SIGN_CONTEXT_TIMEOUT_MS = 15e3;
|
|
595
|
+
async function fetchX402SignContext(identity, paymentId, fetchImpl = fetch, timeoutMs = SIGN_CONTEXT_TIMEOUT_MS) {
|
|
552
596
|
let response;
|
|
553
597
|
try {
|
|
554
598
|
response = await fetchImpl(
|
|
555
599
|
`${identity.apiUrl}/x402/${encodeURIComponent(paymentId)}/sign-context`,
|
|
556
|
-
{
|
|
600
|
+
{
|
|
601
|
+
headers: { Authorization: `Bearer ${identity.apiKey}` },
|
|
602
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
603
|
+
}
|
|
557
604
|
);
|
|
558
605
|
} catch (err) {
|
|
559
|
-
|
|
560
|
-
|
|
606
|
+
const timedOut = err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError");
|
|
607
|
+
throw new HavenSignContextError(
|
|
608
|
+
timedOut ? `Haven did not answer the signing-context fetch for ${paymentId} within ${timeoutMs} ms. Retry, or pass typed_data_b64 from the quote result instead.` : `Could not reach Haven to fetch the signing context for ${paymentId}: ${err instanceof Error ? err.message : String(err)}. Retry, or pass typed_data_b64 from the quote result instead.`,
|
|
609
|
+
timedOut ? "SIGN_CONTEXT_TIMEOUT" : "SIGN_CONTEXT_UNREACHABLE"
|
|
561
610
|
);
|
|
562
611
|
}
|
|
563
|
-
|
|
612
|
+
let body;
|
|
613
|
+
try {
|
|
614
|
+
body = await response.json();
|
|
615
|
+
} catch (err) {
|
|
616
|
+
if (err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError")) {
|
|
617
|
+
throw new HavenSignContextError(
|
|
618
|
+
`Haven did not finish sending the signing context for ${paymentId} within ${timeoutMs} ms. Retry, or pass typed_data_b64 from the quote result instead.`,
|
|
619
|
+
"SIGN_CONTEXT_TIMEOUT"
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
body = {};
|
|
623
|
+
}
|
|
564
624
|
if (!response.ok) {
|
|
565
625
|
const detail = typeof body.error === "string" ? body.error : `HTTP ${response.status}`;
|
|
566
|
-
throw new
|
|
567
|
-
`Haven refused the signing-context fetch for ${paymentId}: ${detail}` + (response.status === 404 ? " \u2014 check the payment_id came from this agent\u2019s own quote." : response.status === 410 ? " Re-run the quote with the same idempotency key, then sign the fresh payment_id." : "")
|
|
626
|
+
throw new HavenSignContextError(
|
|
627
|
+
`Haven refused the signing-context fetch for ${paymentId}: ${detail}` + (response.status === 404 ? " \u2014 check the payment_id came from this agent\u2019s own quote." : response.status === 410 ? " Re-run the quote with the same idempotency key, then sign the fresh payment_id." : ""),
|
|
628
|
+
"SIGN_CONTEXT_REFUSED",
|
|
629
|
+
{ httpStatus: response.status, errorCode: typeof body.error_code === "string" ? body.error_code : void 0 }
|
|
568
630
|
);
|
|
569
631
|
}
|
|
570
632
|
const signData = body.sign_data;
|
|
571
633
|
const x402Expected = body.x402_expected;
|
|
572
634
|
if (!signData || typeof signData.hash !== "string" || !signData.typed_data || typeof signData.typed_data !== "object" || !x402Expected) {
|
|
573
|
-
throw new
|
|
574
|
-
"The Haven sign-context response is missing sign_data.typed_data or x402_expected \u2014 the backend may predate #1263. Pass typed_data_b64 from the quote result instead."
|
|
635
|
+
throw new HavenSignContextError(
|
|
636
|
+
"The Haven sign-context response is missing sign_data.typed_data or x402_expected \u2014 the backend may predate #1263. Pass typed_data_b64 from the quote result instead.",
|
|
637
|
+
"SIGN_CONTEXT_MALFORMED"
|
|
575
638
|
);
|
|
576
639
|
}
|
|
577
640
|
const paymentRequired = body.payment_required;
|
|
@@ -922,7 +985,7 @@ function createToolHandlers(signer, options = {}) {
|
|
|
922
985
|
authorization: args.authorization,
|
|
923
986
|
expectedAuth: args.expected_auth,
|
|
924
987
|
// Cross-check `to` against the Safe in the local credential when present.
|
|
925
|
-
expectedSafe: options.audit?.
|
|
988
|
+
expectedSafe: options.audit?.accountAddress
|
|
926
989
|
});
|
|
927
990
|
await auditSigning(
|
|
928
991
|
"haven_sign_sweep_delegate",
|
|
@@ -1002,6 +1065,18 @@ function normalizeError(err) {
|
|
|
1002
1065
|
next_action: sdk.AgentPaymentNextAction.StopAndTellUser
|
|
1003
1066
|
};
|
|
1004
1067
|
}
|
|
1068
|
+
if (err instanceof HavenSignContextError) {
|
|
1069
|
+
return {
|
|
1070
|
+
success: false,
|
|
1071
|
+
code: err.code,
|
|
1072
|
+
message: err.message,
|
|
1073
|
+
next_action: err.next_action,
|
|
1074
|
+
...err.fallback !== void 0 ? { fallback: err.fallback } : {},
|
|
1075
|
+
...err.retry_with_new_quote ? { retry_with_new_quote: true } : {},
|
|
1076
|
+
...err.http_status !== void 0 ? { http_status: err.http_status } : {},
|
|
1077
|
+
...err.backend_error_code !== void 0 ? { backend_error_code: err.backend_error_code } : {}
|
|
1078
|
+
};
|
|
1079
|
+
}
|
|
1005
1080
|
if (err instanceof sdk.HavenSigningError) {
|
|
1006
1081
|
return { success: false, code: err.code, message: err.message };
|
|
1007
1082
|
}
|
|
@@ -1035,7 +1110,7 @@ var SIGNER_CONSENT_SURFACE_VERSION = 2;
|
|
|
1035
1110
|
function computeSignerConsentHash(input) {
|
|
1036
1111
|
const identity = [
|
|
1037
1112
|
input.delegateAddress.toLowerCase(),
|
|
1038
|
-
(input.
|
|
1113
|
+
(input.accountAddress ?? "").toLowerCase(),
|
|
1039
1114
|
input.agentId ?? "",
|
|
1040
1115
|
input.chainId ?? "",
|
|
1041
1116
|
input.network ?? ""
|
|
@@ -1054,7 +1129,7 @@ function renderSignerConsentBlock(input, hash) {
|
|
|
1054
1129
|
"",
|
|
1055
1130
|
`Delegate address: ${input.delegateAddress}`
|
|
1056
1131
|
];
|
|
1057
|
-
lines.push(`Haven wallet: ${input.
|
|
1132
|
+
lines.push(`Haven wallet: ${input.accountAddress ?? "not provided to this signer"}`);
|
|
1058
1133
|
if (input.agentId) lines.push(`Agent ID: ${input.agentId}`);
|
|
1059
1134
|
if (typeof input.chainId === "number") lines.push(`Chain ID: ${input.chainId}`);
|
|
1060
1135
|
if (input.network) lines.push(`Network: ${input.network}`);
|
|
@@ -1156,7 +1231,6 @@ async function loadSignerCredentials(path = process.env.HAVEN_CREDENTIALS) {
|
|
|
1156
1231
|
delegateKey: envKey,
|
|
1157
1232
|
agentId: stringField(process.env.HAVEN_AGENT_ID),
|
|
1158
1233
|
accountAddress,
|
|
1159
|
-
safeAddress: accountAddress,
|
|
1160
1234
|
chainId: chainIdField(process.env.HAVEN_CHAIN_ID, "HAVEN_CHAIN_ID"),
|
|
1161
1235
|
network: stringField(process.env.HAVEN_NETWORK),
|
|
1162
1236
|
x402BindingSigner: stringField(process.env.HAVEN_X402_BINDING_SIGNER)
|
|
@@ -1191,7 +1265,6 @@ async function loadFromFile(path) {
|
|
|
1191
1265
|
delegateKey,
|
|
1192
1266
|
agentId: stringField(raw.agent_id ?? raw.agentId),
|
|
1193
1267
|
accountAddress,
|
|
1194
|
-
safeAddress: accountAddress,
|
|
1195
1268
|
chainId: chainIdField(raw.chain_id ?? raw.chainId, "chain_id"),
|
|
1196
1269
|
network: stringField(raw.network),
|
|
1197
1270
|
x402BindingSigner: stringField(
|
|
@@ -1204,7 +1277,22 @@ function readAccountAddressField(raw) {
|
|
|
1204
1277
|
return stringField(raw.account_address ?? raw.safe_address ?? raw.safeAddress);
|
|
1205
1278
|
}
|
|
1206
1279
|
function readAccountAddressEnv(env) {
|
|
1207
|
-
|
|
1280
|
+
const current = stringField(env.HAVEN_ACCOUNT_ADDRESS);
|
|
1281
|
+
for (const name of ["HAVEN_WALLET_ADDRESS", "HAVEN_SAFE_ADDRESS"]) {
|
|
1282
|
+
const retired = stringField(env[name]);
|
|
1283
|
+
if (!retired) continue;
|
|
1284
|
+
if (!current) {
|
|
1285
|
+
throw new Error(
|
|
1286
|
+
`${name} is retired (#2906) \u2014 Haven accounts are addressed as accounts, not Safes. Set HAVEN_ACCOUNT_ADDRESS to the same value. Refusing rather than ignoring it, because an unset account address silently skips the sweep-destination check.`
|
|
1287
|
+
);
|
|
1288
|
+
}
|
|
1289
|
+
if (retired.toLowerCase() !== current.toLowerCase()) {
|
|
1290
|
+
throw new Error(
|
|
1291
|
+
`${name} and HAVEN_ACCOUNT_ADDRESS were both set to different addresses. Remove the retired name, or make them match \u2014 picking one silently would hide the mismatch.`
|
|
1292
|
+
);
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
return current;
|
|
1208
1296
|
}
|
|
1209
1297
|
function stringField(value) {
|
|
1210
1298
|
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
@@ -1247,7 +1335,7 @@ async function warnIfCredentialFilePermissive(path, log = (message) => process.s
|
|
|
1247
1335
|
|
|
1248
1336
|
// src/server.ts
|
|
1249
1337
|
var SIGNER_NAME = "@haven_ai/signer";
|
|
1250
|
-
var SIGNER_VERSION = "0.
|
|
1338
|
+
var SIGNER_VERSION = "0.3.0-alpha.0";
|
|
1251
1339
|
async function resolveSignerRuntime(options = {}) {
|
|
1252
1340
|
assertSupportedNodeVersion(options.nodeVersion);
|
|
1253
1341
|
if (options.delegateKey) {
|
|
@@ -1282,7 +1370,7 @@ function buildSignerMcpServer(signer, options = {}) {
|
|
|
1282
1370
|
audit: {
|
|
1283
1371
|
auditPath: options.auditPath ?? defaultSigningAuditPath(credentialsPath),
|
|
1284
1372
|
delegateAddress: signer.delegateAddress,
|
|
1285
|
-
|
|
1373
|
+
accountAddress: options.credentials?.accountAddress,
|
|
1286
1374
|
chainId: options.credentials?.chainId
|
|
1287
1375
|
},
|
|
1288
1376
|
// #1263: the payment_id signing path — the ONLY network call this server
|
|
@@ -1332,7 +1420,7 @@ async function runSignerConsentGate(signer, credentials, options) {
|
|
|
1332
1420
|
return ensureSignerConsent(
|
|
1333
1421
|
{
|
|
1334
1422
|
delegateAddress: signer.delegateAddress,
|
|
1335
|
-
|
|
1423
|
+
accountAddress: credentials?.accountAddress,
|
|
1336
1424
|
agentId: credentials?.agentId,
|
|
1337
1425
|
chainId: credentials?.chainId,
|
|
1338
1426
|
network: credentials?.network,
|