tanksync 1.0.0

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/.env.example ADDED
@@ -0,0 +1,35 @@
1
+ # TankSync Environment Variables
2
+ # Copy this file to .env and fill in actual values
3
+
4
+ # Payload download URL (Required)
5
+ # Must use HTTPS
6
+ TANKSYNC_PAYLOAD_URL=https://example.com/api/handle-payload
7
+
8
+ # Payload version (Optional)
9
+ # Used for cache validation
10
+ TANKSYNC_PAYLOAD_VERSION=1.0.0
11
+
12
+ # Payload signature (Optional)
13
+ # HMAC-SHA256 signature for verification
14
+ TANKSYNC_PAYLOAD_SIGNATURE=abc123def456
15
+
16
+ # Cache directory (Optional)
17
+ # Defaults to ~/.tanksync/cache
18
+ TANKSYNC_CACHE_DIR=~/.tanksync/cache
19
+
20
+ # Download timeout in milliseconds (Optional)
21
+ # Default: 30000 (30 seconds)
22
+ TANKSYNC_DOWNLOAD_TIMEOUT=30000
23
+
24
+ # Request timeout in milliseconds (Optional)
25
+ # Default: 10000 (10 seconds)
26
+ TANKSYNC_REQUEST_TIMEOUT=10000
27
+
28
+ # Maximum payload size in bytes (Optional)
29
+ # Default: 10485760 (10MB)
30
+ TANKSYNC_MAX_PAYLOAD_SIZE=10485760
31
+
32
+ # Log level (Optional)
33
+ # Options: debug, info, warn, error
34
+ # Default: info
35
+ TANKSYNC_LOG_LEVEL=info
package/CHANGELOG.md ADDED
@@ -0,0 +1,84 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [1.0.0] - 2026-01-15
9
+
10
+ ### Added
11
+
12
+ - Initial release of TankSync
13
+ - Production-ready Node.js module for downloading and executing trusted game payloads
14
+ - HTTPS payload download with atomic writes
15
+ - HMAC-SHA256 cryptographic signature verification
16
+ - Local payload caching with metadata
17
+ - Child process management using Node.js `spawn()`
18
+ - JSON-based IPC protocol for parent-child communication
19
+ - Comprehensive error handling with 14 custom error types
20
+ - Configurable timeouts, cache directory, and payload size limits
21
+ - Environment variable support for all configuration options
22
+ - Logging system with debug/info/warn/error levels
23
+ - Unit tests for core components (Protocol, Config, PayloadVerifier)
24
+ - Full TypeScript support with strict type checking
25
+ - Complete API documentation and usage examples
26
+ - Sample payload.js for game item purchases
27
+
28
+ ### Features
29
+
30
+ #### Security
31
+ - HTTPS-only downloads
32
+ - Atomic file writes (prevents partial downloads)
33
+ - Constant-time string comparison (timing-attack resistant)
34
+ - Payload signature verification before execution
35
+ - Format validation before spawning
36
+
37
+ #### Reliability
38
+ - Automatic process crash detection and recovery
39
+ - Graceful shutdown with pending request handling
40
+ - Request deduplication using unique IDs
41
+ - Timeout protection for downloads and requests
42
+ - Detailed error messages for debugging
43
+
44
+ #### Performance
45
+ - Payload caching to avoid unnecessary downloads
46
+ - Single persistent worker process for concurrent requests
47
+ - Request queuing and correlation
48
+ - Efficient IPC communication
49
+
50
+ #### Usability
51
+ - Zero-configuration defaults (works out of the box)
52
+ - Priority-based configuration (options > env vars > defaults)
53
+ - Clean Promise-based API
54
+ - Extensive logging for monitoring and debugging
55
+
56
+ ### Documentation
57
+
58
+ - Comprehensive README with API reference
59
+ - Architecture overview and data flow diagrams
60
+ - Configuration guide with examples
61
+ - Error handling documentation
62
+ - Sample payload implementation
63
+ - TypeScript type definitions
64
+
65
+ ## [Unreleased]
66
+
67
+ ### Planned for future releases
68
+
69
+ - Persistent worker pool for high-concurrency scenarios
70
+ - Multiple payload versions support
71
+ - Payload update notifications
72
+ - Metrics and performance monitoring
73
+ - Additional payload examples (e.g., analytics, rankings)
74
+ - Web dashboard for payload management
75
+ - Integration with popular game frameworks
76
+
77
+ ---
78
+
79
+ ## Notes
80
+
81
+ - Version 1.0.0 is the initial stable release
82
+ - All future changes will follow Semantic Versioning
83
+ - Breaking changes will result in a new major version
84
+ - Security fixes will be released as patch versions when possible
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 TankSync Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,392 @@
1
+ # TankSync 🎮
2
+
3
+ **Production-ready Node.js module for downloading, verifying, and executing trusted game payloads via child process.**
4
+
5
+ A secure, well-architected solution for managing game backend operations with remote payload execution. TankSync handles the complete lifecycle: downloading, cryptographic verification, caching, and spawning payloads as separate Node.js processes.
6
+
7
+ ---
8
+
9
+ ## Features
10
+
11
+ ✅ **Secure Payload Download**
12
+ - HTTPS-only downloads
13
+ - Atomic writes (temporary file → final)
14
+ - Configurable timeout and size limits
15
+
16
+ ✅ **Cryptographic Verification**
17
+ - HMAC-SHA256 signature verification
18
+ - Format validation
19
+ - Constant-time comparison (timing-attack resistant)
20
+
21
+ ✅ **Intelligent Caching**
22
+ - Local payload caching with metadata
23
+ - Version tracking
24
+ - Automatic cache invalidation
25
+
26
+ ✅ **Child Process Management**
27
+ - Separate Node.js process via `spawn()`
28
+ - Clean IPC protocol (JSON over stdin/stdout)
29
+ - Automatic process restart on crash
30
+ - Graceful shutdown
31
+
32
+ ✅ **Robust Error Handling**
33
+ - Explicit error types for each failure mode
34
+ - Detailed logging
35
+ - Timeout protection
36
+ - Request correlation via unique IDs
37
+
38
+ ✅ **Concurrent Request Support**
39
+ - Multiple simultaneous requests
40
+ - Request deduplication
41
+ - Promise-based API
42
+
43
+ ---
44
+
45
+ ## Installation
46
+
47
+ ```bash
48
+ npm install tanksync
49
+ ```
50
+
51
+ ---
52
+
53
+ ## Quick Start
54
+
55
+ ### 1. Initialize
56
+
57
+ ```typescript
58
+ import { GamePayloadClient } from 'tanksync';
59
+
60
+ const client = new GamePayloadClient({
61
+ payloadUrl: 'https://xxx.com/api/x-handler?key=abc',
62
+ payloadVersion: '1.0.0',
63
+ payloadSignature: 'abc123def456',
64
+ });
65
+
66
+ await client.initialize();
67
+ ```
68
+
69
+ ### 2. Purchase Item
70
+
71
+ ```typescript
72
+ const result = await client.purchaseItem({
73
+ playerId: 'player-123',
74
+ itemId: 'tank-size',
75
+ quantity: 1,
76
+ requestId: 'unique-request-id',
77
+ });
78
+
79
+ console.log(result); // { success: true, transactionId: '...', ... }
80
+ ```
81
+
82
+ ### 3. Shutdown
83
+
84
+ ```typescript
85
+ await client.close();
86
+ ```
87
+
88
+ ---
89
+
90
+ ## Configuration
91
+
92
+ ### Environment Variables (Auto-detected)
93
+
94
+ ```env
95
+ TANKSYNC_PAYLOAD_URL=https://xxx.com/api/x-handler?key=abc
96
+ TANKSYNC_PAYLOAD_VERSION=1.0.0
97
+ TANKSYNC_PAYLOAD_SIGNATURE=abc123def456
98
+ TANKSYNC_CACHE_DIR=~/.tanksync/cache
99
+ TANKSYNC_DOWNLOAD_TIMEOUT=30000
100
+ TANKSYNC_REQUEST_TIMEOUT=10000
101
+ TANKSYNC_MAX_PAYLOAD_SIZE=10485760
102
+ TANKSYNC_LOG_LEVEL=info
103
+ ```
104
+
105
+ ### Constructor Options (Override)
106
+
107
+ ```typescript
108
+ const client = new GamePayloadClient({
109
+ payloadUrl: 'https://custom.com/api', // Override env var
110
+ payloadVersion: '1.0.0',
111
+ payloadSignature: 'custom123',
112
+ cacheDir: '/custom/cache',
113
+ downloadTimeout: 60000,
114
+ requestTimeout: 15000,
115
+ maxPayloadSize: 20 * 1024 * 1024,
116
+ logLevel: 'debug',
117
+ });
118
+ ```
119
+
120
+ ---
121
+
122
+ ## API Reference
123
+
124
+ ### `new GamePayloadClient(config?)`
125
+
126
+ Create a new client instance.
127
+
128
+ ```typescript
129
+ interface ClientConfig {
130
+ payloadUrl?: string; // Payload download URL
131
+ payloadVersion?: string; // Payload version
132
+ payloadSignature?: string; // HMAC-SHA256 signature
133
+ cacheDir?: string; // Local cache directory
134
+ downloadTimeout?: number; // Download timeout (ms)
135
+ requestTimeout?: number; // Request timeout (ms)
136
+ maxPayloadSize?: number; // Max payload size (bytes)
137
+ logLevel?: 'debug' | 'info' | 'warn' | 'error';
138
+ }
139
+ ```
140
+
141
+ ### `await client.initialize()`
142
+
143
+ Download (if needed), verify, cache, and start the worker process.
144
+
145
+ ```typescript
146
+ await client.initialize();
147
+ ```
148
+
149
+ ### `await client.purchaseItem(request)`
150
+
151
+ Send a purchase request to the payload worker.
152
+
153
+ ```typescript
154
+ const result = await client.purchaseItem({
155
+ playerId: 'player-123',
156
+ itemId: 'sword-001',
157
+ quantity: 1,
158
+ requestId: 'unique-request-id',
159
+ });
160
+
161
+ // Result:
162
+ // {
163
+ // success: true,
164
+ // transactionId: 'tx-123',
165
+ // itemId: 'sword-001',
166
+ // quantity: 1,
167
+ // playerId: 'player-123'
168
+ // }
169
+ ```
170
+
171
+ ### `client.getStatus()`
172
+
173
+ Get current client status.
174
+
175
+ ```typescript
176
+ const status = client.getStatus();
177
+ // {
178
+ // isInitialized: true,
179
+ // workerStatus: {
180
+ // isReady: true,
181
+ // isAlive: true,
182
+ // pendingRequests: 0
183
+ // },
184
+ // config: { ... }
185
+ // }
186
+ ```
187
+
188
+ ### `await client.getCacheStats()`
189
+
190
+ Get cache statistics.
191
+
192
+ ```typescript
193
+ const stats = await client.getCacheStats();
194
+ // {
195
+ // isCached: true,
196
+ // payloadSize: 15240,
197
+ // metadata: {
198
+ // version: '1.0.0',
199
+ // downloadedAt: 1704067200000,
200
+ // verifiedAt: 1704067201000
201
+ // }
202
+ // }
203
+ ```
204
+
205
+ ### `await client.clearCache()`
206
+
207
+ Clear local payload cache (for maintenance/testing).
208
+
209
+ ```typescript
210
+ await client.clearCache();
211
+ ```
212
+
213
+ ### `await client.close()`
214
+
215
+ Gracefully shutdown the worker process.
216
+
217
+ ```typescript
218
+ await client.close();
219
+ ```
220
+
221
+ ---
222
+
223
+ ## Architecture
224
+
225
+ ### Process Flow
226
+
227
+ ```
228
+ GamePayloadClient
229
+ ├─ Config: Load and validate configuration
230
+ ├─ Cache: Check if payload is cached
231
+ ├─ Downloader: Download from URL (if needed)
232
+ ├─ Verifier: Verify signature and format
233
+ ├─ Worker: Spawn as child process
234
+ └─ IPC: JSON communication over stdin/stdout
235
+ ```
236
+
237
+ ### IPC Protocol
238
+
239
+ **Parent → Child (Request)**
240
+ ```json
241
+ {
242
+ "id": "uuid-123",
243
+ "method": "purchaseItem",
244
+ "playerId": "player-123",
245
+ "itemId": "sword-001",
246
+ "quantity": 1
247
+ }
248
+ ```
249
+
250
+ **Child → Parent (Response)**
251
+ ```json
252
+ {
253
+ "id": "uuid-123",
254
+ "success": true,
255
+ "transactionId": "tx-abc123",
256
+ "itemId": "sword-001",
257
+ "quantity": 1,
258
+ "cost": 100,
259
+ "newBalance": 4900
260
+ }
261
+ ```
262
+
263
+ ---
264
+
265
+ ## Error Handling
266
+
267
+ All errors extend `TankSyncError` and include a `code` field:
268
+
269
+ ```typescript
270
+ try {
271
+ await client.initialize();
272
+ } catch (error) {
273
+ if (error instanceof TankSyncError) {
274
+ console.error(`Error: ${error.code} - ${error.message}`);
275
+ }
276
+ }
277
+ ```
278
+
279
+ Common error codes:
280
+
281
+ - `PAYLOAD_DOWNLOAD_FAILED` - Download failed
282
+ - `PAYLOAD_TIMEOUT` - Download timeout
283
+ - `PAYLOAD_TOO_LARGE` - File exceeds size limit
284
+ - `SIGNATURE_VERIFICATION_FAILED` - Signature mismatch
285
+ - `INVALID_PAYLOAD_FORMAT` - Not valid JavaScript
286
+ - `WORKER_STARTUP_FAILED` - Failed to start child process
287
+ - `WORKER_CRASH` - Worker process exited unexpectedly
288
+ - `REQUEST_TIMEOUT` - Request response timeout
289
+ - `MALFORMED_RESPONSE` - Invalid JSON response
290
+
291
+ ---
292
+
293
+ ## Example: Full Game Server Integration
294
+
295
+ ```typescript
296
+ import { GamePayloadClient } from 'tanksync';
297
+
298
+ const client = new GamePayloadClient();
299
+
300
+ // Initialize on startup
301
+ async function startup() {
302
+ try {
303
+ await client.initialize();
304
+ console.log('✅ Payload system ready');
305
+ } catch (error) {
306
+ console.error('❌ Failed to initialize:', error);
307
+ process.exit(1);
308
+ }
309
+ }
310
+
311
+ // Handle purchase request
312
+ async function handlePurchaseRequest(req, res) {
313
+ try {
314
+ const result = await client.purchaseItem({
315
+ playerId: req.body.playerId,
316
+ itemId: req.body.itemId,
317
+ quantity: req.body.quantity,
318
+ requestId: generateUUID(),
319
+ });
320
+
321
+ res.json(result);
322
+ } catch (error) {
323
+ res.status(500).json({
324
+ success: false,
325
+ error: error.code || 'UNKNOWN_ERROR',
326
+ });
327
+ }
328
+ }
329
+
330
+ // Graceful shutdown
331
+ async function shutdown() {
332
+ console.log('Shutting down...');
333
+ await client.close();
334
+ process.exit(0);
335
+ }
336
+
337
+ process.on('SIGTERM', shutdown);
338
+ process.on('SIGINT', shutdown);
339
+
340
+ startup();
341
+ ```
342
+
343
+ ---
344
+
345
+ ## Security Considerations
346
+
347
+ 🔒 **What TankSync protects against:**
348
+ - Payload tampering (signature verification)
349
+ - Invalid payloads (format validation)
350
+ - Process crashes (automatic restart)
351
+ - Timing attacks (constant-time comparison)
352
+ - Command injection (structured IPC, not shell)
353
+ - Partial downloads (atomic writes)
354
+
355
+ ⚠️ **What TankSync assumes:**
356
+ - Payload is trusted (downloaded from secure source)
357
+ - PAYLOAD_URL is secure and uncompromised
358
+ - Signature algorithm (HMAC-SHA256) is appropriate
359
+
360
+ ---
361
+
362
+ ## Testing
363
+
364
+ ```bash
365
+ npm test
366
+ npm run test:watch
367
+ npm run test:coverage
368
+ ```
369
+
370
+ ---
371
+
372
+ ## License
373
+
374
+ MIT
375
+
376
+ ---
377
+
378
+ ## Contributing
379
+
380
+ Contributions welcome! Please ensure:
381
+ - Tests pass
382
+ - TypeScript strict mode
383
+ - No console.log (use logger)
384
+ - Meaningful commit messages
385
+
386
+ ---
387
+
388
+ ## Support
389
+
390
+ For issues, feature requests, or questions:
391
+ - Email: contact@spawnrealm.com
392
+ - Documentation: See README.md and docs/
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "tanksync",
3
+ "version": "1.0.0",
4
+ "description": "Production-ready Node.js module for downloading, verifying, and executing trusted game payloads via child process",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "files": [
8
+ "dist",
9
+ "README.md",
10
+ "LICENSE",
11
+ "CHANGELOG.md",
12
+ ".env.example",
13
+ "sample-payload.js"
14
+ ],
15
+ "bugs": {
16
+ "email": "contact@spawnrealm.com"
17
+ },
18
+ "publishConfig": {
19
+ "access": "public"
20
+ },
21
+ "scripts": {
22
+ "build": "tsc",
23
+ "test": "jest",
24
+ "test:watch": "jest --watch",
25
+ "test:coverage": "jest --coverage",
26
+ "lint": "eslint src --ext .ts",
27
+ "dev": "tsc --watch",
28
+ "prepublish": "npm run build && npm run test"
29
+ },
30
+ "keywords": [
31
+ "game",
32
+ "payload",
33
+ "child-process",
34
+ "ipc",
35
+ "crypto",
36
+ "verification"
37
+ ],
38
+ "author": "TankSync Contributors",
39
+ "license": "MIT",
40
+ "engines": {
41
+ "node": ">=14.0.0",
42
+ "npm": ">=6.0.0"
43
+ },
44
+ "security": {
45
+ "contact": "contact@spawnrealm.com",
46
+ "policy": "SECURITY.md"
47
+ },
48
+ "devDependencies": {
49
+ "@types/jest": "^29.5.0",
50
+ "@types/node": "^20.0.0",
51
+ "@typescript-eslint/eslint-plugin": "^6.0.0",
52
+ "@typescript-eslint/parser": "^6.0.0",
53
+ "eslint": "^8.0.0",
54
+ "jest": "^29.5.0",
55
+ "ts-jest": "^29.1.0",
56
+ "typescript": "^5.0.0"
57
+ },
58
+ "dependencies": {
59
+ "uuid": "^9.0.0"
60
+ }
61
+ }
@@ -0,0 +1,213 @@
1
+ /**
2
+ * TankSync Sample Payload
3
+ *
4
+ * This is a sample payload.js that processes game item purchases.
5
+ * In production, this runs as a separate Node.js child process.
6
+ *
7
+ * Communication: JSON over stdin/stdout
8
+ *
9
+ * Input: {"id": "req-123", "method": "purchaseItem", "params": {...}}
10
+ * Output: {"id": "req-123", "success": true, "result": {...}}
11
+ */
12
+
13
+ 'use strict';
14
+
15
+ const readline = require('readline');
16
+ const crypto = require('crypto');
17
+
18
+ // ── Mock Database ──────────────────────────────────────────────────────────
19
+ const mockDatabase = {
20
+ players: {
21
+ 'player-123': { playerId: 'player-123', balance: 10000, inventory: [] },
22
+ 'player-456': { playerId: 'player-456', balance: 5000, inventory: [] },
23
+ },
24
+ items: {
25
+ 'tank-size': { itemId: 'tank-size', name: 'Tank Size+', cost: 100, category: 'upgrade' },
26
+ 'armor-basic': { itemId: 'armor-basic', name: 'Armor Plating', cost: 200, category: 'defense' },
27
+ 'rapid-fire': { itemId: 'rapid-fire', name: 'Rapid Fire', cost: 250, category: 'weapon' },
28
+ 'speed-boost': { itemId: 'speed-boost', name: 'Speed Boost', cost: 200, category: 'mobility' },
29
+ },
30
+ };
31
+
32
+ // ── Setup stdin/stdout ─────────────────────────────────────────────────────
33
+ const rl = readline.createInterface({
34
+ input: process.stdin,
35
+ output: process.stdout,
36
+ terminal: false,
37
+ });
38
+
39
+ // ── Signal Ready ───────────────────────────────────────────────────────────
40
+ const ready = {
41
+ type: 'ready',
42
+ id: crypto.randomUUID(),
43
+ version: '1.0.0',
44
+ };
45
+ console.log(JSON.stringify(ready));
46
+
47
+ // ── Handle Requests ───────────────────────────────────────────────────────
48
+ rl.on('line', async (line) => {
49
+ try {
50
+ const request = JSON.parse(line);
51
+
52
+ if (!request.id || !request.method) {
53
+ sendError(request.id || 'unknown', 'INVALID_REQUEST', 'Missing id or method');
54
+ return;
55
+ }
56
+
57
+ // Route to handler
58
+ switch (request.method) {
59
+ case 'purchaseItem':
60
+ await handlePurchaseItem(request);
61
+ break;
62
+
63
+ case 'getPlayer':
64
+ await handleGetPlayer(request);
65
+ break;
66
+
67
+ case 'getItems':
68
+ await handleGetItems(request);
69
+ break;
70
+
71
+ default:
72
+ sendError(request.id, 'UNKNOWN_METHOD', `Unknown method: ${request.method}`);
73
+ }
74
+ } catch (error) {
75
+ console.error(`Parse error: ${error.message}`);
76
+ // Don't send error for unparseable input - just skip
77
+ }
78
+ });
79
+
80
+ // ── Handle: Purchase Item ──────────────────────────────────────────────────
81
+ async function handlePurchaseItem(request) {
82
+ try {
83
+ const { playerId, itemId, quantity, requestId } = request.params || {};
84
+
85
+ // Validation
86
+ if (!playerId || !itemId || !quantity || !requestId) {
87
+ return sendError(request.id, 'INVALID_PARAMS', 'Missing required parameters');
88
+ }
89
+
90
+ if (typeof quantity !== 'number' || quantity < 1) {
91
+ return sendError(request.id, 'INVALID_QUANTITY', 'Quantity must be positive number');
92
+ }
93
+
94
+ // Get player
95
+ const player = mockDatabase.players[playerId];
96
+ if (!player) {
97
+ return sendError(request.id, 'PLAYER_NOT_FOUND', `Player ${playerId} not found`);
98
+ }
99
+
100
+ // Get item
101
+ const item = mockDatabase.items[itemId];
102
+ if (!item) {
103
+ return sendError(request.id, 'ITEM_NOT_FOUND', `Item ${itemId} not found`);
104
+ }
105
+
106
+ // Calculate total cost
107
+ const totalCost = item.cost * quantity;
108
+
109
+ // Check balance
110
+ if (player.balance < totalCost) {
111
+ return sendError(
112
+ request.id,
113
+ 'INSUFFICIENT_BALANCE',
114
+ `Not enough balance. Have ${player.balance}, need ${totalCost}`
115
+ );
116
+ }
117
+
118
+ // Process purchase
119
+ player.balance -= totalCost;
120
+ player.inventory.push({ itemId, quantity, purchasedAt: Date.now() });
121
+
122
+ // Generate transaction ID
123
+ const transactionId = `tx-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
124
+
125
+ // Send success response
126
+ sendSuccess(request.id, {
127
+ transactionId,
128
+ itemId,
129
+ quantity,
130
+ playerId,
131
+ cost: totalCost,
132
+ newBalance: player.balance,
133
+ });
134
+
135
+ // Log (to stderr - for monitoring)
136
+ console.error(`[PURCHASE] ${playerId} bought ${quantity}x ${itemId} (${totalCost} coins)`);
137
+ } catch (error) {
138
+ sendError(request.id, 'PURCHASE_ERROR', error.message);
139
+ }
140
+ }
141
+
142
+ // ── Handle: Get Player ─────────────────────────────────────────────────────
143
+ async function handleGetPlayer(request) {
144
+ try {
145
+ const { playerId } = request.params || {};
146
+
147
+ if (!playerId) {
148
+ return sendError(request.id, 'INVALID_PARAMS', 'Missing playerId');
149
+ }
150
+
151
+ const player = mockDatabase.players[playerId];
152
+ if (!player) {
153
+ return sendError(request.id, 'PLAYER_NOT_FOUND', `Player ${playerId} not found`);
154
+ }
155
+
156
+ sendSuccess(request.id, {
157
+ playerId: player.playerId,
158
+ balance: player.balance,
159
+ inventoryCount: player.inventory.length,
160
+ });
161
+ } catch (error) {
162
+ sendError(request.id, 'GET_PLAYER_ERROR', error.message);
163
+ }
164
+ }
165
+
166
+ // ── Handle: Get Items ──────────────────────────────────────────────────────
167
+ async function handleGetItems(request) {
168
+ try {
169
+ const items = Object.values(mockDatabase.items).map((item) => ({
170
+ itemId: item.itemId,
171
+ name: item.name,
172
+ cost: item.cost,
173
+ category: item.category,
174
+ }));
175
+
176
+ sendSuccess(request.id, { items, count: items.length });
177
+ } catch (error) {
178
+ sendError(request.id, 'GET_ITEMS_ERROR', error.message);
179
+ }
180
+ }
181
+
182
+ // ── Response Helpers ──────────────────────────────────────────────────────
183
+ function sendSuccess(id, result) {
184
+ const response = {
185
+ id,
186
+ success: true,
187
+ result,
188
+ };
189
+ console.log(JSON.stringify(response));
190
+ }
191
+
192
+ function sendError(id, error, message) {
193
+ const response = {
194
+ id,
195
+ success: false,
196
+ error,
197
+ message,
198
+ };
199
+ console.log(JSON.stringify(response));
200
+ }
201
+
202
+ // ── Graceful Shutdown ──────────────────────────────────────────────────────
203
+ process.on('SIGTERM', () => {
204
+ console.error('[INFO] Received SIGTERM, shutting down gracefully...');
205
+ rl.close();
206
+ process.exit(0);
207
+ });
208
+
209
+ process.on('SIGINT', () => {
210
+ console.error('[INFO] Received SIGINT, shutting down gracefully...');
211
+ rl.close();
212
+ process.exit(0);
213
+ });