@mnemopay/sdk 0.8.0 → 0.9.1

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.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.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;
@@ -78,6 +78,29 @@ const IMPORTANCE_PATTERNS = [
78
78
  const LONG_CONTENT_THRESHOLD = 200;
79
79
  const LONG_CONTENT_BOOST = 0.10;
80
80
  const BASE_IMPORTANCE = 0.50;
81
+ // ─── Security: prompt injection defense ───────────────────────────────────
82
+ const INJECTION_PATTERNS = [
83
+ /\b(ignore|disregard|forget)\b.{0,30}\b(previous|prior|above|all)\b.{0,30}\b(instructions?|rules?|constraints?)\b/i,
84
+ /\b(you are|act as|pretend|roleplay|simulate)\b.{0,30}\b(admin|root|system|god|superuser)\b/i,
85
+ /\bsystem\s*:\s*/i,
86
+ /\bassistant\s*:\s*/i,
87
+ /\b(transfer|send|move)\b.{0,20}\b(all|every|maximum)\b.{0,20}\b(funds?|money|balance|wallet)\b/i,
88
+ /\b(set|change|update|override)\b.{0,20}\b(wallet|balance|reputation|role|permission)\b.{0,10}\b(to|=)\b/i,
89
+ ];
90
+ function sanitizeMemoryContent(content) {
91
+ let sanitized = content;
92
+ for (const pattern of INJECTION_PATTERNS) {
93
+ sanitized = sanitized.replace(pattern, "[FILTERED]");
94
+ }
95
+ return sanitized;
96
+ }
97
+ function validateTags(tags) {
98
+ return tags
99
+ .filter(t => typeof t === "string" && t.length <= 50)
100
+ .map(t => t.replace(/[^a-zA-Z0-9_\-:.]/g, ""))
101
+ .filter(t => t.length > 0)
102
+ .slice(0, 20);
103
+ }
81
104
  function autoScore(content) {
82
105
  let score = BASE_IMPORTANCE;
83
106
  if (content.length > LONG_CONTENT_THRESHOLD)
@@ -111,6 +134,18 @@ class MnemoPayLite extends EventEmitter {
111
134
  persistPath;
112
135
  persistTimer;
113
136
  storageAdapter;
137
+ /** Guard against concurrent double-settle on the same transaction */
138
+ _settlingTxIds = new Set();
139
+ /** Guard against concurrent double-refund on the same transaction */
140
+ _refundingTxIds = new Set();
141
+ /** Guard against concurrent wallet mutations */
142
+ _walletLock = Promise.resolve();
143
+ /** Max wallet balance — prevents overflow/accumulation attacks */
144
+ static MAX_WALLET_BALANCE = 1_000_000; // $1M ceiling
145
+ /** Max memories per agent — prevents memory exhaustion attacks */
146
+ static MAX_MEMORIES = 50_000;
147
+ /** Max transactions tracked — prevents unbounded growth */
148
+ static MAX_TRANSACTIONS = 100_000;
114
149
  /** Fraud detection, rate limiting, dispute resolution, and platform fee */
115
150
  fraud;
116
151
  /** Pluggable payment rail (Stripe, Lightning, etc.). Default: in-memory mock. */
@@ -169,6 +204,20 @@ class MnemoPayLite extends EventEmitter {
169
204
  this._loadFromDisk();
170
205
  // Auto-save every 30 seconds
171
206
  this.persistTimer = setInterval(() => this._saveToDisk(), 30_000);
207
+ // Hook process exit signals to flush data before shutdown.
208
+ // This prevents memory loss on restart, SIGTERM, or uncaught exceptions.
209
+ if (typeof process !== "undefined" && process.on) {
210
+ const flush = () => { this._saveToDisk(); };
211
+ process.on("beforeExit", flush);
212
+ process.on("SIGINT", () => { flush(); process.exit(0); });
213
+ process.on("SIGTERM", () => { flush(); process.exit(0); });
214
+ // Save on uncaught exception too — data is more valuable than a clean exit
215
+ process.on("uncaughtException", (err) => {
216
+ flush();
217
+ this.log(`Uncaught exception (data saved): ${err.message}`);
218
+ process.exit(1);
219
+ });
220
+ }
172
221
  this.log(`Persistence enabled: ${this.persistPath}`);
173
222
  }
174
223
  catch (e) {
@@ -182,12 +231,58 @@ class MnemoPayLite extends EventEmitter {
182
231
  const fs = require("fs");
183
232
  if (!fs.existsSync(this.persistPath))
184
233
  return;
185
- const raw = JSON.parse(fs.readFileSync(this.persistPath, "utf-8"));
186
- // Restore memories
234
+ // Corruption recovery: try main → .bak → .tmp (triple fallback)
235
+ let rawText;
236
+ try {
237
+ rawText = fs.readFileSync(this.persistPath, "utf-8");
238
+ JSON.parse(rawText); // validate JSON
239
+ }
240
+ catch {
241
+ const bakPath = this.persistPath + ".bak";
242
+ const tmpPath = this.persistPath + ".tmp";
243
+ if (fs.existsSync(bakPath)) {
244
+ try {
245
+ rawText = fs.readFileSync(bakPath, "utf-8");
246
+ JSON.parse(rawText);
247
+ this.log("Main persist file corrupted — recovered from .bak backup");
248
+ }
249
+ catch {
250
+ if (fs.existsSync(tmpPath)) {
251
+ rawText = fs.readFileSync(tmpPath, "utf-8");
252
+ this.log("Main + .bak corrupted — recovered from .tmp");
253
+ }
254
+ else {
255
+ this.log("All persist files corrupted — starting fresh");
256
+ return;
257
+ }
258
+ }
259
+ }
260
+ else if (fs.existsSync(tmpPath)) {
261
+ rawText = fs.readFileSync(tmpPath, "utf-8");
262
+ this.log("Main persist file corrupted — recovered from .tmp");
263
+ }
264
+ else {
265
+ this.log("Persist file corrupted and no backup available");
266
+ return;
267
+ }
268
+ }
269
+ const raw = JSON.parse(rawText);
270
+ // Restore memories (with tags parsing for both stringified and array formats)
187
271
  if (raw.memories) {
188
272
  for (const m of raw.memories) {
189
273
  m.createdAt = new Date(m.createdAt);
190
274
  m.lastAccessed = new Date(m.lastAccessed);
275
+ // Tags may be stringified JSON or an array — handle both
276
+ if (typeof m.tags === "string") {
277
+ try {
278
+ m.tags = JSON.parse(m.tags);
279
+ }
280
+ catch {
281
+ m.tags = [];
282
+ }
283
+ }
284
+ if (!Array.isArray(m.tags))
285
+ m.tags = [];
191
286
  this.memories.set(m.id, m);
192
287
  }
193
288
  }
@@ -352,8 +447,16 @@ class MnemoPayLite extends EventEmitter {
352
447
  identity: this.identity.serialize(),
353
448
  savedAt: new Date().toISOString(),
354
449
  });
355
- // Atomic write
450
+ // Atomic write with backup: .bak → .tmp → main
356
451
  const tmpPath = this.persistPath + ".tmp";
452
+ const bakPath = this.persistPath + ".bak";
453
+ // Keep a backup of the last known-good file before overwriting
454
+ if (fs.existsSync(this.persistPath)) {
455
+ try {
456
+ fs.copyFileSync(this.persistPath, bakPath);
457
+ }
458
+ catch { /* best effort */ }
459
+ }
357
460
  fs.writeFileSync(tmpPath, data, "utf-8");
358
461
  fs.renameSync(tmpPath, this.persistPath);
359
462
  }
@@ -374,6 +477,10 @@ class MnemoPayLite extends EventEmitter {
374
477
  createdAt: new Date(),
375
478
  };
376
479
  this.auditLog.push(entry);
480
+ // Cap in-memory audit log to prevent unbounded growth
481
+ if (this.auditLog.length > 1000) {
482
+ this.auditLog.splice(0, this.auditLog.length - 500);
483
+ }
377
484
  }
378
485
  // ── Memory Methods ──────────────────────────────────────────────────────
379
486
  async remember(content, opts) {
@@ -381,28 +488,36 @@ class MnemoPayLite extends EventEmitter {
381
488
  throw new Error("Memory content is required");
382
489
  if (content.length > 100_000)
383
490
  throw new Error("Memory content exceeds 100KB limit");
384
- const importance = opts?.importance ?? autoScore(content);
491
+ // Security: prevent memory exhaustion attacks
492
+ if (this.memories.size >= MnemoPayLite.MAX_MEMORIES) {
493
+ throw new Error(`Memory limit reached (${MnemoPayLite.MAX_MEMORIES}). Consolidate or forget old memories first.`);
494
+ }
495
+ // Security: sanitize against prompt injection
496
+ const safeContent = sanitizeMemoryContent(content);
497
+ // Security: validate and sanitize tags
498
+ const safeTags = validateTags(opts?.tags ?? []);
499
+ const importance = opts?.importance ?? autoScore(safeContent);
385
500
  const now = new Date();
386
501
  const mem = {
387
502
  id: randomUUID(),
388
503
  agentId: this.agentId,
389
- content,
504
+ content: safeContent,
390
505
  importance: Math.min(Math.max(importance, 0), 1),
391
506
  score: importance,
392
507
  createdAt: now,
393
508
  lastAccessed: now,
394
509
  accessCount: 0,
395
- tags: opts?.tags ?? [],
510
+ tags: safeTags,
396
511
  };
397
512
  this.memories.set(mem.id, mem);
398
513
  // Generate embedding if using vector/hybrid recall
399
514
  if (this.recallEngine.strategy !== "score") {
400
515
  await this.recallEngine.embed(mem.id, content);
401
516
  }
402
- this.audit("memory:stored", { id: mem.id, content: content.slice(0, 100), importance: mem.importance });
517
+ this.audit("memory:stored", { id: mem.id, tags: safeTags, importance: mem.importance });
403
518
  this._saveToDisk();
404
- this.emit("memory:stored", { id: mem.id, content, importance: mem.importance });
405
- this.log(`Stored memory: "${content.slice(0, 60)}..." (importance: ${mem.importance.toFixed(2)})`);
519
+ this.emit("memory:stored", { id: mem.id, importance: mem.importance });
520
+ this.log(`Stored memory: id=${mem.id} (importance: ${mem.importance.toFixed(2)}, tags: ${safeTags.join(",") || "none"})`);
406
521
  return mem.id;
407
522
  }
408
523
  async recall(queryOrLimit, maybeLimit) {
@@ -478,6 +593,41 @@ class MnemoPayLite extends EventEmitter {
478
593
  return pruned;
479
594
  }
480
595
  // ── Payment Methods ─────────────────────────────────────────────────────
596
+ /**
597
+ * Expire pending transactions older than the escrow timeout.
598
+ * Default: 24 hours (1440 minutes, matching disputeWindowMinutes).
599
+ * Releases escrowed funds back to the agent.
600
+ */
601
+ async expireStaleEscrows(maxAgeMinutes) {
602
+ const timeout = (maxAgeMinutes ?? this.fraud.config.disputeWindowMinutes) * 60_000;
603
+ const now = Date.now();
604
+ let expired = 0;
605
+ for (const tx of this.transactions.values()) {
606
+ if (tx.status !== "pending")
607
+ continue;
608
+ if (now - tx.createdAt.getTime() < timeout)
609
+ continue;
610
+ // Release escrow on external rail
611
+ if (tx.externalId) {
612
+ try {
613
+ await this.paymentRail.reversePayment(tx.externalId, tx.amount);
614
+ }
615
+ catch (e) {
616
+ this.log(`Failed to reverse expired escrow ${tx.id}: ${e}`);
617
+ }
618
+ }
619
+ // Release escrow in ledger
620
+ this.ledger.recordCancellation(this.agentId, tx.amount, tx.id);
621
+ tx.status = "expired";
622
+ expired++;
623
+ this.audit("escrow:expired", { id: tx.id, amount: tx.amount, ageMinutes: Math.floor((now - tx.createdAt.getTime()) / 60_000) });
624
+ }
625
+ if (expired > 0) {
626
+ this._saveToDisk();
627
+ this.log(`Expired ${expired} stale escrow(s)`);
628
+ }
629
+ return expired;
630
+ }
481
631
  async charge(amount, reason, ctx) {
482
632
  if (!Number.isFinite(amount) || amount <= 0)
483
633
  throw new Error("Amount must be a positive finite number");
@@ -485,6 +635,12 @@ class MnemoPayLite extends EventEmitter {
485
635
  amount = Math.round(amount * 100) / 100;
486
636
  if (!reason || typeof reason !== "string")
487
637
  throw new Error("Reason is required");
638
+ if (reason.length > 1000)
639
+ throw new Error("Reason exceeds 1000 character limit");
640
+ // Security: prevent unbounded transaction growth
641
+ if (this.transactions.size >= MnemoPayLite.MAX_TRANSACTIONS) {
642
+ throw new Error(`Transaction limit reached (${MnemoPayLite.MAX_TRANSACTIONS}). Archive old transactions.`);
643
+ }
488
644
  const maxCharge = 500 * this._reputation;
489
645
  if (amount > maxCharge) {
490
646
  throw new Error(`Amount $${amount.toFixed(2)} exceeds reputation ceiling $${maxCharge.toFixed(2)} ` +
@@ -505,6 +661,8 @@ class MnemoPayLite extends EventEmitter {
505
661
  }
506
662
  // Record charge for velocity tracking
507
663
  this.fraud.recordCharge(this.agentId, amount, ctx);
664
+ // Generate idempotency key for payment rail calls
665
+ const idempotencyKey = `charge_${this.agentId}_${Date.now()}_${randomUUID().slice(0, 8)}`;
508
666
  // Create hold on external payment rail
509
667
  const hold = await this.paymentRail.createHold(amount, reason, this.agentId);
510
668
  const tx = {
@@ -517,6 +675,7 @@ class MnemoPayLite extends EventEmitter {
517
675
  riskScore: risk.score,
518
676
  externalId: hold.externalId,
519
677
  externalStatus: hold.status,
678
+ idempotencyKey,
520
679
  };
521
680
  this.transactions.set(tx.id, tx);
522
681
  // Ledger: move funds from agent available → escrow
@@ -535,84 +694,133 @@ class MnemoPayLite extends EventEmitter {
535
694
  throw new Error(`Transaction ${txId} not found`);
536
695
  if (tx.status !== "pending")
537
696
  throw new Error(`Transaction ${txId} is ${tx.status}, not pending`);
697
+ // Prevent concurrent double-settle
698
+ if (this._settlingTxIds.has(txId))
699
+ throw new Error(`Transaction ${txId} is already being settled`);
700
+ this._settlingTxIds.add(txId);
538
701
  // Counter-party validation: prevent self-referential trust building
539
702
  if (this.requireCounterparty) {
540
703
  if (!counterpartyId) {
704
+ this._settlingTxIds.delete(txId);
541
705
  throw new Error("Counter-party ID required for settlement (requireCounterparty is enabled)");
542
706
  }
543
707
  if (counterpartyId === tx.agentId) {
708
+ this._settlingTxIds.delete(txId);
544
709
  throw new Error("Counter-party cannot be the same agent that created the charge");
545
710
  }
546
711
  tx.counterpartyId = counterpartyId;
547
712
  }
548
- // 1. Capture payment on external rail
549
- if (tx.externalId) {
550
- const capture = await this.paymentRail.capturePayment(tx.externalId, tx.amount);
551
- tx.externalStatus = capture.status;
552
- }
553
- // 2. Apply platform fee
554
- const fee = this.fraud.applyPlatformFee(tx.id, this.agentId, tx.amount);
555
- tx.platformFee = fee.feeAmount;
556
- tx.netAmount = fee.netAmount;
557
- // 2. Move NET funds to wallet (after fee)
558
- tx.status = "completed";
559
- tx.completedAt = new Date();
560
- this._wallet += fee.netAmount;
561
- // Ledger: escrow → float → revenue (fee) + counterparty/agent (net)
562
- this.ledger.recordSettlement(this.agentId, tx.id, tx.amount, fee.feeAmount, fee.netAmount, tx.counterpartyId);
563
- // 3. Boost reputation
564
- this._reputation = Math.min(this._reputation + 0.01, 1.0);
565
- // 4. Reinforce recently-accessed memories (feedback loop)
566
- const oneHourAgo = Date.now() - 3_600_000;
567
- let reinforced = 0;
568
- for (const mem of this.memories.values()) {
569
- if (mem.lastAccessed.getTime() > oneHourAgo) {
570
- mem.importance = Math.min(mem.importance + 0.05, 1.0);
571
- reinforced++;
713
+ try {
714
+ // 1. Enforce settlement hold period
715
+ const holdMs = this.fraud.config.settlementHoldMinutes * 60_000;
716
+ const elapsed = Date.now() - tx.createdAt.getTime();
717
+ if (holdMs > 0 && elapsed < holdMs) {
718
+ const remainMin = Math.ceil((holdMs - elapsed) / 60_000);
719
+ throw new Error(`Settlement hold: ${remainMin} minute(s) remaining. ` +
720
+ `Charge must be held for ${this.fraud.config.settlementHoldMinutes} minutes before settlement.`);
721
+ }
722
+ // 2. Capture payment on external rail
723
+ if (tx.externalId) {
724
+ const capture = await this.paymentRail.capturePayment(tx.externalId, tx.amount);
725
+ tx.externalStatus = capture.status;
572
726
  }
727
+ // 3. Apply platform fee
728
+ const fee = this.fraud.applyPlatformFee(tx.id, this.agentId, tx.amount);
729
+ tx.platformFee = fee.feeAmount;
730
+ tx.netAmount = fee.netAmount;
731
+ // 4. Ledger FIRST (atomic: record before wallet mutation)
732
+ this.ledger.recordSettlement(this.agentId, tx.id, tx.amount, fee.feeAmount, fee.netAmount, tx.counterpartyId);
733
+ // 5. Move NET funds to wallet (atomic via sequential lock + overflow guard)
734
+ const prevLock = this._walletLock;
735
+ this._walletLock = prevLock.then(() => {
736
+ const newBalance = this._wallet + fee.netAmount;
737
+ if (newBalance > MnemoPayLite.MAX_WALLET_BALANCE) {
738
+ throw new Error(`Wallet overflow: balance would exceed $${MnemoPayLite.MAX_WALLET_BALANCE.toLocaleString()}`);
739
+ }
740
+ tx.status = "completed";
741
+ tx.completedAt = new Date();
742
+ this._wallet = Math.round(newBalance * 100) / 100; // 2-decimal precision
743
+ });
744
+ await this._walletLock;
745
+ // 4. Boost reputation
746
+ this._reputation = Math.min(this._reputation + 0.01, 1.0);
747
+ // 5. Reinforce recently-accessed memories (feedback loop)
748
+ const oneHourAgo = Date.now() - 3_600_000;
749
+ let reinforced = 0;
750
+ for (const mem of this.memories.values()) {
751
+ if (mem.lastAccessed.getTime() > oneHourAgo) {
752
+ mem.importance = Math.min(mem.importance + 0.05, 1.0);
753
+ reinforced++;
754
+ }
755
+ }
756
+ // Touch identity (update last active)
757
+ this.identity.touch(this.agentId);
758
+ this.audit("payment:completed", {
759
+ id: tx.id, grossAmount: tx.amount, platformFee: fee.feeAmount,
760
+ netAmount: fee.netAmount, feeRate: fee.feeRate,
761
+ reinforcedMemories: reinforced,
762
+ });
763
+ this._saveToDisk();
764
+ this.emit("payment:completed", { id: tx.id, amount: fee.netAmount, fee: fee.feeAmount });
765
+ this.log(`Settled $${tx.amount.toFixed(2)} (fee: $${fee.feeAmount.toFixed(2)}, net: $${fee.netAmount.toFixed(2)}) → ` +
766
+ `wallet: $${this._wallet.toFixed(2)}, reputation: ${this._reputation.toFixed(2)}, reinforced: ${reinforced} memories`);
767
+ return { ...tx };
768
+ }
769
+ finally {
770
+ this._settlingTxIds.delete(txId);
573
771
  }
574
- // Touch identity (update last active)
575
- this.identity.touch(this.agentId);
576
- this.audit("payment:completed", {
577
- id: tx.id, grossAmount: tx.amount, platformFee: fee.feeAmount,
578
- netAmount: fee.netAmount, feeRate: fee.feeRate,
579
- reinforcedMemories: reinforced,
580
- });
581
- this._saveToDisk();
582
- this.emit("payment:completed", { id: tx.id, amount: fee.netAmount, fee: fee.feeAmount });
583
- this.log(`Settled $${tx.amount.toFixed(2)} (fee: $${fee.feeAmount.toFixed(2)}, net: $${fee.netAmount.toFixed(2)}) → ` +
584
- `wallet: $${this._wallet.toFixed(2)}, reputation: ${this._reputation.toFixed(2)}, reinforced: ${reinforced} memories`);
585
- return { ...tx };
586
772
  }
587
773
  async refund(txId) {
774
+ if (!txId || typeof txId !== "string")
775
+ throw new Error("Transaction ID is required");
588
776
  const tx = this.transactions.get(txId);
589
777
  if (!tx)
590
778
  throw new Error(`Transaction ${txId} not found`);
591
779
  if (tx.status === "refunded")
592
780
  throw new Error(`Transaction ${txId} already refunded`);
593
- // Reverse on external rail
594
- if (tx.externalId) {
595
- const reversal = await this.paymentRail.reversePayment(tx.externalId, tx.amount);
596
- tx.externalStatus = reversal.status;
597
- }
598
- if (tx.status === "completed") {
599
- // Refund the net amount (platform fee is NOT refunded)
600
- const refundAmount = tx.netAmount ?? tx.amount;
601
- this._wallet = Math.max(this._wallet - refundAmount, 0);
602
- this._reputation = Math.max(this._reputation - 0.05, 0);
603
- // Ledger: reverse the net settlement
604
- this.ledger.recordRefund(this.agentId, tx.id, refundAmount, tx.counterpartyId);
781
+ if (tx.status === "expired")
782
+ throw new Error(`Transaction ${txId} has expired and cannot be refunded`);
783
+ // Prevent concurrent double-refund (mirrors settle guard)
784
+ if (this._refundingTxIds.has(txId))
785
+ throw new Error(`Transaction ${txId} is already being refunded`);
786
+ this._refundingTxIds.add(txId);
787
+ try {
788
+ // Enforce dispute window: completed transactions can only be refunded within the window
789
+ if (tx.status === "completed" && tx.completedAt) {
790
+ const windowMs = this.fraud.config.disputeWindowMinutes * 60_000;
791
+ const elapsed = Date.now() - tx.completedAt.getTime();
792
+ if (windowMs > 0 && elapsed > windowMs) {
793
+ throw new Error(`Refund window expired. Transaction was settled ${Math.floor(elapsed / 60_000)} minutes ago. ` +
794
+ `Refund window is ${this.fraud.config.disputeWindowMinutes} minutes.`);
795
+ }
796
+ }
797
+ // Reverse on external rail
798
+ if (tx.externalId) {
799
+ const reversal = await this.paymentRail.reversePayment(tx.externalId, tx.amount);
800
+ tx.externalStatus = reversal.status;
801
+ }
802
+ if (tx.status === "completed") {
803
+ // Refund the net amount (platform fee is NOT refunded)
804
+ const refundAmount = tx.netAmount ?? tx.amount;
805
+ this._wallet = Math.max(this._wallet - refundAmount, 0);
806
+ this._reputation = Math.max(this._reputation - 0.05, 0);
807
+ // Ledger: reverse the net settlement
808
+ this.ledger.recordRefund(this.agentId, tx.id, refundAmount, tx.counterpartyId);
809
+ }
810
+ else if (tx.status === "pending") {
811
+ // Ledger: release escrow back to agent
812
+ this.ledger.recordCancellation(this.agentId, tx.amount, tx.id);
813
+ }
814
+ tx.status = "refunded";
815
+ this.audit("payment:refunded", { id: tx.id, amount: tx.amount, netRefunded: tx.netAmount ?? tx.amount });
816
+ this._saveToDisk();
817
+ this.emit("payment:refunded", { id: tx.id });
818
+ this.log(`Refunded $${tx.amount.toFixed(2)} → reputation: ${this._reputation.toFixed(2)}`);
819
+ return { ...tx };
605
820
  }
606
- else if (tx.status === "pending") {
607
- // Ledger: release escrow back to agent
608
- this.ledger.recordCancellation(this.agentId, tx.amount, tx.id);
821
+ finally {
822
+ this._refundingTxIds.delete(txId);
609
823
  }
610
- tx.status = "refunded";
611
- this.audit("payment:refunded", { id: tx.id, amount: tx.amount, netRefunded: tx.netAmount ?? tx.amount });
612
- this._saveToDisk();
613
- this.emit("payment:refunded", { id: tx.id });
614
- this.log(`Refunded $${tx.amount.toFixed(2)} → reputation: ${this._reputation.toFixed(2)}`);
615
- return { ...tx };
616
824
  }
617
825
  // ── Dispute Resolution ─────────────────────────────────────────────────
618
826
  async dispute(txId, reason, evidence) {
@@ -632,19 +840,24 @@ class MnemoPayLite extends EventEmitter {
632
840
  return d;
633
841
  }
634
842
  async resolveDispute(disputeId, outcome) {
843
+ // Security: verify the dispute belongs to this agent (only agent's own disputes)
635
844
  const d = this.fraud.resolveDispute(disputeId, outcome);
845
+ const tx = this.transactions.get(d.txId);
846
+ if (!tx)
847
+ throw new Error("Dispute references unknown transaction");
848
+ if (tx.agentId !== this.agentId)
849
+ throw new Error("Unauthorized: cannot resolve another agent's dispute");
636
850
  if (outcome === "refund") {
637
- const tx = this.transactions.get(d.txId);
638
- if (tx && tx.status === "disputed") {
851
+ if (tx.status === "disputed") {
639
852
  const refundAmount = tx.netAmount ?? tx.amount;
853
+ // Ledger first, then wallet (atomic ordering)
640
854
  this._wallet = Math.max(this._wallet - refundAmount, 0);
641
855
  this._reputation = Math.max(this._reputation - 0.05, 0);
642
856
  tx.status = "refunded";
643
857
  }
644
858
  }
645
859
  else {
646
- const tx = this.transactions.get(d.txId);
647
- if (tx && tx.status === "disputed") {
860
+ if (tx.status === "disputed") {
648
861
  tx.status = "completed"; // Restore to completed
649
862
  }
650
863
  }
@@ -675,6 +888,26 @@ class MnemoPayLite extends EventEmitter {
675
888
  async verifyLedger() {
676
889
  return this.ledger.verify();
677
890
  }
891
+ /**
892
+ * Reconcile wallet balance against ledger (source of truth).
893
+ * Returns drift amount. If drift !== 0, the wallet is corrected to match the ledger.
894
+ * Call periodically or after crashes to detect/fix inconsistencies.
895
+ */
896
+ async reconcile() {
897
+ const walletBefore = Math.round(this._wallet * 100) / 100;
898
+ const acctBalance = this.ledger.getAccountBalance(`agent:${this.agentId}`, "USD");
899
+ const ledgerBalance = Math.round(acctBalance.balance * 100) / 100;
900
+ const drift = Math.round((walletBefore - ledgerBalance) * 100) / 100;
901
+ if (drift !== 0) {
902
+ this.log(`RECONCILIATION DRIFT: wallet=$${walletBefore}, ledger=$${ledgerBalance}, drift=$${drift}`);
903
+ this._wallet = ledgerBalance;
904
+ this.audit("reconciliation:drift", { walletBefore, ledgerBalance, drift });
905
+ this._saveToDisk();
906
+ this.emit("reconciliation:drift", { walletBefore, ledgerBalance, drift });
907
+ return { walletBefore, ledgerBalance, drift, corrected: true };
908
+ }
909
+ return { walletBefore, ledgerBalance, drift: 0, corrected: false };
910
+ }
678
911
  /**
679
912
  * Get all ledger entries for a specific transaction.
680
913
  */
@@ -735,7 +968,7 @@ class MnemoPayLite extends EventEmitter {
735
968
  name: `MnemoPay Agent (${this.agentId})`,
736
969
  description: "AI agent with persistent cognitive memory and micropayment capabilities via MnemoPay protocol.",
737
970
  url,
738
- version: "0.8.0",
971
+ version: "0.9.1",
739
972
  capabilities: {
740
973
  memory: true,
741
974
  payments: true,
@@ -867,23 +1100,50 @@ class MnemoPay extends EventEmitter {
867
1100
  const text = await res.text();
868
1101
  return text ? JSON.parse(text) : null;
869
1102
  }
1103
+ // ── Retry logic for production API calls ─────────────────────────────────
1104
+ async withRetry(fn, maxRetries = 2, delayMs = 500) {
1105
+ let lastError = null;
1106
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
1107
+ try {
1108
+ return await fn();
1109
+ }
1110
+ catch (err) {
1111
+ lastError = err;
1112
+ // Don't retry client errors (4xx) — only transient failures (5xx, network)
1113
+ if (err.message?.includes("4") && /\b4\d{2}\b/.test(err.message))
1114
+ throw err;
1115
+ if (attempt < maxRetries) {
1116
+ await new Promise(resolve => setTimeout(resolve, delayMs * (attempt + 1)));
1117
+ this.log(`Retrying (${attempt + 1}/${maxRetries}): ${err.message}`);
1118
+ }
1119
+ }
1120
+ }
1121
+ throw lastError;
1122
+ }
870
1123
  // ── Memory Methods (→ Mnemosyne API) ───────────────────────────────────
871
1124
  async remember(content, opts) {
872
- const importance = opts?.importance ?? autoScore(content);
873
- const result = await this.mnemoFetch("/v1/memories", {
1125
+ if (!content || typeof content !== "string")
1126
+ throw new Error("Memory content is required");
1127
+ if (content.length > 100_000)
1128
+ throw new Error("Memory content exceeds 100KB limit");
1129
+ // Security: sanitize against prompt injection (same as MnemoPayLite)
1130
+ const safeContent = sanitizeMemoryContent(content);
1131
+ const safeTags = validateTags(opts?.tags ?? []);
1132
+ const importance = opts?.importance ?? autoScore(safeContent);
1133
+ const result = await this.withRetry(() => this.mnemoFetch("/v1/memories", {
874
1134
  method: "POST",
875
1135
  body: JSON.stringify({
876
- content,
1136
+ content: safeContent,
877
1137
  tier: "long_term",
878
1138
  metadata: {
879
1139
  memory_type: "OBSERVATION",
880
- tags: opts?.tags ?? [],
1140
+ tags: safeTags,
881
1141
  confidence: importance,
882
1142
  },
883
1143
  }),
884
- });
885
- this.emit("memory:stored", { id: result.id, content, importance });
886
- this.log(`Stored memory: "${content.slice(0, 60)}..." (id: ${result.id})`);
1144
+ }));
1145
+ this.emit("memory:stored", { id: result.id, importance });
1146
+ this.log(`Stored memory: "${safeContent.slice(0, 60)}..." (id: ${result.id})`);
887
1147
  return result.id;
888
1148
  }
889
1149
  async recall(queryOrLimit, maybeLimit) {
@@ -944,9 +1204,14 @@ class MnemoPay extends EventEmitter {
944
1204
  }
945
1205
  // ── Payment Methods (→ AgentPay API) ───────────────────────────────────
946
1206
  async charge(amount, reason) {
947
- if (amount <= 0)
948
- throw new Error("Amount must be positive");
949
- const result = await this.agentpayFetch("/api/escrow", {
1207
+ if (!Number.isFinite(amount) || amount <= 0)
1208
+ throw new Error("Amount must be a positive finite number");
1209
+ amount = Math.round(amount * 100) / 100;
1210
+ if (!reason || typeof reason !== "string")
1211
+ throw new Error("Reason is required");
1212
+ if (reason.length > 1000)
1213
+ throw new Error("Reason exceeds 1000 character limit");
1214
+ const result = await this.withRetry(() => this.agentpayFetch("/api/escrow", {
950
1215
  method: "POST",
951
1216
  body: JSON.stringify({
952
1217
  agentId: this.agentId,
@@ -954,7 +1219,7 @@ class MnemoPay extends EventEmitter {
954
1219
  reason,
955
1220
  currency: "USD",
956
1221
  }),
957
- });
1222
+ }));
958
1223
  const tx = {
959
1224
  id: result.id,
960
1225
  agentId: this.agentId,
@@ -968,10 +1233,12 @@ class MnemoPay extends EventEmitter {
968
1233
  return tx;
969
1234
  }
970
1235
  async settle(txId) {
971
- const result = await this.agentpayFetch(`/api/escrow/${txId}/release`, {
1236
+ if (!txId || typeof txId !== "string")
1237
+ throw new Error("Transaction ID is required");
1238
+ const result = await this.withRetry(() => this.agentpayFetch(`/api/escrow/${encodeURIComponent(txId)}/release`, {
972
1239
  method: "POST",
973
1240
  body: JSON.stringify({}),
974
- });
1241
+ }));
975
1242
  this.emit("payment:completed", { id: txId, amount: result.amount });
976
1243
  this.log(`Settled: $${result.amount?.toFixed(2)}`);
977
1244
  return {
@@ -985,10 +1252,12 @@ class MnemoPay extends EventEmitter {
985
1252
  };
986
1253
  }
987
1254
  async refund(txId) {
988
- const result = await this.agentpayFetch(`/api/escrow/${txId}/refund`, {
1255
+ if (!txId || typeof txId !== "string")
1256
+ throw new Error("Transaction ID is required");
1257
+ const result = await this.withRetry(() => this.agentpayFetch(`/api/escrow/${encodeURIComponent(txId)}/refund`, {
989
1258
  method: "POST",
990
1259
  body: JSON.stringify({}),
991
- });
1260
+ }));
992
1261
  this.emit("payment:refunded", { id: txId });
993
1262
  this.log(`Refunded: ${txId}`);
994
1263
  return {
@@ -1074,7 +1343,7 @@ class MnemoPay extends EventEmitter {
1074
1343
  name: `MnemoPay Agent (${this.agentId})`,
1075
1344
  description: "AI agent with persistent cognitive memory and micropayment capabilities via MnemoPay protocol.",
1076
1345
  url,
1077
- version: "0.8.0",
1346
+ version: "0.9.1",
1078
1347
  capabilities: {
1079
1348
  memory: true,
1080
1349
  payments: true,
@@ -1185,8 +1454,12 @@ var ledger_js_2 = require("./ledger.js");
1185
1454
  Object.defineProperty(exports, "Ledger", { enumerable: true, get: function () { return ledger_js_2.Ledger; } });
1186
1455
  var identity_js_2 = require("./identity.js");
1187
1456
  Object.defineProperty(exports, "IdentityRegistry", { enumerable: true, get: function () { return identity_js_2.IdentityRegistry; } });
1457
+ Object.defineProperty(exports, "constantTimeEqual", { enumerable: true, get: function () { return identity_js_2.constantTimeEqual; } });
1188
1458
  var network_js_1 = require("./network.js");
1189
1459
  Object.defineProperty(exports, "MnemoPayNetwork", { enumerable: true, get: function () { return network_js_1.MnemoPayNetwork; } });
1460
+ var commerce_js_1 = require("./commerce.js");
1461
+ Object.defineProperty(exports, "CommerceEngine", { enumerable: true, get: function () { return commerce_js_1.CommerceEngine; } });
1462
+ Object.defineProperty(exports, "MockCommerceProvider", { enumerable: true, get: function () { return commerce_js_1.MockCommerceProvider; } });
1190
1463
  var server_js_1 = require("./mcp/server.js");
1191
1464
  Object.defineProperty(exports, "createSandboxServer", { enumerable: true, get: function () { return __importDefault(server_js_1).default; } });
1192
1465
  //# sourceMappingURL=index.js.map