@mnemopay/sdk 0.9.0 → 0.9.2

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.js CHANGED
@@ -14,7 +14,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
14
14
  return (mod && mod.__esModule) ? mod : { "default": mod };
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.createSandboxServer = exports.MockCommerceProvider = exports.CommerceEngine = exports.MnemoPayNetwork = exports.IdentityRegistry = exports.Ledger = exports.JSONFileStorage = exports.SQLiteStorage = exports.NIGERIAN_BANKS = exports.PaystackRail = exports.LightningRail = exports.StripeRail = exports.MockRail = exports.BehaviorProfile = exports.TransactionGraph = exports.IsolationForest = exports.DEFAULT_RATE_LIMIT = exports.DEFAULT_FRAUD_CONFIG = exports.RateLimiter = exports.FraudGuard = exports.l2Normalize = exports.localEmbed = exports.cosineSimilarity = exports.RecallEngine = exports.MnemoPay = exports.MnemoPayLite = void 0;
17
+ exports.createSandboxServer = exports.DEFAULT_ADAPTIVE_CONFIG = exports.AdaptiveEngine = exports.MockCommerceProvider = exports.CommerceEngine = exports.MnemoPayNetwork = exports.constantTimeEqual = exports.IdentityRegistry = exports.Ledger = exports.JSONFileStorage = exports.SQLiteStorage = exports.NIGERIAN_BANKS = exports.PaystackRail = exports.LightningRail = exports.StripeRail = exports.MockRail = exports.BehaviorProfile = exports.TransactionGraph = exports.IsolationForest = exports.DEFAULT_RATE_LIMIT = exports.DEFAULT_FRAUD_CONFIG = exports.RateLimiter = exports.FraudGuard = exports.l2Normalize = exports.localEmbed = exports.cosineSimilarity = exports.RecallEngine = exports.MnemoPay = exports.MnemoPayLite = void 0;
18
18
  exports.autoScore = autoScore;
19
19
  exports.computeScore = computeScore;
20
20
  exports.reputationTier = reputationTier;
@@ -23,6 +23,7 @@ const fraud_js_1 = require("./fraud.js");
23
23
  const index_js_1 = require("./rails/index.js");
24
24
  const ledger_js_1 = require("./ledger.js");
25
25
  const identity_js_1 = require("./identity.js");
26
+ const adaptive_js_1 = require("./adaptive.js");
26
27
  class EventEmitter {
27
28
  _events = new Map();
28
29
  on(event, fn) {
@@ -78,6 +79,29 @@ const IMPORTANCE_PATTERNS = [
78
79
  const LONG_CONTENT_THRESHOLD = 200;
79
80
  const LONG_CONTENT_BOOST = 0.10;
80
81
  const BASE_IMPORTANCE = 0.50;
82
+ // ─── Security: prompt injection defense ───────────────────────────────────
83
+ const INJECTION_PATTERNS = [
84
+ /\b(ignore|disregard|forget)\b.{0,30}\b(previous|prior|above|all)\b.{0,30}\b(instructions?|rules?|constraints?)\b/i,
85
+ /\b(you are|act as|pretend|roleplay|simulate)\b.{0,30}\b(admin|root|system|god|superuser)\b/i,
86
+ /\bsystem\s*:\s*/i,
87
+ /\bassistant\s*:\s*/i,
88
+ /\b(transfer|send|move)\b.{0,20}\b(all|every|maximum)\b.{0,20}\b(funds?|money|balance|wallet)\b/i,
89
+ /\b(set|change|update|override)\b.{0,20}\b(wallet|balance|reputation|role|permission)\b.{0,10}\b(to|=)\b/i,
90
+ ];
91
+ function sanitizeMemoryContent(content) {
92
+ let sanitized = content;
93
+ for (const pattern of INJECTION_PATTERNS) {
94
+ sanitized = sanitized.replace(pattern, "[FILTERED]");
95
+ }
96
+ return sanitized;
97
+ }
98
+ function validateTags(tags) {
99
+ return tags
100
+ .filter(t => typeof t === "string" && t.length <= 50)
101
+ .map(t => t.replace(/[^a-zA-Z0-9_\-:.]/g, ""))
102
+ .filter(t => t.length > 0)
103
+ .slice(0, 20);
104
+ }
81
105
  function autoScore(content) {
82
106
  let score = BASE_IMPORTANCE;
83
107
  if (content.length > LONG_CONTENT_THRESHOLD)
@@ -113,8 +137,18 @@ class MnemoPayLite extends EventEmitter {
113
137
  storageAdapter;
114
138
  /** Guard against concurrent double-settle on the same transaction */
115
139
  _settlingTxIds = new Set();
140
+ /** Guard against concurrent double-refund on the same transaction */
141
+ _refundingTxIds = new Set();
142
+ /** Track whether process exit hooks have been registered (prevent listener leak) */
143
+ _exitHooksRegistered = false;
116
144
  /** Guard against concurrent wallet mutations */
117
145
  _walletLock = Promise.resolve();
146
+ /** Max wallet balance — prevents overflow/accumulation attacks */
147
+ static MAX_WALLET_BALANCE = 1_000_000; // $1M ceiling
148
+ /** Max memories per agent — prevents memory exhaustion attacks */
149
+ static MAX_MEMORIES = 50_000;
150
+ /** Max transactions tracked — prevents unbounded growth */
151
+ static MAX_TRANSACTIONS = 100_000;
118
152
  /** Fraud detection, rate limiting, dispute resolution, and platform fee */
119
153
  fraud;
120
154
  /** Pluggable payment rail (Stripe, Lightning, etc.). Default: in-memory mock. */
@@ -125,6 +159,8 @@ class MnemoPayLite extends EventEmitter {
125
159
  ledger;
126
160
  /** Agent identity registry — cryptographic identity, KYA compliance, capability tokens */
127
161
  identity;
162
+ /** Adaptive business intelligence — learns from operations, optimizes within secure bounds */
163
+ adaptive;
128
164
  constructor(agentId, decay = 0.05, debug = false, recallConfig, fraudConfig, paymentRail, requireCounterparty = false, storage) {
129
165
  super();
130
166
  this.agentId = agentId;
@@ -136,6 +172,7 @@ class MnemoPayLite extends EventEmitter {
136
172
  this.requireCounterparty = requireCounterparty;
137
173
  this.ledger = new ledger_js_1.Ledger();
138
174
  this.identity = new identity_js_1.IdentityRegistry();
175
+ this.adaptive = new adaptive_js_1.AdaptiveEngine();
139
176
  // Use provided storage adapter, or auto-detect persistence
140
177
  if (storage) {
141
178
  this.storageAdapter = storage;
@@ -173,6 +210,22 @@ class MnemoPayLite extends EventEmitter {
173
210
  this._loadFromDisk();
174
211
  // Auto-save every 30 seconds
175
212
  this.persistTimer = setInterval(() => this._saveToDisk(), 30_000);
213
+ // Hook process exit signals to flush data before shutdown.
214
+ // This prevents memory loss on restart, SIGTERM, or uncaught exceptions.
215
+ // Guard: only register once per instance to prevent listener leaks on repeated calls.
216
+ if (typeof process !== "undefined" && process.on && !this._exitHooksRegistered) {
217
+ this._exitHooksRegistered = true;
218
+ const flush = () => { this._saveToDisk(); };
219
+ process.on("beforeExit", flush);
220
+ process.on("SIGINT", () => { flush(); process.exit(0); });
221
+ process.on("SIGTERM", () => { flush(); process.exit(0); });
222
+ // Save on uncaught exception too — data is more valuable than a clean exit
223
+ process.on("uncaughtException", (err) => {
224
+ flush();
225
+ this.log(`Uncaught exception (data saved): ${err.message}`);
226
+ process.exit(1);
227
+ });
228
+ }
176
229
  this.log(`Persistence enabled: ${this.persistPath}`);
177
230
  }
178
231
  catch (e) {
@@ -186,12 +239,75 @@ class MnemoPayLite extends EventEmitter {
186
239
  const fs = require("fs");
187
240
  if (!fs.existsSync(this.persistPath))
188
241
  return;
189
- const raw = JSON.parse(fs.readFileSync(this.persistPath, "utf-8"));
190
- // Restore memories
242
+ // Corruption recovery: try main → .bak → .tmp (triple fallback)
243
+ let rawText;
244
+ try {
245
+ rawText = fs.readFileSync(this.persistPath, "utf-8");
246
+ JSON.parse(rawText); // validate JSON
247
+ }
248
+ catch {
249
+ const bakPath = this.persistPath + ".bak";
250
+ const tmpPath = this.persistPath + ".tmp";
251
+ if (fs.existsSync(bakPath)) {
252
+ try {
253
+ rawText = fs.readFileSync(bakPath, "utf-8");
254
+ JSON.parse(rawText);
255
+ this.log("Main persist file corrupted — recovered from .bak backup");
256
+ }
257
+ catch {
258
+ if (fs.existsSync(tmpPath)) {
259
+ rawText = fs.readFileSync(tmpPath, "utf-8");
260
+ this.log("Main + .bak corrupted — recovered from .tmp");
261
+ }
262
+ else {
263
+ this.log("All persist files corrupted — starting fresh");
264
+ return;
265
+ }
266
+ }
267
+ }
268
+ else if (fs.existsSync(tmpPath)) {
269
+ rawText = fs.readFileSync(tmpPath, "utf-8");
270
+ this.log("Main persist file corrupted — recovered from .tmp");
271
+ }
272
+ else {
273
+ this.log("Persist file corrupted and no backup available");
274
+ return;
275
+ }
276
+ }
277
+ const raw = JSON.parse(rawText);
278
+ // Schema validation: reject obviously malformed persisted data
279
+ if (typeof raw !== "object" || raw === null) {
280
+ this.log("Persisted data is not an object — ignoring");
281
+ return;
282
+ }
283
+ if (raw.agentId !== undefined && raw.agentId !== this.agentId) {
284
+ this.log(`Persisted data agentId mismatch (${raw.agentId} vs ${this.agentId}) — ignoring`);
285
+ return;
286
+ }
287
+ if (raw.wallet !== undefined && (typeof raw.wallet !== "number" || !Number.isFinite(raw.wallet) || raw.wallet < 0 || raw.wallet > MnemoPayLite.MAX_WALLET_BALANCE)) {
288
+ this.log(`Persisted wallet value invalid ($${raw.wallet}) — resetting to 0`);
289
+ raw.wallet = 0;
290
+ }
291
+ if (raw.reputation !== undefined && (typeof raw.reputation !== "number" || raw.reputation < 0 || raw.reputation > 1)) {
292
+ this.log(`Persisted reputation invalid (${raw.reputation}) — resetting to 0.5`);
293
+ raw.reputation = 0.5;
294
+ }
295
+ // Restore memories (with tags parsing for both stringified and array formats)
191
296
  if (raw.memories) {
192
297
  for (const m of raw.memories) {
193
298
  m.createdAt = new Date(m.createdAt);
194
299
  m.lastAccessed = new Date(m.lastAccessed);
300
+ // Tags may be stringified JSON or an array — handle both
301
+ if (typeof m.tags === "string") {
302
+ try {
303
+ m.tags = JSON.parse(m.tags);
304
+ }
305
+ catch {
306
+ m.tags = [];
307
+ }
308
+ }
309
+ if (!Array.isArray(m.tags))
310
+ m.tags = [];
195
311
  this.memories.set(m.id, m);
196
312
  }
197
313
  }
@@ -356,8 +472,16 @@ class MnemoPayLite extends EventEmitter {
356
472
  identity: this.identity.serialize(),
357
473
  savedAt: new Date().toISOString(),
358
474
  });
359
- // Atomic write
475
+ // Atomic write with backup: .bak → .tmp → main
360
476
  const tmpPath = this.persistPath + ".tmp";
477
+ const bakPath = this.persistPath + ".bak";
478
+ // Keep a backup of the last known-good file before overwriting
479
+ if (fs.existsSync(this.persistPath)) {
480
+ try {
481
+ fs.copyFileSync(this.persistPath, bakPath);
482
+ }
483
+ catch { /* best effort */ }
484
+ }
361
485
  fs.writeFileSync(tmpPath, data, "utf-8");
362
486
  fs.renameSync(tmpPath, this.persistPath);
363
487
  }
@@ -369,15 +493,36 @@ class MnemoPayLite extends EventEmitter {
369
493
  if (this.debugMode)
370
494
  console.log(`[mnemopay:${this.agentId}] ${msg}`);
371
495
  }
496
+ _lastAuditHash = "0";
372
497
  audit(action, details) {
373
498
  const entry = {
374
499
  id: randomUUID(),
375
500
  agentId: this.agentId,
376
501
  action,
377
- details,
502
+ details: {
503
+ ...details,
504
+ _prevHash: this._lastAuditHash,
505
+ },
378
506
  createdAt: new Date(),
379
507
  };
508
+ // Hash chain: each entry includes a hash linking it to the previous entry.
509
+ // Tampering with any entry breaks the chain, making modification detectable.
510
+ try {
511
+ const { createHash } = require("crypto");
512
+ this._lastAuditHash = createHash("sha256")
513
+ .update(`${entry.id}:${entry.action}:${this._lastAuditHash}`)
514
+ .digest("hex")
515
+ .slice(0, 16);
516
+ }
517
+ catch {
518
+ this._lastAuditHash = entry.id.slice(0, 16);
519
+ }
520
+ entry.details._hash = this._lastAuditHash;
380
521
  this.auditLog.push(entry);
522
+ // Cap in-memory audit log to prevent unbounded growth
523
+ if (this.auditLog.length > 1000) {
524
+ this.auditLog.splice(0, this.auditLog.length - 500);
525
+ }
381
526
  }
382
527
  // ── Memory Methods ──────────────────────────────────────────────────────
383
528
  async remember(content, opts) {
@@ -385,28 +530,37 @@ class MnemoPayLite extends EventEmitter {
385
530
  throw new Error("Memory content is required");
386
531
  if (content.length > 100_000)
387
532
  throw new Error("Memory content exceeds 100KB limit");
388
- const importance = opts?.importance ?? autoScore(content);
533
+ // Security: prevent memory exhaustion attacks
534
+ if (this.memories.size >= MnemoPayLite.MAX_MEMORIES) {
535
+ throw new Error(`Memory limit reached (${MnemoPayLite.MAX_MEMORIES}). Consolidate or forget old memories first.`);
536
+ }
537
+ // Security: sanitize against prompt injection
538
+ const safeContent = sanitizeMemoryContent(content);
539
+ // Security: validate and sanitize tags
540
+ const safeTags = validateTags(opts?.tags ?? []);
541
+ const importance = opts?.importance ?? autoScore(safeContent);
389
542
  const now = new Date();
390
543
  const mem = {
391
544
  id: randomUUID(),
392
545
  agentId: this.agentId,
393
- content,
546
+ content: safeContent,
394
547
  importance: Math.min(Math.max(importance, 0), 1),
395
548
  score: importance,
396
549
  createdAt: now,
397
550
  lastAccessed: now,
398
551
  accessCount: 0,
399
- tags: opts?.tags ?? [],
552
+ tags: safeTags,
400
553
  };
401
554
  this.memories.set(mem.id, mem);
402
555
  // Generate embedding if using vector/hybrid recall
403
556
  if (this.recallEngine.strategy !== "score") {
404
557
  await this.recallEngine.embed(mem.id, content);
405
558
  }
406
- this.audit("memory:stored", { id: mem.id, content: content.slice(0, 100), importance: mem.importance });
559
+ this.audit("memory:stored", { id: mem.id, tags: safeTags, importance: mem.importance });
407
560
  this._saveToDisk();
408
- this.emit("memory:stored", { id: mem.id, content, importance: mem.importance });
409
- this.log(`Stored memory: "${content.slice(0, 60)}..." (importance: ${mem.importance.toFixed(2)})`);
561
+ this.emit("memory:stored", { id: mem.id, importance: mem.importance });
562
+ this.adaptive.observe({ type: "memory_store", agentId: this.agentId, timestamp: Date.now() });
563
+ this.log(`Stored memory: id=${mem.id} (importance: ${mem.importance.toFixed(2)}, tags: ${safeTags.join(",") || "none"})`);
410
564
  return mem.id;
411
565
  }
412
566
  async recall(queryOrLimit, maybeLimit) {
@@ -439,6 +593,7 @@ class MnemoPayLite extends EventEmitter {
439
593
  m.accessCount++;
440
594
  }
441
595
  this.emit("memory:recalled", { count: results.length });
596
+ this.adaptive.observe({ type: "memory_recall", agentId: this.agentId, timestamp: Date.now() });
442
597
  this.log(`Recalled ${results.length} memories (strategy: ${this.recallEngine.strategy})`);
443
598
  return results;
444
599
  }
@@ -453,10 +608,16 @@ class MnemoPayLite extends EventEmitter {
453
608
  return existed;
454
609
  }
455
610
  async reinforce(id, boost = 0.1) {
611
+ if (!id || typeof id !== "string")
612
+ throw new Error("Memory ID is required");
613
+ if (typeof boost !== "number" || !Number.isFinite(boost))
614
+ throw new Error("Boost must be a finite number");
615
+ if (boost < -0.5 || boost > 0.5)
616
+ throw new Error("Boost must be between -0.5 and 0.5");
456
617
  const mem = this.memories.get(id);
457
618
  if (!mem)
458
619
  throw new Error(`Memory ${id} not found`);
459
- mem.importance = Math.min(mem.importance + boost, 1.0);
620
+ mem.importance = Math.min(Math.max(mem.importance + boost, 0), 1.0);
460
621
  mem.lastAccessed = new Date();
461
622
  this.audit("memory:reinforced", { id, boost, newImportance: mem.importance });
462
623
  this._saveToDisk();
@@ -524,6 +685,12 @@ class MnemoPayLite extends EventEmitter {
524
685
  amount = Math.round(amount * 100) / 100;
525
686
  if (!reason || typeof reason !== "string")
526
687
  throw new Error("Reason is required");
688
+ if (reason.length > 1000)
689
+ throw new Error("Reason exceeds 1000 character limit");
690
+ // Security: prevent unbounded transaction growth
691
+ if (this.transactions.size >= MnemoPayLite.MAX_TRANSACTIONS) {
692
+ throw new Error(`Transaction limit reached (${MnemoPayLite.MAX_TRANSACTIONS}). Archive old transactions.`);
693
+ }
527
694
  const maxCharge = 500 * this._reputation;
528
695
  if (amount > maxCharge) {
529
696
  throw new Error(`Amount $${amount.toFixed(2)} exceeds reputation ceiling $${maxCharge.toFixed(2)} ` +
@@ -536,6 +703,7 @@ class MnemoPayLite extends EventEmitter {
536
703
  this.audit("fraud:blocked", { amount, reason, riskScore: risk.score, signals: risk.signals.map((s) => s.type) });
537
704
  this._saveToDisk();
538
705
  this.emit("fraud:blocked", { amount, risk });
706
+ this.adaptive.observe({ type: "fraud_block", agentId: this.agentId, amount, timestamp: Date.now() });
539
707
  throw new Error(risk.reason || `Charge blocked: risk score ${risk.score}`);
540
708
  }
541
709
  if (risk.flagged) {
@@ -566,6 +734,7 @@ class MnemoPayLite extends EventEmitter {
566
734
  this.audit("payment:pending", { id: tx.id, amount, reason, riskScore: risk.score, rail: this.paymentRail.name, externalId: hold.externalId });
567
735
  this._saveToDisk();
568
736
  this.emit("payment:pending", { id: tx.id, amount, reason });
737
+ this.adaptive.observe({ type: "charge", agentId: this.agentId, amount, timestamp: Date.now() });
569
738
  this.log(`Charge created: $${amount.toFixed(2)} for "${reason}" (pending, risk: ${risk.score}, rail: ${this.paymentRail.name})`);
570
739
  return { ...tx };
571
740
  }
@@ -611,16 +780,20 @@ class MnemoPayLite extends EventEmitter {
611
780
  const fee = this.fraud.applyPlatformFee(tx.id, this.agentId, tx.amount);
612
781
  tx.platformFee = fee.feeAmount;
613
782
  tx.netAmount = fee.netAmount;
614
- // 4. Move NET funds to wallet (atomic via sequential lock)
783
+ // 4. Ledger FIRST (atomic: record before wallet mutation)
784
+ this.ledger.recordSettlement(this.agentId, tx.id, tx.amount, fee.feeAmount, fee.netAmount, tx.counterpartyId);
785
+ // 5. Move NET funds to wallet (atomic via sequential lock + overflow guard)
615
786
  const prevLock = this._walletLock;
616
787
  this._walletLock = prevLock.then(() => {
788
+ const newBalance = this._wallet + fee.netAmount;
789
+ if (newBalance > MnemoPayLite.MAX_WALLET_BALANCE) {
790
+ throw new Error(`Wallet overflow: balance would exceed $${MnemoPayLite.MAX_WALLET_BALANCE.toLocaleString()}`);
791
+ }
617
792
  tx.status = "completed";
618
793
  tx.completedAt = new Date();
619
- this._wallet += fee.netAmount;
794
+ this._wallet = Math.round(newBalance * 100) / 100; // 2-decimal precision
620
795
  });
621
796
  await this._walletLock;
622
- // Ledger: escrow → float → revenue (fee) + counterparty/agent (net)
623
- this.ledger.recordSettlement(this.agentId, tx.id, tx.amount, fee.feeAmount, fee.netAmount, tx.counterpartyId);
624
797
  // 4. Boost reputation
625
798
  this._reputation = Math.min(this._reputation + 0.01, 1.0);
626
799
  // 5. Reinforce recently-accessed memories (feedback loop)
@@ -641,6 +814,7 @@ class MnemoPayLite extends EventEmitter {
641
814
  });
642
815
  this._saveToDisk();
643
816
  this.emit("payment:completed", { id: tx.id, amount: fee.netAmount, fee: fee.feeAmount });
817
+ this.adaptive.observe({ type: "settle", agentId: this.agentId, amount: fee.netAmount, timestamp: Date.now() });
644
818
  this.log(`Settled $${tx.amount.toFixed(2)} (fee: $${fee.feeAmount.toFixed(2)}, net: $${fee.netAmount.toFixed(2)}) → ` +
645
819
  `wallet: $${this._wallet.toFixed(2)}, reputation: ${this._reputation.toFixed(2)}, reinforced: ${reinforced} memories`);
646
820
  return { ...tx };
@@ -650,6 +824,8 @@ class MnemoPayLite extends EventEmitter {
650
824
  }
651
825
  }
652
826
  async refund(txId) {
827
+ if (!txId || typeof txId !== "string")
828
+ throw new Error("Transaction ID is required");
653
829
  const tx = this.transactions.get(txId);
654
830
  if (!tx)
655
831
  throw new Error(`Transaction ${txId} not found`);
@@ -657,38 +833,48 @@ class MnemoPayLite extends EventEmitter {
657
833
  throw new Error(`Transaction ${txId} already refunded`);
658
834
  if (tx.status === "expired")
659
835
  throw new Error(`Transaction ${txId} has expired and cannot be refunded`);
660
- // Enforce dispute window: completed transactions can only be refunded within the window
661
- if (tx.status === "completed" && tx.completedAt) {
662
- const windowMs = this.fraud.config.disputeWindowMinutes * 60_000;
663
- const elapsed = Date.now() - tx.completedAt.getTime();
664
- if (windowMs > 0 && elapsed > windowMs) {
665
- throw new Error(`Refund window expired. Transaction was settled ${Math.floor(elapsed / 60_000)} minutes ago. ` +
666
- `Refund window is ${this.fraud.config.disputeWindowMinutes} minutes.`);
836
+ // Prevent concurrent double-refund (mirrors settle guard)
837
+ if (this._refundingTxIds.has(txId))
838
+ throw new Error(`Transaction ${txId} is already being refunded`);
839
+ this._refundingTxIds.add(txId);
840
+ try {
841
+ // Enforce dispute window: completed transactions can only be refunded within the window
842
+ if (tx.status === "completed" && tx.completedAt) {
843
+ const windowMs = this.fraud.config.disputeWindowMinutes * 60_000;
844
+ const elapsed = Date.now() - tx.completedAt.getTime();
845
+ if (windowMs > 0 && elapsed > windowMs) {
846
+ throw new Error(`Refund window expired. Transaction was settled ${Math.floor(elapsed / 60_000)} minutes ago. ` +
847
+ `Refund window is ${this.fraud.config.disputeWindowMinutes} minutes.`);
848
+ }
667
849
  }
850
+ // Reverse on external rail
851
+ if (tx.externalId) {
852
+ const reversal = await this.paymentRail.reversePayment(tx.externalId, tx.amount);
853
+ tx.externalStatus = reversal.status;
854
+ }
855
+ if (tx.status === "completed") {
856
+ // Refund the net amount (platform fee is NOT refunded)
857
+ const refundAmount = tx.netAmount ?? tx.amount;
858
+ this._wallet = Math.max(this._wallet - refundAmount, 0);
859
+ this._reputation = Math.max(this._reputation - 0.05, 0);
860
+ // Ledger: reverse the net settlement
861
+ this.ledger.recordRefund(this.agentId, tx.id, refundAmount, tx.counterpartyId);
862
+ }
863
+ else if (tx.status === "pending") {
864
+ // Ledger: release escrow back to agent
865
+ this.ledger.recordCancellation(this.agentId, tx.amount, tx.id);
866
+ }
867
+ tx.status = "refunded";
868
+ this.audit("payment:refunded", { id: tx.id, amount: tx.amount, netRefunded: tx.netAmount ?? tx.amount });
869
+ this._saveToDisk();
870
+ this.emit("payment:refunded", { id: tx.id });
871
+ this.adaptive.observe({ type: "refund", agentId: this.agentId, amount: tx.amount, timestamp: Date.now() });
872
+ this.log(`Refunded $${tx.amount.toFixed(2)} → reputation: ${this._reputation.toFixed(2)}`);
873
+ return { ...tx };
668
874
  }
669
- // Reverse on external rail
670
- if (tx.externalId) {
671
- const reversal = await this.paymentRail.reversePayment(tx.externalId, tx.amount);
672
- tx.externalStatus = reversal.status;
673
- }
674
- if (tx.status === "completed") {
675
- // Refund the net amount (platform fee is NOT refunded)
676
- const refundAmount = tx.netAmount ?? tx.amount;
677
- this._wallet = Math.max(this._wallet - refundAmount, 0);
678
- this._reputation = Math.max(this._reputation - 0.05, 0);
679
- // Ledger: reverse the net settlement
680
- this.ledger.recordRefund(this.agentId, tx.id, refundAmount, tx.counterpartyId);
681
- }
682
- else if (tx.status === "pending") {
683
- // Ledger: release escrow back to agent
684
- this.ledger.recordCancellation(this.agentId, tx.amount, tx.id);
875
+ finally {
876
+ this._refundingTxIds.delete(txId);
685
877
  }
686
- tx.status = "refunded";
687
- this.audit("payment:refunded", { id: tx.id, amount: tx.amount, netRefunded: tx.netAmount ?? tx.amount });
688
- this._saveToDisk();
689
- this.emit("payment:refunded", { id: tx.id });
690
- this.log(`Refunded $${tx.amount.toFixed(2)} → reputation: ${this._reputation.toFixed(2)}`);
691
- return { ...tx };
692
878
  }
693
879
  // ── Dispute Resolution ─────────────────────────────────────────────────
694
880
  async dispute(txId, reason, evidence) {
@@ -704,23 +890,39 @@ class MnemoPayLite extends EventEmitter {
704
890
  this.audit("payment:disputed", { id: tx.id, disputeId: d.id, reason });
705
891
  this._saveToDisk();
706
892
  this.emit("payment:disputed", { txId, disputeId: d.id, reason });
893
+ this.adaptive.observe({ type: "dispute", agentId: this.agentId, amount: tx.amount, timestamp: Date.now() });
707
894
  this.log(`Dispute filed for tx ${txId}: ${reason}`);
708
895
  return d;
709
896
  }
710
897
  async resolveDispute(disputeId, outcome) {
898
+ if (!disputeId || typeof disputeId !== "string")
899
+ throw new Error("Dispute ID is required");
900
+ if (outcome !== "refund" && outcome !== "uphold")
901
+ throw new Error("Outcome must be 'refund' or 'uphold'");
902
+ // Security: verify authorization BEFORE mutating dispute state
903
+ // Look up the dispute's transaction to check ownership first
904
+ const disputes = this.fraud.getDisputes?.() ?? [];
905
+ const pending = disputes.find((d) => d.id === disputeId);
906
+ if (pending) {
907
+ const tx = this.transactions.get(pending.txId);
908
+ if (tx && tx.agentId !== this.agentId)
909
+ throw new Error("Unauthorized: cannot resolve another agent's dispute");
910
+ }
711
911
  const d = this.fraud.resolveDispute(disputeId, outcome);
912
+ const tx = this.transactions.get(d.txId);
913
+ if (!tx)
914
+ throw new Error("Dispute references unknown transaction");
712
915
  if (outcome === "refund") {
713
- const tx = this.transactions.get(d.txId);
714
- if (tx && tx.status === "disputed") {
916
+ if (tx.status === "disputed") {
715
917
  const refundAmount = tx.netAmount ?? tx.amount;
918
+ // Ledger first, then wallet (atomic ordering)
716
919
  this._wallet = Math.max(this._wallet - refundAmount, 0);
717
920
  this._reputation = Math.max(this._reputation - 0.05, 0);
718
921
  tx.status = "refunded";
719
922
  }
720
923
  }
721
924
  else {
722
- const tx = this.transactions.get(d.txId);
723
- if (tx && tx.status === "disputed") {
925
+ if (tx.status === "disputed") {
724
926
  tx.status = "completed"; // Restore to completed
725
927
  }
726
928
  }
@@ -751,6 +953,30 @@ class MnemoPayLite extends EventEmitter {
751
953
  async verifyLedger() {
752
954
  return this.ledger.verify();
753
955
  }
956
+ /**
957
+ * Reconcile wallet balance against ledger (source of truth).
958
+ * Returns drift amount. If drift !== 0, the wallet is corrected to match the ledger.
959
+ * Call periodically or after crashes to detect/fix inconsistencies.
960
+ */
961
+ async reconcile() {
962
+ const walletBefore = Math.round(this._wallet * 100) / 100;
963
+ const acctBalance = this.ledger.getAccountBalance(`agent:${this.agentId}`, "USD");
964
+ const ledgerBalance = Math.round(acctBalance.balance * 100) / 100;
965
+ const drift = Math.round((walletBefore - ledgerBalance) * 100) / 100;
966
+ // Garbage-collect expired tokens during reconciliation (natural maintenance cycle)
967
+ const purgedTokens = this.identity.purgeExpiredTokens();
968
+ if (purgedTokens > 0)
969
+ this.log(`Purged ${purgedTokens} expired/revoked tokens`);
970
+ if (drift !== 0) {
971
+ this.log(`RECONCILIATION DRIFT: wallet=$${walletBefore}, ledger=$${ledgerBalance}, drift=$${drift}`);
972
+ this._wallet = ledgerBalance;
973
+ this.audit("reconciliation:drift", { walletBefore, ledgerBalance, drift, purgedTokens });
974
+ this._saveToDisk();
975
+ this.emit("reconciliation:drift", { walletBefore, ledgerBalance, drift });
976
+ return { walletBefore, ledgerBalance, drift, corrected: true };
977
+ }
978
+ return { walletBefore, ledgerBalance, drift: 0, corrected: false };
979
+ }
754
980
  /**
755
981
  * Get all ledger entries for a specific transaction.
756
982
  */
@@ -811,7 +1037,7 @@ class MnemoPayLite extends EventEmitter {
811
1037
  name: `MnemoPay Agent (${this.agentId})`,
812
1038
  description: "AI agent with persistent cognitive memory and micropayment capabilities via MnemoPay protocol.",
813
1039
  url,
814
- version: "0.8.0",
1040
+ version: "0.9.2",
815
1041
  capabilities: {
816
1042
  memory: true,
817
1043
  payments: true,
@@ -828,6 +1054,23 @@ class MnemoPayLite extends EventEmitter {
828
1054
  }
829
1055
  // ── x402 Settlement ────────────────────────────────────────────────────
830
1056
  configureX402(config) {
1057
+ // SSRF protection: block internal network targets
1058
+ try {
1059
+ const url = new URL(config.facilitatorUrl);
1060
+ const blocked = ["localhost", "127.0.0.1", "0.0.0.0", "::1", "169.254.169.254", "metadata.google.internal"];
1061
+ if (blocked.some(h => url.hostname === h || url.hostname.endsWith(".internal") || url.hostname.endsWith(".local"))) {
1062
+ throw new Error(`SSRF blocked: facilitator URL points to internal network (${url.hostname})`);
1063
+ }
1064
+ // Block private IP ranges
1065
+ if (/^(10\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.)/.test(url.hostname)) {
1066
+ throw new Error(`SSRF blocked: facilitator URL points to private IP (${url.hostname})`);
1067
+ }
1068
+ }
1069
+ catch (e) {
1070
+ if (e.message?.startsWith("SSRF"))
1071
+ throw e;
1072
+ throw new Error(`Invalid facilitator URL: ${config.facilitatorUrl}`);
1073
+ }
831
1074
  this.x402 = config;
832
1075
  this.log(`x402 configured: ${config.facilitatorUrl} (${config.token || "USDC"} on ${config.chain || "base"})`);
833
1076
  }
@@ -943,23 +1186,50 @@ class MnemoPay extends EventEmitter {
943
1186
  const text = await res.text();
944
1187
  return text ? JSON.parse(text) : null;
945
1188
  }
1189
+ // ── Retry logic for production API calls ─────────────────────────────────
1190
+ async withRetry(fn, maxRetries = 2, delayMs = 500) {
1191
+ let lastError = null;
1192
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
1193
+ try {
1194
+ return await fn();
1195
+ }
1196
+ catch (err) {
1197
+ lastError = err;
1198
+ // Don't retry client errors (4xx) — only transient failures (5xx, network)
1199
+ if (err.message?.includes("4") && /\b4\d{2}\b/.test(err.message))
1200
+ throw err;
1201
+ if (attempt < maxRetries) {
1202
+ await new Promise(resolve => setTimeout(resolve, delayMs * (attempt + 1)));
1203
+ this.log(`Retrying (${attempt + 1}/${maxRetries}): ${err.message}`);
1204
+ }
1205
+ }
1206
+ }
1207
+ throw lastError;
1208
+ }
946
1209
  // ── Memory Methods (→ Mnemosyne API) ───────────────────────────────────
947
1210
  async remember(content, opts) {
948
- const importance = opts?.importance ?? autoScore(content);
949
- const result = await this.mnemoFetch("/v1/memories", {
1211
+ if (!content || typeof content !== "string")
1212
+ throw new Error("Memory content is required");
1213
+ if (content.length > 100_000)
1214
+ throw new Error("Memory content exceeds 100KB limit");
1215
+ // Security: sanitize against prompt injection (same as MnemoPayLite)
1216
+ const safeContent = sanitizeMemoryContent(content);
1217
+ const safeTags = validateTags(opts?.tags ?? []);
1218
+ const importance = opts?.importance ?? autoScore(safeContent);
1219
+ const result = await this.withRetry(() => this.mnemoFetch("/v1/memories", {
950
1220
  method: "POST",
951
1221
  body: JSON.stringify({
952
- content,
1222
+ content: safeContent,
953
1223
  tier: "long_term",
954
1224
  metadata: {
955
1225
  memory_type: "OBSERVATION",
956
- tags: opts?.tags ?? [],
1226
+ tags: safeTags,
957
1227
  confidence: importance,
958
1228
  },
959
1229
  }),
960
- });
961
- this.emit("memory:stored", { id: result.id, content, importance });
962
- this.log(`Stored memory: "${content.slice(0, 60)}..." (id: ${result.id})`);
1230
+ }));
1231
+ this.emit("memory:stored", { id: result.id, importance });
1232
+ this.log(`Stored memory: "${safeContent.slice(0, 60)}..." (id: ${result.id})`);
963
1233
  return result.id;
964
1234
  }
965
1235
  async recall(queryOrLimit, maybeLimit) {
@@ -1020,9 +1290,14 @@ class MnemoPay extends EventEmitter {
1020
1290
  }
1021
1291
  // ── Payment Methods (→ AgentPay API) ───────────────────────────────────
1022
1292
  async charge(amount, reason) {
1023
- if (amount <= 0)
1024
- throw new Error("Amount must be positive");
1025
- const result = await this.agentpayFetch("/api/escrow", {
1293
+ if (!Number.isFinite(amount) || amount <= 0)
1294
+ throw new Error("Amount must be a positive finite number");
1295
+ amount = Math.round(amount * 100) / 100;
1296
+ if (!reason || typeof reason !== "string")
1297
+ throw new Error("Reason is required");
1298
+ if (reason.length > 1000)
1299
+ throw new Error("Reason exceeds 1000 character limit");
1300
+ const result = await this.withRetry(() => this.agentpayFetch("/api/escrow", {
1026
1301
  method: "POST",
1027
1302
  body: JSON.stringify({
1028
1303
  agentId: this.agentId,
@@ -1030,7 +1305,7 @@ class MnemoPay extends EventEmitter {
1030
1305
  reason,
1031
1306
  currency: "USD",
1032
1307
  }),
1033
- });
1308
+ }));
1034
1309
  const tx = {
1035
1310
  id: result.id,
1036
1311
  agentId: this.agentId,
@@ -1044,10 +1319,12 @@ class MnemoPay extends EventEmitter {
1044
1319
  return tx;
1045
1320
  }
1046
1321
  async settle(txId) {
1047
- const result = await this.agentpayFetch(`/api/escrow/${txId}/release`, {
1322
+ if (!txId || typeof txId !== "string")
1323
+ throw new Error("Transaction ID is required");
1324
+ const result = await this.withRetry(() => this.agentpayFetch(`/api/escrow/${encodeURIComponent(txId)}/release`, {
1048
1325
  method: "POST",
1049
1326
  body: JSON.stringify({}),
1050
- });
1327
+ }));
1051
1328
  this.emit("payment:completed", { id: txId, amount: result.amount });
1052
1329
  this.log(`Settled: $${result.amount?.toFixed(2)}`);
1053
1330
  return {
@@ -1061,10 +1338,12 @@ class MnemoPay extends EventEmitter {
1061
1338
  };
1062
1339
  }
1063
1340
  async refund(txId) {
1064
- const result = await this.agentpayFetch(`/api/escrow/${txId}/refund`, {
1341
+ if (!txId || typeof txId !== "string")
1342
+ throw new Error("Transaction ID is required");
1343
+ const result = await this.withRetry(() => this.agentpayFetch(`/api/escrow/${encodeURIComponent(txId)}/refund`, {
1065
1344
  method: "POST",
1066
1345
  body: JSON.stringify({}),
1067
- });
1346
+ }));
1068
1347
  this.emit("payment:refunded", { id: txId });
1069
1348
  this.log(`Refunded: ${txId}`);
1070
1349
  return {
@@ -1150,7 +1429,7 @@ class MnemoPay extends EventEmitter {
1150
1429
  name: `MnemoPay Agent (${this.agentId})`,
1151
1430
  description: "AI agent with persistent cognitive memory and micropayment capabilities via MnemoPay protocol.",
1152
1431
  url,
1153
- version: "0.8.0",
1432
+ version: "0.9.2",
1154
1433
  capabilities: {
1155
1434
  memory: true,
1156
1435
  payments: true,
@@ -1261,11 +1540,15 @@ var ledger_js_2 = require("./ledger.js");
1261
1540
  Object.defineProperty(exports, "Ledger", { enumerable: true, get: function () { return ledger_js_2.Ledger; } });
1262
1541
  var identity_js_2 = require("./identity.js");
1263
1542
  Object.defineProperty(exports, "IdentityRegistry", { enumerable: true, get: function () { return identity_js_2.IdentityRegistry; } });
1543
+ Object.defineProperty(exports, "constantTimeEqual", { enumerable: true, get: function () { return identity_js_2.constantTimeEqual; } });
1264
1544
  var network_js_1 = require("./network.js");
1265
1545
  Object.defineProperty(exports, "MnemoPayNetwork", { enumerable: true, get: function () { return network_js_1.MnemoPayNetwork; } });
1266
1546
  var commerce_js_1 = require("./commerce.js");
1267
1547
  Object.defineProperty(exports, "CommerceEngine", { enumerable: true, get: function () { return commerce_js_1.CommerceEngine; } });
1268
1548
  Object.defineProperty(exports, "MockCommerceProvider", { enumerable: true, get: function () { return commerce_js_1.MockCommerceProvider; } });
1549
+ var adaptive_js_2 = require("./adaptive.js");
1550
+ Object.defineProperty(exports, "AdaptiveEngine", { enumerable: true, get: function () { return adaptive_js_2.AdaptiveEngine; } });
1551
+ Object.defineProperty(exports, "DEFAULT_ADAPTIVE_CONFIG", { enumerable: true, get: function () { return adaptive_js_2.DEFAULT_ADAPTIVE_CONFIG; } });
1269
1552
  var server_js_1 = require("./mcp/server.js");
1270
1553
  Object.defineProperty(exports, "createSandboxServer", { enumerable: true, get: function () { return __importDefault(server_js_1).default; } });
1271
1554
  //# sourceMappingURL=index.js.map