From 9cfd2b4a079481f0cea60ecd8588604d0adc2a63 Mon Sep 17 00:00:00 2001 From: Jonathan Nagayoshi Date: Tue, 17 Mar 2026 23:34:14 +0000 Subject: [PATCH 1/2] feat: add firstMap parameter to server creation and deployment processes --- .../agents/quickserver-contributor.agent.md | 2 -- .../core/src/models/DeploymentContext.test.ts | 5 +++ packages/core/src/models/DeploymentContext.ts | 4 +++ packages/core/src/services/ServerManager.ts | 1 + .../src/usecase/CreateServerForClient.test.ts | 15 +++++++++ .../core/src/usecase/CreateServerForClient.ts | 2 ++ .../http/routes/servers/createServer.test.ts | 6 +++- .../src/http/routes/servers/createServer.ts | 8 ++++- .../src/http/routes/swaggerOptions.ts | 5 +++ .../cloud-providers/aws/AWSServerManager.ts | 2 ++ .../DefaultTaskDefinitionService.test.ts | 32 +++++++++++++++++++ .../services/DefaultTaskDefinitionService.ts | 3 +- .../oracle/OCIServerManager.test.ts | 21 ++++++++++++ .../oracle/OCIServerManager.ts | 6 ++-- .../oracle/OracleVMManager.test.ts | 28 ++++++++++++++++ .../cloud-providers/oracle/OracleVMManager.ts | 4 ++- .../oracle/utils/generateDockerCompose.ts | 6 ++-- 17 files changed, 140 insertions(+), 10 deletions(-) diff --git a/.github/agents/quickserver-contributor.agent.md b/.github/agents/quickserver-contributor.agent.md index fa10607f..4c90bf5b 100644 --- a/.github/agents/quickserver-contributor.agent.md +++ b/.github/agents/quickserver-contributor.agent.md @@ -1,7 +1,5 @@ --- description: 'Quickserver Contributor' -tools: ['runCommands', 'runTasks', 'edit', 'runNotebooks', 'search', 'new', 'github/*', 'extensions', 'todos', 'runTests', 'usages', 'vscodeAPI', 'problems', 'changes', 'testFailure', 'openSimpleBrowser', 'fetch', 'githubRepo'] -model: Claude Haiku 4.5 (Preview) (copilot) --- You are a Senior Staff Software Engineer, you write code that follows the following principles: diff --git a/packages/core/src/models/DeploymentContext.test.ts b/packages/core/src/models/DeploymentContext.test.ts index 093f7d86..818c3a40 100644 --- a/packages/core/src/models/DeploymentContext.test.ts +++ b/packages/core/src/models/DeploymentContext.test.ts @@ -14,6 +14,7 @@ describe("DeploymentContext", () => { const region = chance.pickone(validRegions); const variantName = chance.pickone(["standard-competitive", "casual", "vanilla"]) as Variant; const serverId = chance.guid(); + const firstMap = chance.pickone(['cp_process_f12', 'koth_product_final']); const sourcemodAdminSteamId = chance.string({ length: 17, pool: '0123456789' }); const extraEnvs = { TEST_ENV: chance.word(), @@ -28,6 +29,7 @@ describe("DeploymentContext", () => { region, variantName, serverId, + firstMap, sourcemodAdminSteamId, extraEnvs } @@ -42,6 +44,7 @@ describe("DeploymentContext", () => { serverId: data.serverId, region: data.region, variantName: data.variantName, + firstMap: data.firstMap, statusUpdater: mocks.statusUpdater, sourcemodAdminSteamId: data.sourcemodAdminSteamId, extraEnvs: data.extraEnvs @@ -50,6 +53,7 @@ describe("DeploymentContext", () => { expect(context.serverId).toBe(data.serverId); expect(context.region).toBe(data.region); expect(context.variantName).toBe(data.variantName); + expect(context.firstMap).toBe(data.firstMap); expect(context.statusUpdater).toBe(mocks.statusUpdater); expect(context.sourcemodAdminSteamId).toBe(data.sourcemodAdminSteamId); expect(context.extraEnvs).toEqual(data.extraEnvs); @@ -68,6 +72,7 @@ describe("DeploymentContext", () => { expect(context.serverId).toBe(data.serverId); expect(context.region).toBe(data.region); expect(context.variantName).toBe(data.variantName); + expect(context.firstMap).toBeUndefined(); expect(context.statusUpdater).toBe(mocks.statusUpdater); expect(context.sourcemodAdminSteamId).toBeUndefined(); expect(context.extraEnvs).toEqual({}); diff --git a/packages/core/src/models/DeploymentContext.ts b/packages/core/src/models/DeploymentContext.ts index 1a9a7cd4..f7804963 100644 --- a/packages/core/src/models/DeploymentContext.ts +++ b/packages/core/src/models/DeploymentContext.ts @@ -9,6 +9,7 @@ export class DeploymentContext { serverId, region, variantName, + firstMap, statusUpdater, sourcemodAdminSteamId, extraEnvs = {} @@ -16,6 +17,7 @@ export class DeploymentContext { serverId: string; region: Region; variantName: Variant; + firstMap?: string; statusUpdater: StatusUpdater; sourcemodAdminSteamId?: string; extraEnvs?: Record; @@ -23,6 +25,7 @@ export class DeploymentContext { this.serverId = serverId; this.region = region; this.variantName = variantName; + this.firstMap = firstMap; this.statusUpdater = statusUpdater; this.sourcemodAdminSteamId = sourcemodAdminSteamId; this.extraEnvs = extraEnvs; @@ -31,6 +34,7 @@ export class DeploymentContext { public readonly serverId: string; public readonly region: Region; public readonly variantName: Variant; + public readonly firstMap?: string; public readonly statusUpdater: StatusUpdater; public readonly sourcemodAdminSteamId?: string; public readonly extraEnvs: Record; diff --git a/packages/core/src/services/ServerManager.ts b/packages/core/src/services/ServerManager.ts index 75588d02..c5ad4b03 100644 --- a/packages/core/src/services/ServerManager.ts +++ b/packages/core/src/services/ServerManager.ts @@ -9,6 +9,7 @@ export interface ServerManager { serverId: string, region: Region, variantName: Variant, + firstMap?: string, statusUpdater: StatusUpdater, sourcemodAdminSteamId?: string, extraEnvs?: Record, diff --git a/packages/core/src/usecase/CreateServerForClient.test.ts b/packages/core/src/usecase/CreateServerForClient.test.ts index 60b41d72..c930c127 100644 --- a/packages/core/src/usecase/CreateServerForClient.test.ts +++ b/packages/core/src/usecase/CreateServerForClient.test.ts @@ -91,6 +91,21 @@ describe('CreateServerForClient', () => { ); }); + it('should pass firstMap to deployServer when provided', async () => { + // Given + const { sut, serverRepository, serverManager } = makeSut(); + serverManager.deployServer.mockResolvedValue(deployedServer); + serverRepository.upsertServer.mockResolvedValue(); + + // When + await sut.execute({ ...baseArgs, firstMap: 'cp_process_f12' }); + + // Then + expect(serverManager.deployServer).toHaveBeenCalledWith( + expect.objectContaining({ firstMap: 'cp_process_f12' }) + ); + }); + it('should pass empty extraEnvs to deployServer when not provided', async () => { // Given const { sut, serverRepository, serverManager } = makeSut(); diff --git a/packages/core/src/usecase/CreateServerForClient.ts b/packages/core/src/usecase/CreateServerForClient.ts index 061a796d..a77f0d95 100644 --- a/packages/core/src/usecase/CreateServerForClient.ts +++ b/packages/core/src/usecase/CreateServerForClient.ts @@ -9,6 +9,7 @@ import { logger } from '@tf2qs/telemetry'; export type CreateServerForClientParams = { region: Region; variantName: Variant; + firstMap?: string; clientId: string; extraEnvs?: Record; statusUpdater?: StatusUpdater; @@ -53,6 +54,7 @@ export class CreateServerForClient { serverId, region: args.region, variantName: args.variantName, + firstMap: args.firstMap, extraEnvs: args.extraEnvs ?? {}, statusUpdater, }); diff --git a/packages/entrypoints/src/http/routes/servers/createServer.test.ts b/packages/entrypoints/src/http/routes/servers/createServer.test.ts index 014b2bfe..94d135f8 100644 --- a/packages/entrypoints/src/http/routes/servers/createServer.test.ts +++ b/packages/entrypoints/src/http/routes/servers/createServer.test.ts @@ -26,11 +26,12 @@ describe('POST /api/servers', () => { const { app, backgroundTaskQueue } = makeSut(); backgroundTaskQueue.enqueue.mockResolvedValue('task-123'); const extraEnvs = { STV_TITLE: 'My Server' }; + const firstMap = 'cp_process_f12'; // When await request(app) .post('/api/servers') - .send({ region: 'eu-frankfurt-1', variantName: 'standard-competitive', extraEnvs }); + .send({ region: 'eu-frankfurt-1', variantName: 'standard-competitive', extraEnvs, firstMap }); // Then expect(backgroundTaskQueue.enqueue).toHaveBeenCalledWith( @@ -40,6 +41,7 @@ describe('POST /api/servers', () => { variantName: 'standard-competitive', clientId: TEST_CLIENT_ID, extraEnvs, + firstMap, }), undefined, undefined, @@ -75,6 +77,8 @@ describe('POST /api/servers', () => { { body: { region: 'us-east-1', variantName: 'standard-competitive', extraEnvs: ['invalid'] }, description: 'extraEnvs is an array' }, { body: { region: 'invalid-region', variantName: 'standard-competitive' }, description: 'region is not a valid Region enum value' }, { body: { region: 'us-east-1', variantName: 'standard-competitive', extraEnvs: { KEY: 123 } }, description: 'extraEnvs has non-string values' }, + { body: { region: 'us-east-1', variantName: 'standard-competitive', firstMap: 123 }, description: 'firstMap is not a string' }, + { body: { region: 'us-east-1', variantName: 'standard-competitive', firstMap: ' ' }, description: 'firstMap is an empty string' }, ])('should return 400 when $description', async ({ body }) => { // Given const { app } = makeSut(); diff --git a/packages/entrypoints/src/http/routes/servers/createServer.ts b/packages/entrypoints/src/http/routes/servers/createServer.ts index b6bb5147..101a8c0d 100644 --- a/packages/entrypoints/src/http/routes/servers/createServer.ts +++ b/packages/entrypoints/src/http/routes/servers/createServer.ts @@ -41,10 +41,11 @@ export function createCreateServerHandler(backgroundTaskQueue: BackgroundTaskQue return; } - const { region, variantName, extraEnvs } = req.body as { + const { region, variantName, extraEnvs, firstMap } = req.body as { region?: unknown; variantName?: unknown; extraEnvs?: unknown; + firstMap?: unknown; }; if (!region || typeof region !== 'string') { @@ -59,6 +60,10 @@ export function createCreateServerHandler(backgroundTaskQueue: BackgroundTaskQue res.status(400).json({ error: 'Bad Request', message: 'variantName is required and must be a string' }); return; } + if (firstMap !== undefined && (typeof firstMap !== 'string' || firstMap.trim().length === 0)) { + res.status(400).json({ error: 'Bad Request', message: 'firstMap must be a non-empty string if provided' }); + return; + } let sanitizedExtraEnvs: Record | undefined; if (extraEnvs !== undefined) { @@ -82,6 +87,7 @@ export function createCreateServerHandler(backgroundTaskQueue: BackgroundTaskQue variantName: variantName as Variant, clientId: clientId as string, extraEnvs: sanitizedExtraEnvs, + firstMap: firstMap as string | undefined, }; const taskId = await backgroundTaskQueue.enqueue('create-server-for-client', taskData, undefined, undefined, { ownerId: clientId as string }); diff --git a/packages/entrypoints/src/http/routes/swaggerOptions.ts b/packages/entrypoints/src/http/routes/swaggerOptions.ts index c18d2e56..45980ab7 100644 --- a/packages/entrypoints/src/http/routes/swaggerOptions.ts +++ b/packages/entrypoints/src/http/routes/swaggerOptions.ts @@ -60,6 +60,11 @@ export const swaggerOptions: swaggerJsdoc.Options = { properties: { region: { type: 'string', example: 'us-east-1', description: 'The AWS or OCI region to deploy in' }, variantName: { type: 'string', example: 'standard-competitive', description: 'The server variant to deploy' }, + firstMap: { + type: 'string', + example: 'cp_process_f12', + description: 'Optional startup map override. If omitted, the variant default map is used.', + }, extraEnvs: { type: 'object', additionalProperties: { type: 'string' }, diff --git a/packages/providers/src/cloud-providers/aws/AWSServerManager.ts b/packages/providers/src/cloud-providers/aws/AWSServerManager.ts index 1f686522..e1d1ff9f 100644 --- a/packages/providers/src/cloud-providers/aws/AWSServerManager.ts +++ b/packages/providers/src/cloud-providers/aws/AWSServerManager.ts @@ -90,6 +90,7 @@ export class AWSServerManager implements ServerManager { serverId: string; region: Region; variantName: Variant; + firstMap?: string; statusUpdater: StatusUpdater; sourcemodAdminSteamId?: string; extraEnvs?: Record; @@ -99,6 +100,7 @@ export class AWSServerManager implements ServerManager { serverId: args.serverId, region: args.region, variantName: args.variantName, + firstMap: args.firstMap, statusUpdater: args.statusUpdater, sourcemodAdminSteamId: args.sourcemodAdminSteamId, extraEnvs: args.extraEnvs, diff --git a/packages/providers/src/cloud-providers/aws/services/DefaultTaskDefinitionService.test.ts b/packages/providers/src/cloud-providers/aws/services/DefaultTaskDefinitionService.test.ts index 5eea03eb..80f3aa6e 100644 --- a/packages/providers/src/cloud-providers/aws/services/DefaultTaskDefinitionService.test.ts +++ b/packages/providers/src/cloud-providers/aws/services/DefaultTaskDefinitionService.test.ts @@ -142,6 +142,38 @@ describe("DefaultTaskDefinitionService", () => { }); }); + it("uses firstMap override when provided in deployment context", async () => { + const taskDefinitionArn = "arn:aws:ecs:us-east-1:123456789012:task-definition/test-server-123:1"; + const contextWithFirstMap = { + ...context, + firstMap: "koth_product_final" + } as DeploymentContext; + + ecsClientMock.on(RegisterTaskDefinitionCommand).resolves({ + taskDefinition: { + taskDefinitionArn + } + }); + + const service = new DefaultTaskDefinitionService(mockConfigManager, mockAWSConfigService, mockTracingService); + + await service.create(contextWithFirstMap, credentials, environment); + + const registerCalls = ecsClientMock.commandCalls(RegisterTaskDefinitionCommand); + expect(registerCalls).toHaveLength(1); + + const command = registerCalls[0].args[0].input.containerDefinitions?.[0].command; + expect(command).toEqual([ + "-enablefakeip", + "+sv_pure", + "2", + "+maxplayers", + "12", + "+map", + "koth_product_final" + ]); + }); + it("creates task definition successfully with NewRelic sidecar", async () => { // Set up NewRelic environment variable process.env.NEW_RELIC_LICENSE_KEY = "test-license-key"; diff --git a/packages/providers/src/cloud-providers/aws/services/DefaultTaskDefinitionService.ts b/packages/providers/src/cloud-providers/aws/services/DefaultTaskDefinitionService.ts index bc53d91b..3f39a073 100644 --- a/packages/providers/src/cloud-providers/aws/services/DefaultTaskDefinitionService.ts +++ b/packages/providers/src/cloud-providers/aws/services/DefaultTaskDefinitionService.ts @@ -36,6 +36,7 @@ export class DefaultTaskDefinitionService implements TaskDefinitionServiceInterf const awsRegionConfig = this.awsConfigService.getRegionConfig(context.region); const { ecsClient } = this.awsConfigService.getClients(context.region); const variantConfig = this.configManager.getVariantConfig(context.variantName); + const startupMap = context.firstMap ?? variantConfig.map; this.tracingService.logOperationStart('Registering task definition', context.serverId, context.region); @@ -61,7 +62,7 @@ export class DefaultTaskDefinitionService implements TaskDefinitionServiceInterf "+maxplayers", variantConfig.maxPlayers.toString(), "+map", - variantConfig.map, + startupMap, ], portMappings: [ { diff --git a/packages/providers/src/cloud-providers/oracle/OCIServerManager.test.ts b/packages/providers/src/cloud-providers/oracle/OCIServerManager.test.ts index bd676df7..e5231834 100644 --- a/packages/providers/src/cloud-providers/oracle/OCIServerManager.test.ts +++ b/packages/providers/src/cloud-providers/oracle/OCIServerManager.test.ts @@ -504,6 +504,27 @@ describe("OCIServerManager", () => { expect(envVars.CUSTOM_ENV_VAR).toBe("custom-value"); expect(envVars.ANOTHER_VAR).toBe("another-value"); }); + + it("should use firstMap override instead of variant default map", async () => { + const { sut, containerClient } = createTestEnvironment(); + const statusUpdater = vi.fn(); + + await sut.deployServer({ + region: testRegion, + variantName: testVariant, + sourcemodAdminSteamId: "12345678901234567", + serverId: "test-server-first-map", + firstMap: "custom_map", + statusUpdater, + }); + + const containerInstanceRequest = containerClient.createContainerInstance.mock.calls[0][0]; + const tf2Args = containerInstanceRequest.createContainerInstanceDetails.containers[0].arguments as string[]; + const mapArgIndex = tf2Args.indexOf("+map"); + + expect(mapArgIndex).toBeGreaterThanOrEqual(0); + expect(tf2Args[mapArgIndex + 1]).toBe("custom_map"); + }); }); describe("newrelic-infra sidecar", () => { it("should add newrelic-infra container when NEW_RELIC_LICENSE_KEY is set", async () => { diff --git a/packages/providers/src/cloud-providers/oracle/OCIServerManager.ts b/packages/providers/src/cloud-providers/oracle/OCIServerManager.ts index 904f6c62..8823d1e8 100644 --- a/packages/providers/src/cloud-providers/oracle/OCIServerManager.ts +++ b/packages/providers/src/cloud-providers/oracle/OCIServerManager.ts @@ -97,6 +97,7 @@ export class OCIServerManager implements ServerManager { serverId: string; region: Region; variantName: Variant; + firstMap?: string; statusUpdater: StatusUpdater; sourcemodAdminSteamId?: string; extraEnvs?: Record; @@ -105,7 +106,7 @@ export class OCIServerManager implements ServerManager { parentSpan.setAttribute('serverId', args.serverId); const startTime = Date.now(); const { serverCommander, configManager, passwordGeneratorService, ociClientFactory, serverAbortManager } = this.dependencies; - const { region, variantName, sourcemodAdminSteamId, serverId, extraEnvs = {}, statusUpdater } = args; + const { region, variantName, firstMap, sourcemodAdminSteamId, serverId, extraEnvs = {}, statusUpdater } = args; const abortController = serverAbortManager.getOrCreate(serverId); try { @@ -124,6 +125,7 @@ export class OCIServerManager implements ServerManager { const tvPassword = passwordGeneratorService.generatePassword(passwordSettings); const containerImage = variantConfig.image; + const startupMap = firstMap ?? variantConfig.map; const logSecret = chance.integer({ min: 1, max: 999999 }) const defaultCfgsEnvironment = variantConfig.defaultCfgs @@ -193,7 +195,7 @@ export class OCIServerManager implements ServerManager { "+maxplayers", variantConfig.maxPlayers.toString(), "+map", - variantConfig.map, + startupMap, "+log", "on", "+logaddress_add", diff --git a/packages/providers/src/cloud-providers/oracle/OracleVMManager.test.ts b/packages/providers/src/cloud-providers/oracle/OracleVMManager.test.ts index 00f1696e..12746e9c 100644 --- a/packages/providers/src/cloud-providers/oracle/OracleVMManager.test.ts +++ b/packages/providers/src/cloud-providers/oracle/OracleVMManager.test.ts @@ -295,6 +295,34 @@ describe('OracleVMManager', () => { expect(tf2Service.cap_add).toContain('ALL'); }); + it('should override startup map when firstMap is provided', async () => { + const { sut, data, capturedLaunchInstanceParams } = createTestEnvironment(); + const statusUpdater = vi.fn() as StatusUpdater; + + await sut.deployServer({ + serverId: data.serverId, + region: data.testRegion, + variantName: data.testVariant, + firstMap: 'koth_product_final', + statusUpdater + }); + + const params = capturedLaunchInstanceParams(); + const cloudInitScript = Buffer.from(params.userDataBase64, 'base64').toString('utf-8'); + const dockerComposeContent = cloudInitScript + .split('content: |')[1] + .split('\n') + .filter((line) => line.trim() && !line.startsWith('#')) + .map((line) => line.replace(/^\s{6}/, '')) + .join('\n'); + + const dockerCompose = yaml.parse(dockerComposeContent); + const tf2Command = dockerCompose.services['tf2-server'].command as string; + + expect(tf2Command).toContain('+map koth_product_final'); + expect(tf2Command).not.toContain(`+map ${data.variantConfig.map}`); + }); + it('should return all expected parameters in a successful run', async () => { const { sut, data } = createTestEnvironment(); const statusUpdater = vi.fn() as StatusUpdater; diff --git a/packages/providers/src/cloud-providers/oracle/OracleVMManager.ts b/packages/providers/src/cloud-providers/oracle/OracleVMManager.ts index bfe2aeda..d4b636c0 100644 --- a/packages/providers/src/cloud-providers/oracle/OracleVMManager.ts +++ b/packages/providers/src/cloud-providers/oracle/OracleVMManager.ts @@ -73,6 +73,7 @@ export class OracleVMManager implements ServerManager { serverId: string; region: Region; variantName: Variant; + firstMap?: string; statusUpdater: StatusUpdater; sourcemodAdminSteamId?: string; extraEnvs?: Record; @@ -81,7 +82,7 @@ export class OracleVMManager implements ServerManager { parentSpan.setAttribute('serverId', args.serverId); const startTime = Date.now(); const { configManager, passwordGeneratorService, serverAbortManager } = this.dependencies; - const { region, variantName, sourcemodAdminSteamId, serverId, extraEnvs = {}, statusUpdater } = args; + const { region, variantName, firstMap, sourcemodAdminSteamId, serverId, extraEnvs = {}, statusUpdater } = args; const abortController = serverAbortManager.getOrCreate(serverId); try { @@ -140,6 +141,7 @@ export class OracleVMManager implements ServerManager { const dockerComposeYaml = generateDockerCompose({ serverId, variantConfig, + firstMap, environmentVariables, containerImage, rconPassword, diff --git a/packages/providers/src/cloud-providers/oracle/utils/generateDockerCompose.ts b/packages/providers/src/cloud-providers/oracle/utils/generateDockerCompose.ts index 74550168..b3e44de4 100644 --- a/packages/providers/src/cloud-providers/oracle/utils/generateDockerCompose.ts +++ b/packages/providers/src/cloud-providers/oracle/utils/generateDockerCompose.ts @@ -4,6 +4,7 @@ import * as yaml from 'yaml'; type DockerComposeParams = { serverId: string; variantConfig: VariantConfig; + firstMap?: string; environmentVariables: Record; containerImage: string; rconPassword: string; @@ -14,13 +15,14 @@ type DockerComposeParams = { }; export function generateDockerCompose(params: DockerComposeParams): string { - const { serverId, variantConfig, environmentVariables, containerImage, rconPassword, region, variantName, oracleRegionConfig, ociCredentials } = params; + const { serverId, variantConfig, firstMap, environmentVariables, containerImage, rconPassword, region, variantName, oracleRegionConfig, ociCredentials } = params; + const startupMap = firstMap ?? variantConfig.map; const tf2ServerCommand = [ "-enablefakeip", `+sv_pure ${variantConfig.svPure}`, `+maxplayers ${variantConfig.maxPlayers}`, - `+map ${variantConfig.map}`, + `+map ${startupMap}`, "+log on", `+logaddress_add ${process.env.SRCDS_LOG_ADDRESS || ""}`, `+sv_logsecret ${environmentVariables.SV_LOGSECRET}`, From 808c3c6a233624e71293a6dc3c10bec01fa5587b Mon Sep 17 00:00:00 2001 From: Jonathan Nagayoshi Date: Tue, 17 Mar 2026 23:59:18 +0000 Subject: [PATCH 2/2] feat: enhance firstMap validation to prevent malicious input --- .../src/http/routes/servers/createServer.test.ts | 1 + .../entrypoints/src/http/routes/servers/createServer.ts | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/entrypoints/src/http/routes/servers/createServer.test.ts b/packages/entrypoints/src/http/routes/servers/createServer.test.ts index 94d135f8..b7388f35 100644 --- a/packages/entrypoints/src/http/routes/servers/createServer.test.ts +++ b/packages/entrypoints/src/http/routes/servers/createServer.test.ts @@ -79,6 +79,7 @@ describe('POST /api/servers', () => { { body: { region: 'us-east-1', variantName: 'standard-competitive', extraEnvs: { KEY: 123 } }, description: 'extraEnvs has non-string values' }, { body: { region: 'us-east-1', variantName: 'standard-competitive', firstMap: 123 }, description: 'firstMap is not a string' }, { body: { region: 'us-east-1', variantName: 'standard-competitive', firstMap: ' ' }, description: 'firstMap is an empty string' }, + { body: { region: 'us-east-1', variantName: 'standard-competitive', firstMap: ';kill;' }, description: 'firstMap is a malicious injection string' }, ])('should return 400 when $description', async ({ body }) => { // Given const { app } = makeSut(); diff --git a/packages/entrypoints/src/http/routes/servers/createServer.ts b/packages/entrypoints/src/http/routes/servers/createServer.ts index 101a8c0d..88676e5d 100644 --- a/packages/entrypoints/src/http/routes/servers/createServer.ts +++ b/packages/entrypoints/src/http/routes/servers/createServer.ts @@ -60,8 +60,8 @@ export function createCreateServerHandler(backgroundTaskQueue: BackgroundTaskQue res.status(400).json({ error: 'Bad Request', message: 'variantName is required and must be a string' }); return; } - if (firstMap !== undefined && (typeof firstMap !== 'string' || firstMap.trim().length === 0)) { - res.status(400).json({ error: 'Bad Request', message: 'firstMap must be a non-empty string if provided' }); + if (firstMap !== undefined && (typeof firstMap !== 'string' || !/^\w+$/.test(firstMap))) { + res.status(400).json({ error: 'Bad Request', message: 'Invalid map' }); return; } @@ -87,7 +87,7 @@ export function createCreateServerHandler(backgroundTaskQueue: BackgroundTaskQue variantName: variantName as Variant, clientId: clientId as string, extraEnvs: sanitizedExtraEnvs, - firstMap: firstMap as string | undefined, + firstMap:firstMap as string | undefined, }; const taskId = await backgroundTaskQueue.enqueue('create-server-for-client', taskData, undefined, undefined, { ownerId: clientId as string });