@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/cli/dashboard.d.ts +9 -0
- package/dist/cli/dashboard.d.ts.map +1 -0
- package/dist/cli/dashboard.js +78 -0
- package/dist/cli/dashboard.js.map +1 -0
- package/dist/client.d.ts +182 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +177 -0
- package/dist/client.js.map +1 -0
- package/dist/commerce.d.ts +227 -0
- package/dist/commerce.d.ts.map +1 -0
- package/dist/commerce.js +450 -0
- package/dist/commerce.js.map +1 -0
- package/dist/identity.d.ts +26 -3
- package/dist/identity.d.ts.map +1 -1
- package/dist/identity.js +136 -23
- package/dist/identity.js.map +1 -1
- package/dist/index.d.ts +36 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +361 -88
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +342 -8
- package/dist/mcp/server.js.map +1 -1
- package/dist/rails/index.d.ts.map +1 -1
- package/dist/rails/index.js +14 -3
- package/dist/rails/index.js.map +1 -1
- package/dist/rails/paystack.js +2 -2
- package/dist/rails/paystack.js.map +1 -1
- package/package.json +22 -7
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
|
-
|
|
186
|
-
|
|
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
|
-
|
|
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:
|
|
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,
|
|
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,
|
|
405
|
-
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"})`);
|
|
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
|
-
|
|
549
|
-
|
|
550
|
-
const
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
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
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
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
|
-
|
|
607
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
873
|
-
|
|
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:
|
|
1140
|
+
tags: safeTags,
|
|
881
1141
|
confidence: importance,
|
|
882
1142
|
},
|
|
883
1143
|
}),
|
|
884
|
-
});
|
|
885
|
-
this.emit("memory:stored", { id: result.id,
|
|
886
|
-
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})`);
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|