June 5, 2026 · v1.3.59
v1.3.58 fixed Lambda layer attachment for the in-process warm worker; v1.3.59 closes the same gap for the docker executor (the path used when ministack itself runs in a container). AppSync gains the full AWS-standard AppSyncResolverEvent for AWS_LAMBDA data sources. API Gateway v2's JWT authorizer now discovers jwks_uri via OIDC for non-Cognito issuers. S3 PutObject checksums survive the round-trip end-to-end (SHA256 / SHA1 / CRC32), the on-disk bucket directory is account-scoped to match the object writer. Glue gains BatchUpdatePartition and two non-default-account fixes (on-disk script resolution + crawler completion). And CloudFormation gains five more AppConfig resource types.
When ministack runs in a container with LAMBDA_EXECUTOR=docker, the layer attach path used Docker's container.put_archive(path, tar) to copy each layer's extracted files into /opt/layer_N. The Docker API requires the destination path to already exist in the container — the base Lambda RIE image has /opt but not /opt/layer_N, so the call returned 404 Not Found ("Could not find the file /opt/layer_0 in container ...") and the container spawn aborted with no layer mounted. The cp now extracts into the existing /opt with arcname=f"layer_{idx}", materialising /opt/layer_N/... straight from the tar. The fix also reaps the docker warm-container pool on UpdateFunctionConfiguration (when worker-affecting fields change) and DeleteFunction, mirroring the in-process worker invalidation from v1.3.58 — otherwise the pool (keyed on acct:func:zip:CodeSha256) reuses a pre-attach container on the next invoke and the layer is never extracted. Reported by @omargr299.
AppSyncResolverEvent for AWS_LAMBDA data sourcesResolvers backed by AWS_LAMBDA previously received only the raw GraphQL arguments as the Lambda event. Real AppSync sends the full AppSyncResolverEvent — consumers reading event.info.fieldName, event.request.headers, event.identity.resolverContext, or event.source all broke against ministack.
Now _resolve_lambda builds the AWS-shape event: arguments, source, request.headers, prev, stash, info.fieldName / info.parentTypeName / info.variables. For AWS_LAMBDA auth, the authorizer Lambda is invoked first and its resolverContext is threaded into the resolver event's identity. The authorizer event itself matches the verbatim AWS shape: authorizationToken, requestHeaders, and requestContext with apiId / accountId / requestId / queryString / operationName / variables. An authorizer that returns isAuthorized:false, an unreachable authorizer Lambda, or an authorizer that raises all surface to the client as the documented UnauthorizedException (HTTP 401). Unhandled resolver exceptions surface as a GraphQL errors entry instead of leaking the RIE error payload as data. Contributed by @AdigaAkhil.
The JWT authorizer hardcoded the JWKS path to {issuer}/.well-known/jwks.json, which 404s for issuers whose keys live elsewhere (Salesforce serves them at /id/keys, Okta at /oauth2/v1/keys). The resolver now fetches {issuer}/.well-known/openid-configuration, reads the published jwks_uri, and caches per-issuer for 2 hours — matching what real AWS API Gateway HTTP APIs do. The Cognito short-circuit (local pool JWKS) is preserved, and the conventional /.well-known/jwks.json path remains the fallback when discovery is unavailable. Contributed by @Pratham2703005.
PutObject checksums survive the round-tripPutObject with ChecksumAlgorithm="SHA256" (or any other) used to drop every x-amz-checksum-* header on the floor; GetObject(ChecksumMode='ENABLED') returned no checksum, so SDK-side integrity checks always failed.
checksums dict. Client-supplied x-amz-checksum-* values are accepted; x-amz-sdk-checksum-algorithm: SHA256 | SHA1 | CRC32 triggers server-side compute; a mismatch between the supplied value and the server-computed one is rejected with BadDigest.CopyObject propagates the source's checksum (or accepts / computes a new algorithm against the copied body when the client requests one).GetObject?versionId=X) return the per-version checksum.Get / HeadObject emit checksum headers only when the request carries x-amz-checksum-mode: ENABLED, and never on 206 Partial Content — a whole-object checksum can't validate a sliced response, and boto3 raises FlexibleChecksumError if it sees one alongside sliced bytes.PutObject with those algorithms is rejected with a clear InvalidRequest pointing at SHA256 / SHA1 / CRC32 as the supported set.Reported by @Guigoz.
CreateBucket persisted its directory at DATA_DIR/<bucket> while every object write goes to the account-scoped DATA_DIR/<account>/<bucket>/<key> (via _object_disk_path). The unscoped makedirs left a spurious empty folder at the data-dir root that no code path ever used, and DeleteBucket left it behind even after the bucket record was gone. _create_bucket / _delete_bucket now scope the directory to the current account, matching the object-write layout end-to-end. Reported by @rsking.
BatchUpdatePartitionCloses the last missing partition action. Matches the AWS shape per-entry — Entries[*].{PartitionValueList, PartitionInput} in, Errors[*].{PartitionValueList, ErrorDetail{ErrorCode, ErrorMessage}} out. Updates the matched partition in place preserving CreationTime and refreshing LastAccessTime; per-entry EntityNotFoundException on a missing partition; request-level EntityNotFoundException on an unknown table. Contributed by @yonatoasis.
StartJobRun script resolution + crawler completion under non-default accountsTwo related bugs, both because account scoping wasn't honored in Glue's background threads: get_account_id() reads a contextvar (default 000000000000), and a bare threading.Thread / threading.Timer doesn't copy the parent context — so background work ran under the default account. _resolve_script built an unscoped on-disk path while S3 persists objects at DATA_DIR/<account>/<bucket>/<key>, so file-backed Glue scripts never resolved; and _finish_crawl ran on a threading.Timer without context, so for any non-default account the account-scoped _crawlers guard missed and the crawler hung in RUNNING forever. The script path now includes get_account_id() to match the canonical writer; the job-run thread and crawler timer are wrapped with contextvars.copy_context().run(...) (the same idiom as stepfunctions.py / rds.py) so the request's account is carried into background work. Contributed by @AdigaAkhil.
Application landed in v1.3.55; the rest of the AppConfig CFN surface lands here. Property names, defaults, Ref returns, and Fn::GetAtt attribute names match the AWS CloudFormation reference verbatim — EnvironmentId; ConfigurationProfileId + KmsKeyArn; VersionNumber; Id; DeploymentNumber + State. HostedConfigurationVersion enforces the optional LatestVersionNumber optimistic-locking token against the current latest version. Deployment tags are stored against the AWS-shape ARN (arn:aws:appconfig:{region}:{account}:application/{app}/environment/{env}/deployment/{num}). A CDK / Terraform template that declares all five (plus Application) with Ref / Fn::GetAtt wiring now deploys end-to-end. Reported by @zdenekmartinec.
docker pull ministackorg/ministack:1.3.59 docker run -d -p 4566:4566 ministackorg/ministack:1.3.59
Or pin in compose.yaml:
services:
ministack:
image: ministackorg/ministack:1.3.59
ports:
- "4566:4566"
Issues and PRs welcome on GitHub. Discussion on r/ministack.