From f2ea41584008224979aa918f4e042bce05fdcef4 Mon Sep 17 00:00:00 2001 From: Vantz Stockwell Date: Thu, 11 Jun 2026 21:53:22 -0400 Subject: [PATCH] =?UTF-8?q?fix(api):=20Beta=20hardening=20=E2=80=94=20real?= =?UTF-8?q?=20500=20fix,=20encryption=20guard,=20honest=20payments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - analytics: getMapAnalytics queried map.name but the map_library column is display_name (no name column) — every map-analytics call 500'd. Fixed select + groupBy to map.display_name. - setup: guard ENCRYPTION_KEY length before AES-256-GCM createCipheriv — an unset key crashed bare-metal setup with an opaque 'Invalid key length' 500; now returns a clear 503. Also stop falsely marking bare-metal connected on completeSetup; leave offline until the agent's first heartbeat. - webstore: public checkout returned a FAKE PayPal order token + sandbox URL that resolves to nowhere. Refuse honestly with 503 (payments coming soon) instead of faking a transaction. - store: module purchase wrote a fake txn_ implying a charge; record it honestly as a free Beta grant (transaction_id=beta-free-grant, amount 0). Backend tsc green. Co-Authored-By: Claude Opus 4.8 --- .../modules/analytics/analytics.service.ts | 4 +-- .../src/modules/setup/setup.service.ts | 18 +++++++++--- .../src/modules/store/store.service.ts | 10 +++++-- .../src/modules/webstore/webstore.service.ts | 28 ++++++------------- 4 files changed, 33 insertions(+), 27 deletions(-) diff --git a/backend-nest/src/modules/analytics/analytics.service.ts b/backend-nest/src/modules/analytics/analytics.service.ts index c98c19c..ad611c0 100644 --- a/backend-nest/src/modules/analytics/analytics.service.ts +++ b/backend-nest/src/modules/analytics/analytics.service.ts @@ -111,13 +111,13 @@ export class AnalyticsService { .createQueryBuilder('wipe') .leftJoinAndSelect('wipe.map', 'map') .select('map.id', 'map_id') - .addSelect('map.name', 'map_name') + .addSelect('map.display_name', 'map_name') .addSelect('COUNT(wipe.id)', 'usage_count') .where('wipe.license_id = :licenseId', { licenseId }) .andWhere('wipe.started_at >= :cutoff', { cutoff }) .andWhere('wipe.map_id IS NOT NULL') .groupBy('map.id') - .addGroupBy('map.name') + .addGroupBy('map.display_name') .getRawMany(); return { diff --git a/backend-nest/src/modules/setup/setup.service.ts b/backend-nest/src/modules/setup/setup.service.ts index 3c0ab37..2b65677 100644 --- a/backend-nest/src/modules/setup/setup.service.ts +++ b/backend-nest/src/modules/setup/setup.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, ServiceUnavailableException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; @@ -55,6 +55,13 @@ export class SetupService { if (dto.panel_api_key) { const encryptionKey = this.configService.get('encryption.key', ''); const keyBuffer = Buffer.from(encryptionKey, 'hex'); + // AES-256-GCM needs a 32-byte key. An unset/short ENCRYPTION_KEY would + // otherwise crash createCipheriv with an opaque "Invalid key length" 500. + if (keyBuffer.length !== 32) { + throw new ServiceUnavailableException( + 'Server encryption is not configured (ENCRYPTION_KEY must be 32 bytes / 64 hex chars). Contact the platform operator.', + ); + } const iv = crypto.randomBytes(16); const cipher = crypto.createCipheriv('aes-256-gcm', keyBuffer, iv); const encrypted = Buffer.concat([ @@ -82,9 +89,12 @@ export class SetupService { }); if (connection) { - // For bare metal, mark as connected immediately (waiting for agent) - if (connection.connection_type === 'bare_metal') { - connection.connection_status = 'connected'; + // Bare-metal stays 'offline' until the agent's first heartbeat flips it + // 'connected' (HostAgentConsumerService). Marking it connected here was a + // false positive — the dashboard showed a live server before any agent + // had checked in. + if (connection.connection_type === 'bare_metal' && connection.connection_status !== 'connected') { + connection.connection_status = 'offline'; connection.updated_at = new Date(); await this.connectionRepo.save(connection); } diff --git a/backend-nest/src/modules/store/store.service.ts b/backend-nest/src/modules/store/store.service.ts index 088bb1d..24e5c13 100644 --- a/backend-nest/src/modules/store/store.service.ts +++ b/backend-nest/src/modules/store/store.service.ts @@ -57,11 +57,17 @@ export class StoreService { throw new NotFoundException('Module not found'); } + // Beta: modules are granted free (no payment processing wired yet). Record + // it honestly as a beta grant at $0 rather than a fake `txn_*` id that + // implies a real charge occurred. + this.logger.log( + `Granting module ${moduleId} to license ${licenseId} free (Beta — no payment processing)`, + ); const purchase = this.purchaseRepo.create({ license_id: licenseId, module_id: moduleId, - transaction_id: `txn_${Date.now()}`, - amount_paid: parseFloat(module.price_usd.toString()), + transaction_id: 'beta-free-grant', + amount_paid: 0, }); return this.purchaseRepo.save(purchase); diff --git a/backend-nest/src/modules/webstore/webstore.service.ts b/backend-nest/src/modules/webstore/webstore.service.ts index a701b16..6f0543d 100644 --- a/backend-nest/src/modules/webstore/webstore.service.ts +++ b/backend-nest/src/modules/webstore/webstore.service.ts @@ -1,4 +1,4 @@ -import { Injectable, NotFoundException } from '@nestjs/common'; +import { Injectable, NotFoundException, ServiceUnavailableException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { StoreConfig } from '../../entities/store-config.entity'; @@ -224,23 +224,13 @@ export class WebstoreService { throw new NotFoundException('Item not found'); } - const transaction = this.transactionRepo.create({ - license_id: license.id, - item_id: item.id, - steam_id: dto.steam_id, - player_name: dto.player_name, - paypal_order_id: `order_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, - amount: parseFloat(item.price.toString()), - currency: 'USD', // Would get from config - status: 'pending', - }); - - await this.transactionRepo.save(transaction); - - // Return mock PayPal approval URL - return { - order_id: transaction.paypal_order_id, - approval_url: `https://www.sandbox.paypal.com/checkoutnow?token=${transaction.paypal_order_id}`, - }; + // Beta: real PayPal/Stripe processing is not wired yet. Refuse honestly + // instead of writing a pending transaction and handing the player a fake + // order token that resolves to nowhere. (item lookup above still validates + // the request so the storefront UI can show the catalogue.) + void item; + throw new ServiceUnavailableException( + 'Storefront checkout is not available yet — payment processing is coming soon.', + ); } }