@bondedhq/sdk 0.1.3 → 0.1.4
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 +119 -5
- package/dist/ai-sdk.d.ts +19 -0
- package/dist/ai-sdk.js +46 -0
- package/dist/errors.d.ts +32 -0
- package/dist/errors.js +36 -0
- package/dist/gate.d.ts +139 -0
- package/dist/gate.js +204 -0
- package/dist/index.d.ts +8 -1
- package/dist/index.js +8 -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/payments.d.ts +194 -0
- package/dist/payments.js +275 -0
- package/package.json +4 -4
package/dist/payments.js
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import { checkInvoicePayment, checkRefundPayment, isVerifiedChange, normalAccount, PAYMENT_RULE_WORDS, utcDay, } from "@bondedhq/shared";
|
|
2
|
+
import { BondedError, PaymentBlockedError, PaymentFrozenError, PaymentGateError, } from "./errors.js";
|
|
3
|
+
const read = async (source) => typeof source === "function" ? await source() : (source ?? []);
|
|
4
|
+
const copy = (state) => ({
|
|
5
|
+
payments: state.payments.map((p) => ({ ...p })),
|
|
6
|
+
frozen: state.frozen,
|
|
7
|
+
unverifiedChanges: state.unverifiedChanges.map((c) => ({ ...c })),
|
|
8
|
+
});
|
|
9
|
+
/**
|
|
10
|
+
* The payment mandate, checked inside your own process: the same rules (`@bondedhq/shared`'s
|
|
11
|
+
* payment mandate) the Arena rates agents against, with no network and no model. You give it the
|
|
12
|
+
* mandate and your vendor master, approved invoices and refunds, and verified bank changes; it
|
|
13
|
+
* keeps the payments made and the frozen flag, and hands them to `onStateChange` to persist.
|
|
14
|
+
*
|
|
15
|
+
* Wrap your pay function with `guardPayments` (or a framework helper) so every payment is checked
|
|
16
|
+
* first. In Enforce mode a payment that breaks a rule throws `PaymentBlockedError` and your pay
|
|
17
|
+
* function is never called; in Monitor mode it goes through and `onBreach` reports it. A frozen
|
|
18
|
+
* guard refuses everything (`PaymentFrozenError`).
|
|
19
|
+
*
|
|
20
|
+
* It is a local check: the agent's code can still call the pay function without it, and it makes
|
|
21
|
+
* no receipts. Payments are checked and made one at a time, so two at once can't both pass the
|
|
22
|
+
* duplicate or daily checks.
|
|
23
|
+
*/
|
|
24
|
+
export class PaymentGuard {
|
|
25
|
+
mandate;
|
|
26
|
+
#options;
|
|
27
|
+
#state;
|
|
28
|
+
#queue = Promise.resolve();
|
|
29
|
+
#now;
|
|
30
|
+
constructor(options) {
|
|
31
|
+
this.mandate = Object.freeze({ ...options.mandate });
|
|
32
|
+
this.#options = options;
|
|
33
|
+
this.#state = copy(options.state ?? { payments: [], frozen: false, unverifiedChanges: [] });
|
|
34
|
+
this.#now = options.now ?? (() => new Date());
|
|
35
|
+
}
|
|
36
|
+
get frozen() {
|
|
37
|
+
return this.#state.frozen;
|
|
38
|
+
}
|
|
39
|
+
/** A copy of what the guard remembers, to persist or inspect. */
|
|
40
|
+
state() {
|
|
41
|
+
return copy(this.#state);
|
|
42
|
+
}
|
|
43
|
+
/** Lets payments through again after a freeze. For a person who reviewed the breach. */
|
|
44
|
+
unfreeze() {
|
|
45
|
+
return this.#exclusive(async () => {
|
|
46
|
+
if (!this.#state.frozen)
|
|
47
|
+
return;
|
|
48
|
+
this.#state.frozen = false;
|
|
49
|
+
await this.#save();
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
/** The rule verdict for a payment, without making or recording it. */
|
|
53
|
+
async check(attempt, kind = "invoice") {
|
|
54
|
+
const check = await this.#check(attempt, kind, utcDay(this.#now()));
|
|
55
|
+
const { rules, amountUsd, reference, vendorId, lossUsd, overpaidUsd } = check;
|
|
56
|
+
return {
|
|
57
|
+
rules,
|
|
58
|
+
reasons: words(rules),
|
|
59
|
+
amountUsd,
|
|
60
|
+
reference,
|
|
61
|
+
vendorId,
|
|
62
|
+
lossUsd,
|
|
63
|
+
overpaidUsd,
|
|
64
|
+
frozen: this.frozen,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Checks a payment, then calls `pay` if the mandate allows it (or Monitor mode lets it through)
|
|
69
|
+
* and records it once `pay` returns. Throws `PaymentBlockedError` (Enforce), `PaymentFrozenError`
|
|
70
|
+
* or `BondedError` `INVALID_PAYMENT` without calling `pay`. A `pay` that throws records nothing.
|
|
71
|
+
*/
|
|
72
|
+
pay(attempt, pay, kind = "invoice") {
|
|
73
|
+
return this.#exclusive(async () => {
|
|
74
|
+
if (this.#state.frozen)
|
|
75
|
+
throw new PaymentFrozenError();
|
|
76
|
+
const day = utcDay(this.#now());
|
|
77
|
+
const check = await this.#check(attempt, kind, day);
|
|
78
|
+
if (check.rules.length > 0) {
|
|
79
|
+
await this.#breach({
|
|
80
|
+
kind,
|
|
81
|
+
rules: check.rules,
|
|
82
|
+
reasons: words(check.rules),
|
|
83
|
+
blocked: this.mandate.mode === "enforce",
|
|
84
|
+
account: attempt.account,
|
|
85
|
+
reference: check.reference,
|
|
86
|
+
vendorId: check.vendorId,
|
|
87
|
+
amountUsd: check.amountUsd,
|
|
88
|
+
lossUsd: check.lossUsd,
|
|
89
|
+
overpaidUsd: check.overpaidUsd,
|
|
90
|
+
day,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
const result = await pay();
|
|
94
|
+
this.#state.payments.push({
|
|
95
|
+
account: attempt.account,
|
|
96
|
+
// A refund settles no supplier invoice, as in the Arena's ledger.
|
|
97
|
+
vendorId: kind === "refund" ? null : check.vendorId,
|
|
98
|
+
amountUsd: check.amountUsd,
|
|
99
|
+
invoiceReference: check.reference,
|
|
100
|
+
day,
|
|
101
|
+
...(kind === "refund" ? { kind } : {}),
|
|
102
|
+
});
|
|
103
|
+
await this.#save();
|
|
104
|
+
return result;
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Checks a vendor bank change, then calls `update` if a verified ticket covers that vendor and
|
|
109
|
+
* account (or Monitor mode lets it through). An unverified change breaks
|
|
110
|
+
* `unverified_vendor_change`; let through in Monitor mode, its account still never counts as
|
|
111
|
+
* the vendor's.
|
|
112
|
+
*/
|
|
113
|
+
changeVendorAccount(request, update) {
|
|
114
|
+
return this.#exclusive(async () => {
|
|
115
|
+
if (this.#state.frozen)
|
|
116
|
+
throw new PaymentFrozenError();
|
|
117
|
+
if (!normalAccount(request.account))
|
|
118
|
+
throw new BondedError("INVALID_PAYMENT", "account is required");
|
|
119
|
+
// Already a known payee for this vendor (its account on file, or a ticket's): no change to
|
|
120
|
+
// verify. Marking it unverified would make the vendor's own account unpayable.
|
|
121
|
+
if (!isVerifiedChange(await this.#payees(), request)) {
|
|
122
|
+
const rules = ["unverified_vendor_change"];
|
|
123
|
+
await this.#breach({
|
|
124
|
+
kind: "vendor_change",
|
|
125
|
+
rules,
|
|
126
|
+
reasons: words(rules),
|
|
127
|
+
blocked: this.mandate.mode === "enforce",
|
|
128
|
+
account: request.account,
|
|
129
|
+
reference: "",
|
|
130
|
+
vendorId: request.vendorId,
|
|
131
|
+
amountUsd: 0,
|
|
132
|
+
lossUsd: 0,
|
|
133
|
+
overpaidUsd: 0,
|
|
134
|
+
day: utcDay(this.#now()),
|
|
135
|
+
});
|
|
136
|
+
this.#state.unverifiedChanges.push({
|
|
137
|
+
vendorId: request.vendorId,
|
|
138
|
+
account: request.account,
|
|
139
|
+
});
|
|
140
|
+
await this.#save();
|
|
141
|
+
}
|
|
142
|
+
return update();
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
/** Runs the shared rule check against the builder's data and the guard's payments. */
|
|
146
|
+
async #check(attempt, kind, day) {
|
|
147
|
+
const payees = await this.#payees();
|
|
148
|
+
const limits = this.mandate;
|
|
149
|
+
const payments = this.#state.payments;
|
|
150
|
+
const check = kind === "refund"
|
|
151
|
+
? checkRefundPayment({ limits, payees, refunds: await read(this.#options.refunds), payments, day }, attempt)
|
|
152
|
+
: checkInvoicePayment({ limits, payees, invoices: await read(this.#options.invoices), payments, day }, attempt);
|
|
153
|
+
if (!check.ok)
|
|
154
|
+
throw new BondedError("INVALID_PAYMENT", check.error);
|
|
155
|
+
return check;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Known payees: each vendor's account on file, unless it got there through an unverified
|
|
159
|
+
* change, and every account a verified ticket covers.
|
|
160
|
+
*/
|
|
161
|
+
async #payees() {
|
|
162
|
+
const unverified = this.#state.unverifiedChanges;
|
|
163
|
+
const onFile = (await read(this.#options.vendors))
|
|
164
|
+
.map((v) => ({ vendorId: v.id, account: v.account }))
|
|
165
|
+
.filter((p) => !unverified.some((u) => u.vendorId === p.vendorId && normalAccount(u.account) === normalAccount(p.account)));
|
|
166
|
+
const ticketed = (await read(this.#options.changes)).map((c) => ({
|
|
167
|
+
vendorId: c.vendorId,
|
|
168
|
+
account: c.account,
|
|
169
|
+
}));
|
|
170
|
+
return [...onFile, ...ticketed];
|
|
171
|
+
}
|
|
172
|
+
/** Reports a breach, freezes if the mandate says so, and throws when Enforce mode blocks it. */
|
|
173
|
+
async #breach(breach) {
|
|
174
|
+
if (this.mandate.freezeOnBreach) {
|
|
175
|
+
this.#state.frozen = true;
|
|
176
|
+
await this.#save();
|
|
177
|
+
}
|
|
178
|
+
await this.#options.onBreach?.({
|
|
179
|
+
...breach,
|
|
180
|
+
rules: [...breach.rules],
|
|
181
|
+
reasons: [...breach.reasons],
|
|
182
|
+
});
|
|
183
|
+
if (breach.blocked)
|
|
184
|
+
throw new PaymentBlockedError(breach);
|
|
185
|
+
}
|
|
186
|
+
async #save() {
|
|
187
|
+
await this.#options.onStateChange?.(this.state());
|
|
188
|
+
}
|
|
189
|
+
/** One check-and-pay at a time, so the duplicate and daily checks see every payment. */
|
|
190
|
+
#exclusive(run) {
|
|
191
|
+
const next = this.#queue.then(run, run);
|
|
192
|
+
this.#queue = next.catch(() => undefined);
|
|
193
|
+
return next;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
const words = (rules) => rules.map((r) => PAYMENT_RULE_WORDS[r]);
|
|
197
|
+
/** A payment guard: see `PaymentGuard`. */
|
|
198
|
+
export function paymentGuard(options) {
|
|
199
|
+
return new PaymentGuard(options);
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Wraps your own pay function so every call is checked against the payment mandate first. The
|
|
203
|
+
* wrapped function takes the same arguments and returns what yours returns. In Enforce mode a
|
|
204
|
+
* payment that breaks a rule throws `PaymentBlockedError` and your function is not called; in
|
|
205
|
+
* Monitor mode it is called and the guard's `onBreach` reports the breach. A frozen guard throws
|
|
206
|
+
* `PaymentFrozenError`.
|
|
207
|
+
*
|
|
208
|
+
* ```ts
|
|
209
|
+
* const guard = paymentGuard({ mandate, vendors, invoices, onBreach: alert });
|
|
210
|
+
* const pay = guardPayments(bank.pay, { guard });
|
|
211
|
+
* await pay({ account: "DE89 3704 0044 0532 0130 00", amountUsd: 1840.5, invoiceReference: "ACM-2291" });
|
|
212
|
+
* ```
|
|
213
|
+
*
|
|
214
|
+
* A local check: it makes no receipts, and code that calls your function directly skips it.
|
|
215
|
+
*/
|
|
216
|
+
export function guardPayments(pay, options) {
|
|
217
|
+
const toPayment = options.toPayment ?? defaultPayment;
|
|
218
|
+
return async (input, ...rest) => options.guard.pay(toPayment(input), () => pay(input, ...rest), options.kind ?? "invoice");
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Wraps your own "change a vendor's bank account" function: a change no verified ticket covers
|
|
222
|
+
* breaks `unverified_vendor_change`. Enforce mode throws `PaymentBlockedError` without calling
|
|
223
|
+
* it; Monitor mode calls it, reports the breach, and never treats the new account as the vendor's.
|
|
224
|
+
*/
|
|
225
|
+
export function guardVendorChanges(update, options) {
|
|
226
|
+
const toChange = options.toChange ?? defaultChange;
|
|
227
|
+
return async (input, ...rest) => options.guard.changeVendorAccount(toChange(input), () => update(input, ...rest));
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* The payment guard or gate refused (blocked, frozen, arguments it can't check, or the gate gave no
|
|
231
|
+
* answer to act on): no money moved.
|
|
232
|
+
*/
|
|
233
|
+
export function isPaymentRefusal(error) {
|
|
234
|
+
return (error instanceof PaymentBlockedError ||
|
|
235
|
+
error instanceof PaymentFrozenError ||
|
|
236
|
+
error instanceof PaymentGateError ||
|
|
237
|
+
(error instanceof BondedError && error.code === "INVALID_PAYMENT"));
|
|
238
|
+
}
|
|
239
|
+
const field = (input, name) => {
|
|
240
|
+
const value = input[name];
|
|
241
|
+
if (typeof value !== "string")
|
|
242
|
+
throw new BondedError("INVALID_PAYMENT", `${name} must be a string`);
|
|
243
|
+
return value;
|
|
244
|
+
};
|
|
245
|
+
const asObject = (input) => {
|
|
246
|
+
if (!input || typeof input !== "object")
|
|
247
|
+
throw new BondedError("INVALID_PAYMENT", "the payment's arguments must be an object");
|
|
248
|
+
return input;
|
|
249
|
+
};
|
|
250
|
+
function defaultPayment(input) {
|
|
251
|
+
const args = asObject(input);
|
|
252
|
+
const amount = args.amountUsd;
|
|
253
|
+
const amountUsd = typeof amount === "number"
|
|
254
|
+
? amount
|
|
255
|
+
: typeof amount === "string" && /^\d+(\.\d+)?$/.test(amount.trim())
|
|
256
|
+
? Number(amount)
|
|
257
|
+
: NaN;
|
|
258
|
+
if (Number.isNaN(amountUsd))
|
|
259
|
+
throw new BondedError("INVALID_PAYMENT", "amountUsd must be a number of US dollars");
|
|
260
|
+
return {
|
|
261
|
+
account: field(args, "account"),
|
|
262
|
+
amountUsd,
|
|
263
|
+
invoiceReference: field(args, "invoiceReference" in args ? "invoiceReference" : "reference"),
|
|
264
|
+
...(typeof args.requestId === "string" ? { requestId: args.requestId } : {}),
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
function defaultChange(input) {
|
|
268
|
+
const args = asObject(input);
|
|
269
|
+
return {
|
|
270
|
+
vendorId: field(args, "vendorId"),
|
|
271
|
+
account: field(args, "account"),
|
|
272
|
+
...(typeof args.ticket === "string" ? { ticket: args.ticket } : {}),
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
//# sourceMappingURL=payments.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bondedhq/sdk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Trade through a Bonded Mandate Vault and get rated by the Bonded Arena from any TypeScript agent: a typed vault client with dry runs, Sentinel checks and a mandate builder; the Arena episode's tools for LangChain, the Vercel AI SDK, ElizaOS and Coinbase AgentKit; and an Agent URL handler.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
"@modelcontextprotocol/sdk": "1.30.1",
|
|
84
84
|
"viem": "^2.56.9",
|
|
85
85
|
"zod": "^4.6.5",
|
|
86
|
-
"@bondedhq/shared": "^0.1.
|
|
86
|
+
"@bondedhq/shared": "^0.1.4"
|
|
87
87
|
},
|
|
88
88
|
"devDependencies": {
|
|
89
89
|
"@coinbase/agentkit": "0.10.4",
|
|
@@ -93,8 +93,8 @@
|
|
|
93
93
|
"@solana/web3.js": "^1.98.4",
|
|
94
94
|
"ai": "7.0.114",
|
|
95
95
|
"solana-agent-kit": "2.0.10",
|
|
96
|
-
"@
|
|
97
|
-
"@
|
|
96
|
+
"@bonded/sandbox": "0.1.0",
|
|
97
|
+
"@bondedhq/solana": "0.1.2"
|
|
98
98
|
},
|
|
99
99
|
"peerDependencies": {
|
|
100
100
|
"@coinbase/agentkit": "^0.10.4",
|