@belticlabs/agent-risk-sdk 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/dist/ai/index.d.ts +19 -0
- package/dist/ai/index.js +120 -0
- package/dist/chunk-5TWO73OD.js +35 -0
- package/dist/chunk-7G5EHNVW.js +17 -0
- package/dist/chunk-FQDHFTVR.js +29 -0
- package/dist/chunk-GCKCAKHA.js +401 -0
- package/dist/chunk-SFGM7KOG.js +312 -0
- package/dist/chunk-U5Z5Z2BQ.js +86 -0
- package/dist/chunk-VM7MK43J.js +13 -0
- package/dist/chunk-YVMJ5CZX.js +95 -0
- package/dist/client-CVx9LgJC.d.ts +63 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.js +499 -0
- package/dist/mcp/index.d.ts +59 -0
- package/dist/mcp/index.js +127 -0
- package/dist/middleware-_DSwvNIx.d.ts +40 -0
- package/dist/seller/anti-fraud-gateway.d.ts +32 -0
- package/dist/seller/anti-fraud-gateway.js +39 -0
- package/dist/session-BMNB1N1g.d.ts +184 -0
- package/dist/x402/express.d.ts +14 -0
- package/dist/x402/express.js +26 -0
- package/dist/x402/hono.d.ts +14 -0
- package/dist/x402/hono.js +20 -0
- package/dist/x402/index.d.ts +54 -0
- package/dist/x402/index.js +67 -0
- package/package.json +106 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
import {
|
|
2
|
+
recordCall
|
|
3
|
+
} from "./chunk-5TWO73OD.js";
|
|
4
|
+
import {
|
|
5
|
+
Chain,
|
|
6
|
+
didKeyFromEd25519,
|
|
7
|
+
fromHex,
|
|
8
|
+
memorySigner,
|
|
9
|
+
toHex
|
|
10
|
+
} from "./chunk-SFGM7KOG.js";
|
|
11
|
+
import {
|
|
12
|
+
presentedFrom,
|
|
13
|
+
summaryOf
|
|
14
|
+
} from "./chunk-7G5EHNVW.js";
|
|
15
|
+
import "./chunk-GCKCAKHA.js";
|
|
16
|
+
|
|
17
|
+
// src/core/api-client.ts
|
|
18
|
+
var BelticApiError = class extends Error {
|
|
19
|
+
constructor(status, code, message, details, requestId) {
|
|
20
|
+
super(message);
|
|
21
|
+
this.status = status;
|
|
22
|
+
this.code = code;
|
|
23
|
+
this.details = details;
|
|
24
|
+
this.requestId = requestId;
|
|
25
|
+
this.name = "BelticApiError";
|
|
26
|
+
}
|
|
27
|
+
/** 5xx and network failures are retried by the transport; 4xx are not. */
|
|
28
|
+
get retryable() {
|
|
29
|
+
return this.status === 0 || this.status >= 500 || this.status === 429;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
var ApiClient = class {
|
|
33
|
+
baseUrl;
|
|
34
|
+
fetchImpl;
|
|
35
|
+
timeoutMs;
|
|
36
|
+
headers;
|
|
37
|
+
constructor(opts) {
|
|
38
|
+
this.baseUrl = opts.baseUrl.replace(/\/+$/, "");
|
|
39
|
+
this.fetchImpl = opts.fetch ?? globalThis.fetch.bind(globalThis);
|
|
40
|
+
this.timeoutMs = opts.timeoutMs ?? 1e4;
|
|
41
|
+
this.headers = {
|
|
42
|
+
authorization: `Bearer ${opts.apiKey}`,
|
|
43
|
+
"content-type": "application/json",
|
|
44
|
+
"user-agent": opts.userAgent ?? "@belticlabs/agent-risk-sdk"
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
post(path, body, headers = {}) {
|
|
48
|
+
return this.request("POST", path, JSON.stringify(body), headers);
|
|
49
|
+
}
|
|
50
|
+
get(path, query = {}) {
|
|
51
|
+
const qs = Object.entries(query).filter((e) => e[1] !== void 0).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("&");
|
|
52
|
+
return this.request("GET", qs ? `${path}?${qs}` : path, void 0, {});
|
|
53
|
+
}
|
|
54
|
+
async request(method, path, body, extra) {
|
|
55
|
+
const controller = new AbortController();
|
|
56
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
57
|
+
let res;
|
|
58
|
+
try {
|
|
59
|
+
res = await this.fetchImpl(`${this.baseUrl}${path}`, {
|
|
60
|
+
method,
|
|
61
|
+
headers: { ...this.headers, ...extra },
|
|
62
|
+
...body !== void 0 ? { body } : {},
|
|
63
|
+
signal: controller.signal
|
|
64
|
+
});
|
|
65
|
+
} catch (err) {
|
|
66
|
+
throw new BelticApiError(
|
|
67
|
+
0,
|
|
68
|
+
"NETWORK",
|
|
69
|
+
`request to ${path} failed: ${err.message}`
|
|
70
|
+
);
|
|
71
|
+
} finally {
|
|
72
|
+
clearTimeout(timer);
|
|
73
|
+
}
|
|
74
|
+
const text = await res.text();
|
|
75
|
+
let json = null;
|
|
76
|
+
try {
|
|
77
|
+
json = text ? JSON.parse(text) : null;
|
|
78
|
+
} catch {
|
|
79
|
+
json = null;
|
|
80
|
+
}
|
|
81
|
+
if (!res.ok) {
|
|
82
|
+
const e = json?.error;
|
|
83
|
+
throw new BelticApiError(
|
|
84
|
+
res.status,
|
|
85
|
+
e?.code ?? `HTTP_${res.status}`,
|
|
86
|
+
e?.message ?? res.statusText,
|
|
87
|
+
e?.details,
|
|
88
|
+
e?.request_id
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
return json;
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
// src/core/correlation.ts
|
|
96
|
+
var MemoryCorrelationStore = class {
|
|
97
|
+
constructor(defaultTtlMs = 60 * 60 * 1e3) {
|
|
98
|
+
this.defaultTtlMs = defaultTtlMs;
|
|
99
|
+
}
|
|
100
|
+
entries = /* @__PURE__ */ new Map();
|
|
101
|
+
async bind(key, sessionId, ttlMs = this.defaultTtlMs) {
|
|
102
|
+
this.entries.set(key, { sessionId, expiresAt: Date.now() + ttlMs });
|
|
103
|
+
}
|
|
104
|
+
async resolve(key) {
|
|
105
|
+
const e = this.entries.get(key);
|
|
106
|
+
if (!e) return null;
|
|
107
|
+
if (e.expiresAt < Date.now()) {
|
|
108
|
+
this.entries.delete(key);
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
return e.sessionId;
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// src/core/session.ts
|
|
116
|
+
var Session = class {
|
|
117
|
+
constructor(deps, id, source, expiresAt, born) {
|
|
118
|
+
this.deps = deps;
|
|
119
|
+
this.id = id;
|
|
120
|
+
this.source = source;
|
|
121
|
+
this.expiresAt = expiresAt;
|
|
122
|
+
this.born = born;
|
|
123
|
+
this.chain = Chain.genesis(id, source);
|
|
124
|
+
this.now = deps.now ?? (() => /* @__PURE__ */ new Date());
|
|
125
|
+
}
|
|
126
|
+
chain;
|
|
127
|
+
building = Promise.resolve();
|
|
128
|
+
dropped = 0;
|
|
129
|
+
droppedFirstTs = null;
|
|
130
|
+
droppedLastTs = null;
|
|
131
|
+
closed = false;
|
|
132
|
+
now;
|
|
133
|
+
get head() {
|
|
134
|
+
return this.chain.head;
|
|
135
|
+
}
|
|
136
|
+
get droppedCount() {
|
|
137
|
+
return this.dropped;
|
|
138
|
+
}
|
|
139
|
+
/** Resolves once the event is sequenced and buffered — not once it is acknowledged. */
|
|
140
|
+
async emit(kind, payload) {
|
|
141
|
+
const halted = this.deps.transport.haltedError(this.id, this.source);
|
|
142
|
+
if (halted) throw halted;
|
|
143
|
+
const ts = this.now().toISOString();
|
|
144
|
+
if (!this.deps.transport.hasRoom()) {
|
|
145
|
+
this.dropped++;
|
|
146
|
+
this.droppedFirstTs ??= ts;
|
|
147
|
+
this.droppedLastTs = ts;
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
if (this.dropped > 0) {
|
|
151
|
+
this.deps.transport.enqueue(
|
|
152
|
+
await this.next(
|
|
153
|
+
"transport.gap",
|
|
154
|
+
{ dropped: this.dropped, firstTs: this.droppedFirstTs, lastTs: this.droppedLastTs },
|
|
155
|
+
ts
|
|
156
|
+
)
|
|
157
|
+
);
|
|
158
|
+
this.dropped = 0;
|
|
159
|
+
this.droppedFirstTs = this.droppedLastTs = null;
|
|
160
|
+
}
|
|
161
|
+
const body = payload;
|
|
162
|
+
this.deps.transport.enqueue(
|
|
163
|
+
await this.next(kind, this.deps.redact ? this.deps.redact(kind, body) : body, ts)
|
|
164
|
+
);
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
async close(reason = "completed", extra = {}) {
|
|
168
|
+
if (this.closed) return;
|
|
169
|
+
this.closed = true;
|
|
170
|
+
await this.emit("session.close", { reason, ...extra });
|
|
171
|
+
await this.flush();
|
|
172
|
+
this.deps.onClosed?.(this);
|
|
173
|
+
}
|
|
174
|
+
/** Read-your-writes: the platform must hold the evidence before anyone judges it (GAP-16/66). */
|
|
175
|
+
flush() {
|
|
176
|
+
return this.deps.transport.flush();
|
|
177
|
+
}
|
|
178
|
+
/** Serialized: two concurrent emits get consecutive seqs, never the same one. */
|
|
179
|
+
next(kind, payload, ts) {
|
|
180
|
+
const run = this.building.then(async () => {
|
|
181
|
+
const built = await this.chain.append({ ts, kind, payload }, this.deps.signer);
|
|
182
|
+
this.chain = built.chain;
|
|
183
|
+
return built.event;
|
|
184
|
+
});
|
|
185
|
+
this.building = run.catch(() => void 0);
|
|
186
|
+
return run;
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
var Sessions = class {
|
|
190
|
+
constructor(deps) {
|
|
191
|
+
this.deps = deps;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* One session object per (session, source) per process: a chain's head
|
|
195
|
+
* lives in it, so two objects for the same chain would both start at
|
|
196
|
+
* seq 0 and fork it. Closed sessions are forgotten; a process restart
|
|
197
|
+
* mid-session still loses the head (GAP-67).
|
|
198
|
+
*/
|
|
199
|
+
attached = /* @__PURE__ */ new Map();
|
|
200
|
+
/** Buyer half: create an AGENT_TRACE session bound to the agent identity, then announce it on the chain. */
|
|
201
|
+
async start(input = {}) {
|
|
202
|
+
const identity = this.deps.identity;
|
|
203
|
+
if (!identity)
|
|
204
|
+
throw new Error("sessions.start needs an agent identity (createBeltic({ identity }))");
|
|
205
|
+
const body = {
|
|
206
|
+
source: "AGENT_TRACE",
|
|
207
|
+
agent: { did: identity.did, credential: identity.credential ?? identity.did },
|
|
208
|
+
...input.intent ? { intent: input.intent } : {}
|
|
209
|
+
};
|
|
210
|
+
const out = await this.deps.api.post("/v1/sessions", body);
|
|
211
|
+
const session = this.attach(out.sessionId, "AGENT_TRACE", out.expiresAt, "buyer");
|
|
212
|
+
await session.emit("session.open", {
|
|
213
|
+
runtime: {
|
|
214
|
+
sdk: "@belticlabs/agent-risk-sdk",
|
|
215
|
+
version: this.deps.sdkVersion,
|
|
216
|
+
...input.runtime
|
|
217
|
+
},
|
|
218
|
+
...input.attestations ? { attestations: input.attestations } : {}
|
|
219
|
+
});
|
|
220
|
+
if (input.intent) await session.emit("intent.declared", input.intent);
|
|
221
|
+
return session;
|
|
222
|
+
}
|
|
223
|
+
/** Seller half: emit INTERNAL_NETWORK evidence into a session the buyer bound, or open a seller-born one. */
|
|
224
|
+
async ensure(sessionId) {
|
|
225
|
+
if (sessionId) return this.attach(sessionId, "INTERNAL_NETWORK", null, "buyer");
|
|
226
|
+
const out = await this.deps.api.post("/v1/sessions", {
|
|
227
|
+
source: "INTERNAL_NETWORK"
|
|
228
|
+
});
|
|
229
|
+
return this.attach(out.sessionId, "INTERNAL_NETWORK", out.expiresAt, "seller");
|
|
230
|
+
}
|
|
231
|
+
attach(id, source, expiresAt, born) {
|
|
232
|
+
const key = `${id}:${source}`;
|
|
233
|
+
const existing = this.attached.get(key);
|
|
234
|
+
if (existing) return existing;
|
|
235
|
+
const session = new Session(
|
|
236
|
+
{
|
|
237
|
+
transport: this.deps.transport,
|
|
238
|
+
signer: source === "AGENT_TRACE" ? this.deps.identity?.signer : void 0,
|
|
239
|
+
redact: this.deps.redact,
|
|
240
|
+
now: this.deps.now,
|
|
241
|
+
onClosed: () => this.attached.delete(key)
|
|
242
|
+
},
|
|
243
|
+
id,
|
|
244
|
+
source,
|
|
245
|
+
expiresAt,
|
|
246
|
+
born
|
|
247
|
+
);
|
|
248
|
+
this.attached.set(key, session);
|
|
249
|
+
return session;
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
// src/core/transport.ts
|
|
254
|
+
var DEFAULT_TRANSPORT = {
|
|
255
|
+
maxBatch: 50,
|
|
256
|
+
flushMs: 1e3,
|
|
257
|
+
maxBuffered: 5e3,
|
|
258
|
+
backoff: { baseMs: 200, maxMs: 3e4, maxAttempts: Number.POSITIVE_INFINITY }
|
|
259
|
+
};
|
|
260
|
+
var ChainRejectedError = class extends Error {
|
|
261
|
+
constructor(sessionId, source, result) {
|
|
262
|
+
super(
|
|
263
|
+
`chain ${sessionId}:${source} halted at seq ${result.seq}: ${result.status}${result.code ? ` ${result.code}` : ""}`
|
|
264
|
+
);
|
|
265
|
+
this.sessionId = sessionId;
|
|
266
|
+
this.source = source;
|
|
267
|
+
this.result = result;
|
|
268
|
+
this.name = "ChainRejectedError";
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
var TransportClosedError = class extends Error {
|
|
272
|
+
constructor() {
|
|
273
|
+
super("transport is closed");
|
|
274
|
+
this.name = "TransportClosedError";
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
var Transport = class {
|
|
278
|
+
constructor(api, opts = {}) {
|
|
279
|
+
this.api = api;
|
|
280
|
+
this.opts = {
|
|
281
|
+
...DEFAULT_TRANSPORT,
|
|
282
|
+
...opts,
|
|
283
|
+
backoff: { ...DEFAULT_TRANSPORT.backoff, ...opts.backoff }
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
opts;
|
|
287
|
+
chains = /* @__PURE__ */ new Map();
|
|
288
|
+
buffered = 0;
|
|
289
|
+
timer = null;
|
|
290
|
+
closed = false;
|
|
291
|
+
inFlightCount = 0;
|
|
292
|
+
drainWaiters = [];
|
|
293
|
+
get size() {
|
|
294
|
+
return this.buffered;
|
|
295
|
+
}
|
|
296
|
+
hasRoom() {
|
|
297
|
+
return !this.closed && this.buffered < this.opts.maxBuffered;
|
|
298
|
+
}
|
|
299
|
+
haltedError(sessionId, source) {
|
|
300
|
+
return this.chains.get(`${sessionId}:${source}`)?.halted ?? null;
|
|
301
|
+
}
|
|
302
|
+
/** Callers check `hasRoom()` first and assign `seq` only then (GAP-38). */
|
|
303
|
+
enqueue(ev) {
|
|
304
|
+
if (this.closed) throw new TransportClosedError();
|
|
305
|
+
const key = `${ev.sessionId}:${ev.source}`;
|
|
306
|
+
let chain = this.chains.get(key);
|
|
307
|
+
if (!chain) {
|
|
308
|
+
chain = { key, pending: [], inFlight: null, attempts: 0, halted: null };
|
|
309
|
+
this.chains.set(key, chain);
|
|
310
|
+
}
|
|
311
|
+
if (chain.halted) throw chain.halted;
|
|
312
|
+
if (!this.hasRoom()) throw new Error("transport buffer is full");
|
|
313
|
+
chain.pending.push(ev);
|
|
314
|
+
this.buffered++;
|
|
315
|
+
if (chain.pending.length >= this.opts.maxBatch) void this.flushChain(chain);
|
|
316
|
+
else this.schedule();
|
|
317
|
+
}
|
|
318
|
+
/** Send everything pending and wait for every in-flight batch to settle (ack or halt). */
|
|
319
|
+
async flush() {
|
|
320
|
+
this.unschedule();
|
|
321
|
+
for (const chain of this.chains.values()) void this.flushChain(chain);
|
|
322
|
+
await this.drained();
|
|
323
|
+
}
|
|
324
|
+
async close() {
|
|
325
|
+
await this.flush();
|
|
326
|
+
this.closed = true;
|
|
327
|
+
}
|
|
328
|
+
schedule() {
|
|
329
|
+
if (this.timer) return;
|
|
330
|
+
const st = this.opts.setTimeout ?? globalThis.setTimeout;
|
|
331
|
+
this.timer = st(() => {
|
|
332
|
+
this.timer = null;
|
|
333
|
+
for (const chain of this.chains.values()) void this.flushChain(chain);
|
|
334
|
+
}, this.opts.flushMs);
|
|
335
|
+
this.timer.unref?.();
|
|
336
|
+
}
|
|
337
|
+
unschedule() {
|
|
338
|
+
if (!this.timer) return;
|
|
339
|
+
(this.opts.clearTimeout ?? globalThis.clearTimeout)(this.timer);
|
|
340
|
+
this.timer = null;
|
|
341
|
+
}
|
|
342
|
+
drained() {
|
|
343
|
+
if (this.inFlightCount === 0 && [...this.chains.values()].every((c) => c.pending.length === 0 || c.halted)) {
|
|
344
|
+
return Promise.resolve();
|
|
345
|
+
}
|
|
346
|
+
return new Promise((resolve) => this.drainWaiters.push(resolve));
|
|
347
|
+
}
|
|
348
|
+
settleWaiters() {
|
|
349
|
+
if (this.inFlightCount > 0) return;
|
|
350
|
+
if (![...this.chains.values()].every((c) => c.pending.length === 0 || c.halted)) return;
|
|
351
|
+
const waiters = this.drainWaiters;
|
|
352
|
+
this.drainWaiters = [];
|
|
353
|
+
for (const w of waiters) w();
|
|
354
|
+
}
|
|
355
|
+
async flushChain(chain) {
|
|
356
|
+
if (chain.inFlight || chain.halted || chain.pending.length === 0) {
|
|
357
|
+
this.settleWaiters();
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
const batch = chain.pending.splice(0, this.opts.maxBatch);
|
|
361
|
+
chain.inFlight = batch;
|
|
362
|
+
this.inFlightCount++;
|
|
363
|
+
try {
|
|
364
|
+
const ack = await this.send(chain, batch);
|
|
365
|
+
this.buffered -= batch.length;
|
|
366
|
+
this.opts.onAck?.(ack);
|
|
367
|
+
const bad = ack.results.find((r) => r.status === "fork" || r.status === "rejected");
|
|
368
|
+
if (bad) {
|
|
369
|
+
const first = batch[0];
|
|
370
|
+
chain.halted = new ChainRejectedError(first.sessionId, first.source, bad);
|
|
371
|
+
this.buffered -= chain.pending.length;
|
|
372
|
+
chain.pending = [];
|
|
373
|
+
this.opts.onChainHalted?.(chain.halted);
|
|
374
|
+
}
|
|
375
|
+
} catch (err) {
|
|
376
|
+
this.buffered -= batch.length + chain.pending.length;
|
|
377
|
+
chain.pending = [];
|
|
378
|
+
const first = batch[0];
|
|
379
|
+
chain.halted = new ChainRejectedError(first.sessionId, first.source, {
|
|
380
|
+
index: 0,
|
|
381
|
+
sessionId: first.sessionId,
|
|
382
|
+
source: first.source,
|
|
383
|
+
seq: first.seq,
|
|
384
|
+
status: "rejected",
|
|
385
|
+
code: "DELIVERY_FAILED"
|
|
386
|
+
});
|
|
387
|
+
this.opts.onError?.(err);
|
|
388
|
+
this.opts.onChainHalted?.(chain.halted);
|
|
389
|
+
} finally {
|
|
390
|
+
chain.inFlight = null;
|
|
391
|
+
this.inFlightCount--;
|
|
392
|
+
}
|
|
393
|
+
if (chain.pending.length > 0 && !chain.halted) void this.flushChain(chain);
|
|
394
|
+
else this.settleWaiters();
|
|
395
|
+
}
|
|
396
|
+
async send(chain, batch) {
|
|
397
|
+
const { baseMs, maxMs, maxAttempts } = this.opts.backoff;
|
|
398
|
+
for (let attempt = 0; ; attempt++) {
|
|
399
|
+
try {
|
|
400
|
+
const ack = await this.api.post("/v1/evidence", batch);
|
|
401
|
+
chain.attempts = 0;
|
|
402
|
+
return ack;
|
|
403
|
+
} catch (err) {
|
|
404
|
+
const retryable = err instanceof BelticApiError ? err.retryable : true;
|
|
405
|
+
if (!retryable || attempt + 1 >= maxAttempts) throw err;
|
|
406
|
+
this.opts.onError?.(err);
|
|
407
|
+
const delay = Math.min(maxMs, baseMs * 2 ** attempt) * (0.5 + Math.random() / 2);
|
|
408
|
+
await new Promise((r) => (this.opts.setTimeout ?? globalThis.setTimeout)(r, delay));
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
// src/client.ts
|
|
415
|
+
var SDK_VERSION = "0.1.0";
|
|
416
|
+
var Beltic = class {
|
|
417
|
+
api;
|
|
418
|
+
transport;
|
|
419
|
+
sessions;
|
|
420
|
+
identity;
|
|
421
|
+
correlation;
|
|
422
|
+
onReview;
|
|
423
|
+
constructor(opts) {
|
|
424
|
+
this.api = new ApiClient({ ...opts, userAgent: `@belticlabs/agent-risk-sdk/${SDK_VERSION}` });
|
|
425
|
+
this.transport = new Transport(this.api, opts.transport);
|
|
426
|
+
this.sessions = new Sessions({
|
|
427
|
+
api: this.api,
|
|
428
|
+
transport: this.transport,
|
|
429
|
+
sdkVersion: SDK_VERSION,
|
|
430
|
+
identity: opts.identity,
|
|
431
|
+
redact: opts.redact,
|
|
432
|
+
now: opts.now
|
|
433
|
+
});
|
|
434
|
+
this.identity = opts.identity;
|
|
435
|
+
this.correlation = opts.correlation ?? new MemoryCorrelationStore();
|
|
436
|
+
this.onReview = opts.onReview ?? "abort";
|
|
437
|
+
}
|
|
438
|
+
/** Read-your-writes: the platform must hold the evidence before it judges it (GAP-16). */
|
|
439
|
+
async evaluate(sessionId, payment) {
|
|
440
|
+
await this.transport.flush();
|
|
441
|
+
return this.api.post("/v1/evaluate", {
|
|
442
|
+
sessionId,
|
|
443
|
+
payment
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
flush() {
|
|
447
|
+
return this.transport.flush();
|
|
448
|
+
}
|
|
449
|
+
shutdown() {
|
|
450
|
+
return this.transport.close();
|
|
451
|
+
}
|
|
452
|
+
};
|
|
453
|
+
function createBeltic(opts) {
|
|
454
|
+
return new Beltic(opts);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// src/core/identity.ts
|
|
458
|
+
import { mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
459
|
+
import { dirname } from "path";
|
|
460
|
+
function identityFromSeed(seed, credential) {
|
|
461
|
+
const signer = memorySigner(seed);
|
|
462
|
+
const did = didKeyFromEd25519(signer.publicKey);
|
|
463
|
+
return { did, signer: { ...signer, keyId: did }, ...credential ? { credential } : {} };
|
|
464
|
+
}
|
|
465
|
+
function ephemeralIdentity(credential) {
|
|
466
|
+
return identityFromSeed(memorySigner().seed, credential);
|
|
467
|
+
}
|
|
468
|
+
function fileIdentity(path, credential) {
|
|
469
|
+
let seed;
|
|
470
|
+
try {
|
|
471
|
+
seed = fromHex(JSON.parse(readFileSync(path, "utf8")).seed);
|
|
472
|
+
} catch {
|
|
473
|
+
seed = memorySigner().seed;
|
|
474
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
475
|
+
writeFileSync(path, `${JSON.stringify({ seed: toHex(seed) })}
|
|
476
|
+
`, { mode: 384 });
|
|
477
|
+
}
|
|
478
|
+
return identityFromSeed(seed, credential);
|
|
479
|
+
}
|
|
480
|
+
export {
|
|
481
|
+
ApiClient,
|
|
482
|
+
Beltic,
|
|
483
|
+
BelticApiError,
|
|
484
|
+
ChainRejectedError,
|
|
485
|
+
DEFAULT_TRANSPORT,
|
|
486
|
+
MemoryCorrelationStore,
|
|
487
|
+
SDK_VERSION,
|
|
488
|
+
Session,
|
|
489
|
+
Sessions,
|
|
490
|
+
Transport,
|
|
491
|
+
TransportClosedError,
|
|
492
|
+
createBeltic,
|
|
493
|
+
ephemeralIdentity,
|
|
494
|
+
fileIdentity,
|
|
495
|
+
identityFromSeed,
|
|
496
|
+
presentedFrom,
|
|
497
|
+
recordCall,
|
|
498
|
+
summaryOf
|
|
499
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { PaymentSummary, PaymentMomentPayload } from './api.js';
|
|
2
|
+
import { B as Beltic } from '../client-CVx9LgJC.js';
|
|
3
|
+
import { S as Session } from '../session-BMNB1N1g.js';
|
|
4
|
+
import './base58.js';
|
|
5
|
+
|
|
6
|
+
interface CallToolParams {
|
|
7
|
+
name: string;
|
|
8
|
+
arguments?: Record<string, unknown> | undefined;
|
|
9
|
+
_meta?: Record<string, unknown> | undefined;
|
|
10
|
+
}
|
|
11
|
+
/** Structural: `Client` from `@modelcontextprotocol/sdk/client` satisfies it. */
|
|
12
|
+
interface McpClientLike {
|
|
13
|
+
callTool(params: CallToolParams, ...rest: unknown[]): Promise<unknown>;
|
|
14
|
+
}
|
|
15
|
+
declare function wrapClient<C extends McpClientLike>(session: Session, client: C, opts?: {
|
|
16
|
+
server?: string;
|
|
17
|
+
}): C;
|
|
18
|
+
/**
|
|
19
|
+
* An in-band payment ask: any object in the result carrying an x402-style
|
|
20
|
+
* `accepts` array (MRTR `input_required`, or a `_meta` envelope). One
|
|
21
|
+
* moment, two artifacts — either becomes the same `payment.requested`.
|
|
22
|
+
*/
|
|
23
|
+
declare function findPaymentAsk(result: unknown): PaymentMomentPayload | null;
|
|
24
|
+
interface ServerCallExtra {
|
|
25
|
+
_meta?: Record<string, unknown> | undefined;
|
|
26
|
+
sessionId?: string | undefined;
|
|
27
|
+
}
|
|
28
|
+
interface WithBelticOptions<Args> {
|
|
29
|
+
toolName: string;
|
|
30
|
+
/** Return the payment this call moves, or null when the call is not value-moving. */
|
|
31
|
+
valueMoving?: (args: Args) => PaymentSummary | null;
|
|
32
|
+
gateway?: string;
|
|
33
|
+
}
|
|
34
|
+
type ToolHandler<Args, R> = (args: Args, extra: ServerCallExtra) => Promise<R> | R;
|
|
35
|
+
interface McpErrorResult {
|
|
36
|
+
isError: true;
|
|
37
|
+
content: Array<{
|
|
38
|
+
type: 'text';
|
|
39
|
+
text: string;
|
|
40
|
+
}>;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Wraps a tool handler on the seller side. Value-moving calls are evaluated
|
|
44
|
+
* synchronously (DENY ⇒ isError result); every decided call is recorded
|
|
45
|
+
* as `gateway.decision` in the buyer's session, or a seller-born one.
|
|
46
|
+
*/
|
|
47
|
+
declare function withBeltic<Args, R>(beltic: Beltic, handler: ToolHandler<Args, R>, opts: WithBelticOptions<Args>): ToolHandler<Args, R | McpErrorResult>;
|
|
48
|
+
/** Structural: `McpServer` from `@modelcontextprotocol/sdk/server/mcp` satisfies it. */
|
|
49
|
+
interface McpServerLike {
|
|
50
|
+
registerTool(name: string, config: unknown, cb: (...a: never[]) => unknown): unknown;
|
|
51
|
+
}
|
|
52
|
+
interface WrapServerOptions {
|
|
53
|
+
valueMoving?: (toolName: string, args: unknown) => PaymentSummary | null;
|
|
54
|
+
gateway?: string;
|
|
55
|
+
}
|
|
56
|
+
/** Patches `registerTool` so every tool goes through `withBeltic`; `valueMoving` decides per tool. */
|
|
57
|
+
declare function wrapServer<S extends McpServerLike>(beltic: Beltic, server: S, opts?: WrapServerOptions): S;
|
|
58
|
+
|
|
59
|
+
export { type CallToolParams, type McpClientLike, type McpErrorResult, type McpServerLike, type ServerCallExtra, type ToolHandler, type WithBelticOptions, type WrapServerOptions, findPaymentAsk, withBeltic, wrapClient, wrapServer };
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import {
|
|
2
|
+
recordCall
|
|
3
|
+
} from "../chunk-5TWO73OD.js";
|
|
4
|
+
import {
|
|
5
|
+
SESSION_EXTENSION
|
|
6
|
+
} from "../chunk-VM7MK43J.js";
|
|
7
|
+
import {
|
|
8
|
+
uuidv7
|
|
9
|
+
} from "../chunk-SFGM7KOG.js";
|
|
10
|
+
import {
|
|
11
|
+
x402Moments
|
|
12
|
+
} from "../chunk-U5Z5Z2BQ.js";
|
|
13
|
+
import {
|
|
14
|
+
presentedFrom
|
|
15
|
+
} from "../chunk-7G5EHNVW.js";
|
|
16
|
+
import {
|
|
17
|
+
toJson,
|
|
18
|
+
toJsonObject
|
|
19
|
+
} from "../chunk-FQDHFTVR.js";
|
|
20
|
+
import {
|
|
21
|
+
Verdict
|
|
22
|
+
} from "../chunk-GCKCAKHA.js";
|
|
23
|
+
|
|
24
|
+
// src/mcp/index.ts
|
|
25
|
+
function wrapClient(session, client, opts = {}) {
|
|
26
|
+
const original = client.callTool.bind(client);
|
|
27
|
+
const callTool = (params, ...rest) => recordCall(
|
|
28
|
+
session,
|
|
29
|
+
"tool_call",
|
|
30
|
+
uuidv7(),
|
|
31
|
+
{
|
|
32
|
+
toolName: params.name,
|
|
33
|
+
input: toJson(params.arguments ?? {}),
|
|
34
|
+
transport: "mcp",
|
|
35
|
+
...opts.server ? { server: opts.server } : {}
|
|
36
|
+
},
|
|
37
|
+
async () => {
|
|
38
|
+
await session.flush();
|
|
39
|
+
return original(
|
|
40
|
+
{ ...params, _meta: { ...params._meta ?? {}, [SESSION_EXTENSION]: session.id } },
|
|
41
|
+
...rest
|
|
42
|
+
);
|
|
43
|
+
},
|
|
44
|
+
async (result) => {
|
|
45
|
+
const ask = findPaymentAsk(result);
|
|
46
|
+
if (ask) await session.emit("payment.requested", ask);
|
|
47
|
+
return { output: toJson(result) };
|
|
48
|
+
}
|
|
49
|
+
);
|
|
50
|
+
return new Proxy(client, {
|
|
51
|
+
get(target, prop, receiver) {
|
|
52
|
+
return prop === "callTool" ? callTool : Reflect.get(target, prop, receiver);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
function findPaymentAsk(result) {
|
|
57
|
+
const seen = /* @__PURE__ */ new Set();
|
|
58
|
+
const walk = (v, depth) => {
|
|
59
|
+
if (!v || typeof v !== "object" || seen.has(v) || depth > 6) return null;
|
|
60
|
+
seen.add(v);
|
|
61
|
+
const o = v;
|
|
62
|
+
const first = Array.isArray(o.accepts) ? o.accepts[0] : void 0;
|
|
63
|
+
if (typeof first?.payTo === "string") {
|
|
64
|
+
return x402Moments.ask(first, toJsonObject(o));
|
|
65
|
+
}
|
|
66
|
+
for (const child of Array.isArray(o) ? o : Object.values(o)) {
|
|
67
|
+
const hit = walk(child, depth + 1);
|
|
68
|
+
if (hit) return hit;
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
};
|
|
72
|
+
return walk(result, 0);
|
|
73
|
+
}
|
|
74
|
+
function withBeltic(beltic, handler, opts) {
|
|
75
|
+
const gateway = opts.gateway ?? "sdk-mcp";
|
|
76
|
+
return async (args, extra) => {
|
|
77
|
+
const bound = extra?._meta?.[SESSION_EXTENSION];
|
|
78
|
+
const session = await beltic.sessions.ensure(typeof bound === "string" ? bound : null);
|
|
79
|
+
const payment = opts.valueMoving?.(args) ?? null;
|
|
80
|
+
let verdict = Verdict.ALLOW;
|
|
81
|
+
let reasonCodes = [];
|
|
82
|
+
if (payment) {
|
|
83
|
+
await session.emit(
|
|
84
|
+
"payment.presented",
|
|
85
|
+
presentedFrom(payment, { tool: opts.toolName, args: toJson(args) })
|
|
86
|
+
);
|
|
87
|
+
const out = await beltic.evaluate(session.id, payment);
|
|
88
|
+
verdict = Verdict.of(out.decision);
|
|
89
|
+
reasonCodes = out.reasonCodes;
|
|
90
|
+
}
|
|
91
|
+
await session.emit("gateway.decision", {
|
|
92
|
+
gateway,
|
|
93
|
+
call: { tool: opts.toolName, args: toJson(args) },
|
|
94
|
+
decision: verdict.effective(beltic.onReview),
|
|
95
|
+
reasonCodes,
|
|
96
|
+
record: { valueMoving: payment !== null, bound: typeof bound === "string" }
|
|
97
|
+
});
|
|
98
|
+
if (verdict.blocks(beltic.onReview)) {
|
|
99
|
+
return {
|
|
100
|
+
isError: true,
|
|
101
|
+
content: [
|
|
102
|
+
{ type: "text", text: `Beltic ${verdict.value}: ${reasonCodes.join(",") || "denied"}` }
|
|
103
|
+
]
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
return handler(args, extra);
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
function wrapServer(beltic, server, opts = {}) {
|
|
110
|
+
const original = server.registerTool.bind(server);
|
|
111
|
+
server.registerTool = ((name, config, cb) => original(
|
|
112
|
+
name,
|
|
113
|
+
config,
|
|
114
|
+
withBeltic(beltic, cb, {
|
|
115
|
+
toolName: name,
|
|
116
|
+
...opts.gateway ? { gateway: opts.gateway } : {},
|
|
117
|
+
valueMoving: (args) => opts.valueMoving?.(name, args) ?? null
|
|
118
|
+
})
|
|
119
|
+
));
|
|
120
|
+
return server;
|
|
121
|
+
}
|
|
122
|
+
export {
|
|
123
|
+
findPaymentAsk,
|
|
124
|
+
withBeltic,
|
|
125
|
+
wrapClient,
|
|
126
|
+
wrapServer
|
|
127
|
+
};
|