@mnemopay/sdk 0.9.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.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.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)
@@ -113,8 +136,16 @@ class MnemoPayLite extends EventEmitter {
113
136
  storageAdapter;
114
137
  /** Guard against concurrent double-settle on the same transaction */
115
138
  _settlingTxIds = new Set();
139
+ /** Guard against concurrent double-refund on the same transaction */
140
+ _refundingTxIds = new Set();
116
141
  /** Guard against concurrent wallet mutations */
117
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;
118
149
  /** Fraud detection, rate limiting, dispute resolution, and platform fee */
119
150
  fraud;
120
151
  /** Pluggable payment rail (Stripe, Lightning, etc.). Default: in-memory mock. */
@@ -173,6 +204,20 @@ class MnemoPayLite extends EventEmitter {
173
204
  this._loadFromDisk();
174
205
  // Auto-save every 30 seconds
175
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
+ }
176
221
  this.log(`Persistence enabled: ${this.persistPath}`);
177
222
  }
178
223
  catch (e) {
@@ -186,12 +231,58 @@ class MnemoPayLite extends EventEmitter {
186
231
  const fs = require("fs");
187
232
  if (!fs.existsSync(this.persistPath))
188
233
  return;
189
- const raw = JSON.parse(fs.readFileSync(this.persistPath, "utf-8"));
190
- // 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)
191
271
  if (raw.memories) {
192
272
  for (const m of raw.memories) {
193
273
  m.createdAt = new Date(m.createdAt);
194
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 = [];
195
286
  this.memories.set(m.id, m);
196
287
  }
197
288
  }
@@ -356,8 +447,16 @@ class MnemoPayLite extends EventEmitter {
356
447
  identity: this.identity.serialize(),
357
448
  savedAt: new Date().toISOString(),
358
449
  });
359
- // Atomic write
450
+ // Atomic write with backup: .bak → .tmp → main
360
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
+ }
361
460
  fs.writeFileSync(tmpPath, data, "utf-8");
362
461
  fs.renameSync(tmpPath, this.persistPath);
363
462
  }
@@ -378,6 +477,10 @@ class MnemoPayLite extends EventEmitter {
378
477
  createdAt: new Date(),
379
478
  };
380
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
+ }
381
484
  }
382
485
  // ── Memory Methods ──────────────────────────────────────────────────────
383
486
  async remember(content, opts) {
@@ -385,28 +488,36 @@ class MnemoPayLite extends EventEmitter {
385
488
  throw new Error("Memory content is required");
386
489
  if (content.length > 100_000)
387
490
  throw new Error("Memory content exceeds 100KB limit");
388
- 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);
389
500
  const now = new Date();
390
501
  const mem = {
391
502
  id: randomUUID(),
392
503
  agentId: this.agentId,
393
- content,
504
+ content: safeContent,
394
505
  importance: Math.min(Math.max(importance, 0), 1),
395
506
  score: importance,
396
507
  createdAt: now,
397
508
  lastAccessed: now,
398
509
  accessCount: 0,
399
- tags: opts?.tags ?? [],
510
+ tags: safeTags,
400
511
  };
401
512
  this.memories.set(mem.id, mem);
402
513
  // Generate embedding if using vector/hybrid recall
403
514
  if (this.recallEngine.strategy !== "score") {
404
515
  await this.recallEngine.embed(mem.id, content);
405
516
  }
406
- 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 });
407
518
  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)})`);
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"})`);
410
521
  return mem.id;
411
522
  }
412
523
  async recall(queryOrLimit, maybeLimit) {
@@ -524,6 +635,12 @@ class MnemoPayLite extends EventEmitter {
524
635
  amount = Math.round(amount * 100) / 100;
525
636
  if (!reason || typeof reason !== "string")
526
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
+ }
527
644
  const maxCharge = 500 * this._reputation;
528
645
  if (amount > maxCharge) {
529
646
  throw new Error(`Amount $${amount.toFixed(2)} exceeds reputation ceiling $${maxCharge.toFixed(2)} ` +
@@ -611,16 +728,20 @@ class MnemoPayLite extends EventEmitter {
611
728
  const fee = this.fraud.applyPlatformFee(tx.id, this.agentId, tx.amount);
612
729
  tx.platformFee = fee.feeAmount;
613
730
  tx.netAmount = fee.netAmount;
614
- // 4. Move NET funds to wallet (atomic via sequential lock)
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)
615
734
  const prevLock = this._walletLock;
616
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
+ }
617
740
  tx.status = "completed";
618
741
  tx.completedAt = new Date();
619
- this._wallet += fee.netAmount;
742
+ this._wallet = Math.round(newBalance * 100) / 100; // 2-decimal precision
620
743
  });
621
744
  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
745
  // 4. Boost reputation
625
746
  this._reputation = Math.min(this._reputation + 0.01, 1.0);
626
747
  // 5. Reinforce recently-accessed memories (feedback loop)
@@ -650,6 +771,8 @@ class MnemoPayLite extends EventEmitter {
650
771
  }
651
772
  }
652
773
  async refund(txId) {
774
+ if (!txId || typeof txId !== "string")
775
+ throw new Error("Transaction ID is required");
653
776
  const tx = this.transactions.get(txId);
654
777
  if (!tx)
655
778
  throw new Error(`Transaction ${txId} not found`);
@@ -657,38 +780,47 @@ class MnemoPayLite extends EventEmitter {
657
780
  throw new Error(`Transaction ${txId} already refunded`);
658
781
  if (tx.status === "expired")
659
782
  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.`);
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
+ }
667
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 };
668
820
  }
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);
821
+ finally {
822
+ this._refundingTxIds.delete(txId);
685
823
  }
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
824
  }
693
825
  // ── Dispute Resolution ─────────────────────────────────────────────────
694
826
  async dispute(txId, reason, evidence) {
@@ -708,19 +840,24 @@ class MnemoPayLite extends EventEmitter {
708
840
  return d;
709
841
  }
710
842
  async resolveDispute(disputeId, outcome) {
843
+ // Security: verify the dispute belongs to this agent (only agent's own disputes)
711
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");
712
850
  if (outcome === "refund") {
713
- const tx = this.transactions.get(d.txId);
714
- if (tx && tx.status === "disputed") {
851
+ if (tx.status === "disputed") {
715
852
  const refundAmount = tx.netAmount ?? tx.amount;
853
+ // Ledger first, then wallet (atomic ordering)
716
854
  this._wallet = Math.max(this._wallet - refundAmount, 0);
717
855
  this._reputation = Math.max(this._reputation - 0.05, 0);
718
856
  tx.status = "refunded";
719
857
  }
720
858
  }
721
859
  else {
722
- const tx = this.transactions.get(d.txId);
723
- if (tx && tx.status === "disputed") {
860
+ if (tx.status === "disputed") {
724
861
  tx.status = "completed"; // Restore to completed
725
862
  }
726
863
  }
@@ -751,6 +888,26 @@ class MnemoPayLite extends EventEmitter {
751
888
  async verifyLedger() {
752
889
  return this.ledger.verify();
753
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
+ }
754
911
  /**
755
912
  * Get all ledger entries for a specific transaction.
756
913
  */
@@ -811,7 +968,7 @@ class MnemoPayLite extends EventEmitter {
811
968
  name: `MnemoPay Agent (${this.agentId})`,
812
969
  description: "AI agent with persistent cognitive memory and micropayment capabilities via MnemoPay protocol.",
813
970
  url,
814
- version: "0.8.0",
971
+ version: "0.9.1",
815
972
  capabilities: {
816
973
  memory: true,
817
974
  payments: true,
@@ -943,23 +1100,50 @@ class MnemoPay extends EventEmitter {
943
1100
  const text = await res.text();
944
1101
  return text ? JSON.parse(text) : null;
945
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
+ }
946
1123
  // ── Memory Methods (→ Mnemosyne API) ───────────────────────────────────
947
1124
  async remember(content, opts) {
948
- const importance = opts?.importance ?? autoScore(content);
949
- 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", {
950
1134
  method: "POST",
951
1135
  body: JSON.stringify({
952
- content,
1136
+ content: safeContent,
953
1137
  tier: "long_term",
954
1138
  metadata: {
955
1139
  memory_type: "OBSERVATION",
956
- tags: opts?.tags ?? [],
1140
+ tags: safeTags,
957
1141
  confidence: importance,
958
1142
  },
959
1143
  }),
960
- });
961
- this.emit("memory:stored", { id: result.id, content, importance });
962
- 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})`);
963
1147
  return result.id;
964
1148
  }
965
1149
  async recall(queryOrLimit, maybeLimit) {
@@ -1020,9 +1204,14 @@ class MnemoPay extends EventEmitter {
1020
1204
  }
1021
1205
  // ── Payment Methods (→ AgentPay API) ───────────────────────────────────
1022
1206
  async charge(amount, reason) {
1023
- if (amount <= 0)
1024
- throw new Error("Amount must be positive");
1025
- 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", {
1026
1215
  method: "POST",
1027
1216
  body: JSON.stringify({
1028
1217
  agentId: this.agentId,
@@ -1030,7 +1219,7 @@ class MnemoPay extends EventEmitter {
1030
1219
  reason,
1031
1220
  currency: "USD",
1032
1221
  }),
1033
- });
1222
+ }));
1034
1223
  const tx = {
1035
1224
  id: result.id,
1036
1225
  agentId: this.agentId,
@@ -1044,10 +1233,12 @@ class MnemoPay extends EventEmitter {
1044
1233
  return tx;
1045
1234
  }
1046
1235
  async settle(txId) {
1047
- 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`, {
1048
1239
  method: "POST",
1049
1240
  body: JSON.stringify({}),
1050
- });
1241
+ }));
1051
1242
  this.emit("payment:completed", { id: txId, amount: result.amount });
1052
1243
  this.log(`Settled: $${result.amount?.toFixed(2)}`);
1053
1244
  return {
@@ -1061,10 +1252,12 @@ class MnemoPay extends EventEmitter {
1061
1252
  };
1062
1253
  }
1063
1254
  async refund(txId) {
1064
- 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`, {
1065
1258
  method: "POST",
1066
1259
  body: JSON.stringify({}),
1067
- });
1260
+ }));
1068
1261
  this.emit("payment:refunded", { id: txId });
1069
1262
  this.log(`Refunded: ${txId}`);
1070
1263
  return {
@@ -1150,7 +1343,7 @@ class MnemoPay extends EventEmitter {
1150
1343
  name: `MnemoPay Agent (${this.agentId})`,
1151
1344
  description: "AI agent with persistent cognitive memory and micropayment capabilities via MnemoPay protocol.",
1152
1345
  url,
1153
- version: "0.8.0",
1346
+ version: "0.9.1",
1154
1347
  capabilities: {
1155
1348
  memory: true,
1156
1349
  payments: true,
@@ -1261,6 +1454,7 @@ var ledger_js_2 = require("./ledger.js");
1261
1454
  Object.defineProperty(exports, "Ledger", { enumerable: true, get: function () { return ledger_js_2.Ledger; } });
1262
1455
  var identity_js_2 = require("./identity.js");
1263
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; } });
1264
1458
  var network_js_1 = require("./network.js");
1265
1459
  Object.defineProperty(exports, "MnemoPayNetwork", { enumerable: true, get: function () { return network_js_1.MnemoPayNetwork; } });
1266
1460
  var commerce_js_1 = require("./commerce.js");