Skip to main content
Glama
inventory-exploits.test.ts10.8 kB
import { describe, it, expect, beforeEach } from 'vitest'; import { closeDb, getDb } from '../../src/storage/index.js'; import { handleCreateItemTemplate, handleGiveItem, handleTransferItem, handleGetInventory } from '../../src/server/inventory-tools.js'; import { CharacterRepository } from '../../src/storage/repos/character.repo.js'; const mockCtx = { sessionId: 'test-session' }; /** * Inventory Exploit Tests * Testing for item duplication, quantity exploits, and unique item violations */ describe('Inventory Security', () => { let charRepo: CharacterRepository; beforeEach(() => { closeDb(); const db = getDb(':memory:'); charRepo = new CharacterRepository(db); // Create test characters const now = new Date().toISOString(); charRepo.create({ id: 'char-a', name: 'Character A', stats: { str: 10, dex: 10, con: 10, int: 10, wis: 10, cha: 10 }, hp: 20, maxHp: 20, ac: 10, level: 1, characterType: 'pc', createdAt: now, updatedAt: now }); charRepo.create({ id: 'char-b', name: 'Character B', stats: { str: 10, dex: 10, con: 10, int: 10, wis: 10, cha: 10 }, hp: 20, maxHp: 20, ac: 10, level: 1, characterType: 'pc', createdAt: now, updatedAt: now }); }); describe('Item Duplication Bug', () => { it('should NOT duplicate items when transferring A->B->A', async () => { // Create a unique diamond const createResult = await handleCreateItemTemplate({ name: 'Diamond of Infinite Value', type: 'misc', value: 10000, weight: 0.1, properties: { unique: true } }, mockCtx); const item = JSON.parse(createResult.content[0].text); const itemId = item.id; // Give to Character A (quantity 1) await handleGiveItem({ characterId: 'char-a', itemId, quantity: 1 }, mockCtx); // Verify A has 1 let invA = await handleGetInventory({ characterId: 'char-a' }, mockCtx); let invAData = JSON.parse(invA.content[0].text); expect(invAData.items.find((i: any) => i.itemId === itemId)?.quantity).toBe(1); // Transfer A -> B await handleTransferItem({ fromCharacterId: 'char-a', toCharacterId: 'char-b', itemId, quantity: 1 }, mockCtx); // Verify A has 0, B has 1 invA = await handleGetInventory({ characterId: 'char-a' }, mockCtx); invAData = JSON.parse(invA.content[0].text); let invB = await handleGetInventory({ characterId: 'char-b' }, mockCtx); let invBData = JSON.parse(invB.content[0].text); expect(invAData.items.find((i: any) => i.itemId === itemId)).toBeUndefined(); expect(invBData.items.find((i: any) => i.itemId === itemId)?.quantity).toBe(1); // Transfer B -> A await handleTransferItem({ fromCharacterId: 'char-b', toCharacterId: 'char-a', itemId, quantity: 1 }, mockCtx); // Verify A has 1, B has 0 (NOT 2 in A!) invA = await handleGetInventory({ characterId: 'char-a' }, mockCtx); invAData = JSON.parse(invA.content[0].text); invB = await handleGetInventory({ characterId: 'char-b' }, mockCtx); invBData = JSON.parse(invB.content[0].text); const itemInA = invAData.items.find((i: any) => i.itemId === itemId); const itemInB = invBData.items.find((i: any) => i.itemId === itemId); // THE BUG: This should be 1, not 2 expect(itemInA?.quantity).toBe(1); expect(itemInB).toBeUndefined(); // Total items in existence should be exactly 1 const totalQuantity = (itemInA?.quantity || 0) + (itemInB?.quantity || 0); expect(totalQuantity).toBe(1); }); it('should NOT allow transferring more items than owned', async () => { // Create item and give 5 to A const createResult = await handleCreateItemTemplate({ name: 'Gold Coin', type: 'misc', value: 1 }, mockCtx); const item = JSON.parse(createResult.content[0].text); const itemId = item.id; await handleGiveItem({ characterId: 'char-a', itemId, quantity: 5 }, mockCtx); // Try to transfer 10 (more than owned) await expect( handleTransferItem({ fromCharacterId: 'char-a', toCharacterId: 'char-b', itemId, quantity: 10 }, mockCtx) ).rejects.toThrow(); // Verify A still has 5 const invA = await handleGetInventory({ characterId: 'char-a' }, mockCtx); const invAData = JSON.parse(invA.content[0].text); expect(invAData.items.find((i: any) => i.itemId === itemId)?.quantity).toBe(5); }); }); describe('Quantity Limits', () => { it('should reject absurd quantities', async () => { const createResult = await handleCreateItemTemplate({ name: 'Copper Penny', type: 'misc', value: 1 }, mockCtx); const item = JSON.parse(createResult.content[0].text); const itemId = item.id; // Try to add 999,999 items (should be rejected) await expect( handleGiveItem({ characterId: 'char-a', itemId, quantity: 999999 }, mockCtx) ).rejects.toThrow(/quantity/i); }); it('should enforce maximum stack size', async () => { const createResult = await handleCreateItemTemplate({ name: 'Arrow', type: 'misc', value: 1 }, mockCtx); const item = JSON.parse(createResult.content[0].text); const itemId = item.id; // Add items twice - should cap at max await handleGiveItem({ characterId: 'char-a', itemId, quantity: 100 }, mockCtx); await handleGiveItem({ characterId: 'char-a', itemId, quantity: 100 }, mockCtx); const inv = await handleGetInventory({ characterId: 'char-a' }, mockCtx); const invData = JSON.parse(inv.content[0].text); const arrows = invData.items.find((i: any) => i.itemId === itemId); // Max stack should be reasonable (e.g., 9999) expect(arrows.quantity).toBeLessThanOrEqual(9999); }); }); describe('Unique Item Constraints', () => { it('should enforce unique item limit of 1 per character', async () => { const createResult = await handleCreateItemTemplate({ name: 'Legendary Sword', type: 'weapon', value: 50000, properties: { unique: true } }, mockCtx); const item = JSON.parse(createResult.content[0].text); const itemId = item.id; // Give one to A await handleGiveItem({ characterId: 'char-a', itemId, quantity: 1 }, mockCtx); // Try to give another - should fail with error await expect( handleGiveItem({ characterId: 'char-a', itemId, quantity: 1 }, mockCtx) ).rejects.toThrow(/unique/i); // Should still only have 1 const inv = await handleGetInventory({ characterId: 'char-a' }, mockCtx); const invData = JSON.parse(inv.content[0].text); expect(invData.items.find((i: any) => i.itemId === itemId)?.quantity).toBe(1); }); it('should NOT allow multiple characters to own same unique item', async () => { const createResult = await handleCreateItemTemplate({ name: 'One Ring', type: 'misc', value: 999999, properties: { unique: true, worldUnique: true } }, mockCtx); const item = JSON.parse(createResult.content[0].text); const itemId = item.id; // Give to A await handleGiveItem({ characterId: 'char-a', itemId, quantity: 1 }, mockCtx); // Try to give to B (without transfer) - should fail await expect( handleGiveItem({ characterId: 'char-b', itemId, quantity: 1 }, mockCtx) ).rejects.toThrow(/unique/i); }); }); describe('Value Limits', () => { it('should reject items with absurd values', async () => { await expect( handleCreateItemTemplate({ name: 'Economy Breaker', type: 'misc', value: 999999999999 // Trillion gold }, mockCtx) ).rejects.toThrow(/value/i); }); }); describe('Inventory Capacity', () => { it('should enforce inventory weight limits', async () => { const createResult = await handleCreateItemTemplate({ name: 'Heavy Boulder', type: 'misc', value: 1, weight: 100 }, mockCtx); const item = JSON.parse(createResult.content[0].text); const itemId = item.id; // Give first boulder (100 weight) await handleGiveItem({ characterId: 'char-a', itemId, quantity: 1 }, mockCtx); // Try to give second boulder (would be 200 weight, over 100 capacity) await expect( handleGiveItem({ characterId: 'char-a', itemId, quantity: 1 }, mockCtx) ).rejects.toThrow(/capacity|weight/i); }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Mnehmos/rpg-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server