@haven_ai/signer 0.8.1-alpha.0 → 0.9.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 +39 -4
- package/dist/cli.cjs +364 -26
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +365 -27
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +352 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +47 -6
- package/dist/index.d.ts +47 -6
- package/dist/index.js +353 -15
- package/dist/index.js.map +1 -1
- package/package.json +6 -5
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomUUID, createHash } from 'crypto';
|
|
2
2
|
import { recoverTypedDataAddress, hashTypedData, hashMessage } from 'viem';
|
|
3
3
|
import { privateKeyToAccount } from 'viem/accounts';
|
|
4
|
-
import { createNextStepBuilder, addressFromKey, HavenSigningError, buildSweepTypedData, selectStandardPaymentOption, HavenApiError, toStandardPaymentRequirements, decodeBase64Json, encodeBase64Json, x402V2PaymentEnvelope, isSettlementChildTypedData, chainIdForNetwork, assertOwnSettlementChild, isPackedUserOperationTypedData, assertUserOpTypedDataBinding, assertBoundDirectPaymentUserOp, assertFundingLegPaysDelegate, HavenTypedDataRefusedError, buildSweepAuthorizationMessage, verifySignature, x402AuthorizationAmount, SignerRefusalCode, connectorUpgradeCommand, SIGNER_UPDATE_FALLBACK, HavenUnsupportedSignerVersionError, buildX402ExpectedMessage, TASK_SIGN_CONTEXT_VERSION, DIRECT_SIGN_CONTEXT_VERSION, AgentPaymentNextAction, HavenError, AgentPaymentFailureCode, havenClientIdentity, isSupportedNodeVersion, unsupportedNodeVersionMessage, HavenUserOpBindingError, connectorRerunCommand, assertOwnTaskChild, assertOwnTaskBudgetCloseUserOp, assertOwnSubBudgetChild, assertOwnSubBudgetCloseUserOp, readClientUpdate, HAVEN_CLIENT_HEADER } from '@haven_ai/sdk/edge';
|
|
4
|
+
import { createNextStepBuilder, addressFromKey, HavenSigningError, buildSweepTypedData, selectStandardPaymentOption, HavenApiError, toStandardPaymentRequirements, decodeBase64Json, encodeBase64Json, x402V2PaymentEnvelope, isSettlementChildTypedData, chainIdForNetwork, assertOwnSettlementChild, isPackedUserOperationTypedData, assertUserOpTypedDataBinding, assertBoundDirectPaymentUserOp, assertFundingLegPaysDelegate, HavenTypedDataRefusedError, buildSweepAuthorizationMessage, verifySignature, x402AuthorizationAmount, SignerRefusalCode, connectorUpgradeCommand, SIGNER_UPDATE_FALLBACK, HavenUnsupportedSignerVersionError, buildX402ExpectedMessage, TASK_SIGN_CONTEXT_VERSION, DIRECT_SIGN_CONTEXT_VERSION, AgentPaymentNextAction, HavenError, AgentPaymentFailureCode, havenClientIdentity, isSupportedNodeVersion, unsupportedNodeVersionMessage, x402RetryHeadersFor, HavenUserOpBindingError, connectorRerunCommand, assertOwnTaskChild, assertOwnTaskBudgetCloseUserOp, assertOwnSubBudgetChild, assertOwnSubBudgetCloseUserOp, readClientUpdate, HAVEN_CLIENT_HEADER } from '@haven_ai/sdk/edge';
|
|
5
5
|
export { deriveDelegateAccountAddress } from '@haven_ai/sdk/edge';
|
|
6
6
|
import { stat, lstat, chmod, readFile, mkdir, rename, appendFile, writeFile } from 'fs/promises';
|
|
7
7
|
import { resolve, dirname, join } from 'path';
|
|
@@ -195,6 +195,10 @@ function createEdgeSigner(delegateKey, options = {}) {
|
|
|
195
195
|
);
|
|
196
196
|
}
|
|
197
197
|
return { signature };
|
|
198
|
+
},
|
|
199
|
+
async signSiwxMessage(message) {
|
|
200
|
+
const account = privateKeyToAccount(delegateKey);
|
|
201
|
+
return account.signMessage({ message });
|
|
198
202
|
}
|
|
199
203
|
};
|
|
200
204
|
}
|
|
@@ -911,7 +915,13 @@ function signerCompatibility() {
|
|
|
911
915
|
function signerCapabilityAdvertisement() {
|
|
912
916
|
return { experimental: { [SIGNER_CAPABILITY_KEY]: signerCompatibility() } };
|
|
913
917
|
}
|
|
914
|
-
function
|
|
918
|
+
function signerIdentityLines(identity) {
|
|
919
|
+
if (!identity?.agentId && !identity?.delegateAddress) return [];
|
|
920
|
+
const agent = identity.agentId ? `agent id ${identity.agentId}` : "no recorded agent id";
|
|
921
|
+
const delegate = identity.delegateAddress ? ` and delegate address ${identity.delegateAddress}` : "";
|
|
922
|
+
return [`This signer is bound to ${agent}${delegate}.`];
|
|
923
|
+
}
|
|
924
|
+
function signerInstructions(identity) {
|
|
915
925
|
const compatibility = signerCompatibility();
|
|
916
926
|
return [
|
|
917
927
|
"Haven edge signer: sign-only tools bound to the local delegate key. It never emits the",
|
|
@@ -920,6 +930,16 @@ function signerInstructions() {
|
|
|
920
930
|
"#3271, and delegation-rail x402) or haven_sign_x402 (x402 only) instead of relaying bulky",
|
|
921
931
|
"typed-data payloads yourself.",
|
|
922
932
|
"",
|
|
933
|
+
...signerIdentityLines(identity),
|
|
934
|
+
"When more than one Haven pair is configured, you act as ONE agent per task: if the",
|
|
935
|
+
"user has not said which (in the request, or a project-level choice they stated), ask",
|
|
936
|
+
"before any payment tool. Sign only through the signer of the hosted server you called:",
|
|
937
|
+
"haven-<slug> with haven-signer-<slug>, bare haven with haven-signer, Codex haven with",
|
|
938
|
+
"haven_signer. Before signing, compare the identity this signer states with",
|
|
939
|
+
"haven_get_agent (its id and delegateAddress) from that hosted server \u2014 the delegate",
|
|
940
|
+
"address alone when this signer has no recorded agent id. If they differ, stop and sign",
|
|
941
|
+
"nothing \u2014 switch to the signer whose identity matches.",
|
|
942
|
+
"",
|
|
923
943
|
"Version compatibility (check this BEFORE signing, not after):",
|
|
924
944
|
`- x402 expected-context versions supported: ${compatibility.x402_expected_context_versions.join(", ")}`,
|
|
925
945
|
`- sweep authorization binding versions supported: ${compatibility.sweep_binding_versions.join(", ")}`,
|
|
@@ -938,14 +958,22 @@ function signerInstructions() {
|
|
|
938
958
|
"machine-readable, not just prose: it carries code, supported_versions, received_version, and",
|
|
939
959
|
"fallback fields alongside the message, so you can branch on it directly. Every signer",
|
|
940
960
|
"refusal also carries the next-step family: next_tool_name + next_tool_server_role when a",
|
|
941
|
-
"hosted tool follows (resolve the role against your own server names
|
|
961
|
+
"hosted tool follows (resolve the role against your own server names \u2014 with several",
|
|
962
|
+
"Haven pairs, the hosted server of THIS signer's pair), else",
|
|
942
963
|
"next_tool_omitted_reason saying why not.",
|
|
943
964
|
"",
|
|
944
965
|
"An undeclared top-level argument is refused, not stripped: haven_sign answers",
|
|
945
966
|
"UNSUPPORTED_ARGUMENT with unknown_arguments, signer_version and fallback (the update",
|
|
946
967
|
"command) instead of dropping the key and answering the generic signing error \u2014 if you see",
|
|
947
968
|
"that refusal, this signer predates the argument form you sent. Update the signer; nothing",
|
|
948
|
-
"was signed, fetched or audited."
|
|
969
|
+
"was signed, fetched or audited.",
|
|
970
|
+
"",
|
|
971
|
+
"Sign-In-With-X (#3728): when a merchant 402 carries a sign-in-with-x extension (or the",
|
|
972
|
+
"merchant documents a CAIP-122 sign-in route), call haven_sign_siwx with { url, challenge }.",
|
|
973
|
+
"The signer composes the EIP-4361 message itself from the validated challenge and signs it with",
|
|
974
|
+
"the delegate key \u2014 it returns { sign_in_with_x_header }; retry the merchant with the",
|
|
975
|
+
"SIGN-IN-WITH-X header set to that value. This proves the delegate wallet paid the merchant; it",
|
|
976
|
+
"moves no funds and cannot approve any payment."
|
|
949
977
|
].join("\n");
|
|
950
978
|
}
|
|
951
979
|
var OWNER_ONLY_MODE = 384;
|
|
@@ -1131,6 +1159,8 @@ function createSigningAuditEntry(tool, payloadHash, context, now = /* @__PURE__
|
|
|
1131
1159
|
};
|
|
1132
1160
|
if (context.accountAddress) entry.safe_address = context.accountAddress;
|
|
1133
1161
|
if (typeof context.chainId === "number") entry.chain_id = context.chainId;
|
|
1162
|
+
if (context.domain !== void 0) entry.domain = context.domain;
|
|
1163
|
+
if (context.nonce !== void 0) entry.nonce = context.nonce;
|
|
1134
1164
|
return entry;
|
|
1135
1165
|
}
|
|
1136
1166
|
function hashPayloadForAudit(payload) {
|
|
@@ -1162,10 +1192,237 @@ var HavenBareHashRefusedError = class extends HavenSigningError {
|
|
|
1162
1192
|
});
|
|
1163
1193
|
}
|
|
1164
1194
|
};
|
|
1195
|
+
var SIWX_REFUSED = "SIWX_REFUSED";
|
|
1196
|
+
var HavenSiwxRefusedError = class extends HavenSigningError {
|
|
1197
|
+
next_action = AgentPaymentNextAction.StopAndTellUser;
|
|
1198
|
+
step;
|
|
1199
|
+
constructor(message) {
|
|
1200
|
+
super(message);
|
|
1201
|
+
this.code = SIWX_REFUSED;
|
|
1202
|
+
this.name = "HavenSiwxRefusedError";
|
|
1203
|
+
this.step = signerRefusalStep({
|
|
1204
|
+
nextAction: AgentPaymentNextAction.StopAndTellUser,
|
|
1205
|
+
nextTool: null,
|
|
1206
|
+
nextToolOmittedReason: "check the 402 sign-in-with-x challenge and the URL against the refusal above, then call haven_sign_siwx again with corrected inputs \u2014 a challenge that fails validation is never signed"
|
|
1207
|
+
});
|
|
1208
|
+
}
|
|
1209
|
+
};
|
|
1210
|
+
var SIWX_MAX_AGE_SECONDS = 300;
|
|
1211
|
+
var SIWX_CLOCK_SKEW_SECONDS = 30;
|
|
1212
|
+
var SIWX_VERSION = "1";
|
|
1213
|
+
function refuse(message) {
|
|
1214
|
+
throw new HavenSiwxRefusedError(message);
|
|
1215
|
+
}
|
|
1216
|
+
function isPlainObject(value) {
|
|
1217
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1218
|
+
}
|
|
1219
|
+
function stringField2(info, name) {
|
|
1220
|
+
const value = info[name];
|
|
1221
|
+
if (value === void 0) return void 0;
|
|
1222
|
+
if (typeof value !== "string") {
|
|
1223
|
+
refuse(`The sign-in-with-x challenge field "${name}" must be a string.`);
|
|
1224
|
+
}
|
|
1225
|
+
return value;
|
|
1226
|
+
}
|
|
1227
|
+
function assertSingleLine(value, name) {
|
|
1228
|
+
if (/[\n\r]/.test(value)) {
|
|
1229
|
+
refuse(`The sign-in-with-x challenge field "${name}" contains a line break; EIP-4361 fields are single-line. Refusing \u2014 a line break could smuggle an attacker-chosen line into the signed message.`);
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
function validateSiwxChallenge(input) {
|
|
1233
|
+
const { url, challenge, chainId } = input;
|
|
1234
|
+
const now = input.now ?? /* @__PURE__ */ new Date();
|
|
1235
|
+
if (typeof url !== "string" || url.length === 0) {
|
|
1236
|
+
refuse("url is required: the URL the agent is calling, so the signature is bound to that origin.");
|
|
1237
|
+
}
|
|
1238
|
+
let urlObj;
|
|
1239
|
+
try {
|
|
1240
|
+
urlObj = new URL(url);
|
|
1241
|
+
} catch {
|
|
1242
|
+
refuse("url is not a valid absolute URL.");
|
|
1243
|
+
}
|
|
1244
|
+
if (urlObj.protocol !== "https:") {
|
|
1245
|
+
refuse(
|
|
1246
|
+
`url must be https \u2014 this signer never signs a sign-in bound to a plain-HTTP origin (got '${urlObj.protocol}//').`
|
|
1247
|
+
);
|
|
1248
|
+
}
|
|
1249
|
+
if (!isPlainObject(challenge)) {
|
|
1250
|
+
refuse('challenge must be the sign-in-with-x object from the merchant 402 (extensions["sign-in-with-x"]).');
|
|
1251
|
+
}
|
|
1252
|
+
const hasWrapper = isPlainObject(challenge.info);
|
|
1253
|
+
const info = hasWrapper ? challenge.info : challenge;
|
|
1254
|
+
const supportedChains = hasWrapper ? challenge.supportedChains : challenge.supportedChains;
|
|
1255
|
+
const domain = stringField2(info, "domain");
|
|
1256
|
+
if (domain === void 0) refuse("The sign-in-with-x challenge carries no domain.");
|
|
1257
|
+
assertSingleLine(domain, "domain");
|
|
1258
|
+
if (/\s/.test(domain)) {
|
|
1259
|
+
refuse("The sign-in-with-x challenge domain is not a valid authority (contains whitespace).");
|
|
1260
|
+
}
|
|
1261
|
+
if (domain.toLowerCase() !== urlObj.host.toLowerCase()) {
|
|
1262
|
+
refuse(
|
|
1263
|
+
`The challenge domain '${domain}' does not match the host of the URL being called ('${urlObj.host}'). A sign-in is only valid for the origin that issued it.`
|
|
1264
|
+
);
|
|
1265
|
+
}
|
|
1266
|
+
const uri = stringField2(info, "uri");
|
|
1267
|
+
if (uri === void 0) refuse("The sign-in-with-x challenge carries no uri.");
|
|
1268
|
+
assertSingleLine(uri, "uri");
|
|
1269
|
+
let uriObj;
|
|
1270
|
+
try {
|
|
1271
|
+
uriObj = new URL(uri);
|
|
1272
|
+
} catch {
|
|
1273
|
+
refuse("The sign-in-with-x challenge uri is not a valid absolute URI.");
|
|
1274
|
+
}
|
|
1275
|
+
if (uriObj.protocol !== urlObj.protocol || uriObj.host.toLowerCase() !== urlObj.host.toLowerCase()) {
|
|
1276
|
+
refuse(
|
|
1277
|
+
`The challenge resource uri origin ('${uriObj.protocol}//${uriObj.host}') does not match the origin of the URL being called ('${urlObj.protocol}//${urlObj.host}').`
|
|
1278
|
+
);
|
|
1279
|
+
}
|
|
1280
|
+
const version = stringField2(info, "version");
|
|
1281
|
+
if (version === void 0) {
|
|
1282
|
+
refuse("The sign-in-with-x challenge carries no version; this signer composes CAIP-122 version 1 only.");
|
|
1283
|
+
}
|
|
1284
|
+
if (version !== SIWX_VERSION) {
|
|
1285
|
+
refuse(
|
|
1286
|
+
`The sign-in-with-x challenge version is '${version}'; this signer composes version '${SIWX_VERSION}' only. Update @haven_ai/signer if the spec has moved on.`
|
|
1287
|
+
);
|
|
1288
|
+
}
|
|
1289
|
+
const nonce = stringField2(info, "nonce");
|
|
1290
|
+
if (nonce === void 0) {
|
|
1291
|
+
refuse("The sign-in-with-x challenge carries no nonce \u2014 refusing: the nonce is what makes the sign-in single-use.");
|
|
1292
|
+
}
|
|
1293
|
+
if (!/^[A-Za-z0-9]{8,}$/.test(nonce)) {
|
|
1294
|
+
refuse("The sign-in-with-x challenge nonce fails the EIP-4361 grammar (alphanumeric, at least 8 characters).");
|
|
1295
|
+
}
|
|
1296
|
+
const issuedAt = stringField2(info, "issuedAt");
|
|
1297
|
+
if (issuedAt === void 0) refuse("The sign-in-with-x challenge carries no issuedAt timestamp.");
|
|
1298
|
+
assertSingleLine(issuedAt, "issuedAt");
|
|
1299
|
+
if (Number.isNaN(Date.parse(issuedAt))) {
|
|
1300
|
+
refuse("The sign-in-with-x challenge issuedAt is not a parseable timestamp.");
|
|
1301
|
+
}
|
|
1302
|
+
const expirationTime = stringField2(info, "expirationTime");
|
|
1303
|
+
if (expirationTime === void 0) {
|
|
1304
|
+
refuse(
|
|
1305
|
+
"The sign-in-with-x challenge carries no absolute expirationTime. A relative expirationSeconds is not accepted by this signer \u2014 only a challenge with an absolute expiry window the signer can bound is signed."
|
|
1306
|
+
);
|
|
1307
|
+
}
|
|
1308
|
+
assertSingleLine(expirationTime, "expirationTime");
|
|
1309
|
+
const expiresAtMs = Date.parse(expirationTime);
|
|
1310
|
+
if (Number.isNaN(expiresAtMs)) {
|
|
1311
|
+
refuse("The sign-in-with-x challenge expirationTime is not a parseable timestamp.");
|
|
1312
|
+
}
|
|
1313
|
+
if (expiresAtMs < now.getTime() - SIWX_CLOCK_SKEW_SECONDS * 1e3) {
|
|
1314
|
+
refuse("The sign-in-with-x challenge has already expired (expirationTime is in the past, beyond the clock-skew tolerance). Nothing was signed.");
|
|
1315
|
+
}
|
|
1316
|
+
if (expiresAtMs - now.getTime() > (SIWX_MAX_AGE_SECONDS + SIWX_CLOCK_SKEW_SECONDS) * 1e3) {
|
|
1317
|
+
refuse(
|
|
1318
|
+
`The sign-in-with-x challenge expiry window is longer than the spec's ${SIWX_MAX_AGE_SECONDS}-second max age (beyond the clock-skew tolerance). Refusing to sign a long-lived challenge.`
|
|
1319
|
+
);
|
|
1320
|
+
}
|
|
1321
|
+
const statement = stringField2(info, "statement");
|
|
1322
|
+
if (statement !== void 0) assertSingleLine(statement, "statement");
|
|
1323
|
+
const notBefore = stringField2(info, "notBefore");
|
|
1324
|
+
if (notBefore !== void 0) {
|
|
1325
|
+
assertSingleLine(notBefore, "notBefore");
|
|
1326
|
+
if (Number.isNaN(Date.parse(notBefore))) {
|
|
1327
|
+
refuse("The sign-in-with-x challenge notBefore is not a parseable timestamp.");
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
const requestId = stringField2(info, "requestId");
|
|
1331
|
+
if (requestId !== void 0) assertSingleLine(requestId, "requestId");
|
|
1332
|
+
let resources;
|
|
1333
|
+
if (info.resources !== void 0) {
|
|
1334
|
+
if (!Array.isArray(info.resources) || info.resources.some((r) => typeof r !== "string")) {
|
|
1335
|
+
refuse("The sign-in-with-x challenge resources must be an array of URI strings.");
|
|
1336
|
+
}
|
|
1337
|
+
for (const resource of info.resources) {
|
|
1338
|
+
assertSingleLine(resource, "resources[]");
|
|
1339
|
+
try {
|
|
1340
|
+
new URL(resource);
|
|
1341
|
+
} catch {
|
|
1342
|
+
refuse(`The sign-in-with-x challenge resources entry '${resource}' is not a valid absolute URI.`);
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
resources = info.resources;
|
|
1346
|
+
}
|
|
1347
|
+
if (typeof chainId !== "number" || !Number.isInteger(chainId) || chainId <= 0) {
|
|
1348
|
+
refuse(
|
|
1349
|
+
"This signer credential carries no chain id, so the signer cannot check the challenge against the chain its delegate key pays on. Re-run the connector to refresh the credential."
|
|
1350
|
+
);
|
|
1351
|
+
}
|
|
1352
|
+
const chainIdCaip2 = `eip155:${chainId}`;
|
|
1353
|
+
if (!Array.isArray(supportedChains) || supportedChains.length === 0) {
|
|
1354
|
+
refuse("The sign-in-with-x challenge carries no supportedChains array.");
|
|
1355
|
+
}
|
|
1356
|
+
const matched = supportedChains.some(
|
|
1357
|
+
(entry) => isPlainObject(entry) && typeof entry.chainId === "string" && entry.chainId.toLowerCase() === chainIdCaip2 && entry.type === "eip191"
|
|
1358
|
+
);
|
|
1359
|
+
if (!matched) {
|
|
1360
|
+
refuse(
|
|
1361
|
+
`The challenge's supportedChains does not offer the credential chain ${chainIdCaip2} with an eip191 signature type \u2014 the delegate EOA cannot answer this challenge.`
|
|
1362
|
+
);
|
|
1363
|
+
}
|
|
1364
|
+
return {
|
|
1365
|
+
domain,
|
|
1366
|
+
uri,
|
|
1367
|
+
version,
|
|
1368
|
+
nonce,
|
|
1369
|
+
issuedAt,
|
|
1370
|
+
expirationTime,
|
|
1371
|
+
statement,
|
|
1372
|
+
notBefore,
|
|
1373
|
+
requestId,
|
|
1374
|
+
resources,
|
|
1375
|
+
chainIdCaip2,
|
|
1376
|
+
numericChainId: chainId
|
|
1377
|
+
};
|
|
1378
|
+
}
|
|
1379
|
+
function composeSiwxMessage(fields, address) {
|
|
1380
|
+
const suffixLines = [
|
|
1381
|
+
`URI: ${fields.uri}`,
|
|
1382
|
+
`Version: ${fields.version ?? SIWX_VERSION}`,
|
|
1383
|
+
`Chain ID: ${fields.numericChainId}`,
|
|
1384
|
+
`Nonce: ${fields.nonce}`,
|
|
1385
|
+
`Issued At: ${fields.issuedAt}`
|
|
1386
|
+
];
|
|
1387
|
+
if (fields.expirationTime !== void 0) suffixLines.push(`Expiration Time: ${fields.expirationTime}`);
|
|
1388
|
+
if (fields.notBefore !== void 0) suffixLines.push(`Not Before: ${fields.notBefore}`);
|
|
1389
|
+
if (fields.requestId !== void 0) suffixLines.push(`Request ID: ${fields.requestId}`);
|
|
1390
|
+
if (fields.resources !== void 0) {
|
|
1391
|
+
suffixLines.push(["Resources:", ...fields.resources.map((r) => `- ${r}`)].join("\n"));
|
|
1392
|
+
}
|
|
1393
|
+
let head = [`${fields.domain} wants you to sign in with your Ethereum account:`, address].join("\n");
|
|
1394
|
+
if (fields.statement === void 0) {
|
|
1395
|
+
head += "\n\n";
|
|
1396
|
+
} else {
|
|
1397
|
+
head = [head, fields.statement].join("\n\n") + "\n";
|
|
1398
|
+
}
|
|
1399
|
+
return [head, suffixLines.join("\n")].join("\n");
|
|
1400
|
+
}
|
|
1401
|
+
function buildSiwxPayload(fields, address, signature) {
|
|
1402
|
+
return {
|
|
1403
|
+
domain: fields.domain,
|
|
1404
|
+
address,
|
|
1405
|
+
statement: fields.statement,
|
|
1406
|
+
uri: fields.uri,
|
|
1407
|
+
version: fields.version ?? SIWX_VERSION,
|
|
1408
|
+
chainId: fields.chainIdCaip2,
|
|
1409
|
+
type: "eip191",
|
|
1410
|
+
nonce: fields.nonce,
|
|
1411
|
+
issuedAt: fields.issuedAt,
|
|
1412
|
+
expirationTime: fields.expirationTime,
|
|
1413
|
+
notBefore: fields.notBefore,
|
|
1414
|
+
requestId: fields.requestId,
|
|
1415
|
+
resources: fields.resources,
|
|
1416
|
+
signature
|
|
1417
|
+
};
|
|
1418
|
+
}
|
|
1419
|
+
function encodeSiwxHeader(payload) {
|
|
1420
|
+
return Buffer.from(JSON.stringify(payload), "utf8").toString("base64");
|
|
1421
|
+
}
|
|
1165
1422
|
|
|
1166
1423
|
// src/tools.ts
|
|
1167
1424
|
var SIGNER_NAME = "@haven_ai/signer";
|
|
1168
|
-
var SIGNER_VERSION = "0.
|
|
1425
|
+
var SIGNER_VERSION = "0.9.0-alpha.0";
|
|
1169
1426
|
var UNSUPPORTED_ARGUMENT_MARKER = "HavenSignerUnsupportedArgument:";
|
|
1170
1427
|
var USEROP_BINDING_MISMATCH = "USEROP_BINDING_MISMATCH";
|
|
1171
1428
|
var HavenUserOpBindingRefusedError = class extends HavenSigningError {
|
|
@@ -1328,6 +1585,18 @@ var toolSchemas = {
|
|
|
1328
1585
|
typed_data: z$1.record(z$1.string(), z$1.unknown()).optional(),
|
|
1329
1586
|
// #1255: see haven_sign.typed_data_b64 — the copy-through-safe form.
|
|
1330
1587
|
typed_data_b64: z$1.string().min(1).max(262144).optional()
|
|
1588
|
+
},
|
|
1589
|
+
haven_sign_siwx: {
|
|
1590
|
+
// #3728: the https URL the agent is calling — the challenge's domain and
|
|
1591
|
+
// resource uri are pinned to THIS origin before anything is signed.
|
|
1592
|
+
url: z$1.string().min(1),
|
|
1593
|
+
// The merchant's sign-in-with-x challenge. Accepted as EITHER the 402
|
|
1594
|
+
// extension object (payment_required.extensions['sign-in-with-x'] —
|
|
1595
|
+
// { info, supportedChains, schema? }) or the flattened info object the
|
|
1596
|
+
// x402 reference client builds ({ ...info, chainId, type }). Validated
|
|
1597
|
+
// field by field in siwx.ts; the signer composes the EIP-4361 message
|
|
1598
|
+
// itself and never signs a caller-supplied message.
|
|
1599
|
+
challenge: z$1.record(z$1.string(), z$1.unknown())
|
|
1331
1600
|
}
|
|
1332
1601
|
// #3101: keys survive on the type (see the hosted server's contracts.ts).
|
|
1333
1602
|
};
|
|
@@ -1366,8 +1635,10 @@ var X402_SIGN_HEADER_DESCRIPTION = [
|
|
|
1366
1635
|
"do not call this tool. The signer validates the merchant, amount, resource, asset, and",
|
|
1367
1636
|
"network against the recorded funding context before signing, checks expires_at when present,",
|
|
1368
1637
|
"and rejects mismatches or expired payment windows.",
|
|
1369
|
-
"Returns { payment_header, accepted }. On your retry set
|
|
1370
|
-
"
|
|
1638
|
+
"Returns { payment_header, retry_headers, accepted }. On your retry set EVERY header named in",
|
|
1639
|
+
"retry_headers to <payment_header> \u2014 the names come from the same rule the wire uses (both",
|
|
1640
|
+
"PAYMENT-SIGNATURE (x402 v2) and X-PAYMENT (v1) on the EIP-3009 bridge; a strict v2 merchant reads",
|
|
1641
|
+
"only the first).",
|
|
1371
1642
|
"Only call after haven_submit has confirmed the funding step (nextAction=none or",
|
|
1372
1643
|
"the funding tx has a confirmed status). Next for paid MCP tools: call mcp__haven__haven_complete_mcp_tool."
|
|
1373
1644
|
].join(" ");
|
|
@@ -1393,8 +1664,8 @@ var SIGN_X402_DESCRIPTION = [
|
|
|
1393
1664
|
"haven_sign, not to this one. payment_header IS the header to use.",
|
|
1394
1665
|
"Next: for a paid MCP tool, call mcp__haven__haven_settle_mcp_tool. For a direct plain-HTTP x402",
|
|
1395
1666
|
"merchant (the haven_pay_x402_quote path), relay signature via mcp__haven__haven_submit and then",
|
|
1396
|
-
"retry the original merchant URL YOURSELF, setting
|
|
1397
|
-
"
|
|
1667
|
+
"retry the original merchant URL YOURSELF, setting EVERY header the result names in",
|
|
1668
|
+
"retry_headers to payment_header \u2014 Haven never sends that merchant the paid request."
|
|
1398
1669
|
].join(" ");
|
|
1399
1670
|
var SIGN_SWEEP_DELEGATE_DESCRIPTION = [
|
|
1400
1671
|
"Sign a Haven-prepared gasless USDC sweep that recovers stranded funds from the delegate",
|
|
@@ -1406,8 +1677,28 @@ var SIGN_SWEEP_DELEGATE_DESCRIPTION = [
|
|
|
1406
1677
|
"when the credential carries one, and otherwise rests on Haven's binding signature. Then it",
|
|
1407
1678
|
"returns { signature } to hand back to mcp__haven__haven_sweep_delegate to complete recovery."
|
|
1408
1679
|
].join(" ");
|
|
1680
|
+
var SIGN_SIWX_DESCRIPTION = [
|
|
1681
|
+
"Answer a merchant x402 Sign-In-With-X (SIWX) sign-in challenge with the local delegate key (#3728).",
|
|
1682
|
+
"Use it when a merchant 402 carries a sign-in-with-x extension, or when the merchant documents a",
|
|
1683
|
+
"sign-in route (CAIP-122): pass url \u2014 the FINAL URL the header will be sent to, i.e. after any",
|
|
1684
|
+
"redirects, not the originally requested one \u2014 and challenge, the sign-in-with-x object from the",
|
|
1685
|
+
"402 (extensions['sign-in-with-x'], or the flattened info object). NEVER follow a redirect with",
|
|
1686
|
+
"the SIGN-IN-WITH-X header (or a resulting session token) attached: re-sign for the final origin.",
|
|
1687
|
+
"This signer composes the EIP-4361 message ITSELF from the validated challenge fields and the",
|
|
1688
|
+
"delegate address read from the key \u2014 it never signs a caller-supplied message, and an address",
|
|
1689
|
+
"inside the challenge cannot change whose wallet signs in. It refuses (SIWX_REFUSED) unless the",
|
|
1690
|
+
"challenge domain matches the URL host, its resource uri origin matches the URL origin, the URL is",
|
|
1691
|
+
"https, the nonce is well-formed, the expiry window is at most 5 minutes (plus a small clock-skew",
|
|
1692
|
+
"tolerance), and supportedChains offers the credential chain with an eip191 signature type. This",
|
|
1693
|
+
"tool MOVES NO FUNDS and grants no spend authority: it only proves the delegate wallet is the one",
|
|
1694
|
+
"that paid the merchant. Returns { sign_in_with_x_header } \u2014 retry the merchant request with the",
|
|
1695
|
+
"SIGN-IN-WITH-X header set to that value. The merchant then recognises the delegate wallet as a",
|
|
1696
|
+
"paying customer (a session it mints typically outlives the 5-minute signature; what the session",
|
|
1697
|
+
"reaches is merchant-defined)."
|
|
1698
|
+
].join(" ");
|
|
1409
1699
|
var toolDescriptions = {
|
|
1410
1700
|
haven_sign: SIGN_DESCRIPTION,
|
|
1701
|
+
haven_sign_siwx: SIGN_SIWX_DESCRIPTION,
|
|
1411
1702
|
haven_x402_sign_header: X402_SIGN_HEADER_DESCRIPTION,
|
|
1412
1703
|
haven_sign_x402: SIGN_X402_DESCRIPTION,
|
|
1413
1704
|
haven_sign_sweep_delegate: SIGN_SWEEP_DELEGATE_DESCRIPTION
|
|
@@ -1735,7 +2026,13 @@ function createToolHandlers(signer, options = {}) {
|
|
|
1735
2026
|
"haven_x402_sign_header",
|
|
1736
2027
|
hashPayloadForAudit(args.payment_required)
|
|
1737
2028
|
);
|
|
1738
|
-
return {
|
|
2029
|
+
return {
|
|
2030
|
+
payment_header: result.paymentHeader,
|
|
2031
|
+
accepted: result.accepted,
|
|
2032
|
+
// #3727: the ready-made retry headers beside the prose — the names
|
|
2033
|
+
// come from the SDK's live rule, never re-derived here.
|
|
2034
|
+
retry_headers: x402RetryHeadersFor(result.paymentHeader)
|
|
2035
|
+
};
|
|
1739
2036
|
}),
|
|
1740
2037
|
haven_sign_x402: async (input) => runTool(async () => {
|
|
1741
2038
|
const args = parseStrictFor("haven_sign_x402", coerceX402Expected(coercePaymentRequired(input)));
|
|
@@ -1770,6 +2067,9 @@ function createToolHandlers(signer, options = {}) {
|
|
|
1770
2067
|
x402_binding: funding.x402Binding,
|
|
1771
2068
|
payment_header: header.paymentHeader,
|
|
1772
2069
|
accepted: header.accepted,
|
|
2070
|
+
// #3727: the ready-made retry headers beside the prose — the names
|
|
2071
|
+
// come from the SDK's live rule, never re-derived here.
|
|
2072
|
+
retry_headers: x402RetryHeadersFor(header.paymentHeader),
|
|
1773
2073
|
...clientUpdateField(resolved)
|
|
1774
2074
|
};
|
|
1775
2075
|
}),
|
|
@@ -1787,15 +2087,35 @@ function createToolHandlers(signer, options = {}) {
|
|
|
1787
2087
|
hashPayloadForAudit(args.authorization)
|
|
1788
2088
|
);
|
|
1789
2089
|
return { signature: result.signature };
|
|
2090
|
+
}),
|
|
2091
|
+
haven_sign_siwx: async (input) => runTool(async () => {
|
|
2092
|
+
const args = parseStrictFor("haven_sign_siwx", input);
|
|
2093
|
+
const chainId = options.audit?.chainId;
|
|
2094
|
+
const fields = validateSiwxChallenge({
|
|
2095
|
+
url: args.url,
|
|
2096
|
+
challenge: args.challenge,
|
|
2097
|
+
chainId
|
|
2098
|
+
});
|
|
2099
|
+
const message = composeSiwxMessage(fields, signer.delegateAddress);
|
|
2100
|
+
const signature = await signer.signSiwxMessage(message);
|
|
2101
|
+
const header = encodeSiwxHeader(
|
|
2102
|
+
buildSiwxPayload(fields, signer.delegateAddress, signature)
|
|
2103
|
+
);
|
|
2104
|
+
await auditSigning("haven_sign_siwx", hashPayloadForAudit(message), {
|
|
2105
|
+
domain: fields.domain,
|
|
2106
|
+
nonce: fields.nonce
|
|
2107
|
+
});
|
|
2108
|
+
return { sign_in_with_x_header: header };
|
|
1790
2109
|
})
|
|
1791
2110
|
};
|
|
1792
|
-
async function auditSigning(tool, payloadHash) {
|
|
2111
|
+
async function auditSigning(tool, payloadHash, extra = {}) {
|
|
1793
2112
|
if (!options.audit) return;
|
|
1794
2113
|
const { auditPath, ...context } = options.audit;
|
|
1795
2114
|
try {
|
|
1796
2115
|
await appendSigningAuditEntry(
|
|
1797
2116
|
createSigningAuditEntry(tool, payloadHash, {
|
|
1798
2117
|
...context,
|
|
2118
|
+
...extra,
|
|
1799
2119
|
delegateAddress: signer.delegateAddress
|
|
1800
2120
|
}),
|
|
1801
2121
|
auditPath
|
|
@@ -1950,6 +2270,15 @@ function normalizeError(err) {
|
|
|
1950
2270
|
...err.next_tool_omitted_reason ? { next_tool_omitted_reason: err.next_tool_omitted_reason } : {}
|
|
1951
2271
|
};
|
|
1952
2272
|
}
|
|
2273
|
+
if (err instanceof HavenSiwxRefusedError) {
|
|
2274
|
+
return {
|
|
2275
|
+
success: false,
|
|
2276
|
+
code: err.code,
|
|
2277
|
+
message: err.message,
|
|
2278
|
+
next_action: err.next_action,
|
|
2279
|
+
...nextStepWireFields(err.step)
|
|
2280
|
+
};
|
|
2281
|
+
}
|
|
1953
2282
|
if (err instanceof HavenSigningError) {
|
|
1954
2283
|
return { success: false, code: err.code, message: err.message };
|
|
1955
2284
|
}
|
|
@@ -1986,6 +2315,7 @@ function normalizeError(err) {
|
|
|
1986
2315
|
// src/consent.ts
|
|
1987
2316
|
var toolSummaries = {
|
|
1988
2317
|
haven_sign: "signs a Haven-prepared payment, task-budget or sub-budget open or close (a UserOp from your own account redeeming a delegation made to it, directly or via a task-budget child; a sub-budget child narrowing your budget for another agent of yours; or an x402 funding intent against a Haven-signed context)",
|
|
2318
|
+
haven_sign_siwx: "signs a merchant x402 Sign-In-With-X sign-in challenge as the delegate wallet \u2014 proves the wallet paid that merchant, moves no funds and grants no spend authority",
|
|
1989
2319
|
haven_x402_sign_header: "builds and signs the EIP-3009 merchant payment header for an x402 payment",
|
|
1990
2320
|
haven_sign_x402: "the two above in one call: funding payload + merchant header",
|
|
1991
2321
|
haven_sign_sweep_delegate: "signs a Haven-prepared gasless sweep of stranded funds back to your own account"
|
|
@@ -2025,13 +2355,16 @@ function renderSignerConsentBlock(input, hash) {
|
|
|
2025
2355
|
if (input.network) lines.push(`Network: ${input.network}`);
|
|
2026
2356
|
lines.push("");
|
|
2027
2357
|
lines.push("This local signer holds the delegate key on this machine and signs");
|
|
2028
|
-
lines.push("payment payloads
|
|
2358
|
+
lines.push("payment payloads, x402 merchant headers, or x402 Sign-In-With-X sign-in");
|
|
2359
|
+
lines.push("challenges for the delegate address above. A sign-in challenge proves to a");
|
|
2360
|
+
lines.push("merchant that this wallet paid it before; it moves no funds and grants no");
|
|
2361
|
+
lines.push("spend authority.");
|
|
2029
2362
|
lines.push("Its one network use is a read-only fetch of a pending payment's signing");
|
|
2030
2363
|
lines.push("payload from Haven, authenticated with the agent credential stored next to");
|
|
2031
2364
|
lines.push("this signer's key file - it never sends the key, a signature, or anything");
|
|
2032
2365
|
lines.push("else outbound, and it cannot show a live allowance summary.");
|
|
2033
2366
|
lines.push("The agent's signed budget delegation is the real spend gate, and the wallet");
|
|
2034
|
-
lines.push("owner can
|
|
2367
|
+
lines.push("owner can stop the agent's budget or remove the agent outside this signer.");
|
|
2035
2368
|
lines.push("");
|
|
2036
2369
|
lines.push("Tools this signer will expose to your agent runtime (one line each; the");
|
|
2037
2370
|
lines.push("full agent-facing descriptions are what the runtime sees):");
|
|
@@ -2041,7 +2374,7 @@ function renderSignerConsentBlock(input, hash) {
|
|
|
2041
2374
|
lines.push("");
|
|
2042
2375
|
lines.push("A local audit entry is appended for every signing operation. Audit entries");
|
|
2043
2376
|
lines.push("record timestamp, tool, payload hash, and delegate address; never the key,");
|
|
2044
|
-
lines.push("signature,
|
|
2377
|
+
lines.push("signature, x402 payment header, or sign-in message.");
|
|
2045
2378
|
lines.push("");
|
|
2046
2379
|
lines.push(`Consent hash: ${hash}`);
|
|
2047
2380
|
lines.push("");
|
|
@@ -2144,7 +2477,12 @@ function buildSignerMcpServer(signer, options = {}) {
|
|
|
2144
2477
|
{ name: SIGNER_NAME, version: SIGNER_VERSION },
|
|
2145
2478
|
{
|
|
2146
2479
|
capabilities: signerCapabilityAdvertisement(),
|
|
2147
|
-
|
|
2480
|
+
// #3738: the identity line lets a model holding several Haven pairs
|
|
2481
|
+
// confirm this signer belongs to the hosted server it called.
|
|
2482
|
+
instructions: signerInstructions({
|
|
2483
|
+
agentId: options.credentials?.agentId,
|
|
2484
|
+
delegateAddress: signer.delegateAddress
|
|
2485
|
+
})
|
|
2148
2486
|
}
|
|
2149
2487
|
);
|
|
2150
2488
|
const credentialsPath = options.credentials?.sourcePath;
|