May 15, 2026 · v1.3.40
Cognito's invitation and verification mail now flows through the in-process SES emulator. Step Functions gains JSONata variable assignment with $name references across states. Users can sign in to Cognito by email, phone number, or preferred username alias.
Five Cognito operations — AdminCreateUser, SignUp, ResendConfirmationCode, ForgotPassword, AdminResetUserPassword — now hand their welcome / temporary-password / verification mail to MiniStack's in-process SES emulator. Tests and simulated apps see the message at /_ministack/ses/messages, and when SMTP_HOST is set the same message relays via SMTP, so end-to-end mail flows work locally.
AWS behaviour mirrored throughout: MessageAction=SUPPRESS skips delivery, RESEND re-sends, DesiredDeliveryMediums=["SMS"] excludes email, and the standard placeholders ({username}, {####}) expand in InviteMessageTemplate / VerificationMessageTemplate. Sender resolves to the pool's EmailConfiguration.From, falling back to no-reply@verificationemail.com — the same default real Cognito uses for COGNITO_DEFAULT sending. Override globally via COGNITO_DEFAULT_FROM, or short-circuit delivery entirely with COGNITO_EMAIL_ENABLED=false.
The previously-missing ResendConfirmationCode action ships alongside, wiring the same delivery path. SES gains a send_internal_email() entry point so any future in-process emitter (SNS-via-email subs, alert notifications) can push mail through the same recorder. Contributed by @kjdev.
Assign + workflow variablesJSONata state machines can now bind values into an execution-scoped variable store and reference them in later expressions. State-level Assign evaluates each entry after the state's Output is computed; downstream JSONata expressions resolve $name against the store, with dotted-path access ($user.email) on object values.
{
"QueryLanguage": "JSONata",
"StartAt": "SetVars",
"States": {
"SetVars": {
"Type": "Pass",
"Output": { "items": ["a", "b"] },
"Assign": { "myList": "{% $states.result.items %}" },
"Next": "CheckList"
},
"CheckList": {
"Type": "Choice",
"Choices": [{ "Next": "HasItems", "Condition": "{% $count($myList) > 0 %}" }],
"Default": "Empty"
},
"HasItems": { "Type": "Succeed" },
"Empty": { "Type": "Succeed" }
}
}
Per AWS semantics: Pass $states.result is the state's computed Output; Task $states.result is the raw API result (pre-Output); Catch handlers expose $states.errorOutput. Undefined variable references surface as States.QueryEvaluationError — the exact AWS error code for JSONata evaluation failures, so existing Retry / Catch wiring continues to work. Reported by @youngkwangk.
Cognito user pools support three configured ways to address a user beyond the literal Username: the modern UsernameAttributes (email or phone_number) and the older AliasAttributes (the same plus preferred_username). MiniStack accepted both fields on CreateUserPool but every user-lookup path called pool["_users"].get(username) directly, so signing in with an alias raised UserNotFoundException even when the alias was correctly configured.
The lookup helper now honors both fields: signing in by email / phone_number requires the corresponding email_verified / phone_number_verified attribute to equal "true" (matches real AWS); preferred_username has no verification gate. AdminInitiateAuth, InitiateAuth, AdminRespondToAuthChallenge, RespondToAuthChallenge, ConfirmSignUp, ForgotPassword, ConfirmForgotPassword, and the hosted-UI /login form all flow through the alias-aware resolver; internal sites that need the canonical username (group iteration, create-time uniqueness, post-code token issuance) are left untouched. Contributed by @rjmackay.
RedrivePolicy validationA double-JSON-encoded RedrivePolicy (a common jq tostring mistake or unusual SDK serialization) used to slip past CreateQueue / SetQueueAttributes and then crash ReceiveMessage on the affected queue with 'str' object has no attribute 'get': json.loads of a doubly-encoded string returns a string (not a dict), and the dead-letter-queue sweep then called .get(...) on it.
MiniStack now validates RedrivePolicy at intake — parseable JSON object with a non-empty deadLetterTargetArn string and a numeric maxReceiveCount between 1 and 1000 — and rejects malformed values with 400 InvalidAttributeValue, matching real AWS. The receive path carries a defensive guard so legacy persisted state from older MiniStack versions doesn't crash. Reported by @rbonestell.
if_not_exists arithmetic in SETSET v = (if_not_exists(v, :d) - :amt) previously dropped the arithmetic entirely and assigned the resolved value directly: the outer parens kept every token at depth > 0, so the top-level operator scan never saw the - at depth 0. The update expression evaluator now strips a single layer of matched outer parens before the operator scan, guarded so two adjacent groups like (a) + (b) aren't accidentally flattened. Reported by @youngkwangk.
Real S3 accepts two ARN-tag shapes for Lambda-targeted bucket notifications: the legacy <CloudFunction> form (what botocore wire-serializes LambdaFunctionArn as) and the modern <LambdaFunctionArn> form documented in the S3 API reference. MiniStack only recognised the legacy form, so any SDK that emits the modern shape — AWS SDK for Java v2, Go SDK, Terraform's aws_s3_bucket_notification, hand-crafted XML — had its configuration silently dropped: put_bucket_notification_configuration returned 200 but uploads never invoked the Lambda. Both shapes are now accepted, matching real S3.
docker pull ministackorg/ministack:1.3.40 docker run -d -p 4566:4566 ministackorg/ministack:1.3.40
Or pin in compose.yaml:
services:
ministack:
image: ministackorg/ministack:1.3.40
ports:
- "4566:4566"
Shipped by the MiniStack community. Contributions credited throughout. GitHub · r/ministack