@goassay-ai/sdk 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +252 -2
- package/dist/client.d.ts +88 -0
- package/dist/client.js +158 -0
- package/dist/core/binding.d.ts +56 -0
- package/dist/core/binding.js +132 -0
- package/dist/core/caip.d.ts +15 -0
- package/dist/core/caip.js +31 -0
- package/dist/core/chain.d.ts +37 -0
- package/dist/core/chain.js +148 -0
- package/dist/core/clock.d.ts +12 -0
- package/dist/core/clock.js +25 -0
- package/dist/core/errors.d.ts +9 -0
- package/dist/core/errors.js +13 -0
- package/dist/core/guard-types.d.ts +174 -0
- package/dist/core/guard-types.js +69 -0
- package/dist/core/guards.d.ts +54 -0
- package/dist/core/guards.js +408 -0
- package/dist/core/hex.d.ts +14 -0
- package/dist/core/hex.js +42 -0
- package/dist/core/index.d.ts +16 -0
- package/dist/core/index.js +16 -0
- package/dist/core/meta.d.ts +38 -0
- package/dist/core/meta.js +79 -0
- package/dist/core/voucher.d.ts +116 -0
- package/dist/core/voucher.js +137 -0
- package/dist/errors.d.ts +149 -0
- package/dist/errors.js +205 -0
- package/dist/http.d.ts +52 -0
- package/dist/http.js +139 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +4 -0
- package/dist/mcp-url.d.ts +17 -0
- package/dist/mcp-url.js +98 -0
- package/dist/mcp.d.ts +23 -0
- package/dist/mcp.js +121 -0
- package/dist/networks.d.ts +5 -0
- package/dist/networks.js +46 -0
- package/dist/resources/context.d.ts +48 -0
- package/dist/resources/context.js +28 -0
- package/dist/resources/paid-calls.d.ts +82 -0
- package/dist/resources/paid-calls.js +156 -0
- package/dist/resources/receipts.d.ts +25 -0
- package/dist/resources/receipts.js +21 -0
- package/dist/resources/tools.d.ts +96 -0
- package/dist/resources/tools.js +241 -0
- package/dist/rest.d.ts +17 -0
- package/dist/rest.js +43 -0
- package/dist/schemes/eip155/bind.d.ts +17 -0
- package/dist/schemes/eip155/bind.js +59 -0
- package/dist/schemes/eip155/index.d.ts +71 -0
- package/dist/schemes/eip155/index.js +264 -0
- package/dist/schemes/types.d.ts +54 -0
- package/dist/schemes/types.js +1 -0
- package/dist/signer.d.ts +39 -0
- package/dist/signer.js +29 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +2 -0
- package/package.json +67 -4
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { Confirmation, GuardContext, GuardId, GuardResult } from "./guard-types.js";
|
|
2
|
+
/** What the SDK tells its caller to do in the G4 payer-changed reason (`{run}` in the vectors). */
|
|
3
|
+
export declare const RUN = "paidCalls.bind() and open";
|
|
4
|
+
/**
|
|
5
|
+
* The local guards (spec section 9): the checks that run before the client signs a voucher. A
|
|
6
|
+
* voucher is signed only when `checkAll` returns `{ok: true}`. Ported unchanged from `assay-mcp`;
|
|
7
|
+
* `vectors/eip155/guards.json` and `seq.json` pin the order, codes and boundaries.
|
|
8
|
+
*
|
|
9
|
+
* - Pure: every input is in the context; no I/O, no clock, no state.
|
|
10
|
+
* - Fail closed: a missing, malformed or unexpected value is a refusal, never a pass, and the
|
|
11
|
+
* function never throws.
|
|
12
|
+
* - G11 (a per-URL paid list) is app-only: the SDK has no such list, so it never runs here and
|
|
13
|
+
* paid calls go through whatever MCP URL the client was built for (spec section 9).
|
|
14
|
+
* - The guards run in the order G1..G12 and the first refusal is returned, so every
|
|
15
|
+
* implementation reports the same guard and code for the same context (`vectors/guards.json`).
|
|
16
|
+
* The G7 confirmation (`confirmAbove`) runs last of all: the user is asked only about a payment
|
|
17
|
+
* every other guard has already accepted.
|
|
18
|
+
* - A reason never carries an address, an amount or a URL from the server.
|
|
19
|
+
*/
|
|
20
|
+
export declare const GUARD_ORDER: readonly GuardId[];
|
|
21
|
+
/**
|
|
22
|
+
* G1: the oldest forwarded call a requirement may answer. A call may take up to the 120 s request
|
|
23
|
+
* timeout (§7.6) before its `PAYMENT_REQUIRED` arrives.
|
|
24
|
+
*/
|
|
25
|
+
export declare const FORWARDED_CALL_MAX_AGE_SECONDS = 120;
|
|
26
|
+
/** G4: the chain is read before the first voucher and at most every 10 minutes (§7.8). */
|
|
27
|
+
export declare const CHAIN_READ_MAX_AGE_SECONDS = 600;
|
|
28
|
+
/** G4: how far in the future a read may be dated: the clock-skew clamp (§7.7, T20). */
|
|
29
|
+
export declare const CHAIN_READ_MAX_FUTURE_SECONDS = 120;
|
|
30
|
+
/** G7: the rolling window of `perDay`. */
|
|
31
|
+
export declare const SPEND_WINDOW_SECONDS = 86400;
|
|
32
|
+
/** G9: a `seq` at or above this would let one voucher void every later one (T17). */
|
|
33
|
+
export declare const MAX_SEQ_EXCLUSIVE: bigint;
|
|
34
|
+
/** G10 bounds. */
|
|
35
|
+
export declare const MAX_RESOURCE_LENGTH = 2048;
|
|
36
|
+
export declare const MAX_TIMEOUT_SECONDS = 300;
|
|
37
|
+
/** G12: the only resource a voucher may pay for is the forwarded tool's. */
|
|
38
|
+
export declare const TOOL_RESOURCE_PREFIX = "/mcp/tools/";
|
|
39
|
+
/** G4: the text the user sees when the server names a payer other than the recorded one. */
|
|
40
|
+
export declare const PAYER_CHANGED_REASON = "payer changed; run paidCalls.bind() and open enable-paid";
|
|
41
|
+
/**
|
|
42
|
+
* Dollars (a plain decimal string such as "0.50", "5" or ".5") as atomic units of an asset with
|
|
43
|
+
* `decimals` places, in exact integer arithmetic. `null` for anything else, including more places
|
|
44
|
+
* than the asset has.
|
|
45
|
+
*/
|
|
46
|
+
export declare function usdToAtomic(value: string, decimals: number): bigint | null;
|
|
47
|
+
/**
|
|
48
|
+
* What the user must approve when `confirmAbove` applies: the values shown in the prompt. The
|
|
49
|
+
* caller shows these, and passes the same object back as `confirmation` once the user approves.
|
|
50
|
+
* `null` when the context has no readable requirement or forwarded call.
|
|
51
|
+
*/
|
|
52
|
+
export declare function confirmationFor(ctx: Pick<GuardContext, "requirement" | "forwardedCall">): Confirmation | null;
|
|
53
|
+
/** Runs G1..G12 in order (G11 is app-only), then the G7 confirmation. `{ok: true}` only when all pass; never throws. */
|
|
54
|
+
export declare function checkAll(ctx: GuardContext): GuardResult;
|
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
import { parseCaip10 } from "./caip.js";
|
|
2
|
+
import { MAX_CLOCK_CORRECTION_SECONDS } from "./clock.js";
|
|
3
|
+
import { getAddress, isAddress } from "./hex.js";
|
|
4
|
+
/** What the SDK tells its caller to do in the G4 payer-changed reason (`{run}` in the vectors). */
|
|
5
|
+
export const RUN = "paidCalls.bind() and open";
|
|
6
|
+
/**
|
|
7
|
+
* The local guards (spec section 9): the checks that run before the client signs a voucher. A
|
|
8
|
+
* voucher is signed only when `checkAll` returns `{ok: true}`. Ported unchanged from `assay-mcp`;
|
|
9
|
+
* `vectors/eip155/guards.json` and `seq.json` pin the order, codes and boundaries.
|
|
10
|
+
*
|
|
11
|
+
* - Pure: every input is in the context; no I/O, no clock, no state.
|
|
12
|
+
* - Fail closed: a missing, malformed or unexpected value is a refusal, never a pass, and the
|
|
13
|
+
* function never throws.
|
|
14
|
+
* - G11 (a per-URL paid list) is app-only: the SDK has no such list, so it never runs here and
|
|
15
|
+
* paid calls go through whatever MCP URL the client was built for (spec section 9).
|
|
16
|
+
* - The guards run in the order G1..G12 and the first refusal is returned, so every
|
|
17
|
+
* implementation reports the same guard and code for the same context (`vectors/guards.json`).
|
|
18
|
+
* The G7 confirmation (`confirmAbove`) runs last of all: the user is asked only about a payment
|
|
19
|
+
* every other guard has already accepted.
|
|
20
|
+
* - A reason never carries an address, an amount or a URL from the server.
|
|
21
|
+
*/
|
|
22
|
+
export const GUARD_ORDER = ["G1", "G2", "G3", "G4", "G5", "G6", "G7", "G8", "G9", "G10", "G12"];
|
|
23
|
+
/**
|
|
24
|
+
* G1: the oldest forwarded call a requirement may answer. A call may take up to the 120 s request
|
|
25
|
+
* timeout (§7.6) before its `PAYMENT_REQUIRED` arrives.
|
|
26
|
+
*/
|
|
27
|
+
export const FORWARDED_CALL_MAX_AGE_SECONDS = 120;
|
|
28
|
+
/** G4: the chain is read before the first voucher and at most every 10 minutes (§7.8). */
|
|
29
|
+
export const CHAIN_READ_MAX_AGE_SECONDS = 600;
|
|
30
|
+
/** G4: how far in the future a read may be dated: the clock-skew clamp (§7.7, T20). */
|
|
31
|
+
export const CHAIN_READ_MAX_FUTURE_SECONDS = MAX_CLOCK_CORRECTION_SECONDS;
|
|
32
|
+
/** G7: the rolling window of `perDay`. */
|
|
33
|
+
export const SPEND_WINDOW_SECONDS = 86_400;
|
|
34
|
+
/** G9: a `seq` at or above this would let one voucher void every later one (T17). */
|
|
35
|
+
export const MAX_SEQ_EXCLUSIVE = 2n ** 53n;
|
|
36
|
+
/** G10 bounds. */
|
|
37
|
+
export const MAX_RESOURCE_LENGTH = 2048;
|
|
38
|
+
export const MAX_TIMEOUT_SECONDS = 300;
|
|
39
|
+
/** G12: the only resource a voucher may pay for is the forwarded tool's. */
|
|
40
|
+
export const TOOL_RESOURCE_PREFIX = "/mcp/tools/";
|
|
41
|
+
/** G4: the text the user sees when the server names a payer other than the recorded one. */
|
|
42
|
+
export const PAYER_CHANGED_REASON = `payer changed; run ${RUN} enable-paid`;
|
|
43
|
+
const MAX_UINT64 = 2n ** 64n - 1n;
|
|
44
|
+
const CANONICAL_UINT = /^(0|[1-9][0-9]*)$/;
|
|
45
|
+
const CAIP2_EIP155 = /^eip155:([1-9][0-9]*)$/;
|
|
46
|
+
const CAIP19_ERC20_ANY_CASE = /^eip155:([1-9][0-9]*)\/erc20:(0x[0-9a-fA-F]{40})$/;
|
|
47
|
+
const BARE_ADDRESS = /^0x[0-9a-fA-F]{40}$/;
|
|
48
|
+
const LOCAL_ADDRESS = /^(?:eip155:([1-9][0-9]*):)?(0x[0-9a-fA-F]{40})$/;
|
|
49
|
+
const REQUEST_HASH = /^sha256:[0-9a-fA-F]{64}$/;
|
|
50
|
+
const TOOL_CALL_RESOURCE = /^\/mcp\/tools\/[A-Za-z0-9_-]+$/;
|
|
51
|
+
const DEBT_OR_SEQ_ENDPOINT = /\/escrow\/(debts|next-seq)(\/|$)/;
|
|
52
|
+
const CONFIRMATION_KEYS = ["tool", "provider", "amount", "asset", "network", "payTo", "seq"];
|
|
53
|
+
function refuse(guard, code, reason) {
|
|
54
|
+
return { ok: false, guard, code, reason };
|
|
55
|
+
}
|
|
56
|
+
function isPlainObject(value) {
|
|
57
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
58
|
+
}
|
|
59
|
+
/** A canonical non-negative decimal integer string (or, where allowed, a safe integer). */
|
|
60
|
+
function uint(value, max, allowNumber = false) {
|
|
61
|
+
let n;
|
|
62
|
+
if (typeof value === "string" && CANONICAL_UINT.test(value))
|
|
63
|
+
n = BigInt(value);
|
|
64
|
+
else if (allowNumber && typeof value === "number" && Number.isSafeInteger(value) && value >= 0)
|
|
65
|
+
n = BigInt(value);
|
|
66
|
+
else
|
|
67
|
+
return null;
|
|
68
|
+
return max !== null && n > max ? null : n;
|
|
69
|
+
}
|
|
70
|
+
/** Unix seconds: a non-negative safe integer. */
|
|
71
|
+
function seconds(value) {
|
|
72
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : null;
|
|
73
|
+
}
|
|
74
|
+
/** A value from the server's requirement: a checksummed CAIP-10 account on `chainId`. */
|
|
75
|
+
function requirementAccount(value, chainId) {
|
|
76
|
+
const parsed = typeof value === "string" ? parseCaip10(value) : null;
|
|
77
|
+
if (!parsed || parsed.chainId !== chainId)
|
|
78
|
+
return null;
|
|
79
|
+
return getAddress(parsed.address);
|
|
80
|
+
}
|
|
81
|
+
/** A value the client holds itself: a `0x` address in any case, or a CAIP-10 account on `chainId`. */
|
|
82
|
+
function localAccount(value, chainId) {
|
|
83
|
+
const m = typeof value === "string" ? LOCAL_ADDRESS.exec(value) : null;
|
|
84
|
+
if (!m || !isAddress(m[2]))
|
|
85
|
+
return null;
|
|
86
|
+
if (m[1] !== undefined && Number(m[1]) !== chainId)
|
|
87
|
+
return null;
|
|
88
|
+
return getAddress(m[2]);
|
|
89
|
+
}
|
|
90
|
+
/** A bare `0x` address that is already checksummed, as the chain module returns it. */
|
|
91
|
+
function checksummedAddress(value) {
|
|
92
|
+
if (typeof value !== "string" || !BARE_ADDRESS.test(value) || !isAddress(value))
|
|
93
|
+
return null;
|
|
94
|
+
return getAddress(value) === value ? value : null;
|
|
95
|
+
}
|
|
96
|
+
/** A CAIP-19 erc20 asset as `{chainId, lower-case address}`, or null. */
|
|
97
|
+
function erc20(value) {
|
|
98
|
+
const m = typeof value === "string" ? CAIP19_ERC20_ANY_CASE.exec(value) : null;
|
|
99
|
+
return m ? { chain: m[1], address: m[2].toLowerCase() } : null;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* The same asset, as the signer compares it: the exact CAIP-19 string (checksummed address), and
|
|
103
|
+
* a readable erc20 id. Never looser than the signer, so a payment the guards accept is never
|
|
104
|
+
* refused at signing time (after the user was asked to confirm it).
|
|
105
|
+
*/
|
|
106
|
+
function sameAsset(a, b) {
|
|
107
|
+
return typeof a === "string" && a === b && erc20(a) !== null;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Dollars (a plain decimal string such as "0.50", "5" or ".5") as atomic units of an asset with
|
|
111
|
+
* `decimals` places, in exact integer arithmetic. `null` for anything else, including more places
|
|
112
|
+
* than the asset has.
|
|
113
|
+
*/
|
|
114
|
+
export function usdToAtomic(value, decimals) {
|
|
115
|
+
if (typeof value !== "string" || !Number.isInteger(decimals) || decimals < 0)
|
|
116
|
+
return null;
|
|
117
|
+
const m = /^(?:([0-9]+)(?:\.([0-9]+))?|\.([0-9]+))$/.exec(value);
|
|
118
|
+
if (!m)
|
|
119
|
+
return null;
|
|
120
|
+
const whole = m[1] ?? "0";
|
|
121
|
+
const fraction = m[2] ?? m[3] ?? "";
|
|
122
|
+
if (fraction.length > decimals)
|
|
123
|
+
return null;
|
|
124
|
+
return BigInt(whole) * 10n ** BigInt(decimals) + BigInt(fraction.padEnd(decimals, "0") || "0");
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* What the user must approve when `confirmAbove` applies: the values shown in the prompt. The
|
|
128
|
+
* caller shows these, and passes the same object back as `confirmation` once the user approves.
|
|
129
|
+
* `null` when the context has no readable requirement or forwarded call.
|
|
130
|
+
*/
|
|
131
|
+
export function confirmationFor(ctx) {
|
|
132
|
+
const req = ctx.requirement;
|
|
133
|
+
const call = ctx.forwardedCall;
|
|
134
|
+
if (!isPlainObject(req) || !isPlainObject(call) || !isPlainObject(req.amount))
|
|
135
|
+
return null;
|
|
136
|
+
const values = {
|
|
137
|
+
tool: call.tool,
|
|
138
|
+
provider: req.provider,
|
|
139
|
+
amount: req.amount.amount,
|
|
140
|
+
asset: req.amount.asset,
|
|
141
|
+
network: req.network,
|
|
142
|
+
payTo: req.payTo,
|
|
143
|
+
seq: req.seq,
|
|
144
|
+
};
|
|
145
|
+
return Object.values(values).every((v) => typeof v === "string") ? values : null;
|
|
146
|
+
}
|
|
147
|
+
function g1(ctx) {
|
|
148
|
+
const req = ctx.requirement;
|
|
149
|
+
if (!isPlainObject(req))
|
|
150
|
+
return refuse("G1", "G1_NOT_A_REQUIREMENT", "the payment request is not a requirement");
|
|
151
|
+
if (req.rail !== "B")
|
|
152
|
+
return refuse("G1", "G1_RAIL", "the payment request is not on rail B");
|
|
153
|
+
if (req.scheme !== "voucher")
|
|
154
|
+
return refuse("G1", "G1_SCHEME", "the payment request is not a voucher");
|
|
155
|
+
const call = ctx.forwardedCall;
|
|
156
|
+
const at = isPlainObject(call) ? seconds(call.at) : null;
|
|
157
|
+
if (!isPlainObject(call) || typeof call.tool !== "string" || call.tool.length === 0 || at === null) {
|
|
158
|
+
return refuse("G1", "G1_NOT_FORWARDED", "the payment request is not the answer to a forwarded tool call");
|
|
159
|
+
}
|
|
160
|
+
const now = seconds(ctx.nowSeconds);
|
|
161
|
+
if (now === null || at > now || now - at > FORWARDED_CALL_MAX_AGE_SECONDS) {
|
|
162
|
+
return refuse("G1", "G1_FORWARD_TOO_OLD", "the payment request does not answer a tool call forwarded just now");
|
|
163
|
+
}
|
|
164
|
+
return { tool: call.tool, now };
|
|
165
|
+
}
|
|
166
|
+
function g2(ctx, req) {
|
|
167
|
+
const net = ctx.network;
|
|
168
|
+
const chain = isPlainObject(net) && typeof net.id === "string" ? CAIP2_EIP155.exec(net.id) : null;
|
|
169
|
+
if (!isPlainObject(net) || !chain || req.network !== net.id) {
|
|
170
|
+
return refuse("G2", "G2_NETWORK", "the network is not in the built-in table");
|
|
171
|
+
}
|
|
172
|
+
const chainId = Number(chain[1]);
|
|
173
|
+
const escrow = typeof net.escrow === "string" ? parseCaip10(net.escrow) : null;
|
|
174
|
+
const payTo = requirementAccount(req.payTo, chainId);
|
|
175
|
+
if (!escrow || escrow.chainId !== chainId || payTo === null || payTo !== getAddress(escrow.address)) {
|
|
176
|
+
return refuse("G2", "G2_PAY_TO", "the payment is not to the built-in escrow");
|
|
177
|
+
}
|
|
178
|
+
const money = req.amount;
|
|
179
|
+
if (!isPlainObject(money))
|
|
180
|
+
return refuse("G2", "G2_AMOUNT", "the amount is missing");
|
|
181
|
+
if (!sameAsset(money.asset, net.asset) || erc20(net.asset).chain !== chain[1]) {
|
|
182
|
+
return refuse("G2", "G2_ASSET", "the asset is not the built-in asset");
|
|
183
|
+
}
|
|
184
|
+
// The signer's bound on built-in decimals (`network.internal.ts`).
|
|
185
|
+
if (!Number.isInteger(net.decimals) || net.decimals < 0 || net.decimals > 36 || money.decimals !== net.decimals) {
|
|
186
|
+
return refuse("G2", "G2_DECIMALS", "the asset decimals are not the built-in decimals");
|
|
187
|
+
}
|
|
188
|
+
const amount = uint(money.amount, MAX_UINT64);
|
|
189
|
+
if (amount === null || amount === 0n)
|
|
190
|
+
return refuse("G2", "G2_AMOUNT", "the amount is not a positive integer");
|
|
191
|
+
return { chainId, decimals: net.decimals, amount };
|
|
192
|
+
}
|
|
193
|
+
function g3(ctx, c) {
|
|
194
|
+
const quote = ctx.quote;
|
|
195
|
+
const max = isPlainObject(quote) ? uint(quote.maxAmount, MAX_UINT64) : null;
|
|
196
|
+
if (!isPlainObject(quote) || max === null)
|
|
197
|
+
return refuse("G3", "G3_NO_QUOTE", "there is no price quote for this tool");
|
|
198
|
+
if (!sameAsset(quote.asset, c.req.amount.asset)) {
|
|
199
|
+
return refuse("G3", "G3_QUOTE_ASSET", "the price quote is in another asset");
|
|
200
|
+
}
|
|
201
|
+
if (c.amount > max)
|
|
202
|
+
return refuse("G3", "G3_ABOVE_QUOTE", "the amount is above the quoted price");
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
function g4(ctx, c) {
|
|
206
|
+
const payer = requirementAccount(c.req.payer, c.chainId);
|
|
207
|
+
if (payer === null)
|
|
208
|
+
return refuse("G4", "G4_PAYER_INVALID", "the payer is not a valid account");
|
|
209
|
+
if (ctx.recordedPayer !== null && localAccount(ctx.recordedPayer, c.chainId) !== payer) {
|
|
210
|
+
return refuse("G4", "G4_PAYER_CHANGED", PAYER_CHANGED_REASON);
|
|
211
|
+
}
|
|
212
|
+
const chain = ctx.chain;
|
|
213
|
+
if (!isPlainObject(chain)) {
|
|
214
|
+
return refuse("G4", "G4_NO_CHAIN_READ", "the payment key's grant could not be read from the chain");
|
|
215
|
+
}
|
|
216
|
+
const validUntil = uint(chain.validUntil, MAX_UINT64, true);
|
|
217
|
+
const chainPayer = localAccount(chain.payer, c.chainId);
|
|
218
|
+
const chainDelegate = checksummedAddress(chain.delegate);
|
|
219
|
+
const readAt = seconds(chain.readAt);
|
|
220
|
+
if (validUntil === null ||
|
|
221
|
+
chainPayer === null ||
|
|
222
|
+
chainDelegate === null ||
|
|
223
|
+
readAt === null ||
|
|
224
|
+
typeof chain.active !== "boolean") {
|
|
225
|
+
return refuse("G4", "G4_NO_CHAIN_READ", "the payment key's grant could not be read from the chain");
|
|
226
|
+
}
|
|
227
|
+
if (readAt - c.now > CHAIN_READ_MAX_FUTURE_SECONDS || c.now - readAt > CHAIN_READ_MAX_AGE_SECONDS) {
|
|
228
|
+
return refuse("G4", "G4_STALE_READ", "the chain read is too old");
|
|
229
|
+
}
|
|
230
|
+
// An unreadable local key is G5's refusal; here only a readable, different key refuses.
|
|
231
|
+
const local = localAccount(ctx.localKey, c.chainId);
|
|
232
|
+
if (local !== null && chainDelegate !== local) {
|
|
233
|
+
return refuse("G4", "G4_OTHER_KEY", "the chain read is for another payment key");
|
|
234
|
+
}
|
|
235
|
+
if (chainPayer !== payer)
|
|
236
|
+
return refuse("G4", "G4_NOT_DELEGATED", "the payer has not authorised this payment key");
|
|
237
|
+
if (chain.active !== true)
|
|
238
|
+
return refuse("G4", "G4_INACTIVE", "the payment key's grant is not active");
|
|
239
|
+
if (validUntil <= BigInt(c.now))
|
|
240
|
+
return refuse("G4", "G4_EXPIRED", "the payment key's grant has expired");
|
|
241
|
+
return payer;
|
|
242
|
+
}
|
|
243
|
+
function g5(ctx, c) {
|
|
244
|
+
const local = localAccount(ctx.localKey, c.chainId);
|
|
245
|
+
if (local === null)
|
|
246
|
+
return refuse("G5", "G5_LOCAL_KEY_INVALID", "the local payment key is not readable");
|
|
247
|
+
const signers = c.req.signers;
|
|
248
|
+
if (!Array.isArray(signers) || !signers.includes("delegate")) {
|
|
249
|
+
return refuse("G5", "G5_NO_DELEGATE_SIGNER", "the server does not accept a payment key signature");
|
|
250
|
+
}
|
|
251
|
+
if (requirementAccount(c.req.delegate, c.chainId) !== local) {
|
|
252
|
+
return refuse("G5", "G5_DELEGATE_MISMATCH", "the bound payment key is not this computer's key");
|
|
253
|
+
}
|
|
254
|
+
if (local === c.payer)
|
|
255
|
+
return refuse("G5", "G5_LOCAL_KEY_IS_PAYER", "the local key is the payer");
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
function g6(ctx, c) {
|
|
259
|
+
const chain = ctx.chain;
|
|
260
|
+
const delegateMax = uint(chain.delegateMaxVoucherAmount, MAX_UINT64);
|
|
261
|
+
const escrowMax = uint(chain.maxVoucherAmount, MAX_UINT64);
|
|
262
|
+
if (delegateMax === null || escrowMax === null) {
|
|
263
|
+
return refuse("G6", "G6_CAPS_UNREADABLE", "the on-chain caps could not be read");
|
|
264
|
+
}
|
|
265
|
+
if (c.amount > delegateMax)
|
|
266
|
+
return refuse("G6", "G6_DELEGATE_CAP", "the amount is above the payment key's on-chain cap");
|
|
267
|
+
if (c.amount > escrowMax)
|
|
268
|
+
return refuse("G6", "G6_ESCROW_CAP", "the amount is above the escrow's on-chain cap");
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
/** G7 caps. Returns the `confirmAbove` threshold (atomic) for the last step, or a refusal. */
|
|
272
|
+
function g7(ctx, c) {
|
|
273
|
+
const limits = ctx.limits;
|
|
274
|
+
if (!isPlainObject(limits))
|
|
275
|
+
return refuse("G7", "G7_LIMITS_UNREADABLE", "the local limits are not readable");
|
|
276
|
+
const perCall = usdToAtomic(limits.perCall, c.decimals);
|
|
277
|
+
const perDay = usdToAtomic(limits.perDay, c.decimals);
|
|
278
|
+
const confirmAbove = limits.confirmAbove === null ? null : usdToAtomic(limits.confirmAbove, c.decimals);
|
|
279
|
+
if (perCall === null || perDay === null || (limits.confirmAbove !== null && confirmAbove === null)) {
|
|
280
|
+
return refuse("G7", "G7_LIMITS_UNREADABLE", "the local limits are not readable");
|
|
281
|
+
}
|
|
282
|
+
if (c.amount > perCall)
|
|
283
|
+
return refuse("G7", "G7_PER_CALL", "the amount is above the local limit per call");
|
|
284
|
+
if (!Array.isArray(ctx.signed))
|
|
285
|
+
return refuse("G7", "G7_STATE_UNREADABLE", "the record of signed payments is not readable");
|
|
286
|
+
let spent = 0n;
|
|
287
|
+
for (const entry of ctx.signed) {
|
|
288
|
+
const at = isPlainObject(entry) ? seconds(entry.at) : null;
|
|
289
|
+
const amount = isPlainObject(entry) ? uint(entry.amount, MAX_UINT64) : null;
|
|
290
|
+
if (at === null || amount === null) {
|
|
291
|
+
return refuse("G7", "G7_STATE_UNREADABLE", "the record of signed payments is not readable");
|
|
292
|
+
}
|
|
293
|
+
// Inside the window, and anything dated in the future (a clock moved back) still counts.
|
|
294
|
+
if (at > c.now - SPEND_WINDOW_SECONDS)
|
|
295
|
+
spent += amount;
|
|
296
|
+
}
|
|
297
|
+
if (spent + c.amount > perDay)
|
|
298
|
+
return refuse("G7", "G7_PER_DAY", "the amount would pass the local limit per 24 hours");
|
|
299
|
+
return { confirmAbove };
|
|
300
|
+
}
|
|
301
|
+
function g8(ctx, c) {
|
|
302
|
+
const provider = requirementAccount(c.req.provider, c.chainId);
|
|
303
|
+
if (provider === null)
|
|
304
|
+
return refuse("G8", "G8_PROVIDER_INVALID", "the provider is not a valid account");
|
|
305
|
+
const providers = ctx.limits.providers;
|
|
306
|
+
if (providers === null)
|
|
307
|
+
return null;
|
|
308
|
+
if (!Array.isArray(providers))
|
|
309
|
+
return refuse("G8", "G8_PROVIDERS_UNREADABLE", "the provider allow-list is not readable");
|
|
310
|
+
const allowed = providers.map((p) => localAccount(p, c.chainId));
|
|
311
|
+
if (allowed.includes(null))
|
|
312
|
+
return refuse("G8", "G8_PROVIDERS_UNREADABLE", "the provider allow-list is not readable");
|
|
313
|
+
if (!allowed.includes(provider))
|
|
314
|
+
return refuse("G8", "G8_PROVIDER_NOT_ALLOWED", "the provider is not in the allow-list");
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
function g9(c) {
|
|
318
|
+
const seq = c.req.seq;
|
|
319
|
+
const ok = typeof seq === "string" && /^[1-9][0-9]*$/.test(seq) && BigInt(seq) < MAX_SEQ_EXCLUSIVE;
|
|
320
|
+
return ok ? null : refuse("G9", "G9_SEQ", "the sequence number is out of range");
|
|
321
|
+
}
|
|
322
|
+
function g10(c) {
|
|
323
|
+
const { requestHash, resource, maxTimeoutSeconds: t } = c.req;
|
|
324
|
+
if (typeof requestHash !== "string" || !REQUEST_HASH.test(requestHash)) {
|
|
325
|
+
return refuse("G10", "G10_REQUEST_HASH", "the request hash is malformed");
|
|
326
|
+
}
|
|
327
|
+
// Length in Unicode code points, so every implementation counts the same.
|
|
328
|
+
const length = typeof resource === "string" ? [...resource].length : 0;
|
|
329
|
+
if (length < 1 || length > MAX_RESOURCE_LENGTH)
|
|
330
|
+
return refuse("G10", "G10_RESOURCE", "the resource length is out of range");
|
|
331
|
+
if (typeof t !== "number" || !Number.isInteger(t) || t < 1 || t > MAX_TIMEOUT_SECONDS) {
|
|
332
|
+
return refuse("G10", "G10_MAX_TIMEOUT", "the timeout is out of range");
|
|
333
|
+
}
|
|
334
|
+
return null;
|
|
335
|
+
}
|
|
336
|
+
function g12(c) {
|
|
337
|
+
const resource = c.req.resource;
|
|
338
|
+
let decoded = resource;
|
|
339
|
+
try {
|
|
340
|
+
decoded = decodeURIComponent(resource);
|
|
341
|
+
}
|
|
342
|
+
catch {
|
|
343
|
+
// Not decodable: the raw form is checked, and it cannot be a tool call either.
|
|
344
|
+
}
|
|
345
|
+
if ([resource, decoded].some((r) => DEBT_OR_SEQ_ENDPOINT.test(r.toLowerCase().split(/[?#]/)[0]))) {
|
|
346
|
+
return refuse("G12", "G12_DEBT_OR_SEQ_ENDPOINT", "the payment is for a debt or sequence endpoint");
|
|
347
|
+
}
|
|
348
|
+
if (!TOOL_CALL_RESOURCE.test(resource))
|
|
349
|
+
return refuse("G12", "G12_NOT_A_TOOL_CALL", "the payment is not for a tool call");
|
|
350
|
+
if (resource !== TOOL_RESOURCE_PREFIX + c.tool) {
|
|
351
|
+
return refuse("G12", "G12_OTHER_TOOL", "the payment is for another tool than the one called");
|
|
352
|
+
}
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
/** The last step: above `confirmAbove`, only a confirmation of exactly this payment passes. */
|
|
356
|
+
function confirm(ctx, c, confirmAbove) {
|
|
357
|
+
const given = ctx.confirmation;
|
|
358
|
+
if (given !== null && !isPlainObject(given)) {
|
|
359
|
+
return refuse("G7", "G7_CONFIRMATION_MISMATCH", "the confirmation is not readable");
|
|
360
|
+
}
|
|
361
|
+
if (confirmAbove === null || c.amount <= confirmAbove)
|
|
362
|
+
return null;
|
|
363
|
+
if (given === null) {
|
|
364
|
+
return refuse("G7", "G7_CONFIRMATION_REQUIRED", "the amount is above the confirmation limit and was not confirmed");
|
|
365
|
+
}
|
|
366
|
+
const expected = confirmationFor(ctx);
|
|
367
|
+
const keys = Object.keys(given);
|
|
368
|
+
const same = expected !== null &&
|
|
369
|
+
keys.length === CONFIRMATION_KEYS.length &&
|
|
370
|
+
CONFIRMATION_KEYS.every((k) => Object.hasOwn(given, k) && given[k] === expected[k]);
|
|
371
|
+
return same ? null : refuse("G7", "G7_CONFIRMATION_MISMATCH", "the confirmation was for another payment");
|
|
372
|
+
}
|
|
373
|
+
function run(ctx) {
|
|
374
|
+
if (!isPlainObject(ctx))
|
|
375
|
+
return refuse("G1", "G1_NOT_A_REQUIREMENT", "there is no payment request");
|
|
376
|
+
const call = g1(ctx);
|
|
377
|
+
if ("ok" in call)
|
|
378
|
+
return call;
|
|
379
|
+
const req = ctx.requirement;
|
|
380
|
+
const net = g2(ctx, req);
|
|
381
|
+
if ("ok" in net)
|
|
382
|
+
return net;
|
|
383
|
+
const base = { req, ...call, ...net };
|
|
384
|
+
const quote = g3(ctx, base);
|
|
385
|
+
if (quote)
|
|
386
|
+
return quote;
|
|
387
|
+
const payer = g4(ctx, base);
|
|
388
|
+
if (typeof payer !== "string")
|
|
389
|
+
return payer;
|
|
390
|
+
const c = { ...base, payer };
|
|
391
|
+
const early = g5(ctx, c) ?? g6(ctx, c);
|
|
392
|
+
if (early)
|
|
393
|
+
return early;
|
|
394
|
+
const caps = g7(ctx, c);
|
|
395
|
+
if ("ok" in caps)
|
|
396
|
+
return caps;
|
|
397
|
+
return g8(ctx, c) ?? g9(c) ?? g10(c) ?? g12(c) ?? confirm(ctx, c, caps.confirmAbove) ?? { ok: true };
|
|
398
|
+
}
|
|
399
|
+
/** Runs G1..G12 in order (G11 is app-only), then the G7 confirmation. `{ok: true}` only when all pass; never throws. */
|
|
400
|
+
export function checkAll(ctx) {
|
|
401
|
+
try {
|
|
402
|
+
return run(ctx);
|
|
403
|
+
}
|
|
404
|
+
catch {
|
|
405
|
+
// An unexpected shape is a refusal, never a pass. G1 is the guard that owns "not a request".
|
|
406
|
+
return refuse("G1", "G1_NOT_A_REQUIREMENT", "the payment request could not be checked");
|
|
407
|
+
}
|
|
408
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** A `0x`-prefixed hex string. */
|
|
2
|
+
export type Hex = `0x${string}`;
|
|
3
|
+
/** A `0x`-prefixed 20-byte address. */
|
|
4
|
+
export type Address = `0x${string}`;
|
|
5
|
+
/** `keccak256` of raw bytes, lower-case hex. */
|
|
6
|
+
export declare function keccak256(bytes: Uint8Array): Hex;
|
|
7
|
+
/** `keccak256` of the UTF-8 bytes of a string, even when it starts with `0x`. */
|
|
8
|
+
export declare function keccakUtf8(text: string): Hex;
|
|
9
|
+
/** A `0x` address of 40 hex digits, in any case. */
|
|
10
|
+
export declare function isAddress(value: unknown): value is string;
|
|
11
|
+
/** A `0x` address that is already in its EIP-55 spelling. */
|
|
12
|
+
export declare function isChecksummed(value: unknown): value is Address;
|
|
13
|
+
/** The EIP-55 spelling of an address; throws on anything that is not one. */
|
|
14
|
+
export declare function getAddress(value: string): Address;
|
package/dist/core/hex.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { keccak_256 } from "@noble/hashes/sha3";
|
|
2
|
+
const ADDRESS = /^0x[0-9a-fA-F]{40}$/;
|
|
3
|
+
const encoder = new TextEncoder();
|
|
4
|
+
function toHex(bytes) {
|
|
5
|
+
let out = "0x";
|
|
6
|
+
for (const b of bytes)
|
|
7
|
+
out += b.toString(16).padStart(2, "0");
|
|
8
|
+
return out;
|
|
9
|
+
}
|
|
10
|
+
/** `keccak256` of raw bytes, lower-case hex. */
|
|
11
|
+
export function keccak256(bytes) {
|
|
12
|
+
return toHex(keccak_256(bytes));
|
|
13
|
+
}
|
|
14
|
+
/** `keccak256` of the UTF-8 bytes of a string, even when it starts with `0x`. */
|
|
15
|
+
export function keccakUtf8(text) {
|
|
16
|
+
return keccak256(encoder.encode(text));
|
|
17
|
+
}
|
|
18
|
+
/** The EIP-55 checksummed spelling of a 20-byte hex address (any case in). */
|
|
19
|
+
function checksum(address) {
|
|
20
|
+
const lower = address.slice(2).toLowerCase();
|
|
21
|
+
const hash = keccak_256(encoder.encode(lower));
|
|
22
|
+
let out = "0x";
|
|
23
|
+
for (let i = 0; i < 40; i++) {
|
|
24
|
+
const nibble = (hash[i >> 1] >> (i % 2 === 0 ? 4 : 0)) & 0xf;
|
|
25
|
+
out += nibble >= 8 ? lower[i].toUpperCase() : lower[i];
|
|
26
|
+
}
|
|
27
|
+
return out;
|
|
28
|
+
}
|
|
29
|
+
/** A `0x` address of 40 hex digits, in any case. */
|
|
30
|
+
export function isAddress(value) {
|
|
31
|
+
return typeof value === "string" && ADDRESS.test(value);
|
|
32
|
+
}
|
|
33
|
+
/** A `0x` address that is already in its EIP-55 spelling. */
|
|
34
|
+
export function isChecksummed(value) {
|
|
35
|
+
return isAddress(value) && checksum(value) === value;
|
|
36
|
+
}
|
|
37
|
+
/** The EIP-55 spelling of an address; throws on anything that is not one. */
|
|
38
|
+
export function getAddress(value) {
|
|
39
|
+
if (!isAddress(value))
|
|
40
|
+
throw new TypeError("not an address");
|
|
41
|
+
return checksum(value);
|
|
42
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The protocol core, without I/O except the chain reader: EIP-712 voucher and binding, guards
|
|
3
|
+
* G1-G12, clock correction, CAIP parsing and `_meta` helpers. For tools built on the protocol
|
|
4
|
+
* itself (such as `assay-mcp`); most callers use the `Assay` client instead.
|
|
5
|
+
*/
|
|
6
|
+
export * from "./binding.js";
|
|
7
|
+
export * from "./caip.js";
|
|
8
|
+
export * from "./chain.js";
|
|
9
|
+
export * from "./clock.js";
|
|
10
|
+
export * from "./errors.js";
|
|
11
|
+
export * from "./guard-types.js";
|
|
12
|
+
export * from "./guards.js";
|
|
13
|
+
export * from "./hex.js";
|
|
14
|
+
export * from "./meta.js";
|
|
15
|
+
export * from "./voucher.js";
|
|
16
|
+
export { guardAndBuild, type GuardedVoucher, type NetworkEntry as Eip155NetworkEntry, } from "../schemes/eip155/index.js";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The protocol core, without I/O except the chain reader: EIP-712 voucher and binding, guards
|
|
3
|
+
* G1-G12, clock correction, CAIP parsing and `_meta` helpers. For tools built on the protocol
|
|
4
|
+
* itself (such as `assay-mcp`); most callers use the `Assay` client instead.
|
|
5
|
+
*/
|
|
6
|
+
export * from "./binding.js";
|
|
7
|
+
export * from "./caip.js";
|
|
8
|
+
export * from "./chain.js";
|
|
9
|
+
export * from "./clock.js";
|
|
10
|
+
export * from "./errors.js";
|
|
11
|
+
export * from "./guard-types.js";
|
|
12
|
+
export * from "./guards.js";
|
|
13
|
+
export * from "./hex.js";
|
|
14
|
+
export * from "./meta.js";
|
|
15
|
+
export * from "./voucher.js";
|
|
16
|
+
export { guardAndBuild, } from "../schemes/eip155/index.js";
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `_meta` keys Assay reads and writes on MCP messages (spec section 3), and pure helpers.
|
|
3
|
+
*/
|
|
4
|
+
export declare const IDEMPOTENCY_META = "assay/idempotency-key";
|
|
5
|
+
export declare const RAIL_META = "assay/rail";
|
|
6
|
+
export declare const VOUCHER_META = "assay/voucher";
|
|
7
|
+
export declare const PAYMENT_REQUIRED_META = "x402/paymentRequired";
|
|
8
|
+
export declare const ERROR_META = "assay/error";
|
|
9
|
+
export declare const RECEIPT_META = "assay/receipt";
|
|
10
|
+
export declare const REPLAYED_META = "assay/replayed";
|
|
11
|
+
export declare const PRICE_META = "assay/price";
|
|
12
|
+
export declare const PAYMENT_REQUIRED_CODE = "PAYMENT_REQUIRED";
|
|
13
|
+
export declare const DELEGATE_INACTIVE_CODE = "PAYMENT_VOUCHER_DELEGATE_INACTIVE";
|
|
14
|
+
export declare const IN_PROGRESS_CODE = "IDEMPOTENCY_IN_PROGRESS";
|
|
15
|
+
/** The meta tools every Assay MCP URL serves. */
|
|
16
|
+
export declare const SEARCH_TOOLS = "search_tools";
|
|
17
|
+
export declare const GET_TOOL = "get_tool";
|
|
18
|
+
export declare const CALL_TOOL = "call_tool";
|
|
19
|
+
export declare function isObject(v: unknown): v is Record<string, unknown>;
|
|
20
|
+
/** 1 to 128 printable ASCII characters (spec section 8). */
|
|
21
|
+
export declare function isIdempotencyKey(value: unknown): value is string;
|
|
22
|
+
/**
|
|
23
|
+
* A tool name as sent to the server, unchanged: a standard `verb_noun`, or a provider tool's
|
|
24
|
+
* `{handle}__{tool}` (for example `acme__get_quote`). The server decides what it serves.
|
|
25
|
+
*/
|
|
26
|
+
export declare function isToolName(value: unknown): value is string;
|
|
27
|
+
/** `assay/price` reduced to what G3 reads: `maxAmount` (atomic units) and its asset. */
|
|
28
|
+
export interface PriceQuote {
|
|
29
|
+
maxAmount: string;
|
|
30
|
+
asset: string;
|
|
31
|
+
}
|
|
32
|
+
export declare function quoteOf(price: unknown): PriceQuote | null;
|
|
33
|
+
/** The structured output of a meta tool result (`structuredContent`, else the JSON text block). */
|
|
34
|
+
export declare function structuredOf(result: unknown): Record<string, unknown> | null;
|
|
35
|
+
/** The voucher envelope of the signed retry (spec section 6.4). */
|
|
36
|
+
export declare function voucherEnvelope(voucher: unknown, signature: string, providerId: string, contractVersion: unknown): Record<string, unknown>;
|
|
37
|
+
/** The payment key as people compare it: the last 8 hex digits, upper case, in two groups. */
|
|
38
|
+
export declare function fingerprint(address: string): string;
|