May 30, 2026 · v1.3.53
One real bug fix on Firehose, a deep DynamoDB conformance pass, AWS error-type clean-up across eight more services, and a small IMDS source change so security scanners stop false-flagging. Three reported issues closed.
KinesisStreamAsSource → S3 fan-outDelivery streams created with DeliveryStreamType=KinesisStreamAsSource and an ExtendedS3 / S3 destination now actually consume records from the source Kinesis stream and forward them to S3. Previously the KinesisStreamSourceConfiguration was stored so DescribeDeliveryStream would echo it back, but no consumer ever read the records — anything put into the source Kinesis stream was silently dropped on the Firehose side.
The fan-out fires inline from Kinesis PutRecord / PutRecords (same pattern as SNS→SQS) so it stays deterministic in tests, honors the configured Prefix, and respects DeliveryStartTimestamp — records older than the delivery-stream creation time are skipped, matching the AWS shard-iterator semantics. Delivery is best-effort: a Firehose-side problem can never break the producer's Kinesis write. Reported by @arivazhaganjeganathan-abc.
import boto3
kinesis = boto3.client("kinesis", endpoint_url="http://localhost:4566")
firehose = boto3.client("firehose", endpoint_url="http://localhost:4566")
firehose.create_delivery_stream(
DeliveryStreamName="ingest",
DeliveryStreamType="KinesisStreamAsSource",
KinesisStreamSourceConfiguration={
"KinesisStreamARN": "arn:aws:kinesis:us-east-1:000000000000:stream/events",
"RoleARN": "arn:aws:iam::000000000000:role/firehose",
},
ExtendedS3DestinationConfiguration={
"BucketARN": "arn:aws:s3:::ingest-bucket",
"RoleARN": "arn:aws:iam::000000000000:role/firehose",
"Prefix": "data/",
},
)
kinesis.put_record(StreamName="events", PartitionKey="k", Data=b'{"a":1}')
# Record now lands in s3://ingest-bucket/data/YYYY/MM/DD/HH/<uuid>
30+ message-text fixes so the strings returned by ministack match the strings captured from real AWS by the dynamodb-conformance.org Tier 3 suite. Highlights:
GetItem / PutItem / UpdateItem / DeleteItem / Query / Scan / Batch* / Transact* — all now return "Requested resource not found" (was a ministack-invented "Cannot do operations on a non-existent table").RequestItems on BatchWriteItem / BatchGetItem returns "The requestItems parameter is required."; over-limit batches use the canonical "1 validation error detected: Value ... at 'requestItems' failed to satisfy constraint: Member must have length less than or equal to N" format.KeyConditionExpression / UpdateExpression use the "Invalid {Expression}: The expression cannot be empty;" template; undefined :val / #name references are now scoped to the offending expression (e.g. "Invalid FilterExpression: An expression attribute value used in expression is not defined; attribute value: :v"); ExpressionAttributeValues / ExpressionAttributeNames with no expression use "... can only be specified when using expressions".Scan Segment validation: AWS-exact phrasing for required-but-missing and zero-based-must-be-less-than-TotalSegments.KeySchema / index validation: duplicate-key-name uses "Invalid KeySchema: Some index key attributes are repeated. RepeatedKeyElement:"; KeyType and length errors use the canonical 1 validation error detected: ... format; LSI-on-hash-only-table and duplicate-index-name both use the "One or more parameter values were invalid:" prefix.[PROVISIONED, PAY_PER_REQUEST], [STANDARD, STANDARD_INFREQUENT_ACCESS]) — no Python-set repr or alphabetical reorder.DeleteTable returns "Table is protected against deletion. To delete the table, disable deletion protection.".UpdateTable non-existent GSI returns "Requested resource not found: Index: {idx}".B is now accepted in a non-key attribute, per the AWS data-types reference (only key attributes reject empty binary).ListTagsOfResource on a syntactically-valid but non-existent ARN returns AccessDeniedException — security-through-obscurity, matching the AWS contract. TagResource / UntagResource still return ResourceNotFoundException because they mutate by ARN.Plus two functional bugs surfaced by the same run:
INCLUDE and KEYS_ONLY projections are now enforced on Query and Scan. Items are trimmed to the index's declared NonKeyAttributes plus base + index keys. Previously the full item leaked through index reads.Scan now actually partitions items across segments by deterministically hashing the partition key. Previously every segment returned every item.Each of these was emitting an exception type that doesn't exist in the corresponding botocore service model — meaning strict-deserializer SDKs (Java v2, Go v2) couldn't classify the error. All now return the canonical type per the AWS API reference:
TagResource / UntagResource / ListTagsForResource — per-resource-type errors (FileSystemNotFound 404 for fs-* ARNs, AccessPointNotFound 404 for fsap-*, BadRequest 400 for unrecognised) instead of the generic ResourceNotFound.NoSuchNamespaceException / NoSuchTableException → NotFoundException.ValidationException (was InvalidRequestException).MethodNotAllowedException (405) → BadRequestException (400) on the unsupported-method branch — APIGW v1 doesn't define a 405 exception.InvalidRequest / ServiceAlreadyExists → ClientException (AWS ECS uses ClientException as the client-error catch-all).ClientException (the Batch model exposes only ClientException / ServerException).InvalidRequestException / ResourceAlreadyExistsException → ValidationException (the MWAA model exposes neither).ValidationException (was the invented InvalidPayloadException).ValidationException (was InvalidRequest).Generic except Exception as e blocks were forwarding str(e) as the AWS error message on InvalidCiphertextException, InternalServerException, and InternalServerError. The new responses are opaque per AWS convention — AWS never surfaces internal exception text in 5xx responses.
Credential-pattern secret scanners (e.g. AquaSec) were false-flagging the literal AIPA… InstanceProfileId value in ministack/services/imds.py as a leaked AWS instance-profile ID. The 20-character wire response is unchanged — the source no longer contains a contiguous AIPA… string, so the heuristic stops matching. Reported by @diplomatic-ms.
docker pull ministackorg/ministack:1.3.53 docker run -d -p 4566:4566 ministackorg/ministack:1.3.53
Or pin in compose.yaml:
services:
ministack:
image: ministackorg/ministack:1.3.53
ports:
- "4566:4566"
Issues and PRs welcome on GitHub. Discussion on r/ministack.