diff --git a/apps/api/src/openapi-docs.spec.ts b/apps/api/src/openapi-docs.spec.ts index 2319299b9a..67a515fb99 100644 --- a/apps/api/src/openapi-docs.spec.ts +++ b/apps/api/src/openapi-docs.spec.ts @@ -263,4 +263,75 @@ describe('OpenAPI document', () => { expect(apiKeyOps.length).toBeGreaterThan(0); }); }); + + // Guardrail against the CS-761 regression: two DTO classes were both named + // `CreateVersionDto` — one in policies (optional sourceVersionId/changelog), + // one in tasks/automations (REQUIRED version + scriptKey). They collided on + // the OpenAPI component name, so the automations shape overwrote the policies + // component. That made the create-policy-version MCP tool demand version + + // scriptKey, which the policies endpoint's ValidationPipe (whitelist + + // forbidNonWhitelisted) then rejected with 400 — no valid path through it. + describe('create-policy-version tool schema', () => { + type SchemaLike = { + $ref?: string; + properties?: Record; + required?: string[]; + }; + + const resolveRequestBodyComponent = ( + routePath: string, + ): { name: string; schema: SchemaLike } | undefined => { + const operation = ( + document.paths[routePath] as + | { post?: { requestBody?: unknown } } + | undefined + )?.post; + const bodySchema = ( + operation?.requestBody as + | { content?: { 'application/json'?: { schema?: SchemaLike } } } + | undefined + )?.content?.['application/json']?.schema; + const ref = bodySchema?.$ref; + if (!ref) return undefined; + const name = ref.replace('#/components/schemas/', ''); + const schema = ( + document.components?.schemas as Record | undefined + )?.[name]; + if (!schema) return undefined; + return { name, schema }; + }; + + it('does not share an OpenAPI component with the automations create-version body', () => { + // Root-cause guard: the two `CreateVersionDto` classes must resolve to + // DISTINCT components, or one silently overwrites the other. + const policy = resolveRequestBodyComponent('/v1/policies/{id}/versions'); + const automation = resolveRequestBodyComponent( + '/v1/tasks/{taskId}/automations/{automationId}/versions', + ); + + expect(policy).toBeDefined(); + expect(automation).toBeDefined(); + expect(policy?.name).not.toBe(automation?.name); + }); + + it('exposes the optional policies shape, not the automations version/scriptKey fields', () => { + const policy = resolveRequestBodyComponent('/v1/policies/{id}/versions'); + expect(policy).toBeDefined(); + + const properties = policy?.schema.properties ?? {}; + const required = policy?.schema.required ?? []; + + // The policies endpoint accepts only these optional fields; the tool + // schema must match so an MCP client can call it without being blocked. + expect(Object.keys(properties)).toEqual( + expect.arrayContaining(['sourceVersionId', 'changelog']), + ); + // The automations-only fields must be absent — their presence (required) + // is exactly what triggered the "property scriptKey should not exist" 400. + expect(properties).not.toHaveProperty('scriptKey'); + expect(properties).not.toHaveProperty('version'); + expect(required).not.toContain('scriptKey'); + expect(required).not.toContain('version'); + }); + }); }); diff --git a/apps/api/src/policies/dto/version.dto.ts b/apps/api/src/policies/dto/version.dto.ts index 9051a04cd9..20f9eda52a 100644 --- a/apps/api/src/policies/dto/version.dto.ts +++ b/apps/api/src/policies/dto/version.dto.ts @@ -1,7 +1,13 @@ -import { ApiProperty } from '@nestjs/swagger'; +import { ApiProperty, ApiSchema } from '@nestjs/swagger'; import { Transform } from 'class-transformer'; import { IsArray, IsBoolean, IsOptional, IsString } from 'class-validator'; +// A second class named `CreateVersionDto` exists in tasks/automations with a +// different, REQUIRED shape (version! + scriptKey!). Without a distinct OpenAPI +// component name the two collide, and the automations shape overwrites this +// one — which broke the create-policy-version MCP tool (it demanded fields the +// policies endpoint's ValidationPipe then rejected with 400). Keep this name. +@ApiSchema({ name: 'CreatePolicyVersionDto' }) export class CreateVersionDto { @ApiProperty({ description: 'Optional version ID to base the new version on', diff --git a/packages/docs/openapi.json b/packages/docs/openapi.json index 152a292ba2..c83f95746e 100644 --- a/packages/docs/openapi.json +++ b/packages/docs/openapi.json @@ -11307,7 +11307,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreateVersionDto" + "$ref": "#/components/schemas/CreatePolicyVersionDto" } } } @@ -16345,6 +16345,39 @@ "put": { "operationId": "TrustPortalController_updateFaqs_v1", "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "faqs" + ], + "properties": { + "faqs": { + "type": "array", + "items": { + "type": "object", + "required": [ + "question", + "answer" + ], + "properties": { + "question": { + "type": "string" + }, + "answer": { + "type": "string" + } + } + } + } + } + } + } + } + }, "responses": { "200": { "description": "" @@ -16738,6 +16771,39 @@ "post": { "operationId": "TrustPortalController_updateOverview_v1", "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "organizationId" + ], + "properties": { + "organizationId": { + "type": "string", + "description": "Organization that owns the trust portal. Must match the authenticated API key's organization.", + "example": "org_6914cd0e16e4c7dccbb54426" + }, + "overviewTitle": { + "type": "string", + "maxLength": 200, + "nullable": true + }, + "overviewContent": { + "type": "string", + "maxLength": 10000, + "nullable": true + }, + "showOverview": { + "type": "boolean" + } + } + } + } + } + }, "responses": { "200": { "description": "Overview updated successfully" @@ -16811,6 +16877,43 @@ "post": { "operationId": "TrustPortalController_createCustomLink_v1", "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "organizationId", + "title", + "url" + ], + "properties": { + "organizationId": { + "type": "string", + "description": "Organization that owns the trust portal. Must match the authenticated API key's organization.", + "example": "org_6914cd0e16e4c7dccbb54426" + }, + "title": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "description": { + "type": "string", + "maxLength": 500, + "nullable": true + }, + "url": { + "type": "string", + "format": "uri", + "maxLength": 2000 + } + } + } + } + } + }, "responses": { "201": { "description": "Custom link created successfully" @@ -16968,6 +17071,34 @@ "post": { "operationId": "TrustPortalController_reorderCustomLinks_v1", "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "organizationId", + "linkIds" + ], + "properties": { + "organizationId": { + "type": "string", + "description": "Organization that owns the trust portal. Must match the authenticated API key's organization.", + "example": "org_6914cd0e16e4c7dccbb54426" + }, + "linkIds": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Custom link IDs in the desired display order." + } + } + } + } + } + }, "responses": { "200": { "description": "Custom links reordered successfully" @@ -21320,6 +21451,41 @@ "type": "string", "nullable": true }, + "whatIsMeasured": { + "type": "string" + }, + "method": { + "type": "string" + }, + "monitorMemberId": { + "type": "string", + "nullable": true + }, + "analyzeMemberId": { + "type": "string", + "nullable": true + }, + "objectiveId": { + "type": "string", + "nullable": true + }, + "isActive": { + "type": "boolean" + }, + "metricId": { + "type": "string" + }, + "periodStart": { + "type": "string", + "description": "First day of the covered period (YYYY-MM-DD), aligned to the metric cadence" + }, + "value": { + "type": "string" + }, + "note": { + "type": "string", + "nullable": true + }, "position": { "type": "integer", "minimum": 0 @@ -21521,6 +21687,41 @@ "type": "string", "nullable": true }, + "whatIsMeasured": { + "type": "string" + }, + "method": { + "type": "string" + }, + "monitorMemberId": { + "type": "string", + "nullable": true + }, + "analyzeMemberId": { + "type": "string", + "nullable": true + }, + "objectiveId": { + "type": "string", + "nullable": true + }, + "isActive": { + "type": "boolean" + }, + "metricId": { + "type": "string" + }, + "periodStart": { + "type": "string", + "description": "First day of the covered period (YYYY-MM-DD), aligned to the metric cadence" + }, + "value": { + "type": "string" + }, + "note": { + "type": "string", + "nullable": true + }, "position": { "type": "integer", "minimum": 0 @@ -21607,6 +21808,92 @@ } } }, + "/v1/isms/documents/{id}/measurements/bulk": { + "post": { + "operationId": "IsmsRegistersController_bulkCreateMeasurements_v1", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "description": "Measurements to record in one save (Metrics due / backfill views)", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "measurements": { + "type": "array", + "minItems": 1, + "maxItems": 200, + "items": { + "type": "object", + "properties": { + "metricId": { + "type": "string" + }, + "periodStart": { + "type": "string" + }, + "value": { + "type": "string" + }, + "note": { + "type": "string", + "nullable": true + } + }, + "required": [ + "metricId", + "periodStart", + "value" + ] + } + } + }, + "required": [ + "measurements" + ] + } + } + } + }, + "responses": { + "200": { + "description": "Measurements recorded" + } + }, + "security": [ + { + "apikey": [] + } + ], + "summary": "Record measurements for several metrics/periods in one save (Metrics due / backfill)", + "tags": [ + "ISMS" + ], + "description": "Record measurements for several metrics/periods in one save (Metrics due / backfill) in Comp AI.", + "x-mint": { + "metadata": { + "title": "Record measurements for several metrics/period | Comp AI API", + "sidebarTitle": "Record measurements for several metrics/periods in one save (Metrics due / backfill)", + "description": "Record measurements for several metrics/periods in one save (Metrics due / backfill) in Comp AI.", + "og:title": "Record measurements for several metrics/period | Comp AI API", + "og:description": "Record measurements for several metrics/periods in one save (Metrics due / backfill) in Comp AI." + } + }, + "x-speakeasy-mcp": { + "name": "bulk-create-measurements" + } + } + }, "/v1/isms/documents/{id}/narrative": { "post": { "operationId": "IsmsRegistersController_saveNarrative_v1", @@ -28836,28 +29123,20 @@ } } }, - "CreateVersionDto": { + "CreatePolicyVersionDto": { "type": "object", "properties": { - "version": { - "type": "number", - "description": "Version number for this published script", - "example": 1 - }, - "scriptKey": { + "sourceVersionId": { "type": "string", - "description": "S3 key of the already-generated & published automation script (returned by the publish step).", - "example": "org_abc123/tsk_abc123/aut_abc123.v1.js" + "description": "Optional version ID to base the new version on", + "example": "pv_abc123def456" }, "changelog": { "type": "string", - "description": "Optional changelog describing this version" + "description": "Optional changelog to associate with the new version", + "example": "Initial draft for quarterly updates" } - }, - "required": [ - "version", - "scriptKey" - ] + } }, "UpdateVersionContentDto": { "type": "object", @@ -29089,6 +29368,29 @@ } } }, + "CreateVersionDto": { + "type": "object", + "properties": { + "version": { + "type": "number", + "description": "Version number for this published script", + "example": 1 + }, + "scriptKey": { + "type": "string", + "description": "S3 key of the already-generated & published automation script (returned by the publish step).", + "example": "org_abc123/tsk_abc123/aut_abc123.v1.js" + }, + "changelog": { + "type": "string", + "description": "Optional changelog describing this version" + } + }, + "required": [ + "version", + "scriptKey" + ] + }, "AuthorResponseDto": { "type": "object", "properties": {