Ctrl K
ComunidadGuías…

CHANGELOG.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# Changelog

All notable changes to MiniStack will be documented here.

Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
Versioning follows [Semantic Versioning](https://semver.org/).

## [Unreleased]

### Fixed
- **ECS — `ListServiceDeployments` and `DescribeServiceDeployments`** — both answered an empty list for every service, so the AWS provider's wait on a new `aws_ecs_service` never finished once it took the service-deployment path (it does when the deployment's `createdAt` is after the time the client noted before `CreateService`). Each deployment of a service is now listed, newest first, with AWS's ARNs, `createdAt` and `status` filters and `status` moving from `IN_PROGRESS` to `SUCCESSFUL`, and described with its source and target revisions. Contributed by @kharabasz.
- **Route 53 — metric and calculated health checks no longer get `RequestInterval` and `FailureThreshold`** — `CreateHealthCheck` added `RequestInterval` 30 and `FailureThreshold` 3 to `CLOUDWATCH_METRIC` and `CALCULATED` checks, so Terraform replaced them on every plan. Only endpoint checks get them now, as on AWS, and a metric check without `InsufficientDataHealthStatus` gets `LastKnownStatus`. `CreateHealthCheck` and `UpdateHealthCheck` also reject what AWS rejects for metric, calculated and recovery-control checks, with AWS's `InvalidInput` messages and order: a metric check now needs an existing metric alarm in its `AlarmIdentifier` region (not a percentile, metric-math or high-resolution alarm), fields the type cannot carry are refused, a calculated check needs a `HealthThreshold` and existing child checks, and a recovery-control check cannot be updated. An `UpdateHealthCheck` with no members succeeds, a body that is not XML answers `MalformedInput`, and child health check IDs are no longer stored empty. MiniStack does not check that a routing control exists. Fixes #2603. Contributed by @kharabasz.
### Added
- **SES — sending events through EventBridge** — configuration set event destinations were stored but never fired. Each send now publishes `aws.ses` events to the set's EventBridge destination, with the outcomes AWS gives the mailbox simulator. Adds `PutEmailIdentityConfigurationSetAttributes`; EventBridge now delivers to CloudWatch Logs log group targets.

## [1.5.39] — 2026-11-08

### Added
- **Secrets Manager — `RemoveRegionsFromReplication` and `StopReplicationToReplica`** — both answered `Unknown action`, so `terraform destroy` of a secret with a `replica` block failed. The first deletes the listed regions' replicas and returns the remaining `ReplicationStatus`; the second, called in a replica's region, makes it a standalone secret. Contributed by @kharabasz.
- **Cognito — attribute verification** — adds `GetUserAttributeVerificationCode` and `VerifyUserAttribute`. A changed email or phone number now becomes unverified and gets a code where the pool auto-verifies it, and `AttributesRequireVerificationBeforeUpdate` keeps the old value until the new one is verified. Contributed by @toby6578.
- **Cognito — hosted UI forgot password and MFA** — the hosted sign-in pages now have a forgot-password flow, ask for a TOTP or SMS code after the password, and enrol TOTP where the pool requires MFA. Messages match AWS's classic hosted UI; the pages render straight from `POST /login`, and reset codes are checked only with `AUTH=true`. Contributed by @toby6578.
- **Cognito — RFC 8707 resource indicators on managed login** — on a `ManagedLoginVersion` 2 domain, a `resource` naming one of the pool's resource servers now becomes the access token's `aud`, and refreshes keep it. An unregistered one fails the code exchange with `invalid_grant`. Contributed by @toby6578.
- **Cognito — the `PostAuthentication` trigger** — `LambdaConfig.PostAuthentication` was stored and never invoked. It now runs after a password, MFA or challenge sign-in through the API, before tokens are issued and before `NEW_PASSWORD_REQUIRED`, but not on refresh. A failing Lambda fails the sign-in with `UserLambdaValidationException`, `InvalidLambdaResponseException` or `UnexpectedLambdaException`. Reported by @ryuzo-hayashi.
- **CloudFront — `cf.kvs()` in CloudFront Functions** — `import cf from 'cloudfront'` failed in `cloudfront-js-2.0`. `get()` (string, json or bytes), `exists()` and `meta()` now read the function's associated KeyValueStore; a missing key throws `KeyNotFound`, and `cf.kvs()` in a function without a store throws `InternalError`. Reported by @mkusaka.
- **KMS — the `aws/s3` AWS managed key** — an S3 write with `aws:kms` and no key ID (or `alias/aws/s3`) was refused. S3 now creates the account's `aws/s3` key on first use (KeyManager `AWS`, AWS's description and key policy, alias `alias/aws/s3`) and encrypts with it, as does a bucket default of `aws:kms` without `KMSMasterKeyID`. Contributed by @kharabasz.
- **S3 — PutObject through another account's access point** — it was refused. It now succeeds when the caller's identity policy, the access point policy and the owner's bucket policy all allow it, and lands in the owner's bucket under its default encryption; a missing grant answers AWS's AccessDenied for that step. Contributed by @kharabasz.

### Changed
- **Secrets Manager — a request body that is not one JSON object** — an empty or blank body was read as `{}`, and `[]`, `null` or a bare string crashed with a 500. Every action now answers `SerializationException` for a body that is not a single JSON object (a list names itself, the rest carry no message); whitespace around the object is fine. Contributed by @kharabasz.
- **Secrets Manager — `DeleteSecret` on a primary that still has replicas** — it deleted the replicas with the primary. It now answers `InvalidParameterException` ("You can't delete secret … that still has replica regions [...]"), so replicas are removed first; called in a replica's region it answers "Operation not permitted on a replica secret". Contributed by @kharabasz.

### Fixed
- **RDS — Aurora member reboots no longer stick** — after `ModifyDBCluster` rotated the master password, a rebooted member stayed `rebooting` or went `failed`; its readiness check now uses the cluster's password. Deleting an instance no longer recomputes every cluster's status, which left a cluster with a member mid-reboot in `creating`. Contributed by @jayjanssen.
- **Auto Scaling — `InstanceRequirements` launch templates and overrides are checked** — a plain `LaunchTemplate` with `InstanceRequirements` now answers `InvalidQueryParameter`, and bad overrides (neither or both of `InstanceType` and `InstanceRequirements`, mixed kinds, partial weights, duplicates, more than 40) answer `ValidationError` with AWS's messages. Contributed by @kharabasz.
- **S3 — SSE-KMS keys are resolved through KMS** — a write stored the key reference as given and accepted keys that do not exist. It now stores and echoes the key ARN and refuses an unknown key with `KMS.NotFoundException` before any authorization, as on AWS; a write naming a key MiniStack's KMS does not hold now fails, so create the key first. Contributed by @kharabasz.
- **S3 — bucket policies on `s3:x-amz-server-side-encryption-aws-kms-key-id`** — with `AUTH=true` the key was never set, so a policy denying uploads under any other KMS key also denied the right one. Writes now carry the key ARN however the key is named, or the bucket default's, and multipart parts carry their upload's key. Contributed by @kharabasz.
- **Auto Scaling — `DesiredCapacityType` is reported as AWS reports it** — since 1.5.38 every group reported `units`, so Terraform planned a change on every run. Only a group with a `MixedInstancesPolicy` now has one, `vcpu` and `memory-mib` need `InstanceRequirements`, and state saved by 1.5.38 is cleaned on restore. Contributed by @kharabasz.
- **CloudFront — `Compress: true` compresses responses** — origin responses passed through uncompressed. They are now gzipped, or Brotli-encoded when the optional `brotli` package is installed, under AWS's conditions (status, size, `Content-Type`, no `Content-Encoding`); a strong `ETag` becomes weak, and the origin gets the normalized `Accept-Encoding` (`br,gzip`, `gzip`, `br` or `identity`). Reported by @skialpine.
- **Cognito — MFA through the API** — a user whose pool requires MFA and who has no factor signed in without one. They now get `MFA_SETUP` and, once enrolled, a `SOFTWARE_TOKEN_MFA` challenge; a TOTP enrolled on the hosted pages is challenged by the API too, and five wrong codes lock the user out for a growing time, at most 15 minutes.
- **IAM — request condition keys with `AUTH=true`** — keys such as `s3:signatureversion`, `s3:x-amz-storage-class`, `kms:EncryptionContext:*`, `secretsmanager:VersionStage`, `ssm:Overwrite`, `sns:Protocol`, `ec2:Region` and `iam:PermissionsBoundary` were never set, so a policy conditioned on them denied valid requests. They are now set from the request.
- **S3 — `ListMultiRegionAccessPoints` pages** — the token was a position, so a create or delete between pages skipped or repeated an access point; it now names the last one returned.

## [1.5.38] — 2026-11-05

### Added
- **RDS — IAM database authentication for PostgreSQL and Aurora PostgreSQL** — a role granted `rds_iam` could not log in with a token from `generate_db_auth_token`. With IAM database authentication enabled it now logs in with one, checked as for MySQL (with `AUTH=true` also the token and `rds-db:connect`); other users keep password authentication. Reported by @kharabasz.
- **Cognito — refresh token rotation** — a client with `RefreshTokenRotation` `ENABLED` now gets a new refresh token from `GetTokensFromRefreshToken` and `/oauth2/token`. Reusing a replaced token outside `RetryGracePeriodSeconds` answers `RefreshTokenReuseException`, and `REFRESH_TOKEN_AUTH` answers `UnsupportedOperationException` for such a client. Contributed by @toby6578.
- **Cognito — the `CustomEmailSender` trigger** — `LambdaConfig.CustomEmailSender` was stored and never invoked. It now receives invitation, sign-up, resend, forgot-password and email OTP messages with AWS's event, the code encrypted as an AWS Encryption SDK message under the pool's `KMSKeyID`, so it decrypts through MiniStack's KMS. Contributed by @toby6578.
- **Cognito — managed login branding** — `CreateManagedLoginBranding`, `DescribeManagedLoginBranding`, `DescribeManagedLoginBrandingByClient`, `UpdateManagedLoginBranding` and `DeleteManagedLoginBranding` answered `InvalidAction`. A branding is now kept per app client with AWS's checks in AWS's order, and `AWS::Cognito::ManagedLoginBranding` deploys in CloudFormation. Contributed by @toby6578.
- **ALB — the `authenticate-oidc` signing key is served** — the PEM key that signs `x-amzn-oidc-data` is now served by `kid` on `public-keys.auth.elb.<region>.amazonaws.com/<kid>` and `/_ministack/elb/public-keys/<kid>`; an unknown `kid` answers 403 `AccessDenied`. Reported by @boesing.

### Changed
- **botocore 1.43.124** — the pinned botocore, whose models MiniStack reads for request and response shapes, moves from 1.43.106 to 1.43.124.

### Fixed
- **S3 and S3 Tables — Object Lock, multipart and metadata tokens** — with `PERSIST_STATE=1` and `S3_PERSIST=1`, retention and legal holds now survive a restart. `CompleteMultipartUpload` answers `InvalidPartOrder`, `MalformedXML` and `InvalidPart` for out-of-order, empty or ETag-less part lists. S3 Tables keeps one `versionToken` per table and answers `ConflictException` for a stale one.
- **Cognito — refreshes, codes and invitations** — `GetTokensFromRefreshToken` now requires a confidential client's secret and refuses an expired token ("Refresh Token has expired"). `AdminCreateUser` `RESEND` issues a new temporary password, sign-up and reset codes go where AWS sends them, and destinations mask as `k***@e***`. Contributed by @toby6578.
- **ALB — `authenticate-oidc` refreshes the access token** — the session kept an expired access token for its whole timeout. It now refreshes the token and user claims from the IdP, signs the user in again when it cannot, and passes `AuthenticationRequestExtraParams`. Reported by @boesing.
- **ALB — the data plane finds load balancers in every account and region** — a request by `DNSName`, `<name>.alb.localhost` or `/_alb/<name>/` found only the default account's load balancers and fell through to S3. Every account and region is now searched, preferring the request's own. Contributed by @kharabasz.
- **ALB — `/_alb/<name>/` selects the load balancer whatever the `Host`** — a `Host` naming another load balancer won and kept the `/_alb/<name>/` prefix on the path. The path now selects the load balancer and `Host` only feeds its listener rules. Reported by @skialpine.
- **EventBridge — `PutEvents` through a global endpoint** — `EndpointId` was ignored. Endpoints now get an `EndpointId` and `EndpointUrl`, `PutEvents` delivers in the primary region with the endpoint ARN in `resources` and replicates to the secondary bus, and `CreateEndpoint` and `UpdateEndpoint` make AWS's checks. Reported by @kharabasz.
- **Core — SigV4a requests run as the signing account with `AUTH` unset** — a SigV4a (`AWS4-ECDSA-P256-SHA256`) request, such as `PutEvents` with `EndpointId`, ran as `000000000000`. Its access key now names the account as a SigV4 one does. Contributed by @kharabasz.
- **ACM — certificate `Options`** — `RequestCertificate` dropped `Options`; they are now stored with AWS's defaults (`ENABLED` logging, `DISABLED` export). `UpdateCertificateOptions` keeps members not sent, and both operations answer AWS's `SerializationException`, `ValidationException` and `InvalidStateException`. Contributed by @kharabasz.
- **EFS — file system policies and creation tokens** — `PutFileSystemPolicy` added an `Id` and a random `Sid`; policies now come back in AWS's canonical form and malformed ones answer `InvalidPolicyException`. A reused `CreationToken` answers 409 `FileSystemAlreadyExists`. Contributed by @kharabasz.
- **Auto Scaling — `SpotInstancePools`, filters and deletes** — `SpotInstancePools` now defaults to 2 only for `lowest-price` and is refused with any other strategy; updates keep unsent `MixedInstancesPolicy` members. Describe calls apply their `Filters` and name lists, and deleting a group with instances needs `ForceDelete`. Contributed by @kharabasz.
- **API Gateway — imported OpenAPI documents keep `ANY` methods and parameters** — `ImportRestApi`, `PutRestApi` and an `AWS::ApiGateway::RestApi` `Body` skipped `x-amazon-apigateway-any-method` and an operation's `parameters`. They now become an `ANY` method and `method.request.*` parameters. Contributed by @mishukdutta-cz.
- **Lambda — MiniStack's failures and handler errors as AWS reports them** — MiniStack's own failures answered 200 with a "Mock response"; they now answer 500 `ServiceException`. Local handler and init errors carry the runtime's error document (`Runtime.ImportModuleError`, `Runtime.HandlerNotFound`, ...), with the exception's own type.
- **Lambda — exiting runtimes and Docker networks** — a runtime that exits answers `Runtime.ExitError` "Runtime exited with error: exit status N". A containerised MiniStack without `DOCKER_NETWORK` starts Lambda containers on its own network instead of the unreachable default bridge. Reported by @kharabasz.
- **Lambda — the local Node shim has `ListTagsCommand` and `Lambda.send()`** — Amplify Gen 2 deploys failed with `ListTagsCommand is not a constructor` under `LAMBDA_EXECUTOR=local`. Both now exist, matching `@aws-sdk/client-lambda`. Contributed by @bpmiranda3099.
- **ECS and RDS — images from MiniStack's ECR and Aurora volumes** — on the containerd image store, ECS tasks pinned to a digest failed "pull access denied for sha256"; they now run, and Lambda, AgentCore and CodeBuild images load from MiniStack's ECR too. With `RDS_PERSIST=0`, Aurora storage volumes are labelled and removed. Reported by @kharabasz.
- **Docker-backed services — taken host ports are skipped** — a containerised MiniStack saw ports other containers publish as free, so RDS, ElastiCache, OpenSearch, EKS, MWAA and DocumentDB containers failed "port is already allocated". Those ports are now skipped and a lost race retries the next port. Reported by @kharabasz.
- **DSQL, DocumentDB and MWAA — tenants and volumes** — DSQL clusters are kept per account and region, DocumentDB container names carry account and region so one tenant can't replace another's, and `MWAA_PERSIST`/`DSQL_PERSIST` volumes are labelled and removed with their resource.
- **AppSync, API Gateway domains and CloudFormation signals — reached from any account** — API-key GraphQL requests, AppSync Events, custom domains and wait-condition signals found only the default account's resources. They now run in the owner's account and region.
- **Error codes and statuses are AWS's** — SNS, ElastiCache, DocumentDB and CloudWatch used shape names as wire codes (SNS now `InvalidParameter`, `ResourceNotFound`). CloudTrail, Transfer, Backup and Bedrock errors are 400, OpenSearch `ResourceNotFoundException` is 409, and IoT and Firehose internal errors are 500.
- **Requests keep what they send** — about 40 services, among them ALB, Auto Scaling, EC2, ECS, RDS, ElastiCache, DynamoDB and Glue, dropped create and update parameters. They are now stored, returned and updatable, network and gateway load balancers get `net/` and `gwy/` ARNs, and updates keep or replace what AWS does (SSM `PutParameter` policies, RDS pending changes; Kinesis resharding closes parent shards).
- **Responses carry AWS's members and pages** — responses are trimmed to botocore's output shapes, without internal members or `null`s. List and Describe calls honour page sizes and return the token, omitted on the last page. SES v2 `GetEmailIdentity`, SSM `Policies`, Cloud Map `DiscoverInstances` and Bedrock `GetGuardrail` match AWS.
- **IAM, CloudControl and WAFv2** — attaching a policy that does not exist answers `NoSuchEntity`, CloudControl uses the type's primary identifier, and `ListResourcesForWebACL` filters by `ResourceType`, defaulting to `APPLICATION_LOAD_BALANCER`.
- **Query and XML responses escape `&` and `<`** — Auto Scaling, ALB and EC2 values such as a tag `R&D` made every later Describe for the account unparseable. They are now escaped.

### Security
- **Aurora DSQL — the backend Postgres is no longer on every interface** — with `DSQL_STRICT=1` each cluster's trust-auth Postgres was published on `0.0.0.0`. It is now unpublished on MiniStack's network, or `127.0.0.1` only for a host-run MiniStack. Contributed by @toby6578.
- **EventBridge — `DescribeConnection` no longer returns secrets** — it returned the stored `ApiKeyValue`, OAuth `ClientSecret` and basic-auth `Password`, which AWS never returns.
- **Containers follow MiniStack's binding** — RDS, ElastiCache, OpenSearch, MWAA, EKS, DocumentDB and ECS task ports were published on every interface even with MiniStack bound to `127.0.0.1`; they now follow it.

## [1.5.37] — 2026-11-03

### Added
- **Glue — table optimizers** — `CreateTableOptimizer`, `GetTableOptimizer`, `UpdateTableOptimizer`, `DeleteTableOptimizer`, `BatchGetTableOptimizer` and `ListTableOptimizerRuns` answered `InvalidAction`. Optimizers are now stored per Iceberg table and type with AWS's defaults and errors, and dropping a table or database removes them. They never run. Reported by @lucasmfraser.
- **ALB — hosts resolve through Route 53 alias records** — the data plane found a load balancer only by its `DNSName` or `<name>.alb.localhost`. A host with an `A`/`AAAA` alias record in a public hosted zone, exact or wildcard (`*.example.com`, the most specific name winning), now reaches the load balancer the record targets, so one ALB can serve many hostnames. Reported by @boesing.
- **Testcontainers — .NET example** — `Testcontainers/dotnet-testcontainers` runs MiniStack from Testcontainers for .NET with the AWS SDK for .NET.

### Fixed
- **ElastiCache — user groups reach the engine** — user groups on a replication group or serverless cache were never applied, so commands ran as the open `default` user. The users now reach the engine on every change, `default` is off unless a group has one, and users can't run `ACL` writes or `CONFIG`. A change restarts the engine, dropping open connections. Contributed by @chris-plucker. Reported by @kharabasz.
- **ElastiCache — user group members and the built-in `default` user** — `ModifyUserGroup` and `CreateUserGroup` accepted current members, non-members, unknown users and duplicates; they now refuse them in AWS's order and messages. Every account has the built-in `default` user, which "can't be modified." or "can't be deleted.", and `DeleteUser` takes a user out of its groups. Reported by @kharabasz.
- **ElastiCache — user channels and `&` in access strings** — a user with a selector and no top-level channel rule, such as `on ~* +@all (+get ~k)`, got every channel outside its selectors; it now gets none, as on AWS. Responses escape `&` and every other value, which botocore could not parse. Contributed by @kharabasz.
- **ElastiCache — a Redis OSS user group needs a `default` user** — `CreateUserGroup` and `ModifyUserGroup` accepted a Redis group with no user named `default`; they now answer `DefaultUserRequired` "Redis user group needs to contain a user with the user name default." `ModifyUserGroup` also applies an `Engine` change to `valkey`, which lifts the rule. Contributed by @kharabasz.
- **ElastiCache — `ModifyUser` applies `AppendAccessString`** — the appended rules were ignored. They are now applied, the result stored with rules ordered and merged; refused rules answer `InvalidParameterValue`, and sending it with `AccessString` answers `InvalidParameterCombination` "Both AccessString and AppendAccessString provided together. Pass either one of them." Contributed by @kharabasz.
- **CloudFormation — `AWS::ElastiCache::UserGroup` updates and ElastiCache failures** — an update changing the users or `Engine` leaves the group with exactly the template's users; one changing only tags or order keeps changes made outside the stack. A user listed twice fails validation ("#/UserIds: array items are not unique"). Failures read `Resource handler returned message: "<message> (Service: ElastiCache, …)"`, as on AWS. Contributed by @kharabasz.
- **DynamoDB — a value SET at several paths is stored once per path** — `SET a = :v, b = :v`, `SET a = src, b = src`, `if_not_exists`, `list_append(:l, :l)` and PartiQL `UPDATE ... SET` stored one shared value, so a later nested write to one path showed up at the others. Each path now gets its own copy. Contributed by @toby6578.
- **ECS — task definition secrets** — `AWS::ECS::TaskDefinition` converted only a few container fields, so `Secrets` were never injected and `RuntimePlatform` and `EphemeralStorage` were dropped; every nested property now reaches the ECS API's form. An SSM `SecureString` secret reaches the container decrypted, not as its stored `ENCRYPTED:` form. Reported by @dimazuien.
- **ECS — tasks run images from MiniStack's own ECR** — a task whose image was a MiniStack ECR `repositoryUri` was pulled from the real `*.dkr.ecr.*.amazonaws.com` host. ECS now loads it from MiniStack's registry for the host's platform, and under `AUTH=true` another account's repository needs a policy allowing the pull. Reported by @kharabasz.
- **CloudFormation — a `secretsmanager` dynamic reference reads a secret in another account** — a reference by ARN looked only in the stack's account. It now resolves in the account the ARN names, and under `AUTH=true` only when the secret's policy and its customer managed KMS key allow the caller, as on AWS. Contributed by @mishukdutta-cz.
- **CloudFormation and Lambda — files in another account's bucket** — a `TemplateURL`, a nested stack, an `AWS::Include` `Location`, a state machine `DefinitionS3Location` or Lambda code and layers in a bucket another account owns were looked for only in the caller's account. They are now read when the owner's bucket ACLs or policy allow it, as an S3 `GetObject` would be. Contributed by @mishukdutta-cz.
- **Lambda — a declared architecture pins the container platform** — a CloudFormation or SAM function ran on the host's platform whatever its `Architectures` said. It now runs the declared platform; a host that cannot run it falls back once and remembers, and `--platform` in `LAMBDA_DOCKER_FLAGS` wins. Contributed by @toby6578.
- **Lambda — cold starts** — the tar copied into a new container stayed in memory until a full garbage collection, so bursts of cold starts grew MiniStack by hundreds of MB; it is now freed when the copy ends. Concurrent cold starts of one function no longer collide on the context-ARN shim. Contributed by @skialpine. Reported by @chandra-purbia.
- **Containers — failed and abandoned ones are cleaned up** — a Lambda container whose start failed after `create` was left behind, and a Docker timeout was taken for an unrunnable platform. Stopped Lambda, Glue, CodeBuild, DSQL and AgentCore containers are now reaped, and spawned containers carry MiniStack's Testcontainers session labels so Ryuk can remove them. Contributed by @kharabasz.
- **EC2 — port ranges are ignored where the protocol covers all ports** — the Terraform provider revokes a new group's default egress rule as `-1` with ports `0`, which matched nothing, so a later allow-all rule lost its description and tags. `-1` and protocols other than tcp, udp, icmp and icmpv6 now ignore `FromPort`/`ToPort`, as botocore documents. Contributed by @kharabasz.
- **RDS — cluster endpoints** — under `MINISTACK_RDS_PUBLIC_ENDPOINT`, a cluster's endpoints and port changed with its first instance, so Terraform showed drift; they are now reported from `CreateDBCluster`. The global writer endpoint now resolves to the writer on the Docker network, follows switchover, failover, rename and removal, and is in every PostgreSQL member's certificate. Contributed by @kharabasz.
- **Step Functions — Map states** — a JSONata Distributed Map now evaluates `Items`, `ItemSelector`, `MaxConcurrency` and `Output`, and `ItemBatcher` gives each child one batch as `{"BatchInput", "Items"}`, up to 256 KiB by default. An Inline Map with `ItemReader`, `ItemBatcher` or `ResultWriter` fails with `States.Runtime` ("…not supported for INLINE maps"), which no `Catch` or `Retry` handles, as on AWS. Contributed by @mishukdutta-cz.
- **Step Functions — stops and `.sync` children** — after `StopExecution`, a `Wait` kept sleeping and branches kept running, so events landed after `ExecutionAborted`; nothing follows it now. A failed, stopped or timed-out `.sync` child fails the parent with `States.TaskFailed` and the child's `DescribeExecution` as the cause, as on AWS. Contributed by @mishukdutta-cz.

## [1.5.36] — 2026-11-02

### Added
- **CloudFormation — `AWS::Events::Archive`** — a template with an EventBridge archive was refused as an unrecognized resource type, though the archive API itself was supported. It now creates the archive: `Ref` is the archive name, a generated one is the logical id cut to 35 characters, `-` and 12 letters and digits, and `Fn::GetAtt Arn` is its ARN; `Fn::GetAtt` on any other property rolls the stack back with AWS's "must be a readonly property in schema". The event pattern is stored compact, with non-ASCII characters as written and each object's keys in the order AWS stores them. A changed `Description`, `RetentionDays`, `EventPattern` or `KmsKeyIdentifier` updates the archive in place, and one left out of the template keeps its value (an empty `KmsKeyIdentifier` clears the key); an update that leaves none of the first three fails with the handler's message, so rolling an update back to such a template ends `UPDATE_ROLLBACK_FAILED`, as on AWS. A new `ArchiveName` replaces the archive, a new `SourceArn` replaces a generated one and is refused for a custom name, and a stack delete removes it. Early Validation refuses a template that breaks the resource schema (a missing `SourceArn`, an `ArchiveName` too long, too short or outside its pattern, a value of the wrong type, an unsupported property, a `KmsKeyIdentifier` over 2048 characters) and a new archive whose name is taken, with AWS's messages and in AWS's order. The handler refuses what the schema lets through, as on AWS: a name or `SourceArn` outside the API's pattern, a negative `RetentionDays`, a KMS key that does not exist, and relays EventBridge's own refusals (a taken name, a bus that does not exist, an invalid pattern) in its `Resource handler returned message` form with the SDK's suffix. Measured against a real account in eu-west-1. Contributed by @toby6578.
- **CloudFormation — `AWS::CloudFormation::StackSet` with one stack instance in its own account and Region** — a template with a StackSet was refused as an unrecognized resource type. A `SELF_MANAGED` StackSet whose one `StackInstancesGroup` targets the stack's own account and Region now deploys its inline `TemplateBody` as a stack instance through the nested-stack engine, which updates it, rolls a failed update back with the stack and deletes it. `Ref` returns the `StackSetId`; `DescribeStackSet` (by name, or by `StackSetId` also once deleted) and `ListStackInstances` answer with the shapes of a real-AWS capture. Any other StackSet fails the resource with an error naming what is deployed. Contributed by @chris-plucker.
- **S3 Control — account-level public access block** — `PutPublicAccessBlock`, `GetPublicAccessBlock` and `DeletePublicAccessBlock` on `/v20180820/configuration/publicAccessBlock`, which answered `InvalidURI`. The configuration is held per account and applies in every region, and its settings apply to the account's buckets with their own (`BlockPublicPolicy` refuses a public bucket policy); `DeletePublicAccessBlock` is authorized as `s3:PutAccountPublicAccessBlock`. An unset `GetPublicAccessBlock` answers 404 `NoSuchPublicAccessBlockConfiguration` carrying `<AccountId>` inside `<Error>`, and an unsupported method 405 `MethodNotAllowed` with `ResourceType` `ACCOUNT_PUBLIC_ACCESS_BLOCK`. Responses and errors verified against a real AWS account. Contributed by @pingedbrain.
- **SSM — documents and automation executions** — `CreateDocument`, `DescribeDocument`, `GetDocument`, `ListDocuments`, `DeleteDocument`, and `StartAutomationExecution`, `GetAutomationExecution`, `DescribeAutomationExecutions`, `StopAutomationExecution`, which answered `InvalidAction`. Documents are stored per account and region with AWS's real shapes: `Hash` is the SHA-256 of the verbatim content, `Status` goes `Creating` → `Active`, `PlatformTypes` defaults to all three platforms, `CreatedDate` is sent as epoch seconds, `GetDocument` returns `DocumentFormat` `JSON` by default with the content re-serialized in AWS's canonical JSON layout, and a document's declared `parameters` appear in the description. Content is validated on create: unparsable JSON/YAML answers `InvalidDocumentContent` with AWS's "not well-formed" messages, an Automation document without `mainSteps` `Missing steps in "mainSteps".`, an unknown step action `Unknown action: <action>`, a bad `DocumentType` AWS's `ValidationException`, a duplicate name `DocumentAlreadyExists`, and a name with a prefix AWS reserves (`aws`, `amazon`, `amzn`, `AWSEC2`, `AWSConfigRemediation`, `AWSSupport`) is refused. Executions build real `StepExecutions` from the document's `mainSteps` (one `StepExecutionId` each, inputs JSON-stringified with `{{ param }}` references resolved from declared defaults and passed `Parameters`), expose `CurrentStepName`/`CurrentAction` while running, and run the steps they can: `aws:sleep` waits its ISO-8601 duration, `aws:executeAwsApi` dispatches into the emulated `ssm` API, `aws:runCommand` dispatches `SendCommand`; a step MiniStack cannot execute fails the execution with AWS's `FailureMessage`/`FailureDetails` shape instead of reporting a fabricated success, and `StopAutomationExecution` lands `Cancelled`; under `AUTH=true` a step calls the API with the permissions of the document's `assumeRole`, or of the caller, so a step the caller may not run fails with `AccessDeniedException`. `StartAutomationExecution` answers `AutomationDefinitionNotFoundException` for a document the account doesn't hold and `InvalidAutomationExecutionParametersException` for undeclared parameters. Responses and errors verified against a real AWS account. Contributed by @pingedbrain.

### Fixed
- **API Gateway — REST, HTTP and WebSocket APIs answer callers from any account** — an execute-api call reached an API only in the account its access key selected, so an unsigned call, which MiniStack serves as the default account, or a call signed by another account answered 404 `{"message": "Not Found"}` for an API another account owns. On AWS an execute-api URL reaches its API whatever the caller (captured on AWS): a method without authorization answered 200 unsigned, with another account's `Authorization` header and when signed by another account. The call now runs as the API's owner account, and the caller is authenticated in its own account. Under `AUTH=true` an `AWS_IAM` method answers a caller from another account that no REST API policy grants with 403 `AccessDeniedException` `{"Message": "User: <caller> is not authorized to access this resource"}`, as AWS does, and a policy allowing that account lets it in. An HTTP or WebSocket API is served as its owner the same way. Contributed by @chandra-purbia.
- **CloudFormation — validation and provisioning errors carry no `OperationStatus`** — `DescribeEvents` gave a create or update's `VALIDATION_ERROR` and `PROVISIONING_ERROR` events `OperationStatus` `FAILED`, which AWS's don't have. Measured against a real account in eu-west-1. Contributed by @toby6578.
- **CloudFormation — change sets run Early Validation** — a change set whose template fails pre-deployment validation was created and failed only on execution; `CreateChangeSet` now checks the resources it adds or modifies and fails it as AWS does: `FAILED` with "The following hook(s)/validation failed: [AWS::EarlyValidation::...]", naming the `ResourceExistenceCheck` and `PropertyValidation` hooks in the order of their first errors, keeping its changes. `DescribeEvents` for the change set (`OperationType` `CREATE_CHANGESET`) lists, newest first, its outcome, its validation errors by logical id and its start, and the stack's own events don't include them; a `CREATE` or `UPDATE` change set that succeeds has its start and `SUCCEEDED`, and one with no changes has none. `DisableValidation` skips it. `ExecuteChangeSet` on a failed change set answers AWS's "cannot be executed in its current status of [FAILED]". AWS let some invalid in-place changes through to fail at execution, from a change set or `UpdateStack`, and refused others at validation; MiniStack refuses them all at validation. Measured against a real account in eu-west-1. Contributed by @toby6578.
- **CloudWatch Logs — every stored event has an `eventId`** — events written by a Lambda, a CodeBuild build or a DynamoDB import came back from `FilterLogEvents` without the `eventId` AWS always returns, so `aws logs tail --follow` stopped with `KeyError: 'eventId'` on a Lambda's log group. Contributed by @chandra-purbia.
- **Cognito — a failing PreTokenGeneration Lambda fails the request** — a trigger that returned an error, including running past its own timeout, was logged and the token issued without its claims, and one that could not be invoked did the same unless `MINISTACK_COGNITO_PRETOKEN_STRICT=1` turned either into a 500. As on AWS, `InitiateAuth`, `RespondToAuthChallenge`, the refresh flows and `/oauth2/token` now answer 400 `UserLambdaValidationException` `<trigger> failed with error <errorMessage>.` for the first and `UnexpectedLambdaException` for the second; `/oauth2/token` carries the text and code in `error`. `MINISTACK_COGNITO_PRETOKEN_STRICT` is removed. Contributed by @ryuzo-hayashi.
- **Cognito — PreSignUp and CustomMessage Lambda errors** — a Lambda that returned an error failed the request with its raw error body as the message; it is now AWS's `UserLambdaValidationException` text, `PreSignUp failed with error <errorMessage>.` and `CustomMessage failed with error <errorMessage>.`
- **ElastiCache — replication group endpoints after a restart** — with persisted state, a replication group's containers were respawned on restart but its `PrimaryEndpoint`, `ReaderEndpoint`, members' `ReadEndpoint` and `ConfigurationEndpoint` kept the old container's address; they now report the new one, as cache clusters already did. Reported by @chris-plucker.
- **EventBridge — an archive created without a description or a pattern has neither** — `DescribeArchive` returned `"Description": ""` and `"EventPattern": ""`; it now leaves both keys out, as AWS does. Contributed by @toby6578.
- **EventBridge — `CreateArchive` refusals** — an archive on an event bus that does not exist was created; it now answers `ResourceNotFoundException` "Event bus <name> does not exist.", and a taken name's `ResourceAlreadyExistsException` ends with a period, as AWS's does. Contributed by @toby6578.
- **EventBridge — `PutEvents` to a bus ARN in another region** — an entry naming an event bus ARN in a region other than the request's failed on its own with `ResourceNotFoundException`. AWS refuses the whole request instead: it answered 400 `ValidationException` "Cross-region api call is not allowed." for a bus in the caller's own account (captured on AWS). `PutEvents` now answers the same, before any entry is delivered. Contributed by @chandra-purbia.
- **EventBridge — rule targets in another account** — `PutTargets` accepted an SQS (or other) target in another account without a `RoleArn`, and the events were then silently dropped. As on AWS, it now answers `ValidationException` "RoleArn is required for target <arn>." for such a target in any region, and with an execution role an SQS target in another account of the same region receives the events, as AWS's cross-account targets do, and under `AUTH=true` only when the queue's policy allows that role. A target in another region is still accepted and not reached. Contributed by @chandra-purbia.
- **EventBridge — rule targets that are event buses in another account or region** — `PutTargets` accepted such a target and every event was then dropped ("event bus … not found"). An event now reaches the target bus's rules in its owner's account and region, keeping its `account` and `region`; crossing into another region gives it a new id and time. As on AWS, a forwarded event reaches another bus in the same region only once and crosses regions only once (after which it may make one more same-region hop), which also stops a chain of rules from looping, so the per-rule recursion guard is gone. `PutTargets` answers AWS's errors for event bus targets: after its existing ARN checks, 400 `AccessDeniedException` `Access to the resource <role> is denied. Reason: Cross-account pass role is not allowed.` for another account's role; then, target by target, `Source EventBus and Target EventBus must not be the same.` for the rule's own bus and `Retry policy is not supported for Event bus targets.` for any `RetryPolicy`; then `RoleArn is required for target <arn>.` for any bus target without a role, the rule's own account and region included (it was `RoleArn is required`, and only for another account or region). CloudFormation's `AWS::Events::Rule` now runs `PutTargets`' checks on create, as it did on update, so a target that is not a well-formed ARN of a supported service now fails the create too. With `AUTH=true` the `RoleArn` must name the rule account's role exactly, its trust policy must let EventBridge assume it for the rule (conditions see the rule's account as `aws:SourceAccount` and its ARN as `aws:SourceArn`), and it must be allowed `events:PutEvents` on the bus, with `aws:RequestedRegion` the bus's region; a Deny in the bus's policy refuses it, and a bus in another account must also allow it, with `events:eventBusInvocation` `true` for its conditions. MiniStack does not model target dead-letter queues, so a refused event is dropped and logged with the `ERROR_CODE` AWS records (`NO_PERMISSIONS`, `THIRD_ACCOUNT_HOP_DETECTED` or `THIRD_REGION_HOP_DETECTED`). A schedule whose target is an event bus still publishes to it as before. Recorded with three accounts in eu-west-2, us-east-1 and us-west-2. Contributed by @toby6578. Reported by @lucasmfraser.
- **KMS — keys in other accounts, missing keys and denials answer as AWS does** — another account's key named by key ARN or alias ARN, or by the ciphertext `Decrypt` is given, is now governed by its key policy in both auth modes. A refusal names the key when it exists and not when it does not; operations AWS does not serve across accounts answer `This operation cannot be called cross account`; another Region's ARN answers `Invalid arn <region>`. A key or alias that does not exist answers `NotFoundException` before any permission check, worded as AWS words it, and a denial no policy allows is HTTP 400 naming the key. `DescribeKey` carries `AWSAccountId`, `GetKeyRotationStatus` carries `KeyId` (and no period while rotation is off), and the default key policy has AWS's layout. Recorded with two accounts in us-east-1. Contributed by @Jolley71717. Reported by @kharabasz.
- **Lambda — `nodejs24.x` refuses callback-based handlers** — a handler taking a callback ran on `nodejs24.x`; as AWS's Node.js 24 runtime does, a handler with three or more parameters that is not a response-streaming handler now fails with `Runtime.CallbackHandlerDeprecated` "ERROR: AWS Lambda has removed support for callback-based function handlers starting with Node.js 24. …", with the local executor and with `LAMBDA_EXECUTOR=docker`, and local workers carry `AWS_EXECUTION_ENV` (`AWS_Lambda_<runtime>`) as AWS's runtimes do.
- **Lambda — Node.js functions in a `"type": "module"` package under `LAMBDA_EXECUTOR=docker`** — the context shim MiniStack adds to a Node function was written as `_msctx_shim.js`, which Node loads as an ES module when the package's `package.json` says `"type": "module"`, so every invoke failed with `ReferenceError: require is not defined in ES module scope`. It is now `_msctx_shim.cjs`, which Node always loads as CommonJS, and the function runs as it does on AWS. Contributed by @Jolley71717. Reported by @evan-burrell.
- **Lambda — a function with no declared architecture after an arm64 one** — with `LAMBDA_EXECUTOR=docker`, invoking an `arm64` function pulled the arm64 image under the shared runtime tag, and a function that declared no architecture then ran that image on an x86 host and failed with `exec format error`; the host's image is now pulled back and run.
- **RDS — a cluster master password change is visible before it settles** — `ModifyDBCluster` with `MasterUserPassword` changed the password at once and every `DescribeDBClusters` reported `available`, so a client waiting to observe the reset never saw it. As captured from real AWS, the `ModifyDBCluster` response and the first `DescribeDBClusters` show the cluster `available` with `PendingModifiedValues.MasterUserPassword` `****`, the next reports `resetting-master-credentials` (still pending), and later ones `available` with nothing pending. A password change on a stopped cluster now fails with `InvalidDBClusterStateFault` ("Db cluster <id> cannot be in the stopped state."), as AWS does. Contributed by @chris-plucker.
- **RDS — default Aurora cluster parameter groups** — `DescribeDBClusterParameterGroups` and `DescribeDBClusterParameters` answered `DBParameterGroupNotFound` for a default group such as `default.aurora-postgresql16`, and a new cluster reported `default.aurora-postgresql` with no version; each Aurora family in the engine-version catalog now has its `default.<family>` group, and a cluster created without a parameter group reports its engine version's default, as AWS names it; an unfiltered `DescribeDBClusterParameterGroups` lists a default group only once it exists in the account (described by name or used by a cluster), as AWS does, and a major engine upgrade moves a cluster on the default group to the target version's default while keeping a custom group (the upgrade is reported as pending, then `upgrading`, then done, and member instances on the old default group follow), and a default group cannot be modified (`InvalidParameterValue` "Cannot modify a default parameter group.") or deleted (`InvalidDBParameterGroupState` "Default DBClusterParameterGroup cannot be deleted: <name>"), while a reset succeeds; deleting a cluster parameter group that does not exist answers "DBClusterParameterGroup not found: <name>". Contributed by @chris-plucker.
- **RDS — `ModifyDBCluster` major upgrades need `AllowMajorVersionUpgrade`** — a change to another major version went through without it; it now answers 400 `InvalidParameterCombination` "The AllowMajorVersionUpgrade flag must be present when upgrading to a new major version.", as AWS's Aurora upgrade guide shows.
- **S3 — `Tagging` on `CreateMultipartUpload`** — the `x-amz-tagging` header was ignored, so an object uploaded in parts had no tags, including S3-to-S3 copies by `aws s3 cp` of objects large enough to copy in parts. The tags now land on the completed object's version, decide tag-filtered replication rules and carry to the replica, as with PutObject; more than 10 answer 400 `BadRequest` "Object tags cannot be greater than 10" at create time, and an untagged multipart upload that overwrites a tagged object leaves it without tags, as on AWS. Contributed by @zalefin.
- **S3 — an overwritten object's tags and ACL** — `PutObject` and a POST upload without tags or an ACL kept the overwritten object's, so `GetObjectTagging` and `GetObjectAcl` reported them on the new object; as on AWS, the new object starts with no tags and the default ACL.
- **S3 Tables — Iceberg `last-column-id` counts nested fields** — a table created through the Iceberg REST catalog reported the number of top-level fields as its `last-column-id`, and `add-schema` advanced it to the highest top-level id, so with a list, map or struct column a client's next column reused a nested field's id and Trino's `ADD COLUMN` failed with `Multiple entries with same key`. It is now the highest field id anywhere in the schema, a list's `element-id` and a map's `key-id` and `value-id` included, as on AWS. Contributed by @Jolley71717. Reported by @Wazabit.
- **S3 Tables — Iceberg REST `dropTable` and `dropNamespace`** — `DELETE …/namespaces/{ns}/tables/{table}` and `DELETE …/namespaces/{ns}` answered `Unknown Iceberg REST path`, so Trino's `DROP TABLE` failed. A table is now dropped, with its metadata files, when the call sets `purgeRequested=true`, so a table created again under the same name starts empty; without the flag the answer is AWS's `purge_enabled` error. A namespace that still holds tables answers `namespace_not_empty`. `HEAD` on a namespace is served, and `HEAD` on a table or namespace answers 204 as AWS does. Contributed by @Jolley71717. Reported by @Wazabit.
- **S3 Tables — Iceberg REST `createTable` location and `stage-create`** — a `"location": null`, which Trino sends for a table without a location, was stored as the table's location, so Trino failed with `Cannot parse to a string value: location: null`. As on AWS, the location is now always the one S3 Tables assigns, whatever the client sends, and a create without `stage-create`, or with a null one, answers 400 `required_field_is_null` "stage-create is a required field and cannot be null". Reported by @Wazabit.
- **S3 Tables — Iceberg REST `createTable` renumbers field ids** — the client's field ids were kept; as on AWS they are now renumbered from 1, each struct's fields before their nested types, with `last-column-id`, the partition spec's and the sort order's `source-id`s following the new ids, and a `write-order` becomes the table's sort order.
- **Secrets Manager — a replicated secret reports its own `PrimaryRegion`** — `DescribeSecret` and `ListSecrets` returned `PrimaryRegion` only for a replica, so a client reading the Region of a secret it replicated got nothing; the primary of a replicated secret now reports its own Region, as AWS does, a replica still reports its source Region, and a secret that is not replicated still has none. Contributed by @chris-plucker.
- **Step Functions — `ecs:runTask.sync` fails with the task** — the state waited for the task to stop and then always succeeded, with the whole DescribeTasks response as its result. As on AWS, a task whose essential container exits non-zero, or that never starts, now fails the state with `States.TaskFailed`, the stopped task as JSON in PascalCase (epoch-millisecond timestamps) as the cause, so `Retry` and `Catch` apply; otherwise the state's result is that task document. Reported by @lucasmfraser.

## [1.5.35] — 2026-10-30

### Added
- **API Gateway — `PutRestApi`** — `PUT /restapis/{id}` answered 404, so Terraform's `aws_api_gateway_rest_api` with a `body` failed; it now imports an OpenAPI or Swagger definition (JSON or YAML) in `merge` mode (the default) or `overwrite` mode, which also removes the API's authorizers, and in both modes the API takes its name from `info.title` its description from `info.description` (removed when absent) and its `version` from `info.version`. An unknown API answers `Invalid API identifier specified <account>:<id>`, a body that is no definition `Unable to determine OAS/Swagger version` and a bad `mode` AWS's enum validation message. `ImportRestApi` now imports its definition too, where it created an empty API named `unnamed`. Reported by @mindsers.
- **API Gateway — client certificates** — `GenerateClientCertificate`, `GetClientCertificate`, `GetClientCertificates`, `UpdateClientCertificate` and `DeleteClientCertificate`, with a 6-character id, a self-signed PEM certificate (C=US, L=Seattle, CN=ApiGateway) valid for one year, tags on the certificate's ARN, and AWS's errors: `Invalid client certificate id specified` (404, and 400 for a stage's `/clientCertificateId` patch) and `Invalid patch path  '<path>' specified for op '<op>'. Must be one of: [/description]`. Reported by @mindsers.
- **API Gateway — domain name access associations** — `CreateDomainNameAccessAssociation`, `GetDomainNameAccessAssociations`, `DeleteDomainNameAccessAssociation` and `RejectDomainNameAccessAssociation` for private custom domain names, named by their `domainNameArn`, with the ARN `.../domainnameaccessassociations/domainname/<name>+<domainNameId>/vpcesource/<vpce>` and tags on it. A duplicate, an unknown domain or a VPC endpoint EC2 doesn't hold answer 400 `BadRequestException` "The domain name access association between this VPC endpoint and domain name already exists.", "Domain not found" and "<vpce> is not a valid VPC endpoint id"; rejecting an association of your own, or any after the domain is deleted, answers 404 "This private custom domain name doesn't have any domain name access associations." and keeps it. Reported by @mindsers.
- **ELBv2 — listener certificates** — `AddListenerCertificates`, `DescribeListenerCertificates` and `RemoveListenerCertificates`, as on AWS: one certificate per call, `CertificateNotFound` for a certificate ACM doesn't hold, the default certificate listed last with `IsDefault` true and never removed. Reported by @kharabasz.
- **IAM — service-specific credentials and server certificates** — clients that call `ListServiceSpecificCredentials` or `ListServerCertificates` got `InvalidAction`. `CreateServiceSpecificCredential`, `ListServiceSpecificCredentials`, `UpdateServiceSpecificCredential`, `ResetServiceSpecificCredential` and `DeleteServiceSpecificCredential` now answer: a CodeCommit or Keyspaces credential is a `ServiceUserName` (`<user>-at-<account>`) and `ServicePassword`, a Bedrock, Claude Platform on AWS, CloudWatch or CloudWatch Logs one a `ServiceCredentialAlias` and `ServiceCredentialSecret` with the `ExpirationDate` from `CredentialAgeDays`; the password or secret is returned only on create and reset, another service answers `NotSupportedService` (404), and `UserName` may be omitted by an IAM-user caller while a non-user caller without it gets `InvalidInput`. `UploadServerCertificate`, `ListServerCertificates`, `GetServerCertificate` (with its `CertificateChain`), `UpdateServerCertificate`, `DeleteServerCertificate` and `ListServerCertificateTags` keep names unique across paths, parse `Expiration` from the PEM (left out on the slim image, which has no `cryptography`), and answer `MalformedCertificate` for a body that is not a certificate. Contributed by @pingedbrain.

### Fixed
- **API Gateway — custom domain name responses** — a regional domain name now has its `domainNameArn` `arn:aws:apigateway:<region>::/domainnames/<name>`, `certificateUploadDate` and a `d-<10 characters>.execute-api.<region>.amazonaws.com` regional name, and responses leave out the fields AWS doesn't send for the endpoint type, empty values among them; `endpointConfiguration` carries `ipAddressType` and `tags` appear on `GetDomainName`. Reported by @cupidchan.
- **API Gateway — private custom domain names** — the `domainNameId` 1.5.34 gave a private domain had 8 characters where AWS's has 10; a private domain now also defaults `ipAddressType` to `dualstack`, and a `policy` statement on `execute-api:/*` is returned naming the domain's own `execute-api` ARN, as on AWS. Reported by @mindsers.
- **AppConfig — `KmsKeyArn` is built from the identifier as given** — 1.5.34 resolved an alias to its key's ARN and only reported a `KmsKeyArn` for a key MiniStack's KMS held; as on AWS, a key ARN or alias ARN is kept, a key id becomes `arn:aws:kms:<region>:<account>:key/<id>` and an alias name `arn:aws:kms:<region>:<account>:alias/<name>`, without checking the key exists. A hosted version created under a profile with a key carries its `KmsKeyArn`, and `StartDeployment` of such a version answers "KmsKeyIdentifier required when deploying a hosted configuration version that is encrypted with a customer managed key (CMK)" without a `KmsKeyIdentifier`, and KMS's messages for a key that does not exist, is disabled or is in another account. Reported by @kharabasz.
- **AppConfig — `ListDeployments` and tag errors** — deployments are listed newest first with `ConfigurationProfileId` and `Type` `USER`, and `ListTagsForResource`, `TagResource` and `UntagResource` answer AWS's errors for an ARN without an account id (400) and another account's ARN (403 `AccessDeniedException`). Reported by @kharabasz.
- **Auth — HTTP/2 clients that sign `:authority`** — with `AUTH=true`, a request signed with an IAM user or STS session key over HTTP/2, as the JavaScript SDK v3 does for Kinesis, Bedrock Runtime and other clients, answered `SignatureDoesNotMatch`: Hypercorn passes `:authority` to the app as `host`, so it was signed as empty. A signed `:authority` now takes the `host` value. Reported by @ScriptSmith.
- **Auto Scaling — `MixedInstancesPolicy`** — `CreateAutoScalingGroup` and `UpdateAutoScalingGroup` dropped it, so the group reported no launch template; it is now kept, in CloudFormation too, with the launch template's id and name filled in, `Overrides` `[]` when none are given and AWS's `InstancesDistribution` defaults, and the group then has no `LaunchTemplate`. A group with both a `LaunchTemplate` and a `MixedInstancesPolicy`, or with no launch source at all, answers "Valid requests must contain either LaunchTemplate, LaunchConfigurationName, InstanceId or MixedInstancesPolicy parameter." Reported by @kharabasz.
- **Auto Scaling — launch template request checks** — a `LaunchTemplate` with both or neither of `LaunchTemplateId` and `LaunchTemplateName` answers "Valid requests must contain either launchTemplateId or LaunchTemplateName", a version other than `$Default`, `$Latest` or a number "Invalid launch template version: either '$Default', '$Latest', or a numeric version are allowed.", and an unknown id "Malformed launch template Id: <id>" or, for a short `lt-` id or one in another region, "The specified launch template, with template ID <id>, does not exist.", on create and update, as on AWS. Reported by @kharabasz.
- **CloudFront — `UpdateFunction` returns its ETag where the SDKs read it** — AWS sends `UpdateFunction`'s ETag in a header named `ETtag`, which is where botocore and the other SDKs read it; MiniStack sent `ETag`, so an SDK client got no ETag back and Terraform's `aws_cloudfront_function` failed to publish a code change with `InvalidIfMatchVersion`. Reported by @kharabasz.
- **CloudFront — Functions LIVE versions, publish, delete and precondition errors** — each stage now counts its own versions: `PublishFunction` moves LIVE to its next version, with the ETag of that version number, and leaves DEVELOPMENT's ETag unchanged, where 1.5.34 copied DEVELOPMENT's ETag to LIVE. `PublishFunction` no longer sends an `ETag` header, `DeleteFunction` of a function a distribution uses answers 409 `FunctionInUse` "Cannot delete function <name>, it is in use by <n> distributions", and a stale `If-Match` on publish or delete answers `PreconditionFailed` "The request failed because it didn't meet the preconditions in one or more request-header fields." Reported by @kharabasz.
- **CloudFront — viewer requests to an alternate domain name** — the data plane served a distribution only at its `<label>.cloudfront.net` name; a request whose `Host` is one of its `Aliases` fell through to path-style S3 (`NoSuchBucket`). It is now served by the distribution that claims that name, as on AWS: the `Host` header chooses it, case-insensitively and ignoring the port; an exact alias beats every wildcard; `*.S` matches any host ending in `.S`, at any depth, but not `S` itself; and the longest matching wildcard wins, whichever distribution is newer. A host another service claims (an API Gateway custom domain, a Function URL, an S3 virtual host, an OpenSearch Serverless collection, a load balancer) still goes to that service. The viewer function sees the `Host` lower-cased and without its port, and a SigV4-signed API call is never treated as viewer traffic. Contributed by @skialpine.
- **EC2 — launch templates keep `InstanceRequirements`** — `CreateLaunchTemplate` and `CreateLaunchTemplateVersion` dropped it, so `aws_launch_template` planned a change on every run; it is now returned as sent, and a template data with both `InstanceType` and `InstanceRequirements` answers `InvalidParameterCombination` "Either the instance type or the instance requirements can be specified in the request, but not both." Reported by @kharabasz.
- **ECS — capacity provider strategies have their `base`** — a strategy item given without `base` is reported with `base` 0 on the service and its deployments, and on the cluster's default strategy from `CreateCluster`, `PutClusterCapacityProviders`, `DescribeClusters` and CloudFormation, as on AWS. Reported by @kharabasz.
- **ElastiCache — a serverless cache with a user group enforces it** — the cache's engine had no users, so any client connected as `default`; the group's users now exist in the engine with their access strings and passwords, `default` is off unless the group holds a user named `default`, and an unauthenticated command answers `NOAUTH Authentication required.`. An `iam` user logs in with its token, which is not verified. Reported by @kharabasz.
- **ElastiCache — host ports already in use are skipped** — a cache took the next host port from a counter without checking it was free, so a port held by another container or process sent a serverless cache to `create-failed`; ports in use are now skipped. Reported by @kharabasz.
- **ElastiCache — IAM users and authentication modes** — `CreateUser` and `ModifyUser` ignored `AuthenticationMode`, so an IAM user was reported as needing no password; users now report `Authentication` `{"Type": "iam"}` or `{"Type": "password", "PasswordCount": n}` and their `MinimumEngineVersion`, and refuse an IAM user whose name differs from its id, an IAM user with passwords and a `no-password-required` Valkey user with AWS's messages. `ModifyUser` answers `modifying`. Reported by @kharabasz.
- **EventBridge Scheduler — `EventBridgeParameters` are validated and an event bus target publishes as AWS does** — `CreateSchedule` and `UpdateSchedule` accepted any `Target.EventBridgeParameters`; they now answer AWS's `SerializationException` for parameters or members sent as the wrong JSON type, then AWS's aggregated `ValidationException` for a missing or empty `DetailType` or `Source`, a `DetailType` over 128 or a `Source` over 256 characters, and a `Source` outside AWS's pattern (such as one starting with `aws.` or holding a line break), before the schedule or group is looked up and for any target type. An event a schedule puts on an event bus now has empty `resources` instead of the schedule ARN, and an event bus target without `EventBridgeParameters` no longer publishes an `aws.scheduler` event. Contributed by @kharabasz.
- **EventBridge — `PutEvents` to another account's event bus** — an entry whose `EventBusName` is the ARN of a bus another account owns in the same region failed with `ResourceNotFoundException`, so no other account could publish to a bus. The event is now delivered to the owner's rules and keeps the publisher's `account`. Under `AUTH=true` the bus policy (a `PutPermission` grant or `Policy`) must allow the caller, with an `events:source` condition checked for each entry; otherwise the whole request answers `AccessDeniedException` "User: <caller> is not authorized to perform: events:PutEvents on resource: <bus ARN> because no resource-based policy allows the events:PutEvents action", naming the first refusing bus in request order. A bus that does not exist refuses the same way. Contributed by @kharabasz.
- **EventBridge — `PutPermission` with a `Policy`** — the policy was ignored and `DescribeEventBus` returned a generic statement, so Terraform showed drift; it now replaces the bus's policy and is returned compact, the legacy form produces AWS's statement, and a malformed policy, a statement on another resource, or a `Policy` combined with `Action`, `Principal`, `StatementId` or `Condition` answer AWS's `ValidationException` messages. Reported by @kharabasz.
- **KMS — another account's key ARN** — a key named by another account's key ARN answered `NotFoundException`; for the operations AWS allows across accounts it is now used from its owner's account, and with `AUTH=true` its key policy must allow the caller (root included), else AWS's `AccessDeniedException` "User: <caller> is not authorized to perform: kms:<action> on this resource because the resource does not exist in this Region, no resource-based policies allow access, or a resource-based policy explicitly denies access", the same answer as for a key that doesn't exist. Reported by @kharabasz.
- **Lambda — cross-account Kinesis mappings follow AWS's access checks** — `CreateEventSourceMapping` on another account's stream now checks the stream's policy under `AUTH=true`: the function's role needs `DescribeStream`, `DescribeStreamSummary`, `GetRecords`, `GetShardIterator` and `ListShards`, and a missing stream or grant answers AWS's `Cannot access stream ...` message. Under `AUTH=true` a running mapping needs only `GetRecords`; without it the mapping stops and reports AWS's `PROBLEM: User: ... is not authorized to perform ...` result (naming `DescribeStream` when the whole grant is gone), and resumes when the grant returns. A mapping on a missing stream in its own account answers `Stream not found`. Contributed by @Jolley71717.
- **RDS — global cluster `Endpoint` and Aurora PostgreSQL versions** — global clusters report `Endpoint` `<id>.global-<12 characters>.global.rds.amazonaws.com`, even with no members, so `aws_rds_global_cluster.endpoint` is filled in; a global cluster also reports `EngineLifecycleSupport`, `StorageEncryptionType` (`sse-rds`, or `sse-kms` when encrypted) and a 16-character `GlobalClusterResourceId`, leaves out an empty `DatabaseName`, and Aurora PostgreSQL defaults to 17.9; and `DescribeDBEngineVersions` lists the current Aurora PostgreSQL versions, adding 14.24, 15.19, 16.15, 17.11 and 18.6. Reported by @kharabasz and @mindsers.
- **Route 53 — `InvalidChangeBatch` messages** — a duplicate `CREATE` and a missing `DELETE` answer AWS's bracketed messages, `[Tried to create resource record set [name='<name.>', type='<type>'] but it already exists]` and `[Tried to delete resource record set [name='<name.>', type='<type>'] but it was not found]`. Reported by @kharabasz.
- **S3 — an explicit Deny in a bucket policy applies to root** — with `AUTH=true`, the account's root credentials skipped the bucket policy, so a Deny stopped IAM users but not root; root is now refused with AWS's "with an explicit deny in a resource-based policy" message, and can still get, put and delete the bucket policy. Reported by @kharabasz.

### Removed
- **`Makefile`** — its targets only wrapped `docker build`, `docker run` and `docker compose`, and nothing used it.

## [1.5.34] — 2026-10-29

### Added
- **Aurora DSQL — `AWS IAM GRANT` and `AWS IAM REVOKE`** — with `DSQL_STRICT=1` both were a syntax error, so a migration that maps a Postgres role to an IAM role stopped there. Sent on their own over the simple query protocol (psql, psycopg2), a GRANT maps one or more ARNs to a role and a REVOKE unmaps them; the mappings read back through `sys.iam_pg_role_mappings` with DSQL's six columns and types, a rollback undoes a GRANT, and a GRANT after other DDL in the same transaction answers DSQL's `0A000`. Re-granting raises DSQL's NOTICE and revoking a missing mapping its WARNING; an unknown role answers `42704`, a value that is not an ARN `0P000`, and granting the signed-in role to itself `42501`. Contributed by @toby6578.
- **Step Functions — `glue:startJobRun` optimized integration** — `arn:aws:states:::glue:startJobRun` failed with "not yet implemented"; it now starts the job run and returns its `JobName` and `JobRunId`. With `.sync` the task waits for the run to end: a `SUCCEEDED` run returns the job run, with its timestamps in epoch milliseconds, and a `FAILED`, `ERROR`, `TIMEOUT` or `STOPPED` run fails the task with `States.TaskFailed` and the job run as the cause. Contributed by @mishukdutta-cz.

### Changed
- **S3 — with `AUTH=false` a presigned URL is accepted like a header-signed request** — SigV4 presigned URLs were verified whatever `AUTH` was set to, so a URL signed with a key MiniStack did not know, a wrong secret or a missing or wrong session token got 403 `InvalidAccessKeyId`, `SignatureDoesNotMatch` or 400 `InvalidToken` while the same client's header-signed calls succeeded. With `AUTH=false` the credential and signature are no longer checked; the key still selects the tenant and an expired URL still answers 403 `AccessDenied`. `AUTH=true` verifies presigned URLs as before. Contributed by @fabio-andre-rodrigues.

### Fixed
- **API Gateway — REST API custom domain names report `domainNameStatus`** — `CreateDomainName`, `GetDomainName` and `GetDomainNames` had no `domainNameStatus`, so Terraform AWS provider 6.x waited for `AVAILABLE` until it timed out; they now answer `AVAILABLE`. A private custom domain name also has its `domainNameId` and the `domainNameArn` `arn:aws:apigateway:<region>:<account>:/domainnames/<name>+<id>`, which `AWS::ApiGateway::DomainName` `DomainNameArn` returns too. Reported by @cupidchan.
- **AppConfig — KMS keys, version descriptions and deployment tags are kept** — `CreateConfigurationProfile` and `UpdateConfigurationProfile` dropped `KmsKeyIdentifier`, `StartDeployment` dropped `KmsKeyIdentifier` and `Tags`, and `CreateHostedConfigurationVersion` dropped its `Description` header, so Terraform re-planned the profile and replaced the hosted version and the deployment on every apply. The profile and the deployment now return `KmsKeyIdentifier` as given and the key's `KmsKeyArn` when MiniStack's KMS holds it, the version returns its `Description` on create, get and list, and the deployment's tags are readable with `ListTagsForResource`. Reported by @kharabasz.
- **AppConfig — Secrets Manager configuration profiles** — a profile whose `LocationUri` is a secret (`secretsmanager://<name or ARN>` or its ARN) got an empty body from `GetLatestConfiguration` and its deployments reported `ConfigurationLocationUri` `hosted`. `CreateConfigurationProfile` now checks the `RetrievalRoleArn`, the type and the secret as AWS does, `StartDeployment` reads the secret version named by `ConfigurationVersion` and keeps its value, and `GetLatestConfiguration` serves it, typed `application/json` when it parses as JSON and `application/octet-stream` otherwise. A missing version or deleted secret answers "Secret <LocationUri> not found", and a secret under a customer managed key needs a `KmsKeyIdentifier`. Another account's secret must be named by its exact ARN, and under `AUTH=true` its resource policy must let the retrieval role call `GetSecretValue`. `StartDeployment` also reports AWS's member validation errors first and, for a deployment in progress, "Environment <id> currently has a state of DEPLOYING. The current deployment must finish before a new deployment can be started." Contributed by @kharabasz.
- **Athena — an enforced workgroup overrides the client-side result location** — when a workgroup has `EnforceWorkGroupConfiguration: true`, `StartQueryExecution` now writes results to the workgroup's `OutputLocation`, even when the request gives its own `ResultConfiguration.OutputLocation`, as AWS does. Before, the request's location was used. Contributed by @mishukdutta-cz.
- **Athena — the result file is saved at the reported `OutputLocation`** — a query's `<id>.csv` and `<id>.csv.metadata` are now saved directly under the result location, at the path that `GetQueryExecution` reports, as AWS does. Before, they were saved one folder too deep, at `<location>/<id>.csv/<id>.csv`, so a client that read the reported path got `NoSuchKey`. Contributed by @mishukdutta-cz.
- **Auto Scaling — a group's launch template is the EC2 template it names** — a group created with `LaunchTemplateName` reported the name as its `LaunchTemplateId`, and `UpdateAutoScalingGroup` ignored `LaunchTemplate`. Both calls, and CloudFormation, now look the template up and report its id and name, with the `Version` as given (none when none was given), and refuse what AWS refuses with `ValidationError`: "The specified launch template, with template name <name>, does not exist.", "Malformed launch template Id: <id>" and "Could not find the specified version <n> for the launch template with ID <id>." Reported by @kharabasz.
- **CloudFront — Functions stages, statuses and ETags** — `DescribeFunction` and `GetFunction` without `Stage` answer the `DEVELOPMENT` stage instead of `InvalidArgument`, and a `Stage` other than `DEVELOPMENT` or `LIVE` answers `MalformedXML` "1 validation error detected: Value '<stage>' at 'stage' failed to satisfy constraint: Member must satisfy enum value set: [LIVE, DEVELOPMENT]". `PublishFunction` gives `LIVE` the `DEVELOPMENT` stage's ETag, both stages report `UNASSOCIATED` once published (`DEPLOYED` while a distribution uses the function) and an `UpdateFunction` no longer turns them back to `UNPUBLISHED`. ETags have AWS's form (`E` and 13 characters) and `GetFunction` answers `application/octet-stream`. Reported by @kharabasz.
- **EventBridge Scheduler — an event bus target publishes with its `EventBridgeParameters`** — a schedule whose target is an event bus put its event on the bus as `aws.scheduler` / `Scheduled Event`, so a rule matching the target's own `Source` and `DetailType` never fired; the event now carries the `EventBridgeParameters` `Source` and `DetailType`, with `Input` as its detail. Reported by @kharabasz.
- **EventBridge Scheduler — schedule error messages** — `GetSchedule`, `UpdateSchedule` and `DeleteSchedule` for a missing schedule now answer `Schedule <name> does not exist.`, and a duplicate `CreateSchedule` answers `Schedule <name> already exists.`, as on AWS; the messages no longer name the group. Contributed by @kharabasz.
- **Glue — Glue 5 file writes to the bucket that holds `--extra-py-files`** — a CSV (or other plain-file) write to that bucket stayed an open multipart upload, so the job succeeded but the output folder had only a 0-byte `part-*` marker under `__magic_job-…/`. `spark-submit` downloads `s3://` `--extra-py-files` before Spark starts, and the S3A filesystem it makes for that bucket saved the magicv2 committer's upload list in S3, where the commit did not look. Every S3A filesystem now tracks these uploads in memory, so the commit completes them. Glue 4 jobs use `DirectFileOutputCommitter` and do not change. Contributed by @mishukdutta-cz.
- **HTTP/2 — a connection is no longer cut after 1000 requests** — Hypercorn's default `keep_alive_max_requests` (1000) sent GOAWAY on an HTTP/2 connection's 1001st request and dropped its response, so a client that keeps one connection (Node's undici, gRPC) lost one request in every thousand (undici: `other side closed`). AWS closes client connections by keepalive duration, not request count; the limit is now 10,000,000, more than one connection sends in AWS's default hour. Contributed by @skialpine.
- **Kinesis — resource policies are validated and stored as AWS does** — `PutResourcePolicy` accepted most invalid documents. It now runs Kinesis's checks in AWS's order with its messages (size, JSON, `Version`, `Statement`, required and prohibited fields, `Sid`, actions per stream or consumer, wildcard and non-AWS principals, one `Resource` equal to the ARN) and, under `AUTH=true`, refuses principals that do not exist. `GetResourcePolicy` returns the document compact, as AWS stores it. Another account's ARN answers `AccessDeniedException` when MiniStack knows the account (it holds an IAM identity or a stream) and `UnrecognizedClientException` when it does not, and Kinesis authorization denials name the stream. Contributed by @Jolley71717.
- **Lambda — a partial ARN as the `FunctionName`** — `123456789012:function:my-function[:qualifier]` resolved to the account ID as the function name, so `Invoke` and the other function operations failed with `Function not found: ...:function:123456789012`; it now resolves as the full ARN in the caller's region, as on AWS, and gives the same result as that full ARN. Contributed by @mishukdutta-cz.
- **Lambda — event source mappings answer as AWS does** — a Kinesis or DynamoDB Streams mapping now reports AWS's defaults (`BatchSize` 100, `MaximumRetryAttempts` and `MaximumRecordAgeInSeconds` -1, `BisectBatchOnFunctionError` false, `ParallelizationFactor` 1, `TumblingWindowInSeconds` 0, `DestinationConfig` `{"OnFailure": {}}`), `StateTransitionReason` `User action`, and `LastProcessingResult` `OK` or `PROBLEM: Function call failed` instead of `OK - N records` and `FAILED`. An SQS mapping has no `LastProcessingResult`, as on AWS. Create, update and delete answer the transitional state (`Creating`, `Updating`, `Enabling`, `Disabling`, `Deleting`), `UpdateEventSourceMapping` answers 202, a second mapping for the same source and function is refused with `ResourceConflictException`, and an unknown mapping's message is AWS's. A mapping created by CloudFormation reports the same. MiniStack's states settle at once, where AWS's SQS mappings stay `Creating` for a few seconds. Contributed by @Jolley71717.
- **RDS — Aurora cluster Performance Insights inheritance** — explicit cluster PI settings now round-trip through create/modify/describe and propagate to existing and future members. Instance-managed clusters retain their independent settings until an explicit cluster handoff. Enabled responses include the retention and PI KMS key; cluster disablement survives removal of all members. Contributed by @jayjanssen.
- **Secrets Manager — a partial ARN resolves only its own secret** — a partial ARN (the full ARN without the `-XXXXXX` suffix) matched any secret whose ARN started with it, so with `my-secret` and `my-secret-admin` both stored, the partial ARN of `my-secret` returned `my-secret-admin` when that one was created first, and a partial ARN that names no secret, such as `my-secret-adm`, returned `my-secret-admin` instead of `ResourceNotFoundException`. It now matches only the secret whose ARN is the partial ARN plus the suffix, as on AWS. Contributed by @zach-tyl.
- **Step Functions — `aws-sdk:scheduler` tasks** — a Task with `arn:aws:states:::aws-sdk:scheduler:<action>` failed with `States.Runtime` ("Service 'scheduler' is not supported in MiniStack aws-sdk integrations"). `createSchedule`, `getSchedule`, `updateSchedule`, `deleteSchedule`, `listSchedules`, `createScheduleGroup`, `getScheduleGroup`, `deleteScheduleGroup` and `listScheduleGroups` now call EventBridge Scheduler in the execution's account and region, as on AWS. The output keeps Scheduler's member names, timestamps are ISO-8601 UTC strings, `StartDate`/`EndDate` are read from ISO-8601 strings with `Z` or a `+hh:mm` offset (cut to milliseconds), `listSchedules` filters by `GroupName`, `NamePrefix` and `State`, and a JSON object or array given as `Target.Input` is sent as compact JSON text. A failed call reports `Scheduler.<ErrorCode>` with the cause suffix `(Service: Scheduler, Status Code: <status>, Request ID: <id>) (SDK Attempt Count: 1)`. Contributed by @kharabasz.
- **Step Functions — JSONPath workflow variables** — a JSONPath state ignored `Assign`, and `$name` was not resolved in `Parameters` or intrinsic-function arguments, so a workflow that saved a result in a variable lost it. `Assign` now works in JSONPath states as AWS documents: `$` is the result in Task, Map and Parallel states, the effective input in Choice and Wait, and the result in Pass; all assignments read the values from state entry (JSONata `Assign` too); and a Parallel branch's variables stay in the branch. Contributed by @jayjanssen.
- **Step Functions — state events inside a Map or a Parallel branch** — the states in an inline Map item or a Parallel branch wrote no `<Type>StateEntered` or `<Type>StateExited` events, so `GetExecutionHistory` did not show them. They are now in the execution history, as on AWS; for a Distributed Map they are in each child execution's history and not in the parent's. Events added at the same time from several Map items or branches can no longer get the same `id`. Contributed by @mishukdutta-cz.

## [1.5.33] — 2026-10-26

### Added
- **Kinesis — stream and consumer resource policies** — `PutResourcePolicy`, `GetResourcePolicy` and `DeleteResourcePolicy` answered `InvalidAction`; they now store a policy per stream or consumer, persisted and dropped with it, with AWS's checks in AWS's order: the ARN's region (`InvalidArgumentException` "The region specified in the ARN '<arn>' does not match the endpoint region"), the policy ("Policy validation error: This policy contains invalid Json", "Missing required field Version" or "Statement", and per statement the actions a stream or consumer can share and a `Resource` equal to the ARN, with the statement's line and column), then the resource ("Stream <name> under account <id> not found."). `GetResourcePolicy` without a policy answers `{"Policy": "{}"}` and `DeleteResourcePolicy` "No resource policy found for resource ARN <arn>.". Reported by @Jolley71717.
- **Lambda — Kinesis event source mappings across accounts** — a function can map a stream in another account in the same region; `CreateEventSourceMapping` reads the stream first and answers "Received Exception while reading from provided stream. " with the reason ("No account found for the given parameters", "Stream <name> under account <id> not found.", or under `AUTH=true` the read the stream's policy doesn't allow the function's execution role), and the policy is checked again on every poll. Another region's stream answers "Event source region must match Lambda region <region>". Reported by @Jolley71717.
- **CloudFormation — `AWS::Athena::WorkGroup`** — created with its `Description`, `State`, `Tags` and `WorkGroupConfiguration`, updated in place, replaced when `Name` changes and deleted with the stack; `Ref` returns the name and `Fn::GetAtt` `CreationTime` and the effective engine version. Reported by @mishukdutta-cz.

### Fixed
- **S3 — GET, HEAD and DELETE error order and responses** — under `AUTH=true` ordinary header-signed downloads check the endpoint and signing region before the bucket's home region, then credentials and missing buckets, and HeadObject, HeadBucket and DeleteObject check the home region first; a DELETE sent to another regional endpoint no longer deletes the object. In both auth modes a `NoSuchKey` carries `Key`, HEAD errors have an empty body and no `Content-Length`, and HeadBucket reports the bucket's region (also on a denial) and its ARN. Iceberg REST HEAD requests keep their JSON errors. Contributed by @gakuto-cw21.
- **S3 — persisted keys that a file path can't hold survive a restart** — with `S3_PERSIST=1` a folder marker such as `project/Incoming/` came back without its slash, and keys under it, a key under another key's object (`a/b` beside `a`) or one with doubled or dot segments (`x//y`) were lost or rewritten; they are now stored by an encoded name under the bucket's `.ministack-keys` directory, and every other key keeps its path, so earlier data still loads. Reported by @jasonthagerty.
- **ALB — requests reach targets as sent** — instance and IP targets now get the path with its percent-encoding, the query string unchanged, the client's `Host` as the load balancer sends it (no port on 80 or 443, the listener's port otherwise, unmodified with `routing.http.preserve_host_header.enabled`), the client's `Accept-Encoding`, and OPTIONS requests instead of a gateway-made CORS answer. A Lambda target's event carries the last value of a repeated query key, not decoded, no multi-value keys unless enabled, and `body: ""`. Contributed by @skialpine.
- **ALB — responses and actions** — the load balancer's own responses (a Lambda target's, fixed-response, redirect, authentication, its errors) carry `Server: awselb/2.0` unless `routing.http.response.server.enabled` is `false`, it writes its own `Date` over an instance or IP target's, and the listener's response-header attributes (the CORS ones, `strict_transport_security`, `content_security_policy`, `x_content_type_options`, `x_frame_options`) are applied. A redirect fills `#{protocol}`, `#{port}` and `#{query}` from the listener and request (`http://host:80/path`), and `authenticate-oidc` on a 443 listener names its callback without the client's port. Contributed by @skialpine.
- **CloudFront — response headers** — responses carry only the origin's headers plus CloudFront's own, with `X-Amz-Cf-Id` as URL-safe base64 of 40 bytes and `Via: 1.1 <32 hex>.cloudfront.net (CloudFront)`; `Server: CloudFront` is added only when a viewer-response function ran and the origin sent none, and a Lambda Function URL origin's response carries a single `x-amzn-RequestId`. Contributed by @skialpine.
- **All services — status line, `Date` and `Server`** — HTTP/1.1 status lines carry their reason phrase (`HTTP/1.1 200 OK`), every response carries one `Date`, also a WebSocket handshake and Hypercorn's own errors, and no response carries `Server: hypercorn-h11`. Contributed by @skialpine.
- **API Gateway — an HTTP API's `rawQueryString` is the query string as sent** — `?flag` reached the function as `flag=`; the Lambda proxy and REQUEST authorizer events now carry the request's own query string. Contributed by @skialpine.
- **Lambda — packages, code caches and restore** — `CreateFunction`, `UpdateFunctionCode` and `PublishLayerVersion` refuse a package that is not a zip, an empty one included, with `InvalidParameterValueException` "Could not unzip uploaded file. Please check your file, then try to upload again.", leaving the function's code as it was. Two MiniStacks on one host no longer remove each other's extracted code, and restoring persisted state keeps one copy of code that versions share. Contributed by @skialpine.
- **Lambda — Kinesis event source mapping filters** — a `FilterCriteria` pattern never matched a Kinesis record, so a filtered mapping dropped everything; metadata fields such as `partitionKey` now match at the top of the pattern and `data` as base64-decoded JSON, and data that isn't JSON still matches metadata patterns. Contributed by @Jolley71717.
- **DynamoDB — `TransactWriteItems` follows DynamoDB's check order** — an `Update` whose condition failed is cancelled with `ConditionalCheckFailed` even when its update expression can't be applied to the item, an update-expression error on one member no longer hides a failed condition on another, a missing table answers `ResourceNotFoundException`, and a `begins_with` condition with a number operand answers `ValidationException`. Contributed by @jun-ut.
- **Step Functions — JSONata on `Map`, `Parallel` and variables** — an inline `Map` evaluates `Items`, `ItemSelector`, `MaxConcurrency`, `Output` and `Assign` (an `Items` that isn't an array fails with `States.QueryEvaluationError`), a `Catch` on `Map` or `Parallel` applies its `Output` and `Assign`, and a variable assigned `null` reads back as `null`. Contributed by @mishukdutta-cz.
- **Glue — Spark job arguments and botocore** — Spark scripts get `--JOB_NAME` and `--JOB_RUN_ID` (a run's `Arguments` still win, `pythonshell` scripts don't get them), and a job's own botocore loads from where the script asks, so a `--py-files` zip that holds botocore no longer fails with `DataNotFoundError`. Contributed by @mishukdutta-cz.
- **OpenSearch — engine heap and the container reaper** — engine containers run with a 512 MB heap instead of the image's 1 GB, and the reaper no longer removes a live domain's stopped engine. Contributed by @skialpine.

## [1.5.32] — 2026-10-23

### Added
- **EC2 — Verified Access control plane** — instances, trust providers (`oidc`, and `jamf`, `crowdstrike` and `jumpcloud` devices), groups, `load-balancer` and `network-interface` endpoints, trust provider attachments, instance logging configuration, and group and endpoint policies, as the `aws_verifiedaccess_*` Terraform resources call them, with AWS's codes, messages and check order. Under `AUTH=true` each call is authorized on every resource AWS checks it against. No access evaluation or Cedar parsing; `rds` and `cidr` endpoints and `NativeApplicationOidcOptions` answer `UnsupportedOperation`. Contributed by @kharabasz.
- **Step Functions — Distributed Map** — a Map whose `ProcessorConfig` mode is `DISTRIBUTED` ran inline, with no map run and its `ItemReader` ignored; it now creates a Map Run (`ListMapRuns`, `DescribeMapRun`, `UpdateMapRun`, `ListExecutions` with `mapRunArn`) whose items run as child executions under `<state machine>/<label>`, reads its items from S3 (`s3:getObject` as JSON, JSONL or CSV, `s3:listObjectsV2` object metadata, `MaxItems`), and fails with `States.ExceedToleratedFailureThreshold` past its tolerated failures. Reported by @cupidchan.
- **SSM — service settings** — `GetServiceSetting`, `UpdateServiceSetting` and `ResetServiceSetting`, per account and region, persisted. `SettingValue` is validated for the ids whose accepted values AWS documents, and `/ssm/parameter-store/default-parameter-tier` sets the `Tier` of a `PutParameter` that names none. Unblocks Terraform's `aws_ssm_service_setting`. Contributed by @skialpine.
- **Auth — SigV4a signatures are verified under `AUTH=true`** — requests signed with `AWS4-ECDSA-P256-SHA256`, which SDKs use for Multi-Region Access Points, were refused for every IAM user. Header and presigned S3 signatures are now checked with the P-256 key SigV4a derives from the secret, with their date or expiry, and an `X-Amz-Region-Set` that leaves out the serving region is refused (`RegionSetMismatch` on S3, the unknown-key error elsewhere). Contributed by @iot-rocket.

### Fixed
- **Auth — requests without credentials are anonymous in every service** — under `AUTH=true` a request with no `Authorization` header and no query signature ran as the account root everywhere but S3; it is now refused in the service's own error, as on AWS: `MissingAuthenticationToken` 403 from Query and REST-XML services, `MissingAuthenticationTokenException` from JSON services, `AccessDenied` 403 from SQS, `UnrecognizedClientException` from Secrets Manager. Operations the SDKs send unsigned (Cognito sign-in, `GetId`, `AssumeRoleWithWebIdentity`), SNS `ConfirmSubscription` and `Unsubscribe`, API Gateway and Function URL invokes and S3 keep their behaviour. Contributed by @iot-rocket.
- **Auth — a query signed for `s3` is refused by other services** — under `AUTH=true` a SigV4 presigned query with an `s3` credential scope skipped the signature check when another service served it; it is now refused with `SignatureDoesNotMatch` "Credential should be scoped to correct service: '<service>'. ", as AWS answers, while S3 keeps verifying its own presigned URLs. Reported by @iot-rocket.
- **S3 — access points and Multi-Region Access Points under `AUTH=true`** — an access point policy was stored but never applied, and unsigned requests, SigV2 `Authorization` headers and `PutBucketPolicy`/`DeleteBucketPolicy` through an access point were served; a request through an access point alias or ARN is now allowed on the access point by an identity policy or its policy, an unsigned or SigV2-header one gets 400 `InvalidRequest`, bucket policy writes get 403 `AccessDenied`, and SigV2 presigned URLs for an access point ARN verify. MRAP requests are authorized on the member bucket and then on the MRAP. Contributed by @iot-rocket.
- **S3 — ACLs and Block Public Access** — on a `BucketOwnerEnforced` bucket ACL writes answered 200 and a stored grant still opened the bucket; `PutBucketAcl` and `PutObjectAcl` now answer 400 `AccessControlListNotSupported`, uploads refuse any ACL but an owner-only one, and `CreateBucket` or a switch to `BucketOwnerEnforced` refuses a granting ACL with `InvalidBucketAclWithObjectOwnership`. Under `AUTH=true` a new bucket has all four Block Public Access settings on, as on AWS, so a public policy or ACL answers 403 `AccessDenied` and `CreateBucket` with a public ACL `InvalidBucketAclWithBlockPublicAccessError`; `RestrictPublicBuckets` drops a bucket policy only when it is public. Contributed by @iot-rocket.
- **S3 — CORS under `AUTH=true`** — every preflight answered 200 with `Access-Control-Allow-Origin: *`, whatever the bucket's CORS configuration; a preflight now has to match one of the bucket's rules (403 `AccessForbidden` without a configuration or a match, 400 `BadRequest` without `Origin`) and gets that rule's headers. Contributed by @iot-rocket.
- **S3 — presigned expiry and multipart object size** — under `AUTH=true` a presigned URL without `X-Amz-Expires` never expired; it now answers 400 `AuthorizationQueryParametersError` "Query-string authentication version 4 requires the X-Amz-Algorithm, X-Amz-Credential, X-Amz-Signature, X-Amz-Date, X-Amz-SignedHeaders, and X-Amz-Expires parameters.", and a negative value "X-Amz-Expires must be non-negative". `CompleteMultipartUpload` checks `x-amz-mp-object-size`: a size other than the parts' total answers 400 `InvalidRequest` "The provided 'x-amz-mp-object-size' header value 11 does not match what was computed: 10", a non-numeric one "Value for x-amz-mp-object-size header is invalid: 'abc'". Reported by @adzcodemi.
- **CloudFront — origins that name a load balancer or a Function URL in this MiniStack** — a custom origin at a load balancer's `DNSName` or a Function URL's `<url-id>.lambda-url.<region>.on.aws` answered 502, as for any AWS hostname. A load balancer is now reached on its HTTP listener at the origin's `HTTPPort` and a Function URL over HTTPS, with the origin's custom headers, so a target that checks a secret header works; `OriginReadTimeout` applies (504). A Function URL refuses a forwarded viewer `Host` with 403 `{"Message":null}` and an unsigned `AWS_IAM` URL answers 403 `{"Message":"Forbidden"}`, as on AWS. Reported by @kinoh and @smiley-uriux.
- **CloudFront — functions** — a viewer-request function that returned the request it was given rebuilt the query string, so `?flag` reached the origin as `?flag=`; it is now rebuilt only when the returned `querystring` differs. `CreateFunction` keeps its `Tags` (deleted with the function), answers invalid base64 code with `MalformedInput` "Invalid Base64 encoding", and refuses a missing code, configuration, runtime or name and invalid tags in one `MalformedXML` before the name lookup, then `InvalidTagging` for repeated, `aws:` or more than 50 keys. Contributed by @jeffreyheller and @kharabasz.
- **ALB — `X-Forwarded-*` and `X-Amzn-Trace-Id` as the load balancer sets them** — targets got an incoming `X-Forwarded-For` unchanged and a client-sent `X-Forwarded-Proto`/`X-Forwarded-Port`, and Lambda targets none of them; all targets now get `X-Forwarded-For` with the client address appended (kept or dropped under `routing.http.xff_header_processing.mode` `preserve`/`remove`), `X-Forwarded-Proto` and `X-Forwarded-Port` from the listener, and an `X-Amzn-Trace-Id` with a new `Root`, or a `Self` added to or updated in the one sent. Reported by @kinoh.
- **Lambda — Function URLs as AWS serves them** — URL ids are 32 lowercase letters and digits; the event's headers carry the URL's own `host`, `x-forwarded-for` (the first address sent, or the caller), `x-forwarded-port` `443`, `x-forwarded-proto` `https`, `x-amzn-trace-id` and the TLS fields; `requestContext.accountId` is `anonymous` for an `AuthType` `NONE` URL; and responses carry `x-amzn-RequestId` and `X-Amzn-Trace-Id`, with `x-amzn-ErrorType` `AccessDeniedException` on a 403. Reported by @smiley-uriux.
- **Lambda — versions, layers and containers** — `Publish: true` on create or code update overwrote `$LATEST`'s `Version`, so `$LATEST` disappeared from `ListVersionsByFunction`; it now stays `$LATEST`. A layer MiniStack doesn't hold from another account, such as Datadog's, resolves as an empty layer instead of `AccessDeniedException`. Function containers no longer publish port 8080 on every host address: none with `DOCKER_NETWORK`, `127.0.0.1` otherwise. Contributed by @skialpine and @kharabasz. Reported by @cupidchan.
- **API Gateway — HTTP API request ids and `AWS_IAM` headers** — an HTTP API's responses carried `x-amzn-requestid` and `x-amz-request-id`; they now carry `apigw-requestid`, 16 base64 characters, as AWS does. Under `AUTH=true` a REST `AWS_IAM` method served a bearer token or a lower-case algorithm; it now answers 403 `IncompleteSignatureException` with API Gateway's message. Contributed by @jeffreyheller and @iot-rocket.
- **API Gateway v2 — WebSocket connections** — `DeleteConnection` answered 204 but left the socket open; it now closes it, `GetConnection` and `PostToConnection` answer 410 `GoneException`, and `$disconnect` runs after the close. A route whose Lambda is a container image or another runtime now runs it, `PostToConnection` sends a text frame, and routes and stages keep `RouteResponseSelectionExpression`, `ModelSelectionExpression`, `AccessLogSettings`, `ClientCertificateId` and `DeploymentId`. Contributed by @toby6578. Reported by @cupidchan.
- **AppSync Events — `AUTH=true` checks the Event API's authorization** — an HTTP publish, a WebSocket connection or a subscription was accepted with any API key or none unless `APPSYNC_EVENTS_ENFORCE_AUTH` was set; it now needs what the API or channel namespace accepts (an API key, a SigV4 signature, a Cognito or OIDC token), and a refused publish answers 401 `UnauthorizedException` "You are not authorized to make this call.". Contributed by @iot-rocket.
- **Cognito — signing keys, managed login and federation** — every pool signed with one shared key (`kid` `ministack-key-1`); each pool now has its own access and ID token keys at its JWKS URI. Managed login refuses PKCE `plain`, an unregistered `redirect_uri` and an unknown `grant_type` (400 `unsupported_grant_type`); a federated user joins `<user pool id>_<provider name>` and carries `identities`; an OIDC provider with only `oidc_issuer` takes its endpoints from the issuer's `/.well-known/openid-configuration`. Contributed by @kinoh. Reported by @cupidchan.
- **Cognito — `client_credentials`** — `/oauth2/token` issued a token to a client without `client_credentials` in `AllowedOAuthFlows`; it now answers `invalid_grant` (`invalid_scope` first for an unknown scope), and the pre token generation trigger runs for this grant only when `LambdaVersion` is `V3_0`. Contributed by @iot-rocket.
- **IoT — MQTT delivery as AWS IoT does** — an unacknowledged QoS 1 message was resent every 10 seconds with DUP set; it is now resent after 30 seconds with DUP 0, on the open connection and after a persistent session resumes, and a No Local subscription receives the client's own messages, as AWS IoT Core ignores that option. Contributed by @iot-rocket.
- **IoT — thing-group membership by ARN** — `AddThingToThingGroup` and `RemoveThingFromThingGroup` ignored `thingGroupArn` and `thingArn` and were authorized on the request path; they now act on the group and thing the ARNs name, refuse a mismatched or foreign ARN with 400 `InvalidRequestException`, and under `AUTH=true` are checked on the group and then the thing. Contributed by @iot-rocket.
- **CloudWatch — alarm tags and `DatapointsToAlarm`** — `PutMetricAlarm` and `PutCompositeAlarm` dropped `Tags` on create; they are now stored over CBOR, JSON 1.0 and Query and ignored on update, as on AWS, with AWS's `ValidationError`, `MalformedInput` and `aws:`-key refusals. An alarm without `DatapointsToAlarm` no longer reports it as `EvaluationPeriods`. Contributed by @kharabasz.
- **DynamoDB — PartiQL statements are parsed as DynamoDB parses them** — a parser replaces the regular-expression reader and answers malformed statements with AWS's messages (`Statement wasn't well formed, can't be processed: Unexpected term`, `Missing right-hand side expression of infix operator`, `Aliasing is not supported`, positions as `at <line>:<column>:<length>`); it skips comments, reads every number literal form, and supports `ORDER BY`, `attribute_exists`, `attribute_not_exists`, `size`, `attribute_type`, `IS`, `set_add`, `set_delete`, `FROM ... WHERE ... SET` and `EXISTS(SELECT ...)` in `ExecuteTransaction`. Reported by @hicksy (paritysuite.org).
- **DynamoDB — validation order, numbers and limits** — transactions and batches report their member errors together before any table lookup; numbers are range-checked with their full exponent (`Number overflow`) and duplicate set members reported in byte order; legacy `Expected` conditions are validated; UpdateItem's 400KB check charges the request clause by clause before the condition; `SET l[n]` past the end appends instead of padding with `NULL`. Reported by @hicksy (paritysuite.org).
- **ElastiCache — transit encryption, AUTH tokens and subnet groups** — a group with `TransitEncryptionEnabled` and an `AuthToken` served plaintext without a password; it now serves TLS only, signed by the CA at `/_ministack/elasticache/ca.pem`, requires the token, and `ModifyReplicationGroup` applies `ROTATE`, `SET` and `DELETE`. `CreateCacheSubnetGroup` now keeps its subnets, VPC and tags, and answers `InvalidSubnet`, `CacheSubnetGroupAlreadyExists` and `CacheSubnetGroupInUse`. Reported by @aditya-drizz.
- **ECS — capacity provider strategies** — a service or task with `capacityProviderStrategy` dropped it and reported `launchType` `EC2`; the strategy is now kept without a `launchType`, the cluster's default applies when neither is given, both together are refused with `InvalidParameterException` "Specifying both a launch type and capacity provider strategy is not supported. Remove one and try again.", and `AWS::ECS::Service` passes it through. Reported by @aditya-drizz.
- **RDS — `MaxAllocatedStorage` only with storage autoscaling** — every DB instance reported `MaxAllocatedStorage` equal to its allocated storage; it is now present only when storage autoscaling was turned on. Reported by @aditya-drizz.
- **Reset — `/_ministack/reset` no longer removes another MiniStack's containers** — the OpenSearch, OpenSearch Serverless, DSQL, ECS and EKS resets removed every container with the service's label on the Docker daemon; they now remove only this instance's. Contributed by @jeffreyheller.

## [1.5.31] — 2026-10-21

### Added
- **CloudFront — VPC origins and distribution web ACLs** — `CreateVpcOrigin`, `GetVpcOrigin`, `UpdateVpcOrigin`, `DeleteVpcOrigin` and `ListVpcOrigins` are implemented: a change answers `Deploying` and the next read shows `Deployed`, the endpoint must be a load balancer or instance that exists in MiniStack, and an origin a distribution names can't be deleted or changed (`CannotDeleteEntityWhileInUse`, `CannotUpdateEntityWhileInUse`). `AssociateDistributionWebACL` and `DisassociateDistributionWebACL` set and clear the distribution's `WebACLId`, with `If-Match` and `EntityNotFound` as on AWS. Contributed by @kharabasz and @OS-kiranmalsetty.
- **Cognito — auth events** — on a pool with threat protection on, password, SRP and admin sign-ins, their MFA answers and password changes are logged as `SignIn` and `PasswordChange` events, `AdminListUserAuthEvents` lists them newest first, and `AdminUpdateAuthEventFeedback` and `UpdateAuthEventFeedback` are implemented. Contributed by @iot-rocket.
- **Route 53 — private hosted zones** — `CreateHostedZone` with a `VPC` made a public zone and dropped the VPC; the zone is now private and keeps its VPCs, and `AssociateVPCWithHostedZone`, `DisassociateVPCFromHostedZone` and `ListHostedZonesByVPC` are implemented with AWS's refusals (`PublicZoneVPCAssociation`, `ConflictingDomainExists`, `VPCAssociationNotFound`, `LastVPCAssociation`). A Cloud Map private DNS namespace's zone carries the namespace VPC. Contributed by @kharabasz.

### Fixed
- **S3 — access points** — under `AUTH=true` a request through an access point alias or ARN is now checked on both the access point and its bucket, with `s3:DataAccessPointArn`, `s3:DataAccessPointAccount` and `s3:AccessPointNetworkOrigin`, and the access point's Block Public Access settings apply to requests through it (`BlockPublicAcls` refuses a public ACL with 403 `AccessDenied`). Contributed by @iot-rocket.
- **S3 Control — access point, Object Lambda and Multi-Region Access Point requests authorize the operation they run** — the IAM action was read from the path separately from the handler, so an extra segment, a double slash or a resource named `policy` ran an operation without its action; the action now comes from the route served, a method a resource lacks answers 405 `MethodNotAllowed` and an unparseable path 400 `InvalidURI`. Contributed by @iot-rocket.
- **S3 — replication rule options** — `PutBucketReplication` dropped `SourceSelectionCriteria`, `EncryptionConfiguration`, `ReplicationTime` and `Metrics`; they are now stored, returned and validated with AWS's codes and order (`MalformedXML`, `InvalidArgument`, `InvalidRequest`). Contributed by @kharabasz.
- **S3 — `EU` location constraint** — eu-west-1 accepts the legacy `EU` constraint again and homes the bucket in eu-west-1, `GetBucketLocation` answers `EU` for it, and other regional endpoints still refuse it. Contributed by @toby6578.
- **CloudFormation — S3 bucket CORS, Block Public Access and website** — `AWS::S3::Bucket` dropped `CorsConfiguration`, `PublicAccessBlockConfiguration` and `WebsiteConfiguration`; they are now stored on create, replaced on update and deleted when an update removes them. Contributed by @iot-rocket.
- **Lambda — Node.js functions** — under `USE_SSL=1` Node requests to the gateway keep TLS on the gateway port in both executors, so `cfn-response` callbacks and SDK calls work, and the generated certificate names `host.docker.internal`. The local executor's SDK stubs now sign with the function's credentials, so a call lands in the function's account and runs as its execution role. Contributed by @iot-rocket.
- **ELBv2 — listener TLS settings and rule transforms** — HTTPS and TLS listeners keep `SslPolicy` (default `ELBSecurityPolicy-2016-08`) and their certificate, and more than one certificate answers `TooManyCertificates` (`Up to '1' certificate ARNs can be specified, but '2' were specified`). Rules keep their `url-rewrite` and `host-header-rewrite` `Transforms`, validated as AWS does, also from `AWS::ElasticLoadBalancingV2::ListenerRule`. Contributed by @kharabasz.
- **Cognito — trigger events and user errors** — the pre token generation event carries its version, `TokenGeneration_NewPasswordChallenge` and `ClientCredentials`, and every trigger's `awsSdkVersion` is `aws-sdk-unknown-unknown`. Unknown users answer `User does not exist.` (`Username/client id combination not found.` on the client calls with `PreventUserExistenceErrors` `LEGACY`), and `ChangePassword` checks `PreviousPassword`. Contributed by @iot-rocket and @jun-ut.
- **Step Functions — encryption and `aws-sdk:ec2` snapshots** — state machines keep and validate `encryptionConfiguration` (`AWS_OWNED_KEY` when unset, a 300-second key reuse period by default), also in `AWS::StepFunctions::StateMachine`, and `aws-sdk:ec2` snapshot results name their lifecycle `State`. Contributed by @kharabasz and @bandle.
- **SQS — message digests** — a FIFO dedup hit answers the current request's `MD5OfMessageBody` and `MD5OfMessageAttributes`, and `ReceiveMessage` computes `MD5OfMessageAttributes` over the attributes it returns, so SDKs that verify them no longer fail. Contributed by @vanessah07 and @Xevos117.
- **EventBridge — archive KMS keys** — `CreateArchive` and `UpdateArchive` keep `KmsKeyIdentifier` and `DescribeArchive` returns it; `ListArchives` entries carry only the `Archive` summary fields. Contributed by @kharabasz.
- **WAF v2 — duplicate names** — `CreateIPSet` and `CreateRuleGroup` refuse a name already used in the scope, and all four creates answer `WAFDuplicateItemException` with `AWS WAF couldn’t perform the operation because some resource in your request is a duplicate of an existing one.` Contributed by @OS-kiranmalsetty.
- **IoT — `#` does not match the level above it** — a filter ending in `/#` no longer receives messages published to the level above it, as on AWS IoT Core. Contributed by @iot-rocket.
- **SSM — public latest Amazon Linux AMI parameters** — `/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64` and the Amazon Linux 2 parameter resolve, also as CloudFormation `AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>` defaults. Contributed by @bandle.
- **Athena — CSV tables with DuckDB 2.0** — DuckDB 2.0's `type_pushdown` optimizer made the CSV reader's `TRY_CAST` strict, failing queries on values of another type; it is disabled on the Athena connection.

## [1.5.30] — 2026-10-20

### Added
- **Auth — more calls are authorized under `AUTH=true`** — RDS Data calls are authorized as `rds-data:<operation>` on the cluster its `resourceArn` names, S3 Control `ListTagsForResource`, `TagResource` and `UntagResource` as `s3:<operation>` on the bucket, and SES v2 calls as `ses:<operation>` on the resource the path or create body names (`*` otherwise), each with AWS's 403 denial (`AccessDeniedException`, S3 Control `AccessDenied`) naming the action and resource. S3 bucket and identity policies now see `s3:x-amz-server-side-encryption` (a part and the completion take their `CreateMultipartUpload`'s), `s3:x-amz-acl`, `s3:x-amz-grant-*` and, on `GetObject` and `HeadObject`, `s3:ExistingObjectTag/<key>`. S3 Tables and Glue Iceberg REST calls at `/iceberg` are authorized for the catalog that serves them, whatever service they are signed for: on a table bucket as the `s3tables` actions AWS lists for the operation (`loadTable` needs `GetTableMetadataLocation`, then `GetTableData`) on the bucket or table (`table/*` for one that does not exist), on the Glue catalog as the route's `glue` action on the catalog, then the database, then the table, and `transactions/commit` as `updateTable` on each table it changes; a denial answers 403 in the Iceberg error shape naming the action and resource ("Access Denied" for a later S3 Tables action, no body for `HEAD`), a `{prefix}` that is neither a table bucket ARN nor `<account>:s3tablescatalog/<bucket>` answers 400 `bad_request`, and a call that names a key is served from its own region only. Contributed by @iot-rocket.
- **IoT — MQTT over WebSocket follows the signer's IAM policies under `AUTH=true`** — a session signed with an IAM user or STS key could connect under any client id and publish, subscribe and receive anywhere; its CONNECT, will, PUBLISH, SUBSCRIBE and deliveries now need `iot:Connect`, `iot:Publish` (and `iot:RetainPublish`), `iot:Subscribe` and `iot:Receive` on the client, topic and topic filter ARNs, with a certificate session's refusals. The handshake (and an `AWS_IAM` WebSocket `$connect`) has its signature, date and session token verified: IoT answers 403 `Forbidden` and expects the token after the signed part of the URL, API Gateway names the canonical string it expected, and the session runs in the signer's account. Contributed by @iot-rocket.
- **Cognito — user pool feature plans** — `CreateUserPool`, `UpdateUserPool` and `AWS::Cognito::UserPool` ignored `UserPoolTier`; a pool now stores and describes its tier (`ESSENTIALS` by default, `PLUS` when created with threat protection on), keeps it when an update omits it, and refuses threat protection on `ESSENTIALS` and password history on `LITE` with `FeatureUnavailableInTierException` or, on a tier change, `TierChangeNotAllowedException`. Contributed by @iot-rocket.

### Fixed
- **Auth — unverifiable credentials under `AUTH=true`** — a request naming an IAM user's key in a SigV2 header, another SigV4 algorithm (`AWS4-HMAC-SHA512`), a header without `Credential=` or a query `X-Amz-Credential` the signature does not cover was served as that user; it now answers `IncompleteSignature` (S3: `InvalidRequest`), and SigV2 header and presigned signatures are verified. A request that maps to no IAM action (SES v2, RDS Data, S3 Control, X-Ray) is authenticated and answers the service's credential error (`InvalidSignatureException` 403 from SES v2 and RDS Data, `SignatureDoesNotMatch` 403 from S3 Control), and an SES v2 call selected by `X-Amz-Target` on a path without `/v2/email` is authorized as the operation it performs. A SigV4 header or presigned query that names no access key ran as the account root; it now answers `IncompleteSignature` naming every missing part (Lambda: `MissingAuthenticationTokenException` for a part left out), and an empty key the unknown-key error. Contributed by @iot-rocket.
- **API Gateway — credentials under `AUTH=true`** — a REST method with `NONE` or `COGNITO_USER_POOLS` authorization, or an HTTP API route without authorization, refused a signed caller its policies or signature failed; such a call now reaches the method and its own authorizer whatever it is signed with, and a REST API policy judges a `NONE` method's caller as anonymous (`ACCESS_DENIED` unless the policy allows everyone). A correctly presigned call (`X-Amz-Signature`) to an `AWS_IAM` method is served instead of 403 `MissingAuthenticationTokenException`. `CreateRestApi`, `GetRestApi`, `GetRestApis` and `UpdateRestApi` now return `apiStatus` `AVAILABLE` and `rootResourceId`, which Terraform AWS provider 6.x waits on. Contributed by @iot-rocket. Reported by @kharabasz.
- **AppSync — GraphQL authorization** — under `AUTH=true` a GraphQL call was authorized as `appsync:UpdateGraphqlApi` whatever the API's authentication type, and an `AWS_IAM` API served any `AWS4-HMAC-SHA256` header. On `/v1/apis/{apiId}/graphql` and on the API's own host an API key request now ignores its signature, and an `AWS_IAM` API needs a verified signature and authorizes `appsync:GraphQL` per top-level field (401 `Permission denied` when none is allowed, a field error per denied field, 400 `BadRequestException` for a wrong signature). Contributed by @iot-rocket.
- **AppSync — data-plane errors** — Event API `POST /event` refusals answered `{"__type", "message"}`, and GraphQL answered 500 for a body that is not a JSON object or a non-string `query` or non-object `variables`, a message-only 400 for a missing query and 200 for an unparseable one; both now answer AWS's `{"errors": [{"errorType", "message"}]}` envelope and `x-amzn-ErrorType` (`SerializationException`, `MalformedHttpRequestException`, `ResourceNotFoundException` for an unknown API or key, `UnknownOperationException` for another path on an Event API host), and publish answers `application/json`. Contributed by @iot-rocket.
- **Cognito — tokens under `AUTH=true`** — `/oauth2/userInfo` returned the user's attributes for any JWT with a user's `sub`; it now refuses an ID token, an altered, revoked or expired token, a disabled or deleted user and a token without the `openid` scope with 401 `invalid_token`, AWS's `error_description` and a `WWW-Authenticate` realm of the pool's issuer. `AdminDisableUser` revokes the user's tokens (`User is disabled.`, then `Access Token has been revoked` / `Refresh Token has been revoked` and `invalid_grant` after `AdminEnableUser`), a deleted user's access token gets `UserNotFoundException: User does not exist.`, and refresh tokens are signature-checked and refused for a disabled user or a deleted user's successor. ID tokens carry `email_verified` and `phone_number_verified` as JSON booleans, as AWS does; userInfo and the attributes keep them as strings. Contributed by @iot-rocket and @SAY-5. Reported by @proeben.
- **Cognito — identity `Logins` and OAuth scopes** — under `AUTH=true` `GetId`, `GetCredentialsForIdentity` and `GetOpenIdToken` took any token in `Logins`; they now refuse an unsupported provider, a token that is no ID token of a trusted client, a bad signature, one expired over five minutes or another user's login with `NotAuthorizedException` (`Access to Identity '<id>' is forbidden.` without `Logins`), `GetId` returns the identity that holds the login, and the first login is linked. A call on an identity without a login answers for the identity that holds its login, an identity with a login takes the logins of the identities holding the others, and a merged-away identity is listed with `["DISABLED", "<region>"]` (`ListIdentities` `HideDisabled` leaves it out). `/oauth2/authorize` with an `identity_provider` redirects with `error_description=invalid_scope` for a scope outside `AllowedOAuthScopes`, and `client_credentials` ignores undefined scopes once a defined one is requested (`api/read bogus/x` gets `api/read`). Contributed by @iot-rocket.
- **IoT — reserved MQTT topics** — a client could subscribe to any `$` filter and publish to the replies AWS IoT writes (fleet provisioning, Jobs, Device Defender, file delivery, Device Location, commands); the broker now refuses them before the policy check (SUBACK 0x8F, PUBACK 0x90 or DISCONNECT 0x90 at QoS 0, a closed connection on 3.1.1), judges a `$share/<name>/<filter>` subscription by its filter, and HTTPS `Publish` answers `Topic can't start with $` for fleet provisioning and Device Location replies. Contributed by @iot-rocket.
- **IoT — Jobs** — a stale `expectedVersion` answered 409 and now answers 400 `InvalidRequestException` (`VersionMismatch` over MQTT); a status a device may not set is refused first with `InvalidStateTransitionException`; a `statusDetails` that is not a string map answers 400 instead of 500, an execution has none until a device reports some, and an empty map replaces it; MQTT `get` and `update` answer `ResourceNotFound` for another `executionNumber`; describe and list calls time out expired executions; documents that are not JSON or nest over 1000 levels are refused; and Jobs MQTT messages are compact UTF-8 in AWS's member order, `clientToken` first, characters outside the BMP escaped, `"jobDocument":null` kept. Contributed by @iot-rocket.
- **IoT — shadows, topic rules, sessions and authorization** — `NaN` and `Infinity` in job documents and shadows are refused (`Payload contains invalid json`), and a number beyond a double is stored as `"Infinity"`; unknown topic rules answer 401 `UnauthorizedException` (`Access to topic rule '<name>' was denied`) and bad names, including a trailing newline, AWS's validation message; unacknowledged QoS 1 messages survive a persistent session reconnect without reusing an in-flight packet id. Under `AUTH=true` the data plane answers 403 `ForbiddenException`, `aws:ResourceTag` conditions read the resource's tags, tagging calls are checked on the `resourceArn`, and denials name the resource. Contributed by @iot-rocket.
- **S3 — ACLs and Block Public Access** — `x-amz-grant-*` headers were refused with `MissingSecurityHeader` or dropped; on buckets with ACLs enabled `PutBucketAcl`, `PutObjectAcl`, `CreateBucket`, `PutObject` and `CopyObject` now set exactly the named grants and refuse what S3 refuses, and a `BucketOwnerEnforced` bucket answers 400 `AccessControlListNotSupported`. `BlockPublicAcls` refuses setting a public ACL (403 `AccessDenied`), `IgnorePublicAcls` drops public grants, `RestrictPublicBuckets` the policy, `BlockPublicPolicy` refuses a public bucket policy, and a bucket ACL grants only what its permission covers. Contributed by @iot-rocket.
- **S3 — copy and policy checks under `AUTH=true`** — a copy source in another account's bucket is checked for read access, `x-amz-*` headers sent in the query string are checked like headers, bucket policies for anonymous and cross-account callers use the request's own action and condition keys, and `s3:ExistingObjectTag` resolves through an access point alias. A header-signed `PutObject` checks the bucket name, regional endpoint and signing region before credentials and a missing bucket after them, answering `InvalidBucketName`, `PermanentRedirect`, `AuthorizationHeaderMalformed`, `InvalidAccessKeyId`, `SignatureDoesNotMatch` or `NoSuchBucket` with S3's XML fields. Contributed by @iot-rocket and @gakuto-cw21.
- **S3 — bucket configuration validation** — out-of-enum values in `PutBucketVersioning`, `PutBucketLifecycleConfiguration` and `PutBucketEncryption` now answer 400 `MalformedXML` (`The XML you provided was not well-formed or did not validate against our published schema`), and a non-positive `Days` or `NoncurrentDays` 400 `InvalidArgument` (`'Days' for Expiration action must be a positive integer`).
- **CloudWatch — composite alarms follow their rule** — a composite alarm stayed in `INSUFFICIENT_DATA`; it now evaluates its `AlarmRule` on create, update and every child state change, with AWS's `StateReason`, `StateReasonData` and history. `PutCompositeAlarm` refuses an unknown child or a malformed rule (also one nested over 256 levels) with `ValidationError`, a long `AND`/`OR` chain no longer answers 500, `DeleteAlarms` and stack deletes refuse an alarm a composite depends on and `DeleteAlarms` more than one composite, and `DescribeAlarmHistory` returns composite items only for `AlarmTypes=["CompositeAlarm"]`. Contributed by @iot-rocket.
- **IAM — trust policies with `NotPrincipal`** — `CreateRole`, `UpdateAssumeRolePolicy` and `AWS::IAM::Role` stored them; they now answer `MalformedPolicyDocument` (`Allow with NotPrincipal is not allowed.`, `AssumeRole policy must not contain NotPrincipal field.`). Contributed by @iot-rocket.
- **CloudTrail — `RecursiveLogging` on trails** — `CreateTrail` and `UpdateTrail` ignored it; trails now store it (default `true`, kept by an `UpdateTrail` without it) and return it from every trail response. Contributed by @iot-rocket.
- **CloudFormation — callback URLs under `USE_SSL=1`** — the custom resource `ResponseURL` and the `WaitConditionHandle` URL were always `http://`, which the TLS-only gateway drops, so a handler's `urllib` PUT never arrived and the stack waited out `ServiceTimeout`; both are now `https://` when `USE_SSL` is on, and MiniStack's Lambdas get `SSL_CERT_FILE` set to the gateway's CA bundle, which Python's `ssl` reads. Contributed by @iot-rocket.

## [1.5.29] — 2026-10-17

### Added
- **Managed Service for Apache Flink — control plane** — `kinesisanalyticsv2` applications for the `FLINK-*` runtimes: create, describe, update and delete, start and stop with the model's status transitions, snapshots, CloudWatch logging options and tags, per account and region. Updates bump `ApplicationVersionId` and the `ConditionalToken`, and a stale one returns `ConcurrentModificationException`. CloudFormation creates, updates and deletes `AWS::KinesisAnalyticsV2::Application`, `AWS::KinesisAnalyticsV2::ApplicationCloudWatchLoggingOption` and `AWS::Logs::LogStream`, so a CDK Flink stack deploys. Contributed by @Jolley71717.
- **Managed Service for Apache Flink — job execution** — with Docker available, starting a `FLINK-1_19` or `FLINK-1_20` application runs its jar from MiniStack S3 in the official Apache Flink image (1.19.1 or 1.20.5, Java 11) with a JobManager and a TaskManager, its runtime properties at `/etc/flink/application_properties.json`, its configured parallelism and checkpointing, and its AWS calls pointed at MiniStack in the application's account. Snapshots are Flink savepoints, restores resume the job's state, and a job that fails to start returns the application to `READY`. Jars in `/opt/ministack/flink-lib` (for example a mounted `aws-kinesisanalytics-runtime`) are added to every cluster. Without Docker the control plane behaves as before. Contributed by @Jolley71717.
- **S3 Access Points** — S3 Control `CreateAccessPoint`, `GetAccessPoint`, `ListAccessPoints`, `DeleteAccessPoint` and `Put`/`Get`/`DeleteAccessPointPolicy`. The bucket must be in the request's region, names are unique per account and region, and errors follow AWS (`AccessPointAlreadyOwnedByYou`, `NoSuchAccessPoint`). Requests through an access point ARN (`<name>-<account>.<host>`) or its `-s3alias` alias reach the bucket, including as a `CopyObject` source; signing for another region gets the bucket-region redirect with the access point's region. `DeleteBucket` refuses a bucket with access points. CloudFormation provisions `AWS::S3::AccessPoint`, and under `AUTH=true` each operation needs its `s3:` action. Contributed by @fabio-andre-rodrigues.
- **S3 Object Lambda** — S3 Control `Create`/`Get`/`List`/`DeleteAccessPointForObjectLambda`, `Get`/`PutAccessPointConfigurationForObjectLambda` and the policy operations, with the supporting access point and Lambda in the same account and region. `GetObject` through an Object Lambda ARN or `--ol-s3` alias invokes the Lambda with the documented `getObjectContext` (a presigned `inputS3Url` through the supporting access point, `outputRoute`, `outputToken`) and returns what it writes with `WriteGetObjectResponse`. `HeadObject`, `ListObjects` and `ListObjectsV2` transformations return the Lambda's JSON result; operations without a transformation pass through; `Range` and `partNumber` need `AllowedFeatures`; Lambda failures map to `LambdaRuntimeError`, `LambdaResponseNotReceived`, `LambdaTimeout` and `LambdaNotFound`. CloudFormation provisions `AWS::S3ObjectLambda::AccessPoint` and `AccessPointPolicy`. AWS no longer onboards new Object Lambda customers, so the data plane follows the S3 user guide. Contributed by @fabio-andre-rodrigues.
- **Lambda — `AWS_DISABLE_HOST_PREFIX_INJECTION=true` by default** — Lambda environments set it unless the function does, so boto3 in a handler reaches `WriteGetObjectResponse` without the `<route>.<host>` prefix, which doesn't resolve locally. SDKs that ignore the variable need their host-prefix injection turned off in client config. Contributed by @fabio-andre-rodrigues.

### Fixed
- **Cognito / API Gateway — OAuth scope refusals** — a client credentials grant that named a scope the client lacks, or one that doesn't exist, dropped it and issued a token; `/oauth2/token` now refuses the whole request with 400 `invalid_grant` or `invalid_scope`, and a REST API method whose authorization scopes match none in the access token answers 401 `Unauthorized` (the `UNAUTHORIZED` gateway response) instead of 403. Contributed by @gakuto-cw21.
- **S3 — browser POST policies are enforced** — a form upload was written whatever its policy said; an expired policy, a field that fails a condition or a field the policy doesn't cover now answers 403 `AccessDenied` (`Invalid according to Policy: …`), and under `AUTH=true` the SigV4 or SigV2 signature and the signer's `s3:PutObject` permission are checked. Contributed by @kinoh.
- **CloudWatch Logs — event order across streams and pages** — `FilterLogEvents` stopped at `limit` before sorting across streams, so a newer event could displace an older match; events now merge across streams by timestamp, then ingestion time, and `searchedLogStreams` is empty, as on AWS. `GetLogEvents` excludes events at `endTime`, and its tokens no longer repeat or skip events when late events arrive. Contributed by @kinoh.
- **Lambda — `RevisionId` on updates** — `UpdateFunctionCode`, `UpdateFunctionConfiguration` and `UpdateAlias` ignored `RevisionId`; a stale one now answers 412 `PreconditionFailedException` before anything changes. Contributed by @kinoh.
- **SQS — an empty policy removes it** — `SetQueueAttributes` with an empty `Policy` or `RedriveAllowPolicy` stored the empty string, and an empty `RedrivePolicy` was refused; each now removes the attribute, as Terraform's policy resources expect. Contributed by @kinoh.
- **AppSync Events — `event` is a string** — subscribers' `data` messages carried `event` as a one-element array; it is now the JSON-encoded string, as AWS sends it. Contributed by @edbramwell.
- **S3 — Object Lock is per version** — retention and legal holds were stored per key, so a lock on one version blocked permanent deletes of the key's other versions; they now belong to the version they were set on, `GetObjectRetention`, `PutObjectRetention` and the legal-hold calls take `versionId`, and a `DeleteObject` or `DeleteObjects` without a `VersionId` on a locked key adds a delete marker instead of answering 403. Reported by @bettiah.
- **S3 — Object Lock headers need an Object Lock bucket** — `PutObject`, `CopyObject`, `CreateMultipartUpload` and browser POST uploads with `x-amz-object-lock-*` headers on a bucket created without Object Lock applied the lock; they now answer 400 `InvalidRequest` `Bucket is missing ObjectLockConfiguration`. Reported by @bettiah.
- **KMS — `ScheduleKeyDeletion` window** — a `PendingWindowInDays` outside 7–30 was accepted; it now answers `ValidationException` `PendingWindowInDays must be between 7 and 30`. Reported by @bettiah.
- **KMS — RSASSA-PSS salt** — signatures used a maximum-length salt; they now use a salt as long as the digest, as AWS does, and `Verify` expects the same. Reported by @bettiah.
- **KMS — RSA encryption algorithms** — `Encrypt` and `Decrypt` used OAEP SHA-256 whatever `EncryptionAlgorithm` named; they now use `RSAES_OAEP_SHA_1` or `RSAES_OAEP_SHA_256` as named, so a mismatched `Decrypt`, or one without `EncryptionAlgorithm`, answers `InvalidCiphertextException`, and an `Encrypt` with an algorithm the key doesn't support answers `InvalidKeyUsageException`. Reported by @bettiah.
- **KMS — `Sign` and `Verify` input checks** — a digest of the wrong length or an algorithm the key spec doesn't support crashed with a 500; they now answer 400 `ValidationException` (`Digest is invalid length for algorithm …`) and `InvalidKeyUsageException`, and an unknown algorithm name answers the enum `ValidationException`. Reported by @bettiah.
- **CloudFormation — `AWS::S3::MultiRegionAccessPoint` `Ref` when the access point exists** — a create that found an existing Multi-Region Access Point of the same name returned its alias as the physical id; `Ref` now returns the name in that case too.

## [1.5.28] — 2026-10-16

### Added
- **RDS — `DescribeEvents` with reboot events** — `DescribeEvents` returns the `availability` event `DB instance restarted.` once a `RebootDBInstance` has finished, filtered by source, source type, category and time, paged with `Marker` and kept for 14 days; other RDS lifecycle events are not recorded yet. Contributed by @jayjanssen.
- **S3 Control — Multi-Region Access Points** — `CreateMultiRegionAccessPoint`, `DeleteMultiRegionAccessPoint`, `PutMultiRegionAccessPointPolicy`, `DescribeMultiRegionAccessPointOperation`, `GetMultiRegionAccessPoint`, `GetMultiRegionAccessPointPolicy`, `ListMultiRegionAccessPoints`, `GetMultiRegionAccessPointRoutes` and `SubmitMultiRegionAccessPointRoutes`. The async operations apply at once and `Describe` reports `SUCCEEDED`, or `FAILED` with AWS's `ErrorDetails` for a missing bucket, two buckets in one region or a name already in use. Management is served from `us-west-2` and route operations from `us-east-1`, `us-west-2`, `eu-west-1`, `ap-northeast-1` and `ap-southeast-2`, as on AWS; other regions get `301 PermanentRedirect`. `DeleteBucket` on a member returns `BucketHasAccessPointsAttached`. Under `AUTH=true` each operation needs its `s3:` IAM action and `x-amz-account-id` must name the caller's account. Access points from `AWS::S3::MultiRegionAccessPoint` appear in the API. Contributed by @fabio-andre-rodrigues.
- **S3 — Multi-Region Access Point failover** — a request through the MRAP host goes to the member with a traffic dial of 100 that is homed in `MINISTACK_REGION`, else the first active member. Setting a dial to 0 moves traffic to the other member, and an update that leaves every dial at 0 is refused. Contributed by @fabio-andre-rodrigues.
- **CloudFormation — `DescribeEvents`** — returns the validation errors of a create that fails pre-deployment validation, which the stack's `CREATE_FAILED` event tells the caller to look up; every request member is optional. Contributed by @fabio-andre-rodrigues.
- **WAF v2 — regex pattern sets** — `CreateRegexPatternSet`, `GetRegexPatternSet`, `UpdateRegexPatternSet`, `DeleteRegexPatternSet` and `ListRegexPatternSets` now manage regional and CloudFront-scoped regex pattern sets, with their ARN (`regexpatternset`), tags and `LockToken` checks on update and delete (`WAFOptimisticLockException` for a stale token), as AWS does. Web ACL and rule group rules are stored as sent, so a `RegexPatternSetReferenceStatement` pointing at a pattern set ARN round-trips; the statement is not evaluated, as MiniStack does not inspect requests. Contributed by @OS-kiranmalsetty.
- **IoT — job rollout rates** — `jobExecutionsRolloutConfig` was stored but every execution was created at once; a job now creates `maximumPerMinute` executions a minute, or a rate that starts at `baseRatePerMinute` and grows by `incrementFactor` for every `rateIncreaseCriteria` step of notified or succeeded things, notifies each thing when its execution is created, reports `isConcurrent` until the last batch and stops when the job is canceled or deleted. Contributed by @iot-rocket.
- **Health — `auth` field** — `/_ministack/health` reports `"auth": "enabled"` or `"disabled"`, the effective `AUTH` mode. Contributed by @jgrumboe.
- **Auth — REST API resource policies are evaluated under `AUTH=true`** — an API Gateway resource policy took no part in the decision; for an `execute-api:Invoke` a statement naming the calling user or role now allows the call without an identity policy, a statement naming only the account still needs one, a Deny wins over an identity Allow, and a call without a SigV4 signature (none, or a bearer token) to a method without authorization passes only when the policy, if any, allows everyone, else it answers `ACCESS_DENIED` with API Gateway's message. Contributed by @iot-rocket.
- **Auth — KMS key policies are evaluated under `AUTH=true`** — a key policy took no part in the decision; it now has to allow even in the key's own account: a statement naming the calling user or role allows the call without an identity policy, identity policies count only when the key policy has a statement for the account (the default key policy does), and a Deny wins over an identity Allow. Contributed by @iot-rocket.
- **Auth — ECR repository policies count under `AUTH=true`** — a repository policy took no part in the decision; a statement naming the calling user or role now allows the call without an identity policy, a statement naming only the account still needs one, and a Deny wins over an identity Allow. Contributed by @iot-rocket.
- **Auth — Secrets Manager resource policies count under `AUTH=true`** — a secret's resource policy took no part in the decision; a statement naming the calling user or role now allows the call without an identity policy, a statement naming only the account still needs one, and a Deny wins over an identity Allow. Contributed by @iot-rocket.
- **Auth — Lambda function policies count under `AUTH=true`** — an `AddPermission` statement naming the calling user or role did not let a caller without an identity policy in; it now allows the actions it names (`InvokeFunction`, `GetFunction`, ...), while a statement naming only the account still needs an identity Allow, and a call that names the function by ARN is checked against that ARN. Contributed by @iot-rocket.
- **Auth — bucket policies count under `AUTH=true` in the owner's account** — a bucket policy was consulted only for another account's requests; in the owner's account a statement naming the calling user or role now allows the request without an identity policy, a statement naming only the account still needs one, and a Deny wins over an identity Allow, CDK's `aws:SecureTransport` Deny on the bootstrap bucket included. Contributed by @iot-rocket.
- **Auth — queue and topic policies count under `AUTH=true`** — a queue or topic policy statement naming the calling user or role did not let a caller without an identity policy in; in the owner's account it now allows the call, a statement naming only the account still needs an identity Allow, and across accounts both have to allow. A denial answers `AuthorizationError` on SNS and reports `AccessDenied` to SDKs on SQS, its message says "with an explicit deny in a resource-based policy" when the resource's policy denied, and the check reads the queue of the account the `QueueUrl` names. Contributed by @iot-rocket.
- **IoT — `EnableTopicRule` and `DisableTopicRule`** — both calls were missing (`POST /rules/{ruleName}/enable` answered `Invalid ruleName`), so a rule could only be switched off by replacing it; they now set `ruleDisabled`, which `GetTopicRule` and `ListTopicRules` report and which stops the rule from acting on matching publishes and Basic Ingest until it is enabled again. Contributed by @iot-rocket.
- **IoT — job documents from S3** — `CreateJob` and `CreateJobTemplate` read a `documentSource` (`s3://` or an S3 object URL) from MiniStack's S3 once, at creation, where they stored a `{"documentSource": "<url>"}` placeholder. A missing bucket or object, an object that is not JSON or is over 32 KB and a URL that names no S3 host are refused with AWS's messages, and `CreateJob` refuses `document` together with `documentSource`. Devices get every job document re-serialized as AWS sends it, without spaces and without object members that are null, while `GetJobDocument` keeps the stored text. Contributed by @iot-rocket.
- **IoT — presigned URLs in job documents** — devices got `${aws:iot:s3-presigned-url:<url>}` and `${aws:iot:s3-presigned-url-v2:<url>}` placeholders as written; the `iot-jobs-data` API and the MQTT jobs topics now replace each with a SigV4 presigned GET URL of MiniStack's S3, signed with a session of `presignedUrlConfig.roleArn` and valid for `expiresInSec`, and `CreateJob` and `CreateJobTemplate` refuse a placeholder without a role or an S3 object URL with AWS's messages. Contributed by @iot-rocket.
- **IoT — fleet provisioning by claim** — `CreateProvisioningClaim` issues a five-minute claim certificate that the mTLS listener accepts until it expires, and a device connected with it (or with any registered certificate) gets a certificate from `$aws/certificates/create/json` and registers itself through `$aws/provisioning-templates/<name>/provision/json`: the template's parameters, `Ref` and `Fn::Join` are resolved, the thing is created or checked against its `OverrideSettings`, and the certificate is activated and attached to the thing and the template's policies, with AWS's replies and rejections on the requesting connection; with `AUTH=true` a claim connection is limited to the certificate topics and its own template's, as on AWS. Contributed by @iot-rocket.
- **IoT — pre-provisioning hook** — `RegisterThing` over MQTT now invokes the template's `preProvisioningHook` Lambda before the template is resolved and applies its answer the way AWS does: `parameterOverrides` are merged into the device's parameters, `allowProvisioning: false` is rejected as `AccessDenied`, and a function that fails, answers in another shape, takes longer than six seconds or may not be invoked gets AWS's `InvocationError` or `InvalidResult`. `CreateProvisioningTemplate` and `UpdateProvisioningTemplate` check the hook like AWS (a Lambda function ARN, payload version `2020-04-01`, no hook on JITP templates, and an invoke permission for `iot.amazonaws.com` that covers the template) and store the default payload version. Contributed by @iot-rocket.
- **IoT — provisioning template versions** — `CreateProvisioningTemplateVersion`, `ListProvisioningTemplateVersions`, `DescribeProvisioningTemplateVersion` and `DeleteProvisioningTemplateVersion` answered `Unsupported IoT path` and `UpdateProvisioningTemplate` refused every `defaultVersionId` but 1; a template now keeps up to five versions whose ids are never reused, `setAsDefault` and `defaultVersionId` choose the version that `DescribeProvisioningTemplate` returns and `RegisterThing` applies, the default version cannot be deleted, and an `AWS::IoT::ProvisioningTemplate` whose `TemplateBody` changes gets a new default version (deleting the oldest at the limit), all with AWS's errors, including its message for a body that is not JSON. Contributed by @iot-rocket.
- **Cognito — `UpdateUserPoolDomain` and `AWS::Cognito::UserPoolDomain` stack updates** — `UpdateUserPoolDomain` was an unknown action; it now changes a domain's `ManagedLoginVersion` and a custom domain's certificate, `CreateUserPoolDomain` and `DescribeUserPoolDomain` return the version, and a second prefix domain on a pool is refused as on AWS. A stack update applies `ManagedLoginVersion` and `CustomDomainConfig` through it instead of running the create again, a `Domain` or `UserPoolId` change replaces the domain, the create keeps `CustomDomainConfig`, `Ref` returns the domain name and `Fn::GetAtt` returns `CloudFrontDistribution`. Contributed by @iot-rocket.
- **CloudFormation — SES configuration set updates** — `AWS::SES::ConfigurationSet` and `AWS::SES::ConfigurationSetEventDestination` stored none of their options, tags or destinations, and a change re-ran the create; both now provision through the SES v2 calls and update in place, a removed option goes back to its default as on AWS, and only a `Name` or `ConfigurationSetName` change replaces them. Contributed by @iot-rocket.
- **SES v2 — configuration set options and event destinations** — `GetConfigurationSet` returned only the name and the tags given at creation; it now returns the tracking, delivery, reputation, sending, suppression, VDM and archiving options that `CreateConfigurationSet` and the seven `PutConfigurationSet*Options` calls set, and a call clears the members it leaves out, as on AWS. `CreateConfigurationSetEventDestination`, `GetConfigurationSetEventDestinations`, `UpdateConfigurationSetEventDestination` and `DeleteConfigurationSetEventDestination` are new. `CreateConfigurationSet` with a name already in use is refused with `AlreadyExistsException` instead of replacing the set. Contributed by @iot-rocket.
- **IoT — job abort criteria** — a job's `abortConfig` was stored but never acted on; once a device report, a `CancelJobExecution`, a `DeleteJobExecution` or a timeout meets one of its criteria, the job is now canceled with reasonCode `ABORT_CANCEL` and AWS's comment, together with its `QUEUED` executions. `CreateJob` and `CreateJobTemplate` stored any `abortConfig`; they now refuse what `UpdateJob` refuses, and all three refuse a missing criterion member, a threshold of 0 or less or with more than two decimals, and a failure type given twice. A `CancelJob` without `force` now reports `forceCanceled` as false. Contributed by @iot-rocket.

### Changed
- **RDS — real instance reboot** — `RebootDBInstance` now returns `rebooting`, stops and starts its backing database container, and waits for SQL readiness before reporting `available`. Data and endpoint bindings are preserved. An instance that is not `available` answers `InvalidDBInstanceState`, and a Docker failure reports `failed` rather than a successful reboot. Contributed by @jayjanssen.

### Fixed
- **CloudFront — function status follows publish and association** — `DescribeFunction` and `ListFunctions` reported the `DEVELOPMENT` stage as `UNPUBLISHED` even after `PublishFunction`, so Terraform's `aws_cloudfront_function.status`, which reads that stage, never left `UNPUBLISHED`; once the current development version is published it now reports `UNASSOCIATED`, or `DEPLOYED` while a distribution's cache behavior references the function, and an `UpdateFunction` makes it `UNPUBLISHED` again until the next publish. Contributed by @fabio-andre-rodrigues.
- **S3 — bucket home region is enforced** — a request signed for a region other than the bucket's now returns `301 PermanentRedirect` with `x-amz-bucket-region`, which SDKs follow; at the bucket's own regional endpoint it returns `400 AuthorizationHeaderMalformed` or, presigned, `AuthorizationQueryParametersError`, with `<Region>`. HEAD gets the bodiless forms. `GetBucketLocation`, `ListBuckets`, `CreateBucket`, unsigned, SigV2 and SigV4a requests, MRAP requests and internal callers are exempt, and `CopyObject` from a source in another region still works. Contributed by @fabio-andre-rodrigues.
- **S3 — `CreateBucket` location constraint (breaking)** — a request signed for a region other than `us-east-1` must name that region as `LocationConstraint`, or gets `IllegalLocationConstraintException`; `LocationConstraint=us-east-1` returns `InvalidLocationConstraint`. Clients that created buckets in another region without a constraint now need one. `BucketAlreadyOwnedByYou` and `BucketAlreadyExists` carry AWS's message and `<BucketName>`. Contributed by @fabio-andre-rodrigues.
- **S3 — `ListBuckets` `bucket-region` filter** — `BucketRegion` lists only that region's buckets, with pagination inside the filter; a filter for another region than the endpoint's returns `InvalidArgument`. Contributed by @fabio-andre-rodrigues.
- **S3 — replication** — delete markers replicate for V1 rules (no `Filter`) and for V2 rules with `DeleteMarkerReplication` enabled and no tag filter, from `DeleteObject` and `DeleteObjects`; version-id deletes don't. Rules with `Filter.Tag` or `Filter.And.Tags` replicate only objects carrying those tags. Contributed by @fabio-andre-rodrigues.
- **SigV4a requests no longer land in region `s3`** — the region was read from a credential scope that SigV4a doesn't have; a single-region `X-Amz-Region-Set` is used, else the default region. Contributed by @fabio-andre-rodrigues.
- **Lambda — S3 code from another region** — `CreateFunction`, `UpdateFunctionCode`, `PublishLayerVersion` and CloudFormation return AWS's `InvalidParameterValueException` when the code bucket is homed elsewhere. Contributed by @fabio-andre-rodrigues.
- **Resource Groups Tagging — S3 buckets by region** — `GetResources`, `GetTagKeys` and `GetTagValues` list only buckets homed in the region they are called in. Contributed by @fabio-andre-rodrigues.
- **CloudFormation — `User Initiated` on stack events** — the stack's `CREATE_IN_PROGRESS` and `UPDATE_IN_PROGRESS` events of `CreateStack` and `UpdateStack` carried no reason; they now carry `User Initiated`, as AWS does.
- **CloudFormation — `AWS::S3::Bucket` and existing names** — an `AWS::S3::Bucket` whose `BucketName` already exists, in this account or another, was adopted (another account's became a second bucket of that name); it now fails pre-deployment validation with `NAME_CONFLICT_VALIDATION`, or with `DisableValidation` the resource's create with `<name> already exists`, as on AWS, and auto-named buckets get a new name for each stack instance. `CreateStack`, `UpdateStack` and `CreateChangeSet` take `DisableValidation`, and an update now runs the same pre-deployment validation, ending in `UPDATE_ROLLBACK_COMPLETE` without touching a resource (a bucket the stack already owns is not a conflict). With rollback disabled, a failed validation emits AWS's second `CREATE_FAILED` event (breaking: a stack no longer adopts a bucket left over from an earlier stack or an S3-only restore). Contributed by @fabio-andre-rodrigues.
- **AppSync — GraphQL requests on the API's own host** — a `POST /graphql` to `{apiId}.appsync-api.{region}.{host}`, the endpoint `uris.GRAPHQL` reports, was routed to AppSync Events when unsigned and found its API only by `x-api-key` when signed, so an `AWS_IAM` API could not be called there once a second API existed; the host label now selects the API (in the host's region for an unsigned request) and `/graphql` on an Event API host answers `404 ResourceNotFoundException`, as on AWS. Contributed by @iot-rocket.
- **Cognito — no ID token without `openid`** — the `/oauth2/token` authorization code and refresh token grants returned an `id_token` whatever was granted; they now return one only when the grant carries `openid`, as AWS does, and a refresh token from `InitiateAuth` or `AdminInitiateAuth` still gets one. Contributed by @iot-rocket.
- **Cognito — token endpoint access tokens carry the granted scopes** — `/oauth2/token` wrote `aws.cognito.signin.user.admin` into every access token whatever was granted; the authorization code, client credentials and refresh token grants now write the requested scopes the app client allows (all of them when none are requested, only resource server scopes for client credentials) in the order AWS lists them, refreshed tokens keep the original scopes, and the pre token generation event's `request.scopes` lists them, so API Gateway scope checks decide as on AWS. Contributed by @iot-rocket.
- **S3 — object ACL grants answer another account's read** — a bucket owned by another account refused every read that only the object ACL granted: the ACL was looked up under the wrong version, and a grant to the caller's canonical ID was never read. With ACLs enabled, a `READ` grant to the account or a group now opens `GetObject` and `HeadObject` of the version it was set on, `?versionId=` included, and `READ_ACP` opens `GetObjectAcl`; a grant to one account is not public, so Block Public Access leaves it in force. Contributed by @iot-rocket.
- **CloudWatch — `DescribeAlarms` and `DescribeAlarmHistory` paging** — both returned at most `MaxRecords` items (default 100) and never a `NextToken`, so a paginator stopped after the first page; they now return a `NextToken` while more items match, default to 50 items and refuse a `MaxRecords` outside 1 to 100 with `ValidationError`. `DescribeAlarms` now lists metric and composite alarms in one name-ordered sequence that `MaxRecords` counts together, and `DescribeAlarmHistory` lists the newest items first unless `ScanBy` is `TimestampAscending`. `DescribeAlarms` without `AlarmTypes` now returns metric alarms only, as on AWS; composite alarms need `AlarmTypes=["CompositeAlarm"]`. Contributed by @iot-rocket.
- **CloudWatch Logs — a log group's resource policy by `resourceArn` alone** — `PutResourcePolicy` and `DeleteResourcePolicy` refused a `resourceArn` without `policyName`; they now take it, store the group's policy without a name and with a counting `revisionId`, and answer a request with neither field or a revision conflict with AWS's errors. `AWS::Logs::LogGroup` now creates, rewrites and removes the group's own policy from `ResourcePolicyDocument`. Contributed by @iot-rocket.
- **CloudTrail — trail read-backs and recording switched on at runtime** — `GetTrail` and `DescribeTrails` returned the stored `IsLogging`, `_StartedAt` and `_StoppedAt` keys and empty `S3KeyPrefix` and `SnsTopicName` members, and `GetTrailStatus` left out the six deprecated string members and moved `StopLoggingTime` when a stopped trail was stopped again; they now answer in AWS's shapes, with the start and stop times in milliseconds, and keep the last stop time. Recording switched on through `/_ministack/config` began only after the first CloudTrail API call and now starts with the next request. Contributed by @iot-rocket.
- **ACM / Signer — certificate key and in-use refusals** — `ImportCertificate` accepted a call without a `PrivateKey` and now answers AWS's `ValidationException`; `DeleteCertificate` removed a certificate an Active Signer signing profile uses and now answers `ResourceInUseException`, with those profiles listed in `DescribeCertificate`'s `InUseBy`; `CancelSigningProfile` answered 200 with `{}` and now answers 204 with an empty body, as on AWS. Contributed by @iot-rocket.
- **IoT — not-found messages match AWS** — a call naming a thing, thing type, thing group, certificate, CA certificate, principal, policy, policy version, job, job execution, job target or provisioning template that does not exist answered `ResourceNotFoundException` with a generic `<Resource> '<name>' not found`, and the device jobs plane (HTTP and MQTT `/rejected`) with `No job execution found for thing … and job …`; each now carries the wording AWS uses for that operation, with the same error code and status. Contributed by @iot-rocket.
- **IoT Jobs — terminal job executions on the device plane** — a device's `DescribeJobExecution` (HTTP and the MQTT `get` topic) of a SUCCEEDED, FAILED, REJECTED, TIMED_OUT, CANCELED or REMOVED execution answered with the execution; it now answers `TerminalStateException` (410) and `TerminalStateReached` over MQTT, as AWS does. `UpdateJobExecution` and `CancelJobExecution` of a terminal execution now refuse with AWS's messages (over MQTT `InvalidStateTransition` with the `executionState`), a refused status such as QUEUED is checked first, and a device that reports SUCCEEDED or FAILED straight from QUEUED now stamps `startedAt`. Contributed by @iot-rocket.
- **IoT / CloudFormation — stack tags and provisioning templates** — stack-level tags never reached IoT resources; they now reach thing types, thing groups, policies, topic rules, CA certificates and provisioning templates without the `aws:cloudformation:` tags (job templates get none, as on AWS), and an update that only changes tags no longer adds a policy version. An `AWS::IoT::ProvisioningTemplate` `Enabled` of `"false"` from a parameter or the template enabled the template; it now disables it. `CreateProvisioningTemplate` and the CloudFormation type accepted any certificate resource; they now refuse one without a `Status` of `Active` or `Inactive` (any case) or whose `CertificateId` is not a reference to `AWS::IoT::Certificate::Id`, a second certificate resource and a resource without `Properties`, with AWS's messages. Contributed by @iot-rocket.
- **IoT — device certificates take the mode of their CA** — a device certificate registered under a CA in `SNI_ONLY` mode, with or without `caCertificatePem`, or created by just-in-time registration reported `certificateMode` `DEFAULT`; it now takes its CA's mode, and RegisterCertificateWithoutCA stays `SNI_ONLY`. Contributed by @iot-rocket.
- **IoT — deletes of unknown resources and `DeleteThing` `expectedVersion`** — `DeleteThing`, `DeleteThingType`, `DeleteThingGroup`, `RemoveThingFromThingGroup` with an unknown group and `DetachPolicy` with an unknown policy answered `ResourceNotFoundException`, and `DeleteThing` ignored `expectedVersion`; they now answer 200 as AWS does, and `DeleteThing` refuses a wrong `expectedVersion` with `VersionConflictException` before the attached-principal check. Contributed by @iot-rocket.
- **CloudFormation — IoT policy and thing type deletes check what still uses them** — a stack delete removed an `AWS::IoT::Policy` or `AWS::IoT::ThingType` directly, even while a certificate outside the stack had the policy attached or a thing used the type; the policy delete now removes the non-default versions and then calls `DeletePolicy`, the thing type delete deprecates the type and then calls `DeleteThingType`, so a resource still in use fails its delete with the service's message and the stack ends in `DELETE_FAILED`, as on AWS. Contributed by @iot-rocket.
- **IoT — MQTT 5 message expiry** — the broker forwarded the Message Expiry Interval unchanged and never expired a message; a subscriber now receives the seconds left (60 arrives as 59, 0 counts as 1, intervals above seven days are capped at 604800), an expired retained message is no longer replayed to new subscribers or returned by `GetRetainedMessage` and `ListRetainedMessages`, and a QoS 1 message queued for an offline persistent session is dropped once it expires (the queue also keeps the MQTT 5 properties now). Contributed by @iot-rocket.
- **IoT — HTTP `Publish` keeps the MQTT 5 properties and a literal `%` in the topic** — `Publish` ignored `userProperties`, `payloadFormatIndicator`, `contentType`, `responseTopic`, `correlationData` and `messageExpiry`, so MQTT 5 subscribers got none of them and `GetRetainedMessage` returned no `userProperties`; they now reach subscribers and the retained message, and invalid values are refused with AWS's `InvalidRequestException` messages. `Publish`, `GetRetainedMessage` and the `AUTH=true` topic ARN decoded the topic a second time, so `p%41q` became `pAq`; the topic is now kept as sent. `/_ministack/reset` kept messages retained over MQTT when no iot-data call had loaded that module; it now clears them. Contributed by @iot-rocket.
- **IoT Core — MQTT 5 PUBACK reports Success without subscribers** — a QoS 1 PUBLISH that no subscriber matched (including a topic only a rule reads, Basic Ingest and a retained publish) was acknowledged with reason code `0x10` No matching subscribers; it now gets `0x00` Success, as AWS IoT answers over both MQTT over TLS and MQTT over WebSocket. Contributed by @iot-rocket.
- **CloudFormation — AppSync data sources and resolvers keep their configuration** — `AWS::AppSync::DataSource` dropped `Description`, `HttpConfig` and `EventBridgeConfig` and stored `DynamoDBConfig` and `LambdaConfig` in the template's PascalCase, and `AWS::AppSync::Resolver` dropped `Kind`, `Runtime`, `Code`, `PipelineConfig` and `MaxBatchSize`, so a DynamoDB resolver from a stack answered `null` and an `APPSYNC_JS` resolver never ran; both now go through `CreateDataSource` and `CreateResolver` and read back as on AWS, and a new data source `Name` or resolver `TypeName` or `FieldName` is reported as a replacement. `GetDataSource` and `GetResolver` leave out a description, service role or mapping template that was never set, a DynamoDB data source reports `useCallerCredentials` and `versioned`, an EventBridge one keeps `eventBridgeConfig`, and a resolver reports `maxBatchSize`. Contributed by @iot-rocket.
- **CloudFormation — `AWS::AutoScaling::LifecycleHook` updates** — a stack update ran the create again, so a member dropped from the template went back to its default and a hook with a generated name moved to another group stayed on the old group too; the hook now updates in place and keeps a dropped member, and an `AutoScalingGroupName` or `LifecycleHookName` change replaces it. `NotificationMetadata` is stored and returned by `DescribeLifecycleHooks`, and a group deleted by its stack takes its lifecycle hooks along. Contributed by @iot-rocket.
- **IoT — mTLS refusals close without a CONNACK under `AUTH=true`** — with `AUTH=true` the mTLS listener closes the connection of an unknown, inactive, revoked or doubly-registered client certificate after its CONNECT without sending a CONNACK, as AWS does on MQTT 3.1.1 and 5; without `AUTH` it still answers a "not authorized" CONNACK, and the log names the reason either way. Contributed by @iot-rocket.
- **IoT — `AttachPolicy` to a target that does not exist** — `AttachPolicy` and `AttachPrincipalPolicy` accepted any target string; they now answer `ResourceNotFoundException` for an unknown certificate or thing group, `InvalidRequestException` for anything else that is not a certificate, a thing group (`AttachPolicy` only) or a Cognito identity id, and `AccessDeniedException` for another account's certificate, which also fails an `AWS::IoT::PolicyPrincipalAttachment` naming such a principal. Contributed by @iot-rocket.
- **IoT — `SetDefaultPolicyVersion`** — the call was missing (`PATCH /policies/{policyName}/version/{policyVersionId}` answered `Unsupported policy path`), so only `CreatePolicyVersion` with `setAsDefault` could move a policy's default version; it now sets the default that `GetPolicy`, `GetPolicyVersion`, `ListPolicyVersions` and the MQTT policy evaluation under `AUTH=true` use, deleting the default version answers `DeleteConflictException` (409) instead of 400, and `ListPolicyVersions` lists the newest version first. Contributed by @iot-rocket.
- **STS — session policies and `GetFederationToken`** — `AssumeRole` ignored `Policy` and `PolicyArns` and `GetFederationToken` answered `InvalidAction`; under `AUTH=true` a role session now gets only what both the role and its session policies allow, a `Deny` in them denies, and `AssumeRole` rejects a malformed policy with `MalformedPolicyDocument` and an unknown policy ARN with `ValidationError`. `GetFederationToken` is added: its federated user gets the calling user's permissions within the session policies (none without one) and its `Tags`, not the user's tags, as `aws:PrincipalTag/*`; it checks its `Name`, policies and tags, and under `AUTH=true` a temporary caller answers `AccessDenied`. Contributed by @iot-rocket.
- **IAM — permissions boundaries are enforced** — the boundaries of users and roles were stored and never applied; under `AUTH=true` a boundary now limits what the identity policies allow and a `Deny` in it denies, a boundary must name a known managed policy (`ValidationError` for a malformed ARN, CloudFormation `AWS::IAM::User` included), deleting an absent boundary answers `NoSuchEntity`, and a policy in use as a boundary cannot be deleted. `ListUsers` and `ListRoles` leave the boundary out, `ListEntitiesForPolicy` lists boundary users and roles and honours `PolicyUsageFilter`, and a managed policy reports `PermissionsBoundaryUsageCount`. Contributed by @iot-rocket.
- **API Gateway — `GetExport` includes Lambda authorizers** — a REST API export had no security scheme for a `TOKEN` or `REQUEST` authorizer and no `security` field on its methods, so tools such as Swagger UI showed no "Authorize" button. Each authorizer that a `CUSTOM` method uses is now a scheme in `components.securitySchemes` (OAS 3.0) or `securityDefinitions` (Swagger 2.0), and the method has `security`. With `extensions=authorizers` or `extensions=apigateway`, the scheme also has the `x-amazon-apigateway-authorizer` block. Checked against a real AWS export and the `x-amazon-apigateway-authorizer` docs. Contributed by @mishukdutta-cz.
- **STS — trust policies see principal tags, session tags and web identities under `AUTH=true`** — `AssumeRole` trust conditions had no `aws:PrincipalTag/*`, a `Service` principal matched any IAM user whose ARN contained the service name, `Tags` passed without `sts:TagSession` in the trust policy, and `AssumeRoleWithWebIdentity` skipped the trust policy; trust conditions now see the caller's keys and the session tags as `aws:RequestTag/*` and `aws:TagKeys`, a `Service` principal no longer matches an IAM user, session tags need `sts:TagSession`, and a web identity needs a `Federated` principal naming its issuer, with `<issuer>:aud`, `:sub` and `:amr` evaluated. Under `AUTH=true` session tag keys that differ only in case answer `InvalidParameterValue`. Contributed by @iot-rocket.
- **IAM — `aws:ResourceTag/*` under `AUTH=true`** — the key was always absent outside CloudWatch Logs, so a tag-scoped grant denied every request; a request on an existing SQS queue, SNS topic, DynamoDB table, Lambda function, IAM user or role, SSM parameter, Secrets Manager secret or KMS key now sees that resource's tags. Contributed by @iot-rocket.
- **IAM — `aws:RequestTag/*` and `aws:TagKeys` under `AUTH=true`** — only CloudWatch Logs filled them; a call whose API takes `Tags` or `TagKeys` (create, tag and untag calls such as `CreateQueue`, `CreateTable`, `CreateUser`, `PutParameter`, `CreateSecret`, `CreateKey`) now carries the tags it sets and the keys it names, `aws:TagKeys` is absent on a call that names none, and Lambda `TagResource`, `UntagResource` and `ListTags` are authorized against the ARN in the path instead of skipped. Contributed by @iot-rocket.
- **IAM — caller and transport condition keys under `AUTH=true`** — identity policies saw none of the caller's keys and a fixed `127.0.0.1` over plain HTTP; they now see `aws:username`, `aws:userid`, `aws:PrincipalTag/*` (the user's or role's tags, `AssumeRole` session tags taking precedence; in `rds-db:connect` too), for temporary credentials `aws:MultiFactorAuthPresent` and `aws:TokenIssueTime`, the client's address as `aws:SourceIp` and `aws:SecureTransport` as `true` over HTTPS or behind a proxy that sends `X-Forwarded-Proto: https`, and `AssumedRoleId` starts with the role's `RoleId` with or without `AUTH`. Contributed by @iot-rocket.
- **S3 — requests without credentials are anonymous under `AUTH=true`** — an unsigned request (no `Authorization` header, presigned query or POST form credential) ran as the root key, so a private bucket in the caller's own account answered 200 to anyone; it is now anonymous, and only the owner's public grants let it in: an `AllUsers` ACL grant or a bucket policy that allows `*`, unless Public Access Block shuts them off. Anything else answers 403 `AccessDenied`. A `public-read` object ACL is now also found for an unversioned object, which the grant check missed for cross-account callers too. Without `AUTH` nothing changes. Contributed by @prandogabriel.
- **Cognito — `InboundFederation` `userName`** — the trigger received `<provider>_<NameID>` for SAML and `<provider>_<sub>` for OIDC; it now receives the bare SAML `NameID` or OIDC `sub`. Contributed by @kjdev.
- **S3 Tables — `CreateTable` keeps Iceberg column types** — a `schema` or `schemaV2` field type other than `string`, `int`, `long`, `boolean`, `date`, `timestamp`, `float` or `double` was stored as `string`, and `timestamp` as `timestamptz`; every Iceberg v2 type, nested `list`, `struct` and `map` included, is now stored as AWS stores it (lowercase, `decimal(P, S)`), and any other type answers `BadRequestException` `The specified metadata is not valid`. Reported by @anatollius.
- **S3 Tables — the Iceberg REST catalog takes the table bucket prefix** — a request without the `{prefix}` searched every table bucket, so two buckets with the same namespace and table name answered with the first one; such a request now answers 404 `<UnknownOperationException/>`, `GET /v1/config` without a table bucket ARN `warehouse` answers 400 `The specified table bucket ARN is not valid.`, and the url-encoded ARN comes back as `prefix` in `defaults`.

## [1.5.27] — 2026-10-13

### Added
- **API Gateway — failed SigV4 authentication answers through the gateway responses** — under `AUTH=true` a REST API invoke that failed authentication got the generic JSON error and ignored the API's gateway responses; a wrong secret, a skewed clock or a credential date other than the request date now answers `INVALID_SIGNATURE` (`InvalidSignatureException`), an expired session `EXPIRED_TOKEN` (`ExpiredTokenException`), and an unknown key or a malformed `Authorization` header `DEFAULT_4XX` with the matching `x-amzn-ErrorType`, 403 with `{"message": ...}`. Contributed by @iot-rocket.
- **Auth — the request time of a header-signed request is checked** — under `AUTH=true` a request signed with an IAM user or STS session key was served whatever its time; one signed more than 15 minutes (STS, IAM, SNS, SQS, DynamoDB, S3, ECR, RDS, ElastiCache) or 5 minutes (the default for other services) from the server clock answers `SignatureDoesNotMatch` / `InvalidSignatureException` with AWS's `Signature expired` or `Signature not yet current` message, `RequestTimeTooSkewed` from S3 and `AuthFailure` from EC2. Contributed by @iot-rocket.
- **Auth — the session token of a header-signed request is checked** — under `AUTH=true` an STS session key sent without its token was accepted and now answers as an unknown key, and a wrong token, or a token with an IAM user key, answers `InvalidClientTokenId` (`UnrecognizedClientException` from JSON services, `InvalidToken` 400 from S3), as on AWS. Contributed by @iot-rocket.
- **Auth — header signatures are verified for IAM user and STS session keys** — under `AUTH=true` a request signed with such a key and the wrong secret, or changed after signing, was served; it now answers `SignatureDoesNotMatch` (or the service's `InvalidSignatureException`), an `Authorization` header without its `Signature` or `SignedHeaders` answers `IncompleteSignature` (Lambda `MissingAuthenticationTokenException`, S3 `AuthorizationHeaderMalformed`) and a credential date other than the request date `SignatureDoesNotMatch` (S3 `AuthorizationHeaderMalformed`), S3 unsigned, streaming and signed-chunk payloads are checked the same way, and the root key still accepts any secret. Contributed by @iot-rocket.
- **IoT — MQTT publish, subscribe and receive evaluate IoT policies under `AUTH=true`** — after the CONNECT a certificate session could publish, subscribe and receive on any topic; it now needs `iot:Publish` on the topic (and `iot:RetainPublish` for a retained message or will), `iot:Subscribe` per topic filter and `iot:Receive` per delivered message. A refused PUBLISH or SUBSCRIBE closes an MQTT 3.1.1 connection (disconnect reason `CLIENT_ERROR`), MQTT 5 answers PUBACK 0x87, DISCONNECT 0x87 at QoS 0 (`AUTH_ERROR`) or 0x87 for the refused filter, which the `subscribed` event leaves out (no event when none is granted), a refused will refuses the CONNECT, and a message the subscriber may not receive is dropped. Contributed by @iot-rocket.
- **IoT — MQTT connections evaluate `iot:Connect` under `AUTH=true`** — a device certificate on the mTLS listener connected under any client id, with or without a policy; with `AUTH=true` the CONNECT now needs an attached policy that allows `iot:Connect` on `client/<client id>` and none that denies it, with `${iot:ClientId}`, `${iot:Connection.Thing.ThingName}`, `iot:Connection.Thing.IsAttached` and `${iot:Certificate.Subject.CommonName}` resolved. A refusal closes an MQTT 3.1.1 connection without a CONNACK, answers MQTT 5 with CONNACK 0x87 and publishes `$aws/events/presence/connect_failed/{clientId}`. Contributed by @iot-rocket.
- **IoT — `TagResource`, `UntagResource` and `ListTagsForResource`** — the three operations answered `Unsupported IoT path`, so the tags the Create calls accept could not be read back. They now work on thing types, thing groups, policies, topic rules, jobs, job templates, CA certificates, provisioning templates and domain configurations with AWS's limits and errors, Create calls and CloudFormation `Tags` write the same tags, and deleting a resource drops them. Contributed by @iot-rocket.
- **IoT — retained messages** — `GetRetainedMessage` and `ListRetainedMessages` answered 501 `not yet implemented`; they now return the broker's retained messages (newest first, paged by `maxResults` and `nextToken`, with the user properties of an MQTT 5 publish), and a `PERSIST_STATE=1` restart keeps each message's `lastModifiedTime` and user properties. Contributed by @iot-rocket.
- **IoT — `ListJobExecutionsForJob`** — `GET /jobs/{jobId}/things` answered 400 `Unsupported IoT path`; it now lists every execution of the job, earlier ones of a rejoined thing included, most recently updated first, with the `status` filter and `maxResults` / `nextToken` paging. Contributed by @iot-rocket.
- **IoT — `DeleteJobExecution`** — `DELETE /things/{thingName}/jobs/{jobId}/executionNumber/{executionNumber}` answered 400 `Unsupported IoT path`; it now deletes the execution (an `IN_PROGRESS` one only with `force`), drops it from the job's lists and counts, completes the job when only finished executions are left, and publishes the `deleted` job execution event and the thing's `jobs/notify` topics; a `CONTINUOUS` job's thing left with no execution gets none back until it leaves the group, which leaves a `REMOVED` execution 1. Contributed by @iot-rocket.
- **Cognito — `InboundFederation` trigger on SAML and OIDC sign-in** — `LambdaConfig.InboundFederation` was stored but never invoked; SAML and OIDC federated sign-in now call the Lambda with `InboundFederation_ExternalProvider`, the provider-prefixed `userName` and the IdP's `samlResponse` or `idToken`. A non-empty `userAttributesToMap` replaces the IdP attributes before `AttributeMapping`, `{}` keeps them, and a failing or missing function fails the sign-in with `UserLambdaValidationException`. Contributed by @kjdev.

### Fixed
- **Step Functions — aws-sdk Parameters for JSON-protocol services take the wire names** — `ecs` and `ecr` received an aws-sdk task's PascalCase `Parameters` as sent, so their handlers ignored them (`describeClusters` with `Clusters` described `default`, `describeRepositories` with `RepositoryNames` returned every repository), and `sfn` and `cloudwatchlogs` lowercased every key, map keys included, so `tagResource` stored `Team-Name` as `team-Name`; the parameters are now renamed by the operation's input model, with map keys such as tag keys kept as written. Reported by @lucasmfraser.
- **Step Functions — aws-sdk results for JSON-protocol services keep the SDK's shape at every depth** — only the result's top-level keys took the SDK's names, so nested members kept their wire casing (`Executions[].executionArn`, `SecretList[].ARN`), timestamps stayed epoch numbers and absent members came back as `null`; the result is now shaped by the service's output model: structure members renamed at every depth (DynamoDB's `BOOL`, `NULL` and `SS` as `Bool`, `Nul` and `Ss`), map keys such as attribute names and tag keys kept as sent, timestamps as ISO 8601 with milliseconds, and absent members left out. A leading acronym is now lowered in every aws-sdk result, including two-letter names and ones with digits (`ID` as `Id`, `EC2InstanceId` as `Ec2InstanceId`, `MD5OfMessageBody` as `Md5OfMessageBody`). Contributed by @lucasmfraser.
- **S3 — `s3:prefix`, `s3:delimiter` and `s3:max-keys` under `AUTH=true`** — `ListObjects`, `ListObjectsV2` and `ListObjectVersions` set none of the three condition keys, so a `ListBucket` or `ListBucketVersions` grant scoped by them, such as `StringLike` on `s3:prefix` for one user's home folder, never matched and the listing answered `AccessDenied`; they now carry the request's `prefix`, `delimiter` and `max-keys`, path-style and virtual-hosted. Reported by @bettiah.
- **Idle memory** — the IoT mTLS listener built its TLS context at boot, which imported `cryptography` and minted the broker certificate before any device connected. It now binds port 8883 without TLS and builds the context on the first connection, and `x509_utils` imports `cryptography` on first use: an idle container drops from about 36.7 to 32.8 MiB.
- **Cognito — token lifetimes follow the app client** — access and ID tokens, `ExpiresIn` and the `/oauth2/token` `expires_in` were always one hour; they now follow the client's `AccessTokenValidity`, `IdTokenValidity` and `TokenValidityUnits` (one hour when unset, as on AWS), a client created without the two validities no longer reports `60` for them, and an expired token is refused by API Gateway's Cognito and JWT authorizers and, with `AUTH=true`, an expired access token by Cognito with `Access Token has expired`. Contributed by @iot-rocket.
- **Cognito — access tokens are verified under `AUTH=true`** — `GetUser` and the other calls that take an `AccessToken` accepted any JWT carrying a user's `sub`, including an ID token, a token with an altered payload and a token revoked by `GlobalSignOut`, `AdminUserGlobalSignOut` or `RevokeToken`; with `AUTH=true` they now answer `NotAuthorizedException` with `Invalid Access Token`, `Could not verify signature for Access Token` or `Access Token has been revoked`, as AWS does. Contributed by @iot-rocket.
- **IAM — user and role tag keys are case-insensitive** — `TagUser` and `TagRole` with `department` added a second tag beside `Department`, and `UntagUser` and `UntagRole` missed a key spelled differently; user and role tag keys now compare without case and keep the spelling last written, and `CreateUser`, `CreateRole`, `TagUser` and `TagRole` refuse keys that repeat without case with `InvalidInput` `Duplicate tag keys found. Please note that Tag keys are case insensitive.`, as does an `AWS::IAM::User` or `AWS::IAM::Role` whose stack tag differs from one of its own tags only in case. Other IAM resources keep case-sensitive keys. Contributed by @gakuto-cw21.
- **Signer — signing material checks in `PutSigningProfile`** — `PutSigningProfile` accepted any certificate ARN, or no `signingMaterial` at all, on every platform; the Lambda and Notation platforms now refuse any `signingMaterial` (`InvalidProfileParameters`), and with `AUTH=true` the IoT and FreeRTOS platforms refuse a profile without `signingMaterial` and a certificate ARN that is malformed, in another region or account, unknown to ACM or not issued, with AWS's errors. Contributed by @iot-rocket.
- **Step Functions — JSONata Fail and Wait fields are evaluated** — a JSONata Fail state raised its `Error` and `Cause` expressions as literal text, a JSONata Wait failed on a `Seconds` expression, ignored a `Timestamp` expression and dropped its `Output` and `Assign`, and JSONPath `ErrorPath`/`CausePath` were ignored; they are now evaluated, with AWS's `States.QueryEvaluationError` or `States.Runtime` for a result of the wrong type, a `Seconds` that is not a non-negative integer and an unparsable `Timestamp`. Contributed by @lucasmfraser.
- **S3 — presigned URLs keep AWS's time limits under `AUTH=true`** — an `X-Amz-Expires` over 604800 seconds or not a number, or a malformed `X-Amz-Date`, answers 400 `AuthorizationQueryParametersError`, and a URL dated more than 15 minutes ahead answers 403 `AccessDenied` `Request is not yet valid`, where they were served. Contributed by @iot-rocket.
- **Auth — credential failures in the service's protocol** — under `AUTH=true` an unknown, inactive or expired access key answered `UnrecognizedClientException` 403 from every service; it now gets the code, status and `Content-Type` AWS sends for the service's protocol, for example `InvalidClientTokenId` 403 from IAM and SQS, `UnrecognizedClientException` 400 from DynamoDB, `InvalidAccessKeyId` 403 from S3 and `AuthFailure` 401 from EC2. Contributed by @iot-rocket.
- **Step Functions — `ListExecutions` order and pagination** — executions came back oldest first, so `maxResults=1` returned the oldest, and `nextToken` was an offset; they now come back most recent first (running executions by `startDate`, the rest by `stopDate`), the token resumes after the last execution returned so one started between pages is not repeated, an unreadable token answers `InvalidToken` and `maxResults=0` uses the default, as on AWS. Contributed by @lucasmfraser.
- **Cognito — confirmation codes are checked under `AUTH=true`** — `ConfirmSignUp` and `ConfirmForgotPassword` accepted any code, and `ConfirmForgotPassword` changed the password even when no reset was requested; with `AUTH=true` they take only the issued code (`123456` for sign-up and `654321` for a password reset, as before), answer a wrong one with `CodeMismatchException` and a code that was never issued with `ExpiredCodeException`, and `ConfirmSignUp` refuses a user who is not `UNCONFIRMED` with `NotAuthorizedException`, as AWS does. Contributed by @iot-rocket.
- **Cognito — `ExplicitAuthFlows` is enforced for the password flows under `AUTH=true`** — with `AUTH=true` a client that lists its auth flows now refuses `USER_PASSWORD_AUTH`, `ADMIN_USER_PASSWORD_AUTH` and `ADMIN_NO_SRP_AUTH` unless the list enables them (the legacy `USER_PASSWORD_AUTH` and `ADMIN_NO_SRP_AUTH` names included), with the `InvalidParameterException` AWS returns. Contributed by @iot-rocket.
- **Cognito — `CUSTOM_AUTH` on clients without `ALLOW_` flows** — a client created without `ExplicitAuthFlows`, or with only the legacy names, refused `CUSTOM_AUTH`; it is now enabled, as on AWS, and a client whose `ALLOW_` list leaves it out answers `CUSTOM_AUTH is not enabled for the client.` as AWS does. Contributed by @iot-rocket.
- **CloudFormation — API Gateway v2 stage `Ref`** — `Ref` returned `<apiId>/<stageName>`, causing an `AWS::ApiGatewayV2::ApiMapping` using the reference as its `Stage` to fail with `Invalid stage identifier specified`; new stages now return the stage name. Stage updates, replacement on `ApiId` or `StageName` changes, and deletion use the resolved resource properties, including for stacks with legacy physical IDs. Contributed by @matuscongrady.
- **IoT — republish inside the rule's own topic filter** — `CreateTopicRule` and `ReplaceTopicRule` accepted a `republish` action whose topic the rule's `FROM` filter matches, so the rule fed on its own output; they now refuse it with AWS's `InvalidRequestException`, and an `AWS::IoT::TopicRule` with such an action fails its stack. Contributed by @iot-rocket.
- **IoT — deleting things, certificates, policies and thing types still in use** — `DeleteThing` detached a thing's certificates and deleted it, and `DeleteCertificate` refused only an `ACTIVE` certificate. Now `DeleteThing` refuses a thing with a certificate attached, `DeleteCertificate` refuses a certificate with things attached and, without `forceDelete`, one with policies attached (`forceDelete` detaches them), `DeletePolicy` refuses a policy with non-default versions and `DeleteThingType` one that things still use, all with AWS's messages. Contributed by @iot-rocket.
- **IoT — `RegisterCertificate` under an inactive CA** — a certificate issued by a CA that is not `ACTIVE` registered anyway; it is now refused with `CertificateStateException`, with or without `caCertificatePem`. An unregistered CA and a CA that did not issue the certificate are refused with AWS's codes and messages. Contributed by @iot-rocket.
- **IoT — rule SQL `principal()` and `traceid()`** — both resolved to Undefined; `principal()` now returns the client certificate id for mTLS, the caller's `GetCallerIdentity` `UserId` for HTTP Publish and SigV4 WebSocket sessions (also their lifecycle events' `principalIdentifier`) and the rule role's session for a republish, and `traceid()` returns a per-message id that the errorAction document carries as `cloudwatchTraceId`. Contributed by @iot-rocket.
- **CloudFormation — `AWS::Logs::LogGroup` delete** — a stack delete or a `LogGroupName` replacement removed only the group record, so a metric filter or resource policy added through the Logs API stayed and came back with a new group of the same name; the group is now deleted through `DeleteLogGroup`. Contributed by @iot-rocket.
- **Cognito Identity — `GetOpenIdToken` claims** — the token was a user-pool ID token with an empty audience; it is now issued by `https://cognito-identity.amazonaws.com` with the identity pool id as `aud`, the identity id as `sub`, an `amr` list naming the login providers and the user-pool sign-in, the pool ARN in `https://cognito-identity.amazonaws.com/identity-pool-arn` and a 10-minute lifetime. An identity id that no pool knows gets `ResourceNotFoundException` instead of a token. Contributed by @iot-rocket.
- **CloudTrail — new trails start with logging off** — `CreateTrail` left a new trail logging, and `GetTrailStatus` reported a `StartLoggingTime` an hour back and a `LatestDeliveryTime` of now. A new trail now reports `IsLogging` false until `StartLogging`, and `GetTrailStatus` returns only the `StartLoggingTime` and `StopLoggingTime` of calls made (a restart keeps the last stop time), without `LatestDeliveryTime` or empty error strings. Contributed by @iot-rocket.
- **CloudFormation — EventBridge rules accept a bus ARN** — `AWS::Events::Rule` stored an ARN-valued `EventBusName` as the bus name, so `PutEvents` succeeded without delivering to its targets. Create, update, delete and drift detection now resolve the ARN through the existing EventBridge parser and use the bus name internally. Contributed by @matuscongrady.
- **Cognito — SAML `/oauth2/authorize` with `MetadataFile`** — a SAML provider created with only `MetadataFile` was refused with `InvalidParameterException`; the IdP sign-in URL now comes from `IDPSSOEndpoint`, then the metadata's HTTP-Redirect `SingleSignOnService`, then `MetadataURL`. Contributed by @kjdev.

## [1.5.26] — 2026-10-11

### Fixed
- **Docker image — `ensurepip` is back in the slim image** — 1.5.24 removed `ensurepip` from the Alpine image, so `python -m venv` could not install pip in images built on top of MiniStack. Reported by @jin-gizmo.
- **ECS — `DescribeServices` on a missing cluster** — returned success with every service listed as `MISSING`; it now returns `ClusterNotFoundException`.
- **Startup memory without Docker** — the Docker Hub pull fallback imported the Docker SDK at boot even when no Docker socket was available. It now loads only when a socket is present.

## [1.5.25] — 2026-10-10

### Added
- **CloudFormation — `AWS::IAM::User`** — templates with an IAM user now provision it with `Path`, `Groups`, `ManagedPolicyArns`, inline `Policies`, `LoginProfile`, `PermissionsBoundary` and `Tags`; every property updates in place except `UserName`, which replaces the user. `Ref` returns the user name and `Fn::GetAtt` returns `Arn`. IAM gains `PutUserPermissionsBoundary` and `DeleteUserPermissionsBoundary`, and `CreateUser` and `GetUser` carry the boundary. Reported by @gakuto-cw21.
- **DocumentDB** — a new service for the `docdb` API: `CreateDBCluster`, `CreateDBInstance`, `DescribeDBClusters`, `DescribeDBInstances`, `ModifyDBCluster`, `ModifyDBInstance`, `RebootDBInstance`, `StartDBCluster`, `StopDBCluster`, `FailoverDBCluster`, `DeleteDBCluster`, `DeleteDBInstance` and the tag operations. Each cluster runs a `documentdb-local` container serving the MongoDB protocol over TLS. Defaults follow AWS: engine `8.0.0`, port `27017`, the `default.docdb8.0` parameter group and `docdb.amazonaws.com` endpoints. The `docdb` client reaches it on the normal endpoint, and an unfiltered `DescribeDBClusters` or `DescribeDBInstances` lists RDS and DocumentDB together, as on AWS. Subnet and parameter groups are shared with RDS. CloudFormation provisions `AWS::DocDB::DBCluster`, `DBInstance`, `DBSubnetGroup` and `DBClusterParameterGroup`, and `AWS::SecretsManager::SecretTargetAttachment` accepts DocumentDB targets. Contributed by @xPTM1219.
- **CloudFormation — `AWS::Logs::MetricFilter`** — templates with a metric filter failed with `Unrecognized resource types`; the filter now provisions, updates in place and is replaced on a `FilterName` or `LogGroupName` change. Contributed by @iot-rocket.
- **DynamoDB — vector search** — `CreateTable` accepts `VectorIndexes`, `UpdateTable` takes `VectorIndexUpdates` (resource allocation, then backfilling, then `ACTIVE`), and `DescribeTable` reports them. `SearchVectors` scores with `COSINE`, `EUCLIDEAN` or `DOT_PRODUCT` at 32-bit float precision and filters on the `SearchSchema`. Writes are checked against each vector index, `INDEXES` capacity reports `VectorWriteRequestBytes` on single, batch and transactional writes, and a PartiQL `SELECT` or `Scan` on a vector index returns "Scan operation not supported on this index type". Backups record the indexes in `SourceTableFeatureDetails`, `RestoreTableFromBackup` and `RestoreTableToPointInTime` restore them or apply `VectorIndexOverride`, and `ImportTable` creates them from `TableCreationParameters`. Reported by @hicksy and the paritysuite.org project.
- **S3 Tables — bucket and table configuration** — table bucket encryption, maintenance, policy, storage class and metrics; table encryption, maintenance, maintenance job status, policy and storage class; `RenameTable`; and `TagResource`, `UntagResource` and `ListTagsForResource`. Unset configurations return the documented defaults: SSE-S3, `STANDARD`, unreferenced file removal after 3 and 10 days, compaction at 512 MB with the `auto` strategy, and snapshot management keeping 1 snapshot for 120 hours.
- **IoT — job and job execution events** — completed, canceled and deleted jobs and finished, removed or deleted executions publish to `$aws/events/job/...` and `$aws/events/jobExecution/...` when `UpdateEventConfigurations` enables them. Contributed by @iot-rocket.
- **IoT — `UpdateJob`** — `PATCH /jobs/{jobId}` updates an `IN_PROGRESS` job's description, presigned URL, rollout, abort, timeout and retry configuration with AWS's checks; running executions keep their timeout. `CreateJob` refuses a bad timeout with the same message. Contributed by @iot-rocket.
- **Signer — real signatures on the IoT platform** — when ACM holds the key of the profile's certificate, `StartSigningJob` on `AWSIoTDeviceManagement-SHA256-ECDSA` writes AWS's signed document instead of a JSON receipt, and an RSA key fails the job. Without a key the receipt stays and a warning is logged. Contributed by @iot-rocket.

### Fixed
- **ELBv2 — `http-header` rule conditions keep their `HttpHeaderConfig`** — the header name and values were dropped when a rule was created, modified or provisioned through CloudFormation, so `DescribeRules` returned an empty condition and requests never matched. Matching follows AWS: the header name and values compare case-insensitively, `*` and `?` are the only wildcards, and a request without the header does not match. Reported by @kinoh.
- **Firehose — dynamic partitioning and Parquet conversion on S3 delivery** — `PutRecord`, `PutRecordBatch` and Kinesis-sourced records ignored `DynamicPartitioningConfiguration` and `DataFormatConversionConfiguration`. The `Prefix` and `ErrorOutputPrefix` now evaluate `!{timestamp:...}`, `!{firehose:random-string}`, `!{firehose:error-output-type}`, `!{partitionKeyFromQuery:...}` (JQ `MetadataExtraction`) and `!{partitionKeyFromLambda:...}` (Lambda `metadata.partitionKeys`), with `yyyy/MM/dd/HH/` appended when the prefix has no timestamp. Records convert to Parquet against the Glue table schema with the configured compression and land as `<stream>-<version>-<timestamp>-<uuid>.parquet` or the `FileExtension`. Failed records go to the error prefix as AWS's error document under `processing-failed` or `format-conversion-failed`. Parquet output needs DuckDB (the full image); ORC is not supported. Reported by @mintel-hgli.
- **CloudWatch Logs — `DescribeMetricFilters` by metric** — `metricName` and `metricNamespace` now select the filters that publish that metric, `filterNamePrefix` applies only with `logGroupName`, and `DeleteLogGroup` removes the group's filters. Contributed by @iot-rocket.
- **Cognito — `Username` is the `sub` in pools with `UsernameAttributes`** — `AdminCreateUser` and `SignUp` now use the `sub` as the `Username`, keep the email or phone number as an alias, and refuse a `Username` that is not a sign-in attribute, a differing email or phone attribute and a caller-supplied `sub`. Contributed by @iot-rocket.
- **Athena — S3 Tables, Glue table data and result shapes** — queries reach S3 Tables as `"s3tablescatalog/<bucket>".<namespace>.<table>` or through `QueryExecutionContext.Catalog`. Glue tables read every object under their `LOCATION` from the S3 store without `S3_PERSIST`, skipping files and folders that start with `_` or `.`; CSV columns map to the Glue schema by position with the SerDe delimiter and `skip.header.line.count`, and Avro tables are readable. `StartQueryExecution` without a result location returns `InvalidRequestException`, and `GetQueryResults` returns `NULL` as a `Datum` without `VarCharValue`.
- **Athena — partitioned Glue tables and qualified columns** — partition columns take their Glue types instead of the types DuckDB inferred from the path, and `db.table.column` references resolve. Contributed by @mayuriphad. Reported by @mintel-hgli.
- **Error responses use each protocol's `Content-Type`** — `application/x-amz-json-1.1` or `1.0` for JSON services by their `jsonVersion`, `text/xml` for query services (`text/xml;charset=UTF-8` for EC2), `application/json` for rest-json except SESv2 (`application/x-amz-json-1.1`), and `text/xml` for rest-xml except S3 (`application/xml`). Errors are normalized at dispatch from the botocore service model. Contributed by @pingedbrain.
- **Unimplemented services no longer answered by S3** — a request signed for a service MiniStack does not implement (for example Glacier or Pinpoint) fell through to S3 and returned `NoSuchBucket`; it now returns `Unsupported service: <name>`. Contributed by @pingedbrain.
- **CloudFormation — error namespace** — `ErrorResponse` uses `http://cloudformation.amazonaws.com/doc/2010-05-15/`, the botocore API version, instead of `2010-05-08`. Contributed by @pingedbrain.
- **EC2 — `DescribeInstances` checks the ID format first** — a malformed `InstanceId` returns `InvalidInstanceID.Malformed`; a well-formed unknown ID still returns `InvalidInstanceID.NotFound`. Contributed by @pingedbrain.
- **ELBv2 — `DescribeLoadBalancers` checks name length first** — a `Names` entry over 32 characters returns `ValidationError`. Contributed by @pingedbrain.
- **WAFv2 — `GetWebACL` checks the `Id` first** — an `Id` that is not a UUID or is longer than 36 characters returns `ValidationException` listing the failed constraints; a well-formed unknown ID still returns `WAFNonexistentItemException`. Contributed by @pingedbrain.
- **Docker images fall back to public mirrors** — when a Docker Hub pull fails, MiniStack retries from ECR Public and then `mirror.gcr.io` and tags the image with the requested name, so a Docker Hub rate limit no longer stops RDS, ElastiCache, ECS and the other container-backed services.
- **Step Functions — optimized DynamoDB error names** — conditional failures and other service errors now use the `DynamoDB.` prefix so exact `Catch` and `Retry` handlers match AWS. Contributed by @jayjanssen.
- **Step Functions — Lambda `GetFunction` SDK integration** — workflows can read function configuration, code metadata and tags through `aws-sdk:lambda:getFunction`, including qualified reads. Contributed by @jayjanssen.
- **Secrets Manager — force delete of a secret scheduled for deletion** — `DeleteSecret` with `ForceDeleteWithoutRecovery` returned `InvalidRequestException` for a secret already scheduled for deletion. It now deletes the secret permanently. Contributed by @fabio-andre-rodrigues.
- **DynamoDB — `RestoreTableToPointInTime` keeps secondary indexes** — the restored table now carries the source table's GSIs and LSIs, or `GlobalSecondaryIndexOverride` / `LocalSecondaryIndexOverride` when given.
- **DynamoDB — item size limits per operation** — numbers are sized as base-100 digit pairs, so the 400 KB limit is exact to the byte. `UpdateItem` counts only the attributes it writes plus a cost per clause and answers "Item size to update has exceeded the maximum allowed size". PartiQL `INSERT` and `UPDATE` now enforce the limit, and an oversized transacted `Update` cancels the transaction. Reported by @hicksy and the paritysuite.org project.
- **DynamoDB — nesting depth and key length** — values nested beyond 32 levels return "Nesting Levels have exceeded supported limits: Attributes in the item have nested levels beyond supported limit", including in `UpdateItem` and `TransactWriteItems` expression values. Over-long keys are refused on reads too. Reported by @hicksy and the paritysuite.org project.
- **DynamoDB — validation messages** — overlapping `ProjectionExpression` paths are refused on `GetItem`, `Query`, `Scan` and `BatchGetItem`. Empty `RequestItems`, `Select`, `Limit`, `Segment`, `ExclusiveStartKey`, `CreateTable`, PartiQL and transaction errors use AWS's wording, and `UpdateItem` reports only the first invalid enum. Reported by @hicksy and the paritysuite.org project.

## [1.5.24] — 2026-10-08

### Added
- **API Gateway v1 — richer SQS request templates** — templates now support `#set`, `##` and `#* *#` comments, quiet `$!` references, double-quoted strings with interpolation and `""` escapes, and `$context.authorizer` values including Cognito claims. Reported by @teruyukisuda.

### Fixed
- **API Gateway — `CreateApiKey` keeps `value` and `customerId`** — the key always got a generated value. A value under 20 characters returns `BadRequestException` "API Key value should be at least 20 characters". Reported by @mjuen.
- **CloudWatch — CBOR timestamps** — timestamps from clients using the CBOR protocol are now read as UTC. Reported by @DimQ1.
- **Container reaping — failed removals are logged** — containers the reaper could not remove are logged at `WARNING` with their ids. Reported by @iot-rocket.
- **DynamoDB Streams — iterator expiry and `GetRecords` limits** — shard iterators expire after 15 minutes with `ExpiredIteratorException`, a page stops at 1 MB, a position behind the trim horizon returns `TrimmedDataAccessException`, and an unknown shard returns `ResourceNotFoundException`. Reported by @DimQ1.
- **Lambda — a returned string is JSON-encoded** — `Invoke` returned `hello` instead of `"hello"`. Reported by @edu-shippo.
- **Lambda — local Node.js console logs** — the one-shot local executor mixed stdout into the `Invoke` response. Logs now go to the invocation log and stdout carries only the return value. Contributed by @gakuto-cw21.
- **Pipes — SNS FIFO targets are refused** — `CreatePipe` and `UpdatePipe` now return "SNS FIFO topics are not supported as a pipe target.". Reported by @DimQ1.
- **S3 Tables — Iceberg REST catalogs and commits** — every catalog read the first catalog's warehouse; each is now scoped to its table bucket ARN. Commits now check update requirements (`CommitFailedException`, 409) and keep snapshot sequence numbers consistent.
- **SES — non-ASCII addresses** — an address with non-ASCII characters returns `InvalidParameterValue` "Missing final '@domain'". Reported by @DimQ1.
- **SNS — filter policies** — `FilterPolicyScope` `MessageBody` was ignored and `suffix` and `equals-ignore-case` never matched. Both scopes now apply, with `wildcard`, `cidr` and `"exists": false` too. Reported by @DimQ1.
- **SQS — retention and dead-letter moves** — messages now expire after `MessageRetentionPeriod`. A message moved to a dead-letter queue keeps its enqueue time on standard queues and carries `DeadLetterQueueSourceArn`. Reported by @DimQ1.
- **Step Functions — a Lambda that returns `null` outputs `null`** — the Task output was `{}`. Reported by @edu-shippo.

## [1.5.23] — 2026-10-07

### Added
- **AppConfig — gradual rollout and alarm rollback** — deployments completed as soon as they started. They now move through `DEPLOYING`, `BAKING` and `COMPLETE` on the strategy's schedule, a CloudWatch alarm in the environment's `Monitors` that goes to `ALARM` rolls the deployment back, and a concurrent or stale deployment returns `ConflictException`. Contributed by @Jolley71717.
- **API Gateway v1 — `AWS` integrations targeting SQS** — a non-proxy integration whose URI is `sqs:path/{account}/{queue}` now renders its `requestTemplates`, sends the message to the queue and maps the reply through `integrationResponses`; other `AWS` service URIs still take the Lambda path. Contributed by @pingedbrain. Reported by @teruyukisuda.
- **CloudFormation — IoT things, certificates and attachments** — `AWS::IoT::Thing`, `AWS::IoT::Certificate`, `AWS::IoT::ThingPrincipalAttachment` and `AWS::IoT::PolicyPrincipalAttachment` failed with `Unrecognized resource types`; they now provision onto the IoT registry with AWS's `Ref` and `Fn::GetAtt` values. `CreateCertificateFromCsr` was added, and `DescribeCertificate` now reports `certificateMode`. Contributed by @iot-rocket.
- **CloudFormation — enum properties are validated before provisioning** — a value outside a resource schema's enum was refused only by that resource's create, after earlier resources existed; it now fails the stack with `VALIDATION_FAILED` before anything is provisioned. Contributed by @iot-rocket.
- **CloudFront — custom error responses** — `CustomErrorResponses` were stored but never applied; when the origin answers a configured `ErrorCode`, the data plane now returns `ResponsePagePath` with `ResponseCode`. Contributed by @skialpine.
- **Cognito — `CustomMessage_AdminCreateUser` trigger** — `AdminCreateUser` now invokes the pool's `CustomMessage` Lambda before sending the invitation, and `{username}` expands to the email or phone number in pools with `UsernameAttributes`. Contributed by @kjdev.
- **DynamoDB — multi-attribute GSI keys** — a GSI with several `HASH` or `RANGE` attributes failed every `Query` with "Query condition missed key schema element". `Query` now needs equality on every partition-key attribute and takes sort-key attributes left to right, only the last with a range condition. Reported by @jun-ut.
- **IoT — domain configurations** — `CreateDomainConfiguration`, `DescribeDomainConfiguration`, `ListDomainConfigurations`, `UpdateDomainConfiguration`, `DeleteDomainConfiguration` and `AWS::IoT::DomainConfiguration`. Contributed by @iot-rocket.
- **IoT — job templates** — `CreateJobTemplate`, `DescribeJobTemplate`, `ListJobTemplates`, `DeleteJobTemplate`, `CreateJob` with `jobTemplateArn`, and `AWS::IoT::JobTemplate`. Contributed by @iot-rocket.
- **SES — identity notifications** — `SetIdentityNotificationTopic` stored topics that nothing published to; v1 sends now publish a `Delivery`, `Bounce` or `Complaint` notification per recipient, following the mailbox simulator. Contributed by @kjdev.
- **SES — receipt rule sets and rules** — rule sets can be created, activated, described, listed and deleted, and rules created, described and deleted; they are stored only, since inbound mail is not emulated. Contributed by @AdrianAcala. Reported by @wparad.
- **SES v2 — tenants and resource associations** — `CreateTenant`, `GetTenant`, `ListTenants`, `DeleteTenant`, `PutTenantSuppressionAttributes` and the tenant resource association operations; a resource with associations cannot be deleted. Contributed by @AdrianAcala. Reported by @wparad.
- **SES v2 — dedicated IP pools** — `CreateDedicatedIpPool`, `GetDedicatedIpPool`, `ListDedicatedIpPools` and `DeleteDedicatedIpPool`, with tags on the pool ARN. Reported by @wparad.
- **Signer — CloudFormation profiles, profile permissions and `CancelSigningProfile`** — `AWS::Signer::SigningProfile` and `AWS::Signer::ProfilePermission` provision, and `AddProfilePermission`, `ListProfilePermissions`, `RemoveProfilePermission` and `CancelSigningProfile` are routed. Contributed by @iot-rocket.

### Fixed
- **API Gateway — management IAM actions** — with `AUTH=true`, management requests were checked against SDK operation names; they now use `apigateway:GET`, `POST`, `PUT`, `PATCH` and `DELETE` on the requested resource path, so scoped grants and stage denies apply. Contributed by @AdrianAcala.
- **AppConfig — `GetLatestConfiguration` and `StopDeployment`** — a poll returned the full configuration every time, and `StopDeployment` marked any deployment `ROLLED_BACK`. An unchanged poll now returns an empty body, and `StopDeployment` follows the documented states. Contributed by @Jolley71717.
- **CloudWatch — `Values`/`Counts` and `StatisticValues`** — values were stored once per count and statistic sets as their average, so `SampleCount`, `Sum`, `Minimum` and `Maximum` were wrong. They now aggregate weighted, and percentiles are omitted for negative values and for statistic sets that are not one repeated value. Contributed by @DimQ1.
- **CloudWatch Logs — IAM tag conditions** — with `AUTH=true`, authorization now includes existing and requested tags, and ARN-based tagging APIs authorize the supplied ARN instead of `*`. Contributed by @AdrianAcala.
- **Cognito — `RESET_REQUIRED` users cannot sign in with a password** — after `AdminResetUserPassword`, password and SRP sign-in with the old password succeeded; they now return `PasswordResetRequiredException`. Contributed by @prandogabriel.
- **Container reaping — one unreadable container no longer stops the sweep** — if one container's inspect failed, the boot sweep removed no leftover containers; containers are now listed without inspecting each one. Reported by @iot-rocket.
- **DynamoDB — Query and Scan stop each page at 1 MB** — without `Limit`, every matching item came back in one response with no `LastEvaluatedKey`; a page now stops at 1 MB of data read and returns `LastEvaluatedKey`. Contributed by @DimQ1.
- **DynamoDB — index Query and writes no longer slow down as a table grows** — a GSI or LSI `Query` read every item in the table and every write recounted it; index partitions and `ItemCount` are now kept per write. Contributed by @DimQ1.
- **DynamoDB — `ClientRequestToken` expires after 10 minutes** — transaction tokens were kept forever; after 10 minutes a reused token is a new request. Contributed by @DimQ1.
- **DynamoDB — transactions are authorized per item** — with `AUTH=true`, `TransactWriteItems` and `TransactGetItems` were checked as non-existent IAM actions on the first table; each item is now checked as `PutItem`, `UpdateItem`, `DeleteItem`, `ConditionCheckItem` or `GetItem` on its own table. Contributed by @drakeo338. Reported by @Zordrak.
- **DynamoDB Streams — no-op writes and closed streams** — a write that changed nothing produced a `MODIFY` record, and a disabled or deleted stream vanished. No record is written now, and a closed stream stays readable for 24 hours as `DISABLED`. Contributed by @DimQ1.
- **IoT — the gateway certificate names the `DescribeEndpoint` hosts** — `https://<endpointAddress>` failed hostname verification under `USE_SSL=1`; the certificate now carries the IoT data and credentials hosts. Contributed by @iot-rocket.
- **Kinesis — `SubscribeToShard` keeps its position across retention pruning** — removing expired records could skip newly appended ones. Contributed by @AdrianAcala.
- **Lambda — one request ID per Docker invocation** — RIE logs carried a second START/END/REPORT set under a different RequestId; MiniStack's ID now reaches RIE and the logs are saved once. Contributed by @gakuto-cw21.
- **Lambda — `invoked_function_arn` keeps the alias or version invoked.** Contributed by @jayjanssen.
- **Lambda — event source mappings pick up new work at once** — a new SQS message, Kinesis record or DynamoDB stream record waited for the poller's idle tick; it now wakes the poller. Contributed by @antonie-popovic.
- **RDS — global cluster tags** — `CreateGlobalCluster` dropped `Tags`; they are now stored and returned in `TagList`. Contributed by @jayjanssen.
- **RDS Data API — MySQL `generatedFields`** — it was always empty; it now holds the `AUTO_INCREMENT` id. Contributed by @skialpine.
- **S3 — cross-account replication** — the destination bucket was looked up only in the caller's account, so every replica `FAILED`; it is now found in its owner's account, and its bucket policy must grant the replication role. Contributed by @pingedbrain.
- **S3 — re-creating a bucket you own** — outside us-east-1 it returned 200; it now returns `BucketAlreadyOwnedByYou` (409). Contributed by @DimQ1.
- **S3 — truncated uploads** — a body shorter than `Content-Length` or a malformed `aws-chunked` upload was stored as a partial object; it now returns `IncompleteBody` (400). Contributed by @DimQ1.
- **SES — `SendEmail` validation** — an address without a domain returns `InvalidParameterValue` "Missing final '@domain'", and an unknown configuration set returns `ConfigurationSetDoesNotExist`. Contributed by @DimQ1.
- **SES — `SendRawEmail` relay sends one `Message-ID`** — a client `Message-ID` was kept next to MiniStack's. Contributed by @kjdev.
- **SNS — published messages are no longer retained** — every publish was kept on its topic and persisted. Contributed by @DimQ1.
- **SQS — FIFO delays and `MaxNumberOfMessages`** — FIFO queues ignored `DelaySeconds` and accepted per-message delays, and `MaxNumberOfMessages` was clamped. A FIFO message now waits the queue delay, a nonzero per-message delay returns `InvalidParameterValue`, and `MaxNumberOfMessages` outside 1–10 returns `InvalidParameterValue`. Contributed by @DimQ1.
- **SQS — queue `DelaySeconds` applies to messages from other services** — deliveries from SNS, EventBridge, S3 notifications and Lambda were visible at once. Contributed by @DimQ1.
- **SSM — `PutParameter` validation** — a parameter without `Type`, a name starting with `aws` or `ssm`, and `Tags` with `Overwrite` were accepted; they are now refused, and an overwrite without `Type` keeps the stored type. Contributed by @fabio-andre-rodrigues. Reported by @jin-gizmo.
- **Step Functions — `$$.State.RetryCount`** — it resolved to `null`; it now counts a state's retries, and a Task's `Parameters` are evaluated for each attempt. Contributed by @skialpine.
- **Step Functions — the first matching retrier decides** — a retrier that had used its `MaxAttempts` was skipped, so a later `States.ALL` retried anyway. Contributed by @skialpine.
- **Step Functions — `StopExecution` keeps its `error` and `cause`** — `DescribeExecution` returned neither; it now returns both. Contributed by @skialpine.
- **Step Functions — a retried `StopExecution` succeeds** — stopping an already-aborted execution returned `ValidationException`; it now returns the original `stopDate`. Contributed by @jayjanssen.
- **TLS — the generated certificate works with rustls** — it was a CA certificate, which rustls refuses as a server's; it is now a server certificate. Contributed by @antonie-popovic.

## [1.5.22] — 2026-10-05

### Added

- **Bedrock AgentCore — Memory data plane** — the `bedrock-agentcore` data-plane endpoint now serves the short-term event log (`CreateEvent`, `GetEvent`, `ListEvents`, `DeleteEvent`, `ListActors`, `ListSessions`), the long-term record store (`BatchCreateMemoryRecords`, `BatchUpdateMemoryRecords`, `BatchDeleteMemoryRecords`, `GetMemoryRecord`, `ListMemoryRecords`, `DeleteMemoryRecord`) and `RetrieveMemoryRecords` (token-overlap scoring, no embeddings); extraction does not run so `StartMemoryExtractionJob` returns `ResourceNotFoundException`; a cross-account memory ARN is refused `AccessDeniedException` under `AUTH=true`. Shapes verified against botocore `bedrock-agentcore`. Contributed by @pingedbrain.
- **AWS Budgets** — `CreateBudget`, `DescribeBudget`, `DescribeBudgets`, `UpdateBudget`, `DeleteBudget`, `CreateNotification`, `UpdateNotification`, `DeleteNotification`, `DescribeNotificationsForBudget`, `CreateSubscriber`, `UpdateSubscriber`, `DeleteSubscriber`, `DescribeSubscribersForNotification`, `TagResource`, `UntagResource` and `ListTagsForResource`. Account-scoped JSON 1.1 service; omitted `TimePeriod.Start` defaults to the period start, omitted `End` to `06/15/87 00:00 UTC` and omitted `ThresholdType` to `PERCENTAGE`; the 10-notification/budget and 11-subscriber/notification limits are enforced with `CreationLimitExceededException`; deleting the last subscriber of a notification deletes it. Budget Actions and `DescribeBudgetPerformanceHistory` are out of scope. Contributed by @skialpine.
- **OpenSearch Serverless — collections, security and access policies, and a proxied data plane** — `CreateCollection`, `BatchGetCollection`, `ListCollections`, `UpdateCollection`, `DeleteCollection`; `Create`/`Get`/`Update`/`Delete`/`ListSecurityPolicy` (`encryption`, `network`); `Create`/`Get`/`Update`/`Delete`/`ListAccessPolicy` (`data`); `GetPoliciesStats`; tag operations; enough for Terraform's `aws_opensearchserverless_collection`, `_security_policy` and `_access_policy` to apply and destroy; a collection requires a matching encryption policy (exact name or `prefix*`), an update must name the current `policyVersion`, a data policy refuses empty or wildcard principals, `TIMESERIES` collections refuse custom ids and updates, and with `OPENSEARCH_DATAPLANE=1` each collection is backed by an OpenSearch container on a named Docker volume, otherwise 503; data access and network policies are stored but not enforced. Contributed by @skialpine.
- **CloudFormation — `AWS::RDS::DBSubnetGroup`, `AWS::RDS::DBParameterGroup`, `AWS::RDS::DBClusterParameterGroup` and `AWS::SecretsManager::SecretTargetAttachment`** — these CDK `rds.DatabaseInstance`/`rds.DatabaseCluster` types were refused as `Unrecognized resource types`; they now create, update and delete via the RDS and Secrets Manager APIs. `Ref` returns the group name or the secret ARN; `Fn::GetAtt` gives `DBSubnetGroupArn`, `DBParameterGroupName` and `DBParameterGroupArn`; a parameter requiring a reboot goes `pending-reboot`; the attachment writes `engine`, `host`, `port` and `dbname` into the secret JSON and removes them on delete. Reported by @fabio-andre-rodrigues.
- **Aurora DSQL — partial indexes** — `CREATE INDEX ASYNC ... WHERE predicate` was refused `0A000`; the predicate now reaches the backend (unique partial indexes scope uniqueness to covered rows). A volatile-function predicate fails `42P17` and a subquery fails `0A000` at submit time, in the order the live service checks (eu-central-1, 2026-10-04). Contributed by @vivedo.
- **Aurora DSQL — `dsql.enable_batched_nestloop` and the reserved `dsql.` prefix** — the setting shows `on` by default, `RESET` returns to `on`, and a non-Boolean value or value list is refused `22023`; any other `dsql.*` name is refused `42602` "reserved prefix"; `work_mem`, `statement_timeout` and `default_statistics_target` can no longer be set (`0A000`), as on the live service. Plans remain Postgres-shaped. Contributed by @vivedo.
- **Aurora DSQL — extended statistics limits** — a sixth `CREATE STATISTICS` on a table fails `54000` "more than 5 extended statistics per table are not allowed"; targets above 100 are refused `22023`; `ALTER TABLE ... ALTER COLUMN ... SET STATISTICS` is refused `0A000`, as on the live service. Contributed by @vivedo.
- **IoT — `ListCertificatesByCA`** — `GET /certificates-by-ca/{caCertificateId}` answered `Unsupported IoT path`; it now lists device certificates under the CA in every status, newest first (or ascending with `isAscendingOrder=true`), with `pageSize` (1–250) and `nextMarker` pagination; an unknown CA returns an empty list; invalid page sizes, ids or markers return `InvalidRequestException`. Contributed by @iot-rocket.
- **IoT — lifecycle events** — the broker publishes `$aws/events/presence/connected|disconnected/{clientId}` and `$aws/events/subscriptions/subscribed|unsubscribed/{clientId}` with the AWS payload (`sessionIdentifier`, `principalIdentifier`, `versionNumber`, `disconnectReason`, `clientInitiatedDisconnect`); rules or subscriptions with a `#`/`+` prefix filter do not receive `$aws/events` messages, and client ids containing `#` or `+` get none. Contributed by @iot-rocket.
- **Cognito — `SetUICustomization` and `GetUICustomization`** — store and return the hosted UI CSS per user pool or per app client (client falls back to pool default); the pool needs a domain first, as on AWS. `ImageFile` is accepted but not stored. Supports Terraform's `aws_cognito_user_pool_ui_customization`. Contributed by @antonie-popovic.

### Fixed

- **Cognito — app client secret hashes** — `GenerateSecret=true` clients now require a valid `SecretHash` on signup, confirmation, password recovery and auth; missing/wrong hashes return `NotAuthorizedException` before any user change. Clients without a secret reject a hash on self-service and initial auth; refresh/challenge ignore it. Refresh auth verifies the token owner and custom challenges cannot be answered through a different client. Applies under `AUTH=true`. Contributed by @AdrianAcala.
- **Cognito — `/oauth2/token` refreshes tokens issued by the API** — the `refresh_token` grant answered `invalid_grant` for tokens from `InitiateAuth`, `AdminInitiateAuth`, `RespondToAuthChallenge` or federated sign-in; it now validates them as `REFRESH_TOKEN_AUTH` does, refusing revoked, signed-out or wrong-client tokens with `invalid_grant` and returning new ID and access tokens. A missing or wrong client secret or unknown client returns `invalid_client`. Contributed by @Pintouch.
- **Bedrock — foundation model agreement offers** — `ListFoundationModelAgreementOffers` returned an empty list, so there was no `offerToken` for `CreateFoundationModelAgreement`; it now returns one offer for third-party models (Amazon models have none), `CreateFoundationModelAgreement` requires `offerToken` and returns `ResourceNotFoundException` for an unknown model, and `GetFoundationModelAvailability` reports `AVAILABLE` only while an agreement exists, so the Terraform `aws_bedrock_foundation_model_agreement` resource can be destroyed. Reported by @wparad.
- **SES — an unreachable SMTP relay no longer blocks the emulator** — with `SMTP_HOST` set, a relay that drops packets held every service request for the OS connect timeout (~2 min). The relay now runs in the background with a 10-second timeout after SES returns the `MessageId`; Cognito email delivery uses the same relay. Reported by @bawdo.
- **RDS — MySQL instances apply their DB parameter group** — parameters were stored but never reached the server; a MySQL or MariaDB instance now starts with its group's parameters as server options. `ModifyDBParameterGroup` with `ApplyMethod=immediate` runs `SET GLOBAL` on running instances; `ResetDBParameterGroup` restores engine defaults; a static parameter with `immediate` is refused `InvalidParameterCombination`; a `pending-reboot` change shows `ParameterApplyStatus: pending-reboot` until next start. Names, values and formula values are not validated. Contributed by @skialpine.
- **RDS — creating an existing parameter group is refused** — `CreateDBParameterGroup` and `CreateDBClusterParameterGroup` with a name already in use replaced the group with an empty one, dropping its parameters; they now return `DBParameterGroupAlreadyExists`, as AWS does. Contributed by @skialpine.
- **RDS — a backup retention period of 0 turns binary logging off** — on RDS for MySQL, `BackupRetentionPeriod=0` turns binary logging off; MiniStack's MySQL 8.0/8.4 instances kept the image default, binary logging on. Such an instance now starts with binary logging off. Contributed by @skialpine.
- **RDS — `ModifyDBInstance` settings that are not pending modifications apply immediately** — without `ApplyImmediately`, `DeletionProtection`, `CopyTagsToSnapshot`, `PreferredBackupWindow`, `PreferredMaintenanceWindow`, `PubliclyAccessible`, `MaxAllocatedStorage`, `MonitoringInterval` and `MonitoringRoleArn` were queued in `PendingModifiedValues`, which has no such members, and never applied. They now apply at once on every instance. Contributed by @AdrianAcala.
- **RDS — the MySQL master user gets the privileges AWS gives it** — the master user was granted `ALL PRIVILEGES` including `SUPER` and `SYSTEM_VARIABLES_ADMIN`, so `SET GLOBAL`/`SET PERSIST` worked where RDS denies them; an instance started in the background got no global grant at all. Every start path now grants the RDS/Aurora master user privileges for the engine and version `WITH GRANT OPTION`. Contributed by @skialpine.
- **API Gateway — `GetApiKeys` filters by `nameQuery`** — `GetApiKeys` ignored `nameQuery` and returned all the API keys, so a lookup by name could get the wrong key. It now returns only the keys whose names start with `nameQuery`. Contributed by @mishukdutta-cz.
- **API Gateway v2 — JWT claims reach a Lambda as strings** — `requestContext.authorizer.jwt.claims` holds every claim as a string, as on AWS: numbers as digits, booleans as `true`/`false` and arrays as their items in brackets, e.g. `[admin dev]`. Typed event models (for example `aws_lambda_events` in Rust, or Go's `map[string]string`) parse the event. Contributed by @antonie-popovic.
- **API Gateway v2 (WebSocket API) — `$connect` with `AWS_IAM`** — an unsigned handshake is refused with 403 `Missing Authentication Token`, and under `AUTH=true` the caller needs `execute-api:Invoke` on `arn:aws:execute-api:<region>:<account>:<api-id>/<stage>/$connect`. Contributed by @iot-rocket.
- **ALB — repeated HTTP target response headers** — forwards every `Set-Cookie` as a separate header, preserving cookie attributes and repeated fields even when their names use different casing; previously only the first value survived. Contributed by @AdrianAcala. Reported by @bawdo.
- **IoT — `CreateThingGroup` on an existing name returns the group when nothing differs** — a repeated `CreateThingGroup` always failed with `ResourceAlreadyExistsException`. It now returns the existing group's name, ARN and id when the description, attributes (in any order), parent and tags match, and answers 409 with AWS's message otherwise, leaving the group unchanged, as AWS does. An `AWS::IoT::ThingGroup` that names an existing group still fails its stack, with or without matching properties, now with AWS's name-conflict message. Contributed by @iot-rocket.
- **IoT — just-in-time registration under a CA in `SNI_ONLY` mode** — a device certificate signed by a CA registered with `certificateMode` `SNI_ONLY` and auto-registration enabled was refused on its first mTLS connect, so only `DEFAULT` CAs auto-registered. Such a CA now registers the certificate `PENDING_ACTIVATION` and publishes the registered event when the device's TLS ClientHello carries a server name, and does nothing without one. Under either mode, an endpoint name with another account's prefix auto-registers nothing. Contributed by @iot-rocket.
- **IoT — `RegisterCertificate` links the CA that signed the certificate** — a certificate registered without `caCertificatePem` carried no `caCertificateId`, even when a registered ACTIVE CA had signed it, so `DescribeCertificate` named no CA and `ListCertificatesByCA` left it out. It is now linked to that CA, as AWS does; when several registered CAs share a subject, the signature decides. Contributed by @iot-rocket.
- **IoT — a CONTINUOUS job runs again for a thing that rejoins its target group** — executions were one per thing/job, so a thing that finished, left and rejoined a group never got another execution. Rejoining now queues the next execution number; leaving moves a QUEUED execution to `REMOVED`; both publish `jobs/notify(-next)`; `notify-next` omits `thingName` and empty `statusDetails`; and `ListJobExecutionsForThing`, `DescribeJobExecution` and `jobProcessDetails` include earlier executions. Contributed by @iot-rocket.
- **IoT — a `#` region no longer widens topic filters** — a WebSocket session whose credential names `#` as its region received every message published under that region, whatever its filter, because filters were matched with the account and region prefix in front of them. Contributed by @iot-rocket.
- **Lambda — a timed-out invocation reports `Sandbox.Timedout`** — functions past their `Timeout` returned `Runtime.ExitError`, so Step Functions `Retry`/`Catch` on `Sandbox.Timedout` never matched; all executors now return `Sandbox.Timedout` with `Task timed out after N.00 seconds`; a runtime that exits before responding still reports `Runtime.ExitError`. Contributed by @mishukdutta-cz.
- **Lambda — throttle `retryAfterSeconds` is a string** — a `TooManyRequestsException` returns `retryAfterSeconds` as a string, as the Lambda API model declares, so SDK clients such as the AWS SDK for Rust parse the response as a throttle. Contributed by @antonie-popovic.
- **Step Functions — `TimeoutSeconds` applies to Lambda and service integration tasks** — only `.waitForTaskToken` tasks respected `TimeoutSeconds`; Lambda and service integration tasks now fail `States.Timeout` after `TimeoutSeconds`; activity tasks and `TimeoutSecondsPath` unchanged. Contributed by @mishukdutta-cz.
- **Step Functions — `States.TaskFailed` no longer matches `States.Timeout`** — `States.TaskFailed` matched timeouts, so retries/catches ran where AWS doesn't; it now matches every error except `States.Timeout`. Contributed by @mishukdutta-cz.
- **AppConfig — predefined deployment strategies** — `AppConfig.AllAtOnce`, `AppConfig.Linear50PercentEvery30Seconds`, `AppConfig.Canary10Percent20Minutes` and `AppConfig.Linear20PercentEvery6Minutes` now exist in every account/region and cannot be updated or deleted; `StartDeployment` and `AWS::AppConfig::Deployment` fail `ResourceNotFoundException` for an unknown strategy id. Contributed by @koh-sh.
- **Aurora DSQL — the 8-key index limit comes first** — measured live, DSQL reports `54011` before the mode, key-expression and `INCLUDE` rules; the proxy reported those first, so a plain `CREATE INDEX` on nine columns drew "unsupported mode". `sys.jobs.details` is now NULL for a job that succeeded, where it was an empty string. Contributed by @vivedo.
- **Aurora DSQL — `CALL sys.wait_for_job($1)` with a bound job id** — a bound job id went to the backing Postgres and failed `3F000`; it now works for `sys.wait_for_job` and `sys.jobs ... WHERE job_id = $1`, `CALL` returns `succeeded` with the `CALL` tag (eu-central-1, 2026-10-04), and a malformed id is refused `22P02`; `CREATE INDEX ASYNC`/`ALTER TABLE ASYNC` return the matching tags, `sys.jobs` declares `oid` and `timestamptz` columns, a constraint validation job carries `class_id` 2606, and a failing unique index reports the service's `details`. Contributed by @vivedo.
- **Aurora DSQL — `TimeZone` missing at startup** — the proxy's startup greeting reported no `TimeZone`, so a driver that decodes `timestamptz` by it could stall on the first value; it now reports `TimeZone`, `IntervalStyle` and the rest of the parameters the live service does. Contributed by @vivedo.
- **OpenSearch — data-plane containers are named per account** — containers were named `ministack-opensearch-<region>-<domain>`, causing Docker name conflicts across accounts and erroneous cleanup; names now include an account and region hash, as RDS does. Contributed by @skialpine.
- **ElastiCache — serverless caches run in cluster mode** — MiniStack's container ran without cluster mode, accepting multi-key commands that hash to different slots where AWS fails with `CROSSSLOT`; the container now runs as one cluster-mode shard holding all slots, and `CLUSTER SLOTS` reports the cache endpoint. Contributed by @skialpine.
- **CloudTrail — `PutEventSelectors` stores `AdvancedEventSelectors` and sets `HasCustomEventSelectors`** — advanced selectors were dropped instead of stored; they are now stored in place of `EventSelectors`, a request with both kinds, neither, or an empty or oversized list fails `InvalidEventSelectorsException`, basic selectors fill in omitted members, and `GetTrail`/`DescribeTrails` report `HasCustomEventSelectors: true` when non-default selectors exist. Contributed by @iot-rocket.
- **CloudTrail — `GetEventSelectors` returns the default selector of a new trail** — a trail that never had `PutEventSelectors` answered an empty `EventSelectors` list instead of the default selector that logs all read and write management events. Contributed by @iot-rocket.
- **STS — `GetAccessKeyInfo` answers the account that owns the key** — it always returned the caller's account; it now returns the owning account for known keys, or decodes it from the key id for any `AKIA`/`ASIA` key as AWS does, and returns `ValidationError` for a key id that encodes no account or violates constraints. Contributed by @iot-rocket.
- **STS — `GetAccessKeyInfo` is authorized like other actions** — under `AUTH=true` it was allowed like `GetCallerIdentity` and `GetSessionToken`, whatever the caller's policies said. It now needs an identity policy that allows it and no deny, and otherwise answers `AccessDenied`, as AWS does; the other two stay allowed. Contributed by @iot-rocket.
- **SNS — `CreateTopic` keeps its tags** — the handler read `Tag.member.N` where the API sends `Tags.member.N`, so tags given at creation were dropped. Contributed by @iot-rocket.

## [1.5.21] — 2026-10-03

### Added

- **API Gateway v2 — custom domain names and API mappings** — `CreateDomainName`, `GetDomainName`, `GetDomainNames`, `UpdateDomainName`, `DeleteDomainName` and `Create`/`Get`/`Update`/`DeleteApiMapping(s)`, with domain tags through `TagResource`, list pagination, and `BadRequestException` "Invalid stage identifier specified" for a mapping to a missing stage; `AWS::ApiGatewayV2::DomainName` and `AWS::ApiGatewayV2::ApiMapping` in CloudFormation. A domain and its mappings are the same resource as the API Gateway v1 domain and base path mappings, and a request whose `Host` is the domain reaches the mapped HTTP API. Reported by @wparad.
- **CloudFormation — stack updates replace resources** — a change to a property the change set reports as `RequiresRecreation: Always` now creates a new resource under a new generated name, points its dependents at it and deletes the old one after the update succeeds (a rollback deletes the new one instead). With an explicit, unchanged name the update fails with the AWS message naming the physical id; this now also covers `AWS::Lambda::Function`. A named SQS queue or SNS topic fails with AWS's already-exists error instead and keeps its messages or subscriptions. Contributed by @iot-rocket.
- **CloudFormation — `AWS::EFS::FileSystem`, `AWS::EFS::MountTarget` and `AWS::EFS::AccessPoint`** — the three types create, update in place and are replaced on create-only changes through the EFS store, with `Ref` and `Fn::GetAtt` as in the template reference, so CDK `efs.FileSystem` stacks deploy. Contributed by @fabio-andre-rodrigues.
- **EFS — file system policy, protection and replication** — `PutFileSystemPolicy`, `DescribeFileSystemPolicy`, `DeleteFileSystemPolicy`, `UpdateFileSystemProtection` and `Create`/`Describe`/`DeleteReplicationConfiguration`; `CreateMountTarget` checks the subnet, security groups, zone and address (`SubnetNotFound`, `SecurityGroupNotFound`, `MountTargetConflict`, `AvailabilityZonesMismatch`, `IpAddressInUse`), and `DeleteFileSystem` with mount targets answers 409. Contributed by @fabio-andre-rodrigues.
- **Bedrock AgentCore — Memory resources** — `CreateMemory`, `GetMemory`, `ListMemories`, `UpdateMemory` and `DeleteMemory`; memory strategies are recorded without extraction. Contributed by @pingedbrain.
- **Athena — databases, DDL, and Trino-style table references** — `ListDatabases` and `GetDatabase` read the Glue Data Catalog; `CREATE EXTERNAL TABLE` and `DROP TABLE` apply to it, creating the Glue table Athena would (formats, SerDe, TBLPROPERTIES); `CREATE TABLE` without `EXTERNAL` is rejected as Athena rejects it, and Iceberg tables are not supported; a query may name a table as `"awsdatacatalog"."db"."t"` or `db.t`. Contributed by @sjincho.
- **CloudFront — distributions serve traffic, plus the AWS-managed policies and monitoring subscriptions** — a request to `<label>.cloudfront.net` (or `<label>.cloudfront.<MINISTACK_HOST>`) is served: ordered cache behaviors, `ViewerProtocolPolicy`, `AllowedMethods`, `DefaultRootObject`, cache and origin request policy forwarding (and legacy `ForwardedValues`), `viewer-request` / `viewer-response` CloudFront Functions, response headers policies, and S3 or custom origins; an S3 origin is read only as the bucket policy allows it (OAC, OAI or public). `DomainName` has the AWS `d` + 13-character shape. `List*Policies` return the AWS-managed cache, origin request and response headers policies, and `Create`/`Get`/`DeleteMonitoringSubscription` are implemented. Not modelled: caching, WAF, logging, geo restrictions, custom error pages, signed URLs and cookies, Lambda@Edge. Contributed by @skialpine.
- **CloudFormation — EKS cluster and node group updates** — `AWS::EKS::Cluster` applies `Version`, `Logging`, `ResourcesVpcConfig`, `AccessConfig.AuthenticationMode` and `Tags` in place and `AWS::EKS::Nodegroup` applies `ScalingConfig`, `Labels`, `Taints`, `UpdateConfig`, `LaunchTemplate`, `Version`, `ReleaseVersion` and `Tags`, where every such change used to report `UPDATE_COMPLETE` and was dropped. Contributed by @iot-rocket.
- **CloudFormation — `AWS::Pipes::Pipe` and `AWS::Scheduler::ScheduleGroup` update in place** — a pipe keeps its stream position and `CreationTime` when `Description`, `Target`, `RoleArn`, `DesiredState` or `Tags` change, gets `Description` and `Tags` on create and refuses a create-only source change under an explicit `Name`, and a schedule group takes template and stack tag changes while keeping tags added through `TagResource`. Contributed by @iot-rocket.
- **CloudFormation — ECR repositories are replaced** — an `EncryptionConfiguration` change on `AWS::ECR::Repository` replaces the repository under a new generated name (generated names now carry the usual suffix) and is refused for an explicit `RepositoryName`, where it used to be ignored. Contributed by @iot-rocket.
- **CloudFormation — IAM roles and managed policies are replaced** — a `Path` change on `AWS::IAM::Role` and a `Path` or `Description` change on `AWS::IAM::ManagedPolicy` replace the resource under a new generated name, a named role refuses it as a custom-named replacement, a named policy fails with the IAM duplicate-name error as on AWS, also when only its `Path` changes, generated policy names get the suffix other generated names have, and both ARNs include the `Path`. Contributed by @iot-rocket.
- **CloudFormation — `IMPORT` change sets execute** — existing SQS queues, SSM parameters, S3 buckets, DynamoDB tables, IAM roles, log groups, Lambda functions, IoT policies, IoT CA certificates and Cognito user pools are adopted into a new or existing stack without being changed (`IMPORT_IN_PROGRESS` to `IMPORT_COMPLETE`, or a rollback when a resource is gone), and an import that changes `Outputs` or stack tags is refused, as on AWS. Contributed by @iot-rocket.

### Fixed

- **Docker — the image healthcheck probes `GATEWAY_PORT`** — it always requested `localhost:4566`, so a container started on another port reported `unhealthy` while serving; it now resolves the port as the server does (`GATEWAY_PORT`, then `EDGE_PORT`, then `4566`). Contributed by @skialpine.
- **Lambda — Node.js and Python functions start in the code directory** — `process.cwd()` and `os.getcwd()` were MiniStack's own directory, so libraries that read files relative to it (such as `node-config`) missed the function's files; the working directory is now the code root (`LAMBDA_TASK_ROOT`), as on AWS. Contributed by @drakeo338. Reported by @shane-patzlsberger.
- **STS — `AssumeRole` honors trust-policy denies and conditions** — with `AUTH=true`, a matching `Deny` (including `NotAction`) overrides an `Allow`, and `sts:ExternalId` and `sts:RoleSessionName` conditions are evaluated; a denied call returns `AccessDenied` and creates no session. Contributed by @AdrianAcala.
- **Cognito — federated sign-in tokens** — the ID token carries the `nonce` from `/oauth2/authorize`, both tokens carry `cognito:groups`, and a user linked by a PreSignUp trigger signs in as the linked profile instead of a new one. Contributed by @kjdev.
- **CloudFormation — nested stack with a `Transform`** — under `AUTH=true`, a nested stack whose template declares a `Transform` or calls `Fn::Transform` fails with `Requires capabilities : [CAPABILITY_AUTO_EXPAND]` unless the parent acknowledged `CAPABILITY_AUTO_EXPAND`, as on AWS. Contributed by @iot-rocket.
- **CloudFormation — `AWS::RDS::DBCluster` and `AWS::RDS::DBInstance` update in place** — a stack update re-ran the create, which gave the resource a new endpoint, resource id and create time and emptied the cluster's member list; the properties the create stores now change on the existing record, a create-only or `Engine` change replaces the resource or, under a custom identifier, is refused, a cluster `MasterUsername` change leaves the cluster as it is, change sets report which properties replace, and a stack-created cluster can now be described and answers `Fn::GetAtt DBClusterResourceId`. Contributed by @iot-rocket.
- **SES — a sandboxed account sends only to verified or simulator recipients** — SES v2 `PutAccountDetails` with `ProductionAccessEnabled=false` puts the account (per region) in the sandbox, as on AWS, and `GetAccount` reports it with the submitted `Details`. A sandboxed send to a recipient that is neither a verified address or domain identity nor a `@simulator.amazonses.com` address fails with `MessageRejected` "Email address is not verified. The following identities failed the check in region …" for v1 `SendEmail`, `SendRawEmail` and `SendTemplatedEmail` and v2 `SendEmail`; bulk sends reject only the affected entries. Accounts stay in production by default. Reported by @skialpine.
- **CloudFormation — `AWS::ECS::Cluster` and `AWS::ECS::TaskDefinition` update in place** — a cluster change keeps the cluster (settings or configuration dropped from the template stay), and a task definition change registers the next revision of the family and deregisters the old one instead of overwriting revision 1. Contributed by @iot-rocket.
- **CloudFormation — more `IMPORT` types** — SNS topics, KMS keys and aliases, IoT thing types, Cognito user pool clients, groups, resource servers and identity pools, and API Gateway REST APIs and stages can be imported, including two-key identifiers, and `Fn::GetAtt` on an identity pool's `Id` resolves. Contributed by @iot-rocket.
- **CloudFormation — `ImportExistingResources`** — a `CREATE` or `UPDATE` change set imports an added resource whose static custom name already exists (it needs `DeletionPolicy` `Retain` or `RetainExceptOnCreate`), and a rollback releases imported resources instead of deleting them. Contributed by @iot-rocket.
- **ECS — `UpdateService` with `forceNewDeployment` replaces the tasks** — it was ignored when the task definition did not change. Rolling deployments now pin image digests from the first task, honor `versionConsistency` `disabled`, use `repositoryCredentials` for private registries, and report `imageDigest` and the Fargate platform version; a task falls back to the local image when the pull fails. Contributed by @AdrianAcala.
- **AutoScaling — target tracking alarms** — `PutScalingPolicy` with `TargetTrackingScaling` creates the `TargetTracking-<group>-AlarmHigh-<uuid>` alarm and, unless `DisableScaleIn` is set, the `AlarmLow` one on the tracked metric, lists them in `Alarms` of `PutScalingPolicy` and `DescribePolicies`, replaces them when the policy is updated and deletes them with the policy or its group. An `AWS::AutoScaling::ScalingPolicy` in a template does the same. Contributed by @iot-rocket.
- **IoT — mTLS trusts every registered device certificate** — the listener no longer fails the TLS handshake for an ACTIVE certificate whose CA was deactivated or deleted before its first connect, or that was registered without a CA; as on AWS, only the certificate's own status refuses it. Contributed by @iot-rocket.
- **API Gateway — usage plan throttling** — a REST API request carrying an API key from a usage plan on the stage is throttled with `429 Too Many Requests` by the plan's `throttle` and its per-method `apiStages[].throttle`, in addition to the stage's method settings, and a throttled response carries `x-amzn-ErrorType: TooManyRequestsException`. Contributed by @iot-rocket.
- **API Gateway — request body validation follows JSON Schema draft 4** — a request validator checks the body against the draft 4 keywords of the model (`enum`, `pattern`, length, range and `multipleOf` bounds, `integer` versus `number` versus `boolean`, `items`, `additionalProperties`, `patternProperties`, `dependencies`, `allOf` / `anyOf` / `oneOf` / `not`, formats, and `$ref` to local definitions and to other models of the API); an empty body and a body nested more than 1000 levels deep are refused, and a schema that applies itself to the same value again answers 500. `BAD_REQUEST_BODY` and `BAD_REQUEST_PARAMETERS` read `{"message": "..."}` and carry `x-amzn-ErrorType: BadRequestException`. Contributed by @iot-rocket.
- **API Gateway v2 (WebSocket API) — `$connect` authorization** — a `CUSTOM` `$connect` route ran no authorizer. It now runs its REQUEST authorizer, refuses the handshake with 401, 403 or 500 and passes `principalId` and the context to `requestContext.authorizer` of the connection's events. `CreateAuthorizer`, `CreateRoute`, `UpdateRoute` and the CloudFormation resources refuse a JWT authorizer, JWT route authorization and authorization on a route other than `$connect` with `BadRequestException`, as AWS does; a JWT `$connect` route kept in saved state refuses the handshake with 500 instead of validating the token. `requestContext.stage` names the stage in the connection URL instead of `$default`. Contributed by @iot-rocket.
- **Lambda — Docker executor under `USE_SSL=1` when MiniStack runs in a container** — the gateway certificate, CA bundle and Java truststore were bind-mounted into every Lambda container from MiniStack's own filesystem (`MINISTACK_SSL_CERT`, or the generated `ministack-tls/server.crt` under the temp directory), paths the host Docker daemon cannot see, so every invocation failed with `bind source path does not exist`. In a container they are now copied into the Lambda container, as function code already is. Contributed by @skialpine.
- **RDS — `DescribeDBClusterSnapshotAttributes`** — was unimplemented (`InvalidAction: Unknown RDS action`), so Terraform's `aws_db_cluster_snapshot` resource failed on read (`reading RDS DB Cluster Snapshot … attribute`) after creating the snapshot. Returns the `restore` attribute with no shared accounts (the manual-snapshot default); `ModifyDBClusterSnapshotAttribute` is not implemented. Unknown snapshot ids answer `DBClusterSnapshotNotFoundFault`. Contributed by @skialpine.
- **SES — a send from an unverified sender is rejected** — v1 `SendEmail`, `SendRawEmail`, `SendTemplatedEmail` and `SendBulkTemplatedEmail` and v2 `SendEmail` and `SendBulkEmail` accepted any `Source` / `FromEmailAddress`. AWS requires the sender to be a verified identity in the account and region, in production as well as the sandbox, and now so does MiniStack: the send fails with `MessageRejected` "Email address is not verified. The following identities failed the check in region …". A verified domain covers its addresses and subdomains, domain names compare case-insensitively and email addresses case-sensitively, as AWS documents. Tests that send from an address they never created as an identity need a `VerifyEmailIdentity` / `CreateEmailIdentity` first. Reported by @skialpine.
- **CloudFormation — a change set with only new stack tags lists them** — it ended `FAILED` with "didn't contain changes"; it now lists each resource the stack holds, other than a custom resource or wait condition, as a `Modify` with `Scope: Tags`, and stack tags given in another order are no change for a change set or `UpdateStack`. Contributed by @iot-rocket.
- **CloudFormation — change sets list what a change reaches** — a resource that references a replaced resource, an attribute of a modified resource or a changed parameter is now listed as a `Modify` whose detail names the cause (`ChangeSource` `ResourceReference`, `ResourceAttribute` or `ParameterReference`, with `CausingEntity`). Contributed by @iot-rocket.
- **ECS — `DescribeClusters` statistics** — `include=["STATISTICS"]` returns the sixteen running/pending task and active/draining service counters per launch type instead of an empty list. Contributed by @iot-rocket.
- **ECS — `DescribeClusters` honours `include`** — `settings` and `tags` come back empty and `attachments` and `configuration` are left out unless requested, `CreateCluster` keeps `configuration`, and a CloudFormation cluster reports its tags and the default `containerInsights` setting. Contributed by @iot-rocket.
- **CloudFormation — `AWS::S3::MultiRegionAccessPoint` and `AWS::AutoScaling::LaunchConfiguration` are replaced on update** — every property of both types is create-only, so a change now creates the resource under a new generated name and deletes the old one after the update (a `Regions` change was dropped and the stack reported the alias as the physical id, a launch configuration was overwritten under its old name), fails with the custom-named-resource error when the name is explicit, and is reported as `Replacement: True` in a change set. Contributed by @iot-rocket.
- **CloudFormation — `AWS::SQS::Queue` with `FifoQueue` gets a generated `.fifo` name** — `FifoQueue: true` without a `QueueName` creates a FIFO queue with a generated `.fifo` name instead of a standard queue, and a `QueueName` whose `.fifo` suffix disagrees with `FifoQueue` fails the resource. Contributed by @iot-rocket.
- **CloudFormation — `Fn::Select` in a condition** — a condition such as `!Not [!Equals [!Select [2, !Ref KeySpec], ""]]` was always true; conditions now resolve `Fn::Select`, and an index outside the list fails with `Template error: Fn::Select cannot select nonexistent value at index N`, in conditions and in properties. Contributed by @mishukdutta-cz.
- **CloudFormation — `Ref` to a list parameter** — a `Ref` to a `CommaDelimitedList` or `List<...>` parameter gave the raw string, so a list property got one item per character; it now gives the space-trimmed list. A list in a nested stack's `Parameters` or in an `AWS::SSM::Parameter` `Value` fails the resource. Contributed by @mishukdutta-cz.

## [1.5.20] — 2026-10-01

### Added

- **DynamoDB — global table replicas through `UpdateTable` `ReplicaUpdates`** — a `Create` was accepted and ignored, so Terraform / OpenTofu `aws_dynamodb_table` with a `replica` block waited forever. A `Create` now copies the table and its items to the other region and turns on `NEW_AND_OLD_IMAGES` streams; `DescribeTable` reports `GlobalTableVersion` `2019.11.21` and the other regions under `Replicas` as `ACTIVE`; writes and TTL settings replicate; `Delete` removes the replica. Reported by @wparad.
- **IoT — registry events** — with a type enabled through `UpdateEventConfigurations`, the thing, thing type, thing type association, thing group, thing group hierarchy and thing group membership operations publish the AWS payload to `$aws/events/...`, where MQTT subscribers and topic rules receive it. Contributed by @iot-rocket.
- **ElastiCache — serverless caches** — `CreateServerlessCache`, `DescribeServerlessCaches`, `ModifyServerlessCache` and `DeleteServerlessCache` for `valkey` (major 7, 8 or 9) and `redis` (major 7). With Docker each cache gets a TLS-only Valkey or Redis container whose certificate chains to the CA at `GET /_ministack/elasticache/ca.pem`. `ModifyServerlessCache` allows a same-engine major upgrade, redis to valkey, and valkey 7 to redis 7. Memcached serverless caches and serverless snapshots are refused with `InvalidParameterValue`. Contributed by @skialpine.
- **CloudFormation — `AWS::Lambda::Version` updates `FunctionScalingConfig` in place** — a stack update re-ran the create, which published a new version for every change; a `FunctionScalingConfig` change now keeps the version, also when the update rolls back. Contributed by @iot-rocket.
- **CloudFormation — `AWS::Glue::Database`, `Table`, `Partition`, `Connection`, `Crawler`, `Job` and `Trigger`** — they failed with `Unrecognized resource types`; they now create, update and delete through the Glue API, so Athena and the Glue API read a stack's tables. Ref and `Fn::GetAtt` follow the CloudFormation reference, as do replacing and in-place properties; a `DatabaseInput.Name` change fails with `Database <name> not found` and rolls back, as on AWS. Crawlers and jobs are only recorded, never run. Contributed by @fabio-andre-rodrigues.
- **CloudFormation — `AWS::ElastiCache::SubnetGroup`, `ParameterGroup`, `CacheCluster`, `ReplicationGroup`, `User` and `UserGroup`** — they failed with `Unrecognized resource types`; they now go through the ElastiCache API, so a stack's cluster or replication group gets a real container-backed endpoint to pass to a Lambda function or ECS task. Replacement follows the CloudFormation reference (including `NumCacheNodes` when no Availability Zone is given), everything else updates in place, and `Fn::GetAtt` on an endpoint the resource does not have fails, as the reference says. Contributed by @fabio-andre-rodrigues.
- **IoT — `DescribeEventConfigurations` and `UpdateEventConfigurations`** — both answered `Unsupported IoT path`; they now store the registry event switches per account and region, every type starting disabled, an update changing only the types it names, and `creationDate` / `lastModifiedDate` set from the first update on, as on AWS. Contributed by @iot-rocket.
- **CloudFormation — `AWS::ServiceDiscovery::HttpNamespace`, `PrivateDnsNamespace`, `PublicDnsNamespace`, `Service` and `Instance`** — they failed with `Unrecognized resource types`, which blocked the CDK ECS Cloud Map constructs; they now go through the Cloud Map API, with Ref, `Fn::GetAtt` and replacement as in the CloudFormation reference. Namespaces now store `Properties.DnsProperties.SOA.TTL`, and `DeleteNamespace` removes the namespace's hosted zone, as on AWS. Contributed by @fabio-andre-rodrigues.
- **Bedrock AgentCore — runtime version history and endpoint pinning** — runtime updates now retain version snapshots, `GetAgentRuntime` retrieves a selected version, and `ListAgentRuntimeVersions` paginates the history. `DEFAULT` advances with the latest version while named endpoints stay pinned until updated; invocations use the selected snapshot, with containers isolated by version. Contributed by @pingedbrain.
- **Bedrock AgentCore — `ListAgentRuntimes` and `ListAgentRuntimeEndpoints` paginate** — both take `maxResults` and `nextToken` and return a `nextToken` only when another page exists. Contributed by @pingedbrain.

### Changed

- **botocore 1.43.106** — the service models MiniStack reads move from 1.43.63 to 1.43.106; the images keep `awscli` 1.45.63, installed on the same botocore instead of its pinned one.

### Fixed


- **SES v2 — `ListEmailIdentities` and `ListConfigurationSets` answer the routes newer SDKs use** — botocore 1.43.106 sends them as `POST /v2/email/list-identities` and `POST /v2/email/list-configuration-sets` with `NextToken`, `PageSize` and `Filter` in the body; those paths answered `NotFoundException`. Both forms page, and the `Filter` keys are applied.
- **Kinesis — `ApproximateArrivalTimestamp` keeps milliseconds** — it was truncated to whole seconds, so an `AT_TIMESTAMP` iterator from an SDK that sends fractional seconds skipped records written earlier in the same second.
- **CloudFormation — an empty `Capabilities` list is accepted** — botocore sends it as a bare `Capabilities=`, which was read as one empty value and refused, so `aws cloudformation deploy` without `--capabilities` and `Capabilities=[]` from an SDK failed with a `ValidationError` since 1.5.11.
- **API Gateway v2 (HTTP API) — the request path keeps a `%25` escape** — `/items/a%252Eb` reached the Lambda's `rawPath` as `/items/a%2Eb`, because the path was fully percent-decoded. The HTTP API path now keeps `%25` and decodes every other escape as before; `rawPath`, `requestContext.http.path`, `pathParameters` and route matching all use it. Contributed by @skialpine.
- **Step Functions — JSONPath `$$.` paths read the context object** — a Choice rule's `Variable` and its variable-to-variable comparison paths (including inside `And`, `Or` and `Not`), and a state's `InputPath`, `OutputPath` and Map `ItemsPath`, resolved `$$.` against the state input, so a Choice on `$$.Execution.Input` silently took its default branch. They now read the context object, as the AWS context object reference lists for those fields. Contributed by @jayjanssen.
- **CloudFormation — `AWS::EC2::VPCGatewayAttachment` updates in place** — a changed `InternetGatewayId` or `VpnGatewayId` moves the attachment under the same `IGW|vpc-…` / `VGW|vpc-…` physical id, `VpnGatewayId` is attached at all, and a `VpcId` change leaves the gateway attached to the new VPC, or to the old one when the update rolls back. `AttachInternetGateway` on a gateway already attached to a VPC answers `Resource.AlreadyAssociated`, and a stack attaching it to a second VPC leaves it on the first, as AWS does. Contributed by @iot-rocket.
- **CloudFormation — `AWS::Lambda::Version` publishes through `PublishVersion`** — the version takes its `Description`, a version of a function unchanged since its latest version fails with the `AlreadyExists` error AWS reports, `FunctionScalingConfig` on a function without a capacity provider is refused, and a function keeps its `CapacityProviderConfig`. Contributed by @iot-rocket.
- **AppConfig — hosted configuration version numbers are never reused** — a version created after a delete, and the replacement CloudFormation makes for a changed `HostedConfigurationVersion`, get the next unused number instead of overwriting the live one, and a `LatestVersionNumber` mismatch reports the AWS message. Contributed by @iot-rocket.
- **CloudFormation — a deleted `AWS::AppConfig::Deployment` stays in `ListDeployments`** — the stack delete removed the deployment, so a replaced deployment left the environment's history and the next one reused its number; it now stays until its environment is deleted, which, through the API or with its stack, also removes its deployments and their tags. Contributed by @iot-rocket.
- **CloudFormation — `AWS::ECS::Service` stores `NetworkConfiguration` and `LoadBalancers` in the ECS API shape** — `DescribeServices` returns them in camelCase on create and update, and a service from a template registers its tasks in its target groups. Contributed by @iot-rocket.
- **SESv2 — `CreateEmailIdentity`/`GetEmailIdentity` return Easy DKIM tokens for a DOMAIN identity** — a domain identity created without `DkimSigningAttributes` answered an empty `Tokens` list with `Status: NOT_STARTED`. AWS provides a set of DKIM tokens for its CNAME records in that case (Easy DKIM), so the Terraform `aws_sesv2_email_identity` resource's `dkim_signing_attributes[0].tokens` indexing failed. A DOMAIN identity now gets three tokens, `SigningAttributesOrigin: AWS_SES` and `Status: PENDING`, unless `DkimSigningAttributes` brings its own key (BYODKIM); EMAIL_ADDRESS identities are unchanged. Contributed by @skialpine.
- **Lambda — functions reach a `USE_SSL=1` gateway** — the gateway listener serves only HTTPS, but a function's default `AWS_ENDPOINT_URL` was plain HTTP in every executor (Docker, the warm worker and the local subprocess), and the Docker executor's Node shim downgraded a function's `https.request` to the gateway to HTTP, which the listener resets. The default endpoint now follows the gateway's scheme, the shim keeps TLS, and a host process trusts the gateway's certificate through `AWS_CA_BUNDLE` / `REQUESTS_CA_BUNDLE` / `NODE_EXTRA_CA_CERTS` unless they are already set. Without `USE_SSL` nothing changes. Contributed by @skialpine.
- **RDS — `MINISTACK_RDS_PUBLIC_ENDPOINT=1` works with a Compose `hostname:`** — a containerised MiniStack whose container sets a hostname left instances `creating`, because the self-lookup by `HOSTNAME` found no container. A containerised MiniStack now detects its network as with the setting off (`DOCKER_NETWORK`, then the self-lookup). Contributed by @skialpine.
- **IoT — `DeleteThingGroup` refuses a group with child groups** — the delete went through and left the children pointing at a missing parent; it now fails with `InvalidRequestException` "Cannot delete thing group : {name} when there are still child groups attached to it", and a CloudFormation stack delete that reaches such a group ends in `DELETE_FAILED`. Contributed by @iot-rocket.
- **IoT — `UpdateThing` honours `removeThingType`** — the flag was ignored, so the thing kept its type, and an update without `attributePayload` cleared the attributes. Contributed by @iot-rocket.
- **API Gateway v2 (HTTP API) — a doubled leading slash still selects the route** — a request to `//items/abc` matched no route and answered `404 {"message":"Not Found"}`. Route selection and `pathParameters` now ignore the extra leading slashes; `rawPath` keeps the path as received. Contributed by @skialpine.
- **Cloud Map — `DeleteNamespace` refuses a namespace that still has services** — it deleted the namespace (and now its hosted zone) anyway. It answers `400 ResourceInUse` "Namespace has associated services; delete the services before deleting the namespace", as on AWS.
- **Lambda — `PublishVersion` of an unchanged function returns the latest version** — it published a new version every time; AWS doesn't publish when the code and configuration haven't changed since the last version, and returns that version with its original description.
- **API Gateway v2 (HTTP API) — REQUEST authorizer identity sources** — a declared identity source missing from the request answers `401 {"message":"Unauthorized"}` without invoking the authorizer, cached or not (it required caching). `$context.*` sources such as `$context.routeKey` are resolved from the request instead of counting as missing, so an authorizer caching per route no longer answered 401 to every request. Contributed by @skialpine.
- **ECS — optional task-definition fields stay omitted** — registering an EC2 task definition without task-level `cpu`, `memory`, or `requiresCompatibilities` no longer invents `256`, `512`, or `["EC2"]` in register, describe, and deregister responses. Explicitly supplied values remain in the definition. Contributed by @AdrianAcala.
- **IAM — customer-managed permissions use the explicit account** — policy evaluation now resolves the complete policy ARN in the principal's account, so a different ambient request account cannot hide that policy or supply a foreign account's document. Permissions continue to follow the policy's current default version. Contributed by @AdrianAcala.
- **Cognito — `USER_SRP_AUTH` checks the password** — `PASSWORD_VERIFIER` accepted any response, so a wrong password or a disabled or unconfirmed user got tokens. The challenge now carries SRP-6a parameters with a stable salt per user, and a response whose signature does not prove the stored password is refused with `NotAuthorizedException`; `TIMESTAMP` must read `EEE MMM d HH:mm:ss z yyyy`, and a temporary password leads to `NEW_PASSWORD_REQUIRED`. `CUSTOM_WITH_SRP` gets the same check. Contributed by @iot-rocket.
- **Cognito — refresh tokens are checked against the pool and client** — `REFRESH_TOKEN_AUTH` (`InitiateAuth`, `AdminInitiateAuth`) and `GetTokensFromRefreshToken` issued tokens for the first user in the pool when the refresh token was malformed, from another pool or of a deleted user, and accepted a token issued to another client; they now answer `NotAuthorizedException` with the message AWS gives for each case, and the refresh token from a SAML or OIDC federated sign-in now refreshes the federated user's session. Contributed by @iot-rocket.
- **CloudFormation — `AWS::S3Tables::TableBucket` and `AWS::S3Tables::Table` update in place** — a stack update re-ran the create, which reset the bucket's `createdAt` and rebuilt the table with a new `createdAt` and its initial metadata location, dropping committed metadata; a maintenance setting or tag change now keeps both, and a create-only table change under an unchanged name fails with the conflict AWS reports. Contributed by @iot-rocket.
- **RDS — `DescribeGlobalClusters` lists clusters as `GlobalClusterMember`** — each cluster was a `<GlobalCluster>` element, so SDKs that match the list member name, such as aws-sdk-go-v2, found none. Contributed by @IamYipi.
- **Amazon MQ — `DescribeBrokerInstanceOptions` returns `supportedEngineVersions` as strings** — they were `{"name": ...}` objects, which aws-sdk-go-v2 could not deserialize. Contributed by @IamYipi.
- **EventBridge — `UpdateEventBus` returns the bus** — it answered `EventBusArn` and `LastModifiedTime`, which are not response members, so SDKs returned nothing; it now returns `Arn`, `Name` and the bus's `Description`, `KmsKeyIdentifier`, `DeadLetterConfig` and `LogConfig`. Contributed by @IamYipi.
- **CloudFormation — `AWS::CodeBuild::Project` stores `Source`, `Artifacts` and `Environment` under CodeBuild API names** — the template's PascalCase members and `BuildSpec` were stored as written, so `BatchGetProjects` dropped them and a build of a stack's project found no buildspec, image or environment variables. Contributed by @IamYipi.
- **Inspector2 — responses aws-sdk-go-v2 can read** — timestamps are epoch seconds, `ListFilters` returns `criteria`, `ownerId`, `createdAt` and `updatedAt`, `CreateFilter` returns `arn`, and coverage `scanStatus` is `{statusCode, reason}`. Saved state is converted on restore. Contributed by @IamYipi.
- **EventBridge — `PutEvents` refuses `aws.*` sources** — an entry whose `Source` starts with `aws.` fails with `NotAuthorizedForSourceException` (`Not authorized for the source.`) in its result entry and counts in `FailedEntryCount`, while the rest of the batch is delivered, as on AWS. Events published with an `aws.*` source through `PutEvents` to simulate AWS services now get this error. Contributed by @iot-rocket.
- **SQS — `QueueUrl` matches the gateway's TLS scheme (`USE_SSL=1`)** — `CreateQueue`/`GetQueueUrl`/`ListQueues` always returned `http://` URLs, and the AWS SDK v3 uses the QueueUrl itself as the request endpoint (`useQueueUrlAsEndpoint` defaults true), so a client handed that URL left the TLS-only gateway. The `AWS::SQS::Queue` CloudFormation provisioner built the same hardcoded scheme and now reuses the shared helper. Contributed by @skialpine.
- **API Gateway v2 (HTTP API) — a request no stage or route matches answers AWS's exact 404 body** — an unmatched route answered `{"message": "No route found"}` and an unknown stage `{"message": "Stage '…' not found"}`; both now answer `{"message":"Not Found"}` (compact JSON), as AWS does. Contributed by @skialpine.
- **API Gateway v2 — `apiEndpoint` follows the gateway's TLS scheme** — with `USE_SSL=1` the gateway listener serves only HTTPS, but CreateApi and the `AWS::ApiGatewayV2::Api` CloudFormation resource always returned an `http://` `apiEndpoint`, which nothing answers. They now return `https://`, or `wss://` for a WebSocket API, as AWS does. Without `USE_SSL` the endpoint keeps `http://`. Contributed by @skialpine.
- **IAM — condition value lists and request-tag key matching** — negated conditions now match only when none of their policy values match, so a `StringNotEquals` deny listing permitted values no longer denies one of those values. Request-tag condition keys match every tag name without regard to case, including names that differ only by case; affirmative conditions match any of those values and negated conditions match none. Comparisons of tag values keep the selected operator's case rules. Contributed by @AdrianAcala.
- **SSM — parameter tag actions authorize the parameter named by `ResourceId`** — with `AUTH=true`, `AddTagsToResource`, `RemoveTagsFromResource`, and `ListTagsForResource` checked `Name` instead, so an exact ARN allow could fail and an exact ARN deny could be missed under a broad allow. Accepted parameter-name and ARN aliases now use the canonical parameter ARN for the IAM check. SSM authorization denials now match AWS's HTTP 400 JSON 1.1 response, including the canonical resource and explicit-deny reason. Contributed by @AdrianAcala.
- **Bedrock AgentCore — a missing runtime answers AWS's message** — the control-plane runtime operations answered `Agent runtime <id> not found`; they now answer `ResourceNotFoundException` "Agent '<id>' was not found. Please check the agent ID and try again.", as AWS does.

## [1.5.19] — 2026-09-30

### Added


- **RDS — IAM database authentication for MySQL and Aurora MySQL** — users created `IDENTIFIED WITH AWSAuthenticationPlugin AS 'RDS'` log in with an SDK-generated token over `mysql_clear_password`, as on AWS. The instance or cluster must have `IAMDatabaseAuthenticationEnabled`; with `AUTH=true` the token and the `rds-db:connect` policy are verified too. `ModifyDBInstance` accepts `EnableIAMDatabaseAuthentication`. Contributed by @Areson.
- **Kinesis — `SubscribeToShard`** — enhanced fan-out: a registered consumer receives the shard's records as `SubscribeToShardEvent`s over an event stream (HTTP/1.1 or HTTP/2) for up to 5 minutes, from any `StartingPosition`, with `ContinuationSequenceNumber` for resuming and `ChildShards` when the shard is split or merged. A second call for the same consumer and shard within 5 seconds is a `ResourceInUseException`; a later one takes the subscription over.
- **CloudWatch Logs — resource policies** — `PutResourcePolicy`, `DescribeResourcePolicies` and `DeleteResourcePolicy`, account-scoped (up to 10) or scoped to one log group through `resourceArn`, with `expectedRevisionId` checks. `AWS::Logs::ResourcePolicy` stacks now create real policies. Contributed by @fabio-andre-rodrigues.
- **CloudFormation — `RollbackStack`** — rolls a stack left `CREATE_FAILED` or `UPDATE_FAILED` with rollback disabled back to its last stable state: a failed create ends `ROLLBACK_COMPLETE`, a failed update reverts its changes, deletes what it added and ends `UPDATE_ROLLBACK_COMPLETE`. `RetainExceptOnCreate` is honoured. Contributed by @fabio-andre-rodrigues.
- **CloudFormation — stack drift detection** — `DetectStackDrift`, `DescribeStackDriftDetectionStatus`, `DetectStackResourceDrift` and `DescribeStackResourceDrifts` compare the properties a template sets, plus stack-level tags, with the service's current record (`IN_SYNC`, `MODIFIED` with `PropertyDifferences`, `DELETED`); stacks and resources report `DriftInformation`. Contributed by @fabio-andre-rodrigues.
- **Bedrock AgentCore — resource-based policies** — `PutResourcePolicy`, `GetResourcePolicy` and `DeleteResourcePolicy` persist policies for runtimes and endpoints. With `AUTH=true`, runtime invocation evaluates caller principals, explicit denies and the runtime-plus-endpoint policy requirement for cross-account calls. This emulates the documented AgentCore policy contract locally; it does not validate behavior against AWS. Contributed by @pingedbrain.
- **KMS — grants** — `CreateGrant`, `RevokeGrant` and `RetireGrant`; `ListGrants` now returns the grants they create, filtered by `GrantId` / `GranteePrincipal` and paged with `Limit` / `Marker`. `CreateGrant` follows the key state, rejects operations the key type cannot perform, and is idempotent for a named grant. Grants persist with the key and are not evaluated for authorization. Contributed by @DaviReisVieira.
- **RDS — IAM database authentication for MySQL and Aurora MySQL** — users created `IDENTIFIED WITH AWSAuthenticationPlugin AS 'RDS'` log in with an SDK-generated token over `mysql_clear_password`, as on AWS. The instance or cluster must have `IAMDatabaseAuthenticationEnabled`; with `AUTH=true` the token and the `rds-db:connect` policy are verified too. `ModifyDBInstance` accepts `EnableIAMDatabaseAuthentication`. Contributed by @Areson.

### Fixed

- **Bedrock AgentCore — runtime and endpoint ARNs match AWS** — `CreateAgentRuntime` returned `…:agent/{uuid}:{version}` and endpoints `…:agentEndpoint/{uuid}`, so identity and resource policies written for AWS never matched and every update changed the runtime ARN. Runtimes are now `…:runtime/{agentRuntimeId}`, stable across updates, endpoints are `…:runtime/{agentRuntimeId}/runtime-endpoint/{name}`, and the `DEFAULT` endpoint is created with the runtime and follows its latest version. Saved state moves to the new ARNs on restore.
- **Bedrock AgentCore — `InvokeAgentRuntime` enforces IAM policies** — with `AUTH=true`, a runtime invocation resolves to `bedrock-agentcore:InvokeAgentRuntime` and is authorized against both the runtime ARN and its runtime-endpoint ARN (the `qualifier`, or `DEFAULT`), as AWS requires, so a policy can allow one runtime and deny another. Before, the action was not extracted and the invocation skipped policy evaluation. Contributed by @pingedbrain.
- **IAM — a negated condition operator is true when its key is absent** — the evaluator treated an absent key as a failed condition for every operator, so a `Deny` guarded by `StringNotEquals`, `StringNotLike`, `ArnNotLike`, `NotIpAddress` or another negated operator never applied to a request without that key. AWS evaluates such a condition as true and denies. The single-valued negated operators now do the same, while `ForAnyValue` and the affirmative operators still fail on an absent key. Contributed by @iot-rocket.
- **Secrets Manager — `BatchGetSecretValue` returns only the secrets the caller may read** — under `AUTH=true` a grant on `secretsmanager:BatchGetSecretValue` alone returned every secret in the request, and `Filters` were ignored, so a filtered call returned the whole store. Each secret now needs `secretsmanager:GetSecretValue` and lands in `Errors` as `AccessDeniedException` without it, a call by `Filters` also needs `secretsmanager:ListSecrets`, and the filters select the secrets as in `ListSecrets`. Contributed by @iot-rocket.
- **CloudFormation — change set members follow the action** — `Add` and `Remove` changes carried `Replacement: False`, a `Remove` had no physical id, `PolicyAction` was never sent and a `Metadata` or policy detail had no `RequiresRecreation`. `Remove` and `Modify` now name the physical resource, a `Remove` answers `PolicyAction: Delete` and a replacing `Modify` `ReplaceAndDelete` unless the resource retains or snapshots, and attribute details answer `Never`. `PolicyAction` also reports `Retain`, `Snapshot` and their `ReplaceAnd` forms from the resource's policy. Contributed by @iot-rocket.
- **CloudFormation — `AWS::SQS::Queue` applies every queue property** — `RedrivePolicy`, `RedriveAllowPolicy`, `KmsMasterKeyId`, `KmsDataKeyReusePeriodSeconds`, `SqsManagedSseEnabled`, `DeduplicationScope` and `FifoThroughputLimit` were dropped on create and update, so a dead-letter queue declared in a template never received messages, and a value SQS refuses now fails the resource instead of being stored. A queue from a template also defaults to a 1 MiB `MaximumMessageSize` and SSE-SQS encryption, as on AWS. Contributed by @iot-rocket.
- **Lambda — Docker executor honors timeouts above 300 seconds** — pass the configured `Timeout` to AWS RIE through `AWS_LAMBDA_FUNCTION_TIMEOUT`, preventing its default 300-second limit from ending longer invocations early. Timeout updates recycle warm containers so the RIE deadline follows the new configuration. Contributed by @gakuto-cw21.
- **CloudFormation — a nested stack's update deletes the resources its template drops** — a resource removed from the child template, or created by a failed child update that was rolled back, stayed in its service and in the nested stack's resource list. A dropped resource with a `Retain` or `Snapshot` `DeletionPolicy` is kept or snapshotted first, as on AWS. Contributed by @iot-rocket.
- **KMS — `KeyMaterialId`** — `GenerateDataKey`, `GenerateDataKeyWithoutPlaintext`, `GenerateDataKeyPair`, `GenerateDataKeyPairWithoutPlaintext`, `Decrypt`, `ImportKeyMaterial` and `DeleteImportedKeyMaterial` return the identifier of the key material they used, and `DescribeKey` reports it as `CurrentKeyMaterialId` for symmetric keys. The identifier stays the same until the key material changes. Contributed by @zlberto.
- **Lambda — VPC configuration includes `VpcId`** — `CreateFunction`, `GetFunction`, `GetFunctionConfiguration`, and `UpdateFunctionConfiguration` now report the VPC of the configured subnets. Previously, VPC-attached functions returned only subnet and security group IDs. Contributed by @jayjanssen.
- **Lambda — a Docker invocation that reaches its timeout fails** — the emulator's plain-text `Task timed out` reply was returned as a successful payload, so Step Functions recorded `TaskSucceeded` and skipped `Catch`. Contributed by @drakeo338. Reported by @gakuto-cw21.
- **RDS — a persisted instance stays reachable after a restart** — when its saved host port was taken, the instance moved to a new port but `Endpoint.Port` kept the old one.
- **RDS — pending boolean modifications read as `true`** — `PendingModifiedValues` wrote Python `True`/`False`, which the SDKs parse as `false`.
- **CloudFormation — `OnFailure` and `OnStackFailure` are honoured** — CreateStack `OnFailure` and CreateChangeSet `OnStackFailure` take `DO_NOTHING`, `ROLLBACK` or `DELETE` (roll back, then delete the stack). `OnFailure` with `DisableRollback`, or `OnStackFailure=DELETE` on a non-`CREATE` change set, is a `ValidationError`. ExecuteChangeSet now honours `DisableRollback=true`. Contributed by @fabio-andre-rodrigues.
- **CloudFormation — `DeleteStack` `DeletionMode=FORCE_DELETE_STACK`** — on a `DELETE_FAILED` stack, resources that fail to delete and their dependencies are retained as `DELETE_SKIPPED` and the stack reaches `DELETE_COMPLETE`; on any other status it is a `ValidationError`. Contributed by @fabio-andre-rodrigues.
- **CloudFormation — `GetTemplate` `TemplateStage` and `ChangeSetName`** — `Processed`, now the default, returns the template after its transforms; `Original` returns it as sent. `StagesAvailable` is reported, and `ChangeSetName` returns a change set's template (`ChangeSetNotFound` when unknown). Contributed by @fabio-andre-rodrigues.
- **CloudFormation — stack events carry `ClientRequestToken`** — every event of CreateStack, UpdateStack, DeleteStack, ExecuteChangeSet, ContinueUpdateRollback, CancelUpdateStack and RollbackStack carries that call's token. Contributed by @fabio-andre-rodrigues.
- **RDS — instances become `available` under `MINISTACK_RDS_PUBLIC_ENDPOINT=1` in a container** — database containers were left off MiniStack's network, so readiness never connected and the instance stayed `creating`. They now join the network for readiness and internal wiring, and `DescribeDBInstances` / `DescribeDBClusters` still report `{MINISTACK_HOST, host_port}`. Contributed by @skialpine.
- **IoT — `RegisterCACertificate` checks the verification certificate** — `DEFAULT` mode requires a `verificationCertificate` signed by the CA with the registration code as its CN, and `SNI_ONLY` refuses one, each with its own error; `AWS::IoT::CACertificate` fails the same way. Contributed by @iot-rocket.

## [1.5.18] — 2026-09-28

### Added

- **Bedrock AgentCore — `InvokeAgentRuntime` runs the runtime's container** — with Docker available (full image), the runtime's `containerConfiguration.containerUri` image is started, checked on `/ping`, and invocations are forwarded to its `/invocations` on port 8080; container failures answer `RuntimeClientError` (424). The container is removed on update, delete or reset. Without Docker, or for a code artifact, the deterministic echo remains. Contributed by @pingedbrain.
- **CloudWatch Logs — delivery destination policy** — `PutDeliveryDestinationPolicy`, `GetDeliveryDestinationPolicy` and `DeleteDeliveryDestinationPolicy`, so a delivery that needs a destination policy can be created. Contributed by @fabio-andre-rodrigues.
- **Cognito — OIDC discovery through the real issuer host (`USE_SSL=1`)** — the generated certificate covers `cognito-idp.<region>.amazonaws.com` for every region and Lambda containers resolve those hosts to MiniStack and trust it. With an `/etc/hosts` entry and the certificate trusted, an unmodified client follows the token's `iss` to MiniStack while `iss` stays exactly as AWS issues it. Reported by @epcap90.
- **KMS — `ListGrants`** — it answered `InvalidAction`. It now lists the key's grants (empty, since grants are not modelled) and answers `NotFoundException` for an unknown key. Reported by @DaviReisVieira.
- **S3 — object annotations** — `PutObjectAnnotation`, `GetObjectAnnotation`, `ListObjectAnnotations` and `DeleteObjectAnnotation`: up to 1,000 UTF-8 payloads per object version, carried by `CopyObject` unless the directive is `EXCLUDE`, removed with their version, and announced with `s3:ObjectAnnotation:*` events. Under `AUTH=true`, Object Lock refuses annotation writes with 403 `AccessDenied`. Previously a `PUT ?annotation` replaced the object with the payload. Contributed by @BoLaMN.
- **SNS — `AddPermission` and `RemovePermission`** — they add and remove labelled statements in the topic's `Policy`; a duplicate label answers `InvalidParameter`. Contributed by @AdrianAcala.

### Fixed

- **API Gateway v1 — REQUEST authorizer events carry `requestContext.identity`** — an authorizer reading `sourceIp` or `userAgent` crashed; both are now present, as in the `AWS_PROXY` event. Contributed by @yosriady.
- **DynamoDB — a table ARN is accepted wherever `TableName` is** — item, query, scan, batch, transaction and table operations answered `ValidationException` or `ResourceNotFoundException` for the table's ARN. Batch responses keep the key form the request used. Contributed by @valeryan.
- **Gateway — the soft open-file limit is raised to the hard limit at startup** — a burst of about a thousand client connections exhausted the default 1024 descriptors. Reported by @fabio-andre-rodrigues.
- **KMS — `ListAliases` entries carry `CreationDate` and `LastUpdatedDate`** — both were missing; `UpdateAlias` moves `LastUpdatedDate` and keeps `CreationDate`. Reported by @DaviReisVieira.
- **Lambda — `ReservedConcurrentExecutions` is shared across versions** — `$LATEST` and published versions each counted separately and could exceed the reservation together. Contributed by @jgrumboe.
- **S3 — event notification records match S3's** — the key is URL-encoded, `versionId` and a growing `sequencer` are set, `eventVersion` is `2.6`, a delete marker is `ObjectRemoved:DeleteMarkerCreated`, and `DeleteObjects` sends one event per object it removes. Contributed by @BoLaMN.
- **S3 — notification destinations are validated at PUT time** — under `AUTH=true`, `PutBucketNotificationConfiguration` fails with `InvalidArgument` when an SQS/SNS destination's policy does not let S3 publish; cross-account destinations are accepted. Contributed by @AdrianAcala.
- **SQS/SNS — queue and topic policies are enforced under `AUTH=true`** — they were stored but never evaluated. Cross-account calls need an explicit Allow (`AccessDenied` for SQS, `AuthorizationError` for SNS), and S3 and SNS deliveries need the destination policy to allow them. With `AUTH=false` nothing changes. Contributed by @AdrianAcala.
- **STS — `GetCallerIdentity` resolves IAM-user callers** — keys from `CreateAccessKey` reported `root`; they now return the user's ARN and ID. Contributed by @AdrianAcala.
- **STS — an unknown access key answers `InvalidClientTokenId`** — under `AUTH=true`, STS answered `UnrecognizedClientException`.

## [1.5.17] — 2026-09-25

### Added

- **Lambda MicroVMs — images build and run in Docker** — with Docker available, `CreateMicrovmImage` / `UpdateMicrovmImage` build the `codeArtifact` Dockerfile (`CREATING` → `CREATED` / `CREATE_FAILED`), call the `/ready` and `/validate` hooks, and `RunMicrovm` runs the image. Only the disk is snapshotted. Contributed by @edersonbrilhante.
- **CloudFront — public keys and key groups** — `CreatePublicKey`, `GetPublicKey`, `GetPublicKeyConfig`, `UpdatePublicKey`, `DeletePublicKey`, `ListPublicKeys` and the same six for key groups, with `PublicKeyInUse` / `ResourceInUse` on delete. Contributed by @fabio-andre-rodrigues.
- **Step Functions — execution history pagination** — `GetExecutionHistory` pages with `nextToken`, and `includeExecutionData=false` drops input and output. Contributed by @jayjanssen.
- **CloudFormation — more types update in place** — AppConfig, AppSync GraphQL APIs and API keys, Auto Scaling groups, policies and scheduled actions, VPCs, subnets, security groups, launch templates, internet gateways, route tables and routes keep their id on a No interruption change instead of being recreated. Contributed by @iot-rocket.

### Fixed

- **CloudFormation — import change sets are validated and refused** — `IMPORT` change sets are validated and listed as `Import` changes; executing one is refused. Every change carries `Type: Resource`. Contributed by @iot-rocket.
- **SSM — `GetParameter` reads `name:version` and `name:label`** — they answered `ParameterNotFound`; a missing version or label is `ParameterVersionNotFound`. Reported by @lobodpav.
- **Lambda — `ReservedConcurrentExecutions=0` throttles every invoke** — zero was treated as unset. Contributed by @AdrianAcala.
- **Step Functions — Secrets Manager `SecretBinary`** — the SDK integration passes literal text instead of base64. Contributed by @jayjanssen.
- **Auto Scaling — scaling policies and scheduled actions keep their members** — target tracking, step and predictive settings, `StartTime`, `EndTime` and `TimeZone` were dropped. Contributed by @iot-rocket.
- **EC2 — subnets, launch templates and route tables answer their attributes** — subnet DNS and IPv6 attributes, launch template `tagSet` and newest-first versions, and every route destination and target. Contributed by @iot-rocket.
- **CloudFormation — a failed update rolls back its in-place changes** — changed resources are reverted, and a failed revert lands in `UPDATE_ROLLBACK_FAILED`. Contributed by @iot-rocket.
- **CloudFormation — ECS cluster settings and stack tags** — `ClusterSettings`, `DefaultCapacityProviderStrategy` and `Configuration` read back, and EC2 network resources carry the stack's tags. Contributed by @iot-rocket.

## [1.5.16] — 2026-09-23

### Added

- **Organizations — member accounts, service control policies and attachments** — `CreateAccount`, `DescribeCreateAccountStatus`, `MoveAccount`, `CloseAccount`, `CreatePolicy`, `DescribePolicy`, `UpdatePolicy`, `DeletePolicy`, `ListPolicies`, `AttachPolicy`, `DetachPolicy`, `ListPoliciesForTarget`, `ListTargetsForPolicy`, `EnablePolicyType` and `DisablePolicyType`, so `aws_organizations_account`, `aws_organizations_policy` and `aws_organizations_policy_attachment` apply. `CreateAccount` answers with a `CreateAccountStatus` whose id the provider reads back through `DescribeCreateAccountStatus`, as on AWS. A root carries `SERVICE_CONTROL_POLICY` enabled and the AWS-managed `p-FullAWSAccess`, attaching a policy whose type the root has disabled is `PolicyTypeNotEnabledException`, and deleting an attached policy is `PolicyInUseException`. Reported by @rv0lt.
- **SNS — direct-to-phone publishes can be read back** — a `Publish` with a `PhoneNumber` and no `TopicArn` is recorded and served at `GET /_ministack/sns/sms-messages`, filterable by `account`, `region` and `phoneNumber`. Contributed by @himangshuj.
- **Lambda MicroVMs — image lifecycle** — `ListMicrovmImages`, `GetMicrovmImage`, `GetMicrovmImageVersion` and `UpdateMicrovmImage`. Contributed by @edersonbrilhante.

### Fixed

- **S3 — a versioned object keeps its history across a restart** — with `S3_PERSIST=1` a delete marker was lost on restart, so a deleted object came back. Every version and delete marker now persists with the object on disk, each version keeps its own bytes, tags and ACL, and object tags and ACLs survive a restart for unversioned objects too. Contributed by @pauloRohling.
- **ECS — service deployments track task health and roll back** — a completed deployment drains the previous task definition's tasks, `deploymentCircuitBreaker` fails a deployment whose tasks keep stopping and, with `rollback`, restores the previous one, and replacement stays within `maximumPercent` while keeping `minimumHealthyPercent` of `desiredCount` running. Contributed by @jgrumboe.
- **CloudFormation — change sets report which property edits replace a resource** — every property edit answered `Replacement: Conditional` and `RequiresRecreation: Conditionally`. For 21 resource types a create-only property is now `Always` with `Replacement: True`, a conditionally create-only one stays `Conditionally`, and every other property is `Never`. Contributed by @iot-rocket.
- **EC2 — Elastic IP tags and IPv6 network ACL entries survive a read** — `DescribeAddresses` omitted EIP tags, so Terraform repeatedly planned `tags` and `tags_all`; network ACL entries always stored and returned an IPv4 CIDR, so an IPv6 rule was read back as a changed IPv4 rule on every plan. Tags and `Ipv6CidrBlock` now round-trip through the EC2 API, and `ReleaseAddress` drops the address's tags. Contributed by @edersonbrilhante.
- **Bedrock — a proxied tool-call turn reports its real token usage** — with `MINISTACK_BEDROCK_PROXY_URL` set, `Converse` and `ConverseStream` estimated usage from the reply text, so a turn that returned only a `toolUse` block reported `outputTokens: 0`, and `inputTokens` ignored the `toolConfig`. Usage now comes from the proxy's own `prompt_tokens` and `completion_tokens`, with the estimate kept for a proxy that sends none. Reported by @Vidminas.

### Internal

- **CI — one Docker preview comment per PR** — the preview-image workflow updates a single comment instead of posting one per push. Contributed by @jgrumboe.
- **Tests** — split test files folded into their service's file.

## [1.5.15] — 2026-09-22

### Added

- **ECS — `awslogs` container output reaches CloudWatch Logs** — a task definition's `awslogs` configuration was stored and ignored, so a Docker-backed `RunTask` container's output went nowhere. Lines now reach the configured group, on a stream named `<prefix>/<container>/<task-id>` or after the container id without a prefix, in `awslogs-region`. Contributed by @rszabo50.
- **RDS — an internal broker answers IAM database authentication** — `POST /_ministack/rds/iam-auth` verifies a token against a process-local capability bound to one endpoint, so the MySQL plugin can decide a login. Capabilities are never persisted or issued over HTTP, and nothing calls the endpoint yet. Contributed by @Areson.

### Fixed

- **KMS — imported key material for HMAC and asymmetric keys** — `CreateKey` with `Origin=EXTERNAL` refused every spec but `SYMMETRIC_DEFAULT`, where AWS supports imported material for symmetric encryption, HMAC and asymmetric keys, ML-DSA excepted. HMAC material is the raw bytes of the spec's length and asymmetric material is the private key alone, DER-encoded PKCS#8, from which the public key is derived; material that does not match the key's `KeySpec` answers `IncorrectKeyMaterialException`, and `GetPublicKey` on a key still awaiting material answers `KMSInvalidStateException`. Reported by @guymahieu.
- **RDS — the server certificate carries the endpoint AWS puts in it** — certificate generation raised `ValueError` once an endpoint passed the X.509 64-byte common-name bound, so a database with a long identifier could not start. The common name is now the advertised endpoint at its full length, the subject carries `OU=RDS, O=Amazon.com, L=Seattle, ST=Washington, C=US`, and the signing CA is named `Amazon RDS <region> Root CA RSA2048 G1`, matching a certificate captured from a real instance. Contributed by @jayjanssen.
- **API Gateway — an OpenAPI body's `securityDefinitions` reach the methods** — the import discarded security outright, so a SAM API declaring a Cognito authorizer created none and every method imported as `authorizationType: NONE`, serving anonymous callers where AWS answers 401. Each scheme carrying `x-amazon-apigateway-authorizer` now becomes an authorizer, and an operation's `security` — or the document's — sets the method's `authorizationType`, `authorizerId` and `authorizationScopes`, so the enforcement added in 1.5.10 engages for body-defined APIs. Contributed by @maximoosemine.
- **CloudFormation — `AWS::ApiGateway::Stage` method settings reach the stage as a map** — the template's `MethodSettings` list was stored verbatim, so the throttling lookup added in 1.5.14 raised on it and every request to a CloudFormation- or SAM-deployed API answered 500. The list is now keyed `"<resourcePath>/<httpMethod>"`, `"*/*"` for the stage-wide entry, over the account-level defaults AWS reports from `GetStage`. Contributed by @maximoosemine.
- **DynamoDB — a table restored outside the current scope keeps working** — startup restore walked the account-scoped store through `values()`, which filters to the request's scope, so another tenant's tables kept the plain dict JSON returns and their first `UpdateItem` raised `KeyError`. Contributed by @ihmpavel.
- **Lambda MicroVMs — requests reach the MicroVM surface** — the AWS CLI signs `/2025-09-09/microvms` and `/2025-09-09/microvm-images` with credential scope `lambda`, so Lambda's function router read the versioned path as a function name. The path now selects MicroVMs first. Contributed by @edersonbrilhante.
- **IoT — the mTLS broker certificate is issued for the endpoint it serves** — its common name was `Ministack IoT Broker` and its SANs covered only `localhost` and the host's addresses, so a client verifying the ATS endpoint hostname refused the handshake. It now carries `*.iot.<region>.<host>` as common name and first SAN.

### Internal

- **CI — control-plane and data-plane tests run in separate lanes** — live-container tests shared the lane with everything else, so a Docker failure looked like a service failure. They now carry a `data_plane` marker and run on their own isolated network, while the control-plane lane runs with no daemon reachable. No user-visible behavior changes. Contributed by @jgrumboe.
- **Contributor guidance** — `CONTRIBUTING.md` documents the three test lanes and the commands that select them, and a new `AGENTS.md` points AI coding agents at the repository layout and the expectations before a behavior change. Contributed by @jgrumboe.

## [1.5.14] — 2026-09-20

### Added

- **KMS — imported key material** — a key created with `Origin=EXTERNAL` stayed unusable because there was no way to supply its material. `GetParametersForImport`, `ImportKeyMaterial` and `DeleteImportedKeyMaterial` now complete the flow: the key waits in `PendingImport`, all five wrapping algorithms unwrap, and `ExpirationModel` and `ValidTo` are reported on the key. Reported by @guymahieu.
- **API Gateway — REST request validators, API keys, throttling and quotas are enforced** — the five `RequestValidator` operations are implemented, a method's `requestValidatorId` is applied to the body and parameters, and an API key is checked against its usage plan's rate, burst and quota. Reported by @iot-rocket.
- **CloudFormation — update handlers for twelve more types** — a stack update of these types re-ran the create, so the resource came back under a new id or the change was lost. Each now updates in place where its reference allows, replaces otherwise, and refuses to replace a custom-named resource. Contributed by @iot-rocket.
- **RDS — resource-bound IAM database authentication** — a verified token is now authorized against the policy's resource, so a grant scoped to one database user stops matching another. Contributed by @Areson.
- **RDS — PostgreSQL instances serve TLS** — a generated CA signs a server certificate for each container, so a client connecting with `sslmode=verify-full` completes the handshake instead of failing. `GET /_ministack/rds/ca.pem` returns that CA, the local stand-in for AWS's certificate bundle. Reported by @jayjanssen.
- **Bedrock Agent Runtime — `Retrieve` applies `vectorSearchConfiguration.filter`** — the filter was parsed and ignored, so every query returned the whole knowledge base. All eleven comparators plus `andAll` and `orAll` now select against the document's metadata, read from its `.metadata.json` sidecar. Reported by @Vidminas.
- **Bedrock Runtime — `toolConfig` reaches the backing model** — a `Converse` call carrying tools was proxied without them, so the model could never answer with a tool use. Tools are forwarded, and a tool call comes back as a `toolUse` block, streamed as `contentBlockStart` when the caller streams. Reported by @Vidminas.
- **IoT — a job execution times out** — `timeoutConfig.inProgressTimeoutInMinutes` and the device's own `stepTimeoutInMinutes` were stored and ignored, so an execution stayed `IN_PROGRESS` for ever. An execution past its timeout is now `TIMED_OUT`, and the device plane reports `approximateSecondsBeforeTimedOut` while it runs.
- **Health — the IoT mTLS listener is reported** — `/_ministack/health` carries `iot_mtls`, and `/_ministack/ready` waits for that listener, so a consumer polling readiness no longer races the MQTT port.

### Changed

- **IAM — wildcard matching is case-sensitive where AWS makes it case-sensitive** — `Resource`, `NotResource`, `StringLike`, `StringNotLike`, `ArnEquals` and `ArnLike` compared case-insensitively, so a policy naming `arn:aws:s3:::MyBucket` also matched `mybucket`. Only `Action` stays case-insensitive, as on AWS.
- **SQS — the default `MaximumMessageSize` is 1 MiB** — queues were created with AWS's old 256 KiB default, so a message between 256 KiB and 1 MiB was rejected where AWS accepts it. The bound accepted by `SetQueueAttributes` moves with it.

### Removed

- **Four timing environment variables** — `TRANSCRIBE_JOB_RUN_SECONDS`, `GLUE_CRAWLER_RUN_SECONDS`, `MINISTACK_DDB_IMPORT_COMPLETE_AFTER_SEC` and `LAMBDA_STATE_TRANSITION_SECONDS` paced emulator-only state transitions and had no AWS counterpart. The transitions keep their previous default pacing.

### Fixed

- **EventBridge — dynamic HTTP parameters for API destinations** — `HttpParameters` JSON paths went to the endpoint as literal `$.` strings. They now resolve against the original event, before input transformation, array indexes and wildcards included. Contributed by @dmgarland.
- **RDS — restarting a cluster keeps its DNS endpoint** — a restart reported the container's IP in place of the `*.rds.amazonaws.com` name handed out at creation, breaking connection strings and hostname-verified TLS. The restart now reports the registered network alias, and the reader endpoint derives its `cluster-ro-` name only when that name was registered. Contributed by @jayjanssen.
- **RDS Data — a PostgreSQL `SELECT` reports the rows it returned** — `numberOfRecordsUpdated` carried the row count for a read, where AWS reports `0` and leaves the rows in `records`. Contributed by @jayjanssen.
- **IoT — the registered-certificate event comes from the device connect** — `RegisterCertificate` published it, and an mTLS connect with an unknown certificate got CONNACK 5 with nothing created. AWS creates the certificate `PENDING_ACTIVATION` on the connect, publishes the event with `sourceIp`, and closes without a CONNACK. Contributed by @iot-rocket.
- **IoT — `DescribeEndpoint` refuses the retired `iot:Data` and `iot:Jobs` types** — both returned a hostname, so a client could keep using an endpoint type AWS no longer serves. AWS answers `InvalidRequestException` 400 and points to `iot:Data-ATS`; both types now get the same errors and messages. Contributed by @iot-rocket.
- **IoT Data — HTTPS `Publish` refuses the topics AWS refuses** — any `$` topic was accepted, so a caller could forge `$aws/events/...` lifecycle events. Reserved topics now answer `InvalidRequestException` 400 "Topic can't start with $", and the MQTT-only jobs, Device Defender and commands topics "Invalid publish to restricted topic using HTTP". Contributed by @iot-rocket.
- **CloudFormation — `AWS::ECR::Repository` keeps what the template declares** — scanning and encryption read back empty, lifecycle and repository policies and tags were dropped, `PutImage` failed, and a delete left images behind. The resource now goes through the ECR store. Contributed by @iot-rocket.
- **CloudFormation — a rolled-back replacement keeps the old resource** — the predecessor was deleted as soon as the replacement existed, so a later failure rolled back to a resource that was gone. It is now deleted after the update succeeds, and a rollback deletes only the replacement. Contributed by @iot-rocket.
- **API Gateway — REST API gateway responses reach the data plane** — a customized response was stored and never applied, so a 401 or 403 carried none of the CORS headers a CDK stack declares. Gateway errors now resolve their type, then `DEFAULT_4XX`/`DEFAULT_5XX`, then the built-in default, with header mappings and `x-amzn-ErrorType`. Contributed by @iot-rocket.
- **API Gateway — REST API gateway errors answer with the AWS status and message** — invented statuses and exception text went out. A failed authorizer is `500` `AUTHORIZER_FAILURE` with a null message, a missing backend `500` `API_CONFIGURATION_ERROR` `Internal server error`, and an unknown stage `403` `Forbidden`. Contributed by @iot-rocket.
- **API Gateway — a MOCK integration honours its `statusCode`** — the `200` integration response was used whatever the request template set, so a MOCK method modelling an error returned `200`. The template's `statusCode` now selects the integration response by `selectionPattern`. Contributed by @iot-rocket.
- **AppConfig — `GetLatestConfiguration` serves a feature-flag profile in retrieval-time format** — the stored `{flags, values, version}` document was returned verbatim, so a client reading a flag found nothing at the top level. A `AWS.AppConfig.FeatureFlags` profile is now flattened to its `values`, disabled flags included. Reported by @dk-tanio.
- **Bedrock AgentCore — timestamps are ISO 8601 strings everywhere** — `CreateAgentRuntime` answered with a JSON number while `ListAgentRuntimes` answered with a string for the same field, so a typed SDK failed on one of the two. Both now match the model's `iso8601` format. Reported by @ykalemi.
- **S3 — an omitted notification configuration `Id` is generated** — a configuration sent without one read back without one, where AWS assigns base64 of a UUID. Reported by @jin-gizmo.
- **S3 — a notification configuration is stored in the form the wire uses** — the request XML was echoed back, so a configuration sent with `LambdaFunctionConfigurations` read back empty for a client expecting the wire name `CloudFunctionConfiguration`, which also hid a CloudFormation-declared Lambda notification. Reported by @jin-gizmo.
- **CloudFormation — `AWS::WAFv2::WebACL` returns the `Ref` AWS returns** — the bare id came back where AWS returns `name|id|scope`, so a template passing the `Ref` to another resource passed an unusable value.
- **Cognito — the OIDC discovery document follows the gateway's scheme** — every endpoint in it was `http` even with `USE_SSL=1`, so the document contradicted its own `https` issuer and a discovery client refused it. Reported by @epcap90.
- **IoT — rule SQL accepts the grammar AWS accepts** — `!=` and `==` were evaluated, where AWS refuses both, and a nested `SELECT` in a `WHERE` clause was accepted, where AWS answers `Unexpected token, 'SELECT'`. `clientid()` now resolves to `n/a` for a publish that did not arrive over MQTT rather than dropping the field from the projection.
- **IoT — the rule error action document matches AWS** — a failure was reported as `{action, errorMessage}`, where AWS names `failedAction` (`DynamoDBv2Action`, `SnsAction`) and `failedResource` (the table, topic, queue or function the action targeted). `clientId` is `N/A` for a non-MQTT publish rather than empty, and the document carries `sourceIp`.
- **IoT — `CreateJob` validates what AWS validates** — a `schedulingConfig` time with seconds or a `Z`, an `inProgressTimeoutInMinutes` outside 1 to 10080, a document over 32768 characters, a `documentSource` outside 1 to 1350, and a request naming neither a document nor a source were all accepted. Each now answers `InvalidRequestException` with AWS's own sentence.
- **IoT — `DescribeJob` echoes the members it accepted** — `abortConfig`, `timeoutConfig`, `schedulingConfig`, `jobExecutionsRetryConfig`, `namespaceId`, `jobTemplateArn`, `documentParameters` and `destinationPackageVersions` were stored and never reported, and `isConcurrent` and `forceCanceled` were missing. Job timestamps are whole epoch seconds, not fractions.
- **IoT Jobs data — two device-plane answers match AWS** — `UpdateJobExecution` with a status a device cannot set answered `InvalidRequestException` 400 instead of `InvalidStateTransitionException` 409, and an `executionNumber` naming an execution that does not exist was ignored instead of answering `ResourceNotFoundException` 404.
- **Auto Scaling — a duplicate group or launch configuration returns `AlreadyExists`** — the wire code was `AlreadyExistsFault`, the shape name rather than the code, so an SDK caught nothing.
- **CloudFormation — `ExecuteChangeSet` returns `InvalidChangeSetStatus`** — the shape name `InvalidChangeSetStatusException` went out as the error code.
- **CloudFormation — the Rules section refuses `Fn::If`** — it was evaluated because the reference lists it, while a real account answers `Following functions are not supported in the Rules block of the template: [Fn::If]`.
- **CloudFormation — the 60 dynamic references per template quota is enforced** — a template over the limit was provisioned instead of being refused.
- **Lambda — an AWS-published extension layer resolves** — a template referencing `LambdaInsightsExtension`, the Parameters and Secrets, AppConfig or OpenTelemetry extensions or `AWSSDKPandas` could not deploy, because the publisher account is unknown offline. It now resolves by layer name with `CodeSize` 0, so the extension does not run.
- **EC2 — a bare `-e NAME` docker flag takes the host value** — it was passed through as an empty string, where docker either forwards the host's value or omits the variable.
- **S3 Control — an unknown `/v20180820` path answers `InvalidURI`** — it fell through to a generic answer instead of AWS's 400 inside an `<ErrorResponse>` echoing the bad segment.
- **Signer — the `clientRequestToken` replay cache is bounded** — every distinct token was kept for ever and persisted with the service state.
- **CloudFormation — `DeletionPolicy: Snapshot` takes a snapshot** — the policy was read and never acted on, so a resource declaring it was deleted with nothing kept, and an RDS cluster or standalone instance, which AWS defaults to `Snapshot`, defaulted to `Delete` here. Both now snapshot the resource and then delete it, on a stack delete and on an update that removes it from the template.

## [1.5.13] — 2