kontext-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/LICENSE +21 -0
- package/README.md +98 -0
- package/dist/index.d.mts +1320 -0
- package/dist/index.d.ts +1320 -0
- package/dist/index.js +3274 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +3244 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +72 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,3274 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var crypto$1 = require('crypto');
|
|
4
|
+
var fs = require('fs');
|
|
5
|
+
var path = require('path');
|
|
6
|
+
|
|
7
|
+
function _interopNamespace(e) {
|
|
8
|
+
if (e && e.__esModule) return e;
|
|
9
|
+
var n = Object.create(null);
|
|
10
|
+
if (e) {
|
|
11
|
+
Object.keys(e).forEach(function (k) {
|
|
12
|
+
if (k !== 'default') {
|
|
13
|
+
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
14
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
15
|
+
enumerable: true,
|
|
16
|
+
get: function () { return e[k]; }
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
n.default = e;
|
|
22
|
+
return Object.freeze(n);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
var fs__namespace = /*#__PURE__*/_interopNamespace(fs);
|
|
26
|
+
var path__namespace = /*#__PURE__*/_interopNamespace(path);
|
|
27
|
+
|
|
28
|
+
// src/types.ts
|
|
29
|
+
var KontextErrorCode = /* @__PURE__ */ ((KontextErrorCode2) => {
|
|
30
|
+
KontextErrorCode2["INITIALIZATION_ERROR"] = "INITIALIZATION_ERROR";
|
|
31
|
+
KontextErrorCode2["VALIDATION_ERROR"] = "VALIDATION_ERROR";
|
|
32
|
+
KontextErrorCode2["TASK_NOT_FOUND"] = "TASK_NOT_FOUND";
|
|
33
|
+
KontextErrorCode2["TASK_ALREADY_CONFIRMED"] = "TASK_ALREADY_CONFIRMED";
|
|
34
|
+
KontextErrorCode2["TASK_EXPIRED"] = "TASK_EXPIRED";
|
|
35
|
+
KontextErrorCode2["INSUFFICIENT_EVIDENCE"] = "INSUFFICIENT_EVIDENCE";
|
|
36
|
+
KontextErrorCode2["API_ERROR"] = "API_ERROR";
|
|
37
|
+
KontextErrorCode2["NETWORK_ERROR"] = "NETWORK_ERROR";
|
|
38
|
+
KontextErrorCode2["EXPORT_ERROR"] = "EXPORT_ERROR";
|
|
39
|
+
KontextErrorCode2["ANOMALY_CONFIG_ERROR"] = "ANOMALY_CONFIG_ERROR";
|
|
40
|
+
return KontextErrorCode2;
|
|
41
|
+
})(KontextErrorCode || {});
|
|
42
|
+
var KontextError = class extends Error {
|
|
43
|
+
code;
|
|
44
|
+
details;
|
|
45
|
+
constructor(code, message, details) {
|
|
46
|
+
super(message);
|
|
47
|
+
this.name = "KontextError";
|
|
48
|
+
this.code = code;
|
|
49
|
+
this.details = details;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// src/store.ts
|
|
54
|
+
var KontextStore = class {
|
|
55
|
+
actions = [];
|
|
56
|
+
transactions = [];
|
|
57
|
+
tasks = /* @__PURE__ */ new Map();
|
|
58
|
+
anomalies = [];
|
|
59
|
+
// --------------------------------------------------------------------------
|
|
60
|
+
// Actions
|
|
61
|
+
// --------------------------------------------------------------------------
|
|
62
|
+
/** Append an action log entry. */
|
|
63
|
+
addAction(action) {
|
|
64
|
+
this.actions.push(action);
|
|
65
|
+
}
|
|
66
|
+
/** Retrieve all action log entries. */
|
|
67
|
+
getActions() {
|
|
68
|
+
return [...this.actions];
|
|
69
|
+
}
|
|
70
|
+
/** Retrieve actions filtered by a predicate. */
|
|
71
|
+
queryActions(predicate) {
|
|
72
|
+
return this.actions.filter(predicate);
|
|
73
|
+
}
|
|
74
|
+
/** Get actions for a specific agent. */
|
|
75
|
+
getActionsByAgent(agentId) {
|
|
76
|
+
return this.actions.filter((a) => a.agentId === agentId);
|
|
77
|
+
}
|
|
78
|
+
// --------------------------------------------------------------------------
|
|
79
|
+
// Transactions
|
|
80
|
+
// --------------------------------------------------------------------------
|
|
81
|
+
/** Append a transaction record. */
|
|
82
|
+
addTransaction(tx) {
|
|
83
|
+
this.transactions.push(tx);
|
|
84
|
+
}
|
|
85
|
+
/** Retrieve all transaction records. */
|
|
86
|
+
getTransactions() {
|
|
87
|
+
return [...this.transactions];
|
|
88
|
+
}
|
|
89
|
+
/** Retrieve transactions filtered by a predicate. */
|
|
90
|
+
queryTransactions(predicate) {
|
|
91
|
+
return this.transactions.filter(predicate);
|
|
92
|
+
}
|
|
93
|
+
/** Get transactions for a specific agent. */
|
|
94
|
+
getTransactionsByAgent(agentId) {
|
|
95
|
+
return this.transactions.filter((t) => t.agentId === agentId);
|
|
96
|
+
}
|
|
97
|
+
/** Get the most recent N transactions for an agent. */
|
|
98
|
+
getRecentTransactions(agentId, limit) {
|
|
99
|
+
return this.transactions.filter((t) => t.agentId === agentId).slice(-limit);
|
|
100
|
+
}
|
|
101
|
+
// --------------------------------------------------------------------------
|
|
102
|
+
// Tasks
|
|
103
|
+
// --------------------------------------------------------------------------
|
|
104
|
+
/** Store a task. */
|
|
105
|
+
addTask(task) {
|
|
106
|
+
this.tasks.set(task.id, task);
|
|
107
|
+
}
|
|
108
|
+
/** Retrieve a task by ID. */
|
|
109
|
+
getTask(taskId) {
|
|
110
|
+
return this.tasks.get(taskId);
|
|
111
|
+
}
|
|
112
|
+
/** Update a task. */
|
|
113
|
+
updateTask(taskId, updates) {
|
|
114
|
+
const existing = this.tasks.get(taskId);
|
|
115
|
+
if (!existing) return void 0;
|
|
116
|
+
const updated = { ...existing, ...updates };
|
|
117
|
+
this.tasks.set(taskId, updated);
|
|
118
|
+
return updated;
|
|
119
|
+
}
|
|
120
|
+
/** Retrieve all tasks. */
|
|
121
|
+
getTasks() {
|
|
122
|
+
return Array.from(this.tasks.values());
|
|
123
|
+
}
|
|
124
|
+
/** Retrieve tasks filtered by a predicate. */
|
|
125
|
+
queryTasks(predicate) {
|
|
126
|
+
return Array.from(this.tasks.values()).filter(predicate);
|
|
127
|
+
}
|
|
128
|
+
// --------------------------------------------------------------------------
|
|
129
|
+
// Anomalies
|
|
130
|
+
// --------------------------------------------------------------------------
|
|
131
|
+
/** Append an anomaly event. */
|
|
132
|
+
addAnomaly(anomaly) {
|
|
133
|
+
this.anomalies.push(anomaly);
|
|
134
|
+
}
|
|
135
|
+
/** Retrieve all anomaly events. */
|
|
136
|
+
getAnomalies() {
|
|
137
|
+
return [...this.anomalies];
|
|
138
|
+
}
|
|
139
|
+
/** Retrieve anomalies filtered by a predicate. */
|
|
140
|
+
queryAnomalies(predicate) {
|
|
141
|
+
return this.anomalies.filter(predicate);
|
|
142
|
+
}
|
|
143
|
+
// --------------------------------------------------------------------------
|
|
144
|
+
// Utilities
|
|
145
|
+
// --------------------------------------------------------------------------
|
|
146
|
+
/** Get total record counts across all stores. */
|
|
147
|
+
getCounts() {
|
|
148
|
+
return {
|
|
149
|
+
actions: this.actions.length,
|
|
150
|
+
transactions: this.transactions.length,
|
|
151
|
+
tasks: this.tasks.size,
|
|
152
|
+
anomalies: this.anomalies.length
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
/** Clear all stored data. Useful for testing. */
|
|
156
|
+
clear() {
|
|
157
|
+
this.actions = [];
|
|
158
|
+
this.transactions = [];
|
|
159
|
+
this.tasks.clear();
|
|
160
|
+
this.anomalies = [];
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
var DIGEST_EXCLUDED_FIELDS = /* @__PURE__ */ new Set(["digest", "priorDigest"]);
|
|
164
|
+
function serializeForDigest(action) {
|
|
165
|
+
const keys = Object.keys(action).filter((k) => !DIGEST_EXCLUDED_FIELDS.has(k)).sort();
|
|
166
|
+
return JSON.stringify(action, keys);
|
|
167
|
+
}
|
|
168
|
+
var GENESIS_HASH = "0".repeat(64);
|
|
169
|
+
var DigestChain = class {
|
|
170
|
+
links = [];
|
|
171
|
+
currentDigest = GENESIS_HASH;
|
|
172
|
+
hrtimeBase;
|
|
173
|
+
constructor() {
|
|
174
|
+
this.hrtimeBase = process.hrtime.bigint();
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Append an action to the digest chain.
|
|
178
|
+
*
|
|
179
|
+
* Computes: HD = SHA-256(HD-1 || Serialize(ED) || SD)
|
|
180
|
+
*
|
|
181
|
+
* @param action - The action log entry to chain
|
|
182
|
+
* @returns The digest link for this event
|
|
183
|
+
*/
|
|
184
|
+
append(action) {
|
|
185
|
+
const timestamp = this.getPrecisionTimestamp();
|
|
186
|
+
const serialized = this.serialize(action);
|
|
187
|
+
const salt = this.deriveSalt(timestamp);
|
|
188
|
+
const priorDigest = this.currentDigest;
|
|
189
|
+
const digest = this.computeDigest(priorDigest, serialized, salt);
|
|
190
|
+
const link = {
|
|
191
|
+
digest,
|
|
192
|
+
priorDigest,
|
|
193
|
+
salt,
|
|
194
|
+
timestamp,
|
|
195
|
+
sequence: this.links.length,
|
|
196
|
+
actionId: action.id
|
|
197
|
+
};
|
|
198
|
+
this.links.push(link);
|
|
199
|
+
this.currentDigest = digest;
|
|
200
|
+
return link;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Get the terminal digest — the latest digest in the chain.
|
|
204
|
+
* This can be embedded in outgoing messages as proof of the entire action history.
|
|
205
|
+
*/
|
|
206
|
+
getTerminalDigest() {
|
|
207
|
+
return this.currentDigest;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Get the number of links in the chain.
|
|
211
|
+
*/
|
|
212
|
+
getChainLength() {
|
|
213
|
+
return this.links.length;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Get all digest links in the chain.
|
|
217
|
+
*/
|
|
218
|
+
getLinks() {
|
|
219
|
+
return this.links;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Get a specific digest link by sequence number.
|
|
223
|
+
*/
|
|
224
|
+
getLink(sequence) {
|
|
225
|
+
return this.links[sequence];
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Verify the integrity of the entire digest chain.
|
|
229
|
+
*
|
|
230
|
+
* Recomputes every digest from the genesis hash and compares.
|
|
231
|
+
* Any tampering (modified, inserted, deleted, or reordered events)
|
|
232
|
+
* will cause verification to fail.
|
|
233
|
+
*
|
|
234
|
+
* @param actions - The original action logs to verify against
|
|
235
|
+
* @returns Verification result with timing data
|
|
236
|
+
*/
|
|
237
|
+
verify(actions) {
|
|
238
|
+
const start = performance.now();
|
|
239
|
+
if (actions.length !== this.links.length) {
|
|
240
|
+
return {
|
|
241
|
+
valid: false,
|
|
242
|
+
linksVerified: 0,
|
|
243
|
+
firstInvalidIndex: 0,
|
|
244
|
+
verificationTimeMs: performance.now() - start,
|
|
245
|
+
terminalDigest: this.currentDigest
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
let computedDigest = GENESIS_HASH;
|
|
249
|
+
for (let i = 0; i < this.links.length; i++) {
|
|
250
|
+
const link = this.links[i];
|
|
251
|
+
const action = actions[i];
|
|
252
|
+
const serialized = this.serialize(action);
|
|
253
|
+
const expectedDigest = this.computeDigest(computedDigest, serialized, link.salt);
|
|
254
|
+
if (expectedDigest !== link.digest) {
|
|
255
|
+
return {
|
|
256
|
+
valid: false,
|
|
257
|
+
linksVerified: i,
|
|
258
|
+
firstInvalidIndex: i,
|
|
259
|
+
verificationTimeMs: performance.now() - start,
|
|
260
|
+
terminalDigest: this.currentDigest
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
computedDigest = expectedDigest;
|
|
264
|
+
}
|
|
265
|
+
return {
|
|
266
|
+
valid: true,
|
|
267
|
+
linksVerified: this.links.length,
|
|
268
|
+
firstInvalidIndex: -1,
|
|
269
|
+
verificationTimeMs: performance.now() - start,
|
|
270
|
+
terminalDigest: this.currentDigest
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Verify a single link in isolation (given the expected prior digest).
|
|
275
|
+
*
|
|
276
|
+
* @param link - The digest link to verify
|
|
277
|
+
* @param action - The action data for this link
|
|
278
|
+
* @param expectedPriorDigest - The expected prior digest
|
|
279
|
+
* @returns Whether the link is valid
|
|
280
|
+
*/
|
|
281
|
+
verifyLink(link, action, expectedPriorDigest) {
|
|
282
|
+
const serialized = this.serialize(action);
|
|
283
|
+
const expectedDigest = this.computeDigest(expectedPriorDigest, serialized, link.salt);
|
|
284
|
+
return expectedDigest === link.digest;
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Export the chain data for independent verification by a third party.
|
|
288
|
+
* Includes all links and enough data for recomputation.
|
|
289
|
+
*/
|
|
290
|
+
exportChain() {
|
|
291
|
+
return {
|
|
292
|
+
genesisHash: GENESIS_HASH,
|
|
293
|
+
links: [...this.links],
|
|
294
|
+
terminalDigest: this.currentDigest
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
// --------------------------------------------------------------------------
|
|
298
|
+
// Core cryptographic operations
|
|
299
|
+
// --------------------------------------------------------------------------
|
|
300
|
+
/**
|
|
301
|
+
* Compute: HD = SHA-256(HD-1 || Serialize(ED) || SD)
|
|
302
|
+
*/
|
|
303
|
+
computeDigest(priorDigest, serializedEvent, salt) {
|
|
304
|
+
const hash = crypto$1.createHash("sha256");
|
|
305
|
+
hash.update(priorDigest);
|
|
306
|
+
hash.update(serializedEvent);
|
|
307
|
+
hash.update(salt);
|
|
308
|
+
return hash.digest("hex");
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Deterministically serialize an action log for digest computation.
|
|
312
|
+
* Uses sorted keys to ensure consistent serialization regardless of
|
|
313
|
+
* property insertion order. Excludes digest/priorDigest fields since
|
|
314
|
+
* those are computed from this serialization.
|
|
315
|
+
*/
|
|
316
|
+
serialize(action) {
|
|
317
|
+
return serializeForDigest(action);
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Derive a salt from the event's high-precision timestamp.
|
|
321
|
+
* SD = SHA-256(microsecond_timestamp)
|
|
322
|
+
*/
|
|
323
|
+
deriveSalt(timestamp) {
|
|
324
|
+
const hash = crypto$1.createHash("sha256");
|
|
325
|
+
hash.update(timestamp.hrtime.toString());
|
|
326
|
+
return hash.digest("hex");
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Get a microsecond-precision timestamp.
|
|
330
|
+
* Combines wall clock time with high-resolution timer for sub-millisecond precision.
|
|
331
|
+
*/
|
|
332
|
+
getPrecisionTimestamp() {
|
|
333
|
+
const hrtime = process.hrtime.bigint();
|
|
334
|
+
const microseconds = Number((hrtime - this.hrtimeBase) % 1000000n);
|
|
335
|
+
return {
|
|
336
|
+
iso: (/* @__PURE__ */ new Date()).toISOString(),
|
|
337
|
+
hrtime,
|
|
338
|
+
microseconds
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
function verifyExportedChain(chain, actions) {
|
|
343
|
+
const start = performance.now();
|
|
344
|
+
if (actions.length !== chain.links.length) {
|
|
345
|
+
return {
|
|
346
|
+
valid: false,
|
|
347
|
+
linksVerified: 0,
|
|
348
|
+
firstInvalidIndex: 0,
|
|
349
|
+
verificationTimeMs: performance.now() - start,
|
|
350
|
+
terminalDigest: chain.terminalDigest
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
let computedDigest = chain.genesisHash;
|
|
354
|
+
for (let i = 0; i < chain.links.length; i++) {
|
|
355
|
+
const link = chain.links[i];
|
|
356
|
+
const action = actions[i];
|
|
357
|
+
const serialized = serializeForDigest(action);
|
|
358
|
+
const hash = crypto$1.createHash("sha256");
|
|
359
|
+
hash.update(computedDigest);
|
|
360
|
+
hash.update(serialized);
|
|
361
|
+
hash.update(link.salt);
|
|
362
|
+
const expectedDigest = hash.digest("hex");
|
|
363
|
+
if (expectedDigest !== link.digest) {
|
|
364
|
+
return {
|
|
365
|
+
valid: false,
|
|
366
|
+
linksVerified: i,
|
|
367
|
+
firstInvalidIndex: i,
|
|
368
|
+
verificationTimeMs: performance.now() - start,
|
|
369
|
+
terminalDigest: chain.terminalDigest
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
computedDigest = expectedDigest;
|
|
373
|
+
}
|
|
374
|
+
const valid = computedDigest === chain.terminalDigest;
|
|
375
|
+
return {
|
|
376
|
+
valid,
|
|
377
|
+
linksVerified: chain.links.length,
|
|
378
|
+
firstInvalidIndex: valid ? -1 : chain.links.length,
|
|
379
|
+
verificationTimeMs: performance.now() - start,
|
|
380
|
+
terminalDigest: chain.terminalDigest
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// src/utils.ts
|
|
385
|
+
function generateId() {
|
|
386
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
387
|
+
return crypto.randomUUID();
|
|
388
|
+
}
|
|
389
|
+
const timestamp = Date.now().toString(36);
|
|
390
|
+
const random = Math.random().toString(36).substring(2, 10);
|
|
391
|
+
return `${timestamp}-${random}`;
|
|
392
|
+
}
|
|
393
|
+
function now() {
|
|
394
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
395
|
+
}
|
|
396
|
+
function isWithinDateRange(date, start, end) {
|
|
397
|
+
const d = typeof date === "string" ? new Date(date) : date;
|
|
398
|
+
return d >= start && d <= end;
|
|
399
|
+
}
|
|
400
|
+
function parseAmount(amount) {
|
|
401
|
+
const parsed = parseFloat(amount);
|
|
402
|
+
return parsed;
|
|
403
|
+
}
|
|
404
|
+
function toCsv(records) {
|
|
405
|
+
if (records.length === 0) return "";
|
|
406
|
+
const firstRecord = records[0];
|
|
407
|
+
if (!firstRecord) return "";
|
|
408
|
+
const headers = Object.keys(firstRecord);
|
|
409
|
+
const headerRow = headers.join(",");
|
|
410
|
+
const rows = records.map((record) => {
|
|
411
|
+
return headers.map((header) => {
|
|
412
|
+
const value = record[header];
|
|
413
|
+
if (value === null || value === void 0) return "";
|
|
414
|
+
const str = typeof value === "object" ? JSON.stringify(value) : String(value);
|
|
415
|
+
if (str.includes(",") || str.includes('"') || str.includes("\n")) {
|
|
416
|
+
return `"${str.replace(/"/g, '""')}"`;
|
|
417
|
+
}
|
|
418
|
+
return str;
|
|
419
|
+
}).join(",");
|
|
420
|
+
});
|
|
421
|
+
return [headerRow, ...rows].join("\n");
|
|
422
|
+
}
|
|
423
|
+
function clamp(value, min, max) {
|
|
424
|
+
return Math.min(Math.max(value, min), max);
|
|
425
|
+
}
|
|
426
|
+
var ActionLogger = class {
|
|
427
|
+
config;
|
|
428
|
+
store;
|
|
429
|
+
digestChain;
|
|
430
|
+
batch = [];
|
|
431
|
+
flushTimer = null;
|
|
432
|
+
batchSize;
|
|
433
|
+
flushIntervalMs;
|
|
434
|
+
isCloudMode;
|
|
435
|
+
constructor(config, store) {
|
|
436
|
+
this.config = config;
|
|
437
|
+
this.store = store;
|
|
438
|
+
this.digestChain = new DigestChain();
|
|
439
|
+
this.batchSize = config.batchSize ?? 50;
|
|
440
|
+
this.flushIntervalMs = config.flushIntervalMs ?? 5e3;
|
|
441
|
+
this.isCloudMode = !!config.apiKey;
|
|
442
|
+
this.flushTimer = setInterval(() => {
|
|
443
|
+
void this.flush();
|
|
444
|
+
}, this.flushIntervalMs);
|
|
445
|
+
if (this.flushTimer && typeof this.flushTimer === "object" && "unref" in this.flushTimer) {
|
|
446
|
+
this.flushTimer.unref();
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Log a generic agent action.
|
|
451
|
+
*
|
|
452
|
+
* @param input - Action details including type, description, agentId, and metadata
|
|
453
|
+
* @returns The created ActionLog entry
|
|
454
|
+
*
|
|
455
|
+
* @example
|
|
456
|
+
* ```typescript
|
|
457
|
+
* const action = await logger.log({
|
|
458
|
+
* type: 'approval',
|
|
459
|
+
* description: 'Agent approved USDC spending',
|
|
460
|
+
* agentId: 'agent-1',
|
|
461
|
+
* metadata: { spender: '0x...', amount: '1000' },
|
|
462
|
+
* });
|
|
463
|
+
* ```
|
|
464
|
+
*/
|
|
465
|
+
async log(input) {
|
|
466
|
+
const action = {
|
|
467
|
+
id: generateId(),
|
|
468
|
+
timestamp: now(),
|
|
469
|
+
projectId: this.config.projectId,
|
|
470
|
+
agentId: input.agentId,
|
|
471
|
+
correlationId: input.correlationId ?? generateId(),
|
|
472
|
+
type: input.type,
|
|
473
|
+
description: input.description,
|
|
474
|
+
metadata: input.metadata ?? {}
|
|
475
|
+
};
|
|
476
|
+
const link = this.digestChain.append(action);
|
|
477
|
+
action.digest = link.digest;
|
|
478
|
+
action.priorDigest = link.priorDigest;
|
|
479
|
+
this.store.addAction(action);
|
|
480
|
+
this.batch.push(action);
|
|
481
|
+
if (this.batch.length >= this.batchSize) {
|
|
482
|
+
await this.flush();
|
|
483
|
+
}
|
|
484
|
+
if (this.config.debug) {
|
|
485
|
+
this.debugLog("Action logged", action);
|
|
486
|
+
}
|
|
487
|
+
return action;
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Log a cryptocurrency transaction with full chain details.
|
|
491
|
+
*
|
|
492
|
+
* @param input - Transaction details including txHash, chain, amount, token, from, to
|
|
493
|
+
* @returns The created TransactionRecord
|
|
494
|
+
*
|
|
495
|
+
* @example
|
|
496
|
+
* ```typescript
|
|
497
|
+
* const tx = await logger.logTransaction({
|
|
498
|
+
* txHash: '0xabc123...',
|
|
499
|
+
* chain: 'base',
|
|
500
|
+
* amount: '100.00',
|
|
501
|
+
* token: 'USDC',
|
|
502
|
+
* from: '0xSender...',
|
|
503
|
+
* to: '0xReceiver...',
|
|
504
|
+
* agentId: 'payment-agent-1',
|
|
505
|
+
* });
|
|
506
|
+
* ```
|
|
507
|
+
*/
|
|
508
|
+
async logTransaction(input) {
|
|
509
|
+
this.validateTransactionInput(input);
|
|
510
|
+
const correlationId = input.correlationId ?? generateId();
|
|
511
|
+
const record = {
|
|
512
|
+
id: generateId(),
|
|
513
|
+
timestamp: now(),
|
|
514
|
+
projectId: this.config.projectId,
|
|
515
|
+
agentId: input.agentId,
|
|
516
|
+
correlationId,
|
|
517
|
+
type: "transaction",
|
|
518
|
+
description: `${input.token} transfer of ${input.amount} on ${input.chain}`,
|
|
519
|
+
metadata: {
|
|
520
|
+
...input.metadata
|
|
521
|
+
},
|
|
522
|
+
txHash: input.txHash,
|
|
523
|
+
chain: input.chain,
|
|
524
|
+
amount: input.amount,
|
|
525
|
+
token: input.token,
|
|
526
|
+
from: input.from,
|
|
527
|
+
to: input.to
|
|
528
|
+
};
|
|
529
|
+
const link = this.digestChain.append(record);
|
|
530
|
+
record.digest = link.digest;
|
|
531
|
+
record.priorDigest = link.priorDigest;
|
|
532
|
+
this.store.addTransaction(record);
|
|
533
|
+
this.store.addAction(record);
|
|
534
|
+
this.batch.push(record);
|
|
535
|
+
if (this.batch.length >= this.batchSize) {
|
|
536
|
+
await this.flush();
|
|
537
|
+
}
|
|
538
|
+
if (this.config.debug) {
|
|
539
|
+
this.debugLog("Transaction logged", record);
|
|
540
|
+
}
|
|
541
|
+
return record;
|
|
542
|
+
}
|
|
543
|
+
/**
|
|
544
|
+
* Flush the current batch of logs.
|
|
545
|
+
* In local mode, writes to a JSON file.
|
|
546
|
+
* In cloud mode, sends to the Kontext API.
|
|
547
|
+
*/
|
|
548
|
+
async flush() {
|
|
549
|
+
if (this.batch.length === 0) return;
|
|
550
|
+
const toFlush = [...this.batch];
|
|
551
|
+
this.batch = [];
|
|
552
|
+
if (this.isCloudMode) {
|
|
553
|
+
await this.flushToApi(toFlush);
|
|
554
|
+
} else {
|
|
555
|
+
this.flushToFile(toFlush);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* Stop the logger and flush any remaining logs.
|
|
560
|
+
*/
|
|
561
|
+
async destroy() {
|
|
562
|
+
if (this.flushTimer) {
|
|
563
|
+
clearInterval(this.flushTimer);
|
|
564
|
+
this.flushTimer = null;
|
|
565
|
+
}
|
|
566
|
+
await this.flush();
|
|
567
|
+
}
|
|
568
|
+
// --------------------------------------------------------------------------
|
|
569
|
+
// Digest Chain Access
|
|
570
|
+
// --------------------------------------------------------------------------
|
|
571
|
+
/**
|
|
572
|
+
* Get the terminal digest — the latest SHA-256 digest in the chain.
|
|
573
|
+
* Can be embedded in outgoing messages as tamper-evident proof.
|
|
574
|
+
*/
|
|
575
|
+
getTerminalDigest() {
|
|
576
|
+
return this.digestChain.getTerminalDigest();
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* Get the full digest chain for export or verification.
|
|
580
|
+
*/
|
|
581
|
+
getDigestChain() {
|
|
582
|
+
return this.digestChain;
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* Verify the integrity of the digest chain against stored actions.
|
|
586
|
+
*/
|
|
587
|
+
verifyChain(actions) {
|
|
588
|
+
return this.digestChain.verify(actions);
|
|
589
|
+
}
|
|
590
|
+
// --------------------------------------------------------------------------
|
|
591
|
+
// Private helpers
|
|
592
|
+
// --------------------------------------------------------------------------
|
|
593
|
+
validateTransactionInput(input) {
|
|
594
|
+
const amount = parseAmount(input.amount);
|
|
595
|
+
if (isNaN(amount) || amount < 0) {
|
|
596
|
+
throw new KontextError(
|
|
597
|
+
"VALIDATION_ERROR" /* VALIDATION_ERROR */,
|
|
598
|
+
`Invalid transaction amount: ${input.amount}`,
|
|
599
|
+
{ field: "amount", value: input.amount }
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
if (!input.txHash || input.txHash.trim() === "") {
|
|
603
|
+
throw new KontextError(
|
|
604
|
+
"VALIDATION_ERROR" /* VALIDATION_ERROR */,
|
|
605
|
+
"Transaction hash is required",
|
|
606
|
+
{ field: "txHash" }
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
if (!input.from || input.from.trim() === "") {
|
|
610
|
+
throw new KontextError(
|
|
611
|
+
"VALIDATION_ERROR" /* VALIDATION_ERROR */,
|
|
612
|
+
"Sender address (from) is required",
|
|
613
|
+
{ field: "from" }
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
if (!input.to || input.to.trim() === "") {
|
|
617
|
+
throw new KontextError(
|
|
618
|
+
"VALIDATION_ERROR" /* VALIDATION_ERROR */,
|
|
619
|
+
"Recipient address (to) is required",
|
|
620
|
+
{ field: "to" }
|
|
621
|
+
);
|
|
622
|
+
}
|
|
623
|
+
const validChains = ["ethereum", "base", "polygon", "arbitrum", "optimism", "arc"];
|
|
624
|
+
if (!validChains.includes(input.chain)) {
|
|
625
|
+
throw new KontextError(
|
|
626
|
+
"VALIDATION_ERROR" /* VALIDATION_ERROR */,
|
|
627
|
+
`Invalid chain: ${input.chain}. Must be one of: ${validChains.join(", ")}`,
|
|
628
|
+
{ field: "chain", value: input.chain }
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
const validTokens = ["USDC", "USDT", "DAI", "EURC"];
|
|
632
|
+
if (!validTokens.includes(input.token)) {
|
|
633
|
+
throw new KontextError(
|
|
634
|
+
"VALIDATION_ERROR" /* VALIDATION_ERROR */,
|
|
635
|
+
`Invalid token: ${input.token}. Must be one of: ${validTokens.join(", ")}`,
|
|
636
|
+
{ field: "token", value: input.token }
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
flushToFile(actions) {
|
|
641
|
+
const outputDir = this.config.localOutputDir ?? ".kontext";
|
|
642
|
+
const logDir = path__namespace.join(outputDir, "logs");
|
|
643
|
+
try {
|
|
644
|
+
fs__namespace.mkdirSync(logDir, { recursive: true });
|
|
645
|
+
const filename = `actions-${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}.jsonl`;
|
|
646
|
+
const filePath = path__namespace.join(logDir, filename);
|
|
647
|
+
const lines = actions.map((a) => JSON.stringify(a)).join("\n") + "\n";
|
|
648
|
+
fs__namespace.appendFileSync(filePath, lines, "utf-8");
|
|
649
|
+
} catch (error) {
|
|
650
|
+
if (this.config.debug) {
|
|
651
|
+
this.debugLog("Failed to write log file", { error });
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
async flushToApi(actions) {
|
|
656
|
+
const apiUrl = this.config.apiUrl ?? "https://api.kontext.dev";
|
|
657
|
+
try {
|
|
658
|
+
const response = await fetch(`${apiUrl}/v1/actions`, {
|
|
659
|
+
method: "POST",
|
|
660
|
+
headers: {
|
|
661
|
+
"Content-Type": "application/json",
|
|
662
|
+
Authorization: `Bearer ${this.config.apiKey}`,
|
|
663
|
+
"X-Project-Id": this.config.projectId
|
|
664
|
+
},
|
|
665
|
+
body: JSON.stringify({ actions })
|
|
666
|
+
});
|
|
667
|
+
if (!response.ok) {
|
|
668
|
+
throw new KontextError(
|
|
669
|
+
"API_ERROR" /* API_ERROR */,
|
|
670
|
+
`API request failed with status ${response.status}`,
|
|
671
|
+
{ status: response.status }
|
|
672
|
+
);
|
|
673
|
+
}
|
|
674
|
+
} catch (error) {
|
|
675
|
+
if (error instanceof KontextError) throw error;
|
|
676
|
+
if (this.config.debug) {
|
|
677
|
+
this.debugLog("API flush failed, falling back to local file", { error });
|
|
678
|
+
}
|
|
679
|
+
this.flushToFile(actions);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
debugLog(message, data) {
|
|
683
|
+
const timestamp = now();
|
|
684
|
+
console.debug(`[Kontext ${timestamp}] ${message}`, data ? JSON.stringify(data, null, 2) : "");
|
|
685
|
+
}
|
|
686
|
+
};
|
|
687
|
+
|
|
688
|
+
// src/tasks.ts
|
|
689
|
+
var DEFAULT_EXPIRATION_MS = 24 * 60 * 60 * 1e3;
|
|
690
|
+
var TaskManager = class {
|
|
691
|
+
config;
|
|
692
|
+
store;
|
|
693
|
+
constructor(config, store) {
|
|
694
|
+
this.config = config;
|
|
695
|
+
this.store = store;
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
698
|
+
* Create a new tracked task.
|
|
699
|
+
*
|
|
700
|
+
* @param input - Task details including description, agentId, and required evidence types
|
|
701
|
+
* @returns The created Task
|
|
702
|
+
*
|
|
703
|
+
* @example
|
|
704
|
+
* ```typescript
|
|
705
|
+
* const task = await taskManager.createTask({
|
|
706
|
+
* description: 'Transfer 100 USDC to vendor wallet',
|
|
707
|
+
* agentId: 'payment-agent-1',
|
|
708
|
+
* requiredEvidence: ['txHash', 'receipt'],
|
|
709
|
+
* });
|
|
710
|
+
* ```
|
|
711
|
+
*/
|
|
712
|
+
async createTask(input) {
|
|
713
|
+
this.validateCreateInput(input);
|
|
714
|
+
const id = generateId();
|
|
715
|
+
const timestamp = now();
|
|
716
|
+
const expiresInMs = input.expiresInMs ?? DEFAULT_EXPIRATION_MS;
|
|
717
|
+
const task = {
|
|
718
|
+
id,
|
|
719
|
+
projectId: this.config.projectId,
|
|
720
|
+
description: input.description,
|
|
721
|
+
agentId: input.agentId,
|
|
722
|
+
status: "pending",
|
|
723
|
+
requiredEvidence: input.requiredEvidence,
|
|
724
|
+
providedEvidence: null,
|
|
725
|
+
correlationId: input.correlationId ?? generateId(),
|
|
726
|
+
createdAt: timestamp,
|
|
727
|
+
updatedAt: timestamp,
|
|
728
|
+
confirmedAt: null,
|
|
729
|
+
expiresAt: new Date(Date.now() + expiresInMs).toISOString(),
|
|
730
|
+
metadata: input.metadata ?? {}
|
|
731
|
+
};
|
|
732
|
+
this.store.addTask(task);
|
|
733
|
+
if (this.config.debug) {
|
|
734
|
+
console.debug(`[Kontext] Task created: ${id} - ${input.description}`);
|
|
735
|
+
}
|
|
736
|
+
return task;
|
|
737
|
+
}
|
|
738
|
+
/**
|
|
739
|
+
* Confirm a task by providing evidence.
|
|
740
|
+
* Validates that all required evidence types are present.
|
|
741
|
+
*
|
|
742
|
+
* @param input - Task ID and evidence data
|
|
743
|
+
* @returns The updated Task
|
|
744
|
+
*
|
|
745
|
+
* @example
|
|
746
|
+
* ```typescript
|
|
747
|
+
* const confirmed = await taskManager.confirmTask({
|
|
748
|
+
* taskId: 'task-123',
|
|
749
|
+
* evidence: {
|
|
750
|
+
* txHash: '0xabc123...',
|
|
751
|
+
* receipt: { status: 'confirmed', blockNumber: 12345 },
|
|
752
|
+
* },
|
|
753
|
+
* });
|
|
754
|
+
* ```
|
|
755
|
+
*/
|
|
756
|
+
async confirmTask(input) {
|
|
757
|
+
const task = this.store.getTask(input.taskId);
|
|
758
|
+
if (!task) {
|
|
759
|
+
throw new KontextError(
|
|
760
|
+
"TASK_NOT_FOUND" /* TASK_NOT_FOUND */,
|
|
761
|
+
`Task not found: ${input.taskId}`,
|
|
762
|
+
{ taskId: input.taskId }
|
|
763
|
+
);
|
|
764
|
+
}
|
|
765
|
+
if (task.status === "confirmed") {
|
|
766
|
+
throw new KontextError(
|
|
767
|
+
"TASK_ALREADY_CONFIRMED" /* TASK_ALREADY_CONFIRMED */,
|
|
768
|
+
`Task already confirmed: ${input.taskId}`,
|
|
769
|
+
{ taskId: input.taskId, confirmedAt: task.confirmedAt }
|
|
770
|
+
);
|
|
771
|
+
}
|
|
772
|
+
if (task.expiresAt && new Date(task.expiresAt) < /* @__PURE__ */ new Date()) {
|
|
773
|
+
this.store.updateTask(input.taskId, {
|
|
774
|
+
status: "expired",
|
|
775
|
+
updatedAt: now()
|
|
776
|
+
});
|
|
777
|
+
throw new KontextError(
|
|
778
|
+
"TASK_EXPIRED" /* TASK_EXPIRED */,
|
|
779
|
+
`Task has expired: ${input.taskId}`,
|
|
780
|
+
{ taskId: input.taskId, expiresAt: task.expiresAt }
|
|
781
|
+
);
|
|
782
|
+
}
|
|
783
|
+
const missingEvidence = this.findMissingEvidence(task.requiredEvidence, input.evidence);
|
|
784
|
+
if (missingEvidence.length > 0) {
|
|
785
|
+
throw new KontextError(
|
|
786
|
+
"INSUFFICIENT_EVIDENCE" /* INSUFFICIENT_EVIDENCE */,
|
|
787
|
+
`Missing required evidence: ${missingEvidence.join(", ")}`,
|
|
788
|
+
{ taskId: input.taskId, missingEvidence }
|
|
789
|
+
);
|
|
790
|
+
}
|
|
791
|
+
const timestamp = now();
|
|
792
|
+
const updated = this.store.updateTask(input.taskId, {
|
|
793
|
+
status: "confirmed",
|
|
794
|
+
providedEvidence: input.evidence,
|
|
795
|
+
confirmedAt: timestamp,
|
|
796
|
+
updatedAt: timestamp
|
|
797
|
+
});
|
|
798
|
+
if (!updated) {
|
|
799
|
+
throw new KontextError(
|
|
800
|
+
"TASK_NOT_FOUND" /* TASK_NOT_FOUND */,
|
|
801
|
+
`Failed to update task: ${input.taskId}`,
|
|
802
|
+
{ taskId: input.taskId }
|
|
803
|
+
);
|
|
804
|
+
}
|
|
805
|
+
if (this.config.debug) {
|
|
806
|
+
console.debug(`[Kontext] Task confirmed: ${input.taskId}`);
|
|
807
|
+
}
|
|
808
|
+
return updated;
|
|
809
|
+
}
|
|
810
|
+
/**
|
|
811
|
+
* Get the current status and details of a task.
|
|
812
|
+
*
|
|
813
|
+
* @param taskId - The task identifier
|
|
814
|
+
* @returns The task, or undefined if not found
|
|
815
|
+
*/
|
|
816
|
+
async getTaskStatus(taskId) {
|
|
817
|
+
const task = this.store.getTask(taskId);
|
|
818
|
+
if (!task) return void 0;
|
|
819
|
+
if (task.expiresAt && task.status === "pending" && new Date(task.expiresAt) < /* @__PURE__ */ new Date()) {
|
|
820
|
+
const updated = this.store.updateTask(taskId, {
|
|
821
|
+
status: "expired",
|
|
822
|
+
updatedAt: now()
|
|
823
|
+
});
|
|
824
|
+
return updated;
|
|
825
|
+
}
|
|
826
|
+
return task;
|
|
827
|
+
}
|
|
828
|
+
/**
|
|
829
|
+
* Mark a task as in-progress.
|
|
830
|
+
*
|
|
831
|
+
* @param taskId - The task identifier
|
|
832
|
+
* @returns The updated Task
|
|
833
|
+
*/
|
|
834
|
+
async startTask(taskId) {
|
|
835
|
+
const task = this.store.getTask(taskId);
|
|
836
|
+
if (!task) {
|
|
837
|
+
throw new KontextError(
|
|
838
|
+
"TASK_NOT_FOUND" /* TASK_NOT_FOUND */,
|
|
839
|
+
`Task not found: ${taskId}`,
|
|
840
|
+
{ taskId }
|
|
841
|
+
);
|
|
842
|
+
}
|
|
843
|
+
if (task.status !== "pending") {
|
|
844
|
+
throw new KontextError(
|
|
845
|
+
"VALIDATION_ERROR" /* VALIDATION_ERROR */,
|
|
846
|
+
`Task cannot be started from status: ${task.status}`,
|
|
847
|
+
{ taskId, currentStatus: task.status }
|
|
848
|
+
);
|
|
849
|
+
}
|
|
850
|
+
const updated = this.store.updateTask(taskId, {
|
|
851
|
+
status: "in_progress",
|
|
852
|
+
updatedAt: now()
|
|
853
|
+
});
|
|
854
|
+
if (!updated) {
|
|
855
|
+
throw new KontextError(
|
|
856
|
+
"TASK_NOT_FOUND" /* TASK_NOT_FOUND */,
|
|
857
|
+
`Failed to update task: ${taskId}`,
|
|
858
|
+
{ taskId }
|
|
859
|
+
);
|
|
860
|
+
}
|
|
861
|
+
return updated;
|
|
862
|
+
}
|
|
863
|
+
/**
|
|
864
|
+
* Mark a task as failed.
|
|
865
|
+
*
|
|
866
|
+
* @param taskId - The task identifier
|
|
867
|
+
* @param reason - Reason for failure
|
|
868
|
+
* @returns The updated Task
|
|
869
|
+
*/
|
|
870
|
+
async failTask(taskId, reason) {
|
|
871
|
+
const task = this.store.getTask(taskId);
|
|
872
|
+
if (!task) {
|
|
873
|
+
throw new KontextError(
|
|
874
|
+
"TASK_NOT_FOUND" /* TASK_NOT_FOUND */,
|
|
875
|
+
`Task not found: ${taskId}`,
|
|
876
|
+
{ taskId }
|
|
877
|
+
);
|
|
878
|
+
}
|
|
879
|
+
const updated = this.store.updateTask(taskId, {
|
|
880
|
+
status: "failed",
|
|
881
|
+
updatedAt: now(),
|
|
882
|
+
metadata: { ...task.metadata, failureReason: reason }
|
|
883
|
+
});
|
|
884
|
+
if (!updated) {
|
|
885
|
+
throw new KontextError(
|
|
886
|
+
"TASK_NOT_FOUND" /* TASK_NOT_FOUND */,
|
|
887
|
+
`Failed to update task: ${taskId}`,
|
|
888
|
+
{ taskId }
|
|
889
|
+
);
|
|
890
|
+
}
|
|
891
|
+
return updated;
|
|
892
|
+
}
|
|
893
|
+
/**
|
|
894
|
+
* Get all tasks, optionally filtered by status.
|
|
895
|
+
*
|
|
896
|
+
* @param status - Optional status filter
|
|
897
|
+
* @returns Array of matching tasks
|
|
898
|
+
*/
|
|
899
|
+
getTasks(status) {
|
|
900
|
+
if (status) {
|
|
901
|
+
return this.store.queryTasks((t) => t.status === status);
|
|
902
|
+
}
|
|
903
|
+
return this.store.getTasks();
|
|
904
|
+
}
|
|
905
|
+
// --------------------------------------------------------------------------
|
|
906
|
+
// Private helpers
|
|
907
|
+
// --------------------------------------------------------------------------
|
|
908
|
+
validateCreateInput(input) {
|
|
909
|
+
if (!input.description || input.description.trim() === "") {
|
|
910
|
+
throw new KontextError(
|
|
911
|
+
"VALIDATION_ERROR" /* VALIDATION_ERROR */,
|
|
912
|
+
"Task description is required",
|
|
913
|
+
{ field: "description" }
|
|
914
|
+
);
|
|
915
|
+
}
|
|
916
|
+
if (!input.agentId || input.agentId.trim() === "") {
|
|
917
|
+
throw new KontextError(
|
|
918
|
+
"VALIDATION_ERROR" /* VALIDATION_ERROR */,
|
|
919
|
+
"Agent ID is required",
|
|
920
|
+
{ field: "agentId" }
|
|
921
|
+
);
|
|
922
|
+
}
|
|
923
|
+
if (!input.requiredEvidence || input.requiredEvidence.length === 0) {
|
|
924
|
+
throw new KontextError(
|
|
925
|
+
"VALIDATION_ERROR" /* VALIDATION_ERROR */,
|
|
926
|
+
"At least one required evidence type must be specified",
|
|
927
|
+
{ field: "requiredEvidence" }
|
|
928
|
+
);
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
findMissingEvidence(required, provided) {
|
|
932
|
+
return required.filter((key) => {
|
|
933
|
+
const value = provided[key];
|
|
934
|
+
return value === void 0 || value === null;
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
};
|
|
938
|
+
|
|
939
|
+
// src/audit.ts
|
|
940
|
+
var AuditExporter = class {
|
|
941
|
+
config;
|
|
942
|
+
store;
|
|
943
|
+
constructor(config, store) {
|
|
944
|
+
this.config = config;
|
|
945
|
+
this.store = store;
|
|
946
|
+
}
|
|
947
|
+
/**
|
|
948
|
+
* Export audit data in the specified format.
|
|
949
|
+
*
|
|
950
|
+
* @param options - Export configuration including format, date range, and filters
|
|
951
|
+
* @returns ExportResult containing the formatted data
|
|
952
|
+
*
|
|
953
|
+
* @example
|
|
954
|
+
* ```typescript
|
|
955
|
+
* const result = await exporter.export({
|
|
956
|
+
* format: 'json',
|
|
957
|
+
* dateRange: { start: new Date('2026-01-01'), end: new Date() },
|
|
958
|
+
* agentIds: ['payment-agent-1'],
|
|
959
|
+
* includeTasks: true,
|
|
960
|
+
* includeAnomalies: true,
|
|
961
|
+
* });
|
|
962
|
+
* ```
|
|
963
|
+
*/
|
|
964
|
+
async export(options) {
|
|
965
|
+
const actions = this.filterActions(options);
|
|
966
|
+
const transactions = this.filterTransactions(options);
|
|
967
|
+
const tasks = options.includeTasks ? this.filterTasks(options) : [];
|
|
968
|
+
const anomalies = options.includeAnomalies ? this.filterAnomalies(options) : [];
|
|
969
|
+
const records = {
|
|
970
|
+
actions,
|
|
971
|
+
transactions,
|
|
972
|
+
tasks,
|
|
973
|
+
anomalies,
|
|
974
|
+
exportMetadata: {
|
|
975
|
+
projectId: this.config.projectId,
|
|
976
|
+
exportedAt: now(),
|
|
977
|
+
filters: {
|
|
978
|
+
dateRange: options.dateRange ? { start: options.dateRange.start.toISOString(), end: options.dateRange.end.toISOString() } : null,
|
|
979
|
+
agentIds: options.agentIds ?? null,
|
|
980
|
+
types: options.types ?? null,
|
|
981
|
+
chains: options.chains ?? null
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
};
|
|
985
|
+
const totalCount = actions.length + transactions.length + tasks.length + anomalies.length;
|
|
986
|
+
let data;
|
|
987
|
+
if (options.format === "csv") {
|
|
988
|
+
data = this.formatAsCsv(actions, transactions, tasks, anomalies);
|
|
989
|
+
} else {
|
|
990
|
+
data = JSON.stringify(records, null, 2);
|
|
991
|
+
}
|
|
992
|
+
return {
|
|
993
|
+
format: options.format,
|
|
994
|
+
exportedAt: now(),
|
|
995
|
+
recordCount: totalCount,
|
|
996
|
+
data
|
|
997
|
+
};
|
|
998
|
+
}
|
|
999
|
+
/**
|
|
1000
|
+
* Generate a compliance report for a given period.
|
|
1001
|
+
*
|
|
1002
|
+
* @param options - Report configuration including type, period, and filters
|
|
1003
|
+
* @returns ComplianceReport with summary statistics and detailed records
|
|
1004
|
+
*
|
|
1005
|
+
* @example
|
|
1006
|
+
* ```typescript
|
|
1007
|
+
* const report = await exporter.generateReport({
|
|
1008
|
+
* type: 'compliance',
|
|
1009
|
+
* period: { start: new Date('2026-01-01'), end: new Date() },
|
|
1010
|
+
* });
|
|
1011
|
+
* ```
|
|
1012
|
+
*/
|
|
1013
|
+
async generateReport(options) {
|
|
1014
|
+
const exportOptions = {
|
|
1015
|
+
format: "json",
|
|
1016
|
+
dateRange: options.period,
|
|
1017
|
+
agentIds: options.agentIds,
|
|
1018
|
+
includeTasks: true,
|
|
1019
|
+
includeAnomalies: true
|
|
1020
|
+
};
|
|
1021
|
+
const actions = this.filterActions(exportOptions);
|
|
1022
|
+
const transactions = this.filterTransactions(exportOptions);
|
|
1023
|
+
const tasks = this.filterTasks(exportOptions);
|
|
1024
|
+
const anomalies = this.filterAnomalies(exportOptions);
|
|
1025
|
+
const confirmedTasks = tasks.filter((t) => t.status === "confirmed").length;
|
|
1026
|
+
const failedTasks = tasks.filter((t) => t.status === "failed").length;
|
|
1027
|
+
const taskCompletionRate = tasks.length > 0 ? confirmedTasks / tasks.length : 1;
|
|
1028
|
+
const anomalyRate = actions.length > 0 ? 1 - anomalies.length / actions.length : 1;
|
|
1029
|
+
const averageTrustScore = Math.round(
|
|
1030
|
+
(taskCompletionRate * 50 + anomalyRate * 50) * 100
|
|
1031
|
+
) / 100;
|
|
1032
|
+
const report = {
|
|
1033
|
+
id: generateId(),
|
|
1034
|
+
type: options.type,
|
|
1035
|
+
generatedAt: now(),
|
|
1036
|
+
period: options.period,
|
|
1037
|
+
projectId: this.config.projectId,
|
|
1038
|
+
summary: {
|
|
1039
|
+
totalActions: actions.length,
|
|
1040
|
+
totalTransactions: transactions.length,
|
|
1041
|
+
totalTasks: tasks.length,
|
|
1042
|
+
confirmedTasks,
|
|
1043
|
+
failedTasks,
|
|
1044
|
+
totalAnomalies: anomalies.length,
|
|
1045
|
+
averageTrustScore
|
|
1046
|
+
},
|
|
1047
|
+
actions,
|
|
1048
|
+
transactions,
|
|
1049
|
+
tasks,
|
|
1050
|
+
anomalies
|
|
1051
|
+
};
|
|
1052
|
+
return report;
|
|
1053
|
+
}
|
|
1054
|
+
// --------------------------------------------------------------------------
|
|
1055
|
+
// SAR Report Generation
|
|
1056
|
+
// --------------------------------------------------------------------------
|
|
1057
|
+
/**
|
|
1058
|
+
* Generate a Suspicious Activity Report (SAR) template.
|
|
1059
|
+
*
|
|
1060
|
+
* This produces a structured SAR template populated with data from the SDK.
|
|
1061
|
+
* It is a template/structure, not an actual regulatory filing. Organizations
|
|
1062
|
+
* should review and supplement this data before formal submission.
|
|
1063
|
+
*
|
|
1064
|
+
* @param options - Report configuration including period and optional filters
|
|
1065
|
+
* @returns SARReport template populated with flagged transactions and anomalies
|
|
1066
|
+
*
|
|
1067
|
+
* @example
|
|
1068
|
+
* ```typescript
|
|
1069
|
+
* const sar = await exporter.generateSARReport({
|
|
1070
|
+
* type: 'sar',
|
|
1071
|
+
* period: { start: new Date('2026-01-01'), end: new Date() },
|
|
1072
|
+
* });
|
|
1073
|
+
* console.log(`SAR contains ${sar.suspiciousTransactions.length} flagged transactions`);
|
|
1074
|
+
* ```
|
|
1075
|
+
*/
|
|
1076
|
+
async generateSARReport(options) {
|
|
1077
|
+
const exportOptions = {
|
|
1078
|
+
format: "json",
|
|
1079
|
+
dateRange: options.period,
|
|
1080
|
+
agentIds: options.agentIds,
|
|
1081
|
+
includeTasks: true,
|
|
1082
|
+
includeAnomalies: true
|
|
1083
|
+
};
|
|
1084
|
+
const actions = this.filterActions(exportOptions);
|
|
1085
|
+
const transactions = this.filterTransactions(exportOptions);
|
|
1086
|
+
const anomalies = this.filterAnomalies(exportOptions);
|
|
1087
|
+
const anomalyActionIds = new Set(anomalies.map((a) => a.actionId));
|
|
1088
|
+
const suspiciousTransactions = transactions.filter(
|
|
1089
|
+
(tx) => anomalyActionIds.has(tx.id)
|
|
1090
|
+
);
|
|
1091
|
+
const anomalyAgentIds = new Set(anomalies.map((a) => a.agentId));
|
|
1092
|
+
const additionalSuspicious = suspiciousTransactions.length === 0 ? transactions.filter((tx) => anomalyAgentIds.has(tx.agentId)) : [];
|
|
1093
|
+
const allSuspicious = [...suspiciousTransactions, ...additionalSuspicious];
|
|
1094
|
+
const subjectMap = /* @__PURE__ */ new Map();
|
|
1095
|
+
for (const tx of allSuspicious) {
|
|
1096
|
+
if (!subjectMap.has(tx.agentId)) {
|
|
1097
|
+
const agentTxs = transactions.filter((t) => t.agentId === tx.agentId);
|
|
1098
|
+
const addresses = /* @__PURE__ */ new Set();
|
|
1099
|
+
for (const t of agentTxs) {
|
|
1100
|
+
addresses.add(t.from);
|
|
1101
|
+
addresses.add(t.to);
|
|
1102
|
+
}
|
|
1103
|
+
subjectMap.set(tx.agentId, {
|
|
1104
|
+
name: tx.agentId,
|
|
1105
|
+
agentId: tx.agentId,
|
|
1106
|
+
addresses: Array.from(addresses)
|
|
1107
|
+
});
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
const totalAmount = allSuspicious.reduce((sum, tx) => sum + (parseAmount(tx.amount) || 0), 0).toFixed(2);
|
|
1111
|
+
const tokenCounts = /* @__PURE__ */ new Map();
|
|
1112
|
+
for (const tx of allSuspicious) {
|
|
1113
|
+
tokenCounts.set(tx.token, (tokenCounts.get(tx.token) ?? 0) + 1);
|
|
1114
|
+
}
|
|
1115
|
+
const currency = tokenCounts.size > 0 ? Array.from(tokenCounts.entries()).sort((a, b) => b[1] - a[1])[0][0] : "USDC";
|
|
1116
|
+
const activityCategories = Array.from(
|
|
1117
|
+
new Set(anomalies.map((a) => this.anomalyTypeToCategory(a.type)))
|
|
1118
|
+
);
|
|
1119
|
+
const narrative = this.generateSARNarrative(
|
|
1120
|
+
allSuspicious,
|
|
1121
|
+
anomalies,
|
|
1122
|
+
options.period
|
|
1123
|
+
);
|
|
1124
|
+
return {
|
|
1125
|
+
id: generateId(),
|
|
1126
|
+
type: "sar",
|
|
1127
|
+
generatedAt: now(),
|
|
1128
|
+
period: options.period,
|
|
1129
|
+
projectId: this.config.projectId,
|
|
1130
|
+
filingInstitution: this.config.projectId,
|
|
1131
|
+
subjects: Array.from(subjectMap.values()),
|
|
1132
|
+
narrative,
|
|
1133
|
+
activityCategories,
|
|
1134
|
+
totalAmount,
|
|
1135
|
+
currency,
|
|
1136
|
+
suspiciousTransactions: allSuspicious,
|
|
1137
|
+
anomalies,
|
|
1138
|
+
supportingActions: actions.filter(
|
|
1139
|
+
(a) => anomalyAgentIds.has(a.agentId)
|
|
1140
|
+
),
|
|
1141
|
+
isContinuingActivity: false,
|
|
1142
|
+
priorReportId: null,
|
|
1143
|
+
status: "draft"
|
|
1144
|
+
};
|
|
1145
|
+
}
|
|
1146
|
+
// --------------------------------------------------------------------------
|
|
1147
|
+
// CTR Report Generation
|
|
1148
|
+
// --------------------------------------------------------------------------
|
|
1149
|
+
/**
|
|
1150
|
+
* Generate a Currency Transaction Report (CTR) template.
|
|
1151
|
+
*
|
|
1152
|
+
* This produces a structured CTR template for transactions that meet or
|
|
1153
|
+
* exceed reporting thresholds. It is a template/structure, not an actual
|
|
1154
|
+
* regulatory filing. Organizations should review and supplement this data
|
|
1155
|
+
* before formal submission.
|
|
1156
|
+
*
|
|
1157
|
+
* @param options - Report configuration including period and optional filters
|
|
1158
|
+
* @returns CTRReport template populated with qualifying transactions
|
|
1159
|
+
*
|
|
1160
|
+
* @example
|
|
1161
|
+
* ```typescript
|
|
1162
|
+
* const ctr = await exporter.generateCTRReport({
|
|
1163
|
+
* type: 'ctr',
|
|
1164
|
+
* period: { start: new Date('2026-01-01'), end: new Date() },
|
|
1165
|
+
* });
|
|
1166
|
+
* console.log(`CTR covers ${ctr.transactions.length} reportable transactions`);
|
|
1167
|
+
* ```
|
|
1168
|
+
*/
|
|
1169
|
+
async generateCTRReport(options) {
|
|
1170
|
+
const REPORTING_THRESHOLD2 = 1e4;
|
|
1171
|
+
const exportOptions = {
|
|
1172
|
+
format: "json",
|
|
1173
|
+
dateRange: options.period,
|
|
1174
|
+
agentIds: options.agentIds,
|
|
1175
|
+
includeTasks: false,
|
|
1176
|
+
includeAnomalies: false
|
|
1177
|
+
};
|
|
1178
|
+
const actions = this.filterActions(exportOptions);
|
|
1179
|
+
const transactions = this.filterTransactions(exportOptions);
|
|
1180
|
+
const reportableTransactions = transactions.filter((tx) => {
|
|
1181
|
+
const amount = parseAmount(tx.amount);
|
|
1182
|
+
return !isNaN(amount) && amount >= REPORTING_THRESHOLD2;
|
|
1183
|
+
});
|
|
1184
|
+
const agentDailyTotals = /* @__PURE__ */ new Map();
|
|
1185
|
+
for (const tx of transactions) {
|
|
1186
|
+
const day = tx.timestamp.split("T")[0];
|
|
1187
|
+
const key = `${tx.agentId}:${day}`;
|
|
1188
|
+
agentDailyTotals.set(key, (agentDailyTotals.get(key) ?? 0) + (parseAmount(tx.amount) || 0));
|
|
1189
|
+
}
|
|
1190
|
+
const structuringKeys = /* @__PURE__ */ new Set();
|
|
1191
|
+
for (const [key, total] of agentDailyTotals.entries()) {
|
|
1192
|
+
if (total >= REPORTING_THRESHOLD2) {
|
|
1193
|
+
structuringKeys.add(key);
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
const reportableIds = new Set(reportableTransactions.map((tx) => tx.id));
|
|
1197
|
+
const additionalStructuring = transactions.filter((tx) => {
|
|
1198
|
+
if (reportableIds.has(tx.id)) return false;
|
|
1199
|
+
const day = tx.timestamp.split("T")[0];
|
|
1200
|
+
const key = `${tx.agentId}:${day}`;
|
|
1201
|
+
return structuringKeys.has(key);
|
|
1202
|
+
});
|
|
1203
|
+
const allReportable = [...reportableTransactions, ...additionalStructuring];
|
|
1204
|
+
const isAggregated = additionalStructuring.length > 0;
|
|
1205
|
+
const conductorMap = /* @__PURE__ */ new Map();
|
|
1206
|
+
for (const tx of allReportable) {
|
|
1207
|
+
if (!conductorMap.has(tx.agentId)) {
|
|
1208
|
+
const agentTxs = allReportable.filter((t) => t.agentId === tx.agentId);
|
|
1209
|
+
const addresses = /* @__PURE__ */ new Set();
|
|
1210
|
+
for (const t of agentTxs) {
|
|
1211
|
+
addresses.add(t.from);
|
|
1212
|
+
addresses.add(t.to);
|
|
1213
|
+
}
|
|
1214
|
+
conductorMap.set(tx.agentId, {
|
|
1215
|
+
name: tx.agentId,
|
|
1216
|
+
agentId: tx.agentId,
|
|
1217
|
+
addresses: Array.from(addresses)
|
|
1218
|
+
});
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
let cashIn = 0;
|
|
1222
|
+
let cashOut = 0;
|
|
1223
|
+
for (const tx of allReportable) {
|
|
1224
|
+
const amount = parseAmount(tx.amount) || 0;
|
|
1225
|
+
cashOut += amount;
|
|
1226
|
+
cashIn += amount;
|
|
1227
|
+
}
|
|
1228
|
+
const chainsInvolved = Array.from(
|
|
1229
|
+
new Set(allReportable.map((tx) => tx.chain))
|
|
1230
|
+
);
|
|
1231
|
+
const tokenCounts = /* @__PURE__ */ new Map();
|
|
1232
|
+
for (const tx of allReportable) {
|
|
1233
|
+
tokenCounts.set(tx.token, (tokenCounts.get(tx.token) ?? 0) + 1);
|
|
1234
|
+
}
|
|
1235
|
+
const currency = tokenCounts.size > 0 ? Array.from(tokenCounts.entries()).sort((a, b) => b[1] - a[1])[0][0] : "USDC";
|
|
1236
|
+
return {
|
|
1237
|
+
id: generateId(),
|
|
1238
|
+
type: "ctr",
|
|
1239
|
+
generatedAt: now(),
|
|
1240
|
+
period: options.period,
|
|
1241
|
+
projectId: this.config.projectId,
|
|
1242
|
+
filingInstitution: this.config.projectId,
|
|
1243
|
+
conductors: Array.from(conductorMap.values()),
|
|
1244
|
+
transactions: allReportable,
|
|
1245
|
+
totalCashIn: cashIn.toFixed(2),
|
|
1246
|
+
totalCashOut: cashOut.toFixed(2),
|
|
1247
|
+
currency,
|
|
1248
|
+
isAggregated,
|
|
1249
|
+
chainsInvolved,
|
|
1250
|
+
supportingActions: actions.filter(
|
|
1251
|
+
(a) => conductorMap.has(a.agentId)
|
|
1252
|
+
),
|
|
1253
|
+
status: "draft"
|
|
1254
|
+
};
|
|
1255
|
+
}
|
|
1256
|
+
// --------------------------------------------------------------------------
|
|
1257
|
+
// SAR/CTR helpers
|
|
1258
|
+
// --------------------------------------------------------------------------
|
|
1259
|
+
anomalyTypeToCategory(type) {
|
|
1260
|
+
const mapping = {
|
|
1261
|
+
unusualAmount: "Unusual transaction amount",
|
|
1262
|
+
frequencySpike: "Unusually high transaction frequency",
|
|
1263
|
+
newDestination: "Transactions to unknown destinations",
|
|
1264
|
+
offHoursActivity: "Activity during unusual hours",
|
|
1265
|
+
rapidSuccession: "Rapid succession of transactions",
|
|
1266
|
+
roundAmount: "Potential structuring (round amounts)"
|
|
1267
|
+
};
|
|
1268
|
+
return mapping[type] ?? `Other: ${type}`;
|
|
1269
|
+
}
|
|
1270
|
+
generateSARNarrative(transactions, anomalies, period) {
|
|
1271
|
+
const startDate = period.start.toISOString().split("T")[0];
|
|
1272
|
+
const endDate = period.end.toISOString().split("T")[0];
|
|
1273
|
+
const parts = [];
|
|
1274
|
+
parts.push(
|
|
1275
|
+
`During the period from ${startDate} to ${endDate}, ${transactions.length} transaction(s) were identified as potentially suspicious.`
|
|
1276
|
+
);
|
|
1277
|
+
if (anomalies.length > 0) {
|
|
1278
|
+
const typeCounts = /* @__PURE__ */ new Map();
|
|
1279
|
+
for (const a of anomalies) {
|
|
1280
|
+
typeCounts.set(a.type, (typeCounts.get(a.type) ?? 0) + 1);
|
|
1281
|
+
}
|
|
1282
|
+
const typeDesc = Array.from(typeCounts.entries()).map(([type, count]) => `${this.anomalyTypeToCategory(type)} (${count} occurrence(s))`).join("; ");
|
|
1283
|
+
parts.push(`Anomaly detection identified the following patterns: ${typeDesc}.`);
|
|
1284
|
+
}
|
|
1285
|
+
const totalAmount = transactions.reduce((sum, tx) => sum + (parseAmount(tx.amount) || 0), 0).toFixed(2);
|
|
1286
|
+
parts.push(`Total amount involved: ${totalAmount}.`);
|
|
1287
|
+
const agents = new Set(transactions.map((t) => t.agentId));
|
|
1288
|
+
parts.push(`Agent(s) involved: ${Array.from(agents).join(", ")}.`);
|
|
1289
|
+
parts.push(
|
|
1290
|
+
"This report is generated as a template for review. Additional investigation and supporting documentation should be attached before filing."
|
|
1291
|
+
);
|
|
1292
|
+
return parts.join(" ");
|
|
1293
|
+
}
|
|
1294
|
+
// --------------------------------------------------------------------------
|
|
1295
|
+
// Private filtering
|
|
1296
|
+
// --------------------------------------------------------------------------
|
|
1297
|
+
filterActions(options) {
|
|
1298
|
+
return this.store.queryActions((action) => {
|
|
1299
|
+
if (options.dateRange && !isWithinDateRange(action.timestamp, options.dateRange.start, options.dateRange.end)) {
|
|
1300
|
+
return false;
|
|
1301
|
+
}
|
|
1302
|
+
if (options.agentIds && !options.agentIds.includes(action.agentId)) {
|
|
1303
|
+
return false;
|
|
1304
|
+
}
|
|
1305
|
+
if (options.types && !options.types.includes(action.type)) {
|
|
1306
|
+
return false;
|
|
1307
|
+
}
|
|
1308
|
+
return true;
|
|
1309
|
+
});
|
|
1310
|
+
}
|
|
1311
|
+
filterTransactions(options) {
|
|
1312
|
+
return this.store.queryTransactions((tx) => {
|
|
1313
|
+
if (options.dateRange && !isWithinDateRange(tx.timestamp, options.dateRange.start, options.dateRange.end)) {
|
|
1314
|
+
return false;
|
|
1315
|
+
}
|
|
1316
|
+
if (options.agentIds && !options.agentIds.includes(tx.agentId)) {
|
|
1317
|
+
return false;
|
|
1318
|
+
}
|
|
1319
|
+
if (options.chains && !options.chains.includes(tx.chain)) {
|
|
1320
|
+
return false;
|
|
1321
|
+
}
|
|
1322
|
+
return true;
|
|
1323
|
+
});
|
|
1324
|
+
}
|
|
1325
|
+
filterTasks(options) {
|
|
1326
|
+
return this.store.queryTasks((task) => {
|
|
1327
|
+
if (options.dateRange && !isWithinDateRange(task.createdAt, options.dateRange.start, options.dateRange.end)) {
|
|
1328
|
+
return false;
|
|
1329
|
+
}
|
|
1330
|
+
if (options.agentIds && !options.agentIds.includes(task.agentId)) {
|
|
1331
|
+
return false;
|
|
1332
|
+
}
|
|
1333
|
+
return true;
|
|
1334
|
+
});
|
|
1335
|
+
}
|
|
1336
|
+
filterAnomalies(options) {
|
|
1337
|
+
return this.store.queryAnomalies((anomaly) => {
|
|
1338
|
+
if (options.dateRange && !isWithinDateRange(anomaly.detectedAt, options.dateRange.start, options.dateRange.end)) {
|
|
1339
|
+
return false;
|
|
1340
|
+
}
|
|
1341
|
+
if (options.agentIds && !options.agentIds.includes(anomaly.agentId)) {
|
|
1342
|
+
return false;
|
|
1343
|
+
}
|
|
1344
|
+
return true;
|
|
1345
|
+
});
|
|
1346
|
+
}
|
|
1347
|
+
// --------------------------------------------------------------------------
|
|
1348
|
+
// CSV formatting
|
|
1349
|
+
// --------------------------------------------------------------------------
|
|
1350
|
+
formatAsCsv(actions, transactions, tasks, anomalies) {
|
|
1351
|
+
const sections = [];
|
|
1352
|
+
if (actions.length > 0) {
|
|
1353
|
+
const actionRecords = actions.map((a) => ({
|
|
1354
|
+
section: "action",
|
|
1355
|
+
id: a.id,
|
|
1356
|
+
timestamp: a.timestamp,
|
|
1357
|
+
projectId: a.projectId,
|
|
1358
|
+
agentId: a.agentId,
|
|
1359
|
+
correlationId: a.correlationId,
|
|
1360
|
+
type: a.type,
|
|
1361
|
+
description: a.description,
|
|
1362
|
+
metadata: JSON.stringify(a.metadata)
|
|
1363
|
+
}));
|
|
1364
|
+
sections.push("# Actions\n" + toCsv(actionRecords));
|
|
1365
|
+
}
|
|
1366
|
+
if (transactions.length > 0) {
|
|
1367
|
+
const txRecords = transactions.map((t) => ({
|
|
1368
|
+
section: "transaction",
|
|
1369
|
+
id: t.id,
|
|
1370
|
+
timestamp: t.timestamp,
|
|
1371
|
+
txHash: t.txHash,
|
|
1372
|
+
chain: t.chain,
|
|
1373
|
+
amount: t.amount,
|
|
1374
|
+
token: t.token,
|
|
1375
|
+
from: t.from,
|
|
1376
|
+
to: t.to,
|
|
1377
|
+
agentId: t.agentId
|
|
1378
|
+
}));
|
|
1379
|
+
sections.push("# Transactions\n" + toCsv(txRecords));
|
|
1380
|
+
}
|
|
1381
|
+
if (tasks.length > 0) {
|
|
1382
|
+
const taskRecords = tasks.map((t) => ({
|
|
1383
|
+
section: "task",
|
|
1384
|
+
id: t.id,
|
|
1385
|
+
description: t.description,
|
|
1386
|
+
agentId: t.agentId,
|
|
1387
|
+
status: t.status,
|
|
1388
|
+
createdAt: t.createdAt,
|
|
1389
|
+
confirmedAt: t.confirmedAt ?? "",
|
|
1390
|
+
requiredEvidence: t.requiredEvidence.join(";")
|
|
1391
|
+
}));
|
|
1392
|
+
sections.push("# Tasks\n" + toCsv(taskRecords));
|
|
1393
|
+
}
|
|
1394
|
+
if (anomalies.length > 0) {
|
|
1395
|
+
const anomalyRecords = anomalies.map((a) => ({
|
|
1396
|
+
section: "anomaly",
|
|
1397
|
+
id: a.id,
|
|
1398
|
+
type: a.type,
|
|
1399
|
+
severity: a.severity,
|
|
1400
|
+
description: a.description,
|
|
1401
|
+
agentId: a.agentId,
|
|
1402
|
+
detectedAt: a.detectedAt,
|
|
1403
|
+
reviewed: String(a.reviewed)
|
|
1404
|
+
}));
|
|
1405
|
+
sections.push("# Anomalies\n" + toCsv(anomalyRecords));
|
|
1406
|
+
}
|
|
1407
|
+
return sections.join("\n\n");
|
|
1408
|
+
}
|
|
1409
|
+
};
|
|
1410
|
+
|
|
1411
|
+
// src/trust.ts
|
|
1412
|
+
var TrustScorer = class {
|
|
1413
|
+
config;
|
|
1414
|
+
store;
|
|
1415
|
+
constructor(config, store) {
|
|
1416
|
+
this.config = config;
|
|
1417
|
+
this.store = store;
|
|
1418
|
+
}
|
|
1419
|
+
/**
|
|
1420
|
+
* Compute the trust score for a given agent.
|
|
1421
|
+
*
|
|
1422
|
+
* @param agentId - The agent identifier
|
|
1423
|
+
* @returns TrustScore with overall score, factor breakdown, and trust level
|
|
1424
|
+
*
|
|
1425
|
+
* @example
|
|
1426
|
+
* ```typescript
|
|
1427
|
+
* const score = await scorer.getTrustScore('payment-agent-1');
|
|
1428
|
+
* console.log(`Trust: ${score.score}/100 (${score.level})`);
|
|
1429
|
+
* ```
|
|
1430
|
+
*/
|
|
1431
|
+
async getTrustScore(agentId) {
|
|
1432
|
+
const factors = this.computeAgentFactors(agentId);
|
|
1433
|
+
const weightedScore = factors.reduce((sum, f) => sum + f.score * f.weight, 0);
|
|
1434
|
+
const totalWeight = factors.reduce((sum, f) => sum + f.weight, 0);
|
|
1435
|
+
const score = totalWeight > 0 ? Math.round(weightedScore / totalWeight) : 50;
|
|
1436
|
+
const clampedScore = clamp(score, 0, 100);
|
|
1437
|
+
return {
|
|
1438
|
+
agentId,
|
|
1439
|
+
score: clampedScore,
|
|
1440
|
+
factors,
|
|
1441
|
+
computedAt: now(),
|
|
1442
|
+
level: this.scoreToLevel(clampedScore)
|
|
1443
|
+
};
|
|
1444
|
+
}
|
|
1445
|
+
/**
|
|
1446
|
+
* Evaluate the risk of a specific transaction.
|
|
1447
|
+
*
|
|
1448
|
+
* @param tx - Transaction input to evaluate
|
|
1449
|
+
* @returns TransactionEvaluation with risk score, factors, and recommendation
|
|
1450
|
+
*
|
|
1451
|
+
* @example
|
|
1452
|
+
* ```typescript
|
|
1453
|
+
* const eval = await scorer.evaluateTransaction({
|
|
1454
|
+
* txHash: '0x...',
|
|
1455
|
+
* chain: 'base',
|
|
1456
|
+
* amount: '50000',
|
|
1457
|
+
* token: 'USDC',
|
|
1458
|
+
* from: '0xSender',
|
|
1459
|
+
* to: '0xReceiver',
|
|
1460
|
+
* agentId: 'agent-1',
|
|
1461
|
+
* });
|
|
1462
|
+
* if (eval.flagged) console.log('Transaction flagged for review');
|
|
1463
|
+
* ```
|
|
1464
|
+
*/
|
|
1465
|
+
async evaluateTransaction(tx) {
|
|
1466
|
+
const factors = this.computeTransactionRiskFactors(tx);
|
|
1467
|
+
const totalScore = factors.reduce((sum, f) => sum + f.score, 0);
|
|
1468
|
+
const riskScore = clamp(Math.round(totalScore / Math.max(factors.length, 1)), 0, 100);
|
|
1469
|
+
const riskLevel = this.riskScoreToLevel(riskScore);
|
|
1470
|
+
const flagged = riskScore >= 60;
|
|
1471
|
+
const recommendation = riskScore >= 80 ? "block" : riskScore >= 50 ? "review" : "approve";
|
|
1472
|
+
return {
|
|
1473
|
+
txHash: tx.txHash,
|
|
1474
|
+
riskScore,
|
|
1475
|
+
riskLevel,
|
|
1476
|
+
factors,
|
|
1477
|
+
flagged,
|
|
1478
|
+
recommendation,
|
|
1479
|
+
evaluatedAt: now()
|
|
1480
|
+
};
|
|
1481
|
+
}
|
|
1482
|
+
// --------------------------------------------------------------------------
|
|
1483
|
+
// Agent trust factor computation
|
|
1484
|
+
// --------------------------------------------------------------------------
|
|
1485
|
+
computeAgentFactors(agentId) {
|
|
1486
|
+
const factors = [];
|
|
1487
|
+
factors.push(this.computeHistoryDepthFactor(agentId));
|
|
1488
|
+
factors.push(this.computeTaskCompletionFactor(agentId));
|
|
1489
|
+
factors.push(this.computeAnomalyFrequencyFactor(agentId));
|
|
1490
|
+
factors.push(this.computeTransactionConsistencyFactor(agentId));
|
|
1491
|
+
factors.push(this.computeComplianceAdherenceFactor(agentId));
|
|
1492
|
+
return factors;
|
|
1493
|
+
}
|
|
1494
|
+
computeHistoryDepthFactor(agentId) {
|
|
1495
|
+
const actions = this.store.getActionsByAgent(agentId);
|
|
1496
|
+
const count = actions.length;
|
|
1497
|
+
let score;
|
|
1498
|
+
if (count === 0) score = 10;
|
|
1499
|
+
else if (count < 5) score = 30;
|
|
1500
|
+
else if (count < 20) score = 50;
|
|
1501
|
+
else if (count < 50) score = 70;
|
|
1502
|
+
else if (count < 100) score = 85;
|
|
1503
|
+
else score = 95;
|
|
1504
|
+
return {
|
|
1505
|
+
name: "history_depth",
|
|
1506
|
+
score,
|
|
1507
|
+
weight: 0.15,
|
|
1508
|
+
description: `Agent has ${count} recorded actions`
|
|
1509
|
+
};
|
|
1510
|
+
}
|
|
1511
|
+
computeTaskCompletionFactor(agentId) {
|
|
1512
|
+
const tasks = this.store.queryTasks((t) => t.agentId === agentId);
|
|
1513
|
+
const totalTasks = tasks.length;
|
|
1514
|
+
if (totalTasks === 0) {
|
|
1515
|
+
return {
|
|
1516
|
+
name: "task_completion",
|
|
1517
|
+
score: 50,
|
|
1518
|
+
// Neutral if no tasks
|
|
1519
|
+
weight: 0.25,
|
|
1520
|
+
description: "No tasks recorded yet"
|
|
1521
|
+
};
|
|
1522
|
+
}
|
|
1523
|
+
const confirmed = tasks.filter((t) => t.status === "confirmed").length;
|
|
1524
|
+
const failed = tasks.filter((t) => t.status === "failed").length;
|
|
1525
|
+
const completionRate = confirmed / totalTasks;
|
|
1526
|
+
const failureRate = failed / totalTasks;
|
|
1527
|
+
const score = Math.round(completionRate * 100 - failureRate * 30);
|
|
1528
|
+
return {
|
|
1529
|
+
name: "task_completion",
|
|
1530
|
+
score: clamp(score, 0, 100),
|
|
1531
|
+
weight: 0.25,
|
|
1532
|
+
description: `${confirmed}/${totalTasks} tasks confirmed (${Math.round(completionRate * 100)}% rate)`
|
|
1533
|
+
};
|
|
1534
|
+
}
|
|
1535
|
+
computeAnomalyFrequencyFactor(agentId) {
|
|
1536
|
+
const anomalies = this.store.queryAnomalies((a) => a.agentId === agentId);
|
|
1537
|
+
const actions = this.store.getActionsByAgent(agentId);
|
|
1538
|
+
const anomalyCount = anomalies.length;
|
|
1539
|
+
const actionCount = actions.length;
|
|
1540
|
+
if (actionCount === 0) {
|
|
1541
|
+
return {
|
|
1542
|
+
name: "anomaly_frequency",
|
|
1543
|
+
score: 50,
|
|
1544
|
+
weight: 0.25,
|
|
1545
|
+
description: "No actions recorded yet"
|
|
1546
|
+
};
|
|
1547
|
+
}
|
|
1548
|
+
const anomalyRate = anomalyCount / actionCount;
|
|
1549
|
+
let score;
|
|
1550
|
+
if (anomalyRate === 0) score = 100;
|
|
1551
|
+
else if (anomalyRate < 0.01) score = 90;
|
|
1552
|
+
else if (anomalyRate < 0.05) score = 70;
|
|
1553
|
+
else if (anomalyRate < 0.1) score = 50;
|
|
1554
|
+
else if (anomalyRate < 0.25) score = 30;
|
|
1555
|
+
else score = 10;
|
|
1556
|
+
const criticalCount = anomalies.filter((a) => a.severity === "critical").length;
|
|
1557
|
+
const highCount = anomalies.filter((a) => a.severity === "high").length;
|
|
1558
|
+
const penaltyFromSeverity = criticalCount * 15 + highCount * 8;
|
|
1559
|
+
return {
|
|
1560
|
+
name: "anomaly_frequency",
|
|
1561
|
+
score: clamp(score - penaltyFromSeverity, 0, 100),
|
|
1562
|
+
weight: 0.25,
|
|
1563
|
+
description: `${anomalyCount} anomalies across ${actionCount} actions (${Math.round(anomalyRate * 100)}% rate)`
|
|
1564
|
+
};
|
|
1565
|
+
}
|
|
1566
|
+
computeTransactionConsistencyFactor(agentId) {
|
|
1567
|
+
const transactions = this.store.getTransactionsByAgent(agentId);
|
|
1568
|
+
if (transactions.length < 2) {
|
|
1569
|
+
return {
|
|
1570
|
+
name: "transaction_consistency",
|
|
1571
|
+
score: 50,
|
|
1572
|
+
weight: 0.2,
|
|
1573
|
+
description: "Insufficient transaction history for consistency analysis"
|
|
1574
|
+
};
|
|
1575
|
+
}
|
|
1576
|
+
const amounts = transactions.map((t) => parseAmount(t.amount)).filter((a) => !isNaN(a));
|
|
1577
|
+
if (amounts.length < 2) {
|
|
1578
|
+
return {
|
|
1579
|
+
name: "transaction_consistency",
|
|
1580
|
+
score: 50,
|
|
1581
|
+
weight: 0.2,
|
|
1582
|
+
description: "Insufficient valid amounts for consistency analysis"
|
|
1583
|
+
};
|
|
1584
|
+
}
|
|
1585
|
+
const mean = amounts.reduce((sum, a) => sum + a, 0) / amounts.length;
|
|
1586
|
+
const variance = amounts.reduce((sum, a) => sum + Math.pow(a - mean, 2), 0) / amounts.length;
|
|
1587
|
+
const stdDev = Math.sqrt(variance);
|
|
1588
|
+
const cv = mean > 0 ? stdDev / mean : 0;
|
|
1589
|
+
let score;
|
|
1590
|
+
if (cv < 0.1) score = 95;
|
|
1591
|
+
else if (cv < 0.3) score = 80;
|
|
1592
|
+
else if (cv < 0.5) score = 65;
|
|
1593
|
+
else if (cv < 1) score = 45;
|
|
1594
|
+
else if (cv < 2) score = 30;
|
|
1595
|
+
else score = 15;
|
|
1596
|
+
const destinations = new Set(transactions.map((t) => t.to));
|
|
1597
|
+
const destRatio = destinations.size / transactions.length;
|
|
1598
|
+
if (destRatio > 0.8 && transactions.length > 5) {
|
|
1599
|
+
score = Math.max(score - 15, 0);
|
|
1600
|
+
}
|
|
1601
|
+
return {
|
|
1602
|
+
name: "transaction_consistency",
|
|
1603
|
+
score: clamp(score, 0, 100),
|
|
1604
|
+
weight: 0.2,
|
|
1605
|
+
description: `CV=${cv.toFixed(2)}, ${destinations.size} unique destinations across ${transactions.length} transactions`
|
|
1606
|
+
};
|
|
1607
|
+
}
|
|
1608
|
+
computeComplianceAdherenceFactor(agentId) {
|
|
1609
|
+
const tasks = this.store.queryTasks((t) => t.agentId === agentId);
|
|
1610
|
+
const transactions = this.store.getTransactionsByAgent(agentId);
|
|
1611
|
+
const confirmedTasks = tasks.filter((t) => t.status === "confirmed");
|
|
1612
|
+
const tasksWithEvidence = confirmedTasks.filter(
|
|
1613
|
+
(t) => t.providedEvidence !== null && Object.keys(t.providedEvidence).length > 0
|
|
1614
|
+
);
|
|
1615
|
+
let score = 50;
|
|
1616
|
+
if (confirmedTasks.length > 0) {
|
|
1617
|
+
const evidenceRate = tasksWithEvidence.length / confirmedTasks.length;
|
|
1618
|
+
score += Math.round(evidenceRate * 30);
|
|
1619
|
+
}
|
|
1620
|
+
if (transactions.length > 0 && tasks.length > 0) {
|
|
1621
|
+
const coverageRate = Math.min(tasks.length / transactions.length, 1);
|
|
1622
|
+
score += Math.round(coverageRate * 20);
|
|
1623
|
+
}
|
|
1624
|
+
return {
|
|
1625
|
+
name: "compliance_adherence",
|
|
1626
|
+
score: clamp(score, 0, 100),
|
|
1627
|
+
weight: 0.15,
|
|
1628
|
+
description: `${tasksWithEvidence.length} tasks with evidence, ${transactions.length} total transactions`
|
|
1629
|
+
};
|
|
1630
|
+
}
|
|
1631
|
+
// --------------------------------------------------------------------------
|
|
1632
|
+
// Transaction risk factor computation
|
|
1633
|
+
// --------------------------------------------------------------------------
|
|
1634
|
+
computeTransactionRiskFactors(tx) {
|
|
1635
|
+
const factors = [];
|
|
1636
|
+
factors.push(this.computeAmountRisk(tx));
|
|
1637
|
+
factors.push(this.computeNewDestinationRisk(tx));
|
|
1638
|
+
factors.push(this.computeFrequencyRisk(tx));
|
|
1639
|
+
factors.push(this.computeAgentRisk(tx.agentId));
|
|
1640
|
+
factors.push(this.computeRoundAmountRisk(tx));
|
|
1641
|
+
return factors;
|
|
1642
|
+
}
|
|
1643
|
+
computeAmountRisk(tx) {
|
|
1644
|
+
const amount = parseAmount(tx.amount);
|
|
1645
|
+
if (isNaN(amount)) {
|
|
1646
|
+
return { name: "amount_risk", score: 50, description: "Unable to parse transaction amount" };
|
|
1647
|
+
}
|
|
1648
|
+
let score;
|
|
1649
|
+
if (amount < 100) score = 5;
|
|
1650
|
+
else if (amount < 1e3) score = 15;
|
|
1651
|
+
else if (amount < 1e4) score = 30;
|
|
1652
|
+
else if (amount < 5e4) score = 55;
|
|
1653
|
+
else if (amount < 1e5) score = 75;
|
|
1654
|
+
else score = 95;
|
|
1655
|
+
const history = this.store.getTransactionsByAgent(tx.agentId);
|
|
1656
|
+
if (history.length > 0) {
|
|
1657
|
+
const avgAmount = history.reduce((sum, t) => sum + parseAmount(t.amount), 0) / history.length;
|
|
1658
|
+
if (avgAmount > 0 && amount > avgAmount * 5) {
|
|
1659
|
+
score = Math.min(score + 20, 100);
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
return {
|
|
1663
|
+
name: "amount_risk",
|
|
1664
|
+
score,
|
|
1665
|
+
description: `Transaction amount ${tx.amount} ${tx.token}`
|
|
1666
|
+
};
|
|
1667
|
+
}
|
|
1668
|
+
computeNewDestinationRisk(tx) {
|
|
1669
|
+
const history = this.store.getTransactionsByAgent(tx.agentId);
|
|
1670
|
+
const knownDestinations = new Set(history.map((t) => t.to.toLowerCase()));
|
|
1671
|
+
const isNew = !knownDestinations.has(tx.to.toLowerCase());
|
|
1672
|
+
if (history.length === 0) {
|
|
1673
|
+
return {
|
|
1674
|
+
name: "new_destination",
|
|
1675
|
+
score: 30,
|
|
1676
|
+
description: "First transaction for this agent -- no destination history"
|
|
1677
|
+
};
|
|
1678
|
+
}
|
|
1679
|
+
return {
|
|
1680
|
+
name: "new_destination",
|
|
1681
|
+
score: isNew ? 45 : 5,
|
|
1682
|
+
description: isNew ? `New destination address: ${tx.to}` : `Known destination address: ${tx.to}`
|
|
1683
|
+
};
|
|
1684
|
+
}
|
|
1685
|
+
computeFrequencyRisk(tx) {
|
|
1686
|
+
const oneHourAgo = new Date(Date.now() - 36e5);
|
|
1687
|
+
const recentTxs = this.store.queryTransactions(
|
|
1688
|
+
(t) => t.agentId === tx.agentId && new Date(t.timestamp) >= oneHourAgo
|
|
1689
|
+
);
|
|
1690
|
+
const count = recentTxs.length;
|
|
1691
|
+
let score;
|
|
1692
|
+
if (count < 5) score = 5;
|
|
1693
|
+
else if (count < 10) score = 20;
|
|
1694
|
+
else if (count < 25) score = 45;
|
|
1695
|
+
else if (count < 50) score = 70;
|
|
1696
|
+
else score = 90;
|
|
1697
|
+
return {
|
|
1698
|
+
name: "frequency_risk",
|
|
1699
|
+
score,
|
|
1700
|
+
description: `${count} transactions in the last hour`
|
|
1701
|
+
};
|
|
1702
|
+
}
|
|
1703
|
+
computeAgentRisk(agentId) {
|
|
1704
|
+
const actions = this.store.getActionsByAgent(agentId);
|
|
1705
|
+
const anomalies = this.store.queryAnomalies((a) => a.agentId === agentId);
|
|
1706
|
+
if (actions.length === 0) {
|
|
1707
|
+
return {
|
|
1708
|
+
name: "agent_reputation",
|
|
1709
|
+
score: 40,
|
|
1710
|
+
description: "New agent with no history"
|
|
1711
|
+
};
|
|
1712
|
+
}
|
|
1713
|
+
const anomalyRate = anomalies.length / actions.length;
|
|
1714
|
+
const score = Math.round(anomalyRate * 100);
|
|
1715
|
+
return {
|
|
1716
|
+
name: "agent_reputation",
|
|
1717
|
+
score: clamp(score, 0, 100),
|
|
1718
|
+
description: `Agent anomaly rate: ${Math.round(anomalyRate * 100)}%`
|
|
1719
|
+
};
|
|
1720
|
+
}
|
|
1721
|
+
computeRoundAmountRisk(tx) {
|
|
1722
|
+
const amount = parseAmount(tx.amount);
|
|
1723
|
+
if (isNaN(amount)) {
|
|
1724
|
+
return { name: "round_amount", score: 10, description: "Unable to parse amount" };
|
|
1725
|
+
}
|
|
1726
|
+
const isRound1000 = amount >= 1e3 && amount % 1e3 === 0;
|
|
1727
|
+
const isRound10000 = amount >= 1e4 && amount % 1e4 === 0;
|
|
1728
|
+
const isJustUnderThreshold = amount >= 9e3 && amount <= 1e4;
|
|
1729
|
+
let score = 5;
|
|
1730
|
+
if (isRound10000) score = 25;
|
|
1731
|
+
else if (isRound1000) score = 15;
|
|
1732
|
+
if (isJustUnderThreshold) score += 20;
|
|
1733
|
+
return {
|
|
1734
|
+
name: "round_amount",
|
|
1735
|
+
score,
|
|
1736
|
+
description: `Amount ${tx.amount} -- ${isRound1000 ? "round amount" : "non-round amount"}`
|
|
1737
|
+
};
|
|
1738
|
+
}
|
|
1739
|
+
// --------------------------------------------------------------------------
|
|
1740
|
+
// Scoring helpers
|
|
1741
|
+
// --------------------------------------------------------------------------
|
|
1742
|
+
scoreToLevel(score) {
|
|
1743
|
+
if (score >= 90) return "verified";
|
|
1744
|
+
if (score >= 70) return "high";
|
|
1745
|
+
if (score >= 50) return "medium";
|
|
1746
|
+
if (score >= 30) return "low";
|
|
1747
|
+
return "untrusted";
|
|
1748
|
+
}
|
|
1749
|
+
riskScoreToLevel(score) {
|
|
1750
|
+
if (score >= 80) return "critical";
|
|
1751
|
+
if (score >= 60) return "high";
|
|
1752
|
+
if (score >= 35) return "medium";
|
|
1753
|
+
return "low";
|
|
1754
|
+
}
|
|
1755
|
+
};
|
|
1756
|
+
|
|
1757
|
+
// src/anomaly.ts
|
|
1758
|
+
var DEFAULT_THRESHOLDS = {
|
|
1759
|
+
maxAmount: "10000",
|
|
1760
|
+
maxFrequency: 30,
|
|
1761
|
+
offHours: [22, 23, 0, 1, 2, 3, 4, 5],
|
|
1762
|
+
minIntervalSeconds: 10
|
|
1763
|
+
};
|
|
1764
|
+
var AnomalyDetector = class {
|
|
1765
|
+
config;
|
|
1766
|
+
store;
|
|
1767
|
+
detectionConfig = null;
|
|
1768
|
+
thresholds = { ...DEFAULT_THRESHOLDS };
|
|
1769
|
+
callbacks = [];
|
|
1770
|
+
enabled = false;
|
|
1771
|
+
constructor(config, store) {
|
|
1772
|
+
this.config = config;
|
|
1773
|
+
this.store = store;
|
|
1774
|
+
}
|
|
1775
|
+
/**
|
|
1776
|
+
* Enable anomaly detection with the specified configuration.
|
|
1777
|
+
*
|
|
1778
|
+
* @param detectionConfig - Rules and thresholds for detection
|
|
1779
|
+
*
|
|
1780
|
+
* @example
|
|
1781
|
+
* ```typescript
|
|
1782
|
+
* detector.enableAnomalyDetection({
|
|
1783
|
+
* rules: ['unusualAmount', 'frequencySpike', 'newDestination'],
|
|
1784
|
+
* thresholds: { maxAmount: '10000', maxFrequency: 50 },
|
|
1785
|
+
* });
|
|
1786
|
+
* ```
|
|
1787
|
+
*/
|
|
1788
|
+
enableAnomalyDetection(detectionConfig) {
|
|
1789
|
+
if (!detectionConfig.rules || detectionConfig.rules.length === 0) {
|
|
1790
|
+
throw new KontextError(
|
|
1791
|
+
"ANOMALY_CONFIG_ERROR" /* ANOMALY_CONFIG_ERROR */,
|
|
1792
|
+
"At least one detection rule must be specified"
|
|
1793
|
+
);
|
|
1794
|
+
}
|
|
1795
|
+
this.detectionConfig = detectionConfig;
|
|
1796
|
+
this.thresholds = {
|
|
1797
|
+
...DEFAULT_THRESHOLDS,
|
|
1798
|
+
...detectionConfig.thresholds
|
|
1799
|
+
};
|
|
1800
|
+
this.enabled = true;
|
|
1801
|
+
if (this.config.debug) {
|
|
1802
|
+
console.debug(
|
|
1803
|
+
`[Kontext] Anomaly detection enabled with rules: ${detectionConfig.rules.join(", ")}`
|
|
1804
|
+
);
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
/**
|
|
1808
|
+
* Disable anomaly detection.
|
|
1809
|
+
*/
|
|
1810
|
+
disableAnomalyDetection() {
|
|
1811
|
+
this.enabled = false;
|
|
1812
|
+
this.detectionConfig = null;
|
|
1813
|
+
if (this.config.debug) {
|
|
1814
|
+
console.debug("[Kontext] Anomaly detection disabled");
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
/**
|
|
1818
|
+
* Register a callback for anomaly events.
|
|
1819
|
+
*
|
|
1820
|
+
* @param callback - Function to call when an anomaly is detected
|
|
1821
|
+
* @returns Unsubscribe function
|
|
1822
|
+
*
|
|
1823
|
+
* @example
|
|
1824
|
+
* ```typescript
|
|
1825
|
+
* const unsub = detector.onAnomaly((anomaly) => {
|
|
1826
|
+
* console.log(`Anomaly: ${anomaly.type} [${anomaly.severity}]`);
|
|
1827
|
+
* });
|
|
1828
|
+
* // Later: unsub();
|
|
1829
|
+
* ```
|
|
1830
|
+
*/
|
|
1831
|
+
onAnomaly(callback) {
|
|
1832
|
+
this.callbacks.push(callback);
|
|
1833
|
+
return () => {
|
|
1834
|
+
const index = this.callbacks.indexOf(callback);
|
|
1835
|
+
if (index !== -1) {
|
|
1836
|
+
this.callbacks.splice(index, 1);
|
|
1837
|
+
}
|
|
1838
|
+
};
|
|
1839
|
+
}
|
|
1840
|
+
/**
|
|
1841
|
+
* Evaluate a transaction against all enabled detection rules.
|
|
1842
|
+
* Called automatically when transactions are logged (via the client).
|
|
1843
|
+
*
|
|
1844
|
+
* @param tx - The transaction record to evaluate
|
|
1845
|
+
* @returns Array of detected anomalies (empty if none)
|
|
1846
|
+
*/
|
|
1847
|
+
evaluateTransaction(tx) {
|
|
1848
|
+
if (!this.enabled || !this.detectionConfig) return [];
|
|
1849
|
+
const anomalies = [];
|
|
1850
|
+
for (const rule of this.detectionConfig.rules) {
|
|
1851
|
+
const anomaly = this.runRule(rule, tx);
|
|
1852
|
+
if (anomaly) {
|
|
1853
|
+
anomalies.push(anomaly);
|
|
1854
|
+
this.store.addAnomaly(anomaly);
|
|
1855
|
+
this.notifyCallbacks(anomaly);
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
return anomalies;
|
|
1859
|
+
}
|
|
1860
|
+
/**
|
|
1861
|
+
* Evaluate a generic action against all enabled detection rules.
|
|
1862
|
+
*
|
|
1863
|
+
* @param action - The action log to evaluate
|
|
1864
|
+
* @returns Array of detected anomalies (empty if none)
|
|
1865
|
+
*/
|
|
1866
|
+
evaluateAction(action) {
|
|
1867
|
+
if (!this.enabled || !this.detectionConfig) return [];
|
|
1868
|
+
const anomalies = [];
|
|
1869
|
+
const applicableRules = ["offHoursActivity", "frequencySpike"];
|
|
1870
|
+
for (const rule of this.detectionConfig.rules) {
|
|
1871
|
+
if (!applicableRules.includes(rule)) continue;
|
|
1872
|
+
const anomaly = this.runActionRule(rule, action);
|
|
1873
|
+
if (anomaly) {
|
|
1874
|
+
anomalies.push(anomaly);
|
|
1875
|
+
this.store.addAnomaly(anomaly);
|
|
1876
|
+
this.notifyCallbacks(anomaly);
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
return anomalies;
|
|
1880
|
+
}
|
|
1881
|
+
/**
|
|
1882
|
+
* Check whether anomaly detection is currently enabled.
|
|
1883
|
+
*/
|
|
1884
|
+
isEnabled() {
|
|
1885
|
+
return this.enabled;
|
|
1886
|
+
}
|
|
1887
|
+
/**
|
|
1888
|
+
* Get the current detection configuration.
|
|
1889
|
+
*/
|
|
1890
|
+
getConfig() {
|
|
1891
|
+
return this.detectionConfig;
|
|
1892
|
+
}
|
|
1893
|
+
// --------------------------------------------------------------------------
|
|
1894
|
+
// Rule execution
|
|
1895
|
+
// --------------------------------------------------------------------------
|
|
1896
|
+
runRule(rule, tx) {
|
|
1897
|
+
switch (rule) {
|
|
1898
|
+
case "unusualAmount":
|
|
1899
|
+
return this.checkUnusualAmount(tx);
|
|
1900
|
+
case "frequencySpike":
|
|
1901
|
+
return this.checkFrequencySpike(tx);
|
|
1902
|
+
case "newDestination":
|
|
1903
|
+
return this.checkNewDestination(tx);
|
|
1904
|
+
case "offHoursActivity":
|
|
1905
|
+
return this.checkOffHours(tx);
|
|
1906
|
+
case "rapidSuccession":
|
|
1907
|
+
return this.checkRapidSuccession(tx);
|
|
1908
|
+
case "roundAmount":
|
|
1909
|
+
return this.checkRoundAmount(tx);
|
|
1910
|
+
default:
|
|
1911
|
+
return null;
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
runActionRule(rule, action) {
|
|
1915
|
+
switch (rule) {
|
|
1916
|
+
case "offHoursActivity":
|
|
1917
|
+
return this.checkOffHoursAction(action);
|
|
1918
|
+
case "frequencySpike":
|
|
1919
|
+
return this.checkActionFrequencySpike(action);
|
|
1920
|
+
default:
|
|
1921
|
+
return null;
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
// --------------------------------------------------------------------------
|
|
1925
|
+
// Individual rule implementations
|
|
1926
|
+
// --------------------------------------------------------------------------
|
|
1927
|
+
checkUnusualAmount(tx) {
|
|
1928
|
+
const amount = parseAmount(tx.amount);
|
|
1929
|
+
if (isNaN(amount)) return null;
|
|
1930
|
+
const threshold = parseAmount(this.thresholds.maxAmount);
|
|
1931
|
+
if (amount > threshold) {
|
|
1932
|
+
return this.createAnomaly(
|
|
1933
|
+
"unusualAmount",
|
|
1934
|
+
amount > threshold * 5 ? "critical" : amount > threshold * 2 ? "high" : "medium",
|
|
1935
|
+
`Transaction amount ${tx.amount} ${tx.token} exceeds threshold of ${this.thresholds.maxAmount}`,
|
|
1936
|
+
tx.agentId,
|
|
1937
|
+
tx.id,
|
|
1938
|
+
{ amount: tx.amount, threshold: this.thresholds.maxAmount, token: tx.token }
|
|
1939
|
+
);
|
|
1940
|
+
}
|
|
1941
|
+
const history = this.store.getTransactionsByAgent(tx.agentId);
|
|
1942
|
+
if (history.length >= 3) {
|
|
1943
|
+
const amounts = history.map((t) => parseAmount(t.amount)).filter((a) => !isNaN(a));
|
|
1944
|
+
if (amounts.length >= 3) {
|
|
1945
|
+
const avg = amounts.reduce((s, a) => s + a, 0) / amounts.length;
|
|
1946
|
+
if (avg > 0 && amount > avg * 5) {
|
|
1947
|
+
return this.createAnomaly(
|
|
1948
|
+
"unusualAmount",
|
|
1949
|
+
amount > avg * 10 ? "high" : "medium",
|
|
1950
|
+
`Transaction amount ${tx.amount} is ${(amount / avg).toFixed(1)}x the agent's average of ${avg.toFixed(2)}`,
|
|
1951
|
+
tx.agentId,
|
|
1952
|
+
tx.id,
|
|
1953
|
+
{ amount: tx.amount, average: avg.toFixed(2), multiplier: (amount / avg).toFixed(1) }
|
|
1954
|
+
);
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
return null;
|
|
1959
|
+
}
|
|
1960
|
+
checkFrequencySpike(tx) {
|
|
1961
|
+
const oneHourAgo = new Date(Date.now() - 36e5);
|
|
1962
|
+
const recentTxs = this.store.queryTransactions(
|
|
1963
|
+
(t) => t.agentId === tx.agentId && new Date(t.timestamp) >= oneHourAgo
|
|
1964
|
+
);
|
|
1965
|
+
const count = recentTxs.length;
|
|
1966
|
+
const maxFrequency = this.thresholds.maxFrequency;
|
|
1967
|
+
if (count > maxFrequency) {
|
|
1968
|
+
return this.createAnomaly(
|
|
1969
|
+
"frequencySpike",
|
|
1970
|
+
count > maxFrequency * 3 ? "critical" : count > maxFrequency * 2 ? "high" : "medium",
|
|
1971
|
+
`Agent ${tx.agentId} has ${count} transactions in the last hour (threshold: ${maxFrequency})`,
|
|
1972
|
+
tx.agentId,
|
|
1973
|
+
tx.id,
|
|
1974
|
+
{ count, threshold: maxFrequency }
|
|
1975
|
+
);
|
|
1976
|
+
}
|
|
1977
|
+
return null;
|
|
1978
|
+
}
|
|
1979
|
+
checkNewDestination(tx) {
|
|
1980
|
+
const history = this.store.getTransactionsByAgent(tx.agentId);
|
|
1981
|
+
if (history.length < 3) return null;
|
|
1982
|
+
const knownDestinations = new Set(
|
|
1983
|
+
history.filter((t) => t.id !== tx.id).map((t) => t.to.toLowerCase())
|
|
1984
|
+
);
|
|
1985
|
+
if (!knownDestinations.has(tx.to.toLowerCase())) {
|
|
1986
|
+
const amount = parseAmount(tx.amount);
|
|
1987
|
+
const severity = !isNaN(amount) && amount > parseAmount(this.thresholds.maxAmount) * 0.5 ? "high" : "low";
|
|
1988
|
+
return this.createAnomaly(
|
|
1989
|
+
"newDestination",
|
|
1990
|
+
severity,
|
|
1991
|
+
`Transaction to new destination ${tx.to} (agent has ${knownDestinations.size} known destinations)`,
|
|
1992
|
+
tx.agentId,
|
|
1993
|
+
tx.id,
|
|
1994
|
+
{ destination: tx.to, knownDestinationCount: knownDestinations.size }
|
|
1995
|
+
);
|
|
1996
|
+
}
|
|
1997
|
+
return null;
|
|
1998
|
+
}
|
|
1999
|
+
checkOffHours(tx) {
|
|
2000
|
+
const txHour = new Date(tx.timestamp).getUTCHours();
|
|
2001
|
+
if (this.thresholds.offHours.includes(txHour)) {
|
|
2002
|
+
return this.createAnomaly(
|
|
2003
|
+
"offHoursActivity",
|
|
2004
|
+
"low",
|
|
2005
|
+
`Transaction at ${txHour}:00 UTC falls within off-hours window`,
|
|
2006
|
+
tx.agentId,
|
|
2007
|
+
tx.id,
|
|
2008
|
+
{ hour: txHour, offHours: this.thresholds.offHours }
|
|
2009
|
+
);
|
|
2010
|
+
}
|
|
2011
|
+
return null;
|
|
2012
|
+
}
|
|
2013
|
+
checkRapidSuccession(tx) {
|
|
2014
|
+
const recentTxs = this.store.getTransactionsByAgent(tx.agentId).filter((t) => t.id !== tx.id);
|
|
2015
|
+
if (recentTxs.length === 0) return null;
|
|
2016
|
+
const lastTx = recentTxs[recentTxs.length - 1];
|
|
2017
|
+
if (!lastTx) return null;
|
|
2018
|
+
const timeDiffMs = new Date(tx.timestamp).getTime() - new Date(lastTx.timestamp).getTime();
|
|
2019
|
+
const timeDiffSeconds = timeDiffMs / 1e3;
|
|
2020
|
+
if (timeDiffSeconds >= 0 && timeDiffSeconds < this.thresholds.minIntervalSeconds) {
|
|
2021
|
+
return this.createAnomaly(
|
|
2022
|
+
"rapidSuccession",
|
|
2023
|
+
timeDiffSeconds < 2 ? "high" : "medium",
|
|
2024
|
+
`Transaction occurred ${timeDiffSeconds.toFixed(1)}s after previous transaction (minimum: ${this.thresholds.minIntervalSeconds}s)`,
|
|
2025
|
+
tx.agentId,
|
|
2026
|
+
tx.id,
|
|
2027
|
+
{
|
|
2028
|
+
intervalSeconds: timeDiffSeconds,
|
|
2029
|
+
threshold: this.thresholds.minIntervalSeconds,
|
|
2030
|
+
previousTxId: lastTx.id
|
|
2031
|
+
}
|
|
2032
|
+
);
|
|
2033
|
+
}
|
|
2034
|
+
return null;
|
|
2035
|
+
}
|
|
2036
|
+
checkRoundAmount(tx) {
|
|
2037
|
+
const amount = parseAmount(tx.amount);
|
|
2038
|
+
if (isNaN(amount)) return null;
|
|
2039
|
+
const structuringThresholds = [1e4, 5e3, 3e3, 1e3];
|
|
2040
|
+
for (const threshold of structuringThresholds) {
|
|
2041
|
+
const diff = threshold - amount;
|
|
2042
|
+
if (diff > 0 && diff <= threshold * 0.05) {
|
|
2043
|
+
return this.createAnomaly(
|
|
2044
|
+
"roundAmount",
|
|
2045
|
+
threshold >= 1e4 ? "high" : "medium",
|
|
2046
|
+
`Transaction amount ${tx.amount} is just below the ${threshold} threshold (potential structuring)`,
|
|
2047
|
+
tx.agentId,
|
|
2048
|
+
tx.id,
|
|
2049
|
+
{ amount: tx.amount, nearThreshold: threshold, difference: diff }
|
|
2050
|
+
);
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
if (amount >= 5e3 && amount % 1e3 === 0) {
|
|
2054
|
+
return this.createAnomaly(
|
|
2055
|
+
"roundAmount",
|
|
2056
|
+
"low",
|
|
2057
|
+
`Transaction amount ${tx.amount} is a round number`,
|
|
2058
|
+
tx.agentId,
|
|
2059
|
+
tx.id,
|
|
2060
|
+
{ amount: tx.amount }
|
|
2061
|
+
);
|
|
2062
|
+
}
|
|
2063
|
+
return null;
|
|
2064
|
+
}
|
|
2065
|
+
checkOffHoursAction(action) {
|
|
2066
|
+
const actionHour = new Date(action.timestamp).getUTCHours();
|
|
2067
|
+
if (this.thresholds.offHours.includes(actionHour)) {
|
|
2068
|
+
return this.createAnomaly(
|
|
2069
|
+
"offHoursActivity",
|
|
2070
|
+
"low",
|
|
2071
|
+
`Action at ${actionHour}:00 UTC falls within off-hours window`,
|
|
2072
|
+
action.agentId,
|
|
2073
|
+
action.id,
|
|
2074
|
+
{ hour: actionHour, offHours: this.thresholds.offHours }
|
|
2075
|
+
);
|
|
2076
|
+
}
|
|
2077
|
+
return null;
|
|
2078
|
+
}
|
|
2079
|
+
checkActionFrequencySpike(action) {
|
|
2080
|
+
const oneHourAgo = new Date(Date.now() - 36e5);
|
|
2081
|
+
const recentActions = this.store.queryActions(
|
|
2082
|
+
(a) => a.agentId === action.agentId && new Date(a.timestamp) >= oneHourAgo
|
|
2083
|
+
);
|
|
2084
|
+
const count = recentActions.length;
|
|
2085
|
+
const maxFrequency = this.thresholds.maxFrequency * 3;
|
|
2086
|
+
if (count > maxFrequency) {
|
|
2087
|
+
return this.createAnomaly(
|
|
2088
|
+
"frequencySpike",
|
|
2089
|
+
count > maxFrequency * 2 ? "high" : "medium",
|
|
2090
|
+
`Agent ${action.agentId} has ${count} actions in the last hour (threshold: ${maxFrequency})`,
|
|
2091
|
+
action.agentId,
|
|
2092
|
+
action.id,
|
|
2093
|
+
{ count, threshold: maxFrequency }
|
|
2094
|
+
);
|
|
2095
|
+
}
|
|
2096
|
+
return null;
|
|
2097
|
+
}
|
|
2098
|
+
// --------------------------------------------------------------------------
|
|
2099
|
+
// Helpers
|
|
2100
|
+
// --------------------------------------------------------------------------
|
|
2101
|
+
createAnomaly(type, severity, description, agentId, actionId, data) {
|
|
2102
|
+
return {
|
|
2103
|
+
id: generateId(),
|
|
2104
|
+
type,
|
|
2105
|
+
severity,
|
|
2106
|
+
description,
|
|
2107
|
+
agentId,
|
|
2108
|
+
actionId,
|
|
2109
|
+
detectedAt: now(),
|
|
2110
|
+
data,
|
|
2111
|
+
reviewed: false
|
|
2112
|
+
};
|
|
2113
|
+
}
|
|
2114
|
+
notifyCallbacks(anomaly) {
|
|
2115
|
+
for (const cb of this.callbacks) {
|
|
2116
|
+
try {
|
|
2117
|
+
cb(anomaly);
|
|
2118
|
+
} catch (error) {
|
|
2119
|
+
if (this.config.debug) {
|
|
2120
|
+
console.debug("[Kontext] Anomaly callback error:", error);
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2125
|
+
};
|
|
2126
|
+
|
|
2127
|
+
// src/integrations/usdc.ts
|
|
2128
|
+
var USDC_CONTRACTS = {
|
|
2129
|
+
ethereum: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
|
2130
|
+
base: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
|
2131
|
+
polygon: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359",
|
|
2132
|
+
arbitrum: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
|
|
2133
|
+
optimism: "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85",
|
|
2134
|
+
// Arc (Circle's stablecoin-native blockchain) -- placeholder address, update when Arc mainnet launches
|
|
2135
|
+
arc: "0xa0c0000000000000000000000000000000000001"
|
|
2136
|
+
};
|
|
2137
|
+
var BLOCKED_ADDRESS_PREFIXES = [
|
|
2138
|
+
// These are examples. Real implementation would query an OFAC API.
|
|
2139
|
+
];
|
|
2140
|
+
var ENHANCED_DUE_DILIGENCE_THRESHOLD = 3e3;
|
|
2141
|
+
var REPORTING_THRESHOLD = 1e4;
|
|
2142
|
+
var LARGE_TRANSACTION_THRESHOLD = 5e4;
|
|
2143
|
+
var UsdcCompliance = class _UsdcCompliance {
|
|
2144
|
+
/**
|
|
2145
|
+
* Run a full compliance check on a USDC transaction.
|
|
2146
|
+
*
|
|
2147
|
+
* @param tx - Transaction to evaluate
|
|
2148
|
+
* @returns UsdcComplianceCheck with pass/fail results and recommendations
|
|
2149
|
+
*
|
|
2150
|
+
* @example
|
|
2151
|
+
* ```typescript
|
|
2152
|
+
* const check = UsdcCompliance.checkTransaction({
|
|
2153
|
+
* txHash: '0x...',
|
|
2154
|
+
* chain: 'base',
|
|
2155
|
+
* amount: '5000',
|
|
2156
|
+
* token: 'USDC',
|
|
2157
|
+
* from: '0xSender...',
|
|
2158
|
+
* to: '0xReceiver...',
|
|
2159
|
+
* agentId: 'agent-1',
|
|
2160
|
+
* });
|
|
2161
|
+
* if (!check.compliant) {
|
|
2162
|
+
* console.log('Non-compliant:', check.recommendations);
|
|
2163
|
+
* }
|
|
2164
|
+
* ```
|
|
2165
|
+
*/
|
|
2166
|
+
static checkTransaction(tx) {
|
|
2167
|
+
const checks = [];
|
|
2168
|
+
checks.push(_UsdcCompliance.checkTokenType(tx));
|
|
2169
|
+
checks.push(_UsdcCompliance.checkChainSupport(tx.chain));
|
|
2170
|
+
checks.push(_UsdcCompliance.checkAddressFormat(tx.from, "sender"));
|
|
2171
|
+
checks.push(_UsdcCompliance.checkAddressFormat(tx.to, "recipient"));
|
|
2172
|
+
checks.push(_UsdcCompliance.checkAmountValid(tx.amount));
|
|
2173
|
+
checks.push(_UsdcCompliance.checkSanctions(tx.from, "sender"));
|
|
2174
|
+
checks.push(_UsdcCompliance.checkSanctions(tx.to, "recipient"));
|
|
2175
|
+
checks.push(_UsdcCompliance.checkEnhancedDueDiligence(tx.amount));
|
|
2176
|
+
checks.push(_UsdcCompliance.checkReportingThreshold(tx.amount));
|
|
2177
|
+
const failedChecks = checks.filter((c) => !c.passed);
|
|
2178
|
+
const compliant = failedChecks.every((c) => c.severity === "low");
|
|
2179
|
+
const highestSeverity = failedChecks.reduce(
|
|
2180
|
+
(max, c) => {
|
|
2181
|
+
const order = ["low", "medium", "high", "critical"];
|
|
2182
|
+
return order.indexOf(c.severity) > order.indexOf(max) ? c.severity : max;
|
|
2183
|
+
},
|
|
2184
|
+
"low"
|
|
2185
|
+
);
|
|
2186
|
+
const recommendations = _UsdcCompliance.generateRecommendations(checks, tx);
|
|
2187
|
+
return {
|
|
2188
|
+
compliant,
|
|
2189
|
+
checks,
|
|
2190
|
+
riskLevel: highestSeverity,
|
|
2191
|
+
recommendations
|
|
2192
|
+
};
|
|
2193
|
+
}
|
|
2194
|
+
/**
|
|
2195
|
+
* Get the USDC contract address for a given chain.
|
|
2196
|
+
*
|
|
2197
|
+
* @param chain - The blockchain network
|
|
2198
|
+
* @returns The USDC contract address, or undefined for unsupported chains
|
|
2199
|
+
*/
|
|
2200
|
+
static getContractAddress(chain) {
|
|
2201
|
+
return USDC_CONTRACTS[chain];
|
|
2202
|
+
}
|
|
2203
|
+
/**
|
|
2204
|
+
* Get the chains supported for USDC compliance monitoring.
|
|
2205
|
+
*/
|
|
2206
|
+
static getSupportedChains() {
|
|
2207
|
+
return Object.keys(USDC_CONTRACTS);
|
|
2208
|
+
}
|
|
2209
|
+
// --------------------------------------------------------------------------
|
|
2210
|
+
// Individual compliance checks
|
|
2211
|
+
// --------------------------------------------------------------------------
|
|
2212
|
+
static checkTokenType(tx) {
|
|
2213
|
+
const isUsdc = tx.token === "USDC";
|
|
2214
|
+
return {
|
|
2215
|
+
name: "token_type",
|
|
2216
|
+
passed: isUsdc,
|
|
2217
|
+
description: isUsdc ? "Transaction token is USDC" : `Expected USDC but got ${tx.token}`,
|
|
2218
|
+
severity: isUsdc ? "low" : "high"
|
|
2219
|
+
};
|
|
2220
|
+
}
|
|
2221
|
+
static checkChainSupport(chain) {
|
|
2222
|
+
const supported = chain in USDC_CONTRACTS;
|
|
2223
|
+
return {
|
|
2224
|
+
name: "chain_support",
|
|
2225
|
+
passed: supported,
|
|
2226
|
+
description: supported ? `Chain ${chain} is supported for USDC compliance monitoring` : `Chain ${chain} is not in the supported USDC compliance list`,
|
|
2227
|
+
severity: supported ? "low" : "medium"
|
|
2228
|
+
};
|
|
2229
|
+
}
|
|
2230
|
+
static checkAddressFormat(address, label) {
|
|
2231
|
+
const isValid = /^0x[a-fA-F0-9]{40}$/.test(address);
|
|
2232
|
+
return {
|
|
2233
|
+
name: `address_format_${label}`,
|
|
2234
|
+
passed: isValid,
|
|
2235
|
+
description: isValid ? `${label} address format is valid` : `${label} address format is invalid: ${address}`,
|
|
2236
|
+
severity: isValid ? "low" : "high"
|
|
2237
|
+
};
|
|
2238
|
+
}
|
|
2239
|
+
static checkAmountValid(amount) {
|
|
2240
|
+
const parsed = parseAmount(amount);
|
|
2241
|
+
const isValid = !isNaN(parsed) && parsed > 0;
|
|
2242
|
+
return {
|
|
2243
|
+
name: "amount_valid",
|
|
2244
|
+
passed: isValid,
|
|
2245
|
+
description: isValid ? `Transaction amount ${amount} is valid` : `Transaction amount ${amount} is invalid`,
|
|
2246
|
+
severity: isValid ? "low" : "critical"
|
|
2247
|
+
};
|
|
2248
|
+
}
|
|
2249
|
+
static checkSanctions(address, label) {
|
|
2250
|
+
const isBlocked = BLOCKED_ADDRESS_PREFIXES.some(
|
|
2251
|
+
(prefix) => address.toLowerCase().startsWith(prefix.toLowerCase())
|
|
2252
|
+
);
|
|
2253
|
+
return {
|
|
2254
|
+
name: `sanctions_${label}`,
|
|
2255
|
+
passed: !isBlocked,
|
|
2256
|
+
description: isBlocked ? `${label} address ${address} appears on sanctions list` : `${label} address passed sanctions screening`,
|
|
2257
|
+
severity: isBlocked ? "critical" : "low"
|
|
2258
|
+
};
|
|
2259
|
+
}
|
|
2260
|
+
static checkEnhancedDueDiligence(amount) {
|
|
2261
|
+
const parsed = parseAmount(amount);
|
|
2262
|
+
const requiresEdd = !isNaN(parsed) && parsed >= ENHANCED_DUE_DILIGENCE_THRESHOLD;
|
|
2263
|
+
return {
|
|
2264
|
+
name: "enhanced_due_diligence",
|
|
2265
|
+
passed: true,
|
|
2266
|
+
// This is informational -- it always "passes" but flags the need
|
|
2267
|
+
description: requiresEdd ? `Amount ${amount} USDC requires enhanced due diligence (threshold: ${ENHANCED_DUE_DILIGENCE_THRESHOLD})` : `Amount ${amount} USDC is below enhanced due diligence threshold`,
|
|
2268
|
+
severity: requiresEdd ? "medium" : "low"
|
|
2269
|
+
};
|
|
2270
|
+
}
|
|
2271
|
+
static checkReportingThreshold(amount) {
|
|
2272
|
+
const parsed = parseAmount(amount);
|
|
2273
|
+
const requiresReporting = !isNaN(parsed) && parsed >= REPORTING_THRESHOLD;
|
|
2274
|
+
const isLarge = !isNaN(parsed) && parsed >= LARGE_TRANSACTION_THRESHOLD;
|
|
2275
|
+
let description;
|
|
2276
|
+
let severity;
|
|
2277
|
+
if (isLarge) {
|
|
2278
|
+
description = `Amount ${amount} USDC is a large transaction (>= ${LARGE_TRANSACTION_THRESHOLD}) -- requires enhanced monitoring`;
|
|
2279
|
+
severity = "high";
|
|
2280
|
+
} else if (requiresReporting) {
|
|
2281
|
+
description = `Amount ${amount} USDC meets reporting threshold (>= ${REPORTING_THRESHOLD})`;
|
|
2282
|
+
severity = "medium";
|
|
2283
|
+
} else {
|
|
2284
|
+
description = `Amount ${amount} USDC is below reporting threshold`;
|
|
2285
|
+
severity = "low";
|
|
2286
|
+
}
|
|
2287
|
+
return {
|
|
2288
|
+
name: "reporting_threshold",
|
|
2289
|
+
passed: true,
|
|
2290
|
+
// Informational
|
|
2291
|
+
description,
|
|
2292
|
+
severity
|
|
2293
|
+
};
|
|
2294
|
+
}
|
|
2295
|
+
// --------------------------------------------------------------------------
|
|
2296
|
+
// Recommendations
|
|
2297
|
+
// --------------------------------------------------------------------------
|
|
2298
|
+
static generateRecommendations(checks, tx) {
|
|
2299
|
+
const recommendations = [];
|
|
2300
|
+
const amount = parseAmount(tx.amount);
|
|
2301
|
+
const criticalFailures = checks.filter(
|
|
2302
|
+
(c) => !c.passed && c.severity === "critical"
|
|
2303
|
+
);
|
|
2304
|
+
if (criticalFailures.length > 0) {
|
|
2305
|
+
recommendations.push("BLOCK: Critical compliance check failures detected. Do not proceed.");
|
|
2306
|
+
}
|
|
2307
|
+
if (!isNaN(amount)) {
|
|
2308
|
+
if (amount >= LARGE_TRANSACTION_THRESHOLD) {
|
|
2309
|
+
recommendations.push(
|
|
2310
|
+
"Require manual review for large transaction per GENIUS Act Section 4(b)."
|
|
2311
|
+
);
|
|
2312
|
+
recommendations.push("Verify recipient identity through KYC process.");
|
|
2313
|
+
recommendations.push("Document business purpose for the transfer.");
|
|
2314
|
+
} else if (amount >= REPORTING_THRESHOLD) {
|
|
2315
|
+
recommendations.push(
|
|
2316
|
+
"Generate Currency Transaction Report (CTR) per BSA requirements."
|
|
2317
|
+
);
|
|
2318
|
+
recommendations.push("Retain transaction records for minimum 5 years.");
|
|
2319
|
+
} else if (amount >= ENHANCED_DUE_DILIGENCE_THRESHOLD) {
|
|
2320
|
+
recommendations.push(
|
|
2321
|
+
"Enhanced due diligence recommended -- verify transaction purpose."
|
|
2322
|
+
);
|
|
2323
|
+
}
|
|
2324
|
+
}
|
|
2325
|
+
const addressFailures = checks.filter(
|
|
2326
|
+
(c) => c.name.startsWith("address_format") && !c.passed
|
|
2327
|
+
);
|
|
2328
|
+
if (addressFailures.length > 0) {
|
|
2329
|
+
recommendations.push("Verify address format before proceeding.");
|
|
2330
|
+
}
|
|
2331
|
+
if (recommendations.length === 0) {
|
|
2332
|
+
recommendations.push("Transaction passes all compliance checks. Safe to proceed.");
|
|
2333
|
+
}
|
|
2334
|
+
return recommendations;
|
|
2335
|
+
}
|
|
2336
|
+
};
|
|
2337
|
+
|
|
2338
|
+
// src/client.ts
|
|
2339
|
+
var Kontext = class _Kontext {
|
|
2340
|
+
config;
|
|
2341
|
+
store;
|
|
2342
|
+
logger;
|
|
2343
|
+
taskManager;
|
|
2344
|
+
auditExporter;
|
|
2345
|
+
trustScorer;
|
|
2346
|
+
anomalyDetector;
|
|
2347
|
+
mode;
|
|
2348
|
+
constructor(config) {
|
|
2349
|
+
this.config = config;
|
|
2350
|
+
this.mode = config.apiKey ? "cloud" : "local";
|
|
2351
|
+
this.store = new KontextStore();
|
|
2352
|
+
this.logger = new ActionLogger(config, this.store);
|
|
2353
|
+
this.taskManager = new TaskManager(config, this.store);
|
|
2354
|
+
this.auditExporter = new AuditExporter(config, this.store);
|
|
2355
|
+
this.trustScorer = new TrustScorer(config, this.store);
|
|
2356
|
+
this.anomalyDetector = new AnomalyDetector(config, this.store);
|
|
2357
|
+
}
|
|
2358
|
+
/**
|
|
2359
|
+
* Initialize the Kontext SDK.
|
|
2360
|
+
*
|
|
2361
|
+
* @param config - Configuration options
|
|
2362
|
+
* @returns Initialized Kontext client instance
|
|
2363
|
+
*
|
|
2364
|
+
* @example
|
|
2365
|
+
* ```typescript
|
|
2366
|
+
* // Local/OSS mode (no API key)
|
|
2367
|
+
* const kontext = Kontext.init({
|
|
2368
|
+
* projectId: 'my-project',
|
|
2369
|
+
* environment: 'development',
|
|
2370
|
+
* });
|
|
2371
|
+
*
|
|
2372
|
+
* // Cloud mode (with API key)
|
|
2373
|
+
* const kontext = Kontext.init({
|
|
2374
|
+
* apiKey: 'sk_live_...',
|
|
2375
|
+
* projectId: 'my-project',
|
|
2376
|
+
* environment: 'production',
|
|
2377
|
+
* });
|
|
2378
|
+
* ```
|
|
2379
|
+
*/
|
|
2380
|
+
static init(config) {
|
|
2381
|
+
if (!config.projectId || config.projectId.trim() === "") {
|
|
2382
|
+
throw new KontextError(
|
|
2383
|
+
"INITIALIZATION_ERROR" /* INITIALIZATION_ERROR */,
|
|
2384
|
+
"projectId is required"
|
|
2385
|
+
);
|
|
2386
|
+
}
|
|
2387
|
+
const validEnvironments = ["development", "staging", "production"];
|
|
2388
|
+
if (!validEnvironments.includes(config.environment)) {
|
|
2389
|
+
throw new KontextError(
|
|
2390
|
+
"INITIALIZATION_ERROR" /* INITIALIZATION_ERROR */,
|
|
2391
|
+
`Invalid environment: ${config.environment}. Must be one of: ${validEnvironments.join(", ")}`
|
|
2392
|
+
);
|
|
2393
|
+
}
|
|
2394
|
+
if (config.debug) {
|
|
2395
|
+
const mode = config.apiKey ? "cloud" : "local";
|
|
2396
|
+
console.debug(
|
|
2397
|
+
`[Kontext] Initializing in ${mode} mode for project ${config.projectId} (${config.environment})`
|
|
2398
|
+
);
|
|
2399
|
+
}
|
|
2400
|
+
return new _Kontext(config);
|
|
2401
|
+
}
|
|
2402
|
+
// --------------------------------------------------------------------------
|
|
2403
|
+
// Mode & Config
|
|
2404
|
+
// --------------------------------------------------------------------------
|
|
2405
|
+
/**
|
|
2406
|
+
* Get the current operating mode.
|
|
2407
|
+
*/
|
|
2408
|
+
getMode() {
|
|
2409
|
+
return this.mode;
|
|
2410
|
+
}
|
|
2411
|
+
/**
|
|
2412
|
+
* Get the current configuration (API key is masked).
|
|
2413
|
+
*/
|
|
2414
|
+
getConfig() {
|
|
2415
|
+
return {
|
|
2416
|
+
...this.config,
|
|
2417
|
+
apiKey: this.config.apiKey ? `${this.config.apiKey.slice(0, 8)}...` : void 0
|
|
2418
|
+
};
|
|
2419
|
+
}
|
|
2420
|
+
// --------------------------------------------------------------------------
|
|
2421
|
+
// Action Logging
|
|
2422
|
+
// --------------------------------------------------------------------------
|
|
2423
|
+
/**
|
|
2424
|
+
* Log a generic agent action.
|
|
2425
|
+
*
|
|
2426
|
+
* @param input - Action details
|
|
2427
|
+
* @returns The created action log entry
|
|
2428
|
+
*/
|
|
2429
|
+
async log(input) {
|
|
2430
|
+
const action = await this.logger.log(input);
|
|
2431
|
+
if (this.anomalyDetector.isEnabled()) {
|
|
2432
|
+
this.anomalyDetector.evaluateAction(action);
|
|
2433
|
+
}
|
|
2434
|
+
return action;
|
|
2435
|
+
}
|
|
2436
|
+
/**
|
|
2437
|
+
* Log a cryptocurrency transaction with full chain details.
|
|
2438
|
+
*
|
|
2439
|
+
* @param input - Transaction details
|
|
2440
|
+
* @returns The created transaction record
|
|
2441
|
+
*/
|
|
2442
|
+
async logTransaction(input) {
|
|
2443
|
+
const record = await this.logger.logTransaction(input);
|
|
2444
|
+
if (this.anomalyDetector.isEnabled()) {
|
|
2445
|
+
this.anomalyDetector.evaluateTransaction(record);
|
|
2446
|
+
}
|
|
2447
|
+
return record;
|
|
2448
|
+
}
|
|
2449
|
+
/**
|
|
2450
|
+
* Flush any pending log batches.
|
|
2451
|
+
*/
|
|
2452
|
+
async flushLogs() {
|
|
2453
|
+
await this.logger.flush();
|
|
2454
|
+
}
|
|
2455
|
+
// --------------------------------------------------------------------------
|
|
2456
|
+
// Task Confirmation
|
|
2457
|
+
// --------------------------------------------------------------------------
|
|
2458
|
+
/**
|
|
2459
|
+
* Create a new tracked task that requires evidence for confirmation.
|
|
2460
|
+
*
|
|
2461
|
+
* @param input - Task details including required evidence types
|
|
2462
|
+
* @returns The created task
|
|
2463
|
+
*/
|
|
2464
|
+
async createTask(input) {
|
|
2465
|
+
return this.taskManager.createTask(input);
|
|
2466
|
+
}
|
|
2467
|
+
/**
|
|
2468
|
+
* Confirm a task by providing evidence.
|
|
2469
|
+
*
|
|
2470
|
+
* @param input - Task ID and evidence data
|
|
2471
|
+
* @returns The confirmed task
|
|
2472
|
+
*/
|
|
2473
|
+
async confirmTask(input) {
|
|
2474
|
+
return this.taskManager.confirmTask(input);
|
|
2475
|
+
}
|
|
2476
|
+
/**
|
|
2477
|
+
* Get the current status of a task.
|
|
2478
|
+
*
|
|
2479
|
+
* @param taskId - Task identifier
|
|
2480
|
+
* @returns The task or undefined if not found
|
|
2481
|
+
*/
|
|
2482
|
+
async getTaskStatus(taskId) {
|
|
2483
|
+
return this.taskManager.getTaskStatus(taskId);
|
|
2484
|
+
}
|
|
2485
|
+
/**
|
|
2486
|
+
* Mark a task as in-progress.
|
|
2487
|
+
*
|
|
2488
|
+
* @param taskId - Task identifier
|
|
2489
|
+
* @returns The updated task
|
|
2490
|
+
*/
|
|
2491
|
+
async startTask(taskId) {
|
|
2492
|
+
return this.taskManager.startTask(taskId);
|
|
2493
|
+
}
|
|
2494
|
+
/**
|
|
2495
|
+
* Mark a task as failed.
|
|
2496
|
+
*
|
|
2497
|
+
* @param taskId - Task identifier
|
|
2498
|
+
* @param reason - Reason for failure
|
|
2499
|
+
* @returns The updated task
|
|
2500
|
+
*/
|
|
2501
|
+
async failTask(taskId, reason) {
|
|
2502
|
+
return this.taskManager.failTask(taskId, reason);
|
|
2503
|
+
}
|
|
2504
|
+
/**
|
|
2505
|
+
* Get all tasks, optionally filtered by status.
|
|
2506
|
+
*
|
|
2507
|
+
* @param status - Optional status filter
|
|
2508
|
+
* @returns Array of tasks
|
|
2509
|
+
*/
|
|
2510
|
+
getTasks(status) {
|
|
2511
|
+
return this.taskManager.getTasks(status);
|
|
2512
|
+
}
|
|
2513
|
+
// --------------------------------------------------------------------------
|
|
2514
|
+
// Audit Export
|
|
2515
|
+
// --------------------------------------------------------------------------
|
|
2516
|
+
/**
|
|
2517
|
+
* Export audit data in JSON or CSV format.
|
|
2518
|
+
*
|
|
2519
|
+
* @param options - Export configuration
|
|
2520
|
+
* @returns Export result with formatted data
|
|
2521
|
+
*/
|
|
2522
|
+
async export(options) {
|
|
2523
|
+
return this.auditExporter.export(options);
|
|
2524
|
+
}
|
|
2525
|
+
/**
|
|
2526
|
+
* Generate a compliance report for a given period.
|
|
2527
|
+
*
|
|
2528
|
+
* @param options - Report configuration
|
|
2529
|
+
* @returns Compliance report with summary and detailed records
|
|
2530
|
+
*/
|
|
2531
|
+
async generateReport(options) {
|
|
2532
|
+
return this.auditExporter.generateReport(options);
|
|
2533
|
+
}
|
|
2534
|
+
/**
|
|
2535
|
+
* Generate a Suspicious Activity Report (SAR) template.
|
|
2536
|
+
*
|
|
2537
|
+
* This produces a structured SAR template populated with data from the SDK.
|
|
2538
|
+
* It is a template/structure, not an actual regulatory filing.
|
|
2539
|
+
*
|
|
2540
|
+
* @param options - Report configuration
|
|
2541
|
+
* @returns SAR report template
|
|
2542
|
+
*/
|
|
2543
|
+
async generateSARReport(options) {
|
|
2544
|
+
return this.auditExporter.generateSARReport(options);
|
|
2545
|
+
}
|
|
2546
|
+
/**
|
|
2547
|
+
* Generate a Currency Transaction Report (CTR) template.
|
|
2548
|
+
*
|
|
2549
|
+
* This produces a structured CTR template for transactions that meet or
|
|
2550
|
+
* exceed reporting thresholds. It is a template/structure, not an actual
|
|
2551
|
+
* regulatory filing.
|
|
2552
|
+
*
|
|
2553
|
+
* @param options - Report configuration
|
|
2554
|
+
* @returns CTR report template
|
|
2555
|
+
*/
|
|
2556
|
+
async generateCTRReport(options) {
|
|
2557
|
+
return this.auditExporter.generateCTRReport(options);
|
|
2558
|
+
}
|
|
2559
|
+
// --------------------------------------------------------------------------
|
|
2560
|
+
// Trust Scoring
|
|
2561
|
+
// --------------------------------------------------------------------------
|
|
2562
|
+
/**
|
|
2563
|
+
* Get the trust score for an agent.
|
|
2564
|
+
*
|
|
2565
|
+
* @param agentId - Agent identifier
|
|
2566
|
+
* @returns Trust score with factor breakdown
|
|
2567
|
+
*/
|
|
2568
|
+
async getTrustScore(agentId) {
|
|
2569
|
+
return this.trustScorer.getTrustScore(agentId);
|
|
2570
|
+
}
|
|
2571
|
+
/**
|
|
2572
|
+
* Evaluate the risk of a specific transaction.
|
|
2573
|
+
*
|
|
2574
|
+
* @param tx - Transaction to evaluate
|
|
2575
|
+
* @returns Transaction evaluation with risk score and recommendation
|
|
2576
|
+
*/
|
|
2577
|
+
async evaluateTransaction(tx) {
|
|
2578
|
+
return this.trustScorer.evaluateTransaction(tx);
|
|
2579
|
+
}
|
|
2580
|
+
// --------------------------------------------------------------------------
|
|
2581
|
+
// Anomaly Detection
|
|
2582
|
+
// --------------------------------------------------------------------------
|
|
2583
|
+
/**
|
|
2584
|
+
* Enable anomaly detection with the specified rules and thresholds.
|
|
2585
|
+
*
|
|
2586
|
+
* @param config - Detection configuration
|
|
2587
|
+
*/
|
|
2588
|
+
enableAnomalyDetection(config) {
|
|
2589
|
+
this.anomalyDetector.enableAnomalyDetection(config);
|
|
2590
|
+
}
|
|
2591
|
+
/**
|
|
2592
|
+
* Disable anomaly detection.
|
|
2593
|
+
*/
|
|
2594
|
+
disableAnomalyDetection() {
|
|
2595
|
+
this.anomalyDetector.disableAnomalyDetection();
|
|
2596
|
+
}
|
|
2597
|
+
/**
|
|
2598
|
+
* Register a callback for anomaly events.
|
|
2599
|
+
*
|
|
2600
|
+
* @param callback - Function to call when an anomaly is detected
|
|
2601
|
+
* @returns Unsubscribe function
|
|
2602
|
+
*/
|
|
2603
|
+
onAnomaly(callback) {
|
|
2604
|
+
return this.anomalyDetector.onAnomaly(callback);
|
|
2605
|
+
}
|
|
2606
|
+
// --------------------------------------------------------------------------
|
|
2607
|
+
// Digest Chain
|
|
2608
|
+
// --------------------------------------------------------------------------
|
|
2609
|
+
/**
|
|
2610
|
+
* Get the terminal digest — the latest SHA-256 hash in the rolling digest chain.
|
|
2611
|
+
* Embed this in outgoing messages as tamper-evident proof of the entire action history.
|
|
2612
|
+
*
|
|
2613
|
+
* @returns The terminal SHA-256 digest hex string
|
|
2614
|
+
*/
|
|
2615
|
+
getTerminalDigest() {
|
|
2616
|
+
return this.logger.getTerminalDigest();
|
|
2617
|
+
}
|
|
2618
|
+
/**
|
|
2619
|
+
* Verify the integrity of the digest chain.
|
|
2620
|
+
* Recomputes every digest from genesis and compares against stored values.
|
|
2621
|
+
* Any tampering will cause verification to fail.
|
|
2622
|
+
*
|
|
2623
|
+
* @returns Verification result with timing and validity data
|
|
2624
|
+
*/
|
|
2625
|
+
verifyDigestChain() {
|
|
2626
|
+
const actions = this.store.getActions();
|
|
2627
|
+
return this.logger.verifyChain(actions);
|
|
2628
|
+
}
|
|
2629
|
+
/**
|
|
2630
|
+
* Export the digest chain for independent third-party verification.
|
|
2631
|
+
*
|
|
2632
|
+
* @returns Chain data including genesis hash, all links, and terminal digest
|
|
2633
|
+
*/
|
|
2634
|
+
exportDigestChain() {
|
|
2635
|
+
return this.logger.getDigestChain().exportChain();
|
|
2636
|
+
}
|
|
2637
|
+
// --------------------------------------------------------------------------
|
|
2638
|
+
// USDC Integration
|
|
2639
|
+
// --------------------------------------------------------------------------
|
|
2640
|
+
/**
|
|
2641
|
+
* Run USDC-specific compliance checks on a transaction.
|
|
2642
|
+
*
|
|
2643
|
+
* @param tx - Transaction to check
|
|
2644
|
+
* @returns Compliance check result
|
|
2645
|
+
*/
|
|
2646
|
+
checkUsdcCompliance(tx) {
|
|
2647
|
+
return UsdcCompliance.checkTransaction(tx);
|
|
2648
|
+
}
|
|
2649
|
+
// --------------------------------------------------------------------------
|
|
2650
|
+
// Lifecycle
|
|
2651
|
+
// --------------------------------------------------------------------------
|
|
2652
|
+
/**
|
|
2653
|
+
* Gracefully shut down the SDK, flushing any pending data.
|
|
2654
|
+
*/
|
|
2655
|
+
async destroy() {
|
|
2656
|
+
await this.logger.destroy();
|
|
2657
|
+
}
|
|
2658
|
+
};
|
|
2659
|
+
|
|
2660
|
+
// src/integrations/cctp.ts
|
|
2661
|
+
var CCTP_DOMAINS = {
|
|
2662
|
+
ethereum: 0,
|
|
2663
|
+
arbitrum: 3,
|
|
2664
|
+
optimism: 2,
|
|
2665
|
+
base: 6,
|
|
2666
|
+
polygon: 7,
|
|
2667
|
+
// Arc (Circle's stablecoin-native blockchain) -- placeholder domain ID, update when Arc mainnet launches
|
|
2668
|
+
arc: 10
|
|
2669
|
+
};
|
|
2670
|
+
var CCTPTransferManager = class {
|
|
2671
|
+
transfers = /* @__PURE__ */ new Map();
|
|
2672
|
+
actionLinks = /* @__PURE__ */ new Map();
|
|
2673
|
+
/**
|
|
2674
|
+
* Validate a cross-chain transfer before execution.
|
|
2675
|
+
*
|
|
2676
|
+
* Checks include:
|
|
2677
|
+
* - Source and destination chain support
|
|
2678
|
+
* - Route validity (different chains)
|
|
2679
|
+
* - Token support on both chains
|
|
2680
|
+
* - Amount validation
|
|
2681
|
+
* - Address format validation
|
|
2682
|
+
*
|
|
2683
|
+
* @param input - Transfer details to validate
|
|
2684
|
+
* @returns Validation result with checks and recommendations
|
|
2685
|
+
*/
|
|
2686
|
+
validateTransfer(input) {
|
|
2687
|
+
const checks = [];
|
|
2688
|
+
checks.push(this.checkChainSupport(input.sourceChain, "source"));
|
|
2689
|
+
checks.push(this.checkChainSupport(input.destinationChain, "destination"));
|
|
2690
|
+
checks.push(this.checkRouteValidity(input.sourceChain, input.destinationChain));
|
|
2691
|
+
checks.push(this.checkTokenSupport(input.token));
|
|
2692
|
+
checks.push(this.checkAmountValidity(input.amount));
|
|
2693
|
+
checks.push(this.checkAddressFormat(input.sender, "sender"));
|
|
2694
|
+
checks.push(this.checkAddressFormat(input.recipient, "recipient"));
|
|
2695
|
+
const failedChecks = checks.filter((c) => !c.passed);
|
|
2696
|
+
const valid = failedChecks.length === 0;
|
|
2697
|
+
const highestSeverity = failedChecks.reduce(
|
|
2698
|
+
(max, c) => {
|
|
2699
|
+
const order = ["low", "medium", "high", "critical"];
|
|
2700
|
+
return order.indexOf(c.severity) > order.indexOf(max) ? c.severity : max;
|
|
2701
|
+
},
|
|
2702
|
+
"low"
|
|
2703
|
+
);
|
|
2704
|
+
const recommendations = this.generateRecommendations(checks, input);
|
|
2705
|
+
return {
|
|
2706
|
+
valid,
|
|
2707
|
+
checks,
|
|
2708
|
+
riskLevel: valid ? "low" : highestSeverity,
|
|
2709
|
+
recommendations
|
|
2710
|
+
};
|
|
2711
|
+
}
|
|
2712
|
+
/**
|
|
2713
|
+
* Record a new cross-chain transfer initiated via CCTP depositForBurn.
|
|
2714
|
+
*
|
|
2715
|
+
* @param input - Transfer initiation details
|
|
2716
|
+
* @returns The created CrossChainTransfer record
|
|
2717
|
+
*/
|
|
2718
|
+
initiateTransfer(input) {
|
|
2719
|
+
const id = generateId();
|
|
2720
|
+
const correlationId = input.correlationId ?? generateId();
|
|
2721
|
+
const transfer = {
|
|
2722
|
+
id,
|
|
2723
|
+
sourceChain: input.sourceChain,
|
|
2724
|
+
destinationChain: input.destinationChain,
|
|
2725
|
+
sourceDomain: CCTP_DOMAINS[input.sourceChain] ?? -1,
|
|
2726
|
+
destinationDomain: CCTP_DOMAINS[input.destinationChain] ?? -1,
|
|
2727
|
+
amount: input.amount,
|
|
2728
|
+
token: input.token,
|
|
2729
|
+
sender: input.sender,
|
|
2730
|
+
recipient: input.recipient,
|
|
2731
|
+
sourceTxHash: input.sourceTxHash,
|
|
2732
|
+
destinationTxHash: null,
|
|
2733
|
+
messageHash: null,
|
|
2734
|
+
status: "pending",
|
|
2735
|
+
nonce: input.nonce ?? null,
|
|
2736
|
+
initiatedAt: now(),
|
|
2737
|
+
attestedAt: null,
|
|
2738
|
+
confirmedAt: null,
|
|
2739
|
+
correlationId,
|
|
2740
|
+
agentId: input.agentId,
|
|
2741
|
+
metadata: input.metadata ?? {}
|
|
2742
|
+
};
|
|
2743
|
+
this.transfers.set(id, transfer);
|
|
2744
|
+
this.actionLinks.set(id, {});
|
|
2745
|
+
return transfer;
|
|
2746
|
+
}
|
|
2747
|
+
/**
|
|
2748
|
+
* Record a CCTP attestation for a pending transfer.
|
|
2749
|
+
* Called after the attestation service has signed the burn message.
|
|
2750
|
+
*
|
|
2751
|
+
* @param input - Attestation details
|
|
2752
|
+
* @returns The updated CrossChainTransfer record
|
|
2753
|
+
* @throws Error if transfer not found or not in pending status
|
|
2754
|
+
*/
|
|
2755
|
+
recordAttestation(input) {
|
|
2756
|
+
const transfer = this.transfers.get(input.transferId);
|
|
2757
|
+
if (!transfer) {
|
|
2758
|
+
throw new Error(`Cross-chain transfer not found: ${input.transferId}`);
|
|
2759
|
+
}
|
|
2760
|
+
if (transfer.status !== "pending") {
|
|
2761
|
+
throw new Error(
|
|
2762
|
+
`Transfer ${input.transferId} is not in pending status (current: ${transfer.status})`
|
|
2763
|
+
);
|
|
2764
|
+
}
|
|
2765
|
+
const updated = {
|
|
2766
|
+
...transfer,
|
|
2767
|
+
messageHash: input.messageHash,
|
|
2768
|
+
status: "attested",
|
|
2769
|
+
attestedAt: now(),
|
|
2770
|
+
metadata: {
|
|
2771
|
+
...transfer.metadata,
|
|
2772
|
+
...input.metadata
|
|
2773
|
+
}
|
|
2774
|
+
};
|
|
2775
|
+
this.transfers.set(input.transferId, updated);
|
|
2776
|
+
return updated;
|
|
2777
|
+
}
|
|
2778
|
+
/**
|
|
2779
|
+
* Confirm a cross-chain transfer has been received on the destination chain.
|
|
2780
|
+
* Called after receiveMessage has been executed on the destination.
|
|
2781
|
+
*
|
|
2782
|
+
* @param input - Confirmation details
|
|
2783
|
+
* @returns The updated CrossChainTransfer record
|
|
2784
|
+
* @throws Error if transfer not found or not in attested status
|
|
2785
|
+
*/
|
|
2786
|
+
confirmTransfer(input) {
|
|
2787
|
+
const transfer = this.transfers.get(input.transferId);
|
|
2788
|
+
if (!transfer) {
|
|
2789
|
+
throw new Error(`Cross-chain transfer not found: ${input.transferId}`);
|
|
2790
|
+
}
|
|
2791
|
+
if (transfer.status !== "attested") {
|
|
2792
|
+
throw new Error(
|
|
2793
|
+
`Transfer ${input.transferId} is not in attested status (current: ${transfer.status})`
|
|
2794
|
+
);
|
|
2795
|
+
}
|
|
2796
|
+
const updated = {
|
|
2797
|
+
...transfer,
|
|
2798
|
+
destinationTxHash: input.destinationTxHash,
|
|
2799
|
+
status: "confirmed",
|
|
2800
|
+
confirmedAt: now(),
|
|
2801
|
+
metadata: {
|
|
2802
|
+
...transfer.metadata,
|
|
2803
|
+
...input.metadata
|
|
2804
|
+
}
|
|
2805
|
+
};
|
|
2806
|
+
this.transfers.set(input.transferId, updated);
|
|
2807
|
+
return updated;
|
|
2808
|
+
}
|
|
2809
|
+
/**
|
|
2810
|
+
* Mark a transfer as failed.
|
|
2811
|
+
*
|
|
2812
|
+
* @param transferId - The transfer to mark as failed
|
|
2813
|
+
* @param reason - Reason for failure
|
|
2814
|
+
* @returns The updated CrossChainTransfer record
|
|
2815
|
+
*/
|
|
2816
|
+
failTransfer(transferId, reason) {
|
|
2817
|
+
const transfer = this.transfers.get(transferId);
|
|
2818
|
+
if (!transfer) {
|
|
2819
|
+
throw new Error(`Cross-chain transfer not found: ${transferId}`);
|
|
2820
|
+
}
|
|
2821
|
+
const updated = {
|
|
2822
|
+
...transfer,
|
|
2823
|
+
status: "failed",
|
|
2824
|
+
metadata: {
|
|
2825
|
+
...transfer.metadata,
|
|
2826
|
+
failureReason: reason,
|
|
2827
|
+
failedAt: now()
|
|
2828
|
+
}
|
|
2829
|
+
};
|
|
2830
|
+
this.transfers.set(transferId, updated);
|
|
2831
|
+
return updated;
|
|
2832
|
+
}
|
|
2833
|
+
/**
|
|
2834
|
+
* Link a Kontext action log ID to a cross-chain transfer.
|
|
2835
|
+
* Used to correlate source and destination chain actions in the audit trail.
|
|
2836
|
+
*
|
|
2837
|
+
* @param transferId - The cross-chain transfer ID
|
|
2838
|
+
* @param actionId - The action log ID to link
|
|
2839
|
+
* @param side - Whether this is the source or destination action
|
|
2840
|
+
*/
|
|
2841
|
+
linkAction(transferId, actionId, side) {
|
|
2842
|
+
const transfer = this.transfers.get(transferId);
|
|
2843
|
+
if (!transfer) {
|
|
2844
|
+
throw new Error(`Cross-chain transfer not found: ${transferId}`);
|
|
2845
|
+
}
|
|
2846
|
+
const links = this.actionLinks.get(transferId) ?? {};
|
|
2847
|
+
if (side === "source") {
|
|
2848
|
+
links.sourceActionId = actionId;
|
|
2849
|
+
} else {
|
|
2850
|
+
links.destinationActionId = actionId;
|
|
2851
|
+
}
|
|
2852
|
+
this.actionLinks.set(transferId, links);
|
|
2853
|
+
}
|
|
2854
|
+
/**
|
|
2855
|
+
* Get a cross-chain transfer by ID.
|
|
2856
|
+
*/
|
|
2857
|
+
getTransfer(transferId) {
|
|
2858
|
+
return this.transfers.get(transferId);
|
|
2859
|
+
}
|
|
2860
|
+
/**
|
|
2861
|
+
* Get all cross-chain transfers, optionally filtered by status.
|
|
2862
|
+
*/
|
|
2863
|
+
getTransfers(status) {
|
|
2864
|
+
const all = Array.from(this.transfers.values());
|
|
2865
|
+
if (status) {
|
|
2866
|
+
return all.filter((t) => t.status === status);
|
|
2867
|
+
}
|
|
2868
|
+
return all;
|
|
2869
|
+
}
|
|
2870
|
+
/**
|
|
2871
|
+
* Get transfers by correlation ID.
|
|
2872
|
+
* Useful for finding all transfers related to a single workflow.
|
|
2873
|
+
*/
|
|
2874
|
+
getTransfersByCorrelation(correlationId) {
|
|
2875
|
+
return Array.from(this.transfers.values()).filter(
|
|
2876
|
+
(t) => t.correlationId === correlationId
|
|
2877
|
+
);
|
|
2878
|
+
}
|
|
2879
|
+
/**
|
|
2880
|
+
* Build a cross-chain audit trail for a given transfer.
|
|
2881
|
+
* Links source and destination chain actions together.
|
|
2882
|
+
*
|
|
2883
|
+
* @param transferId - The transfer to build an audit trail for
|
|
2884
|
+
* @returns CrossChainAuditEntry with linked action references
|
|
2885
|
+
*/
|
|
2886
|
+
getAuditEntry(transferId) {
|
|
2887
|
+
const transfer = this.transfers.get(transferId);
|
|
2888
|
+
if (!transfer) return void 0;
|
|
2889
|
+
const links = this.actionLinks.get(transferId) ?? {};
|
|
2890
|
+
let durationMs = null;
|
|
2891
|
+
if (transfer.confirmedAt && transfer.initiatedAt) {
|
|
2892
|
+
durationMs = new Date(transfer.confirmedAt).getTime() - new Date(transfer.initiatedAt).getTime();
|
|
2893
|
+
}
|
|
2894
|
+
return {
|
|
2895
|
+
transfer,
|
|
2896
|
+
sourceActionId: links.sourceActionId ?? null,
|
|
2897
|
+
destinationActionId: links.destinationActionId ?? null,
|
|
2898
|
+
linked: !!(links.sourceActionId && links.destinationActionId),
|
|
2899
|
+
durationMs
|
|
2900
|
+
};
|
|
2901
|
+
}
|
|
2902
|
+
/**
|
|
2903
|
+
* Build audit trail entries for all transfers, optionally filtered.
|
|
2904
|
+
*
|
|
2905
|
+
* @param agentId - Optional filter by agent
|
|
2906
|
+
* @returns Array of CrossChainAuditEntry records
|
|
2907
|
+
*/
|
|
2908
|
+
getAuditTrail(agentId) {
|
|
2909
|
+
let transfers = Array.from(this.transfers.values());
|
|
2910
|
+
if (agentId) {
|
|
2911
|
+
transfers = transfers.filter((t) => t.agentId === agentId);
|
|
2912
|
+
}
|
|
2913
|
+
return transfers.map((t) => this.getAuditEntry(t.id)).filter((entry) => entry !== void 0);
|
|
2914
|
+
}
|
|
2915
|
+
/**
|
|
2916
|
+
* Get the CCTP domain ID for a given chain.
|
|
2917
|
+
*
|
|
2918
|
+
* @param chain - The blockchain network
|
|
2919
|
+
* @returns The CCTP domain ID, or undefined for unsupported chains
|
|
2920
|
+
*/
|
|
2921
|
+
static getDomainId(chain) {
|
|
2922
|
+
return CCTP_DOMAINS[chain];
|
|
2923
|
+
}
|
|
2924
|
+
/**
|
|
2925
|
+
* Get the chains supported for CCTP transfers.
|
|
2926
|
+
*/
|
|
2927
|
+
static getSupportedChains() {
|
|
2928
|
+
return Object.keys(CCTP_DOMAINS);
|
|
2929
|
+
}
|
|
2930
|
+
// --------------------------------------------------------------------------
|
|
2931
|
+
// Validation checks
|
|
2932
|
+
// --------------------------------------------------------------------------
|
|
2933
|
+
checkChainSupport(chain, label) {
|
|
2934
|
+
const supported = chain in CCTP_DOMAINS;
|
|
2935
|
+
return {
|
|
2936
|
+
name: `cctp_${label}_chain`,
|
|
2937
|
+
passed: supported,
|
|
2938
|
+
description: supported ? `${label} chain ${chain} supports CCTP (domain ${CCTP_DOMAINS[chain]})` : `${label} chain ${chain} does not support CCTP`,
|
|
2939
|
+
severity: supported ? "low" : "high"
|
|
2940
|
+
};
|
|
2941
|
+
}
|
|
2942
|
+
checkRouteValidity(source, destination) {
|
|
2943
|
+
const valid = source !== destination;
|
|
2944
|
+
return {
|
|
2945
|
+
name: "cctp_route_validity",
|
|
2946
|
+
passed: valid,
|
|
2947
|
+
description: valid ? `Valid cross-chain route: ${source} -> ${destination}` : `Invalid route: source and destination chains are the same (${source})`,
|
|
2948
|
+
severity: valid ? "low" : "critical"
|
|
2949
|
+
};
|
|
2950
|
+
}
|
|
2951
|
+
checkTokenSupport(token) {
|
|
2952
|
+
const supported = token === "USDC" || token === "EURC";
|
|
2953
|
+
return {
|
|
2954
|
+
name: "cctp_token_support",
|
|
2955
|
+
passed: supported,
|
|
2956
|
+
description: supported ? `Token ${token} is supported for CCTP transfers` : `Token ${token} is not natively supported by CCTP (only USDC and EURC)`,
|
|
2957
|
+
severity: supported ? "low" : "high"
|
|
2958
|
+
};
|
|
2959
|
+
}
|
|
2960
|
+
checkAmountValidity(amount) {
|
|
2961
|
+
const parsed = parseAmount(amount);
|
|
2962
|
+
const valid = !isNaN(parsed) && parsed > 0;
|
|
2963
|
+
return {
|
|
2964
|
+
name: "cctp_amount_validity",
|
|
2965
|
+
passed: valid,
|
|
2966
|
+
description: valid ? `Transfer amount ${amount} is valid` : `Transfer amount ${amount} is invalid`,
|
|
2967
|
+
severity: valid ? "low" : "critical"
|
|
2968
|
+
};
|
|
2969
|
+
}
|
|
2970
|
+
checkAddressFormat(address, label) {
|
|
2971
|
+
const isValid = /^0x[a-fA-F0-9]{40}$/.test(address);
|
|
2972
|
+
return {
|
|
2973
|
+
name: `cctp_address_${label}`,
|
|
2974
|
+
passed: isValid,
|
|
2975
|
+
description: isValid ? `${label} address format is valid` : `${label} address format is invalid: ${address}`,
|
|
2976
|
+
severity: isValid ? "low" : "high"
|
|
2977
|
+
};
|
|
2978
|
+
}
|
|
2979
|
+
// --------------------------------------------------------------------------
|
|
2980
|
+
// Recommendations
|
|
2981
|
+
// --------------------------------------------------------------------------
|
|
2982
|
+
generateRecommendations(checks, input) {
|
|
2983
|
+
const recommendations = [];
|
|
2984
|
+
const amount = parseAmount(input.amount);
|
|
2985
|
+
const failedChecks = checks.filter((c) => !c.passed);
|
|
2986
|
+
if (failedChecks.some((c) => c.severity === "critical")) {
|
|
2987
|
+
recommendations.push(
|
|
2988
|
+
"Do not proceed with this transfer. Critical validation failures detected."
|
|
2989
|
+
);
|
|
2990
|
+
}
|
|
2991
|
+
if (failedChecks.some((c) => c.name === "cctp_token_support")) {
|
|
2992
|
+
recommendations.push(
|
|
2993
|
+
"Consider using USDC for native CCTP support. Other tokens require bridge protocols."
|
|
2994
|
+
);
|
|
2995
|
+
}
|
|
2996
|
+
if (!isNaN(amount) && amount >= 5e4) {
|
|
2997
|
+
recommendations.push(
|
|
2998
|
+
"Large cross-chain transfer detected. Verify recipient identity and document purpose."
|
|
2999
|
+
);
|
|
3000
|
+
}
|
|
3001
|
+
if (!isNaN(amount) && amount >= 1e4) {
|
|
3002
|
+
recommendations.push(
|
|
3003
|
+
"Cross-chain transfer meets reporting threshold. Ensure CTR filing if applicable."
|
|
3004
|
+
);
|
|
3005
|
+
}
|
|
3006
|
+
if (failedChecks.length === 0) {
|
|
3007
|
+
recommendations.push(
|
|
3008
|
+
"Transfer validation passed. Monitor attestation status for completion."
|
|
3009
|
+
);
|
|
3010
|
+
}
|
|
3011
|
+
return recommendations;
|
|
3012
|
+
}
|
|
3013
|
+
};
|
|
3014
|
+
|
|
3015
|
+
// src/webhooks.ts
|
|
3016
|
+
var DEFAULT_RETRY_CONFIG = {
|
|
3017
|
+
maxRetries: 3,
|
|
3018
|
+
baseDelayMs: 1e3,
|
|
3019
|
+
maxDelayMs: 3e4
|
|
3020
|
+
};
|
|
3021
|
+
var WebhookManager = class {
|
|
3022
|
+
webhooks = /* @__PURE__ */ new Map();
|
|
3023
|
+
deliveryResults = [];
|
|
3024
|
+
retryConfig;
|
|
3025
|
+
fetchFn;
|
|
3026
|
+
constructor(retryConfig, fetchFn) {
|
|
3027
|
+
this.retryConfig = { ...DEFAULT_RETRY_CONFIG, ...retryConfig };
|
|
3028
|
+
this.fetchFn = fetchFn ?? globalThis.fetch;
|
|
3029
|
+
}
|
|
3030
|
+
/**
|
|
3031
|
+
* Register a new webhook endpoint.
|
|
3032
|
+
*
|
|
3033
|
+
* @param input - Webhook configuration
|
|
3034
|
+
* @returns The created WebhookConfig
|
|
3035
|
+
*/
|
|
3036
|
+
register(input) {
|
|
3037
|
+
if (!input.url || input.url.trim() === "") {
|
|
3038
|
+
throw new Error("Webhook URL is required");
|
|
3039
|
+
}
|
|
3040
|
+
if (!input.events || input.events.length === 0) {
|
|
3041
|
+
throw new Error("At least one event type is required");
|
|
3042
|
+
}
|
|
3043
|
+
const config = {
|
|
3044
|
+
id: generateId(),
|
|
3045
|
+
url: input.url,
|
|
3046
|
+
events: input.events,
|
|
3047
|
+
secret: input.secret,
|
|
3048
|
+
active: true,
|
|
3049
|
+
createdAt: now(),
|
|
3050
|
+
metadata: input.metadata
|
|
3051
|
+
};
|
|
3052
|
+
this.webhooks.set(config.id, config);
|
|
3053
|
+
return config;
|
|
3054
|
+
}
|
|
3055
|
+
/**
|
|
3056
|
+
* Unregister a webhook by ID.
|
|
3057
|
+
*
|
|
3058
|
+
* @param webhookId - The webhook to remove
|
|
3059
|
+
* @returns Whether the webhook was found and removed
|
|
3060
|
+
*/
|
|
3061
|
+
unregister(webhookId) {
|
|
3062
|
+
return this.webhooks.delete(webhookId);
|
|
3063
|
+
}
|
|
3064
|
+
/**
|
|
3065
|
+
* Enable or disable a webhook.
|
|
3066
|
+
*
|
|
3067
|
+
* @param webhookId - The webhook to update
|
|
3068
|
+
* @param active - Whether to enable or disable
|
|
3069
|
+
* @returns The updated WebhookConfig, or undefined if not found
|
|
3070
|
+
*/
|
|
3071
|
+
setActive(webhookId, active) {
|
|
3072
|
+
const webhook = this.webhooks.get(webhookId);
|
|
3073
|
+
if (!webhook) return void 0;
|
|
3074
|
+
const updated = { ...webhook, active };
|
|
3075
|
+
this.webhooks.set(webhookId, updated);
|
|
3076
|
+
return updated;
|
|
3077
|
+
}
|
|
3078
|
+
/**
|
|
3079
|
+
* Get all registered webhooks.
|
|
3080
|
+
*/
|
|
3081
|
+
getWebhooks() {
|
|
3082
|
+
return Array.from(this.webhooks.values());
|
|
3083
|
+
}
|
|
3084
|
+
/**
|
|
3085
|
+
* Get a specific webhook by ID.
|
|
3086
|
+
*/
|
|
3087
|
+
getWebhook(webhookId) {
|
|
3088
|
+
return this.webhooks.get(webhookId);
|
|
3089
|
+
}
|
|
3090
|
+
/**
|
|
3091
|
+
* Get delivery results for a specific webhook or all webhooks.
|
|
3092
|
+
*/
|
|
3093
|
+
getDeliveryResults(webhookId) {
|
|
3094
|
+
if (webhookId) {
|
|
3095
|
+
return this.deliveryResults.filter((r) => r.webhookId === webhookId);
|
|
3096
|
+
}
|
|
3097
|
+
return [...this.deliveryResults];
|
|
3098
|
+
}
|
|
3099
|
+
/**
|
|
3100
|
+
* Notify all subscribed webhooks of an anomaly detection event.
|
|
3101
|
+
*
|
|
3102
|
+
* @param anomaly - The detected anomaly event
|
|
3103
|
+
* @returns Array of delivery results
|
|
3104
|
+
*/
|
|
3105
|
+
async notifyAnomalyDetected(anomaly) {
|
|
3106
|
+
const payload = {
|
|
3107
|
+
id: generateId(),
|
|
3108
|
+
event: "anomaly.detected",
|
|
3109
|
+
timestamp: now(),
|
|
3110
|
+
data: {
|
|
3111
|
+
anomalyId: anomaly.id,
|
|
3112
|
+
type: anomaly.type,
|
|
3113
|
+
severity: anomaly.severity,
|
|
3114
|
+
description: anomaly.description,
|
|
3115
|
+
agentId: anomaly.agentId,
|
|
3116
|
+
actionId: anomaly.actionId,
|
|
3117
|
+
detectedAt: anomaly.detectedAt,
|
|
3118
|
+
data: anomaly.data
|
|
3119
|
+
}
|
|
3120
|
+
};
|
|
3121
|
+
return this.deliver("anomaly.detected", payload);
|
|
3122
|
+
}
|
|
3123
|
+
/**
|
|
3124
|
+
* Notify all subscribed webhooks of a task confirmation.
|
|
3125
|
+
*
|
|
3126
|
+
* @param task - The confirmed task
|
|
3127
|
+
* @returns Array of delivery results
|
|
3128
|
+
*/
|
|
3129
|
+
async notifyTaskConfirmed(task) {
|
|
3130
|
+
const payload = {
|
|
3131
|
+
id: generateId(),
|
|
3132
|
+
event: "task.confirmed",
|
|
3133
|
+
timestamp: now(),
|
|
3134
|
+
data: {
|
|
3135
|
+
taskId: task.id,
|
|
3136
|
+
description: task.description,
|
|
3137
|
+
agentId: task.agentId,
|
|
3138
|
+
status: task.status,
|
|
3139
|
+
confirmedAt: task.confirmedAt,
|
|
3140
|
+
correlationId: task.correlationId
|
|
3141
|
+
}
|
|
3142
|
+
};
|
|
3143
|
+
return this.deliver("task.confirmed", payload);
|
|
3144
|
+
}
|
|
3145
|
+
/**
|
|
3146
|
+
* Notify all subscribed webhooks of a task failure.
|
|
3147
|
+
*
|
|
3148
|
+
* @param task - The failed task
|
|
3149
|
+
* @returns Array of delivery results
|
|
3150
|
+
*/
|
|
3151
|
+
async notifyTaskFailed(task) {
|
|
3152
|
+
const payload = {
|
|
3153
|
+
id: generateId(),
|
|
3154
|
+
event: "task.failed",
|
|
3155
|
+
timestamp: now(),
|
|
3156
|
+
data: {
|
|
3157
|
+
taskId: task.id,
|
|
3158
|
+
description: task.description,
|
|
3159
|
+
agentId: task.agentId,
|
|
3160
|
+
status: task.status,
|
|
3161
|
+
correlationId: task.correlationId,
|
|
3162
|
+
metadata: task.metadata
|
|
3163
|
+
}
|
|
3164
|
+
};
|
|
3165
|
+
return this.deliver("task.failed", payload);
|
|
3166
|
+
}
|
|
3167
|
+
/**
|
|
3168
|
+
* Notify all subscribed webhooks of a trust score change.
|
|
3169
|
+
*
|
|
3170
|
+
* @param trustScore - The new trust score
|
|
3171
|
+
* @param previousScore - The previous score value (if known)
|
|
3172
|
+
* @returns Array of delivery results
|
|
3173
|
+
*/
|
|
3174
|
+
async notifyTrustScoreChanged(trustScore, previousScore) {
|
|
3175
|
+
const payload = {
|
|
3176
|
+
id: generateId(),
|
|
3177
|
+
event: "trust.score_changed",
|
|
3178
|
+
timestamp: now(),
|
|
3179
|
+
data: {
|
|
3180
|
+
agentId: trustScore.agentId,
|
|
3181
|
+
score: trustScore.score,
|
|
3182
|
+
previousScore: previousScore ?? null,
|
|
3183
|
+
level: trustScore.level,
|
|
3184
|
+
factors: trustScore.factors,
|
|
3185
|
+
computedAt: trustScore.computedAt
|
|
3186
|
+
}
|
|
3187
|
+
};
|
|
3188
|
+
return this.deliver("trust.score_changed", payload);
|
|
3189
|
+
}
|
|
3190
|
+
// --------------------------------------------------------------------------
|
|
3191
|
+
// Delivery with retry
|
|
3192
|
+
// --------------------------------------------------------------------------
|
|
3193
|
+
async deliver(eventType, payload) {
|
|
3194
|
+
const subscribers = Array.from(this.webhooks.values()).filter(
|
|
3195
|
+
(w) => w.active && w.events.includes(eventType)
|
|
3196
|
+
);
|
|
3197
|
+
const results = [];
|
|
3198
|
+
for (const webhook of subscribers) {
|
|
3199
|
+
const result = await this.deliverToWebhook(webhook, payload);
|
|
3200
|
+
results.push(result);
|
|
3201
|
+
this.deliveryResults.push(result);
|
|
3202
|
+
}
|
|
3203
|
+
return results;
|
|
3204
|
+
}
|
|
3205
|
+
async deliverToWebhook(webhook, payload) {
|
|
3206
|
+
let lastError = null;
|
|
3207
|
+
let statusCode = null;
|
|
3208
|
+
for (let attempt = 0; attempt <= this.retryConfig.maxRetries; attempt++) {
|
|
3209
|
+
try {
|
|
3210
|
+
if (attempt > 0) {
|
|
3211
|
+
const delay = Math.min(
|
|
3212
|
+
this.retryConfig.baseDelayMs * Math.pow(2, attempt - 1),
|
|
3213
|
+
this.retryConfig.maxDelayMs
|
|
3214
|
+
);
|
|
3215
|
+
await this.sleep(delay);
|
|
3216
|
+
}
|
|
3217
|
+
const response = await this.fetchFn(webhook.url, {
|
|
3218
|
+
method: "POST",
|
|
3219
|
+
headers: {
|
|
3220
|
+
"Content-Type": "application/json",
|
|
3221
|
+
"X-Kontext-Event": payload.event,
|
|
3222
|
+
"X-Kontext-Delivery": payload.id,
|
|
3223
|
+
...webhook.secret ? { "X-Kontext-Signature": await this.computeSignature(payload, webhook.secret) } : {}
|
|
3224
|
+
},
|
|
3225
|
+
body: JSON.stringify(payload)
|
|
3226
|
+
});
|
|
3227
|
+
statusCode = response.status;
|
|
3228
|
+
if (response.ok) {
|
|
3229
|
+
return {
|
|
3230
|
+
webhookId: webhook.id,
|
|
3231
|
+
payloadId: payload.id,
|
|
3232
|
+
success: true,
|
|
3233
|
+
statusCode,
|
|
3234
|
+
attempts: attempt + 1,
|
|
3235
|
+
error: null,
|
|
3236
|
+
lastAttemptAt: now()
|
|
3237
|
+
};
|
|
3238
|
+
}
|
|
3239
|
+
lastError = `HTTP ${response.status}`;
|
|
3240
|
+
} catch (error) {
|
|
3241
|
+
lastError = error instanceof Error ? error.message : String(error);
|
|
3242
|
+
}
|
|
3243
|
+
}
|
|
3244
|
+
return {
|
|
3245
|
+
webhookId: webhook.id,
|
|
3246
|
+
payloadId: payload.id,
|
|
3247
|
+
success: false,
|
|
3248
|
+
statusCode,
|
|
3249
|
+
attempts: this.retryConfig.maxRetries + 1,
|
|
3250
|
+
error: lastError,
|
|
3251
|
+
lastAttemptAt: now()
|
|
3252
|
+
};
|
|
3253
|
+
}
|
|
3254
|
+
async computeSignature(payload, secret) {
|
|
3255
|
+
const { createHmac } = await import('crypto');
|
|
3256
|
+
const hmac = createHmac("sha256", secret);
|
|
3257
|
+
hmac.update(JSON.stringify(payload));
|
|
3258
|
+
return hmac.digest("hex");
|
|
3259
|
+
}
|
|
3260
|
+
sleep(ms) {
|
|
3261
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
3262
|
+
}
|
|
3263
|
+
};
|
|
3264
|
+
|
|
3265
|
+
exports.CCTPTransferManager = CCTPTransferManager;
|
|
3266
|
+
exports.DigestChain = DigestChain;
|
|
3267
|
+
exports.Kontext = Kontext;
|
|
3268
|
+
exports.KontextError = KontextError;
|
|
3269
|
+
exports.KontextErrorCode = KontextErrorCode;
|
|
3270
|
+
exports.UsdcCompliance = UsdcCompliance;
|
|
3271
|
+
exports.WebhookManager = WebhookManager;
|
|
3272
|
+
exports.verifyExportedChain = verifyExportedChain;
|
|
3273
|
+
//# sourceMappingURL=index.js.map
|
|
3274
|
+
//# sourceMappingURL=index.js.map
|