@bondedhq/sdk 0.1.3 → 0.1.5
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 +203 -5
- package/dist/ai-sdk.d.ts +19 -0
- package/dist/ai-sdk.js +46 -0
- package/dist/budget.d.ts +97 -0
- package/dist/budget.js +98 -0
- package/dist/errors.d.ts +32 -0
- package/dist/errors.js +36 -0
- package/dist/gate.d.ts +158 -0
- package/dist/gate.js +416 -0
- package/dist/index.d.ts +10 -1
- package/dist/index.js +10 -1
- package/dist/langchain.d.ts +19 -1
- package/dist/langchain.js +34 -0
- package/dist/mcp.d.ts +20 -0
- package/dist/mcp.js +30 -0
- package/dist/openai-agents.d.ts +33 -0
- package/dist/openai-agents.js +52 -0
- package/dist/payments.d.ts +227 -0
- package/dist/payments.js +474 -0
- package/dist/x402.d.ts +136 -0
- package/dist/x402.js +182 -0
- package/package.json +17 -3
package/dist/gate.d.ts
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { type GateInput, type GateLogProof, type GateRule, type PaymentAttempt, type SignedGateReceipt } from "@bondedhq/shared";
|
|
2
|
+
import { type Address, type Hex } from "viem";
|
|
3
|
+
import type { PaymentBreach } from "./payments.js";
|
|
4
|
+
import { type BudgetRequest, type PaymentBudget } from "./budget.js";
|
|
5
|
+
/** Bonded's hosted API. */
|
|
6
|
+
export declare const BONDED_API_URL = "https://api-production-ba50.up.railway.app";
|
|
7
|
+
/** A payment for the gate. Pass the same `requestId` to retry one: see `PaymentGate.pay`. */
|
|
8
|
+
export type GatePayment = PaymentAttempt & {
|
|
9
|
+
requestId?: string;
|
|
10
|
+
};
|
|
11
|
+
/** A vendor bank change for the gate. `ticket` names the finance ticket that verified it. */
|
|
12
|
+
export interface GateVendorChange {
|
|
13
|
+
vendorId: string;
|
|
14
|
+
account: string;
|
|
15
|
+
ticket?: string;
|
|
16
|
+
requestId?: string;
|
|
17
|
+
}
|
|
18
|
+
/** What `POST /payments/check` takes. */
|
|
19
|
+
export type GateCheck = (GatePayment & {
|
|
20
|
+
kind?: "invoice" | "refund";
|
|
21
|
+
}) | (GateVendorChange & {
|
|
22
|
+
kind: "bank_change";
|
|
23
|
+
});
|
|
24
|
+
/** A decision, as the gate's log keeps it. */
|
|
25
|
+
export interface GateDecision {
|
|
26
|
+
id: string;
|
|
27
|
+
kind: "invoice" | "refund" | "bank_change" | "card" | "payment" | "budget" | "budget_close";
|
|
28
|
+
decision: "pass" | "breach" | "frozen";
|
|
29
|
+
result: "allowed" | "blocked" | "refused";
|
|
30
|
+
allowed: boolean;
|
|
31
|
+
message: string;
|
|
32
|
+
mode: "monitor" | "enforce";
|
|
33
|
+
/** Every rule broken, most serious first. */
|
|
34
|
+
rules: GateRule[];
|
|
35
|
+
/** Each rule in plain words, in the same order. */
|
|
36
|
+
reasons: {
|
|
37
|
+
rule: GateRule;
|
|
38
|
+
words: string;
|
|
39
|
+
}[];
|
|
40
|
+
amountUsd: number;
|
|
41
|
+
lossUsd: number;
|
|
42
|
+
overpaidUsd: number;
|
|
43
|
+
payee: string;
|
|
44
|
+
vendorId: string | null;
|
|
45
|
+
reference: string;
|
|
46
|
+
requestId: string | null;
|
|
47
|
+
inputHash: Hex;
|
|
48
|
+
/** What the rules were given. */
|
|
49
|
+
input?: GateInput | null;
|
|
50
|
+
mandate: {
|
|
51
|
+
version: number;
|
|
52
|
+
hash: Hex;
|
|
53
|
+
};
|
|
54
|
+
createdAt: string;
|
|
55
|
+
/** Bonded's signed receipt. Anyone can check it at bondedhq.xyz/gate/verify. */
|
|
56
|
+
receipt: SignedGateReceipt;
|
|
57
|
+
}
|
|
58
|
+
/** The gate's answer to one check. */
|
|
59
|
+
export interface GateAnswer extends GateDecision {
|
|
60
|
+
frozen: boolean;
|
|
61
|
+
/** The same `requestId` was checked before: this is that first decision, unchanged. */
|
|
62
|
+
replayed: boolean;
|
|
63
|
+
}
|
|
64
|
+
/** Where a receipt sits in its day's log root, and where that root is on chain. */
|
|
65
|
+
export interface GateReceiptProof {
|
|
66
|
+
decisionId: string;
|
|
67
|
+
day: string;
|
|
68
|
+
dayNumber: number;
|
|
69
|
+
accountKey: Hex;
|
|
70
|
+
receiptHash: Hex;
|
|
71
|
+
root: Hex;
|
|
72
|
+
proof: GateLogProof;
|
|
73
|
+
posted: {
|
|
74
|
+
chainId: number;
|
|
75
|
+
contract: Address;
|
|
76
|
+
txHash: Hex | null;
|
|
77
|
+
postedAt: string;
|
|
78
|
+
} | null;
|
|
79
|
+
}
|
|
80
|
+
export interface PaymentGateOptions {
|
|
81
|
+
/** The gate account's API key (`bonded_gk_...`), from bondedhq.xyz/gate. Keep it server side. */
|
|
82
|
+
apiKey: string;
|
|
83
|
+
/** Default: Bonded's hosted API. */
|
|
84
|
+
apiUrl?: string;
|
|
85
|
+
/**
|
|
86
|
+
* The address receipts must be signed by. Default: the one `GET /gate/receipt-key` publishes,
|
|
87
|
+
* read once. Pin it so that a wrong API address can't vouch for itself.
|
|
88
|
+
*/
|
|
89
|
+
receiptKey?: Address;
|
|
90
|
+
/** Check each answer's signed receipt before acting on it (default true). */
|
|
91
|
+
verifyReceipts?: boolean;
|
|
92
|
+
/**
|
|
93
|
+
* Called with every breach before the gate acts on it: blocked in Enforce mode, let through in
|
|
94
|
+
* Monitor mode. If it throws, the payment is not made.
|
|
95
|
+
*/
|
|
96
|
+
onBreach?: (breach: PaymentBreach) => void | Promise<void>;
|
|
97
|
+
/** How long to wait for each try, in milliseconds (default 10,000). */
|
|
98
|
+
timeoutMs?: number;
|
|
99
|
+
fetch?: typeof fetch;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* The hosted payment gate: every payment goes to Bonded before it is made, and comes back allowed
|
|
103
|
+
* or blocked with a signed receipt. The rules are the ones the Arena rates agents against, the
|
|
104
|
+
* vendors and invoices are the gate account's (set up at bondedhq.xyz/gate), and the decisions are
|
|
105
|
+
* logged where the owner can see them and anyone can check them.
|
|
106
|
+
*
|
|
107
|
+
* It fails closed: if the gate can't be reached, or its answer doesn't check out, your pay function
|
|
108
|
+
* isn't called and `PaymentGateError` says why. Pass it as `guard` to `guardPayments` or a framework
|
|
109
|
+
* helper in place of a `PaymentGuard`. Like the guard, it only sees payments your code sends it:
|
|
110
|
+
* keep the unwrapped pay function out of the agent's reach.
|
|
111
|
+
*/
|
|
112
|
+
export declare class PaymentGate {
|
|
113
|
+
#private;
|
|
114
|
+
constructor(options: PaymentGateOptions);
|
|
115
|
+
/** Asks the gate about a payment or bank change. An allowed payment counts as made from here. */
|
|
116
|
+
check(check: GateCheck): Promise<GateAnswer>;
|
|
117
|
+
/**
|
|
118
|
+
* Checks a payment, then calls `pay` if the gate allows it. Throws `PaymentBlockedError`
|
|
119
|
+
* (Enforce), `PaymentFrozenError` or `PaymentGateError` without calling `pay`.
|
|
120
|
+
*
|
|
121
|
+
* The gate counts an allowed payment as made, so if `pay` fails and you try again, pass the same
|
|
122
|
+
* `requestId` both times: the gate gives the first decision back instead of a duplicate breach.
|
|
123
|
+
*/
|
|
124
|
+
pay<R>(attempt: GatePayment, pay: () => R | Promise<R>, kind?: "invoice" | "refund"): Promise<R>;
|
|
125
|
+
/** The same for a vendor bank change: `update` runs only if the gate allows the change. */
|
|
126
|
+
changeVendorAccount<R>(request: GateVendorChange, update: () => R | Promise<R>): Promise<R>;
|
|
127
|
+
/**
|
|
128
|
+
* Opens a budget: one check for many small payments to one payee (see `PaymentBudget`). The gate
|
|
129
|
+
* checks it against the general rules (the account needs invoice matching off), reserves the
|
|
130
|
+
* whole budget against the day's limit and signs a receipt whose input commits to the terms;
|
|
131
|
+
* this checks the receipt, then recomputes the input's hash and reruns its rules. Each `spend`
|
|
132
|
+
* is checked here with no network call; payments go to the gate in the background (every
|
|
133
|
+
* `flushMs`, 5 seconds by default, once `holdMs` has passed so a turned-down payment can be taken
|
|
134
|
+
* back) and on `close()`, whose receipt commits to every one. Throws `PaymentBlockedError`
|
|
135
|
+
* (Enforce), `PaymentFrozenError` or `PaymentGateError` without opening one.
|
|
136
|
+
*/
|
|
137
|
+
budget(request: BudgetRequest, options?: {
|
|
138
|
+
flushMs?: number;
|
|
139
|
+
holdMs?: number;
|
|
140
|
+
onError?: (error: unknown) => void;
|
|
141
|
+
}): Promise<PaymentBudget>;
|
|
142
|
+
/** The gate account's decisions, newest first. Pass `next` back as `before` for older ones. */
|
|
143
|
+
decisions(query?: {
|
|
144
|
+
limit?: number;
|
|
145
|
+
before?: string;
|
|
146
|
+
result?: GateDecision["result"];
|
|
147
|
+
}): Promise<{
|
|
148
|
+
items: GateDecision[];
|
|
149
|
+
next: string | null;
|
|
150
|
+
}>;
|
|
151
|
+
/** One decision with its receipt. */
|
|
152
|
+
decision(id: string): Promise<GateDecision>;
|
|
153
|
+
/** A receipt's proof in its day's log root. The gate has one once that UTC day has ended. */
|
|
154
|
+
proof(id: string): Promise<GateReceiptProof>;
|
|
155
|
+
}
|
|
156
|
+
/** The hosted payment gate: see `PaymentGate`. */
|
|
157
|
+
export declare function paymentGate(options: PaymentGateOptions): PaymentGate;
|
|
158
|
+
//# sourceMappingURL=gate.d.ts.map
|
package/dist/gate.js
ADDED
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
import { budgetCloseCents, evaluateGateInput, gateInputHash, normalAccount, verifyGateReceipt, } from "@bondedhq/shared";
|
|
2
|
+
import { isAddress } from "viem";
|
|
3
|
+
import { BondedError, PaymentBlockedError, PaymentFrozenError, PaymentGateError, } from "./errors.js";
|
|
4
|
+
import { BudgetBase } from "./budget.js";
|
|
5
|
+
/** Bonded's hosted API. */
|
|
6
|
+
export const BONDED_API_URL = "https://api-production-ba50.up.railway.app";
|
|
7
|
+
/** One retry of a check the gate may not have answered: its request id makes that safe. */
|
|
8
|
+
const TRIES = 2;
|
|
9
|
+
/**
|
|
10
|
+
* The hosted payment gate: every payment goes to Bonded before it is made, and comes back allowed
|
|
11
|
+
* or blocked with a signed receipt. The rules are the ones the Arena rates agents against, the
|
|
12
|
+
* vendors and invoices are the gate account's (set up at bondedhq.xyz/gate), and the decisions are
|
|
13
|
+
* logged where the owner can see them and anyone can check them.
|
|
14
|
+
*
|
|
15
|
+
* It fails closed: if the gate can't be reached, or its answer doesn't check out, your pay function
|
|
16
|
+
* isn't called and `PaymentGateError` says why. Pass it as `guard` to `guardPayments` or a framework
|
|
17
|
+
* helper in place of a `PaymentGuard`. Like the guard, it only sees payments your code sends it:
|
|
18
|
+
* keep the unwrapped pay function out of the agent's reach.
|
|
19
|
+
*/
|
|
20
|
+
export class PaymentGate {
|
|
21
|
+
#options;
|
|
22
|
+
#url;
|
|
23
|
+
#signer;
|
|
24
|
+
constructor(options) {
|
|
25
|
+
if (!options.apiKey)
|
|
26
|
+
throw new BondedError("INVALID_PAYMENT", "apiKey is required");
|
|
27
|
+
this.#options = options;
|
|
28
|
+
this.#url = (options.apiUrl ?? BONDED_API_URL).replace(/\/+$/, "");
|
|
29
|
+
}
|
|
30
|
+
/** Asks the gate about a payment or bank change. An allowed payment counts as made from here. */
|
|
31
|
+
async check(check) {
|
|
32
|
+
// Only what the API takes: it refuses unknown fields, and a caller's `toPayment` may add some.
|
|
33
|
+
const requestId = check.requestId ?? crypto.randomUUID();
|
|
34
|
+
const body = check.kind === "bank_change"
|
|
35
|
+
? {
|
|
36
|
+
kind: check.kind,
|
|
37
|
+
vendorId: check.vendorId,
|
|
38
|
+
account: check.account,
|
|
39
|
+
...(check.ticket === undefined ? {} : { ticket: check.ticket }),
|
|
40
|
+
requestId,
|
|
41
|
+
}
|
|
42
|
+
: {
|
|
43
|
+
kind: check.kind ?? "invoice",
|
|
44
|
+
account: check.account,
|
|
45
|
+
amountUsd: check.amountUsd,
|
|
46
|
+
...(check.invoiceReference?.trim() ? { invoiceReference: check.invoiceReference } : {}),
|
|
47
|
+
requestId,
|
|
48
|
+
};
|
|
49
|
+
let last;
|
|
50
|
+
for (let attempt = 0; attempt < TRIES; attempt++) {
|
|
51
|
+
try {
|
|
52
|
+
const answer = await this.#request("/payments/check", {
|
|
53
|
+
method: "POST",
|
|
54
|
+
body: JSON.stringify(body),
|
|
55
|
+
});
|
|
56
|
+
await this.#verify(answer, body);
|
|
57
|
+
return answer;
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
last = error;
|
|
61
|
+
// Only a check the gate may not have answered is worth asking again.
|
|
62
|
+
const unanswered = error instanceof PaymentGateError && (!error.status || error.status >= 500);
|
|
63
|
+
if (!unanswered || error.receipt)
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
throw last;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Checks a payment, then calls `pay` if the gate allows it. Throws `PaymentBlockedError`
|
|
71
|
+
* (Enforce), `PaymentFrozenError` or `PaymentGateError` without calling `pay`.
|
|
72
|
+
*
|
|
73
|
+
* The gate counts an allowed payment as made, so if `pay` fails and you try again, pass the same
|
|
74
|
+
* `requestId` both times: the gate gives the first decision back instead of a duplicate breach.
|
|
75
|
+
*/
|
|
76
|
+
async pay(attempt, pay, kind = "invoice") {
|
|
77
|
+
await this.#settle(await this.check({ ...attempt, kind }));
|
|
78
|
+
return pay();
|
|
79
|
+
}
|
|
80
|
+
/** The same for a vendor bank change: `update` runs only if the gate allows the change. */
|
|
81
|
+
async changeVendorAccount(request, update) {
|
|
82
|
+
await this.#settle(await this.check({ ...request, kind: "bank_change" }));
|
|
83
|
+
return update();
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Opens a budget: one check for many small payments to one payee (see `PaymentBudget`). The gate
|
|
87
|
+
* checks it against the general rules (the account needs invoice matching off), reserves the
|
|
88
|
+
* whole budget against the day's limit and signs a receipt whose input commits to the terms;
|
|
89
|
+
* this checks the receipt, then recomputes the input's hash and reruns its rules. Each `spend`
|
|
90
|
+
* is checked here with no network call; payments go to the gate in the background (every
|
|
91
|
+
* `flushMs`, 5 seconds by default, once `holdMs` has passed so a turned-down payment can be taken
|
|
92
|
+
* back) and on `close()`, whose receipt commits to every one. Throws `PaymentBlockedError`
|
|
93
|
+
* (Enforce), `PaymentFrozenError` or `PaymentGateError` without opening one.
|
|
94
|
+
*/
|
|
95
|
+
async budget(request, options = {}) {
|
|
96
|
+
const body = {
|
|
97
|
+
account: request.account,
|
|
98
|
+
amountUsd: request.amountUsd,
|
|
99
|
+
...(request.maxPaymentUsd === undefined ? {} : { maxPaymentUsd: request.maxPaymentUsd }),
|
|
100
|
+
...(request.ttlSeconds === undefined ? {} : { ttlSeconds: request.ttlSeconds }),
|
|
101
|
+
requestId: request.requestId ?? crypto.randomUUID(),
|
|
102
|
+
};
|
|
103
|
+
let answer;
|
|
104
|
+
let last;
|
|
105
|
+
for (let attempt = 0; attempt < TRIES && !answer; attempt++) {
|
|
106
|
+
try {
|
|
107
|
+
answer = await this.#request("/payments/budgets", {
|
|
108
|
+
method: "POST",
|
|
109
|
+
body: JSON.stringify(body),
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
last = error;
|
|
114
|
+
const unanswered = error instanceof PaymentGateError && (!error.status || error.status >= 500);
|
|
115
|
+
if (!unanswered)
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (!answer)
|
|
120
|
+
throw last;
|
|
121
|
+
const terms = await this.#verifyBudget(answer, body);
|
|
122
|
+
await this.#settle(answer);
|
|
123
|
+
return new HostedBudget(answer.id, terms, answer.mode, Date.now, this.#options.onBreach, {
|
|
124
|
+
flushMs: options.flushMs ?? 5_000,
|
|
125
|
+
holdMs: options.holdMs ?? 10_000,
|
|
126
|
+
onError: options.onError ?? (() => undefined),
|
|
127
|
+
send: (spends) => this.#request(`/payments/budgets/${encodeURIComponent(answer.id)}/spends`, {
|
|
128
|
+
method: "POST",
|
|
129
|
+
body: JSON.stringify({
|
|
130
|
+
spends: spends.map((s) => ({
|
|
131
|
+
id: s.id,
|
|
132
|
+
amountMicros: s.amountMicros,
|
|
133
|
+
reference: s.reference,
|
|
134
|
+
at: new Date(s.atMs).toISOString(),
|
|
135
|
+
})),
|
|
136
|
+
}),
|
|
137
|
+
}),
|
|
138
|
+
close: async () => {
|
|
139
|
+
const closed = await this.#request(`/payments/budgets/${encodeURIComponent(answer.id)}/close`, { method: "POST" });
|
|
140
|
+
await this.#verifyClose(closed, answer.id);
|
|
141
|
+
return closed;
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
/** The gate account's decisions, newest first. Pass `next` back as `before` for older ones. */
|
|
146
|
+
decisions(query = {}) {
|
|
147
|
+
const params = new URLSearchParams();
|
|
148
|
+
for (const [name, value] of Object.entries(query))
|
|
149
|
+
if (value !== undefined)
|
|
150
|
+
params.set(name, String(value));
|
|
151
|
+
return this.#request(`/payments/decisions${params.size ? `?${params}` : ""}`);
|
|
152
|
+
}
|
|
153
|
+
/** One decision with its receipt. */
|
|
154
|
+
decision(id) {
|
|
155
|
+
return this.#request(`/payments/decisions/${encodeURIComponent(id)}`);
|
|
156
|
+
}
|
|
157
|
+
/** A receipt's proof in its day's log root. The gate has one once that UTC day has ended. */
|
|
158
|
+
proof(id) {
|
|
159
|
+
return this.#request(`/payments/decisions/${encodeURIComponent(id)}/proof`);
|
|
160
|
+
}
|
|
161
|
+
/** Reports a breach, then stops the payment unless the gate allowed it. */
|
|
162
|
+
async #settle(answer) {
|
|
163
|
+
if (answer.decision === "frozen")
|
|
164
|
+
throw new PaymentFrozenError();
|
|
165
|
+
if (answer.decision === "breach")
|
|
166
|
+
await this.#options.onBreach?.(breachOf(answer));
|
|
167
|
+
if (!answer.allowed)
|
|
168
|
+
throw new PaymentBlockedError(breachOf(answer));
|
|
169
|
+
}
|
|
170
|
+
async #verify(answer, asked) {
|
|
171
|
+
if (this.#options.verifyReceipts === false)
|
|
172
|
+
return;
|
|
173
|
+
const wrong = (what) => new PaymentGateError(`the gate's answer can't be trusted: ${what}`, undefined, {
|
|
174
|
+
receipt: true,
|
|
175
|
+
});
|
|
176
|
+
const signed = answer.receipt;
|
|
177
|
+
if (!signed?.receipt)
|
|
178
|
+
throw wrong("it has no receipt");
|
|
179
|
+
const signer = await this.#receiptSigner();
|
|
180
|
+
if (!(await verifyGateReceipt(signed, signer).catch(() => false)))
|
|
181
|
+
throw wrong("its receipt isn't signed by Bonded's receipt key");
|
|
182
|
+
const r = signed.receipt;
|
|
183
|
+
const matches = r.decisionId === answer.id &&
|
|
184
|
+
r.decision === answer.decision &&
|
|
185
|
+
r.result === answer.result &&
|
|
186
|
+
answer.allowed === (r.result === "allowed") &&
|
|
187
|
+
r.rules.join() === answer.rules.join() &&
|
|
188
|
+
r.payee === normalAccount(asked.account) &&
|
|
189
|
+
(asked.kind === "bank_change"
|
|
190
|
+
? r.kind === "bank_change"
|
|
191
|
+
: // An account with invoice matching off answers an invoice check as kind "payment".
|
|
192
|
+
(r.kind === (asked.kind ?? "invoice") ||
|
|
193
|
+
(r.kind === "payment" && (asked.kind ?? "invoice") === "invoice")) &&
|
|
194
|
+
r.amountCents === Math.round(asked.amountUsd * 100) &&
|
|
195
|
+
r.reference === (asked.invoiceReference ?? "").trim());
|
|
196
|
+
if (!matches)
|
|
197
|
+
throw wrong("its receipt is for a different decision or payment");
|
|
198
|
+
}
|
|
199
|
+
/** The budget answer's receipt and input check out; returns the terms it grants. */
|
|
200
|
+
async #verifyBudget(answer, asked) {
|
|
201
|
+
const wrong = (what) => new PaymentGateError(`the gate's budget can't be trusted: ${what}`, undefined, {
|
|
202
|
+
receipt: true,
|
|
203
|
+
});
|
|
204
|
+
const signed = answer.receipt;
|
|
205
|
+
if (!signed?.receipt)
|
|
206
|
+
throw wrong("it has no receipt");
|
|
207
|
+
if (this.#options.verifyReceipts !== false) {
|
|
208
|
+
const signer = await this.#receiptSigner();
|
|
209
|
+
if (!(await verifyGateReceipt(signed, signer).catch(() => false)))
|
|
210
|
+
throw wrong("its receipt isn't signed by Bonded's receipt key");
|
|
211
|
+
}
|
|
212
|
+
const r = signed.receipt;
|
|
213
|
+
const input = answer.input;
|
|
214
|
+
if (r.kind !== "budget" ||
|
|
215
|
+
r.decisionId !== answer.id ||
|
|
216
|
+
r.decision !== answer.decision ||
|
|
217
|
+
r.result !== answer.result ||
|
|
218
|
+
answer.allowed !== (r.result === "allowed") ||
|
|
219
|
+
r.payee !== normalAccount(asked.account) ||
|
|
220
|
+
r.amountCents !== Math.round(asked.amountUsd * 100))
|
|
221
|
+
throw wrong("its receipt is for a different budget");
|
|
222
|
+
if (!input || input.kind !== "budget" || gateInputHash(input) !== r.inputHash)
|
|
223
|
+
throw wrong("its input doesn't match the receipt");
|
|
224
|
+
if (r.decision !== "frozen") {
|
|
225
|
+
const verdict = evaluateGateInput(input);
|
|
226
|
+
if (!verdict.ok || verdict.rules.join() !== r.rules.join())
|
|
227
|
+
throw wrong("its rules don't follow from its input");
|
|
228
|
+
}
|
|
229
|
+
const t = input.terms;
|
|
230
|
+
const ttlMs = (asked.ttlSeconds ?? 3600) * 1000;
|
|
231
|
+
const maxAsked = asked.maxPaymentUsd === undefined ? Infinity : Math.round(asked.maxPaymentUsd * 1e6);
|
|
232
|
+
if (t.account !== r.payee ||
|
|
233
|
+
t.budgetMicros !== r.amountCents * 10_000 ||
|
|
234
|
+
t.expiresAtMs - t.startsAtMs !== ttlMs ||
|
|
235
|
+
t.maxPaymentMicros > maxAsked)
|
|
236
|
+
throw wrong("its terms aren't the ones asked for");
|
|
237
|
+
return t;
|
|
238
|
+
}
|
|
239
|
+
/** A budget close's receipt checks out and commits to its input. */
|
|
240
|
+
async #verifyClose(answer, budgetId) {
|
|
241
|
+
if (this.#options.verifyReceipts === false)
|
|
242
|
+
return;
|
|
243
|
+
const wrong = (what) => new PaymentGateError(`the gate's budget close can't be trusted: ${what}`, undefined, {
|
|
244
|
+
receipt: true,
|
|
245
|
+
});
|
|
246
|
+
const signed = answer.receipt;
|
|
247
|
+
if (!signed?.receipt)
|
|
248
|
+
throw wrong("it has no receipt");
|
|
249
|
+
const signer = await this.#receiptSigner();
|
|
250
|
+
if (!(await verifyGateReceipt(signed, signer).catch(() => false)))
|
|
251
|
+
throw wrong("its receipt isn't signed by Bonded's receipt key");
|
|
252
|
+
const r = signed.receipt;
|
|
253
|
+
if (r.kind !== "budget_close" || r.reference !== budgetId || r.decisionId !== answer.id)
|
|
254
|
+
throw wrong("its receipt is for a different budget");
|
|
255
|
+
const input = answer.input;
|
|
256
|
+
// A close given back again has no input to check beyond its signed receipt.
|
|
257
|
+
if (!input)
|
|
258
|
+
return;
|
|
259
|
+
if (input.kind !== "budget_close" ||
|
|
260
|
+
input.budgetId !== budgetId ||
|
|
261
|
+
gateInputHash(input) !== r.inputHash)
|
|
262
|
+
throw wrong("its input doesn't match the receipt");
|
|
263
|
+
const verdict = evaluateGateInput(input);
|
|
264
|
+
if (!verdict.ok ||
|
|
265
|
+
verdict.rules.join() !== r.rules.join() ||
|
|
266
|
+
r.amountCents !== budgetCloseCents(input))
|
|
267
|
+
throw wrong("its rules or amount don't follow from its input");
|
|
268
|
+
}
|
|
269
|
+
#receiptSigner() {
|
|
270
|
+
if (this.#options.receiptKey)
|
|
271
|
+
return Promise.resolve(this.#options.receiptKey);
|
|
272
|
+
this.#signer ??= this.#request("/gate/receipt-key")
|
|
273
|
+
.then((key) => {
|
|
274
|
+
// Without an address verifyGateReceipt would accept any self-signed receipt.
|
|
275
|
+
if (!isAddress(key?.address))
|
|
276
|
+
throw new PaymentGateError("the gate's receipt key is not an address");
|
|
277
|
+
return key.address;
|
|
278
|
+
})
|
|
279
|
+
.catch((error) => {
|
|
280
|
+
this.#signer = undefined;
|
|
281
|
+
throw error;
|
|
282
|
+
});
|
|
283
|
+
return this.#signer;
|
|
284
|
+
}
|
|
285
|
+
async #request(path, init = {}) {
|
|
286
|
+
const url = `${this.#url}${path}`;
|
|
287
|
+
let response;
|
|
288
|
+
try {
|
|
289
|
+
response = await (this.#options.fetch ?? fetch)(url, {
|
|
290
|
+
...init,
|
|
291
|
+
headers: {
|
|
292
|
+
Authorization: `Bearer ${this.#options.apiKey}`,
|
|
293
|
+
...(init.body ? { "Content-Type": "application/json" } : {}),
|
|
294
|
+
},
|
|
295
|
+
signal: AbortSignal.timeout(this.#options.timeoutMs ?? 10_000),
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
catch (error) {
|
|
299
|
+
throw new PaymentGateError(`couldn't reach ${url}: ${error.message}`, undefined, {
|
|
300
|
+
cause: error,
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
const body = (await response.json().catch(() => undefined));
|
|
304
|
+
if (response.ok && body)
|
|
305
|
+
return body;
|
|
306
|
+
const message = typeof body?.error === "string" ? body.error : `unexpected answer from ${url}`;
|
|
307
|
+
if (response.status === 400)
|
|
308
|
+
throw new BondedError("INVALID_PAYMENT", message);
|
|
309
|
+
throw new PaymentGateError(message, response.status);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
/** The hosted payment gate: see `PaymentGate`. */
|
|
313
|
+
export function paymentGate(options) {
|
|
314
|
+
return new PaymentGate(options);
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* A budget the hosted gate opened. Spends wait `holdMs` (so one the payee turned down can be taken
|
|
318
|
+
* back), then go to the gate every `flushMs`; `close()` sends the rest and closes it.
|
|
319
|
+
*/
|
|
320
|
+
class HostedBudget extends BudgetBase {
|
|
321
|
+
link;
|
|
322
|
+
#pending = [];
|
|
323
|
+
/** Spends on their way to the gate: too late to take back. */
|
|
324
|
+
#inFlight = new Set();
|
|
325
|
+
#sending;
|
|
326
|
+
#timer;
|
|
327
|
+
#closing;
|
|
328
|
+
constructor(id, terms, mode, now, onBreach, link) {
|
|
329
|
+
super(id, terms, mode, now, onBreach);
|
|
330
|
+
this.link = link;
|
|
331
|
+
}
|
|
332
|
+
/** The close decision, once closed. */
|
|
333
|
+
closeDecision;
|
|
334
|
+
recorded(spend) {
|
|
335
|
+
this.#pending.push(spend);
|
|
336
|
+
if (!this.#timer) {
|
|
337
|
+
this.#timer = setInterval(() => {
|
|
338
|
+
this.#send(false).catch(this.link.onError);
|
|
339
|
+
}, this.link.flushMs);
|
|
340
|
+
this.#timer.unref?.();
|
|
341
|
+
}
|
|
342
|
+
return Promise.resolve();
|
|
343
|
+
}
|
|
344
|
+
releasable(spend) {
|
|
345
|
+
return this.#pending.includes(spend) && !this.#inFlight.has(spend);
|
|
346
|
+
}
|
|
347
|
+
released(spend) {
|
|
348
|
+
this.#pending = this.#pending.filter((s) => s !== spend);
|
|
349
|
+
}
|
|
350
|
+
flush() {
|
|
351
|
+
return this.#send(true);
|
|
352
|
+
}
|
|
353
|
+
close() {
|
|
354
|
+
this.#closing ??= (async () => {
|
|
355
|
+
this.markClosed();
|
|
356
|
+
if (this.#timer)
|
|
357
|
+
clearInterval(this.#timer);
|
|
358
|
+
while (this.#pending.length > 0 || this.#sending)
|
|
359
|
+
await this.#send(true);
|
|
360
|
+
this.closeDecision = await this.link.close();
|
|
361
|
+
})().catch((error) => {
|
|
362
|
+
// Nothing is lost: what wasn't sent is still pending, so closing again tries again.
|
|
363
|
+
this.#closing = undefined;
|
|
364
|
+
throw error;
|
|
365
|
+
});
|
|
366
|
+
return this.#closing;
|
|
367
|
+
}
|
|
368
|
+
/** Sends what's due (everything when `all`), 500 at a time, one send at a time. */
|
|
369
|
+
async #send(all) {
|
|
370
|
+
while (this.#sending)
|
|
371
|
+
await this.#sending;
|
|
372
|
+
const cutoff = this.now() - this.link.holdMs;
|
|
373
|
+
const due = this.#pending.filter((s) => all || s.atMs <= cutoff).slice(0, 500);
|
|
374
|
+
if (due.length === 0)
|
|
375
|
+
return;
|
|
376
|
+
for (const s of due)
|
|
377
|
+
this.#inFlight.add(s);
|
|
378
|
+
this.#sending = (async () => {
|
|
379
|
+
const answer = await this.link.send(due);
|
|
380
|
+
this.#pending = this.#pending.filter((s) => !due.includes(s));
|
|
381
|
+
if (answer.frozen)
|
|
382
|
+
this.markClosed();
|
|
383
|
+
})().finally(() => {
|
|
384
|
+
for (const s of due)
|
|
385
|
+
this.#inFlight.delete(s);
|
|
386
|
+
this.#sending = undefined;
|
|
387
|
+
});
|
|
388
|
+
await this.#sending;
|
|
389
|
+
if (all && this.#pending.length > 0)
|
|
390
|
+
await this.#send(true);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
function breachOf(answer) {
|
|
394
|
+
return {
|
|
395
|
+
kind: answer.kind === "bank_change"
|
|
396
|
+
? "vendor_change"
|
|
397
|
+
: answer.kind === "refund"
|
|
398
|
+
? "refund"
|
|
399
|
+
: answer.kind === "budget"
|
|
400
|
+
? "budget"
|
|
401
|
+
: "invoice",
|
|
402
|
+
rules: answer.rules,
|
|
403
|
+
reasons: answer.reasons.map((r) => (typeof r === "string" ? r : r.words)),
|
|
404
|
+
blocked: !answer.allowed,
|
|
405
|
+
account: answer.payee,
|
|
406
|
+
reference: answer.reference,
|
|
407
|
+
vendorId: answer.vendorId,
|
|
408
|
+
amountUsd: answer.amountUsd,
|
|
409
|
+
lossUsd: answer.lossUsd,
|
|
410
|
+
overpaidUsd: answer.overpaidUsd,
|
|
411
|
+
day: answer.createdAt.slice(0, 10),
|
|
412
|
+
decisionId: answer.id,
|
|
413
|
+
receipt: answer.receipt,
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
//# sourceMappingURL=gate.js.map
|
package/dist/index.d.ts
CHANGED
|
@@ -5,6 +5,12 @@
|
|
|
5
5
|
* `@bondedhq/sdk/agentkit`, `@bondedhq/sdk/solana-agent-kit`, `@bondedhq/sdk/eliza`,
|
|
6
6
|
* `@bondedhq/sdk/langchain`, `@bondedhq/sdk/ai-sdk`. The episode's tools over MCP are in
|
|
7
7
|
* `@bondedhq/sdk/mcp`, and the Agent URL handler in `@bondedhq/sdk/agent-url`.
|
|
8
|
+
*
|
|
9
|
+
* Off chain, `paymentGuard` and `guardPayments` check an agent's payments against the payment
|
|
10
|
+
* mandate inside your own process (no network, no receipts), and `paymentGate` asks Bonded's
|
|
11
|
+
* hosted gate instead. The framework subpaths wrap a pay tool with either, `@bondedhq/sdk/openai-agents`
|
|
12
|
+
* does the same for the OpenAI Agents SDK, and `@bondedhq/sdk/x402` checks x402 payments before
|
|
13
|
+
* the client signs them.
|
|
8
14
|
*/
|
|
9
15
|
export { BondedVault, type ActionResult, type BondedVaultConfig, type VaultStatus, } from "./vault.js";
|
|
10
16
|
export { demoRouter, describeAction, type ApproveParams, type CallParams, type SwapParams, type SwapRouter, type TransferParams, type VaultAction, type VaultCall, } from "./actions.js";
|
|
@@ -12,8 +18,11 @@ export { breachReason, type DryRunResult, type VaultBreach } from "./breaches.js
|
|
|
12
18
|
export { guarded, GuardedVault, type GuardCheck, type GuardContext, type GuardedResult, type GuardOptions, type SentinelGuardOptions, type TokenInfo, } from "./guarded.js";
|
|
13
19
|
export { checkWithSentinel, type MessageSigner, type SentinelCheckRequest, type SentinelInput, } from "./sentinel.js";
|
|
14
20
|
export { buildMandate, describeMandate, type BuiltMandate, type EvmMandate, type EvmMandateParams, type EvmMandateRules, } from "./mandate.js";
|
|
15
|
-
export { BlockedByMandateError, BondedError, CallRevertedError, NotAgentError, PredictedBreachError, SentinelError, SentinelRefusedError, VaultFrozenError, type SentinelVerdict, } from "./errors.js";
|
|
21
|
+
export { BlockedByMandateError, BondedError, CallRevertedError, NotAgentError, PaymentBlockedError, PaymentFrozenError, PaymentGateError, PredictedBreachError, SentinelError, SentinelRefusedError, VaultFrozenError, type SentinelVerdict, } from "./errors.js";
|
|
16
22
|
export type { BreachType } from "@bondedhq/shared";
|
|
23
|
+
export { BONDED_API_URL, PaymentGate, paymentGate, type GateAnswer, type GateCheck, type GateDecision, type GatePayment, type GateReceiptProof, type GateVendorChange, type PaymentGateOptions, } from "./gate.js";
|
|
24
|
+
export { guardPayments, guardVendorChanges, isPaymentRefusal, PaymentGuard, paymentGuard, type GuardPaymentsOptions, type GuardVendorChangesOptions, type PaymentBreach, type PaymentGuardOptions, type PaymentGuardState, type PaymentMandate, type PaymentSource, type PaymentVendor, type PaymentVerdict, } from "./payments.js";
|
|
25
|
+
export { describePaymentMandate, GATE_RULE_WORDS, GATE_RULES, PAYMENT_RULE_WORDS, PAYMENT_RULES, type GateRule, type PaidPayment, type PayableInvoice, type PayableRefund, type PaymentAttempt, type PaymentRule, type VerifiedChange, } from "@bondedhq/shared";
|
|
17
26
|
export { BONDED_VAULT_TOOLS, bondedVaultFromEnv, bondedVaultTools, type BondedVaultTool, type BondedVaultToolName, type BondedVaultToolsOptions, } from "./vault-tools.js";
|
|
18
27
|
export { bondedEpisodeEnv, episodeVault, fromBondedEnv, isBondedEpisode, type BondedEpisode, } from "./episode.js";
|
|
19
28
|
export { checkUnprotectedAllowances, deploymentTokens, unprotectedAllowancesWarning, type AllowanceCheck, type AllowanceFinding, type TokenRef, installMandateModule, type InstallMandateModuleInput, type ModuleAccountType, type ModuleInstallPlan, } from "./module.js";
|
package/dist/index.js
CHANGED
|
@@ -5,6 +5,12 @@
|
|
|
5
5
|
* `@bondedhq/sdk/agentkit`, `@bondedhq/sdk/solana-agent-kit`, `@bondedhq/sdk/eliza`,
|
|
6
6
|
* `@bondedhq/sdk/langchain`, `@bondedhq/sdk/ai-sdk`. The episode's tools over MCP are in
|
|
7
7
|
* `@bondedhq/sdk/mcp`, and the Agent URL handler in `@bondedhq/sdk/agent-url`.
|
|
8
|
+
*
|
|
9
|
+
* Off chain, `paymentGuard` and `guardPayments` check an agent's payments against the payment
|
|
10
|
+
* mandate inside your own process (no network, no receipts), and `paymentGate` asks Bonded's
|
|
11
|
+
* hosted gate instead. The framework subpaths wrap a pay tool with either, `@bondedhq/sdk/openai-agents`
|
|
12
|
+
* does the same for the OpenAI Agents SDK, and `@bondedhq/sdk/x402` checks x402 payments before
|
|
13
|
+
* the client signs them.
|
|
8
14
|
*/
|
|
9
15
|
export { BondedVault, } from "./vault.js";
|
|
10
16
|
export { demoRouter, describeAction, } from "./actions.js";
|
|
@@ -12,7 +18,10 @@ export { breachReason } from "./breaches.js";
|
|
|
12
18
|
export { guarded, GuardedVault, } from "./guarded.js";
|
|
13
19
|
export { checkWithSentinel, } from "./sentinel.js";
|
|
14
20
|
export { buildMandate, describeMandate, } from "./mandate.js";
|
|
15
|
-
export { BlockedByMandateError, BondedError, CallRevertedError, NotAgentError, PredictedBreachError, SentinelError, SentinelRefusedError, VaultFrozenError, } from "./errors.js";
|
|
21
|
+
export { BlockedByMandateError, BondedError, CallRevertedError, NotAgentError, PaymentBlockedError, PaymentFrozenError, PaymentGateError, PredictedBreachError, SentinelError, SentinelRefusedError, VaultFrozenError, } from "./errors.js";
|
|
22
|
+
export { BONDED_API_URL, PaymentGate, paymentGate, } from "./gate.js";
|
|
23
|
+
export { guardPayments, guardVendorChanges, isPaymentRefusal, PaymentGuard, paymentGuard, } from "./payments.js";
|
|
24
|
+
export { describePaymentMandate, GATE_RULE_WORDS, GATE_RULES, PAYMENT_RULE_WORDS, PAYMENT_RULES, } from "@bondedhq/shared";
|
|
16
25
|
export { BONDED_VAULT_TOOLS, bondedVaultFromEnv, bondedVaultTools, } from "./vault-tools.js";
|
|
17
26
|
export { bondedEpisodeEnv, episodeVault, fromBondedEnv, isBondedEpisode, } from "./episode.js";
|
|
18
27
|
export { checkUnprotectedAllowances, deploymentTokens, unprotectedAllowancesWarning, installMandateModule, } from "./module.js";
|
package/dist/langchain.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { DynamicStructuredTool } from "@langchain/core/tools";
|
|
1
|
+
import { DynamicStructuredTool, type StructuredToolInterface } from "@langchain/core/tools";
|
|
2
2
|
import { type EpisodeTools, type EpisodeToolsSource } from "./mcp.js";
|
|
3
|
+
import { type GuardPaymentsOptions } from "./payments.js";
|
|
3
4
|
import { type BondedVaultToolsOptions } from "./vault-tools.js";
|
|
4
5
|
export type { BondedVaultToolsOptions } from "./vault-tools.js";
|
|
6
|
+
export type { GuardPaymentsOptions } from "./payments.js";
|
|
5
7
|
/**
|
|
6
8
|
* The episode's tools as LangChain `StructuredTool`s, for LangChain and LangGraph agents
|
|
7
9
|
* (`createReactAgent`, `ToolNode`, `model.bindTools`). Each takes the tool's own JSON Schema and
|
|
@@ -36,4 +38,20 @@ export declare function bondedLangChainTools(from?: EpisodeTools | EpisodeToolsS
|
|
|
36
38
|
* ```
|
|
37
39
|
*/
|
|
38
40
|
export declare function bondedVaultLangChainTools(options: BondedVaultToolsOptions): DynamicStructuredTool[];
|
|
41
|
+
/**
|
|
42
|
+
* Your own LangChain pay tool, with every call checked against the payment mandate first (see
|
|
43
|
+
* `PaymentGuard`). Same name, description and schema. When the guard refuses (Enforce mode
|
|
44
|
+
* blocked it, the guard is frozen, or it can't read the payment) your tool is not called and the
|
|
45
|
+
* model gets `Error: ...` naming the rules broken, so it can react. In Monitor mode a breaking
|
|
46
|
+
* payment goes through and the guard's `onBreach` reports it. A local check: no receipts.
|
|
47
|
+
*
|
|
48
|
+
* ```ts
|
|
49
|
+
* const guard = paymentGuard({ mandate, vendors, invoices, onBreach: alert });
|
|
50
|
+
* const tools = [...readOnlyTools, guardLangChainPayTool(payInvoice, { guard })];
|
|
51
|
+
* ```
|
|
52
|
+
*
|
|
53
|
+
* The payment is read from the tool's arguments: `{ account, amountUsd, invoiceReference }` unless
|
|
54
|
+
* you pass `toPayment`.
|
|
55
|
+
*/
|
|
56
|
+
export declare function guardLangChainPayTool(payTool: StructuredToolInterface, options: GuardPaymentsOptions<Record<string, unknown>>): DynamicStructuredTool;
|
|
39
57
|
//# sourceMappingURL=langchain.d.ts.map
|