$ErrorActionPreference = 'Stop' $contractPath = Join-Path $PSScriptRoot 'family-feed-read-openapi-contract.ps1' $temporaryPath = [System.IO.Path]::GetTempFileName() $identifierPattern = '^[A-Za-z0-9][A-Za-z0-9._~-]{0,127}$' $cursorPattern = '^[A-Za-z0-9_-]{1,512}$' function New-Ref([string]$Ref) { return [ordered]@{ '$ref' = $Ref } } function New-StringOwner( [int]$MinLength, [int]$MaxLength, [string]$Pattern = '' ) { $schema = [ordered]@{ type = 'string' minLength = $MinLength maxLength = $MaxLength nullable = $false } if ($Pattern) { $schema.pattern = $Pattern } return $schema } function New-ClosedObject( [System.Collections.Specialized.OrderedDictionary]$Properties, [string[]]$Required ) { return [ordered]@{ type = 'object' properties = $Properties required = $Required additionalProperties = $false nullable = $false } } function New-SuccessEnvelope([string]$DataRef) { return New-ClosedObject ([ordered]@{ code = [ordered]@{ type = 'integer' enum = @(200) nullable = $false } data = New-Ref $DataRef }) @('code', 'data') } function New-ErrorEnvelope([int]$Status, [string[]]$BusinessCodes) { return New-ClosedObject ([ordered]@{ businessCode = [ordered]@{ type = 'string' enum = $BusinessCodes nullable = $false } code = [ordered]@{ type = 'integer' enum = @($Status) nullable = $false } message = [ordered]@{ type = 'string' minLength = 1 maxLength = 200 nullable = $false } }) @('businessCode', 'code', 'message') } function New-Response([string]$SchemaRef, [bool]$RateLimited = $false) { $headers = [ordered]@{ 'Cache-Control' = New-Ref '#/components/headers/PrivateNoStore' } if ($RateLimited) { $headers.'Retry-After' = New-Ref '#/components/headers/RetryAfter' } return [ordered]@{ description = 'typed response' headers = $headers content = [ordered]@{ 'application/json' = [ordered]@{ schema = New-Ref $SchemaRef } } } } function New-Responses([string]$SuccessRef) { return [ordered]@{ '200' = New-Response $SuccessRef '400' = New-Response '#/components/schemas/RFamilyFeedReadBadRequest' '401' = New-Response '#/components/schemas/RFamilyFeedReadUnauthorized' '404' = New-Response '#/components/schemas/RFamilyFeedReadNotFound' '429' = New-Response '#/components/schemas/RFamilyFeedReadRateLimited' $true '500' = New-Response '#/components/schemas/RFamilyFeedReadUnavailable' } } function New-ClientIdParameter { return [ordered]@{ name = 'clientid' in = 'header' required = $true schema = [ordered]@{ type = 'string' minLength = 1 maxLength = 128 nullable = $false } } } function New-IdParameter([string]$Name, [string]$SchemaRef) { return [ordered]@{ name = $Name in = 'path' required = $true schema = New-Ref $SchemaRef } } function New-CursorParameter([string]$SchemaRef) { return [ordered]@{ name = 'cursor' in = 'query' required = $false schema = New-Ref $SchemaRef } } function New-LimitParameter { return [ordered]@{ name = 'limit' in = 'query' required = $false schema = [ordered]@{ type = 'integer' minimum = 1 maximum = 50 default = 20 nullable = $false } } } function New-ReadOperation( [string]$OperationId, [object[]]$Parameters, [string]$SuccessRef, [string[]]$AuthorizationScope ) { return [ordered]@{ operationId = $OperationId parameters = $Parameters responses = New-Responses $SuccessRef security = @([ordered]@{ SaToken = @() }) 'x-read-only' = $true 'x-non-disclosing-not-found' = $true 'x-authorize-every-request' = $true 'x-cors-policy-owner' = 'APP_GATEWAY_PREFLIGHT' 'x-authorization-scope' = $AuthorizationScope } } function Add-CursorContract( [System.Collections.Specialized.OrderedDictionary]$Operation, [string[]]$Scope, [string[]]$Order ) { $Operation.'x-cursor-scope' = $Scope $Operation.'x-cursor-order' = $Order $Operation.'x-read-window' = 'UPPER_BOUND_KEYSET_LATEST_VISIBLE' $Operation.'x-cursor-no-total' = $true $Operation.'x-refresh-discards-cursor' = $true $Operation.'x-authorize-every-page' = $true $Operation.'x-invalid-or-expired-cursor' = '400_FAMILY_FEED_CURSOR_INVALID' $Operation.'x-cross-scope-cursor' = '404_FAMILY_FEED_NOT_AVAILABLE' } function New-CursorPage( [string]$ItemRef, [string]$CursorRef, [string[]]$Order ) { $page = New-ClosedObject ([ordered]@{ items = [ordered]@{ type = 'array' items = New-Ref $ItemRef minItems = 0 maxItems = 50 nullable = $false } nextCursor = New-Ref $CursorRef }) @('items') $page.'x-no-total' = $true $page.'x-next-cursor-absent-at-end' = $true $page.'x-order' = $Order $page.'x-read-window' = 'UPPER_BOUND_KEYSET_LATEST_VISIBLE' $page.'x-new-items-after-window' = 'EXCLUDED_UNTIL_REFRESH' $page.'x-deletion-or-visibility-change' = 'OMIT_ON_LATER_PAGE' $page.'x-edit-policy' = 'LATEST_VISIBLE_AT_PAGE_READ' return $page } function New-ValidDocument { $genealogyId = New-StringOwner 1 128 $identifierPattern $feedId = New-StringOwner 1 128 $identifierPattern $feedId.'x-opaque' = $true $feedId.'x-client-semantics' = 'COMPARE_ONLY' $commentId = New-StringOwner 1 128 $identifierPattern $commentId.'x-opaque' = $true $commentId.'x-client-semantics' = 'COMPARE_ONLY' $feedCursor = New-StringOwner 1 512 $cursorPattern $feedCursor.'x-opaque' = $true $feedCursor.'x-purpose' = 'FAMILY_FEED_PAGE' $commentCursor = New-StringOwner 1 512 $cursorPattern $commentCursor.'x-opaque' = $true $commentCursor.'x-purpose' = 'FAMILY_FEED_ROOT_COMMENT_PAGE' $feedContent = New-StringOwner 1 300 $feedContent.'x-text-normalizer' = 'FAMILY_FEED_TEXT_V1' $feedContent.'x-length-unit' = 'UNICODE_CODE_POINT' $commentContent = New-StringOwner 1 1000 $commentContent.'x-text-normalizer' = 'FAMILY_FEED_COMMENT_TEXT_V1' $commentContent.'x-length-unit' = 'UNICODE_CODE_POINT' $displayName = New-StringOwner 1 100 $displayName.'x-projection' = 'AUTHORIZED_DISPLAY_NAME_ONLY' $displayName.'x-missing-author-policy' = 'NON_EMPTY_SERVER_FALLBACK' $publishedAt = [ordered]@{ type = 'string' format = 'date-time' nullable = $false 'x-server-generated' = $true 'x-immutable' = $true } $feedItem = New-ClosedObject ([ordered]@{ authorDisplayName = New-Ref '#/components/schemas/FamilyFeedAuthorDisplayName' feedContent = New-Ref '#/components/schemas/FamilyFeedContent' feedId = New-Ref '#/components/schemas/FamilyFeedId' hasMedia = [ordered]@{ type = 'boolean' nullable = $false } publishedAt = New-Ref '#/components/schemas/FamilyFeedPublishedAt' }) @('authorDisplayName', 'feedContent', 'feedId', 'hasMedia', 'publishedAt') $feedItem.description = 'Annotations may mention phone or audit examples without becoming response fields.' $feedItem.example = [ordered]@{ annotationOnly = 'appUserPhone is not a schema property' } $feedItem.'x-projection' = 'VISIBLE_FEED_PRESENTATION_ONLY' $feedItem.'x-media-policy' = 'HAS_MEDIA_REQUIRES_HONEST_CLIENT_PLACEHOLDER_UNTIL_MEDIA_READ_CONTRACT' $commentItem = New-ClosedObject ([ordered]@{ authorDisplayName = New-Ref '#/components/schemas/FamilyFeedAuthorDisplayName' commentContent = New-Ref '#/components/schemas/FamilyFeedCommentContent' commentId = New-Ref '#/components/schemas/FamilyFeedCommentId' publishedAt = New-Ref '#/components/schemas/FamilyFeedPublishedAt' }) @('authorDisplayName', 'commentContent', 'commentId', 'publishedAt') $commentItem.'x-projection' = 'VISIBLE_COMMENT_PRESENTATION_ONLY' $commentItem.'x-comment-level' = 'ROOT_ONLY' $commentItem.'x-deleted-placeholder-policy' = 'EXCLUDE' $feedOrder = @('publishedAt:DESC', 'feedId:DESC_ORDINAL') $commentOrder = @('publishedAt:ASC', 'commentId:ASC_ORDINAL') $feedList = New-ReadOperation 'appListFamilyFeeds' @( (New-ClientIdParameter), (New-IdParameter 'genealogyId' '#/components/schemas/GenealogyId'), (New-CursorParameter '#/components/schemas/FamilyFeedCursor'), (New-LimitParameter) ) '#/components/schemas/RAppFamilyFeedCursorPage' @('tenant', 'genealogy', 'membership') Add-CursorContract $feedList @( 'tenant', 'account', 'authSession', 'client', 'genealogyId', 'projection', 'order', 'limit', 'windowUpperBound', 'lastTuple' ) $feedOrder $feedDetail = New-ReadOperation 'appGetFamilyFeed' @( (New-ClientIdParameter), (New-IdParameter 'genealogyId' '#/components/schemas/GenealogyId'), (New-IdParameter 'feedId' '#/components/schemas/FamilyFeedId') ) '#/components/schemas/RAppFamilyFeedReadItem' @( 'tenant', 'genealogy', 'membership', 'feedBelongsToGenealogy', 'feedVisibility' ) $comments = New-ReadOperation 'appListFamilyFeedRootComments' @( (New-ClientIdParameter), (New-IdParameter 'genealogyId' '#/components/schemas/GenealogyId'), (New-IdParameter 'feedId' '#/components/schemas/FamilyFeedId'), (New-CursorParameter '#/components/schemas/FamilyFeedRootCommentCursor'), (New-LimitParameter) ) '#/components/schemas/RAppFamilyFeedRootCommentCursorPage' @( 'tenant', 'genealogy', 'membership', 'feedBelongsToGenealogy', 'feedVisibility' ) Add-CursorContract $comments @( 'tenant', 'account', 'authSession', 'client', 'genealogyId', 'feedId', 'projection', 'order', 'limit', 'windowUpperBound', 'lastTuple' ) $commentOrder $schemas = [ordered]@{ GenealogyId = $genealogyId FamilyFeedId = $feedId FamilyFeedCommentId = $commentId FamilyFeedCursor = $feedCursor FamilyFeedRootCommentCursor = $commentCursor FamilyFeedContent = $feedContent FamilyFeedCommentContent = $commentContent FamilyFeedAuthorDisplayName = $displayName FamilyFeedPublishedAt = $publishedAt AppFamilyFeedReadItem = $feedItem AppFamilyFeedRootCommentReadItem = $commentItem AppFamilyFeedCursorPage = New-CursorPage '#/components/schemas/AppFamilyFeedReadItem' '#/components/schemas/FamilyFeedCursor' $feedOrder AppFamilyFeedRootCommentCursorPage = New-CursorPage '#/components/schemas/AppFamilyFeedRootCommentReadItem' '#/components/schemas/FamilyFeedRootCommentCursor' $commentOrder RAppFamilyFeedCursorPage = New-SuccessEnvelope '#/components/schemas/AppFamilyFeedCursorPage' RAppFamilyFeedReadItem = New-SuccessEnvelope '#/components/schemas/AppFamilyFeedReadItem' RAppFamilyFeedRootCommentCursorPage = New-SuccessEnvelope '#/components/schemas/AppFamilyFeedRootCommentCursorPage' RFamilyFeedReadBadRequest = New-ErrorEnvelope 400 @('FAMILY_FEED_CURSOR_INVALID', 'FAMILY_FEED_QUERY_INVALID') RFamilyFeedReadUnauthorized = New-ErrorEnvelope 401 @('AUTH_REQUIRED') RFamilyFeedReadNotFound = New-ErrorEnvelope 404 @('FAMILY_FEED_NOT_AVAILABLE') RFamilyFeedReadRateLimited = New-ErrorEnvelope 429 @('RATE_LIMITED') RFamilyFeedReadUnavailable = New-ErrorEnvelope 500 @('FAMILY_FEED_READ_UNAVAILABLE') } $document = [ordered]@{ openapi = '3.0.1' info = [ordered]@{ title = 'family feed read adversarial fixture' version = '1' } paths = [ordered]@{ '/genealogy/app/genealogies/{genealogyId}/feeds' = [ordered]@{ get = $feedList } '/genealogy/app/genealogies/{genealogyId}/feeds/{feedId}' = [ordered]@{ get = $feedDetail } '/genealogy/app/genealogies/{genealogyId}/feeds/{feedId}/comments' = [ordered]@{ get = $comments } } components = [ordered]@{ schemas = $schemas headers = [ordered]@{ PrivateNoStore = [ordered]@{ schema = [ordered]@{ type = 'string' enum = @('private, no-store') nullable = $false } } RetryAfter = [ordered]@{ schema = [ordered]@{ type = 'integer' minimum = 1 maximum = 300 nullable = $false } } } securitySchemes = [ordered]@{ SaToken = [ordered]@{ type = 'apiKey' in = 'header' name = 'Authorization' } } } } return ($document | ConvertTo-Json -Depth 100 | ConvertFrom-Json) } function Copy-Document([object]$Document) { return ($Document | ConvertTo-Json -Depth 100 | ConvertFrom-Json) } function Get-ContractIssues([object]$Document) { $json = $Document | ConvertTo-Json -Depth 100 [System.IO.File]::WriteAllText($temporaryPath, $json, [System.Text.UTF8Encoding]::new($false)) $output = @( & $contractPath -SkipProtectedParity -ReturnIssues -DocumentPath $temporaryPath ) return @($output | Where-Object { $_ -is [string] -and $_.Length -gt 0 }) } function Rename-NoteProperty([object]$Owner, [string]$From, [string]$To) { $property = @($Owner.PSObject.Properties | Where-Object { $_.Name -ceq $From })[0] if (-not $property) { throw "mutation setup missing property: $From" } $value = $property.Value $Owner.PSObject.Properties.Remove($From) $Owner.PSObject.Properties.Add([System.Management.Automation.PSNoteProperty]::new($To, $value)) } function Add-NoteProperty([object]$Owner, [string]$Name, [object]$Value) { $Owner.PSObject.Properties.Add([System.Management.Automation.PSNoteProperty]::new($Name, $Value)) } function Assert-MutantRejected( [object]$Seed, [string]$Label, [scriptblock]$Mutate, [string]$ExpectedIssuePattern ) { $mutant = Copy-Document $Seed & $Mutate $mutant $mutantIssues = @(Get-ContractIssues $mutant) if ($mutantIssues.Count -eq 0) { throw "adversarial mutant fake-greened: $Label" } if ($ExpectedIssuePattern -and -not (($mutantIssues -join "`n") -match $ExpectedIssuePattern)) { throw "adversarial mutant rejected for the wrong reason: $Label`n$($mutantIssues -join "`n")" } } try { $seed = New-ValidDocument $seedIssues = @(Get-ContractIssues $seed) if ($seedIssues.Count -gt 0) { throw "valid zero-issue seed was rejected:`n$($seedIssues -join "`n")" } $mutations = @( @{ Label = 'legacy /feeds/page GET owner' Pattern = 'legacy duplicate GET owner' Apply = { param($doc) Add-NoteProperty $doc.paths '/genealogy/app/genealogies/{genealogyId}/feeds/page' ([pscustomobject]@{ get = $doc.paths.'/genealogy/app/genealogies/{genealogyId}/feeds'.get }) } }, @{ Label = 'operationId drift' Pattern = 'operationId must be appListFamilyFeeds' Apply = { param($doc) $doc.paths.'/genealogy/app/genealogies/{genealogyId}/feeds'.get.operationId = 'list_17' } }, @{ Label = 'Path Item Get keyword casing' Pattern = 'Path Item keyword casing is invalid' Apply = { param($doc) Rename-NoteProperty $doc.paths.'/genealogy/app/genealogies/{genealogyId}/feeds' 'get' 'Get' } }, @{ Label = 'security object instead of array' Pattern = 'security must be a JSON array' Apply = { param($doc) $doc.paths.'/genealogy/app/genealogies/{genealogyId}/feeds'.get.security = [pscustomobject]@{ SaToken = @() } } }, @{ Label = 'SaToken key casing' Pattern = 'must require only exact SaToken' Apply = { param($doc) $requirement = $doc.paths.'/genealogy/app/genealogies/{genealogyId}/feeds'.get.security[0] Rename-NoteProperty $requirement 'SaToken' 'satoken' } }, @{ Label = 'non-empty SaToken scopes' Pattern = 'SaToken scopes must be an empty JSON array' Apply = { param($doc) $doc.paths.'/genealogy/app/genealogies/{genealogyId}/feeds'.get.security[0].SaToken = @('feed:read') } }, @{ Label = 'int64 feed identity' Pattern = 'sole exact local ref #/components/schemas/FamilyFeedId' Apply = { param($doc) $doc.components.schemas.AppFamilyFeedReadItem.properties.feedId = [pscustomobject]@{ type = 'integer'; format = 'int64' } } }, @{ Label = 'open feed projection' Pattern = 'AppFamilyFeedReadItem must be closed' Apply = { param($doc) $doc.components.schemas.AppFamilyFeedReadItem.additionalProperties = $true } }, @{ Label = 'phone field leak' Pattern = 'success graph leaks forbidden/internal field' Apply = { param($doc) Add-NoteProperty $doc.components.schemas.AppFamilyFeedReadItem.properties 'appUserPhone' ([pscustomobject]@{ type = 'string'; nullable = $false }) $doc.components.schemas.AppFamilyFeedReadItem.required += 'appUserPhone' } }, @{ Label = 'moderation field leak' Pattern = 'success graph leaks forbidden/internal field' Apply = { param($doc) Add-NoteProperty $doc.components.schemas.AppFamilyFeedRootCommentReadItem.properties 'moderationReason' ([pscustomobject]@{ type = 'string'; nullable = $false }) $doc.components.schemas.AppFamilyFeedRootCommentReadItem.required += 'moderationReason' } }, @{ Label = 'offset total in cursor page' Pattern = 'AppFamilyFeedCursorPage must be closed' Apply = { param($doc) Add-NoteProperty $doc.components.schemas.AppFamilyFeedCursorPage.properties 'total' ([pscustomobject]@{ type = 'integer'; nullable = $false }) $doc.components.schemas.AppFamilyFeedCursorPage.required += 'total' } }, @{ Label = 'pageNum query bypass' Pattern = 'parameters must be exactly' Apply = { param($doc) $operation = $doc.paths.'/genealogy/app/genealogies/{genealogyId}/feeds'.get $operation.parameters += [pscustomobject]@{ name = 'pageNum'; in = 'query'; required = $false schema = [pscustomobject]@{ type = 'integer'; minimum = 1; nullable = $false } } } }, @{ Label = 'cursor scope comma string' Pattern = 'x-cursor-scope must be the exact JSON array' Apply = { param($doc) $doc.paths.'/genealogy/app/genealogies/{genealogyId}/feeds'.get.'x-cursor-scope' = 'tenant,account,authSession' } }, @{ Label = 'cross-scope cursor returns 400' Pattern = 'cursor/refresh/per-page authorization semantics drifted' Apply = { param($doc) $doc.paths.'/genealogy/app/genealogies/{genealogyId}/feeds'.get.'x-cross-scope-cursor' = '400_FAMILY_FEED_CURSOR_INVALID' } }, @{ Label = '403 resource existence split' Pattern = 'responses must be exactly' Apply = { param($doc) Add-NoteProperty $doc.paths.'/genealogy/app/genealogies/{genealogyId}/feeds/{feedId}'.get.responses '403' ( New-Response '#/components/schemas/RFamilyFeedReadNotFound' ) } }, @{ Label = 'wildcard response media type' Pattern = 'must expose only application/json' Apply = { param($doc) $response = $doc.paths.'/genealogy/app/genealogies/{genealogyId}/feeds'.get.responses.'200' Rename-NoteProperty $response.content 'application/json' '*/*' } }, @{ Label = 'missing private cache header' Pattern = 'must define Cache-Control' Apply = { param($doc) $doc.paths.'/genealogy/app/genealogies/{genealogyId}/feeds'.get.responses.'200'.headers.PSObject.Properties.Remove('Cache-Control') } }, @{ Label = 'unbounded Retry-After' Pattern = 'Retry-After must be a non-null integer in 1..300' Apply = { param($doc) $doc.components.headers.RetryAfter.schema.maximum = 301 } }, @{ Label = 'generic list response' Pattern = '200 schema ref must be #/components/schemas/RAppFamilyFeedCursorPage' Apply = { param($doc) $doc.paths.'/genealogy/app/genealogies/{genealogyId}/feeds'.get.responses.'200'.content.'application/json'.schema.'$ref' = '#/components/schemas/RObject' } }, @{ Label = 'external feedId ref' Pattern = 'must be the sole exact local ref' Apply = { param($doc) $doc.components.schemas.AppFamilyFeedReadItem.properties.feedId.'$ref' = 'https://example.invalid/schemas.json#/FamilyFeedId' } }, @{ Label = 'hasMedia removed' Pattern = 'AppFamilyFeedReadItem must be closed' Apply = { param($doc) $doc.components.schemas.AppFamilyFeedReadItem.properties.PSObject.Properties.Remove('hasMedia') $doc.components.schemas.AppFamilyFeedReadItem.required = @( $doc.components.schemas.AppFamilyFeedReadItem.required | Where-Object { $_ -cne 'hasMedia' } ) } }, @{ Label = 'feed cursor order drift' Pattern = 'x-cursor-order must be the exact JSON array' Apply = { param($doc) $doc.paths.'/genealogy/app/genealogies/{genealogyId}/feeds'.get.'x-cursor-order' = @('feedId:DESC_ORDINAL', 'publishedAt:DESC') } }, @{ Label = 'mutable publishedAt' Pattern = 'must be a non-null immutable server-generated RFC3339' Apply = { param($doc) $doc.components.schemas.FamilyFeedPublishedAt.'x-immutable' = $false } }, @{ Label = 'feed content length drift' Pattern = 'FamilyFeedContent must be a non-null string length 1..300' Apply = { param($doc) $doc.components.schemas.FamilyFeedContent.maxLength = 301 } }, @{ Label = 'comment level internal field' Pattern = 'success graph leaks forbidden/internal field' Apply = { param($doc) Add-NoteProperty $doc.components.schemas.AppFamilyFeedRootCommentReadItem.properties 'commentLevel' ([pscustomobject]@{ type = 'string'; enum = @('root'); nullable = $false }) $doc.components.schemas.AppFamilyFeedRootCommentReadItem.required += 'commentLevel' } }, @{ Label = 'duplicate operationId' Pattern = 'operationId must be appListFamilyFeedRootComments|must have exactly one global operation owner' Apply = { param($doc) $doc.paths.'/genealogy/app/genealogies/{genealogyId}/feeds/{feedId}/comments'.get.operationId = 'appListFamilyFeeds' } }, @{ Label = 'explicit HEAD read bypass' Pattern = 'must not expose an explicit HEAD read bypass' Apply = { param($doc) Add-NoteProperty $doc.paths.'/genealogy/app/genealogies/{genealogyId}/feeds' 'head' ([pscustomobject]@{ responses = [pscustomobject]@{} }) } }, @{ Label = 'callback side channel' Pattern = 'must not define callbacks' Apply = { param($doc) Add-NoteProperty $doc.paths.'/genealogy/app/genealogies/{genealogyId}/feeds'.get 'callbacks' ([pscustomobject]@{ leak = [pscustomobject]@{} }) } }, @{ Label = 'schema Type keyword casing' Pattern = 'contains an unowned schema keyword: Type' Apply = { param($doc) Rename-NoteProperty $doc.components.schemas.FamilyFeedContent 'type' 'Type' } }, @{ Label = 'nextCursor made required' Pattern = 'AppFamilyFeedCursorPage must be closed' Apply = { param($doc) $doc.components.schemas.AppFamilyFeedCursorPage.required += 'nextCursor' } }, @{ Label = 'alternate APP GET reuses feed read projection' Pattern = 'alternate APP operation exposes the family-feed read projection' Apply = { param($doc) $shadow = Copy-Document $doc.paths.'/genealogy/app/genealogies/{genealogyId}/feeds'.get $shadow.operationId = 'shadowFamilyFeedRead' Add-NoteProperty $doc.paths '/genealogy/app/family-feed-shadow' ([pscustomobject]@{ get = $shadow }) } }, @{ Label = 'Response Object links side channel' Pattern = 'contains an unowned Response Object keyword: links' Apply = { param($doc) Add-NoteProperty $doc.paths.'/genealogy/app/genealogies/{genealogyId}/feeds'.get.responses.'200' 'links' ([pscustomobject]@{ next = [pscustomobject]@{ operationId = 'shadowFamilyFeedRead' } }) } }, @{ Label = 'Header Object content side channel' Pattern = 'contains an unowned Header Object keyword: content' Apply = { param($doc) Add-NoteProperty $doc.components.headers.PrivateNoStore 'content' ([pscustomobject]@{ 'application/json' = [pscustomobject]@{ schema = [pscustomobject]@{ type = 'string' } } }) } }, @{ Label = 'OpenAPI 3.1 dialect drift' Pattern = 'OpenAPI version must be exact 3.0.1' Apply = { param($doc) $doc.openapi = '3.1.0' } }, @{ Label = 'OpenAPI 3.1 webhooks keyword' Pattern = 'OpenAPI root contains an unowned keyword: webhooks' Apply = { param($doc) Add-NoteProperty $doc 'webhooks' ([pscustomobject]@{}) } }, @{ Label = 'OpenAPI Paths keyword casing' Pattern = 'OpenAPI root keyword casing is invalid: Paths' Apply = { param($doc) Rename-NoteProperty $doc 'paths' 'Paths' } }, @{ Label = 'OpenAPI Components keyword casing' Pattern = 'OpenAPI root keyword casing is invalid: Components' Apply = { param($doc) Rename-NoteProperty $doc 'components' 'Components' } }, @{ Label = 'cursor extension keyword casing' Pattern = 'operation keyword casing is invalid: X-Cursor-Scope' Apply = { param($doc) Rename-NoteProperty $doc.paths.'/genealogy/app/genealogies/{genealogyId}/feeds'.get 'x-cursor-scope' 'X-Cursor-Scope' } }, @{ Label = 'cursor scope comma join collision' Pattern = 'x-cursor-scope must be the exact JSON array' Apply = { param($doc) $doc.paths.'/genealogy/app/genealogies/{genealogyId}/feeds'.get.'x-cursor-scope' = @( 'tenant,account', 'authSession', 'client', 'genealogyId', 'projection', 'order', 'limit', 'windowUpperBound', 'lastTuple' ) } }, @{ Label = 'Parameter schema keyword casing' Pattern = 'Parameter Object keyword casing is invalid: Schema' Apply = { param($doc) $parameter = $doc.paths.'/genealogy/app/genealogies/{genealogyId}/feeds'.get.parameters[1] Rename-NoteProperty $parameter 'schema' 'Schema' } }, @{ Label = 'path parameter case-shadow' Pattern = 'parameters must be exactly' Apply = { param($doc) $pathItem = $doc.paths.'/genealogy/app/genealogies/{genealogyId}/feeds' Add-NoteProperty $pathItem 'parameters' @( [pscustomobject]@{ name = 'GenealogyId' in = 'path' required = $true schema = New-Ref '#/components/schemas/GenealogyId' } ) } }, @{ Label = 'Parameter Reference Object sibling' Pattern = 'parameter ref must contain only its exact local' Apply = { param($doc) Add-NoteProperty $doc.components 'parameters' ([pscustomobject]@{ GenealogyIdParameter = [pscustomobject]@{ name = 'genealogyId' in = 'path' required = $true schema = New-Ref '#/components/schemas/GenealogyId' } }) $doc.paths.'/genealogy/app/genealogies/{genealogyId}/feeds'.get.parameters[1] = [pscustomobject]@{ '$ref' = '#/components/parameters/GenealogyIdParameter' description = 'forbidden sibling' } } }, @{ Label = 'Schema Reference Object sibling' Pattern = 'must be the sole exact local ref' Apply = { param($doc) Add-NoteProperty $doc.paths.'/genealogy/app/genealogies/{genealogyId}/feeds'.get.parameters[1].schema 'description' 'forbidden sibling' } }, @{ Label = 'SaToken security scheme drift' Pattern = 'SaToken must be the exact apiKey/header/Authorization security owner' Apply = { param($doc) $doc.components.securitySchemes.SaToken = [pscustomobject]@{ type = 'oauth2' flows = [pscustomobject]@{} } } }, @{ Label = 'alternate 206 owner' Pattern = 'alternate APP operation exposes the family-feed read projection' Apply = { param($doc) $shadow = Copy-Document $doc.paths.'/genealogy/app/genealogies/{genealogyId}/feeds'.get $shadow.operationId = 'shadowFamilyFeed206' $shadow.responses = [pscustomobject]@{ '206' = New-Response '#/components/schemas/RAppFamilyFeedCursorPage' } Add-NoteProperty $doc.paths '/genealogy/app/family-feed-shadow-206' ([pscustomobject]@{ get = $shadow }) } }, @{ Label = 'replies path reuses canonical projection' Pattern = 'alternate APP operation exposes the family-feed read projection' Apply = { param($doc) $shadow = Copy-Document $doc.paths.'/genealogy/app/genealogies/{genealogyId}/feeds'.get $shadow.operationId = 'shadowRepliesFamilyFeed' Add-NoteProperty $doc.paths '/genealogy/app/genealogies/{genealogyId}/replies-shadow' ([pscustomobject]@{ get = $shadow }) } }, @{ Label = 'alternate HEAD projection owner' Pattern = 'alternate APP operation exposes the family-feed read projection' Apply = { param($doc) $shadow = Copy-Document $doc.paths.'/genealogy/app/genealogies/{genealogyId}/feeds'.get $shadow.operationId = 'shadowHeadFamilyFeed' Add-NoteProperty $doc.paths '/genealogy/app/family-feed-shadow-head' ([pscustomobject]@{ head = $shadow }) } }, @{ Label = 'alternate OPTIONS projection owner' Pattern = 'alternate APP operation exposes the family-feed read projection' Apply = { param($doc) $shadow = Copy-Document $doc.paths.'/genealogy/app/genealogies/{genealogyId}/feeds'.get $shadow.operationId = 'shadowOptionsFamilyFeed' Add-NoteProperty $doc.paths '/genealogy/app/family-feed-shadow-options' ([pscustomobject]@{ options = $shadow }) } }, @{ Label = 'callback GET projection owner' Pattern = 'alternate APP operation exposes the family-feed read projection' Apply = { param($doc) $shadow = Copy-Document $doc.paths.'/genealogy/app/genealogies/{genealogyId}/feeds'.get $shadow.operationId = 'shadowCallbackFamilyFeed' Add-NoteProperty $doc.paths '/genealogy/app/callback-carrier' ([pscustomobject]@{ post = [pscustomobject]@{ operationId = 'callbackCarrier' responses = [pscustomobject]@{ '204' = [pscustomobject]@{ description = 'accepted' } } callbacks = [pscustomobject]@{ leak = [pscustomobject]@{ '{$request.body#/callbackUrl}' = [pscustomobject]@{ get = $shadow } } } } }) } }, @{ Label = 'inline alternate projection owner' Pattern = 'alternate APP operation exposes the family-feed read projection' Apply = { param($doc) $shadow = Copy-Document $doc.paths.'/genealogy/app/genealogies/{genealogyId}/feeds'.get $shadow.operationId = 'shadowInlineFamilyFeed' $shadow.responses.'200'.content.'application/json'.schema = Copy-Document $doc.components.schemas.RAppFamilyFeedCursorPage Add-NoteProperty $doc.paths '/genealogy/app/family-feed-shadow-inline' ([pscustomobject]@{ get = $shadow }) } }, @{ Label = 'two-hop schema alias owner' Pattern = 'alternate APP operation exposes the family-feed read projection' Apply = { param($doc) Add-NoteProperty $doc.components.schemas 'FamilyFeedAliasTwo' (New-Ref '#/components/schemas/RAppFamilyFeedCursorPage') Add-NoteProperty $doc.components.schemas 'FamilyFeedAliasOne' (New-Ref '#/components/schemas/FamilyFeedAliasTwo') $shadow = Copy-Document $doc.paths.'/genealogy/app/genealogies/{genealogyId}/feeds'.get $shadow.operationId = 'shadowSchemaAliasFamilyFeed' $shadow.responses.'200'.content.'application/json'.schema = New-Ref '#/components/schemas/FamilyFeedAliasOne' Add-NoteProperty $doc.paths '/genealogy/app/family-feed-shadow-schema-alias' ([pscustomobject]@{ get = $shadow }) } }, @{ Label = 'two-hop response alias owner' Pattern = 'alternate APP operation exposes the family-feed read projection' Apply = { param($doc) Add-NoteProperty $doc.components 'responses' ([pscustomobject]@{ FamilyFeedAliasOne = New-Ref '#/components/responses/FamilyFeedAliasTwo' FamilyFeedAliasTwo = New-Response '#/components/schemas/RAppFamilyFeedCursorPage' }) $shadow = Copy-Document $doc.paths.'/genealogy/app/genealogies/{genealogyId}/feeds'.get $shadow.operationId = 'shadowResponseAliasFamilyFeed' $shadow.responses = [pscustomobject]@{ '200' = New-Ref '#/components/responses/FamilyFeedAliasOne' } Add-NoteProperty $doc.paths '/genealogy/app/family-feed-shadow-response-alias' ([pscustomobject]@{ get = $shadow }) } }, @{ Label = 'external alternate response schema' Pattern = 'schema ref is not an inspectable exact local component ref' Apply = { param($doc) $shadow = Copy-Document $doc.paths.'/genealogy/app/genealogies/{genealogyId}/feeds'.get $shadow.operationId = 'shadowExternalFamilyFeed' $shadow.responses.'200'.content.'application/json'.schema = New-Ref 'https://example.invalid/feed.json#/FeedPage' Add-NoteProperty $doc.paths '/genealogy/app/family-feed-shadow-external' ([pscustomobject]@{ get = $shadow }) } }, @{ Label = 'default response projection owner' Pattern = 'alternate APP operation exposes the family-feed read projection' Apply = { param($doc) $shadow = Copy-Document $doc.paths.'/genealogy/app/genealogies/{genealogyId}/feeds'.get $shadow.operationId = 'shadowDefaultFamilyFeed' $shadow.responses = [pscustomobject]@{ default = New-Response '#/components/schemas/RAppFamilyFeedCursorPage' } Add-NoteProperty $doc.paths '/genealogy/app/family-feed-shadow-default' ([pscustomobject]@{ get = $shadow }) } }, @{ Label = 'Response Reference Object sibling' Pattern = 'response ref must contain only its exact local' Apply = { param($doc) Add-NoteProperty $doc.components 'responses' ([pscustomobject]@{ FeedListSuccess = Copy-Document $doc.paths.'/genealogy/app/genealogies/{genealogyId}/feeds'.get.responses.'200' }) $doc.paths.'/genealogy/app/genealogies/{genealogyId}/feeds'.get.responses.'200' = [pscustomobject]@{ '$ref' = '#/components/responses/FeedListSuccess' description = 'forbidden sibling' } } }, @{ Label = 'Header Reference Object sibling' Pattern = 'header ref must contain only its exact local' Apply = { param($doc) Add-NoteProperty $doc.paths.'/genealogy/app/genealogies/{genealogyId}/feeds'.get.responses.'200'.headers.'Cache-Control' 'description' 'forbidden sibling' } }, @{ Label = 'Parameter content side channel' Pattern = 'Parameter Object contains an unowned keyword: content' Apply = { param($doc) Add-NoteProperty $doc.paths.'/genealogy/app/genealogies/{genealogyId}/feeds'.get.parameters[1] 'content' ([pscustomobject]@{ 'application/json' = [pscustomobject]@{ schema = New-Ref '#/components/schemas/GenealogyId' } }) } }, @{ Label = 'unknown operation extension' Pattern = 'operation contains an unowned keyword: x-shadow-owner' Apply = { param($doc) Add-NoteProperty $doc.paths.'/genealogy/app/genealogies/{genealogyId}/feeds'.get 'x-shadow-owner' 'legacy' } } ) foreach ($mutation in $mutations) { Assert-MutantRejected $seed $mutation.Label $mutation.Apply $mutation.Pattern } Write-Output "FAMILY-FEED-READ-OPENAPI-ADVERSARIAL-CONTRACT PASS MUTANTS=$($mutations.Count)" } finally { if (Test-Path -LiteralPath $temporaryPath -PathType Leaf) { Remove-Item -LiteralPath $temporaryPath -Force } }