469 lines
18 KiB
Markdown
469 lines
18 KiB
Markdown
|
|
# VUKeyServerIntegration Component
|
||
|
|
|
||
|
|
## Overview
|
||
|
|
The VUKeyServerIntegration component handles license key retrieval and syncing for VoiceUwu in an **offline-first** architecture. The app functions completely offline, with the keyserver only used for purchasing and retrieving keys associated with the user's account.
|
||
|
|
|
||
|
|
## Architecture (Offline-First)
|
||
|
|
|
||
|
|
```
|
||
|
|
┌─────────────────────────────────┐ ┌─────────────────┐ ┌─────────────────────────────────┐
|
||
|
|
│ iOS App │ │ Your Server │ │ Android App │
|
||
|
|
│ (Offline-First) │ │ (KeyServer) │ │ (Offline-First) │
|
||
|
|
│ │ │ │ │ │
|
||
|
|
│ ┌─────────────────────────────┐ │ │ │ │ ┌─────────────────────────────┐ │
|
||
|
|
│ │ Local Key Storage │ │ │ │ │ │ Local Key Storage │ │
|
||
|
|
│ │ - Encrypted in Keychain │ │ │ │ │ │ - Encrypted locally │ │
|
||
|
|
│ │ - Full offline validation │ │ │ │ │ │ - Full offline validation │ │
|
||
|
|
│ │ - No network required │ │ │ │ │ │ - No network required │ │
|
||
|
|
│ └─────────────────────────────┘ │ │ │ │ └─────────────────────────────┘ │
|
||
|
|
│ │ │ │ │ │
|
||
|
|
│ ┌─────────────────────────────┐ │ │ │ │ ┌─────────────────────────────┐ │
|
||
|
|
│ │ Keys for All Feature │ │ │ │ │ │ Keys for All Feature │ │
|
||
|
|
│ │ - Auto-check on open │ │ │ │ │ │ - Auto-check on open │ │
|
||
|
|
│ │ - Manual sync button │ │ │ │ │ │ - Manual sync button │ │
|
||
|
|
│ │ - Shows available keys │ │ │ │ │ │ - Shows available keys │ │
|
||
|
|
│ └─────────────────────────────┘ │ │ │ │ └─────────────────────────────┘ │
|
||
|
|
└─────────────────────────────────┘ └─────────────────┘ └─────────────────────────────────┘
|
||
|
|
│ │ │
|
||
|
|
│ Sync: Get user's keys │ │
|
||
|
|
│──────────────────────────────→│ │
|
||
|
|
│ │ │
|
||
|
|
│ Response: [key1, key2, ...] │ │
|
||
|
|
│←──────────────────────────────│ │
|
||
|
|
│ │ │
|
||
|
|
│ │ Sync: Get user's keys │
|
||
|
|
│ │←──────────────────────────────│
|
||
|
|
│ │ │
|
||
|
|
│ │ Response: [key1, key2, ...] │
|
||
|
|
│ │──────────────────────────────→│
|
||
|
|
```
|
||
|
|
|
||
|
|
## Components
|
||
|
|
|
||
|
|
### 1. VUKeyServerClient
|
||
|
|
**Purpose**: Client for keyserver communication (purchase, wallet management, feature unlocking)
|
||
|
|
**Platform**: Cross-platform interface supporting multiple apps
|
||
|
|
|
||
|
|
```swift
|
||
|
|
protocol VUKeyServerClient {
|
||
|
|
// Multi-platform key pack purchasing
|
||
|
|
func purchaseKeyPack(pack: KeyPack, via platform: Platform) async throws -> KeyPurchaseResult
|
||
|
|
|
||
|
|
// Wallet management
|
||
|
|
func getWalletBalance(userId: String) async throws -> Int
|
||
|
|
func spendKeys(amount: Int, feature: String, app: String, userId: String) async throws -> SpendResult
|
||
|
|
func syncWallet(userId: String) async throws -> WalletState
|
||
|
|
|
||
|
|
// App-specific features
|
||
|
|
func getAppFeatures(appId: String) async throws -> [AppFeature]
|
||
|
|
func unlockFeature(featureId: String, appId: String, userId: String) async throws -> UnlockResult
|
||
|
|
func getUserFeatures(userId: String, appId: String) async throws -> [String]
|
||
|
|
|
||
|
|
// Community features
|
||
|
|
func donateKeys(amount: Int, userId: String) async throws -> DonationResult
|
||
|
|
func shareKeys(amount: Int, recipientEmail: String, userId: String) async throws -> ShareResult
|
||
|
|
}
|
||
|
|
|
||
|
|
enum Platform {
|
||
|
|
case apple, google, stripe, paypal, steam
|
||
|
|
}
|
||
|
|
|
||
|
|
struct KeyPurchaseResult {
|
||
|
|
let keysAdded: Int
|
||
|
|
let bonusKeys: Int
|
||
|
|
let communityDonated: Int
|
||
|
|
let transactionId: String
|
||
|
|
}
|
||
|
|
|
||
|
|
struct SpendResult {
|
||
|
|
let remainingBalance: Int
|
||
|
|
let featureUnlocked: Bool
|
||
|
|
let transactionId: String
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 2. Platform-Specific Purchase Managers
|
||
|
|
|
||
|
|
#### VUAppleStoreKitManager
|
||
|
|
**Purpose**: Handle Apple App Store purchases (iOS/macOS)
|
||
|
|
**Platform**: iOS/macOS
|
||
|
|
|
||
|
|
```swift
|
||
|
|
class VUAppleStoreKitManager {
|
||
|
|
func purchaseProduct(_ productId: String) async throws -> StoreKit.Transaction
|
||
|
|
func restorePurchases() async throws -> [StoreKit.Transaction]
|
||
|
|
func sendTransactionToKeyServer(_ transaction: StoreKit.Transaction) async throws -> [VULicenseKey]
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
#### VUGooglePlayManager
|
||
|
|
**Purpose**: Handle Google Play purchases (Android)
|
||
|
|
**Platform**: Android
|
||
|
|
|
||
|
|
```swift
|
||
|
|
class VUGooglePlayManager {
|
||
|
|
func purchaseProduct(_ productId: String) async throws -> GooglePlayPurchase
|
||
|
|
func sendPurchaseToKeyServer(_ purchase: GooglePlayPurchase) async throws -> [VULicenseKey]
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
#### VUStripeManager
|
||
|
|
**Purpose**: Handle Stripe payments (Web/Direct)
|
||
|
|
**Platform**: Web/Cross-platform
|
||
|
|
|
||
|
|
```swift
|
||
|
|
class VUStripeManager {
|
||
|
|
func createPaymentIntent(amount: Int, email: String) async throws -> String
|
||
|
|
func confirmPaymentAndGetKeys(_ paymentIntentId: String) async throws -> [VULicenseKey]
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
#### VUPayPalManager
|
||
|
|
**Purpose**: Handle PayPal payments (Web/Direct)
|
||
|
|
**Platform**: Web/Cross-platform
|
||
|
|
|
||
|
|
```swift
|
||
|
|
class VUPayPalManager {
|
||
|
|
func createPayment(amount: Double, email: String) async throws -> String
|
||
|
|
func executePaymentAndGetKeys(_ paymentId: String) async throws -> [VULicenseKey]
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
#### VUSteamManager
|
||
|
|
**Purpose**: Handle Steam purchases (PC Gaming)
|
||
|
|
**Platform**: PC/Steam
|
||
|
|
|
||
|
|
```swift
|
||
|
|
class VUSteamManager {
|
||
|
|
func initiateSteamPurchase(_ productId: String) async throws -> String
|
||
|
|
func validateSteamPurchaseAndGetKeys(_ transactionId: String) async throws -> [VULicenseKey]
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 3. VUKeysForAllSync
|
||
|
|
**Purpose**: Handles the "Keys for All" feature sync functionality
|
||
|
|
**Platform**: iOS/Android
|
||
|
|
|
||
|
|
```swift
|
||
|
|
class VUKeysForAllSync {
|
||
|
|
private let keyServerClient: VUKeyServerClient
|
||
|
|
private let keyStorage: VUKeyStorage
|
||
|
|
|
||
|
|
// Auto-sync when feature is opened
|
||
|
|
func autoSyncOnOpen() async throws -> [VULicenseKey]
|
||
|
|
|
||
|
|
// Manual sync via button
|
||
|
|
func manualSync() async throws -> [VULicenseKey]
|
||
|
|
|
||
|
|
// Check if user has account/email for syncing
|
||
|
|
func canSync() -> Bool
|
||
|
|
|
||
|
|
// Store retrieved keys locally
|
||
|
|
func storeRetrievedKeys(_ keys: [VULicenseKey]) async throws
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## Data Models
|
||
|
|
|
||
|
|
### VULicenseKey
|
||
|
|
```swift
|
||
|
|
struct VULicenseKey: Codable {
|
||
|
|
let keyValue: String // The actual license key (e.g., "VUUW-ABCD-1234-WXYZ-L2")
|
||
|
|
let planType: VUPlanType // Level 1, Level 2, etc.
|
||
|
|
let purchaseDate: Date // When it was purchased
|
||
|
|
let serverMetadata: [String: Any]? // Optional server data
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### VUUserKeySync
|
||
|
|
```swift
|
||
|
|
struct VUUserKeySync: Codable {
|
||
|
|
let userEmail: String
|
||
|
|
let availableKeys: [VULicenseKey]
|
||
|
|
let syncTimestamp: Date
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## Integration Flows
|
||
|
|
|
||
|
|
### Purchase Flow (Apple - iOS/macOS)
|
||
|
|
1. User initiates purchase in iOS/macOS app
|
||
|
|
2. StoreKit 2 handles payment with Apple
|
||
|
|
3. App receives JWS-signed transaction
|
||
|
|
4. App sends transaction to keyserver via `purchaseWithApple()`
|
||
|
|
5. Keyserver validates JWS signature and generates keys
|
||
|
|
6. Keyserver returns generated keys to app
|
||
|
|
7. App stores keys locally using KeyStorage
|
||
|
|
|
||
|
|
### Purchase Flow (Google Play - Android)
|
||
|
|
1. User initiates purchase in Android app
|
||
|
|
2. Google Play Billing handles payment
|
||
|
|
3. App receives purchase token
|
||
|
|
4. App sends purchase token to keyserver via `purchaseWithGoogle()`
|
||
|
|
5. Keyserver validates with Google Play API and generates keys
|
||
|
|
6. Keyserver returns generated keys to app
|
||
|
|
7. App stores keys locally
|
||
|
|
|
||
|
|
### Purchase Flow (Stripe - Web/Direct)
|
||
|
|
1. User completes payment on website/web app
|
||
|
|
2. Stripe processes payment and returns payment intent ID
|
||
|
|
3. Client calls `purchaseWithStripe()` with payment intent and email
|
||
|
|
4. Keyserver validates with Stripe API and generates keys
|
||
|
|
5. Keyserver returns generated keys
|
||
|
|
6. Keys stored locally in app
|
||
|
|
|
||
|
|
### Purchase Flow (PayPal - Web/Direct)
|
||
|
|
1. User completes payment via PayPal
|
||
|
|
2. PayPal processes payment and returns payment ID
|
||
|
|
3. Client calls `purchaseWithPayPal()` with payment ID and email
|
||
|
|
4. Keyserver validates with PayPal API and generates keys
|
||
|
|
5. Keyserver returns generated keys
|
||
|
|
6. Keys stored locally in app
|
||
|
|
|
||
|
|
### Purchase Flow (Steam - PC Gaming)
|
||
|
|
1. User purchases through Steam store
|
||
|
|
2. Steam processes payment and provides transaction ID
|
||
|
|
3. Steam client calls `purchaseWithSteam()` with Steam ID and transaction
|
||
|
|
4. Keyserver validates with Steam Web API and generates keys
|
||
|
|
5. Keyserver returns generated keys
|
||
|
|
6. Keys stored locally in Steam client/game
|
||
|
|
|
||
|
|
### Keys for All Feature Flow
|
||
|
|
1. User opens "Keys for All" feature
|
||
|
|
2. App calls `autoSyncOnOpen()` which:
|
||
|
|
- Checks if user has email/account configured
|
||
|
|
- Calls `syncUserKeys()` if available
|
||
|
|
- Compares server keys with local keys
|
||
|
|
- Stores any new keys locally
|
||
|
|
3. User can tap "Sync" button to trigger `manualSync()`
|
||
|
|
4. All keys stored locally work offline immediately
|
||
|
|
|
||
|
|
## Server-Side Integration
|
||
|
|
|
||
|
|
### Endpoints
|
||
|
|
```
|
||
|
|
# Key pack purchase endpoints
|
||
|
|
POST /api/v1/keys/purchase/apple # Process Apple key pack purchase
|
||
|
|
POST /api/v1/keys/purchase/google # Process Google Play key pack purchase
|
||
|
|
POST /api/v1/keys/purchase/stripe # Process Stripe key pack payment
|
||
|
|
POST /api/v1/keys/purchase/paypal # Process PayPal key pack payment
|
||
|
|
POST /api/v1/keys/purchase/steam # Process Steam key pack purchase
|
||
|
|
|
||
|
|
# Wallet management endpoints
|
||
|
|
GET /api/v1/wallet/:userId # Get user's key wallet balance
|
||
|
|
POST /api/v1/wallet/spend # Spend keys on app feature
|
||
|
|
POST /api/v1/wallet/sync # Sync wallet across devices
|
||
|
|
GET /api/v1/wallet/history/:userId # Get spending transaction history
|
||
|
|
|
||
|
|
# App feature endpoints
|
||
|
|
GET /api/v1/apps/:appId/features # Get app's feature definitions
|
||
|
|
POST /api/v1/apps/:appId/unlock # Unlock feature in specific app
|
||
|
|
GET /api/v1/apps/:appId/unlocked/:userId # Get user's unlocked features
|
||
|
|
|
||
|
|
# Community endpoints
|
||
|
|
POST /api/v1/community/donate # Donate keys to community pool
|
||
|
|
POST /api/v1/community/share # Share keys with another user
|
||
|
|
GET /api/v1/community/pool # Get community pool status
|
||
|
|
```
|
||
|
|
|
||
|
|
### Platform Validation Examples
|
||
|
|
```javascript
|
||
|
|
// Apple JWS Validation
|
||
|
|
const jwt = require('jsonwebtoken');
|
||
|
|
const appleRootCert = loadAppleRootCertificate();
|
||
|
|
|
||
|
|
function validateAppleTransaction(jws) {
|
||
|
|
try {
|
||
|
|
const decoded = jwt.verify(jws, appleRootCert, {
|
||
|
|
algorithms: ['ES256'],
|
||
|
|
issuer: 'https://appleid.apple.com'
|
||
|
|
});
|
||
|
|
return decoded;
|
||
|
|
} catch (error) {
|
||
|
|
throw new Error('Invalid Apple transaction');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Google Play Validation
|
||
|
|
async function validateGooglePlayPurchase(purchaseToken, productId) {
|
||
|
|
const response = await googlePlayAPI.purchases.products.get({
|
||
|
|
packageName: 'com.uwuapps.voiceuwu',
|
||
|
|
productId: productId,
|
||
|
|
token: purchaseToken
|
||
|
|
});
|
||
|
|
return response.data;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Stripe Validation
|
||
|
|
async function validateStripePayment(paymentIntentId) {
|
||
|
|
const paymentIntent = await stripe.paymentIntents.retrieve(paymentIntentId);
|
||
|
|
if (paymentIntent.status !== 'succeeded') {
|
||
|
|
throw new Error('Payment not completed');
|
||
|
|
}
|
||
|
|
return paymentIntent;
|
||
|
|
}
|
||
|
|
|
||
|
|
// PayPal Validation
|
||
|
|
async function validatePayPalPayment(paymentId) {
|
||
|
|
const payment = await paypal.payment.get(paymentId);
|
||
|
|
if (payment.state !== 'approved') {
|
||
|
|
throw new Error('Payment not approved');
|
||
|
|
}
|
||
|
|
return payment;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Steam Validation
|
||
|
|
async function validateSteamPurchase(steamId, transactionId) {
|
||
|
|
const response = await steamWebAPI.GetUserInfo({
|
||
|
|
steamid: steamId,
|
||
|
|
transactionid: transactionId
|
||
|
|
});
|
||
|
|
return response;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Purchase endpoints
|
||
|
|
app.post('/api/v1/purchase/apple', async (req, res) => {
|
||
|
|
const { transaction, userEmail } = req.body;
|
||
|
|
const validated = validateAppleTransaction(transaction);
|
||
|
|
const keys = generateKeysForPurchase(validated.productId, userEmail);
|
||
|
|
await storeUserKeys(userEmail, keys);
|
||
|
|
res.json({ keys });
|
||
|
|
});
|
||
|
|
|
||
|
|
app.post('/api/v1/purchase/google', async (req, res) => {
|
||
|
|
const { purchaseToken, productId, userEmail } = req.body;
|
||
|
|
const validated = await validateGooglePlayPurchase(purchaseToken, productId);
|
||
|
|
const keys = generateKeysForPurchase(productId, userEmail);
|
||
|
|
await storeUserKeys(userEmail, keys);
|
||
|
|
res.json({ keys });
|
||
|
|
});
|
||
|
|
|
||
|
|
app.post('/api/v1/purchase/stripe', async (req, res) => {
|
||
|
|
const { paymentIntentId, userEmail } = req.body;
|
||
|
|
const validated = await validateStripePayment(paymentIntentId);
|
||
|
|
const keys = generateKeysForPurchase(validated.metadata.productId, userEmail);
|
||
|
|
await storeUserKeys(userEmail, keys);
|
||
|
|
res.json({ keys });
|
||
|
|
});
|
||
|
|
|
||
|
|
app.post('/api/v1/purchase/paypal', async (req, res) => {
|
||
|
|
const { paymentId, userEmail } = req.body;
|
||
|
|
const validated = await validatePayPalPayment(paymentId);
|
||
|
|
const keys = generateKeysForPurchase(validated.transactions[0].item_list.items[0].sku, userEmail);
|
||
|
|
await storeUserKeys(userEmail, keys);
|
||
|
|
res.json({ keys });
|
||
|
|
});
|
||
|
|
|
||
|
|
app.post('/api/v1/purchase/steam', async (req, res) => {
|
||
|
|
const { steamId, transactionId } = req.body;
|
||
|
|
const validated = await validateSteamPurchase(steamId, transactionId);
|
||
|
|
const keys = generateKeysForPurchase(validated.productId, steamId);
|
||
|
|
await storeUserKeys(steamId, keys, 'steam');
|
||
|
|
res.json({ keys });
|
||
|
|
});
|
||
|
|
|
||
|
|
// Key sync endpoints
|
||
|
|
app.get('/api/v1/keys/email/:email', async (req, res) => {
|
||
|
|
const { email } = req.params;
|
||
|
|
const keys = await getUserKeys(email);
|
||
|
|
res.json({ keys });
|
||
|
|
});
|
||
|
|
|
||
|
|
app.get('/api/v1/keys/steam/:steamId', async (req, res) => {
|
||
|
|
const { steamId } = req.params;
|
||
|
|
const keys = await getUserKeys(steamId, 'steam');
|
||
|
|
res.json({ keys });
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
## Security Features
|
||
|
|
|
||
|
|
### Transaction Integrity
|
||
|
|
- Apple JWS signature validation
|
||
|
|
- HTTPS for all API calls
|
||
|
|
- Rate limiting on sync endpoints
|
||
|
|
- Input validation and sanitization
|
||
|
|
|
||
|
|
### Key Protection
|
||
|
|
- Hardware-backed keystore (iOS Keychain)
|
||
|
|
- Encrypted local storage
|
||
|
|
- Local-only key validation
|
||
|
|
- No device binding required (offline-first)
|
||
|
|
|
||
|
|
## Error Handling
|
||
|
|
|
||
|
|
### Network Errors (Sync Only)
|
||
|
|
- Graceful failure - app continues working offline
|
||
|
|
- Retry logic with exponential backoff
|
||
|
|
- User feedback on sync failures
|
||
|
|
- Cached keys always work
|
||
|
|
|
||
|
|
### Sync Errors
|
||
|
|
- Invalid email/token handling
|
||
|
|
- Server unavailable scenarios
|
||
|
|
- Network timeout handling
|
||
|
|
- Malformed response handling
|
||
|
|
|
||
|
|
## Configuration
|
||
|
|
|
||
|
|
### Server Configuration
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"keyserver": {
|
||
|
|
"baseUrl": "https://api.voiceuwu.com",
|
||
|
|
"timeout": 15000,
|
||
|
|
"retryAttempts": 2
|
||
|
|
},
|
||
|
|
"apple": {
|
||
|
|
"bundleId": "com.uwuapps.VoiceUwu",
|
||
|
|
"environment": "production"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Client Configuration
|
||
|
|
```swift
|
||
|
|
struct VUKeyServerConfig {
|
||
|
|
let baseUrl: URL
|
||
|
|
let timeout: TimeInterval = 15.0 // Shorter timeout for sync
|
||
|
|
let retryAttempts: Int = 2 // Fewer retries for sync
|
||
|
|
let autoSyncOnOpen: Bool = true // Auto-sync when Keys for All opens
|
||
|
|
let syncCooldown: TimeInterval = 300 // 5 minutes between auto-syncs
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## Testing Strategy
|
||
|
|
|
||
|
|
### Unit Tests
|
||
|
|
- Mock keyserver responses for sync operations
|
||
|
|
- Apple transaction validation
|
||
|
|
- Offline-first key validation logic
|
||
|
|
- Error handling scenarios (network failures)
|
||
|
|
|
||
|
|
### Integration Tests
|
||
|
|
- End-to-end purchase flow
|
||
|
|
- Keys for All sync functionality
|
||
|
|
- Network failure scenarios (app continues working)
|
||
|
|
- Cross-platform key sharing
|
||
|
|
|
||
|
|
## Implementation Notes
|
||
|
|
|
||
|
|
1. **Offline-First**: App must work completely offline - network only for sync/purchase
|
||
|
|
2. **Multi-Platform Support**: Single keyserver supports Apple, Google, Stripe, PayPal, and Steam
|
||
|
|
3. **Platform Requirements**:
|
||
|
|
- Apple: StoreKit 2 for iOS/macOS
|
||
|
|
- Google: Play Billing API for Android
|
||
|
|
- Stripe: Payment Intents API for web/direct
|
||
|
|
- PayPal: REST API for web/direct
|
||
|
|
- Steam: Web API for PC gaming
|
||
|
|
4. **Server Validation**: Each platform has its own validation method
|
||
|
|
5. **Minimal Network**: 8 endpoints total (5 purchase + 3 sync)
|
||
|
|
6. **User Experience**: Auto-sync on feature open + manual sync button
|
||
|
|
7. **Cross-Platform Keys**: Same keys work across all platforms once synced
|
||
|
|
8. **No Session Management**: Stateless key retrieval by email/token/steamId
|
||
|
|
9. **Security**: Local key validation, HTTPS for sync, encrypted storage
|
||
|
|
10. **Web Portal Integration**: Keys purchased through web portal immediately sync to all platforms
|
||
|
|
|
||
|
|
## Related Documentation
|
||
|
|
|
||
|
|
- **[VU Key Web Portal](./VUKeyWebPortal.md)**: User-facing web application for key management, sharing, and purchasing
|