add keycrow feature ideas, exclude features/ from biome

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-10 17:03:13 +01:00
parent 1b5cff78e2
commit f29332f3dd
19 changed files with 6959 additions and 1 deletions

View File

@@ -1,7 +1,7 @@
{ {
"$schema": "https://biomejs.dev/schemas/1.9.0/schema.json", "$schema": "https://biomejs.dev/schemas/1.9.0/schema.json",
"files": { "files": {
"ignore": ["src/client/routeTree.gen.ts", "drizzle/"] "ignore": ["src/client/routeTree.gen.ts", "drizzle/", "dist/", "features/"]
}, },
"organizeImports": { "organizeImports": {
"enabled": true "enabled": true

View File

@@ -0,0 +1,80 @@
# KeyCrow - Steam Key Trading Platform with Escrow
Technical foundation for a automated Steam key trading platform with escrow system.
## Architecture Overview
```
┌─────────────────────────────────────────────────────────────┐
│ Client/App │
└──────────────────────────┬──────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Backend API (Express) │
├─────────────────────────────────────────────────────────────┤
│ Routes: auth | listings | transactions | theoretical │
└──────┬──────────────┬──────────────────┬───────────────────┘
│ │ │
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────────────────┐
│ Store │ │ Encryption │ │ Services │
│ (In-Memory) │ │ Service │ │ - PaymentProvider (Mock) │
│ │ │ (AES) │ │ - KeyActivationProvider │
└──────────────┘ └──────────────┘ └──────────────────────────┘
```
## What's Implemented
### Realistic Flow (Production-Ready Pattern)
1. **Seller** creates a listing with encrypted Steam key
2. **Buyer** purchases via escrow (payment held)
3. **Platform** delivers decrypted key to buyer
4. **Buyer** confirms key works → money released to seller
5. **Buyer** reports failure → dispute, refund initiated
### Theoretica/Ideal Flow (Mock Only)
- Automated server-side key activation on buyer's Steam account
- **DISABLED by default** - requires `ALLOW_THEORETICAL_ACTIVATION=true`
- Clearly marked as potentially violating Steam ToS
## API Endpoints
### Authentication
- `POST /auth/register` - Register user
- `GET /auth/me` - Get current user
- `POST /auth/auth/steam/login` - Steam login (mock)
### Listings
- `POST /listings` - Create listing
- `GET /listings` - Get active listings
- `GET /listings/:id` - Get listing by ID
- `GET /listings/seller/me` - Get seller's listings
- `DELETE /listings/:id` - Cancel listing
### Transactions
- `POST /transactions` - Create purchase (escrow hold)
- `GET /transactions/:id` - Get transaction
- `GET /transactions/:id/key` - Get decrypted key (buyer only)
- `POST /transactions/:id/confirm` - Confirm key works/failed
- `GET /transactions/buyer/me` - Buyer's transactions
- `GET /transactions/seller/me` - Seller's transactions
### Theoretical (Mock)
- `POST /theoretical/activate` - Attempt automated activation
## Environment Variables
```bash
PORT=3000
ENCRYPTION_KEY=your-256-bit-key
STEAM_API_KEY=your-steam-api-key
STEAM_REDIRECT_URI=http://localhost:3000/auth/steam/callback
ALLOW_THEORETICAL_ACTIVATION=false
```
## Legal Notice
This implementation is a **technical proof-of-concept**. Automated Steam key activation is likely to violate Steam's Terms of Service unless you have an official partnership with Valve.
The "theoretical" module is clearly marked and disabled by default. Use at your own risk.

5617
features/keycrow/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,34 @@
{
"name": "keycrow-opencode",
"version": "2026.02.18",
"description": "Steam Key Trading Platform with Escrow - Technical Foundation",
"main": "dist/index.js",
"scripts": {
"build": "tsc && cp -r src/gui dist/",
"start": "node dist/index.js",
"dev": "ts-node src/index.ts",
"test": "jest"
},
"dependencies": {
"bcrypt": "^5.1.1",
"cors": "^2.8.5",
"crypto-js": "^4.2.0",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"uuid": "^9.0.0"
},
"devDependencies": {
"@types/bcrypt": "^5.0.2",
"@types/cors": "^2.8.17",
"@types/crypto-js": "^4.2.1",
"@types/express": "^4.17.21",
"@types/jest": "^29.5.11",
"@types/node": "^20.10.0",
"@types/supertest": "^6.0.3",
"@types/uuid": "^9.0.7",
"jest": "^29.7.0",
"supertest": "^7.2.2",
"ts-node": "^10.9.2",
"typescript": "^5.3.2"
}
}

View File

@@ -0,0 +1,16 @@
export const config = {
port: process.env.PORT ? parseInt(process.env.PORT) : 3000,
encryption: {
key: process.env.ENCRYPTION_KEY || 'default-dev-key-change-in-prod',
},
steam: {
apiKey: process.env.STEAM_API_KEY || '',
redirectUri: process.env.STEAM_REDIRECT_URI || 'http://localhost:3000/auth/steam/callback',
},
escrow: {
holdDurationDays: 7,
},
features: {
allowTheoreticalActivation: process.env.ALLOW_THEORETICAL_ACTIVATION === 'true',
},
};

View File

@@ -0,0 +1,246 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>KeyCrow Admin</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; padding: 20px; }
.container { max-width: 1200px; margin: 0 auto; }
h1 { color: #333; margin-bottom: 20px; }
h2 { color: #555; margin: 20px 0 10px; }
.card { background: white; border-radius: 8px; padding: 20px; margin-bottom: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.form-group { margin-bottom: 15px; }
label { display: block; margin-bottom: 5px; color: #666; }
input, select { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; }
button { background: #007AFF; color: white; border: none; padding: 12px 20px; border-radius: 6px; cursor: pointer; font-size: 14px; }
button:hover { background: #0056b3; }
button.secondary { background: #6c757d; }
button.danger { background: #dc3545; }
button.success { background: #28a745; }
table { width: 100%; border-collapse: collapse; }
th, td { padding: 12px; text-align: left; border-bottom: 1px solid #eee; }
th { background: #f8f9fa; font-weight: 600; }
.status { display: inline-block; padding: 4px 8px; border-radius: 4px; font-size: 12px; }
.status.ACTIVE { background: #d4edda; color: #155724; }
.status.SOLD { background: #cce5ff; color: #004085; }
.status.HELD { background: #fff3cd; color: #856404; }
.status.RELEASED { background: #d4edda; color: #155724; }
.status.REFUNDED { background: #f8d7da; color: #721c24; }
.status.PENDING { background: #fff3cd; color: #856404; }
.tabs { display: flex; gap: 10px; margin-bottom: 20px; }
.tab { padding: 10px 20px; background: white; border: 1px solid #ddd; border-radius: 6px; cursor: pointer; }
.tab.active { background: #007AFF; color: white; border-color: #007AFF; }
.tab-content { display: none; }
.tab-content.active { display: block; }
.alert { padding: 15px; border-radius: 6px; margin-bottom: 15px; }
.alert.error { background: #f8d7da; color: #721c24; }
.alert.success { background: #d4edda; color: #155724; }
.user-info { background: #e9ecef; padding: 10px; border-radius: 4px; margin-bottom: 20px; }
pre { background: #f8f9fa; padding: 10px; border-radius: 4px; overflow-x: auto; font-size: 12px; }
</style>
</head>
<body>
<div class="container">
<h1>KeyCrow Admin</h1>
<div class="card">
<h2>Session</h2>
<div class="form-group">
<label>User ID (for testing)</label>
<input type="text" id="userId" placeholder="Enter user ID from registration">
</div>
<button onclick="setUser()">Set User</button>
<span id="currentUser" style="margin-left: 15px; color: #666;"></span>
</div>
<div class="tabs">
<div class="tab active" onclick="showTab('listings')">Listings</div>
<div class="tab" onclick="showTab('transactions')">Transactions</div>
<div class="tab" onclick="showTab('create')">Create Listing</div>
</div>
<div id="listings" class="tab-content active">
<div class="card">
<h2>Active Listings</h2>
<button class="secondary" onclick="loadListings()">Refresh</button>
<table style="margin-top: 15px;">
<thead>
<tr><th>ID</th><th>Game</th><th>Platform</th><th>Price</th><th>Status</th><th>Actions</th></tr>
</thead>
<tbody id="listingsTable"></tbody>
</table>
</div>
</div>
<div id="transactions" class="tab-content">
<div class="card">
<h2>Transactions</h2>
<button class="secondary" onclick="loadTransactions()">Refresh</button>
<table style="margin-top: 15px;">
<thead>
<tr><th>ID</th><th>Listing</th><th>Amount</th><th>Escrow</th><th>Status</th><th>Key</th><th>Confirm</th></tr>
</thead>
<tbody id="transactionsTable"></tbody>
</table>
</div>
</div>
<div id="create" class="tab-content">
<div class="card">
<h2>Create New Listing</h2>
<div class="form-group">
<label>Game Title</label>
<input type="text" id="gameTitle" placeholder="e.g., Cyberpunk 2077">
</div>
<div class="form-group">
<label>Platform</label>
<select id="platform">
<option value="STEAM">Steam</option>
<option value="GOG">GOG</option>
<option value="EPIC">Epic</option>
</select>
</div>
<div class="form-group">
<label>Price</label>
<input type="number" id="price" step="0.01" placeholder="9.99">
</div>
<div class="form-group">
<label>Steam Key</label>
<input type="text" id="key" placeholder="XXXX-XXXX-XXXX">
</div>
<button onclick="createListing()">Create Listing</button>
</div>
</div>
<div id="alert" class="alert" style="display: none;"></div>
</div>
<script>
let currentUser = null;
function showTab(tabId) {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(t => t.classList.remove('active'));
event.target.classList.add('active');
document.getElementById(tabId).classList.add('active');
}
function showAlert(message, type = 'success') {
const alert = document.getElementById('alert');
alert.className = `alert ${type}`;
alert.textContent = message;
alert.style.display = 'block';
setTimeout(() => alert.style.display = 'none', 5000);
}
function setUser() {
currentUser = document.getElementById('userId').value;
if (currentUser) {
document.getElementById('currentUser').textContent = `Current user: ${currentUser.substring(0, 8)}...`;
}
}
async function api(method, endpoint, body = null) {
const options = { method, headers: { 'Content-Type': 'application/json' } };
if (currentUser) options.headers['x-user-id'] = currentUser;
if (body) options.body = JSON.stringify(body);
const res = await fetch(endpoint, options);
return res.json();
}
async function loadListings() {
const result = await api('GET', '/listings');
const tbody = document.getElementById('listingsTable');
if (!result.data?.listings?.length) {
tbody.innerHTML = '<tr><td colspan="6">No listings found</td></tr>';
return;
}
tbody.innerHTML = result.data.listings.map(l => `
<tr>
<td>${l.id.substring(0, 8)}...</td>
<td>${l.gameTitle}</td>
<td>${l.platform}</td>
<td>${l.price} ${l.currency}</td>
<td><span class="status ${l.status}">${l.status}</span></td>
<td><button class="secondary" onclick="deleteListing('${l.id}')">Cancel</button></td>
</tr>
`).join('');
}
async function createListing() {
const data = {
gameTitle: document.getElementById('gameTitle').value,
platform: document.getElementById('platform').value,
price: parseFloat(document.getElementById('price').value),
currency: 'EUR',
key: document.getElementById('key').value
};
const result = await api('POST', '/listings', data);
if (result.success) {
showAlert('Listing created!');
document.getElementById('gameTitle').value = '';
document.getElementById('price').value = '';
document.getElementById('key').value = '';
loadListings();
} else {
showAlert(result.error || 'Failed to create listing', 'error');
}
}
async function deleteListing(id) {
if (!confirm('Cancel this listing?')) return;
const result = await api('DELETE', `/listings/${id}`);
if (result.success) {
showAlert('Listing cancelled');
loadListings();
} else {
showAlert(result.error, 'error');
}
}
async function loadTransactions() {
const result = await api('GET', '/transactions/buyer/me');
const tbody = document.getElementById('transactionsTable');
if (!result.data?.transactions?.length) {
tbody.innerHTML = '<tr><td colspan="7">No transactions found</td></tr>';
return;
}
tbody.innerHTML = result.data.transactions.map(t => `
<tr>
<td>${t.id.substring(0, 8)}...</td>
<td>${t.listingId.substring(0, 8)}...</td>
<td>${t.amount} ${t.currency}</td>
<td><span class="status ${t.escrowStatus}">${t.escrowStatus}</span></td>
<td><span class="status ${t.transactionStatus}">${t.transactionStatus}</span></td>
<td>${t.keyDelivered ? '✓' : '<button class="secondary" onclick="getKey(\'' + t.id + '\')">Get Key</button>'}</td>
<td>${t.transactionStatus === 'PENDING' ? '<button class="success" onclick="confirmTx(\'' + t.id + '\', \'SUCCESS\')">✓</button> <button class="danger" onclick="confirmTx(\'' + t.id + '\', \'FAILED\')">✗</button>' : '-'}</td>
</tr>
`).join('');
}
async function getKey(txId) {
const result = await api('GET', `/transactions/${txId}/key`);
if (result.success && result.data.key) {
showAlert(`Key: ${result.data.key}`);
loadTransactions();
} else {
showAlert(result.error || 'Failed to get key', 'error');
}
}
async function confirmTx(txId, status) {
const result = await api('POST', `/transactions/${txId}/confirm`, { status });
if (result.success) {
showAlert(`Transaction ${status === 'SUCCESS' ? 'confirmed' : 'disputed'}`);
loadTransactions();
} else {
showAlert(result.error, 'error');
}
}
loadListings();
</script>
</body>
</html>

View File

@@ -0,0 +1,43 @@
import express, { Express } from 'express';
import cors from 'cors';
import path from 'path';
import { config } from './config';
import authRoutes from './routes/auth';
import listingsRoutes from './routes/listings';
import transactionsRoutes from './routes/transactions';
import theoreticalRoutes from './routes/theoretical';
const app: Express = express();
app.use(cors());
app.use(express.json());
app.use(express.static(path.join(__dirname, 'gui')));
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, 'gui', 'index.html'));
});
app.get('/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
app.use('/auth', authRoutes);
app.use('/listings', listingsRoutes);
app.use('/transactions', transactionsRoutes);
app.use('/theoretical', theoreticalRoutes);
app.use((err: Error, req: express.Request, res: express.Response, next: express.NextFunction) => {
console.error('Unhandled error:', err);
res.status(500).json({ success: false, error: 'Internal server error' });
});
const startServer = () => {
app.listen(config.port, () => {
console.log(`Server running on port ${config.port}`);
console.log(`Theoretical activation: ${config.features.allowTheoreticalActivation ? 'ENABLED' : 'DISABLED'}`);
});
};
startServer();
export default app;

View File

@@ -0,0 +1,50 @@
import { Request, Response, NextFunction } from 'express';
import { store } from '../services/Store';
declare global {
namespace Express {
interface Request {
userId?: string;
}
}
}
export interface AuthRequest extends Request {
userId?: string;
}
export const mockAuthMiddleware = (req: AuthRequest, res: Response, next: NextFunction) => {
const userId = req.headers['x-user-id'] as string;
if (!userId) {
return res.status(401).json({ success: false, error: 'Unauthorized' });
}
const user = store.getUserById(userId);
if (!user) {
return res.status(401).json({ success: false, error: 'User not found' });
}
req.userId = userId;
next();
};
export const requireSteamAuth = (req: AuthRequest, res: Response, next: NextFunction) => {
const userId = req.headers['x-user-id'] as string;
if (!userId) {
return res.status(401).json({ success: false, error: 'Unauthorized' });
}
const user = store.getUserById(userId);
if (!user) {
return res.status(401).json({ success: false, error: 'User not found' });
}
if (!user.steamId) {
return res.status(403).json({ success: false, error: 'Steam authentication required' });
}
req.userId = userId;
next();
};

View File

@@ -0,0 +1,65 @@
export interface User {
id: string;
steamId?: string;
username: string;
email: string;
createdAt: Date;
updatedAt: Date;
}
export type ListingStatus = 'ACTIVE' | 'SOLD' | 'CANCELLED' | 'EXPIRED';
export type Platform = 'STEAM' | 'GOG' | 'EPIC' | 'OTHER';
export interface Listing {
id: string;
sellerId: string;
gameTitle: string;
platform: Platform;
price: number;
currency: string;
keyEncrypted: string;
status: ListingStatus;
createdAt: Date;
updatedAt: Date;
}
export type EscrowStatus = 'PENDING' | 'HELD' | 'RELEASED' | 'REFUNDED';
export type TransactionStatus = 'PENDING' | 'COMPLETED' | 'DISPUTED' | 'CANCELLED';
export interface Transaction {
id: string;
listingId: string;
buyerId: string;
sellerId: string;
amount: number;
currency: string;
escrowStatus: EscrowStatus;
transactionStatus: TransactionStatus;
holdId?: string;
keyDelivered: boolean;
confirmedAt?: Date;
createdAt: Date;
updatedAt: Date;
}
export interface CreateListingDto {
gameTitle: string;
platform: Platform;
price: number;
currency?: string;
key: string;
}
export interface CreateTransactionDto {
listingId: string;
}
export interface ConfirmTransactionDto {
status: 'SUCCESS' | 'FAILED';
}
export interface ApiResponse<T = unknown> {
success: boolean;
data?: T;
error?: string;
}

View File

@@ -0,0 +1,75 @@
import { Router, Request, Response } from 'express';
import { store } from '../services/Store';
import { ApiResponse } from '../models';
const router = Router();
router.post('/register', (req: Request, res: Response) => {
const { username, email } = req.body;
if (!username || !email) {
return res.status(400).json({
success: false,
error: 'Username and email required'
});
}
const user = store.createUser({ username, email });
res.json({
success: true,
data: { user: { id: user.id, username: user.username, email: user.email } },
});
});
router.get('/me', (req: Request, res: Response) => {
const userId = req.headers['x-user-id'] as string;
if (!userId) {
return res.status(401).json({ success: false, error: 'Unauthorized' });
}
const user = store.getUserById(userId);
if (!user) {
return res.status(404).json({ success: false, error: 'User not found' });
}
res.json({
success: true,
data: { user },
});
});
router.post('/auth/steam/login', (req: Request, res: Response) => {
const { steamId, username } = req.body;
if (!steamId) {
return res.status(400).json({
success: false,
error: 'Steam ID required'
});
}
let user = store.getUserBySteamId(steamId);
if (!user) {
user = store.createUser({
username: username || `SteamUser_${steamId}`,
email: '',
steamId,
});
} else {
store.updateUser(user.id, { steamId });
}
res.json({
success: true,
data: {
user: { id: user.id, username: user.username, steamId: user.steamId },
message: 'Steam login successful (mock)',
},
});
});
export default router;

View File

@@ -0,0 +1,121 @@
import { Router, Request, Response } from 'express';
import { store } from '../services/Store';
import { encryptionService } from '../utils/encryption';
import { mockAuthMiddleware } from '../middleware/auth';
import { CreateListingDto } from '../models';
const router = Router();
router.post('/', mockAuthMiddleware, (req, res: Response) => {
const { gameTitle, platform, price, currency = 'EUR', key } = req.body as CreateListingDto;
if (!gameTitle || !platform || !price || !key) {
return res.status(400).json({
success: false,
error: 'gameTitle, platform, price, and key are required',
});
}
if (price <= 0) {
return res.status(400).json({
success: false,
error: 'Price must be greater than 0',
});
}
const keyEncrypted = encryptionService.encrypt(key);
const listing = store.createListing({
sellerId: req.userId!,
gameTitle,
platform,
price,
currency,
keyEncrypted,
});
res.json({
success: true,
data: {
listing: {
id: listing.id,
gameTitle: listing.gameTitle,
platform: listing.platform,
price: listing.price,
currency: listing.currency,
status: listing.status,
createdAt: listing.createdAt,
},
},
});
});
router.get('/', (req, res: Response) => {
const listings = store.getActiveListings();
const safeListings = listings.map(l => ({
id: l.id,
gameTitle: l.gameTitle,
platform: l.platform,
price: l.price,
currency: l.currency,
sellerId: l.sellerId,
status: l.status,
createdAt: l.createdAt,
}));
res.json({
success: true,
data: { listings: safeListings },
});
});
router.get('/:id', (req, res: Response) => {
const listing = store.getListingById(req.params.id);
if (!listing) {
return res.status(404).json({ success: false, error: 'Listing not found' });
}
res.json({
success: true,
data: { listing },
});
});
router.get('/seller/me', mockAuthMiddleware, (req, res: Response) => {
const listings = store.getListingsBySeller(req.userId!);
res.json({
success: true,
data: { listings },
});
});
router.delete('/:id', mockAuthMiddleware, (req, res: Response) => {
const listing = store.getListingById(req.params.id);
if (!listing) {
return res.status(404).json({ success: false, error: 'Listing not found' });
}
if (listing.sellerId !== req.userId) {
return res.status(403).json({ success: false, error: 'Forbidden' });
}
if (listing.status !== 'ACTIVE') {
return res.status(400).json({
success: false,
error: 'Can only cancel active listings'
});
}
store.updateListing(listing.id, { status: 'CANCELLED' });
res.json({
success: true,
data: { message: 'Listing cancelled' },
});
});
export default router;

View File

@@ -0,0 +1,35 @@
import { Router, Response } from 'express';
import { keyActivationProvider } from '../services/KeyActivationProvider';
import { mockAuthMiddleware, requireSteamAuth } from '../middleware/auth';
import { config } from '../config';
const router = Router();
router.post('/activate', requireSteamAuth, async (req, res: Response) => {
if (!config.features.allowTheoreticalActivation) {
return res.status(403).json({
success: false,
error: 'Automated key activation is disabled. This feature is for demonstration only.',
});
}
const { key } = req.body;
if (!key) {
return res.status(400).json({
success: false,
error: 'Key is required',
});
}
const user = req.userId;
const result = await keyActivationProvider.activateKey(user!, key);
res.json({
success: result.success,
data: result,
});
});
export default router;

View File

@@ -0,0 +1,225 @@
import { Router, Response } from 'express';
import { store } from '../services/Store';
import { paymentProvider } from '../services/PaymentProvider';
import { keyActivationProvider } from '../services/KeyActivationProvider';
import { encryptionService } from '../utils/encryption';
import { mockAuthMiddleware, requireSteamAuth } from '../middleware/auth';
import { config } from '../config';
import { CreateTransactionDto } from '../models';
const router = Router();
router.post('/', mockAuthMiddleware, async (req, res: Response) => {
const { listingId } = req.body as CreateTransactionDto;
if (!listingId) {
return res.status(400).json({
success: false,
error: 'listingId is required',
});
}
const listing = store.getListingById(listingId);
if (!listing) {
return res.status(404).json({ success: false, error: 'Listing not found' });
}
if (listing.status !== 'ACTIVE') {
return res.status(400).json({
success: false,
error: 'Listing is not available',
});
}
if (listing.sellerId === req.userId) {
return res.status(400).json({
success: false,
error: 'Cannot buy your own listing',
});
}
const holdResult = await paymentProvider.createHold(listing.price, listing.currency);
if (!holdResult.success) {
return res.status(400).json({
success: false,
error: holdResult.error || 'Payment failed',
});
}
const transaction = store.createTransaction({
listingId: listing.id,
buyerId: req.userId!,
sellerId: listing.sellerId,
amount: listing.price,
currency: listing.currency,
escrowStatus: 'HELD',
transactionStatus: 'PENDING',
holdId: holdResult.holdId,
keyDelivered: false,
});
store.updateListing(listing.id, { status: 'SOLD' });
res.json({
success: true,
data: {
transaction: {
id: transaction.id,
listingId: transaction.listingId,
amount: transaction.amount,
currency: transaction.currency,
escrowStatus: transaction.escrowStatus,
transactionStatus: transaction.transactionStatus,
},
},
});
});
router.get('/:id', mockAuthMiddleware, (req, res: Response) => {
const transaction = store.getTransactionById(req.params.id);
if (!transaction) {
return res.status(404).json({ success: false, error: 'Transaction not found' });
}
if (transaction.buyerId !== req.userId && transaction.sellerId !== req.userId) {
return res.status(403).json({ success: false, error: 'Forbidden' });
}
res.json({
success: true,
data: { transaction },
});
});
router.get('/:id/key', mockAuthMiddleware, (req, res: Response) => {
const transaction = store.getTransactionById(req.params.id);
if (!transaction) {
return res.status(404).json({ success: false, error: 'Transaction not found' });
}
if (transaction.buyerId !== req.userId) {
return res.status(403).json({ success: false, error: 'Forbidden' });
}
if (transaction.escrowStatus !== 'HELD') {
return res.status(400).json({
success: false,
error: 'Key only available when payment is held in escrow',
});
}
if (transaction.keyDelivered) {
return res.json({
success: true,
data: {
keyAlreadyDelivered: true,
message: 'Key was already delivered in this transaction',
},
});
}
const listing = store.getListingById(transaction.listingId);
if (!listing) {
return res.status(404).json({ success: false, error: 'Listing not found' });
}
const key = encryptionService.decrypt(listing.keyEncrypted);
store.updateTransaction(transaction.id, { keyDelivered: true });
res.json({
success: true,
data: { key },
});
});
router.post('/:id/confirm', requireSteamAuth, async (req, res: Response) => {
const { status } = req.body;
if (!status || !['SUCCESS', 'FAILED'].includes(status)) {
return res.status(400).json({
success: false,
error: 'status must be SUCCESS or FAILED',
});
}
const transaction = store.getTransactionById(req.params.id);
if (!transaction) {
return res.status(404).json({ success: false, error: 'Transaction not found' });
}
if (transaction.buyerId !== req.userId) {
return res.status(403).json({ success: false, error: 'Forbidden' });
}
if (transaction.transactionStatus !== 'PENDING') {
return res.status(400).json({
success: false,
error: 'Transaction already confirmed or disputed',
});
}
if (status === 'SUCCESS') {
if (transaction.holdId) {
await paymentProvider.release(transaction.holdId);
}
store.updateTransaction(transaction.id, {
escrowStatus: 'RELEASED',
transactionStatus: 'COMPLETED',
confirmedAt: new Date(),
});
res.json({
success: true,
data: {
message: 'Key confirmed. Payment released to seller.',
escrowStatus: 'RELEASED',
},
});
} else {
if (transaction.holdId) {
await paymentProvider.refund(transaction.holdId);
}
store.updateTransaction(transaction.id, {
escrowStatus: 'REFUNDED',
transactionStatus: 'DISPUTED',
confirmedAt: new Date(),
});
res.json({
success: true,
data: {
message: 'Key marked as failed. Payment refunded to buyer.',
escrowStatus: 'REFUNDED',
transactionStatus: 'DISPUTED',
},
});
}
});
router.get('/buyer/me', mockAuthMiddleware, (req, res: Response) => {
const transactions = store.getTransactionsByBuyer(req.userId!);
res.json({
success: true,
data: { transactions },
});
});
router.get('/seller/me', mockAuthMiddleware, (req, res: Response) => {
const transactions = store.getTransactionsBySeller(req.userId!);
res.json({
success: true,
data: { transactions },
});
});
export default router;

View File

@@ -0,0 +1,49 @@
export interface ActivationResult {
success: boolean;
error?: string;
productId?: string;
purchaseId?: string;
}
export interface KeyActivationProvider {
activateKey(steamId: string, key: string): Promise<ActivationResult>;
}
export class MockKeyActivationProvider implements KeyActivationProvider {
async activateKey(steamId: string, key: string): Promise<ActivationResult> {
await this.simulateDelay();
if (!key || key.length < 5) {
return {
success: false,
error: 'Invalid key format',
};
}
if (key.toLowerCase().includes('invalid')) {
return {
success: false,
error: 'Key has already been redeemed',
};
}
if (key.toLowerCase().includes('expired')) {
return {
success: false,
error: 'Key has expired',
};
}
return {
success: true,
productId: `prod_${Math.random().toString(36).substr(2, 9)}`,
purchaseId: `purch_${Math.random().toString(36).substr(2, 9)}`,
};
}
private simulateDelay(): Promise<void> {
return new Promise(resolve => setTimeout(resolve, 500));
}
}
export const keyActivationProvider = new MockKeyActivationProvider();

View File

@@ -0,0 +1,53 @@
export type PaymentResult = {
success: boolean;
holdId?: string;
error?: string;
};
export interface PaymentProvider {
createHold(amount: number, currency: string): Promise<PaymentResult>;
release(holdId: string): Promise<{ success: boolean; error?: string }>;
refund(holdId: string): Promise<{ success: boolean; error?: string }>;
}
export class MockPaymentProvider implements PaymentProvider {
private holds: Map<string, { amount: number; currency: string; released: boolean }> = new Map();
async createHold(amount: number, currency: string): Promise<PaymentResult> {
const holdId = `hold_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
this.holds.set(holdId, { amount, currency, released: false });
return {
success: true,
holdId,
};
}
async release(holdId: string): Promise<{ success: boolean; error?: string }> {
const hold = this.holds.get(holdId);
if (!hold) {
return { success: false, error: 'Hold not found' };
}
if (hold.released) {
return { success: false, error: 'Hold already released' };
}
hold.released = true;
return { success: true };
}
async refund(holdId: string): Promise<{ success: boolean; error?: string }> {
const hold = this.holds.get(holdId);
if (!hold) {
return { success: false, error: 'Hold not found' };
}
this.holds.delete(holdId);
return { success: true };
}
}
export const paymentProvider = new MockPaymentProvider();

View File

@@ -0,0 +1,103 @@
import { User, Listing, Transaction } from '../models';
import { v4 as uuidv4 } from 'uuid';
class InMemoryStore {
users: Map<string, User> = new Map();
listings: Map<string, Listing> = new Map();
transactions: Map<string, Transaction> = new Map();
createUser(data: Omit<User, 'id' | 'createdAt' | 'updatedAt'>): User {
const user: User = {
...data,
id: uuidv4(),
createdAt: new Date(),
updatedAt: new Date(),
};
this.users.set(user.id, user);
return user;
}
getUserById(id: string): User | undefined {
return this.users.get(id);
}
getUserBySteamId(steamId: string): User | undefined {
return Array.from(this.users.values()).find(u => u.steamId === steamId);
}
updateUser(id: string, data: Partial<User>): User | undefined {
const user = this.users.get(id);
if (!user) return undefined;
const updated = { ...user, ...data, updatedAt: new Date() };
this.users.set(id, updated);
return updated;
}
createListing(data: Omit<Listing, 'id' | 'status' | 'createdAt' | 'updatedAt'>): Listing {
const listing: Listing = {
...data,
id: uuidv4(),
status: 'ACTIVE',
createdAt: new Date(),
updatedAt: new Date(),
};
this.listings.set(listing.id, listing);
return listing;
}
getListingById(id: string): Listing | undefined {
return this.listings.get(id);
}
getActiveListings(): Listing[] {
return Array.from(this.listings.values()).filter(l => l.status === 'ACTIVE');
}
getListingsBySeller(sellerId: string): Listing[] {
return Array.from(this.listings.values()).filter(l => l.sellerId === sellerId);
}
updateListing(id: string, data: Partial<Listing>): Listing | undefined {
const listing = this.listings.get(id);
if (!listing) return undefined;
const updated = { ...listing, ...data, updatedAt: new Date() };
this.listings.set(id, updated);
return updated;
}
createTransaction(data: Omit<Transaction, 'id' | 'createdAt' | 'updatedAt'>): Transaction {
const transaction: Transaction = {
...data,
id: uuidv4(),
createdAt: new Date(),
updatedAt: new Date(),
};
this.transactions.set(transaction.id, transaction);
return transaction;
}
getTransactionById(id: string): Transaction | undefined {
return this.transactions.get(id);
}
getTransactionsByBuyer(buyerId: string): Transaction[] {
return Array.from(this.transactions.values()).filter(t => t.buyerId === buyerId);
}
getTransactionsBySeller(sellerId: string): Transaction[] {
return Array.from(this.transactions.values()).filter(t => t.sellerId === sellerId);
}
updateTransaction(id: string, data: Partial<Transaction>): Transaction | undefined {
const transaction = this.transactions.get(id);
if (!transaction) return undefined;
const updated = { ...transaction, ...data, updatedAt: new Date() };
this.transactions.set(id, updated);
return updated;
}
}
export const store = new InMemoryStore();

View File

@@ -0,0 +1,33 @@
import CryptoJS from 'crypto-js';
import { config } from '../config';
export class EncryptionService {
private static instance: EncryptionService;
private readonly key: string;
private constructor() {
this.key = config.encryption.key;
}
static getInstance(): EncryptionService {
if (!EncryptionService.instance) {
EncryptionService.instance = new EncryptionService();
}
return EncryptionService.instance;
}
encrypt(plainText: string): string {
return CryptoJS.AES.encrypt(plainText, this.key).toString();
}
decrypt(cipherText: string): string {
const bytes = CryptoJS.AES.decrypt(cipherText, this.key);
return bytes.toString(CryptoJS.enc.Utf8);
}
hash(data: string): string {
return CryptoJS.SHA256(data).toString();
}
}
export const encryptionService = EncryptionService.getInstance();

View File

@@ -0,0 +1,94 @@
import request from 'supertest';
import app from '../src/index';
describe('KeyCrow API', () => {
let sellerId: string;
let buyerId: string;
let listingId: string;
let transactionId: string;
const testKey = 'ABCD-EFGH-IJKL-MNOP';
describe('Auth Flow', () => {
it('should register a seller', async () => {
const res = await request(app)
.post('/auth/register')
.send({ username: 'seller1', email: 'seller@test.com' });
expect(res.status).toBe(200);
expect(res.body.success).toBe(true);
sellerId = res.body.data.user.id;
});
it('should register a buyer with steam', async () => {
const res = await request(app)
.post('/auth/auth/steam/login')
.send({ steamId: '76561198000000001', username: 'buyer1' });
expect(res.status).toBe(200);
expect(res.body.success).toBe(true);
buyerId = res.body.data.user.id;
});
});
describe('Listings Flow', () => {
it('should create a listing', async () => {
const res = await request(app)
.post('/listings')
.set('x-user-id', sellerId)
.send({
gameTitle: 'Test Game',
platform: 'STEAM',
price: 9.99,
currency: 'EUR',
key: testKey,
});
expect(res.status).toBe(200);
expect(res.body.success).toBe(true);
listingId = res.body.data.listing.id;
});
it('should get active listings', async () => {
const res = await request(app).get('/listings');
expect(res.status).toBe(200);
expect(res.body.success).toBe(true);
expect(res.body.data.listings.length).toBeGreaterThan(0);
});
});
describe('Transaction Flow (Escrow)', () => {
it('should create a purchase with escrow hold', async () => {
const res = await request(app)
.post('/transactions')
.set('x-user-id', buyerId)
.send({ listingId });
expect(res.status).toBe(200);
expect(res.body.success).toBe(true);
expect(res.body.data.transaction.escrowStatus).toBe('HELD');
transactionId = res.body.data.transaction.id;
});
it('should deliver key to buyer', async () => {
const res = await request(app)
.get(`/transactions/${transactionId}/key`)
.set('x-user-id', buyerId);
expect(res.status).toBe(200);
expect(res.body.success).toBe(true);
expect(res.body.data.key).toBe(testKey);
});
it('should confirm success and release escrow', async () => {
const res = await request(app)
.post(`/transactions/${transactionId}/confirm`)
.set('x-user-id', buyerId)
.send({ status: 'SUCCESS' });
expect(res.status).toBe(200);
expect(res.body.success).toBe(true);
expect(res.body.data.escrowStatus).toBe('RELEASED');
});
});
});

View File

@@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "tests"]
}