@tollxyz/guard 0.4.0 → 0.5.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 +465 -381
- package/dist/budget.d.ts +96 -0
- package/dist/budget.js +110 -0
- package/dist/check.d.ts +6 -0
- package/dist/check.js +3 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -0
- package/dist/policy.d.ts +6 -0
- package/dist/policy.js +3 -1
- package/dist/ticket.d.ts +56 -0
- package/dist/ticket.js +90 -0
- package/package.json +70 -69
package/dist/budget.d.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { SpendPolicy } from "./policy.js";
|
|
2
|
+
/** The budget's EIP-712 domain: no verifyingContract (nothing on chain checks it). */
|
|
3
|
+
export declare const BUDGET_DOMAIN: {
|
|
4
|
+
readonly name: "Toll Facilitator Budget";
|
|
5
|
+
readonly version: "1";
|
|
6
|
+
readonly chainId: 4663;
|
|
7
|
+
};
|
|
8
|
+
export declare const BUDGET_TYPES: {
|
|
9
|
+
readonly Budget: readonly [{
|
|
10
|
+
readonly name: "payer";
|
|
11
|
+
readonly type: "address";
|
|
12
|
+
}, {
|
|
13
|
+
readonly name: "perPayment";
|
|
14
|
+
readonly type: "uint256";
|
|
15
|
+
}, {
|
|
16
|
+
readonly name: "perDay";
|
|
17
|
+
readonly type: "uint256";
|
|
18
|
+
}, {
|
|
19
|
+
readonly name: "perPayTo";
|
|
20
|
+
readonly type: "uint256";
|
|
21
|
+
}, {
|
|
22
|
+
readonly name: "payToCaps";
|
|
23
|
+
readonly type: "PayToCap[]";
|
|
24
|
+
}, {
|
|
25
|
+
readonly name: "allowedPayTos";
|
|
26
|
+
readonly type: "address[]";
|
|
27
|
+
}, {
|
|
28
|
+
readonly name: "validUntil";
|
|
29
|
+
readonly type: "uint64";
|
|
30
|
+
}, {
|
|
31
|
+
readonly name: "nonce";
|
|
32
|
+
readonly type: "uint256";
|
|
33
|
+
}];
|
|
34
|
+
readonly PayToCap: readonly [{
|
|
35
|
+
readonly name: "payTo";
|
|
36
|
+
readonly type: "address";
|
|
37
|
+
}, {
|
|
38
|
+
readonly name: "perDay";
|
|
39
|
+
readonly type: "uint256";
|
|
40
|
+
}];
|
|
41
|
+
};
|
|
42
|
+
/** at most this many payToCaps and allowedPayTos */
|
|
43
|
+
export declare const BUDGET_MAX_PAYTOS = 50;
|
|
44
|
+
export interface GuardBudgetOptions {
|
|
45
|
+
/** the agent's paying wallet (it signs the budget) */
|
|
46
|
+
payer: string;
|
|
47
|
+
/** newer wins at the facilitator: use a growing number, e.g. BigInt(Date.now()) */
|
|
48
|
+
nonce: bigint | number | string;
|
|
49
|
+
/** unix seconds the budget ends; 0 or unset = no end */
|
|
50
|
+
validUntil?: bigint | number | string;
|
|
51
|
+
/** the payTo of each perEndpoint prefix (same keys); a perEndpoint key that is itself an address is its own payTo */
|
|
52
|
+
endpointPayTo?: Record<string, string>;
|
|
53
|
+
/** one day cap for every payTo, base units; 0 or unset = not set */
|
|
54
|
+
perPayTo?: bigint;
|
|
55
|
+
/** perPayment when the policy has no assets[USDG].perCall (the facilitator needs one: 0 lets nothing through) */
|
|
56
|
+
perPayment?: bigint;
|
|
57
|
+
/** perDay when the policy has no assets[USDG].perDay */
|
|
58
|
+
perDay?: bigint;
|
|
59
|
+
}
|
|
60
|
+
export interface GuardBudgetMessage {
|
|
61
|
+
payer: string;
|
|
62
|
+
perPayment: bigint;
|
|
63
|
+
perDay: bigint;
|
|
64
|
+
perPayTo: bigint;
|
|
65
|
+
payToCaps: Array<{
|
|
66
|
+
payTo: string;
|
|
67
|
+
perDay: bigint;
|
|
68
|
+
}>;
|
|
69
|
+
allowedPayTos: string[];
|
|
70
|
+
validUntil: bigint;
|
|
71
|
+
nonce: bigint;
|
|
72
|
+
}
|
|
73
|
+
export interface GuardBudget {
|
|
74
|
+
/** the budget (the typed message), for @tollxyz/sdk's setBudget({ budget, signature }) */
|
|
75
|
+
budget: GuardBudgetMessage;
|
|
76
|
+
/** EIP-712 typed data for the payer to sign (viem: account.signTypedData(typedData)) */
|
|
77
|
+
typedData: {
|
|
78
|
+
domain: {
|
|
79
|
+
name: string;
|
|
80
|
+
version: string;
|
|
81
|
+
chainId: number;
|
|
82
|
+
};
|
|
83
|
+
types: typeof BUDGET_TYPES;
|
|
84
|
+
primaryType: "Budget";
|
|
85
|
+
message: GuardBudgetMessage;
|
|
86
|
+
};
|
|
87
|
+
/** guard rules the facilitator budget cannot hold (guard still enforces them in-process) */
|
|
88
|
+
notMirrored: string[];
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* The facilitator budget that mirrors a guard SpendPolicy. Throws when the policy has no USDG per-call or per-day cap
|
|
92
|
+
* and none is passed (the facilitator treats a missing cap as 0, which refuses every payment), or when a list is over 50.
|
|
93
|
+
*/
|
|
94
|
+
export declare function guardBudget(policy: SpendPolicy | {
|
|
95
|
+
spend?: SpendPolicy;
|
|
96
|
+
}, opts: GuardBudgetOptions): GuardBudget;
|
package/dist/budget.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mirror guard's spend caps into a per-payer budget at Toll's x402 facilitator (x402.t0ll.xyz/budget, COMING: the
|
|
3
|
+
* facilitator answers 404 budgets_off until budgets are switched on). guard enforces caps in the agent's process; the
|
|
4
|
+
* facilitator budget enforces them again where the payment settles, so a second process, a leaked key used elsewhere or
|
|
5
|
+
* a bug in the agent cannot spend past them through that facilitator.
|
|
6
|
+
*
|
|
7
|
+
* The words match: guard's assets[USDG].perCall is the budget's perPayment, assets[USDG].perDay its perDay, perEndpoint
|
|
8
|
+
* day caps become payToCaps (the facilitator sees a payTo, not a URL, so each endpoint needs its payTo) and allow.payTo
|
|
9
|
+
* becomes allowedPayTos. Nothing here changes how guard itself behaves. Amounts are USDG base units (6 decimals).
|
|
10
|
+
*/
|
|
11
|
+
import { isAddress } from "./check.js";
|
|
12
|
+
import { USDG } from "./spend.js";
|
|
13
|
+
/** The budget's EIP-712 domain: no verifyingContract (nothing on chain checks it). */
|
|
14
|
+
export const BUDGET_DOMAIN = { name: "Toll Facilitator Budget", version: "1", chainId: 4663 };
|
|
15
|
+
export const BUDGET_TYPES = {
|
|
16
|
+
Budget: [
|
|
17
|
+
{ name: "payer", type: "address" },
|
|
18
|
+
{ name: "perPayment", type: "uint256" },
|
|
19
|
+
{ name: "perDay", type: "uint256" },
|
|
20
|
+
{ name: "perPayTo", type: "uint256" },
|
|
21
|
+
{ name: "payToCaps", type: "PayToCap[]" },
|
|
22
|
+
{ name: "allowedPayTos", type: "address[]" },
|
|
23
|
+
{ name: "validUntil", type: "uint64" },
|
|
24
|
+
{ name: "nonce", type: "uint256" },
|
|
25
|
+
],
|
|
26
|
+
PayToCap: [
|
|
27
|
+
{ name: "payTo", type: "address" },
|
|
28
|
+
{ name: "perDay", type: "uint256" },
|
|
29
|
+
],
|
|
30
|
+
};
|
|
31
|
+
/** at most this many payToCaps and allowedPayTos */
|
|
32
|
+
export const BUDGET_MAX_PAYTOS = 50;
|
|
33
|
+
const MAX_U64 = (1n << 64n) - 1n;
|
|
34
|
+
function uint(v, name, max = (1n << 256n) - 1n) {
|
|
35
|
+
let n = null;
|
|
36
|
+
if (typeof v === "bigint")
|
|
37
|
+
n = v;
|
|
38
|
+
else if (typeof v === "number" && Number.isSafeInteger(v))
|
|
39
|
+
n = BigInt(v);
|
|
40
|
+
else if (typeof v === "string" && /^\d{1,78}$/.test(v.trim()))
|
|
41
|
+
n = BigInt(v.trim());
|
|
42
|
+
if (n === null || n < 0n || n > max)
|
|
43
|
+
throw new Error(`guardBudget: ${name} must be a whole number from 0`);
|
|
44
|
+
return n;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The facilitator budget that mirrors a guard SpendPolicy. Throws when the policy has no USDG per-call or per-day cap
|
|
48
|
+
* and none is passed (the facilitator treats a missing cap as 0, which refuses every payment), or when a list is over 50.
|
|
49
|
+
*/
|
|
50
|
+
export function guardBudget(policy, opts) {
|
|
51
|
+
const sp = (policy && "spend" in policy ? policy.spend : policy) ?? {};
|
|
52
|
+
if (!isAddress(opts.payer))
|
|
53
|
+
throw new Error("guardBudget: payer must be a 0x address");
|
|
54
|
+
const notMirrored = [];
|
|
55
|
+
const assets = sp.assets ?? {};
|
|
56
|
+
const usdgKey = Object.keys(assets).find((k) => k.toLowerCase() === USDG);
|
|
57
|
+
const caps = usdgKey ? assets[usdgKey] : undefined;
|
|
58
|
+
for (const k of Object.keys(assets))
|
|
59
|
+
if (k !== usdgKey)
|
|
60
|
+
notMirrored.push(`assets["${k}"]: the facilitator budget is USDG only`);
|
|
61
|
+
const perPayment = opts.perPayment ?? caps?.perCall;
|
|
62
|
+
const perDay = opts.perDay ?? caps?.perDay;
|
|
63
|
+
if (perPayment === undefined)
|
|
64
|
+
throw new Error("guardBudget: no USDG per-call cap (set spend.assets[USDG].perCall or pass perPayment)");
|
|
65
|
+
if (perDay === undefined)
|
|
66
|
+
throw new Error("guardBudget: no USDG per-day cap (set spend.assets[USDG].perDay or pass perDay)");
|
|
67
|
+
const byPayTo = new Map();
|
|
68
|
+
for (const [prefix, c] of Object.entries(sp.perEndpoint ?? {})) {
|
|
69
|
+
const payTo = opts.endpointPayTo?.[prefix] ?? (isAddress(prefix) ? prefix : undefined);
|
|
70
|
+
if (c.perCall !== undefined)
|
|
71
|
+
notMirrored.push(`perEndpoint["${prefix}"].perCall: the facilitator has no per-payTo per-payment cap (perPayment applies)`);
|
|
72
|
+
if (c.perDay === undefined)
|
|
73
|
+
continue;
|
|
74
|
+
if (!payTo) {
|
|
75
|
+
notMirrored.push(`perEndpoint["${prefix}"].perDay: no payTo known for it (pass endpointPayTo)`);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (!isAddress(payTo))
|
|
79
|
+
throw new Error(`guardBudget: endpointPayTo["${prefix}"] must be a 0x address`);
|
|
80
|
+
const had = byPayTo.get(payTo.toLowerCase());
|
|
81
|
+
// two endpoints paying one wallet: the stricter day cap holds for both
|
|
82
|
+
if (!had || c.perDay < had.perDay)
|
|
83
|
+
byPayTo.set(payTo.toLowerCase(), { payTo, perDay: uint(c.perDay, `perEndpoint["${prefix}"].perDay`) });
|
|
84
|
+
}
|
|
85
|
+
const payToCaps = [...byPayTo.values()];
|
|
86
|
+
const allowed = [];
|
|
87
|
+
for (const a of sp.allow?.payTo ?? []) {
|
|
88
|
+
if (!isAddress(a))
|
|
89
|
+
throw new Error(`guardBudget: allow.payTo entry "${a}" is not an address`);
|
|
90
|
+
if (!allowed.some((x) => x.toLowerCase() === a.toLowerCase()))
|
|
91
|
+
allowed.push(a);
|
|
92
|
+
}
|
|
93
|
+
if (payToCaps.length > BUDGET_MAX_PAYTOS || allowed.length > BUDGET_MAX_PAYTOS)
|
|
94
|
+
throw new Error(`guardBudget: at most ${BUDGET_MAX_PAYTOS} payToCaps and allowed payTos`);
|
|
95
|
+
if (sp.allow?.to?.length || sp.allow?.spender?.length)
|
|
96
|
+
notMirrored.push("allow.to / allow.spender: the facilitator only sees x402 payments (payTo)");
|
|
97
|
+
if (sp.approveAbove || sp.approve)
|
|
98
|
+
notMirrored.push("approveAbove / approve: human approval stays in guard");
|
|
99
|
+
const budget = {
|
|
100
|
+
payer: opts.payer,
|
|
101
|
+
perPayment: uint(perPayment, "perPayment"),
|
|
102
|
+
perDay: uint(perDay, "perDay"),
|
|
103
|
+
perPayTo: uint(opts.perPayTo ?? 0n, "perPayTo"),
|
|
104
|
+
payToCaps,
|
|
105
|
+
allowedPayTos: allowed,
|
|
106
|
+
validUntil: uint(opts.validUntil ?? 0, "validUntil", MAX_U64),
|
|
107
|
+
nonce: uint(opts.nonce, "nonce"),
|
|
108
|
+
};
|
|
109
|
+
return { budget, typedData: { domain: { ...BUDGET_DOMAIN }, types: BUDGET_TYPES, primaryType: "Budget", message: budget }, notMirrored };
|
|
110
|
+
}
|
package/dist/check.d.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* The one call to Toll: POST https://t0ll.xyz/api/tx/check. Every check runs on Toll's server, so a new check reaches
|
|
3
3
|
* every agent without a package update. This file only sends the request and reads the answer.
|
|
4
4
|
*/
|
|
5
|
+
import { type TicketAsk } from "./ticket.js";
|
|
5
6
|
export declare const DEFAULT_BASE_URL = "https://t0ll.xyz";
|
|
6
7
|
/** Robinhood Chain: the only chain the pre-sign check reads for now */
|
|
7
8
|
export declare const TX_CHAIN_ID = 4663;
|
|
@@ -117,6 +118,11 @@ export interface CheckOptions {
|
|
|
117
118
|
maxRetryWaitMs?: number;
|
|
118
119
|
/** for tests */
|
|
119
120
|
sleep?: (ms: number) => Promise<void>;
|
|
121
|
+
/**
|
|
122
|
+
* 0.4.1: ask Toll for a single-use ALLOW ticket for this exact call (`true`, or {consumer?, scope?}). It comes back
|
|
123
|
+
* in x-toll.ticket (and on the GuardReport): signed, or with why none was issued. A BLOCK is never signed.
|
|
124
|
+
*/
|
|
125
|
+
ticket?: boolean | TicketAsk;
|
|
120
126
|
}
|
|
121
127
|
export declare class TollBlockedError extends Error {
|
|
122
128
|
readonly assessment: RiskAssessment;
|
package/dist/check.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* The one call to Toll: POST https://t0ll.xyz/api/tx/check. Every check runs on Toll's server, so a new check reaches
|
|
3
3
|
* every agent without a package update. This file only sends the request and reads the answer.
|
|
4
4
|
*/
|
|
5
|
+
import { ticketAskOf } from "./ticket.js";
|
|
5
6
|
export const DEFAULT_BASE_URL = "https://t0ll.xyz";
|
|
6
7
|
/** Robinhood Chain: the only chain the pre-sign check reads for now */
|
|
7
8
|
export const TX_CHAIN_ID = 4663;
|
|
@@ -183,7 +184,8 @@ export async function checkTx(input, opts = {}) {
|
|
|
183
184
|
const base = trimBase(opts.baseUrl ?? DEFAULT_BASE_URL);
|
|
184
185
|
const url = `${base}/api/tx/check`;
|
|
185
186
|
const paid = `${base}${PAID_TX_PATH}`;
|
|
186
|
-
const
|
|
187
|
+
const ask = ticketAskOf(opts.ticket);
|
|
188
|
+
const body = toJson(ask ? { ...bodyOf(input), ticket: ask } : bodyOf(input));
|
|
187
189
|
const first = await once(url, body, opts, f, paid);
|
|
188
190
|
if (first.retryS === null || opts.retry === false)
|
|
189
191
|
return first.a;
|
package/dist/index.d.ts
CHANGED
|
@@ -12,3 +12,7 @@ export { spendOf, NATIVE, USDG, WETH, DEFAULT_SPEND_ASSETS, UNLIMITED } from "./
|
|
|
12
12
|
export type { Spend, SpendItem } from "./spend.js";
|
|
13
13
|
export { MemorySpendStore, dayStart } from "./store.js";
|
|
14
14
|
export type { SpendStore } from "./store.js";
|
|
15
|
+
export { appendTicketSuffix, encodeTicketAndSig, ticketAskOf, ticketOfAnswer, TICKET_SUFFIX_MAGIC, TOLL_ALLOW_SIGNER, ALLOW_DOCS } from "./ticket.js";
|
|
16
|
+
export type { TicketAsk, AllowTicket, TicketResult } from "./ticket.js";
|
|
17
|
+
export { guardBudget, BUDGET_DOMAIN, BUDGET_TYPES, BUDGET_MAX_PAYTOS } from "./budget.js";
|
|
18
|
+
export type { GuardBudget, GuardBudgetOptions, GuardBudgetMessage } from "./budget.js";
|
package/dist/index.js
CHANGED
|
@@ -5,3 +5,5 @@ export { guardX402Fetch, readPaymentRequired, readPaymentForms, paymentFormsDiff
|
|
|
5
5
|
export { read8470, latestFinding8470, decodeGetObservation, decodeLatestFinding, encodeGetObservation, encodeLatestByToken, encodeLatestFinding, FINDING_KINDS, NOT_READ, OBS_REGISTRY, OBS_DETECTOR, OBS_CHAIN_ID, RH_RPC, } from "./erc8470.js";
|
|
6
6
|
export { spendOf, NATIVE, USDG, WETH, DEFAULT_SPEND_ASSETS, UNLIMITED } from "./spend.js";
|
|
7
7
|
export { MemorySpendStore, dayStart } from "./store.js";
|
|
8
|
+
export { appendTicketSuffix, encodeTicketAndSig, ticketAskOf, ticketOfAnswer, TICKET_SUFFIX_MAGIC, TOLL_ALLOW_SIGNER, ALLOW_DOCS } from "./ticket.js";
|
|
9
|
+
export { guardBudget, BUDGET_DOMAIN, BUDGET_TYPES, BUDGET_MAX_PAYTOS } from "./budget.js";
|
package/dist/policy.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { TollBlockedError, TollUnavailableError, type CheckInput, type CheckOptions, type RiskAssessment } from "./check.js";
|
|
2
|
+
import { type TicketResult } from "./ticket.js";
|
|
2
3
|
import { type Spend } from "./spend.js";
|
|
3
4
|
import { type SpendStore } from "./store.js";
|
|
4
5
|
export type WarnPolicy = "allow" | "block" | "block-high" | ((assessment: RiskAssessment) => boolean | Promise<boolean>);
|
|
@@ -29,6 +30,11 @@ export interface GuardReport {
|
|
|
29
30
|
approvalRef?: string;
|
|
30
31
|
needsApproval?: boolean;
|
|
31
32
|
};
|
|
33
|
+
/**
|
|
34
|
+
* 0.4.1, with the `ticket` option: Toll's single-use ALLOW ticket for this exact call ({ticket, signature, signer}),
|
|
35
|
+
* or {why} none was issued. Put it on the call with appendTicketSuffix (routers) or pass abi.encode(ticket, signature).
|
|
36
|
+
*/
|
|
37
|
+
ticket?: TicketResult;
|
|
32
38
|
}
|
|
33
39
|
export type PolicyCode = "not-allowlisted" | "cap-per-call" | "cap-per-day" | "cap-endpoint" | "approval-denied" | "duplicate-payment" | "unknown-spend";
|
|
34
40
|
export interface Caps {
|
package/dist/policy.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { TX_CHAIN_ID, TollBlockedError, TollUnavailableError, checkTx, chainIdOf, paidUrlOf, bodyOf, toJson, } from "./check.js";
|
|
2
|
+
import { ticketOfAnswer } from "./ticket.js";
|
|
2
3
|
import { DEFAULT_SPEND_ASSETS, spendOf } from "./spend.js";
|
|
3
4
|
import { MemorySpendStore, dayStart } from "./store.js";
|
|
4
5
|
/** blocked by your own spend rules; a TollBlockedError, so existing catch blocks still see a block */
|
|
@@ -155,7 +156,8 @@ async function enforceVerdict(kind, build, o) {
|
|
|
155
156
|
}
|
|
156
157
|
const paidUrl = paidUrlOf(a);
|
|
157
158
|
const done = async (decision, basis) => {
|
|
158
|
-
const
|
|
159
|
+
const tk = ticketOfAnswer(a);
|
|
160
|
+
const r = { kind, input, assessment: a, decision, basis, paidUrl, ...(tk ? { ticket: tk } : {}) };
|
|
159
161
|
await report(o, r);
|
|
160
162
|
return r;
|
|
161
163
|
};
|
package/dist/ticket.d.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 0.4.1: Toll's single-use ALLOW ticket. Ask for one with the `ticket` option; Toll signs a compact EIP-712 verdict
|
|
3
|
+
* bound to one exact call, one account, an optional consumer contract and a 60 second window. A contract built on
|
|
4
|
+
* toll-allow (https://github.com/t0ll-xyz/toll-allow) accepts the call only with a valid, unused ticket.
|
|
5
|
+
* A BLOCK is never signed. No ticket without `from`, or for a transaction with an EIP-7702 authorization list.
|
|
6
|
+
*
|
|
7
|
+
* appendTicketSuffix MUST match toll-allow's TollAllow.encodeSuffix / splitSuffix byte for byte:
|
|
8
|
+
* body ‖ abi.encode((uint8,uint8,address,address,bytes32,uint64,uint64,bytes32,bytes32), bytes) ‖ uint32 len ‖ 0x900bc3e1
|
|
9
|
+
* where len is the byte length of the abi.encode part and 0x900bc3e1 = bytes4(keccak256("TollAllow.v1")).
|
|
10
|
+
*/
|
|
11
|
+
import type { Hex } from "./check.js";
|
|
12
|
+
/** what to ask Toll for: `true` = scope "call", any consumer */
|
|
13
|
+
export interface TicketAsk {
|
|
14
|
+
/** the contract that will check the ticket (its `consumer` field); unset = any contract */
|
|
15
|
+
consumer?: string;
|
|
16
|
+
/** "call" (default): bound to (target, value, keccak256(data)); "v4swap": bound to the swap a HookrRouter call makes */
|
|
17
|
+
scope?: "call" | "v4swap";
|
|
18
|
+
}
|
|
19
|
+
/** the ticket as Toll returns it (x-toll.ticket.ticket); uint64 times are unix seconds */
|
|
20
|
+
export interface AllowTicket {
|
|
21
|
+
scope: number;
|
|
22
|
+
verdict: number;
|
|
23
|
+
account: string;
|
|
24
|
+
consumer: string;
|
|
25
|
+
subject: string;
|
|
26
|
+
validAfter: number | bigint | string;
|
|
27
|
+
validUntil: number | bigint | string;
|
|
28
|
+
nonce: string;
|
|
29
|
+
reportHash: string;
|
|
30
|
+
}
|
|
31
|
+
/** x-toll.ticket: a signed ticket, or why none was issued */
|
|
32
|
+
export type TicketResult = {
|
|
33
|
+
ticket: AllowTicket;
|
|
34
|
+
signature: Hex;
|
|
35
|
+
signer: string;
|
|
36
|
+
} | {
|
|
37
|
+
why: string;
|
|
38
|
+
};
|
|
39
|
+
/** bytes4(keccak256("TollAllow.v1")) */
|
|
40
|
+
export declare const TICKET_SUFFIX_MAGIC = "0x900bc3e1";
|
|
41
|
+
/** Toll's production ALLOW signer on Robinhood Chain (4663): pin it, or a TollSignerSet you control that includes it */
|
|
42
|
+
export declare const TOLL_ALLOW_SIGNER = "0x4A2C93066d94F1E69f3EAd5438c4efDdf80a46ba";
|
|
43
|
+
export declare const ALLOW_DOCS = "https://t0ll.xyz/agents/allow";
|
|
44
|
+
/** the ask as the route reads it (`ticket: {consumer?, scope?}`), or null for none */
|
|
45
|
+
export declare function ticketAskOf(t: boolean | TicketAsk | undefined | null): TicketAsk | null;
|
|
46
|
+
/** the ticket in an answer, when the answer carries one (signed or with its why) */
|
|
47
|
+
export declare function ticketOfAnswer(a: {
|
|
48
|
+
"x-toll"?: Record<string, unknown>;
|
|
49
|
+
} | null | undefined): TicketResult | undefined;
|
|
50
|
+
/** abi.encode(Ticket, bytes) as hex without 0x: the static tuple inline (9 words), then the offset and the bytes */
|
|
51
|
+
export declare function encodeTicketAndSig(t: AllowTicket, signature: string): string;
|
|
52
|
+
/**
|
|
53
|
+
* The calldata a ticket-checking router reads: `data ‖ abi.encode(ticket, signature) ‖ uint32 len ‖ 0x900bc3e1`.
|
|
54
|
+
* The ticket's subject is over `data` (the body) only. Identical to TollAllow.encodeSuffix in toll-allow.
|
|
55
|
+
*/
|
|
56
|
+
export declare function appendTicketSuffix(data: Hex, ticket: AllowTicket, signature: Hex): Hex;
|
package/dist/ticket.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/** bytes4(keccak256("TollAllow.v1")) */
|
|
2
|
+
export const TICKET_SUFFIX_MAGIC = "0x900bc3e1";
|
|
3
|
+
/** Toll's production ALLOW signer on Robinhood Chain (4663): pin it, or a TollSignerSet you control that includes it */
|
|
4
|
+
export const TOLL_ALLOW_SIGNER = "0x4A2C93066d94F1E69f3EAd5438c4efDdf80a46ba";
|
|
5
|
+
export const ALLOW_DOCS = "https://t0ll.xyz/agents/allow";
|
|
6
|
+
/** the ask as the route reads it (`ticket: {consumer?, scope?}`), or null for none */
|
|
7
|
+
export function ticketAskOf(t) {
|
|
8
|
+
if (t === undefined || t === null || t === false)
|
|
9
|
+
return null;
|
|
10
|
+
if (t === true)
|
|
11
|
+
return {};
|
|
12
|
+
const o = {};
|
|
13
|
+
if (t.consumer !== undefined)
|
|
14
|
+
o.consumer = t.consumer;
|
|
15
|
+
if (t.scope !== undefined)
|
|
16
|
+
o.scope = t.scope;
|
|
17
|
+
return o;
|
|
18
|
+
}
|
|
19
|
+
/** the ticket in an answer, when the answer carries one (signed or with its why) */
|
|
20
|
+
export function ticketOfAnswer(a) {
|
|
21
|
+
const t = a?.["x-toll"]?.ticket;
|
|
22
|
+
if (!t || typeof t !== "object")
|
|
23
|
+
return undefined;
|
|
24
|
+
if (typeof t.why === "string")
|
|
25
|
+
return { why: t.why };
|
|
26
|
+
if (t.ticket && typeof t.ticket === "object" && typeof t.signature === "string" && typeof t.signer === "string") {
|
|
27
|
+
return { ticket: t.ticket, signature: t.signature, signer: t.signer };
|
|
28
|
+
}
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
/* ------------------------------------------------------------------ ABI encoding (no dependencies) */
|
|
32
|
+
const hexBody = (h, what) => {
|
|
33
|
+
if (typeof h !== "string" || !/^0x([0-9a-fA-F]{2})*$/.test(h))
|
|
34
|
+
throw new TypeError(`${what} must be 0x hex with whole bytes`);
|
|
35
|
+
return h.slice(2).toLowerCase();
|
|
36
|
+
};
|
|
37
|
+
const word = (n, bits, what) => {
|
|
38
|
+
if (n < 0n || n >= 1n << BigInt(bits))
|
|
39
|
+
throw new RangeError(`${what} does not fit in uint${bits}`);
|
|
40
|
+
return n.toString(16).padStart(64, "0");
|
|
41
|
+
};
|
|
42
|
+
const uint = (v, bits, what) => {
|
|
43
|
+
let n;
|
|
44
|
+
try {
|
|
45
|
+
n = BigInt(v);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
throw new TypeError(`${what} must be a whole number`);
|
|
49
|
+
}
|
|
50
|
+
return word(n, bits, what);
|
|
51
|
+
};
|
|
52
|
+
const addr = (a, what) => {
|
|
53
|
+
if (typeof a !== "string" || !/^0x[0-9a-fA-F]{40}$/.test(a))
|
|
54
|
+
throw new TypeError(`${what} must be an address`);
|
|
55
|
+
return a.slice(2).toLowerCase().padStart(64, "0");
|
|
56
|
+
};
|
|
57
|
+
const b32 = (h, what) => {
|
|
58
|
+
if (typeof h !== "string" || !/^0x[0-9a-fA-F]{64}$/.test(h))
|
|
59
|
+
throw new TypeError(`${what} must be 32 bytes`);
|
|
60
|
+
return h.slice(2).toLowerCase();
|
|
61
|
+
};
|
|
62
|
+
/** abi.encode(Ticket, bytes) as hex without 0x: the static tuple inline (9 words), then the offset and the bytes */
|
|
63
|
+
export function encodeTicketAndSig(t, signature) {
|
|
64
|
+
const sig = hexBody(signature, "signature");
|
|
65
|
+
const head = uint(t.scope, 8, "ticket.scope") +
|
|
66
|
+
uint(t.verdict, 8, "ticket.verdict") +
|
|
67
|
+
addr(t.account, "ticket.account") +
|
|
68
|
+
addr(t.consumer, "ticket.consumer") +
|
|
69
|
+
b32(t.subject, "ticket.subject") +
|
|
70
|
+
uint(t.validAfter, 64, "ticket.validAfter") +
|
|
71
|
+
uint(t.validUntil, 64, "ticket.validUntil") +
|
|
72
|
+
b32(t.nonce, "ticket.nonce") +
|
|
73
|
+
b32(t.reportHash, "ticket.reportHash");
|
|
74
|
+
const offset = word(10n * 32n, 256, "offset"); // 9 tuple words + this offset word
|
|
75
|
+
const len = word(BigInt(sig.length / 2), 256, "signature length");
|
|
76
|
+
const padded = sig + "0".repeat((64 - (sig.length % 64)) % 64);
|
|
77
|
+
return head + offset + len + padded;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The calldata a ticket-checking router reads: `data ‖ abi.encode(ticket, signature) ‖ uint32 len ‖ 0x900bc3e1`.
|
|
81
|
+
* The ticket's subject is over `data` (the body) only. Identical to TollAllow.encodeSuffix in toll-allow.
|
|
82
|
+
*/
|
|
83
|
+
export function appendTicketSuffix(data, ticket, signature) {
|
|
84
|
+
const body = hexBody(data, "data");
|
|
85
|
+
const enc = encodeTicketAndSig(ticket, signature);
|
|
86
|
+
const n = enc.length / 2;
|
|
87
|
+
if (n >= 2 ** 32)
|
|
88
|
+
throw new RangeError("ticket encoding too long");
|
|
89
|
+
return `0x${body}${enc}${n.toString(16).padStart(8, "0")}${TICKET_SUFFIX_MAGIC.slice(2)}`;
|
|
90
|
+
}
|
package/package.json
CHANGED
|
@@ -1,69 +1,70 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@tollxyz/guard",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Toll's live pre-sign check in front of an AI agent's wallet: viem, viem smart-account, ethers and x402 fetch wrappers that ask Toll (ALLOW / WARN / BLOCK, ERC-8265) before anything is signed on Robinhood Chain: transactions, messages, typed data, batches, user operations, EIP-7702 authorizations. Spend caps, allowlists and human approval around the verdict. Zero runtime dependencies.",
|
|
5
|
-
"license": "MIT",
|
|
6
|
-
"author": "Toll",
|
|
7
|
-
"type": "module",
|
|
8
|
-
"main": "./dist/index.js",
|
|
9
|
-
"types": "./dist/index.d.ts",
|
|
10
|
-
"exports": {
|
|
11
|
-
".": {
|
|
12
|
-
"types": "./dist/index.d.ts",
|
|
13
|
-
"import": "./dist/index.js"
|
|
14
|
-
}
|
|
15
|
-
},
|
|
16
|
-
"files": [
|
|
17
|
-
"dist",
|
|
18
|
-
"README.md"
|
|
19
|
-
],
|
|
20
|
-
"engines": {
|
|
21
|
-
"node": ">=20.19"
|
|
22
|
-
},
|
|
23
|
-
"publishConfig": {
|
|
24
|
-
"access": "public"
|
|
25
|
-
},
|
|
26
|
-
"repository": {
|
|
27
|
-
"type": "git",
|
|
28
|
-
"url": "git+https://github.com/t0ll-xyz/toll-plugins.git",
|
|
29
|
-
"directory": "packages/guard"
|
|
30
|
-
},
|
|
31
|
-
"homepage": "https://t0ll.xyz/agents/plugins",
|
|
32
|
-
"keywords": [
|
|
33
|
-
"agent-wallet",
|
|
34
|
-
"transaction-guard",
|
|
35
|
-
"pre-sign",
|
|
36
|
-
"x402",
|
|
37
|
-
"erc-8004",
|
|
38
|
-
"erc-8470",
|
|
39
|
-
"erc-8265",
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
"
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
"
|
|
67
|
-
"
|
|
68
|
-
|
|
69
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@tollxyz/guard",
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "Toll's live pre-sign check in front of an AI agent's wallet: viem, viem smart-account, ethers and x402 fetch wrappers that ask Toll (ALLOW / WARN / BLOCK, ERC-8265) before anything is signed on Robinhood Chain: transactions, messages, typed data, batches, user operations, EIP-7702 authorizations. Spend caps, allowlists and human approval around the verdict; single-use ALLOW tickets for contracts that require one. Zero runtime dependencies.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Toll",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"import": "./dist/index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"README.md"
|
|
19
|
+
],
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=20.19"
|
|
22
|
+
},
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public"
|
|
25
|
+
},
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+https://github.com/t0ll-xyz/toll-plugins.git",
|
|
29
|
+
"directory": "packages/guard"
|
|
30
|
+
},
|
|
31
|
+
"homepage": "https://t0ll.xyz/agents/plugins",
|
|
32
|
+
"keywords": [
|
|
33
|
+
"agent-wallet",
|
|
34
|
+
"transaction-guard",
|
|
35
|
+
"pre-sign",
|
|
36
|
+
"x402",
|
|
37
|
+
"erc-8004",
|
|
38
|
+
"erc-8470",
|
|
39
|
+
"erc-8265",
|
|
40
|
+
"allow-ticket",
|
|
41
|
+
"robinhood-chain",
|
|
42
|
+
"viem",
|
|
43
|
+
"ethers",
|
|
44
|
+
"ai-agent",
|
|
45
|
+
"toll"
|
|
46
|
+
],
|
|
47
|
+
"scripts": {
|
|
48
|
+
"build": "tsc -p tsconfig.json",
|
|
49
|
+
"test": "tsc -p tsconfig.json --noEmit && tsx --test tests/*.test.ts",
|
|
50
|
+
"smoke": "node scripts/smoke.mjs",
|
|
51
|
+
"prepublishOnly": "npm run build"
|
|
52
|
+
},
|
|
53
|
+
"peerDependencies": {
|
|
54
|
+
"ethers": "^6.0.0",
|
|
55
|
+
"viem": "^2.0.0"
|
|
56
|
+
},
|
|
57
|
+
"peerDependenciesMeta": {
|
|
58
|
+
"ethers": {
|
|
59
|
+
"optional": true
|
|
60
|
+
},
|
|
61
|
+
"viem": {
|
|
62
|
+
"optional": true
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
"devDependencies": {
|
|
66
|
+
"@types/node": "^22.0.0",
|
|
67
|
+
"tsx": "^4.19.0",
|
|
68
|
+
"typescript": "^5.6.0"
|
|
69
|
+
}
|
|
70
|
+
}
|