@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.
Files changed (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +252 -2
  3. package/dist/client.d.ts +88 -0
  4. package/dist/client.js +158 -0
  5. package/dist/core/binding.d.ts +56 -0
  6. package/dist/core/binding.js +132 -0
  7. package/dist/core/caip.d.ts +15 -0
  8. package/dist/core/caip.js +31 -0
  9. package/dist/core/chain.d.ts +37 -0
  10. package/dist/core/chain.js +148 -0
  11. package/dist/core/clock.d.ts +12 -0
  12. package/dist/core/clock.js +25 -0
  13. package/dist/core/errors.d.ts +9 -0
  14. package/dist/core/errors.js +13 -0
  15. package/dist/core/guard-types.d.ts +174 -0
  16. package/dist/core/guard-types.js +69 -0
  17. package/dist/core/guards.d.ts +54 -0
  18. package/dist/core/guards.js +408 -0
  19. package/dist/core/hex.d.ts +14 -0
  20. package/dist/core/hex.js +42 -0
  21. package/dist/core/index.d.ts +16 -0
  22. package/dist/core/index.js +16 -0
  23. package/dist/core/meta.d.ts +38 -0
  24. package/dist/core/meta.js +79 -0
  25. package/dist/core/voucher.d.ts +116 -0
  26. package/dist/core/voucher.js +137 -0
  27. package/dist/errors.d.ts +149 -0
  28. package/dist/errors.js +205 -0
  29. package/dist/http.d.ts +52 -0
  30. package/dist/http.js +139 -0
  31. package/dist/index.d.ts +12 -0
  32. package/dist/index.js +4 -0
  33. package/dist/mcp-url.d.ts +17 -0
  34. package/dist/mcp-url.js +98 -0
  35. package/dist/mcp.d.ts +23 -0
  36. package/dist/mcp.js +121 -0
  37. package/dist/networks.d.ts +5 -0
  38. package/dist/networks.js +46 -0
  39. package/dist/resources/context.d.ts +48 -0
  40. package/dist/resources/context.js +28 -0
  41. package/dist/resources/paid-calls.d.ts +82 -0
  42. package/dist/resources/paid-calls.js +156 -0
  43. package/dist/resources/receipts.d.ts +25 -0
  44. package/dist/resources/receipts.js +21 -0
  45. package/dist/resources/tools.d.ts +96 -0
  46. package/dist/resources/tools.js +241 -0
  47. package/dist/rest.d.ts +17 -0
  48. package/dist/rest.js +43 -0
  49. package/dist/schemes/eip155/bind.d.ts +17 -0
  50. package/dist/schemes/eip155/bind.js +59 -0
  51. package/dist/schemes/eip155/index.d.ts +71 -0
  52. package/dist/schemes/eip155/index.js +264 -0
  53. package/dist/schemes/types.d.ts +54 -0
  54. package/dist/schemes/types.js +1 -0
  55. package/dist/signer.d.ts +39 -0
  56. package/dist/signer.js +29 -0
  57. package/dist/version.d.ts +2 -0
  58. package/dist/version.js +2 -0
  59. package/package.json +67 -4
package/dist/rest.js ADDED
@@ -0,0 +1,43 @@
1
+ import { isObject } from "./core/meta.js";
2
+ import { ServerError } from "./errors.js";
3
+ import { restError } from "./mcp.js";
4
+ /** The REST API (spec section 2.2): `{data}` on success, a typed error from `{error}` otherwise. */
5
+ export class RestClient {
6
+ http;
7
+ base;
8
+ now;
9
+ constructor(http, base, now = Date.now) {
10
+ this.http = http;
11
+ this.base = base;
12
+ this.now = now;
13
+ }
14
+ /** GETs are retried; POSTs are not (a challenge or a bind is not resent blindly). */
15
+ async get(path) {
16
+ return this.#send("GET", path, undefined);
17
+ }
18
+ async post(path, body) {
19
+ return this.#send("POST", path, body);
20
+ }
21
+ async #send(method, path, body) {
22
+ const res = await this.http.request({
23
+ url: `${this.base}${path}`,
24
+ method,
25
+ ...(body !== undefined ? { headers: { "content-type": "application/json" }, body: JSON.stringify(body) } : {}),
26
+ retry: method === "GET",
27
+ });
28
+ if (res.status < 200 || res.status > 299) {
29
+ throw (restError(res.status, res.text, res.headers.get("retry-after"), this.now()) ??
30
+ new ServerError(`HTTP_${res.status}`, `Assay answered HTTP ${res.status}`, { status: res.status }));
31
+ }
32
+ let parsed;
33
+ try {
34
+ parsed = JSON.parse(res.text);
35
+ }
36
+ catch {
37
+ parsed = undefined;
38
+ }
39
+ if (!isObject(parsed) || !("data" in parsed))
40
+ throw new ServerError("API_BAD_ANSWER", "Assay sent an answer the SDK cannot read", { status: res.status });
41
+ return { data: parsed.data, date: res.headers.get("date") };
42
+ }
43
+ }
@@ -0,0 +1,17 @@
1
+ import type { RestClient } from "../../rest.js";
2
+ import type { EvmSigner } from "../../signer.js";
3
+ /** The `data` of `POST /agents/me/delegate`; `status` is `ACTIVE` or `PENDING_APPROVAL`. */
4
+ export type BindAnswer = Record<string, unknown>;
5
+ /**
6
+ * Binds an EVM key to the API key's credential (spec section 5): the challenge, its field-by-field
7
+ * check, the signature over typed data rebuilt from the SDK's own constants, the bind request, and
8
+ * a check that the answer names this key (as `delegate` when `ACTIVE`, as `pendingDelegate` when
9
+ * `PENDING_APPROVAL`). A challenge refused only on its expiry is retried once.
10
+ */
11
+ export declare function bindEvmKey(o: {
12
+ signer: EvmSigner;
13
+ chainId: number;
14
+ credentialId: string;
15
+ api: RestClient;
16
+ now: () => number;
17
+ }): Promise<BindAnswer>;
@@ -0,0 +1,59 @@
1
+ import { checkBindingChallenge } from "../../core/binding.js";
2
+ import { parseCaip10 } from "../../core/caip.js";
3
+ import { correctedNow, dateHeaderOffset } from "../../core/clock.js";
4
+ import { SignerRefusal } from "../../core/errors.js";
5
+ import { getAddress } from "../../core/hex.js";
6
+ import { isObject } from "../../core/meta.js";
7
+ import { PaymentError } from "../../errors.js";
8
+ /**
9
+ * Binds an EVM key to the API key's credential (spec section 5): the challenge, its field-by-field
10
+ * check, the signature over typed data rebuilt from the SDK's own constants, the bind request, and
11
+ * a check that the answer names this key (as `delegate` when `ACTIVE`, as `pendingDelegate` when
12
+ * `PENDING_APPROVAL`). A challenge refused only on its expiry is retried once.
13
+ */
14
+ export async function bindEvmKey(o) {
15
+ const address = getAddress(o.signer.address);
16
+ for (let attempt = 0; attempt < 2; attempt++) {
17
+ const challenge = await o.api.post("/agents/me/delegate/challenge", { delegate: address });
18
+ const local = o.now() / 1000;
19
+ const nowSeconds = correctedNow(local, dateHeaderOffset(challenge.date, local));
20
+ let typedData;
21
+ try {
22
+ typedData = checkBindingChallenge(challenge.data, {
23
+ localAddress: address,
24
+ credentialId: o.credentialId,
25
+ chainId: o.chainId,
26
+ nowSeconds,
27
+ });
28
+ }
29
+ catch (err) {
30
+ if (!(err instanceof SignerRefusal))
31
+ throw err;
32
+ if (err.field === "message.expiresAt") {
33
+ if (attempt === 0)
34
+ continue;
35
+ throw new PaymentError("BINDING_CLOCK", "the binding challenge expired twice; check this machine's clock");
36
+ }
37
+ throw new PaymentError("BINDING_CHALLENGE_REFUSED", `the binding challenge failed its check (${err.field}); nothing was signed`, { details: { field: err.field } });
38
+ }
39
+ const challengeId = challenge.data.challengeId;
40
+ const signature = await o.signer.signTypedData(typedData);
41
+ if (typeof signature !== "string" || !/^0x[0-9a-fA-F]{130}$/.test(signature)) {
42
+ throw new PaymentError("SIGNER_FAILED", "the signer did not return a 65-byte signature");
43
+ }
44
+ const bound = await o.api.post("/agents/me/delegate", { challengeId, signature });
45
+ const data = isObject(bound.data) ? bound.data : {};
46
+ const names = (v) => {
47
+ const p = typeof v === "string" ? parseCaip10(v) : null;
48
+ return p !== null && p.chainId === o.chainId && p.address === address;
49
+ };
50
+ // ACTIVE: this key is the delegate now. PENDING_APPROVAL: another key stays bound and keeps
51
+ // signing; this one waits as `pendingDelegate` until a person approves it.
52
+ const accepted = (data.status === "ACTIVE" && names(data.delegate)) ||
53
+ (data.status === "PENDING_APPROVAL" && names(data.pendingDelegate));
54
+ if (!accepted)
55
+ throw new PaymentError("BINDING_NOT_ACCEPTED", "Assay did not confirm the binding of this key");
56
+ return data;
57
+ }
58
+ throw new PaymentError("BINDING_CLOCK", "the binding challenge expired twice; check this machine's clock");
59
+ }
@@ -0,0 +1,71 @@
1
+ import { ChainReader } from "../../core/chain.js";
2
+ import type { GuardContext, GuardResult } from "../../core/guard-types.js";
3
+ import { type Address } from "../../core/hex.js";
4
+ import { type VoucherTypedData, type WireVoucher } from "../../core/voucher.js";
5
+ import type { PaymentScheme, Prepared, PrepareInput } from "../types.js";
6
+ /** One EVM network: built into the SDK, or configured by the caller. */
7
+ export interface Eip155Network {
8
+ /** The chain id; when given it must be the one in the CAIP-2 key. */
9
+ chainId?: number;
10
+ /** CAIP-10 of the escrow (the EIP-712 `verifyingContract`). */
11
+ escrow: string;
12
+ /** CAIP-19 erc20 asset. */
13
+ asset: string;
14
+ decimals: number;
15
+ /** JSON-RPC URLs for the chain reads of G4/G6. Every one must agree. */
16
+ rpcUrls: string[];
17
+ }
18
+ export interface NetworkEntry {
19
+ id: string;
20
+ chainId: number;
21
+ escrow: Address;
22
+ escrowCaip10: string;
23
+ asset: string;
24
+ decimals: number;
25
+ rpc: readonly string[];
26
+ }
27
+ /** Local limits in dollars, as decimal strings (G7). */
28
+ export interface Limits {
29
+ maxPerCall: string;
30
+ maxPerDay: string;
31
+ }
32
+ export declare const DEFAULT_LIMITS: Readonly<Limits>;
33
+ /**
34
+ * Checks and freezes a network table. `fail` makes the error: a `ConfigError` for the caller's
35
+ * own table, a server error for one the API sent. Non-eip155 entries are skipped.
36
+ */
37
+ export declare function eip155Networks(table: Record<string, Eip155Network>, fail?: (message: string) => Error): Map<string, NetworkEntry>;
38
+ export declare function checkLimits(limits: Limits): Limits;
39
+ export type GuardedVoucher = {
40
+ ok: true;
41
+ wire: WireVoucher;
42
+ typedData: VoucherTypedData;
43
+ };
44
+ /**
45
+ * The one path from a requirement to a voucher: the guards and the builder see the same frozen
46
+ * copy, and the network the guards check is the caller's own entry for `requirement.network`.
47
+ * Returns the first refusal, or the voucher to sign.
48
+ */
49
+ export declare function guardAndBuild(ctx: Omit<GuardContext, "network">, issuedAt: number, networks: ReadonlyMap<string, NetworkEntry>): GuardedVoucher | Exclude<GuardResult, {
50
+ ok: true;
51
+ }>;
52
+ /** Tests only: forget every signed voucher of this process. */
53
+ export declare function resetSpendLedger(): void;
54
+ export interface Eip155SchemeOptions {
55
+ /** The network table: built into the SDK, or the caller's for non-production environments. */
56
+ networks: ReadonlyMap<string, NetworkEntry>;
57
+ limits: Limits;
58
+ chain: ChainReader;
59
+ /** Milliseconds clock. */
60
+ now: () => number;
61
+ /** Server minus local clock, seconds (spec section 7). */
62
+ dateOffset: () => number;
63
+ }
64
+ export declare class Eip155Scheme implements PaymentScheme {
65
+ #private;
66
+ private readonly o;
67
+ readonly namespace = "eip155";
68
+ constructor(o: Eip155SchemeOptions);
69
+ prepare(input: PrepareInput): Promise<Prepared>;
70
+ onRefused(code: string, input: PrepareInput): Promise<void>;
71
+ }
@@ -0,0 +1,264 @@
1
+ /**
2
+ * The `eip155` payment scheme: EIP-712 vouchers and the EIP-712 key binding, behind the
3
+ * {@link PaymentScheme} interface. Guards G1-G12 run on one frozen copy of the requirement, the
4
+ * voucher is built from that same copy and the caller's own network entry, and only then signed.
5
+ */
6
+ import { eip155ChainId, parseCaip10, parseCaip19Erc20 } from "../../core/caip.js";
7
+ import { ChainReader, isUsableRpcUrl } from "../../core/chain.js";
8
+ import { correctedNow } from "../../core/clock.js";
9
+ import { SignerRefusal } from "../../core/errors.js";
10
+ import { checkAll, usdToAtomic } from "../../core/guards.js";
11
+ import { getAddress } from "../../core/hex.js";
12
+ import { isObject, voucherEnvelope } from "../../core/meta.js";
13
+ import { buildVoucher } from "../../core/voucher.js";
14
+ import { ConfigError } from "../../errors.js";
15
+ export const DEFAULT_LIMITS = Object.freeze({ maxPerCall: "0.50", maxPerDay: "5" });
16
+ /**
17
+ * Checks and freezes a network table. `fail` makes the error: a `ConfigError` for the caller's
18
+ * own table, a server error for one the API sent. Non-eip155 entries are skipped.
19
+ */
20
+ export function eip155Networks(table, fail = (m) => new ConfigError(m)) {
21
+ const out = new Map();
22
+ for (const [id, n] of Object.entries(table)) {
23
+ if (!id.startsWith("eip155:"))
24
+ continue;
25
+ const chainId = eip155ChainId(id);
26
+ const escrow = typeof n?.escrow === "string" ? parseCaip10(n.escrow) : null;
27
+ const asset = typeof n?.asset === "string" ? parseCaip19Erc20(n.asset) : null;
28
+ if (chainId === null)
29
+ throw fail(`networks: ${id} is not a CAIP-2 eip155 chain`);
30
+ if (n?.chainId !== undefined && n.chainId !== chainId)
31
+ throw fail(`networks.${id}.chainId is not ${chainId}`);
32
+ if (!escrow || escrow.chainId !== chainId)
33
+ throw fail(`networks.${id}.escrow must be a checksummed CAIP-10 account on ${id}`);
34
+ if (!asset || asset.chainId !== chainId)
35
+ throw fail(`networks.${id}.asset must be a checksummed CAIP-19 erc20 asset on ${id}`);
36
+ if (!Number.isInteger(n.decimals) || n.decimals < 0 || n.decimals > 36)
37
+ throw fail(`networks.${id}.decimals is invalid`);
38
+ const rpc = n.rpcUrls;
39
+ if (!Array.isArray(rpc) || rpc.length === 0 || !rpc.every((u) => typeof u === "string" && isUsableRpcUrl(u))) {
40
+ throw fail(`networks.${id}.rpcUrls must list at least one https RPC URL`);
41
+ }
42
+ out.set(id, Object.freeze({
43
+ id,
44
+ chainId,
45
+ escrow: escrow.address,
46
+ escrowCaip10: n.escrow,
47
+ asset: n.asset,
48
+ decimals: n.decimals,
49
+ rpc: Object.freeze([...rpc]),
50
+ }));
51
+ }
52
+ return out;
53
+ }
54
+ export function checkLimits(limits) {
55
+ const perCall = typeof limits?.maxPerCall === "string" ? usdToAtomic(limits.maxPerCall, 6) : null;
56
+ const perDay = typeof limits?.maxPerDay === "string" ? usdToAtomic(limits.maxPerDay, 6) : null;
57
+ if (perCall === null || perCall === 0n)
58
+ throw new ConfigError('limits.maxPerCall must be a positive dollar amount such as "0.50"');
59
+ if (perDay === null || perDay === 0n)
60
+ throw new ConfigError('limits.maxPerDay must be a positive dollar amount such as "5"');
61
+ if (perCall > perDay)
62
+ throw new ConfigError("limits.maxPerCall must not be above limits.maxPerDay");
63
+ return Object.freeze({ maxPerCall: limits.maxPerCall, maxPerDay: limits.maxPerDay });
64
+ }
65
+ function deepFreeze(value) {
66
+ if (typeof value === "object" && value !== null) {
67
+ for (const v of Object.values(value))
68
+ deepFreeze(v);
69
+ Object.freeze(value);
70
+ }
71
+ return value;
72
+ }
73
+ /**
74
+ * The one path from a requirement to a voucher: the guards and the builder see the same frozen
75
+ * copy, and the network the guards check is the caller's own entry for `requirement.network`.
76
+ * Returns the first refusal, or the voucher to sign.
77
+ */
78
+ export function guardAndBuild(ctx, issuedAt, networks) {
79
+ let requirement;
80
+ try {
81
+ requirement = deepFreeze(structuredClone(ctx.requirement));
82
+ }
83
+ catch {
84
+ return { ok: false, guard: "G1", code: "G1_NOT_A_REQUIREMENT", reason: "the payment request could not be copied" };
85
+ }
86
+ const id = isObject(requirement) && typeof requirement.network === "string" ? requirement.network : null;
87
+ const entry = id !== null ? networks.get(id) : undefined;
88
+ const network = entry
89
+ ? { id: entry.id, escrow: entry.escrowCaip10, asset: entry.asset, decimals: entry.decimals }
90
+ : null;
91
+ const result = checkAll({ ...ctx, requirement, network });
92
+ if (!result.ok)
93
+ return result;
94
+ const req = requirement;
95
+ try {
96
+ const built = buildVoucher(req, issuedAt, {
97
+ caip2: entry.id,
98
+ escrow: entry.escrowCaip10,
99
+ asset: entry.asset,
100
+ decimals: entry.decimals,
101
+ });
102
+ const m = built.typedData.message;
103
+ const same = m.amount === BigInt(req.amount.amount) &&
104
+ m.seq === BigInt(req.seq) &&
105
+ m.payer === getAddress(parseCaip10(req.payer).address) &&
106
+ m.provider === getAddress(parseCaip10(req.provider).address) &&
107
+ built.typedData.domain.verifyingContract === entry.escrow &&
108
+ built.typedData.domain.chainId === entry.chainId;
109
+ if (!same)
110
+ return {
111
+ ok: false,
112
+ guard: "G1",
113
+ code: "G1_NOT_A_REQUIREMENT",
114
+ reason: "the voucher differs from the checked requirement",
115
+ };
116
+ return { ok: true, ...built };
117
+ }
118
+ catch (err) {
119
+ if (err instanceof SignerRefusal)
120
+ return { ok: false, guard: "G1", code: "G1_NOT_A_REQUIREMENT", reason: err.message };
121
+ throw err;
122
+ }
123
+ }
124
+ const SPEND_WINDOW_SECONDS = 86_400;
125
+ /**
126
+ * Vouchers signed in this process, by key: every client using one key shares one 24 h cap. One
127
+ * array per key, only ever changed in place, so a reservation is always released from the list
128
+ * the guards read. Per process only: not shared across worker threads or restarts; the
129
+ * on-chain caps are the bound across processes.
130
+ */
131
+ const LEDGER = new Map();
132
+ function spentBy(address, nowSeconds) {
133
+ const key = address.toLowerCase();
134
+ let list = LEDGER.get(key);
135
+ if (list === undefined) {
136
+ list = [];
137
+ LEDGER.set(key, list);
138
+ }
139
+ for (let i = list.length - 1; i >= 0; i--)
140
+ if (list[i].at <= nowSeconds - SPEND_WINDOW_SECONDS)
141
+ list.splice(i, 1);
142
+ return list;
143
+ }
144
+ /** Tests only: forget every signed voucher of this process. */
145
+ export function resetSpendLedger() {
146
+ LEDGER.clear();
147
+ }
148
+ function accountOn(value, chainId) {
149
+ const parsed = typeof value === "string" ? parseCaip10(value) : null;
150
+ return parsed && parsed.chainId === chainId ? parsed.address : null;
151
+ }
152
+ export class Eip155Scheme {
153
+ o;
154
+ namespace = "eip155";
155
+ /** The payer of the first voucher this client signed (G4): a different one later is refused. */
156
+ #recordedPayer = null;
157
+ constructor(o) {
158
+ this.o = o;
159
+ }
160
+ async prepare(input) {
161
+ const signer = input.signer;
162
+ let localKey;
163
+ try {
164
+ localKey = getAddress(signer.address);
165
+ }
166
+ catch {
167
+ return { ok: false, guard: "G5", code: "G5_LOCAL_KEY_INVALID", reason: "the signer's address is not readable" };
168
+ }
169
+ const networks = this.o.networks;
170
+ const req = isObject(input.requirement) ? input.requirement : null;
171
+ const entry = req && typeof req.network === "string" ? networks.get(req.network) : undefined;
172
+ const payer = entry ? accountOn(req?.payer, entry.chainId) : null;
173
+ // Before any chain read, as the reference does: these refusals say what to do.
174
+ if (entry && payer === localKey) {
175
+ return {
176
+ ok: false,
177
+ guard: "G5",
178
+ code: "G5_LOCAL_KEY_IS_PAYER",
179
+ reason: "the signer is the payer; use a dedicated payment key",
180
+ };
181
+ }
182
+ if (entry && accountOn(req?.delegate, entry.chainId) !== localKey) {
183
+ return {
184
+ ok: false,
185
+ guard: "G5",
186
+ code: "G5_DELEGATE_MISMATCH",
187
+ reason: "this signer is not the payment key bound to this API key; call paidCalls.bind() and approve it",
188
+ };
189
+ }
190
+ const chain = entry && payer ? await this.o.chain.read(entry, payer, localKey) : null;
191
+ const quote = await input.quote(input.tool);
192
+ // No await from here to the spend record, so two calls in flight cannot both pass the cap.
193
+ const nowSeconds = Math.floor(this.o.now() / 1000);
194
+ const signed = spentBy(localKey, nowSeconds);
195
+ const ctx = {
196
+ requirement: input.requirement,
197
+ forwardedCall: { tool: input.tool, at: input.calledAt },
198
+ quote,
199
+ chain,
200
+ localKey,
201
+ recordedPayer: this.#recordedPayer,
202
+ limits: {
203
+ perCall: this.o.limits.maxPerCall,
204
+ perDay: this.o.limits.maxPerDay,
205
+ confirmAbove: null,
206
+ providers: null,
207
+ },
208
+ signed,
209
+ confirmation: null,
210
+ nowSeconds,
211
+ };
212
+ const built = guardAndBuild(ctx, correctedNow(nowSeconds, this.o.dateOffset()), networks);
213
+ if (!built.ok) {
214
+ if ((built.guard === "G4" || built.guard === "G6") && entry && payer)
215
+ this.o.chain.invalidate(entry.id, payer, localKey);
216
+ return built;
217
+ }
218
+ const r = req;
219
+ const providerId = r.providerId;
220
+ if (typeof providerId !== "string" || providerId.length === 0 || providerId.length > 256) {
221
+ return { ok: false, guard: "G1", code: "G1_NOT_A_REQUIREMENT", reason: "the payment request names no provider" };
222
+ }
223
+ const reserved = { at: nowSeconds, amount: built.typedData.message.amount.toString() };
224
+ signed.push(reserved);
225
+ let used = false;
226
+ return {
227
+ ok: true,
228
+ details: {
229
+ tool: input.tool,
230
+ network: r.network,
231
+ amount: { ...built.wire.amount },
232
+ provider: r.provider,
233
+ payer: r.payer,
234
+ idempotencyKey: input.idempotencyKey,
235
+ },
236
+ sign: async () => {
237
+ if (used)
238
+ throw new Error("a voucher is signed at most once");
239
+ used = true;
240
+ const signature = await signer.signTypedData(built.typedData);
241
+ if (typeof signature !== "string" || !/^0x[0-9a-fA-F]{130}$/.test(signature))
242
+ throw new Error("the signer did not return a 65-byte signature");
243
+ this.#recordedPayer ??= getAddress(parseCaip10(r.payer).address);
244
+ return voucherEnvelope(built.wire, signature, providerId, r.contractVersion);
245
+ },
246
+ release: () => {
247
+ const list = spentBy(localKey, Math.floor(this.o.now() / 1000));
248
+ const i = list.indexOf(reserved);
249
+ if (i !== -1)
250
+ list.splice(i, 1);
251
+ },
252
+ };
253
+ }
254
+ async onRefused(code, input) {
255
+ if (code !== "PAYMENT_VOUCHER_DELEGATE_INACTIVE")
256
+ return;
257
+ const req = isObject(input.requirement) ? input.requirement : null;
258
+ const networks = this.o.networks;
259
+ const entry = req && typeof req.network === "string" ? networks.get(req.network) : undefined;
260
+ const payer = entry ? accountOn(req?.payer, entry.chainId) : null;
261
+ if (entry && payer)
262
+ this.o.chain.invalidate(entry.id, payer, getAddress(input.signer.address));
263
+ }
264
+ }
@@ -0,0 +1,54 @@
1
+ import type { PriceQuote } from "../core/meta.js";
2
+ import type { Money } from "../core/voucher.js";
3
+ import type { Signer } from "../signer.js";
4
+ /** What a caller's `onPayment` hook sees before anything is signed. */
5
+ export interface PaymentDetails {
6
+ /** The tool being paid for. */
7
+ tool: string;
8
+ /** CAIP-2. */
9
+ network: string;
10
+ /** The exact amount, in atomic units of `amount.asset`. */
11
+ amount: Money;
12
+ /** CAIP-10 of the provider paid. */
13
+ provider: string;
14
+ /** CAIP-10 of the payer (the escrow owner). */
15
+ payer: string;
16
+ /** The idempotency key of this logical call. */
17
+ idempotencyKey: string;
18
+ }
19
+ export interface PrepareInput {
20
+ /** `_meta["x402/paymentRequired"]` as received. Untrusted. */
21
+ requirement: unknown;
22
+ /** The tool the call was for. */
23
+ tool: string;
24
+ /** When the unsigned attempt was sent, Unix seconds (local clock). */
25
+ calledAt: number;
26
+ signer: Signer;
27
+ idempotencyKey: string;
28
+ /** The quote for a tool (G3) on the calling client's URL: cached from list/search/get, or a fresh `get_tool`. */
29
+ quote: (tool: string) => Promise<PriceQuote | null>;
30
+ }
31
+ export type Prepared = {
32
+ ok: true;
33
+ details: PaymentDetails;
34
+ /** Signs the checked voucher; returns the `assay/voucher` envelope. Call at most once. */
35
+ sign(): Promise<Record<string, unknown>>;
36
+ /** Gives back the spend reserved for this voucher, when it is not signed after all. */
37
+ release(): void;
38
+ } | {
39
+ ok: false;
40
+ guard: string;
41
+ code: string;
42
+ reason: string;
43
+ };
44
+ /**
45
+ * One payment scheme per CAIP-2 namespace. `call()` only sees this interface, so a new chain is a
46
+ * new scheme module (plus its vectors and signer variant), with no change to `call()`.
47
+ */
48
+ export interface PaymentScheme {
49
+ readonly namespace: string;
50
+ /** Runs every local guard and builds the voucher. Nothing is signed here. */
51
+ prepare(input: PrepareInput): Promise<Prepared>;
52
+ /** Told about a server refusal of a signed voucher (for example a revoked key). */
53
+ onRefused(code: string, input: PrepareInput): Promise<void>;
54
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Signers. The SDK never holds, stores or creates a key: the caller passes a signer, and the SDK
3
+ * hands it only typed data the SDK built itself after every local check passed.
4
+ *
5
+ * `Signer` is a tagged union keyed by CAIP-2 namespace, so other chains can be added as new
6
+ * variants without changing the existing ones.
7
+ */
8
+ /** EIP-712 typed data, as viem and eth-account take it. Integers may be bigints. */
9
+ export interface TypedData {
10
+ domain: Record<string, unknown>;
11
+ types: Record<string, readonly {
12
+ name: string;
13
+ type: string;
14
+ }[]>;
15
+ primaryType: string;
16
+ message: Record<string, unknown>;
17
+ }
18
+ /** An EVM key: any object with an address and an EIP-712 signer (a viem account, a KMS, an HSM). */
19
+ export interface EvmSigner {
20
+ readonly namespace: "eip155";
21
+ /** The key's `0x` address. */
22
+ readonly address: string;
23
+ /** Signs EIP-712 typed data; returns the 65-byte `r || s || v` signature as `0x` hex. */
24
+ signTypedData(typedData: TypedData): Promise<string> | string;
25
+ }
26
+ /** A signer for one CAIP-2 namespace. Only `eip155` exists in this release. */
27
+ export type Signer = EvmSigner;
28
+ /** The parts of a viem `Account` (`privateKeyToAccount`, `toAccount`) that `evmSigner` uses. */
29
+ export interface ViemLikeAccount {
30
+ readonly address: string;
31
+ signTypedData(typedData: never): Promise<string>;
32
+ }
33
+ /**
34
+ * Wraps a viem account as an {@link EvmSigner}. The returned object has only the address and the
35
+ * signing function: no key, and nothing else of the account, is reachable through it.
36
+ */
37
+ export declare function evmSigner(account: ViemLikeAccount): EvmSigner;
38
+ /** The namespace a signer is for, or `null` when it is not a signer. */
39
+ export declare function signerNamespace(value: unknown): string | null;
package/dist/signer.js ADDED
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Signers. The SDK never holds, stores or creates a key: the caller passes a signer, and the SDK
3
+ * hands it only typed data the SDK built itself after every local check passed.
4
+ *
5
+ * `Signer` is a tagged union keyed by CAIP-2 namespace, so other chains can be added as new
6
+ * variants without changing the existing ones.
7
+ */
8
+ /**
9
+ * Wraps a viem account as an {@link EvmSigner}. The returned object has only the address and the
10
+ * signing function: no key, and nothing else of the account, is reachable through it.
11
+ */
12
+ export function evmSigner(account) {
13
+ const address = account.address;
14
+ return Object.freeze({
15
+ namespace: "eip155",
16
+ address,
17
+ signTypedData: (typedData) => account.signTypedData(typedData),
18
+ toJSON: () => ({ namespace: "eip155", address }),
19
+ });
20
+ }
21
+ /** The namespace a signer is for, or `null` when it is not a signer. */
22
+ export function signerNamespace(value) {
23
+ if (typeof value !== "object" || value === null)
24
+ return null;
25
+ const s = value;
26
+ return typeof s.namespace === "string" && typeof s.address === "string" && typeof s.signTypedData === "function"
27
+ ? s.namespace
28
+ : null;
29
+ }
@@ -0,0 +1,2 @@
1
+ /** This package's version, sent in the user agent. */
2
+ export declare const VERSION = "0.1.0";
@@ -0,0 +1,2 @@
1
+ /** This package's version, sent in the user agent. */
2
+ export const VERSION = "0.1.0";
package/package.json CHANGED
@@ -1,6 +1,69 @@
1
1
  {
2
2
  "name": "@goassay-ai/sdk",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Call tools through Assay over MCP, with free and paid calls signed by your own key.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/assay-ai/assay-sdk.git",
9
+ "directory": "node"
10
+ },
11
+ "homepage": "https://docs.goassay.ai/developers",
12
+ "bugs": {
13
+ "url": "https://github.com/assay-ai/assay-sdk/issues"
14
+ },
15
+ "type": "module",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/index.d.ts",
19
+ "default": "./dist/index.js"
20
+ },
21
+ "./package.json": "./package.json",
22
+ "./core": {
23
+ "types": "./dist/core/index.d.ts",
24
+ "default": "./dist/core/index.js"
25
+ }
26
+ },
27
+ "files": [
28
+ "dist",
29
+ "README.md",
30
+ "LICENSE"
31
+ ],
32
+ "sideEffects": false,
33
+ "engines": {
34
+ "node": ">=22"
35
+ },
36
+ "publishConfig": {
37
+ "access": "public",
38
+ "provenance": true
39
+ },
40
+ "scripts": {
41
+ "build": "tsc -p tsconfig.build.json",
42
+ "typecheck": "tsc --noEmit -p tsconfig.json",
43
+ "lint": "eslint . && prettier --check .",
44
+ "format": "prettier --write .",
45
+ "test": "vitest run"
46
+ },
47
+ "dependencies": {
48
+ "@noble/hashes": "1.8.0"
49
+ },
50
+ "devDependencies": {
51
+ "@eslint/js": "10.0.1",
52
+ "@types/node": "22.20.5",
53
+ "eslint": "10.12.0",
54
+ "globals": "17.13.0",
55
+ "prettier": "3.9.9",
56
+ "typescript": "6.0.3",
57
+ "typescript-eslint": "8.71.0",
58
+ "viem": "2.57.3",
59
+ "vitest": "5.0.3"
60
+ },
61
+ "peerDependencies": {
62
+ "viem": ">=2"
63
+ },
64
+ "peerDependenciesMeta": {
65
+ "viem": {
66
+ "optional": true
67
+ }
68
+ }
69
+ }