@x402sentinel/x402 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,1091 @@
1
+ 'use strict';
2
+
3
+ var chunk7H4FRU7K_cjs = require('./chunk-7H4FRU7K.cjs');
4
+ var crypto = require('crypto');
5
+ var fs = require('fs');
6
+ var path = require('path');
7
+
8
+ // src/budget/spike-detector.ts
9
+ var SpikeDetector = class {
10
+ windowSize;
11
+ threshold;
12
+ window = [];
13
+ constructor(threshold = 3, windowSize = 20) {
14
+ this.threshold = threshold;
15
+ this.windowSize = windowSize;
16
+ }
17
+ /** Record a payment amount into the rolling window */
18
+ record(amount) {
19
+ this.window.push(amount);
20
+ if (this.window.length > this.windowSize) {
21
+ this.window.shift();
22
+ }
23
+ }
24
+ /**
25
+ * Check if a proposed amount is a spike relative to the rolling average.
26
+ * Returns null if no spike, or a description string if spike detected.
27
+ * Needs at least 3 data points before spike detection activates.
28
+ */
29
+ check(amount) {
30
+ if (this.window.length < 3) return null;
31
+ const avg = chunk7H4FRU7K_cjs.calculateAverage(this.window);
32
+ const avgRaw = chunk7H4FRU7K_cjs.parseUSDC(avg);
33
+ if (avgRaw === 0n) return null;
34
+ const amountRaw = chunk7H4FRU7K_cjs.parseUSDC(amount);
35
+ const ratio = Number(amountRaw) / Number(avgRaw);
36
+ if (ratio > this.threshold) {
37
+ return `Payment $${amount} is ${ratio.toFixed(1)}x the rolling average $${chunk7H4FRU7K_cjs.formatUSDC(avgRaw)} (threshold: ${this.threshold}x)`;
38
+ }
39
+ return null;
40
+ }
41
+ /** Get the current rolling average */
42
+ getAverage() {
43
+ return chunk7H4FRU7K_cjs.calculateAverage(this.window);
44
+ }
45
+ /** Get the current window contents */
46
+ getWindow() {
47
+ return [...this.window];
48
+ }
49
+ /** Restore state from serialized data */
50
+ loadWindow(amounts) {
51
+ this.window.length = 0;
52
+ for (const a of amounts.slice(-this.windowSize)) {
53
+ this.window.push(a);
54
+ }
55
+ }
56
+ };
57
+
58
+ // src/budget/policies.ts
59
+ function conservativePolicy() {
60
+ return {
61
+ maxPerCall: "0.10",
62
+ maxPerHour: "5.00",
63
+ maxPerDay: "50.00",
64
+ spikeThreshold: 3
65
+ };
66
+ }
67
+ function standardPolicy() {
68
+ return {
69
+ maxPerCall: "1.00",
70
+ maxPerHour: "25.00",
71
+ maxPerDay: "200.00",
72
+ spikeThreshold: 3
73
+ };
74
+ }
75
+ function liberalPolicy() {
76
+ return {
77
+ maxPerCall: "10.00",
78
+ maxPerHour: "100.00",
79
+ maxPerDay: "1000.00",
80
+ spikeThreshold: 5
81
+ };
82
+ }
83
+ function unlimitedPolicy() {
84
+ return {};
85
+ }
86
+ function customPolicy(overrides) {
87
+ return { ...standardPolicy(), ...overrides };
88
+ }
89
+
90
+ // src/budget/index.ts
91
+ var BudgetManager = class _BudgetManager {
92
+ state;
93
+ policy;
94
+ spikeDetector;
95
+ constructor(policy) {
96
+ this.policy = policy;
97
+ this.spikeDetector = new SpikeDetector(
98
+ policy.spikeThreshold ?? 3
99
+ );
100
+ const now = Date.now();
101
+ this.state = {
102
+ totalSpent: "0.000000",
103
+ hourlySpent: "0.000000",
104
+ dailySpent: "0.000000",
105
+ callCount: 0,
106
+ lastReset: { hourly: chunk7H4FRU7K_cjs.getHourStart(now), daily: chunk7H4FRU7K_cjs.getDayStart(now) },
107
+ rollingAverage: "0.000000",
108
+ rollingWindow: []
109
+ };
110
+ }
111
+ /**
112
+ * Evaluate a proposed payment against the budget policy.
113
+ * Called BEFORE every payment. Returns allow/deny with reason.
114
+ */
115
+ evaluate(context) {
116
+ this.maybeResetWindows();
117
+ const warnings = [];
118
+ if (this.policy.blockedEndpoints?.length) {
119
+ for (const pattern of this.policy.blockedEndpoints) {
120
+ if (endpointMatches(context.endpoint, pattern)) {
121
+ return {
122
+ allowed: false,
123
+ violation: this.violation("blocked_endpoint", "0.000000", "0.000000", context)
124
+ };
125
+ }
126
+ }
127
+ }
128
+ if (this.policy.allowedEndpoints?.length) {
129
+ const matched = this.policy.allowedEndpoints.some(
130
+ (p) => endpointMatches(context.endpoint, p)
131
+ );
132
+ if (!matched) {
133
+ return {
134
+ allowed: false,
135
+ violation: this.violation("blocked_endpoint", "0.000000", "0.000000", context)
136
+ };
137
+ }
138
+ }
139
+ if (this.policy.maxPerCall) {
140
+ if (chunk7H4FRU7K_cjs.compareUSDC(context.amount, this.policy.maxPerCall) > 0) {
141
+ return {
142
+ allowed: false,
143
+ violation: this.violation("per_call", this.policy.maxPerCall, "0.000000", context)
144
+ };
145
+ }
146
+ }
147
+ const spikeMsg = this.spikeDetector.check(context.amount);
148
+ if (spikeMsg) {
149
+ if (this.policy.spikeThreshold !== void 0) {
150
+ return {
151
+ allowed: false,
152
+ violation: this.violation(
153
+ "spike",
154
+ this.spikeDetector.getAverage(),
155
+ this.state.hourlySpent,
156
+ context
157
+ )
158
+ };
159
+ }
160
+ warnings.push(spikeMsg);
161
+ }
162
+ if (this.policy.maxPerHour) {
163
+ const projectedHourly = chunk7H4FRU7K_cjs.addUSDC(this.state.hourlySpent, context.amount);
164
+ if (chunk7H4FRU7K_cjs.compareUSDC(projectedHourly, this.policy.maxPerHour) > 0) {
165
+ return {
166
+ allowed: false,
167
+ violation: this.violation("hourly", this.policy.maxPerHour, this.state.hourlySpent, context)
168
+ };
169
+ }
170
+ }
171
+ if (this.policy.maxPerDay) {
172
+ const projectedDaily = chunk7H4FRU7K_cjs.addUSDC(this.state.dailySpent, context.amount);
173
+ if (chunk7H4FRU7K_cjs.compareUSDC(projectedDaily, this.policy.maxPerDay) > 0) {
174
+ return {
175
+ allowed: false,
176
+ violation: this.violation("daily", this.policy.maxPerDay, this.state.dailySpent, context)
177
+ };
178
+ }
179
+ }
180
+ if (this.policy.maxTotal) {
181
+ const projectedTotal = chunk7H4FRU7K_cjs.addUSDC(this.state.totalSpent, context.amount);
182
+ if (chunk7H4FRU7K_cjs.compareUSDC(projectedTotal, this.policy.maxTotal) > 0) {
183
+ return {
184
+ allowed: false,
185
+ violation: this.violation("total", this.policy.maxTotal, this.state.totalSpent, context)
186
+ };
187
+ }
188
+ }
189
+ if (this.policy.requireApproval) {
190
+ if (chunk7H4FRU7K_cjs.compareUSDC(context.amount, this.policy.requireApproval.above) > 0) {
191
+ return {
192
+ allowed: false,
193
+ violation: this.violation(
194
+ "approval_required",
195
+ this.policy.requireApproval.above,
196
+ this.state.totalSpent,
197
+ context
198
+ )
199
+ };
200
+ }
201
+ }
202
+ return { allowed: true, warnings };
203
+ }
204
+ /** Record a completed payment. Called AFTER payment succeeds. */
205
+ record(amount, _endpoint) {
206
+ this.maybeResetWindows();
207
+ this.state.totalSpent = chunk7H4FRU7K_cjs.addUSDC(this.state.totalSpent, amount);
208
+ this.state.hourlySpent = chunk7H4FRU7K_cjs.addUSDC(this.state.hourlySpent, amount);
209
+ this.state.dailySpent = chunk7H4FRU7K_cjs.addUSDC(this.state.dailySpent, amount);
210
+ this.state.callCount++;
211
+ this.spikeDetector.record(amount);
212
+ this.state.rollingAverage = this.spikeDetector.getAverage();
213
+ this.state.rollingWindow = this.spikeDetector.getWindow();
214
+ }
215
+ /** Get current budget state (for dashboard/debugging) */
216
+ getState() {
217
+ this.maybeResetWindows();
218
+ return { ...this.state, rollingWindow: [...this.state.rollingWindow] };
219
+ }
220
+ /** Reset spending counters for a given scope */
221
+ reset(scope) {
222
+ const now = Date.now();
223
+ switch (scope) {
224
+ case "hourly":
225
+ this.state.hourlySpent = "0.000000";
226
+ this.state.lastReset.hourly = chunk7H4FRU7K_cjs.getHourStart(now);
227
+ break;
228
+ case "daily":
229
+ this.state.dailySpent = "0.000000";
230
+ this.state.lastReset.daily = chunk7H4FRU7K_cjs.getDayStart(now);
231
+ break;
232
+ case "total":
233
+ this.state.totalSpent = "0.000000";
234
+ this.state.hourlySpent = "0.000000";
235
+ this.state.dailySpent = "0.000000";
236
+ this.state.callCount = 0;
237
+ break;
238
+ }
239
+ }
240
+ /** Serialize state for persistence */
241
+ serialize() {
242
+ return JSON.stringify(this.state);
243
+ }
244
+ /** Restore a BudgetManager from serialized state */
245
+ static deserialize(data, policy) {
246
+ const mgr = new _BudgetManager(policy);
247
+ const parsed = JSON.parse(data);
248
+ mgr.state = parsed;
249
+ mgr.spikeDetector.loadWindow(parsed.rollingWindow);
250
+ return mgr;
251
+ }
252
+ /** Auto-reset hourly/daily windows when the time window has rolled over */
253
+ maybeResetWindows() {
254
+ const now = Date.now();
255
+ const hourStart = chunk7H4FRU7K_cjs.getHourStart(now);
256
+ const dayStart = chunk7H4FRU7K_cjs.getDayStart(now);
257
+ if (hourStart > this.state.lastReset.hourly) {
258
+ this.state.hourlySpent = "0.000000";
259
+ this.state.lastReset.hourly = hourStart;
260
+ }
261
+ if (dayStart > this.state.lastReset.daily) {
262
+ this.state.dailySpent = "0.000000";
263
+ this.state.lastReset.daily = dayStart;
264
+ }
265
+ }
266
+ violation(type, limit, current, context) {
267
+ return {
268
+ type,
269
+ limit,
270
+ current,
271
+ attempted: context.amount,
272
+ agentId: context.agentId,
273
+ endpoint: context.endpoint,
274
+ timestamp: Date.now()
275
+ };
276
+ }
277
+ };
278
+ function endpointMatches(url, pattern) {
279
+ const regex = new RegExp(
280
+ "^" + pattern.replace(/\*/g, ".*").replace(/\?/g, ".") + "$"
281
+ );
282
+ return regex.test(url);
283
+ }
284
+
285
+ // src/audit/storage/memory.ts
286
+ var MemoryStorage = class {
287
+ records = /* @__PURE__ */ new Map();
288
+ insertOrder = [];
289
+ maxRecords;
290
+ constructor(maxRecords = 1e4) {
291
+ this.maxRecords = maxRecords;
292
+ }
293
+ async write(record) {
294
+ if (this.records.size >= this.maxRecords) {
295
+ const oldest = this.insertOrder.shift();
296
+ if (oldest) this.records.delete(oldest);
297
+ }
298
+ this.records.set(record.id, record);
299
+ this.insertOrder.push(record.id);
300
+ }
301
+ async query(query) {
302
+ let results = this.filter(query);
303
+ results = this.sort(results, query.orderBy, query.order);
304
+ const offset = query.offset ?? 0;
305
+ const limit = query.limit ?? results.length;
306
+ return results.slice(offset, offset + limit);
307
+ }
308
+ async summarize(query) {
309
+ const records = this.filter(query);
310
+ return buildSummary(records, query);
311
+ }
312
+ async count(query) {
313
+ return this.filter(query).length;
314
+ }
315
+ async getById(id) {
316
+ return this.records.get(id) ?? null;
317
+ }
318
+ filter(query) {
319
+ return [...this.records.values()].filter((r) => matchesQuery(r, query));
320
+ }
321
+ sort(records, orderBy, order) {
322
+ if (!orderBy) return records;
323
+ const dir = order === "desc" ? -1 : 1;
324
+ return records.sort((a, b) => {
325
+ switch (orderBy) {
326
+ case "created_at":
327
+ return (a.created_at - b.created_at) * dir;
328
+ case "amount":
329
+ return chunk7H4FRU7K_cjs.compareUSDC(a.amount, b.amount) * dir;
330
+ case "endpoint":
331
+ return a.endpoint.localeCompare(b.endpoint) * dir;
332
+ default:
333
+ return 0;
334
+ }
335
+ });
336
+ }
337
+ };
338
+ function matchesQuery(record, query) {
339
+ if (query.agentId && record.agent_id !== query.agentId) return false;
340
+ if (query.team && record.team !== query.team) return false;
341
+ if (query.endpoint && !record.endpoint.includes(query.endpoint)) return false;
342
+ if (query.minAmount && chunk7H4FRU7K_cjs.compareUSDC(record.amount, query.minAmount) < 0) return false;
343
+ if (query.maxAmount && chunk7H4FRU7K_cjs.compareUSDC(record.amount, query.maxAmount) > 0) return false;
344
+ if (query.startTime && record.created_at < query.startTime) return false;
345
+ if (query.endTime && record.created_at > query.endTime) return false;
346
+ if (query.status?.length && !query.status.includes(record.policy_evaluation)) return false;
347
+ if (query.tags?.length && !query.tags.some((t) => record.tags.includes(t))) return false;
348
+ return true;
349
+ }
350
+ function buildSummary(records, query) {
351
+ let totalRaw = 0n;
352
+ let maxRaw = 0n;
353
+ const agents = /* @__PURE__ */ new Set();
354
+ const endpoints = /* @__PURE__ */ new Set();
355
+ const byAgent = {};
356
+ const byEndpoint = {};
357
+ const byTeam = {};
358
+ let violations = 0;
359
+ let minTs = Infinity;
360
+ let maxTs = 0;
361
+ for (const r of records) {
362
+ const raw = chunk7H4FRU7K_cjs.parseUSDC(r.amount);
363
+ totalRaw += raw;
364
+ if (raw > maxRaw) maxRaw = raw;
365
+ agents.add(r.agent_id);
366
+ endpoints.add(r.endpoint);
367
+ if (r.created_at < minTs) minTs = r.created_at;
368
+ if (r.created_at > maxTs) maxTs = r.created_at;
369
+ if (r.policy_evaluation === "blocked") violations++;
370
+ const agentEntry = byAgent[r.agent_id] ?? { spend: "0.000000", count: 0 };
371
+ agentEntry.spend = chunk7H4FRU7K_cjs.formatUSDC(chunk7H4FRU7K_cjs.parseUSDC(agentEntry.spend) + raw);
372
+ agentEntry.count++;
373
+ byAgent[r.agent_id] = agentEntry;
374
+ const epEntry = byEndpoint[r.endpoint] ?? { spend: "0.000000", count: 0 };
375
+ epEntry.spend = chunk7H4FRU7K_cjs.formatUSDC(chunk7H4FRU7K_cjs.parseUSDC(epEntry.spend) + raw);
376
+ epEntry.count++;
377
+ byEndpoint[r.endpoint] = epEntry;
378
+ const teamKey = r.team ?? "(none)";
379
+ const teamEntry = byTeam[teamKey] ?? { spend: "0.000000", count: 0 };
380
+ teamEntry.spend = chunk7H4FRU7K_cjs.formatUSDC(chunk7H4FRU7K_cjs.parseUSDC(teamEntry.spend) + raw);
381
+ teamEntry.count++;
382
+ byTeam[teamKey] = teamEntry;
383
+ }
384
+ const count = records.length;
385
+ return {
386
+ total_spend: chunk7H4FRU7K_cjs.formatUSDCHuman(totalRaw),
387
+ total_transactions: count,
388
+ unique_endpoints: endpoints.size,
389
+ unique_agents: agents.size,
390
+ avg_payment: count > 0 ? chunk7H4FRU7K_cjs.formatUSDCHuman(totalRaw / BigInt(count)) : "0.00",
391
+ max_payment: chunk7H4FRU7K_cjs.formatUSDCHuman(maxRaw),
392
+ by_agent: byAgent,
393
+ by_endpoint: byEndpoint,
394
+ by_team: byTeam,
395
+ violations,
396
+ period: {
397
+ start: query.startTime ?? (minTs === Infinity ? 0 : minTs),
398
+ end: query.endTime ?? (maxTs === 0 ? 0 : maxTs)
399
+ }
400
+ };
401
+ }
402
+ function generateRecordId(agentId, endpoint, timestamp, amount) {
403
+ const input = `${agentId}|${endpoint}|${timestamp}|${amount}`;
404
+ const hash = crypto.createHash("sha256").update(input).digest("hex");
405
+ return hash.slice(0, 16);
406
+ }
407
+
408
+ // src/audit/enrichment.ts
409
+ function enrichRecord(input) {
410
+ const { context, config, statusCode, responseTimeMs, settlement, policyEvaluation, budgetRemaining } = input;
411
+ const now = Date.now();
412
+ const id = generateRecordId(context.agentId, context.endpoint, now, context.amount);
413
+ const tags = computeTags(context, config.audit?.enrichment);
414
+ return {
415
+ id,
416
+ agent_id: context.agentId,
417
+ team: context.team,
418
+ human_sponsor: config.humanSponsor ?? null,
419
+ amount: context.amount,
420
+ amount_raw: context.amountRaw,
421
+ asset: context.asset,
422
+ network: context.network,
423
+ scheme: context.scheme,
424
+ tx_hash: settlement?.transaction ?? null,
425
+ payer_address: settlement?.payer ?? "",
426
+ payee_address: context.payTo,
427
+ facilitator: null,
428
+ endpoint: context.endpoint,
429
+ method: context.method,
430
+ status_code: statusCode,
431
+ response_time_ms: responseTimeMs,
432
+ policy_id: null,
433
+ policy_evaluation: policyEvaluation,
434
+ budget_remaining: budgetRemaining,
435
+ task_id: context.metadata["task_id"] ?? null,
436
+ session_id: context.metadata["session_id"] ?? null,
437
+ metadata: { ...context.metadata, ...config.metadata },
438
+ created_at: now,
439
+ settled_at: settlement?.success ? now : null,
440
+ tags
441
+ };
442
+ }
443
+ function enrichBlockedRecord(context, config, policyEvaluation) {
444
+ return enrichRecord({
445
+ context,
446
+ config,
447
+ statusCode: 0,
448
+ responseTimeMs: 0,
449
+ settlement: null,
450
+ policyEvaluation,
451
+ budgetRemaining: null
452
+ });
453
+ }
454
+ function computeTags(context, enrichment) {
455
+ const tags = [];
456
+ if (enrichment?.staticTags) {
457
+ tags.push(...enrichment.staticTags);
458
+ }
459
+ if (enrichment?.tagRules) {
460
+ for (const rule of enrichment.tagRules) {
461
+ const regex = new RegExp(rule.pattern);
462
+ if (regex.test(context.endpoint)) {
463
+ tags.push(...rule.tags);
464
+ }
465
+ }
466
+ }
467
+ return tags;
468
+ }
469
+
470
+ // src/audit/export.ts
471
+ var CSV_HEADERS = [
472
+ "id",
473
+ "agent_id",
474
+ "team",
475
+ "human_sponsor",
476
+ "amount",
477
+ "amount_raw",
478
+ "asset",
479
+ "network",
480
+ "scheme",
481
+ "tx_hash",
482
+ "payer_address",
483
+ "payee_address",
484
+ "facilitator",
485
+ "endpoint",
486
+ "method",
487
+ "status_code",
488
+ "response_time_ms",
489
+ "policy_id",
490
+ "policy_evaluation",
491
+ "budget_remaining",
492
+ "task_id",
493
+ "session_id",
494
+ "created_at",
495
+ "settled_at"
496
+ ];
497
+ function toCSV(records) {
498
+ const lines = [CSV_HEADERS.join(",")];
499
+ for (const record of records) {
500
+ const values = CSV_HEADERS.map((key) => {
501
+ const val = record[key];
502
+ if (val === null || val === void 0) return "";
503
+ const str = String(val);
504
+ if (str.includes(",") || str.includes('"') || str.includes("\n")) {
505
+ return `"${str.replace(/"/g, '""')}"`;
506
+ }
507
+ return str;
508
+ });
509
+ lines.push(values.join(","));
510
+ }
511
+ return lines.join("\n") + "\n";
512
+ }
513
+ function toJSON(records, pretty = false) {
514
+ return JSON.stringify(records, null, pretty ? 2 : void 0);
515
+ }
516
+
517
+ // src/audit/index.ts
518
+ var AuditLogger = class {
519
+ storage;
520
+ enabled;
521
+ redactFields;
522
+ constructor(config) {
523
+ this.enabled = config?.enabled !== false;
524
+ this.storage = config?.storage ?? new MemoryStorage();
525
+ this.redactFields = config?.redactFields ?? [];
526
+ }
527
+ /** Log a completed payment record */
528
+ async log(record) {
529
+ const now = Date.now();
530
+ const full = {
531
+ ...record,
532
+ id: generateRecordId(record.agent_id, record.endpoint, now, record.amount),
533
+ created_at: now
534
+ };
535
+ const redacted = this.redact(full);
536
+ if (this.enabled) {
537
+ try {
538
+ await this.storage.write(redacted);
539
+ } catch (err) {
540
+ console.warn("[sentinel] Audit write failed (non-fatal):", err);
541
+ }
542
+ }
543
+ return redacted;
544
+ }
545
+ /** Log a blocked payment attempt */
546
+ async logBlocked(context, violation, config) {
547
+ const record = enrichBlockedRecord(context, {
548
+ agentId: context.agentId,
549
+ ...config
550
+ }, "blocked");
551
+ record.metadata["violation_type"] = violation.type;
552
+ record.metadata["violation_limit"] = violation.limit;
553
+ if (this.enabled) {
554
+ try {
555
+ await this.storage.write(this.redact(record));
556
+ } catch (err) {
557
+ console.warn("[sentinel] Audit write failed (non-fatal):", err);
558
+ }
559
+ }
560
+ return record;
561
+ }
562
+ /** Query audit records */
563
+ async query(query) {
564
+ return this.storage.query(query);
565
+ }
566
+ /** Get summary statistics */
567
+ async summarize(query) {
568
+ return this.storage.summarize(query ?? {});
569
+ }
570
+ /** Export records as CSV */
571
+ async exportCSV(query) {
572
+ const records = await this.storage.query(query ?? {});
573
+ return toCSV(records);
574
+ }
575
+ /** Export records as JSON */
576
+ async exportJSON(query) {
577
+ const records = await this.storage.query(query ?? {});
578
+ return toJSON(records, true);
579
+ }
580
+ /** Flush pending writes to storage */
581
+ async flush() {
582
+ if ("flush" in this.storage && typeof this.storage.flush === "function") {
583
+ await this.storage.flush();
584
+ }
585
+ }
586
+ /** Get the underlying storage backend (for dashboard integration) */
587
+ getStorage() {
588
+ return this.storage;
589
+ }
590
+ redact(record) {
591
+ if (this.redactFields.length === 0) return record;
592
+ const copy = { ...record, metadata: { ...record.metadata } };
593
+ for (const field of this.redactFields) {
594
+ if (field in copy.metadata) {
595
+ copy.metadata[field] = "[REDACTED]";
596
+ }
597
+ }
598
+ return copy;
599
+ }
600
+ };
601
+
602
+ // src/errors.ts
603
+ var SentinelError = class extends Error {
604
+ code;
605
+ constructor(message, code) {
606
+ super(message);
607
+ this.name = "SentinelError";
608
+ this.code = code;
609
+ }
610
+ };
611
+ var SentinelBudgetError = class extends SentinelError {
612
+ violation;
613
+ constructor(violation) {
614
+ super(buildBudgetMessage(violation), "BUDGET_EXCEEDED");
615
+ this.name = "SentinelBudgetError";
616
+ this.violation = violation;
617
+ }
618
+ };
619
+ var SentinelAuditError = class extends SentinelError {
620
+ record;
621
+ constructor(message, record) {
622
+ super(message, "AUDIT_ERROR");
623
+ this.name = "SentinelAuditError";
624
+ this.record = record;
625
+ }
626
+ };
627
+ var SentinelConfigError = class extends SentinelError {
628
+ constructor(message) {
629
+ super(message, "CONFIG_ERROR");
630
+ this.name = "SentinelConfigError";
631
+ }
632
+ };
633
+ function buildBudgetMessage(v) {
634
+ const attempted = `$${v.attempted}`;
635
+ switch (v.type) {
636
+ case "per_call":
637
+ return `Budget exceeded: ${attempted} exceeds per-call limit of $${v.limit} on ${v.agentId}`;
638
+ case "hourly":
639
+ return `Budget exceeded: $${v.current} spent of $${v.limit} hourly limit on ${v.agentId} (attempted ${attempted})`;
640
+ case "daily":
641
+ return `Budget exceeded: $${v.current} spent of $${v.limit} daily limit on ${v.agentId} (attempted ${attempted})`;
642
+ case "total":
643
+ return `Budget exceeded: $${v.current} spent of $${v.limit} total limit on ${v.agentId} (attempted ${attempted})`;
644
+ case "spike":
645
+ return `Price spike detected: ${attempted} vs rolling average $${v.limit} on ${v.agentId}`;
646
+ case "blocked_endpoint":
647
+ return `Blocked endpoint: ${v.endpoint} is not allowed for ${v.agentId}`;
648
+ case "approval_required":
649
+ return `Approval required: ${attempted} exceeds approval threshold $${v.limit} on ${v.agentId}`;
650
+ }
651
+ }
652
+ function validateConfig(config) {
653
+ if (!config.agentId || config.agentId.trim() === "") {
654
+ throw new SentinelConfigError("agentId is required and must be non-empty");
655
+ }
656
+ const budget = config.budget;
657
+ if (!budget) return;
658
+ const amountFields = ["maxPerCall", "maxPerHour", "maxPerDay", "maxTotal"];
659
+ for (const field of amountFields) {
660
+ const value = budget[field];
661
+ if (value !== void 0) {
662
+ try {
663
+ chunk7H4FRU7K_cjs.parseUSDC(value);
664
+ } catch {
665
+ throw new SentinelConfigError(
666
+ `budget.${field} "${value}" is not a valid USDC amount`
667
+ );
668
+ }
669
+ }
670
+ }
671
+ if (budget.spikeThreshold !== void 0 && budget.spikeThreshold <= 1) {
672
+ throw new SentinelConfigError(
673
+ `budget.spikeThreshold must be > 1.0, got ${budget.spikeThreshold}`
674
+ );
675
+ }
676
+ if (budget.allowedEndpoints?.length && budget.blockedEndpoints?.length) {
677
+ throw new SentinelConfigError(
678
+ "Cannot set both allowedEndpoints and blockedEndpoints \u2014 use one or the other"
679
+ );
680
+ }
681
+ }
682
+
683
+ // src/wrapper/headers.ts
684
+ function parsePaymentRequired(header) {
685
+ const json = Buffer.from(header, "base64").toString("utf-8");
686
+ return JSON.parse(json);
687
+ }
688
+ function parsePaymentResponse(header) {
689
+ if (!header) return null;
690
+ try {
691
+ const json = Buffer.from(header, "base64").toString("utf-8");
692
+ return JSON.parse(json);
693
+ } catch {
694
+ return null;
695
+ }
696
+ }
697
+ function extractFromPaymentRequired(paymentRequired) {
698
+ const first = paymentRequired.accepts[0];
699
+ if (!first) return null;
700
+ return {
701
+ amount: first.amount,
702
+ asset: first.asset,
703
+ network: first.network,
704
+ scheme: first.scheme,
705
+ payTo: first.payTo
706
+ };
707
+ }
708
+
709
+ // src/wrapper/interceptor.ts
710
+ function beforeRequest(url, init, deps) {
711
+ const method = init?.method ?? "GET";
712
+ const context = {
713
+ endpoint: url,
714
+ method: method.toUpperCase(),
715
+ agentId: deps.config.agentId,
716
+ team: deps.config.team ?? null,
717
+ amount: "0.000000",
718
+ amountRaw: "0",
719
+ asset: "",
720
+ network: "",
721
+ scheme: "",
722
+ payTo: "",
723
+ timestamp: Date.now(),
724
+ metadata: { ...deps.config.metadata ?? {} }
725
+ };
726
+ if (deps.budgetManager) {
727
+ const eval_ = deps.budgetManager.evaluate(context);
728
+ if (!eval_.allowed && eval_.violation.type === "blocked_endpoint") {
729
+ return { proceed: false, context, evaluation: eval_ };
730
+ }
731
+ }
732
+ return { proceed: true, context };
733
+ }
734
+ async function afterResponse(response, context, startTime, deps) {
735
+ const responseTimeMs = Date.now() - startTime;
736
+ const paymentResponseHeader = response.headers.get("payment-response");
737
+ if (paymentResponseHeader) {
738
+ const settlement = parsePaymentResponse(paymentResponseHeader);
739
+ if (settlement) {
740
+ context.network = settlement.network ?? context.network;
741
+ const policyEvaluation = determinePolicyEvaluation(context, deps);
742
+ let budgetRemaining = null;
743
+ if (deps.budgetManager && context.amount !== "0.000000") {
744
+ deps.budgetManager.record(context.amount, context.endpoint);
745
+ const state = deps.budgetManager.getState();
746
+ if (deps.config.budget?.maxTotal) {
747
+ budgetRemaining = chunk7H4FRU7K_cjs.formatUSDCHuman(
748
+ chunk7H4FRU7K_cjs.parseUSDC(deps.config.budget.maxTotal) - chunk7H4FRU7K_cjs.parseUSDC(state.totalSpent)
749
+ );
750
+ }
751
+ }
752
+ const record = enrichRecord({
753
+ context,
754
+ config: deps.config,
755
+ statusCode: response.status,
756
+ responseTimeMs,
757
+ settlement,
758
+ policyEvaluation: policyEvaluation ? "allowed" : "flagged",
759
+ budgetRemaining
760
+ });
761
+ try {
762
+ await deps.auditLogger.log(record);
763
+ } catch (err) {
764
+ console.warn("[sentinel] Audit log failed (non-fatal):", err);
765
+ }
766
+ if (deps.config.hooks?.afterPayment) {
767
+ try {
768
+ await deps.config.hooks.afterPayment(record);
769
+ } catch {
770
+ }
771
+ }
772
+ return record;
773
+ }
774
+ }
775
+ if (response.status === 402) {
776
+ const paymentRequiredHeader = response.headers.get("payment-required");
777
+ if (paymentRequiredHeader) {
778
+ try {
779
+ const paymentRequired = parsePaymentRequired(paymentRequiredHeader);
780
+ const extracted = extractFromPaymentRequired(paymentRequired);
781
+ if (extracted) {
782
+ context.amount = extracted.amount;
783
+ context.asset = extracted.asset;
784
+ context.network = extracted.network;
785
+ context.scheme = extracted.scheme;
786
+ context.payTo = extracted.payTo;
787
+ }
788
+ } catch {
789
+ }
790
+ }
791
+ const record = enrichRecord({
792
+ context,
793
+ config: deps.config,
794
+ statusCode: 402,
795
+ responseTimeMs,
796
+ settlement: null,
797
+ policyEvaluation: "flagged",
798
+ budgetRemaining: null
799
+ });
800
+ try {
801
+ await deps.auditLogger.log(record);
802
+ } catch (err) {
803
+ console.warn("[sentinel] Audit log failed (non-fatal):", err);
804
+ }
805
+ return record;
806
+ }
807
+ return null;
808
+ }
809
+ function determinePolicyEvaluation(context, deps) {
810
+ if (!deps.budgetManager || context.amount === "0.000000") return true;
811
+ const eval_ = deps.budgetManager.evaluate(context);
812
+ return eval_.allowed;
813
+ }
814
+
815
+ // src/wrapper/index.ts
816
+ function wrapWithSentinel(fetchWithPayment, config) {
817
+ validateConfig(config);
818
+ const budgetManager = config.budget ? new BudgetManager(config.budget) : null;
819
+ const auditLogger = new AuditLogger(config.audit ?? { enabled: true, storage: new MemoryStorage() });
820
+ const deps = { budgetManager, auditLogger, config };
821
+ const sentinelFetch = async (input, init) => {
822
+ const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
823
+ const startTime = Date.now();
824
+ const pre = beforeRequest(url, init, deps);
825
+ if (!pre.proceed && pre.evaluation && !pre.evaluation.allowed) {
826
+ const violation = pre.evaluation.violation;
827
+ try {
828
+ await auditLogger.logBlocked(pre.context, violation, {
829
+ humanSponsor: config.humanSponsor,
830
+ metadata: config.metadata
831
+ });
832
+ } catch {
833
+ }
834
+ if (config.hooks?.onBudgetExceeded) {
835
+ try {
836
+ await config.hooks.onBudgetExceeded(violation);
837
+ } catch {
838
+ }
839
+ }
840
+ throw new SentinelBudgetError(violation);
841
+ }
842
+ let response;
843
+ try {
844
+ response = await fetchWithPayment(input, init);
845
+ } catch (err) {
846
+ try {
847
+ const record = {
848
+ agent_id: pre.context.agentId,
849
+ team: pre.context.team,
850
+ human_sponsor: config.humanSponsor ?? null,
851
+ amount: "0.000000",
852
+ amount_raw: "0",
853
+ asset: "",
854
+ network: "",
855
+ scheme: "",
856
+ tx_hash: null,
857
+ payer_address: "",
858
+ payee_address: "",
859
+ facilitator: null,
860
+ endpoint: url,
861
+ method: pre.context.method,
862
+ status_code: 0,
863
+ response_time_ms: Date.now() - startTime,
864
+ policy_id: null,
865
+ policy_evaluation: "flagged",
866
+ budget_remaining: null,
867
+ task_id: null,
868
+ session_id: null,
869
+ metadata: { error: err instanceof Error ? err.message : String(err) },
870
+ settled_at: null,
871
+ tags: ["error", "network_failure"]
872
+ };
873
+ await auditLogger.log(record);
874
+ } catch {
875
+ }
876
+ throw err;
877
+ }
878
+ await afterResponse(response, pre.context, startTime, deps);
879
+ return response;
880
+ };
881
+ return sentinelFetch;
882
+ }
883
+ var FileStorage = class {
884
+ filePath;
885
+ buffer = [];
886
+ flushThreshold;
887
+ flushTimer = null;
888
+ constructor(filePath = ".valeo/audit.jsonl", flushThreshold = 100) {
889
+ this.filePath = filePath;
890
+ this.flushThreshold = flushThreshold;
891
+ this.ensureDir();
892
+ this.startAutoFlush();
893
+ }
894
+ async write(record) {
895
+ this.buffer.push(record);
896
+ if (this.buffer.length >= this.flushThreshold) {
897
+ await this.flush();
898
+ }
899
+ }
900
+ async query(query) {
901
+ await this.flush();
902
+ const all = this.readAll();
903
+ let results = all.filter((r) => matchesQuery(r, query));
904
+ const offset = query.offset ?? 0;
905
+ const limit = query.limit ?? results.length;
906
+ return results.slice(offset, offset + limit);
907
+ }
908
+ async summarize(query) {
909
+ await this.flush();
910
+ const records = this.readAll().filter((r) => matchesQuery(r, query));
911
+ return buildSummary(records, query);
912
+ }
913
+ async count(query) {
914
+ await this.flush();
915
+ return this.readAll().filter((r) => matchesQuery(r, query)).length;
916
+ }
917
+ async getById(id) {
918
+ await this.flush();
919
+ return this.readAll().find((r) => r.id === id) ?? null;
920
+ }
921
+ /** Write buffered records to disk */
922
+ async flush() {
923
+ if (this.buffer.length === 0) return;
924
+ const lines = this.buffer.map((r) => JSON.stringify(r)).join("\n") + "\n";
925
+ fs.appendFileSync(this.filePath, lines, "utf-8");
926
+ this.buffer = [];
927
+ }
928
+ /** Stop the auto-flush timer (for clean shutdown) */
929
+ destroy() {
930
+ if (this.flushTimer) {
931
+ clearInterval(this.flushTimer);
932
+ this.flushTimer = null;
933
+ }
934
+ }
935
+ readAll() {
936
+ if (!fs.existsSync(this.filePath)) return [];
937
+ const content = fs.readFileSync(this.filePath, "utf-8");
938
+ return content.split("\n").filter((line) => line.trim().length > 0).map((line) => JSON.parse(line));
939
+ }
940
+ ensureDir() {
941
+ const dir = path.dirname(this.filePath);
942
+ if (!fs.existsSync(dir)) {
943
+ fs.mkdirSync(dir, { recursive: true });
944
+ }
945
+ }
946
+ startAutoFlush() {
947
+ this.flushTimer = setInterval(() => {
948
+ void this.flush();
949
+ }, 5e3);
950
+ this.flushTimer.unref();
951
+ }
952
+ };
953
+
954
+ // src/audit/storage/api.ts
955
+ var ApiStorage = class {
956
+ apiKey;
957
+ baseUrl;
958
+ batchSize;
959
+ buffer = [];
960
+ fallback;
961
+ flushTimer = null;
962
+ useFallback = false;
963
+ constructor(config) {
964
+ this.apiKey = config.apiKey;
965
+ this.baseUrl = config.baseUrl ?? "https://api.valeo.money/v1";
966
+ this.batchSize = config.batchSize ?? 50;
967
+ this.fallback = new MemoryStorage();
968
+ this.startAutoFlush(config.flushIntervalMs ?? 1e4);
969
+ }
970
+ async write(record) {
971
+ this.buffer.push(record);
972
+ await this.fallback.write(record);
973
+ if (this.buffer.length >= this.batchSize) {
974
+ await this.flush();
975
+ }
976
+ }
977
+ async query(query) {
978
+ if (this.useFallback) return this.fallback.query(query);
979
+ try {
980
+ return await this.apiRequest("POST", "/audit/query", query);
981
+ } catch {
982
+ this.useFallback = true;
983
+ return this.fallback.query(query);
984
+ }
985
+ }
986
+ async summarize(query) {
987
+ if (this.useFallback) return this.fallback.summarize(query);
988
+ try {
989
+ return await this.apiRequest("POST", "/audit/summarize", query);
990
+ } catch {
991
+ this.useFallback = true;
992
+ return this.fallback.summarize(query);
993
+ }
994
+ }
995
+ async count(query) {
996
+ if (this.useFallback) return this.fallback.count(query);
997
+ try {
998
+ const result = await this.apiRequest("POST", "/audit/count", query);
999
+ return result.count;
1000
+ } catch {
1001
+ this.useFallback = true;
1002
+ return this.fallback.count(query);
1003
+ }
1004
+ }
1005
+ async getById(id) {
1006
+ if (this.useFallback) return this.fallback.getById(id);
1007
+ try {
1008
+ return await this.apiRequest("GET", `/audit/${id}`);
1009
+ } catch {
1010
+ this.useFallback = true;
1011
+ return this.fallback.getById(id);
1012
+ }
1013
+ }
1014
+ /** Flush buffered records to the remote API */
1015
+ async flush() {
1016
+ if (this.buffer.length === 0) return;
1017
+ const batch = this.buffer.splice(0, this.batchSize);
1018
+ try {
1019
+ await this.apiRequest("POST", "/audit/batch", { records: batch });
1020
+ } catch {
1021
+ this.useFallback = true;
1022
+ console.warn(
1023
+ `[sentinel] API unreachable, falling back to in-memory storage. ${batch.length} records buffered locally.`
1024
+ );
1025
+ }
1026
+ }
1027
+ /** Stop the auto-flush timer */
1028
+ destroy() {
1029
+ if (this.flushTimer) {
1030
+ clearInterval(this.flushTimer);
1031
+ this.flushTimer = null;
1032
+ }
1033
+ }
1034
+ async apiRequest(method, path, body) {
1035
+ const url = `${this.baseUrl}${path}`;
1036
+ const init = {
1037
+ method,
1038
+ headers: {
1039
+ "Content-Type": "application/json",
1040
+ Authorization: `Bearer ${this.apiKey}`
1041
+ }
1042
+ };
1043
+ if (body) {
1044
+ init.body = JSON.stringify(body);
1045
+ }
1046
+ let lastError;
1047
+ for (let attempt = 0; attempt < 3; attempt++) {
1048
+ try {
1049
+ const response = await fetch(url, init);
1050
+ if (!response.ok) {
1051
+ throw new Error(`API returned ${response.status}: ${response.statusText}`);
1052
+ }
1053
+ return await response.json();
1054
+ } catch (err) {
1055
+ lastError = err instanceof Error ? err : new Error(String(err));
1056
+ if (attempt < 2) {
1057
+ await sleep(Math.pow(2, attempt) * 1e3);
1058
+ }
1059
+ }
1060
+ }
1061
+ throw lastError;
1062
+ }
1063
+ startAutoFlush(intervalMs) {
1064
+ this.flushTimer = setInterval(() => {
1065
+ void this.flush();
1066
+ }, intervalMs);
1067
+ this.flushTimer.unref();
1068
+ }
1069
+ };
1070
+ function sleep(ms) {
1071
+ return new Promise((resolve) => setTimeout(resolve, ms));
1072
+ }
1073
+
1074
+ exports.ApiStorage = ApiStorage;
1075
+ exports.AuditLogger = AuditLogger;
1076
+ exports.BudgetManager = BudgetManager;
1077
+ exports.FileStorage = FileStorage;
1078
+ exports.MemoryStorage = MemoryStorage;
1079
+ exports.SentinelAuditError = SentinelAuditError;
1080
+ exports.SentinelBudgetError = SentinelBudgetError;
1081
+ exports.SentinelConfigError = SentinelConfigError;
1082
+ exports.SentinelError = SentinelError;
1083
+ exports.conservativePolicy = conservativePolicy;
1084
+ exports.customPolicy = customPolicy;
1085
+ exports.liberalPolicy = liberalPolicy;
1086
+ exports.standardPolicy = standardPolicy;
1087
+ exports.unlimitedPolicy = unlimitedPolicy;
1088
+ exports.validateConfig = validateConfig;
1089
+ exports.wrapWithSentinel = wrapWithSentinel;
1090
+ //# sourceMappingURL=index.cjs.map
1091
+ //# sourceMappingURL=index.cjs.map