@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/commerce.d.ts +2 -0
- package/dist/commerce.d.ts.map +1 -1
- package/dist/commerce.js +37 -7
- package/dist/commerce.js.map +1 -1
- package/dist/identity.d.ts +26 -3
- package/dist/identity.d.ts.map +1 -1
- package/dist/identity.js +124 -3
- package/dist/identity.js.map +1 -1
- package/dist/index.d.ts +21 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +258 -64
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +47 -5
- package/dist/mcp/server.js.map +1 -1
- package/package.json +2 -2
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
|
-
|
|
190
|
-
|
|
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
|
-
|
|
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:
|
|
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,
|
|
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,
|
|
409
|
-
this.log(`Stored memory:
|
|
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.
|
|
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
|
|
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
|
-
//
|
|
661
|
-
if (
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
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
|
-
|
|
670
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
949
|
-
|
|
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:
|
|
1140
|
+
tags: safeTags,
|
|
957
1141
|
confidence: importance,
|
|
958
1142
|
},
|
|
959
1143
|
}),
|
|
960
|
-
});
|
|
961
|
-
this.emit("memory:stored", { id: result.id,
|
|
962
|
-
this.log(`Stored memory: "${
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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");
|