@haven_ai/signer 0.1.23-alpha.2 → 0.1.25-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/dist/cli.cjs +153 -1
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +153 -1
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +153 -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 +153 -1
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
package/dist/cli.cjs
CHANGED
|
@@ -46,6 +46,134 @@ function stableStringify(value) {
|
|
|
46
46
|
const object = value;
|
|
47
47
|
return `{${Object.keys(object).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(object[key])}`).join(",")}}`;
|
|
48
48
|
}
|
|
49
|
+
var DELEGATION_MANAGER = "0xdb9B1e94B5b69Df7e401DDbedE43491141047dB3";
|
|
50
|
+
var CAVEAT_ENFORCERS = {
|
|
51
|
+
erc20TransferAmount: "0xf100b0819427117EcF76Ed94B358B1A5b5C6D2Fc",
|
|
52
|
+
allowedCalldata: "0xc2b0d624c1c4319760C96503BA27C347F3260f55",
|
|
53
|
+
timestamp: "0x1046bb45C8d673d4ea75321280DB34899413c069"};
|
|
54
|
+
var MAX_SETTLEMENT_WINDOW_SECONDS = 600;
|
|
55
|
+
function isSettlementChildTypedData(value) {
|
|
56
|
+
const td = value;
|
|
57
|
+
return !!td && td.primaryType === "Delegation" && typeof td.domain?.verifyingContract === "string" && Array.isArray(td.message?.caveats);
|
|
58
|
+
}
|
|
59
|
+
function same(a, b) {
|
|
60
|
+
return !!a && !!b && a.toLowerCase() === b.toLowerCase();
|
|
61
|
+
}
|
|
62
|
+
function hex(value) {
|
|
63
|
+
return value.startsWith("0x") ? value.slice(2).toLowerCase() : value.toLowerCase();
|
|
64
|
+
}
|
|
65
|
+
function slice(terms, start, end) {
|
|
66
|
+
const body = hex(terms);
|
|
67
|
+
return end === void 0 ? body.slice(start * 2) : body.slice(start * 2, end * 2);
|
|
68
|
+
}
|
|
69
|
+
function findCaveat(caveats, enforcer) {
|
|
70
|
+
return caveats.find((c) => same(c.enforcer, enforcer));
|
|
71
|
+
}
|
|
72
|
+
function refuse(what, detail) {
|
|
73
|
+
throw new sdk.HavenSigningError(
|
|
74
|
+
`Refusing to sign the x402 settlement child: ${what}. ${detail} The signature on this child is what lets a merchant pull from the treasury, so it is verified locally against the Haven-signed expected context rather than trusted.`
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
function chainIdForNetwork(network) {
|
|
78
|
+
if (!network) return void 0;
|
|
79
|
+
const caip = /^eip155:(\d+)$/.exec(network);
|
|
80
|
+
if (caip) return Number(caip[1]);
|
|
81
|
+
if (network === "base") return 8453;
|
|
82
|
+
if (network === "base-sepolia") return 84532;
|
|
83
|
+
return void 0;
|
|
84
|
+
}
|
|
85
|
+
function verifySettlementChild(typedData, expected, now = Date.now()) {
|
|
86
|
+
if (typedData.primaryType !== "Delegation") {
|
|
87
|
+
refuse("it is not a Delegation payload", `primaryType was '${typedData.primaryType}'.`);
|
|
88
|
+
}
|
|
89
|
+
if (!same(typedData.domain?.verifyingContract, DELEGATION_MANAGER)) {
|
|
90
|
+
refuse(
|
|
91
|
+
"the EIP-712 domain names an unknown DelegationManager",
|
|
92
|
+
`Expected ${DELEGATION_MANAGER}, got ${typedData.domain?.verifyingContract}.`
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
const domainChain = Number(typedData.domain?.chainId);
|
|
96
|
+
if (!Number.isFinite(domainChain) || domainChain !== expected.chainId) {
|
|
97
|
+
refuse(
|
|
98
|
+
"it is scoped to the wrong chain",
|
|
99
|
+
`The expected context says chain ${expected.chainId}; the child says ${typedData.domain?.chainId}.`
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
const caveats = typedData.message?.caveats ?? [];
|
|
103
|
+
if (caveats.length === 0) {
|
|
104
|
+
refuse("it carries no caveats at all", "An unconstrained delegation is not a payment.");
|
|
105
|
+
}
|
|
106
|
+
const transfer = findCaveat(caveats, CAVEAT_ENFORCERS.erc20TransferAmount);
|
|
107
|
+
if (!transfer) {
|
|
108
|
+
refuse(
|
|
109
|
+
"it has no ERC-20 transfer-amount caveat",
|
|
110
|
+
"Without it the delegation is not bounded to an amount."
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
const token = `0x${slice(transfer.terms, 0, 20)}`;
|
|
114
|
+
if (!same(token, expected.asset)) {
|
|
115
|
+
refuse("it spends a different token", `Expected ${expected.asset}, child pins ${token}.`);
|
|
116
|
+
}
|
|
117
|
+
const amount = BigInt(`0x${slice(transfer.terms, 20, 52)}`);
|
|
118
|
+
if (amount !== BigInt(expected.amount)) {
|
|
119
|
+
refuse(
|
|
120
|
+
"the amount does not match",
|
|
121
|
+
`The expected context says ${expected.amount}; the child allows ${amount.toString()}.`
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
const calldata = findCaveat(caveats, CAVEAT_ENFORCERS.allowedCalldata);
|
|
125
|
+
if (!calldata) {
|
|
126
|
+
refuse(
|
|
127
|
+
"it has no payee pin",
|
|
128
|
+
"Without an allowed-calldata caveat the merchant could redeem it to any address."
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
const startIndex = BigInt(`0x${slice(calldata.terms, 0, 32)}`);
|
|
132
|
+
if (startIndex !== 4n) {
|
|
133
|
+
refuse(
|
|
134
|
+
"the payee pin points at the wrong calldata offset",
|
|
135
|
+
`Expected offset 4 (the transfer's \`to\` word), got ${startIndex.toString()}.`
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
const paddedPayee = slice(calldata.terms, 32, 64);
|
|
139
|
+
if (paddedPayee.slice(0, 24) !== "0".repeat(24)) {
|
|
140
|
+
refuse("the pinned payee is not a plain address", "Its 32-byte word is not a padded address.");
|
|
141
|
+
}
|
|
142
|
+
const payee = `0x${paddedPayee.slice(24)}`;
|
|
143
|
+
if (!same(payee, expected.merchantTo)) {
|
|
144
|
+
refuse(
|
|
145
|
+
"it pays a different address than Haven declared",
|
|
146
|
+
`The expected context says ${expected.merchantTo}; the child pins ${payee}.`
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
const timestamp = findCaveat(caveats, CAVEAT_ENFORCERS.timestamp);
|
|
150
|
+
if (!timestamp) {
|
|
151
|
+
refuse("it never expires", "A settlement child without a timestamp caveat is open-ended.");
|
|
152
|
+
}
|
|
153
|
+
const beforeThreshold = Number(BigInt(`0x${slice(timestamp.terms, 16, 32)}`));
|
|
154
|
+
if (beforeThreshold <= 0) {
|
|
155
|
+
refuse("its expiry is unset", "The timestamp caveat has no upper bound.");
|
|
156
|
+
}
|
|
157
|
+
const nowSec = Math.floor(now / 1e3);
|
|
158
|
+
if (beforeThreshold <= nowSec) {
|
|
159
|
+
refuse("it has already expired", `Expiry ${beforeThreshold} is not in the future.`);
|
|
160
|
+
}
|
|
161
|
+
if (beforeThreshold > nowSec + MAX_SETTLEMENT_WINDOW_SECONDS) {
|
|
162
|
+
refuse(
|
|
163
|
+
"its window is longer than a settlement may live",
|
|
164
|
+
`Expiry is ${beforeThreshold - nowSec}s out; the ceiling is ${MAX_SETTLEMENT_WINDOW_SECONDS}s.`
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
if (expected.expiresAt) {
|
|
168
|
+
const declared = Math.floor(Date.parse(expected.expiresAt) / 1e3);
|
|
169
|
+
if (Number.isFinite(declared) && beforeThreshold > declared) {
|
|
170
|
+
refuse(
|
|
171
|
+
"it outlives the payment window Haven declared",
|
|
172
|
+
`The expected context expires at ${expected.expiresAt}; the child lives to ${beforeThreshold}.`
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
49
177
|
function createEdgeSigner(delegateKey, options = {}) {
|
|
50
178
|
let delegateAddress;
|
|
51
179
|
try {
|
|
@@ -89,6 +217,25 @@ function createEdgeSigner(delegateKey, options = {}) {
|
|
|
89
217
|
"x402 typed data does not match the digest Haven committed to in the expected context. Refusing to sign \u2014 the payload was altered in transit or Haven declared a different one. The most common cause is the typed data being truncated or reshaped while being copied between tool calls (#1255): re-run the hosted quote and pass its typed_data_b64 string through UNCHANGED instead of re-emitting the nested JSON."
|
|
90
218
|
);
|
|
91
219
|
}
|
|
220
|
+
if (isSettlementChildTypedData(typedData)) {
|
|
221
|
+
const settlementChainId = chainIdForNetwork(expected.network);
|
|
222
|
+
if (settlementChainId === void 0) {
|
|
223
|
+
throw new sdk.HavenSigningError(
|
|
224
|
+
`Refusing to sign the x402 settlement child: this signer cannot map the network '${expected.network}' to a chain id, so it cannot check which chain the child is scoped to. Update @haven_ai/signer.`
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
verifySettlementChild(typedData, {
|
|
228
|
+
merchantTo: expected.merchantTo,
|
|
229
|
+
amount: expected.amount,
|
|
230
|
+
asset: expected.asset,
|
|
231
|
+
// From the SIGNED network, not the payload's own claim. Passing
|
|
232
|
+
// Number(typedData.domain.chainId) here made the check compare a
|
|
233
|
+
// value to itself — vacuous, and precisely the "declared one thing,
|
|
234
|
+
// signed another" class this file exists to catch (#1455 review).
|
|
235
|
+
chainId: settlementChainId,
|
|
236
|
+
expiresAt: expected.expiresAt
|
|
237
|
+
});
|
|
238
|
+
}
|
|
92
239
|
const account = accounts.privateKeyToAccount(delegateKey);
|
|
93
240
|
const signature = await account.signTypedData(
|
|
94
241
|
typedData
|
|
@@ -655,6 +802,11 @@ function createToolHandlers(signer, options = {}) {
|
|
|
655
802
|
const result = x402Expected ? await signFundingLeg(signer, x402Expected, payloadHash, typedData) : null;
|
|
656
803
|
if (!result) {
|
|
657
804
|
if (typedData) {
|
|
805
|
+
if (isSettlementChildTypedData(typedData)) {
|
|
806
|
+
throw new sdk.HavenSigningError(
|
|
807
|
+
"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."
|
|
808
|
+
);
|
|
809
|
+
}
|
|
658
810
|
const signature2 = await signer.signDelegationTypedData(typedData);
|
|
659
811
|
await auditSigning("haven_sign", payloadHash);
|
|
660
812
|
return { signature: signature2 };
|
|
@@ -1034,7 +1186,7 @@ async function warnIfCredentialFilePermissive(path, log = (message) => process.s
|
|
|
1034
1186
|
|
|
1035
1187
|
// src/server.ts
|
|
1036
1188
|
var SIGNER_NAME = "@haven_ai/signer";
|
|
1037
|
-
var SIGNER_VERSION = "0.1.
|
|
1189
|
+
var SIGNER_VERSION = "0.1.25-alpha.0";
|
|
1038
1190
|
async function resolveSignerRuntime(options = {}) {
|
|
1039
1191
|
assertSupportedNodeVersion(options.nodeVersion);
|
|
1040
1192
|
if (options.delegateKey) {
|