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 +35 -0
- package/CHANGELOG.md +84 -0
- package/LICENSE +21 -0
- package/README.md +392 -0
- package/package.json +61 -0
- package/sample-payload.js +213 -0
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
|
+
});
|