
Enhances AWS application development by offering language-level extensions, standardized access to environment variables, and structured logging utilities with built-in serialization for seamless integration.
Kotlin libraries for building AWS applications, with a focus on AWS Lambda. Most modules are Kotlin
Multiplatform and target the JVM and Kotlin/Native (linuxX64, linuxArm64, macosArm64), so the
same handler code runs on the JVM or as a native provided.al2023 Lambda.
Since 3.0, awskt does not use the AWS SDK for Kotlin at runtime. The aws/* modules are a small
hand-written client: SigV4 signing plus a Ktor transport.
Moving from 2.x? Read Migrating from awskt 2.x to 3.x. The coordinates changed, and so did the default credentials chain.
Every release is published to the Steamstreet repository; some are also on Maven Central. Add the repository, limited to the awskt group so that nothing else is looked up there:
repositories {
mavenCentral()
maven("https://repo.steamstreet.com") {
content { includeGroup("com.steamstreet.awskt") }
}
}
dependencies {
implementation("com.steamstreet.awskt:dynamokt:VERSION")
implementation("com.steamstreet.awskt:lambda-api-gateway-ktor:VERSION")
implementation("com.steamstreet.awskt:aws-sqs:VERSION")
// Add other modules as needed
}Artifacts publish as com.steamstreet.awskt:<module> from 3.1.0 on. Versions through 3.0.0 used
com.steamstreet:awskt-<module>; those coordinates are no longer maintained, and 3.0.0 should not be
used. Maven and other non-Gradle builds must name the platform variant of a multiplatform module,
for example dynamokt-jvm.
Requires Kotlin 2.3 and Ktor 3.5 or later. Handlers in lambda-sqs and lambda-sns declare context
parameters, so a build that subclasses them needs -Xcontext-parameters.
Each service module exposes an interface, a factory function that takes a configuration lambda, and
suspending calls. Every service module depends only on aws-core.
val sqs = Sqs { region = "us-west-2" }
sqs.sendMessage(SendMessageRequest(queueUrl = queueUrl, messageBody = body))| Module | Provides |
|---|---|
aws-signing |
SigV4 signing (SigV4, AwsCredentials) |
aws-core |
The signed transport: credentials, endpoints, retries, error mapping, AwsServiceClient
|
aws-dynamodb |
DynamoDb: items, query, scan, batch and transact operations, table create/describe/delete |
aws-eventbridge |
EventBridge.putEvents |
aws-kinesis |
Put records, shards, iterators, get records |
aws-kms |
Encrypt, decrypt, re-encrypt, data keys, random, sign, verify |
aws-lambda |
invoke, invokeWithResponseStream, and listFunctions / listFunctionsPaginated
|
aws-opensearch |
A signed OpenSearch transport: request, search, bulk, getDocument
|
aws-s3 |
Get, put, head and delete object, and S3Presigner
|
aws-scheduler |
EventBridge Scheduler schedules |
aws-secretsmanager |
Get, put and batch-get secret values |
aws-ses |
SES v2 sendEmail
|
aws-sns |
Publish, batch publish, mobile push endpoints and APNs/FCM payloads |
aws-sqs |
Send, receive and delete (single and batch), visibility, queue URLs |
aws-sts |
assumeRole, getCallerIdentity, and AssumeRoleCredentialsProvider
|
aws-bedrock-runtime |
converse and converseStream
|
aws-cloudwatch-logs |
Logs Insights queries |
aws-dynamodb-sdk-adapter (JVM) |
SdkBackedDynamoDb: the DynamoDb interface backed by the SDK's DynamoDbClient
|
aws-sdk-credentials (JVM) |
The AWS SDK's default credential chain for every awskt client |
By default, clients read credentials from environment variables only, which is what Lambda
provides. On ECS or EC2, or on a machine that uses ~/.aws profiles or SSO, add
aws-sdk-credentials and install the SDK's chain once at startup:
AwsCredentialsDefaults.provider = sdkDefaultChainCredentialsProvider()To act as another role, add aws-sts. AssumeRoleCredentialsProvider calls AssumeRole, caches
the result and refreshes it five minutes before it expires. It works on native as well as the JVM,
and can be given to one client or installed for all of them:
AwsCredentialsDefaults.provider =
AssumeRoleCredentialsProvider(AssumeRoleRequest(roleArn, roleSessionName = "reports"))The provider calls STS with credentials from the environment, not from
AwsCredentialsDefaults.provider, so installing it there does not make it its own source.
A client can also be given its own provider through credentialsProvider in its configuration. The
region comes from configuration, AWS_REGION, AWS_DEFAULT_REGION or the aws.region system
property, never from ~/.aws/config.
General Kotlin helpers: asyncLazy, mutableLazy, cached(Duration), whenNotNull,
string and collection extensions, and exception types such as NotFoundException. Also targets JS,
Wasm and iOS.
val config = asyncLazy { loadConfig() } // config.get() suspends
var token: String by cached(5.minutes) { fetchToken() }Reads environment variables through Env and the env() delegate. A value of the form
Secret_<id>[.jsonKey], or a separate Secret_<KEY> variable, is resolved from Secrets Manager: on
the JVM through the AWS SDK, and on native through aws-secretsmanager. Resolved values are not
cached, so read them once. On the JVM only, AppConfig.<app>.<env>.<config>.<key> values are
resolved from AppConfig.
val tableName = Env["TABLE_NAME"] // throws IllegalStateException naming TABLE_NAME if missing
val region: String? = Env.optional("AWS_REGION")
val apiKey by env("API_KEY")Structured JSON logging. The suspending log object carries its context through coroutines:
log.ctx({ "requestId" `is` id }) {
log.info("Processing")
log.data("User created", user, field = "user")
}The non-suspending helpers logInfo, logWarning, logError (each with vararg metadata, and the
last two with a throwable), logValue, logJson and mdcContext are available on every platform:
mdcContext("requestId" to id) {
logInfo("Order placed", "orderId" to order.id)
logValue("Order", "order", order)
}On the JVM they go through slf4j's MDC and the Logstash encoder, and their output is unchanged
from 3.1.5. Everywhere else each call writes one JSON object per line, in JsonLogPublisher's
format. Metadata values are written as strings and nulls are left out, on every platform.
One difference to know about: Kotlin/Native has no ThreadContextElement, so off the JVM a
non-suspending call cannot read its own coroutine's context. mdcContext fields reach the
suspending log exactly, through the coroutine context. The non-suspending helpers read a
process-wide registry of the mdcContext blocks currently running, so two coroutines that run
concurrently under different values can see each other's fields. Nothing outlives its block, and a
native Lambda runs one invocation at a time. The JVM-only extras (logInfo with a builder, the
Logger extensions, logEvent, and the JsonElement-context overloads) remain JVM-only.
JsonObject helpers: copy, copyOrBuild, diff, comprehensiveDiff and deepEquals.
Typed EventBridge publishing. The default poster is an EventBridgeSubmitter built on
aws-eventbridge, configured from the EventBusArn and EventPosterSource environment variables.
@Serializable data class OrderEvent(val orderId: String, val status: String)
val OrderCompleted = eventSchema<OrderEvent>("OrderCompleted")
OrderCompleted.post(OrderEvent("123", "completed"))JWT verification and signing on the JVM and native, where com.auth0:java-jwt cannot run.
Verifies RS256 and ES256 against a cached JWKS, and signs ES256, for example a Sign in with Apple
client secret. Built on cryptography-kotlin: the JDK's providers on the JVM, and a statically linked
OpenSSL 3 on native.
val apple = JwtVerifier(
keys = JwksKeySource("https://appleid.apple.com/auth/keys"),
issuers = setOf("https://appleid.apple.com"),
audiences = setOf("com.example.app"),
)
val userId = apple.verify(idToken).claims.subject
val clientSecret = Es256Signer.fromPrivateKey(p8, keyId = keyId).sign {
issuer = teamId; subject = clientId
audience("https://appleid.apple.com")
issuedAt = now; expiresAt = now + 180.days
}JwtVerifier requires both issuers and an audience rule, and takes the accepted algorithms from
its own configuration, never from the token.
Cognito access tokens carry no aud. They name the app client in client_id and set token_use
to access, and cognitoAccessToken checks both, along with the pool's issuer and keys:
val cognito = JwtVerifier.cognitoAccessToken("us-east-1", "us-east-1_AbCdEf123", setOf(appClientId))
val userId = cognito.verify(accessToken).claims.subjectFor other issuers, JwtAudience.Claim checks a claim other than aud, and requiredClaims requires
exact string values.
A single-table DynamoDB library built on aws-dynamodb. DynamoKt holds the table configuration,
and sessions provide get, put, update, query, scan and transactions over Item and MutableItem,
with typed attribute delegates.
val db = DynamoKt(table = "app", pkName = "pk", skName = "sk")
val session = db.session()
session.put("user#1", "profile") { set("name", "Alice") }
val name = session.get("user#1", "profile").getString("name")AttributeValue and its serializer, and the DynamoDB stream models, shared by dynamokt and
aws-dynamodb.
An Exposed-style typed DSL for DynamoDB: Table, typed columns, Op expressions and Database.
See its own README.
JVM handlers are abstract classes you subclass. Native handlers are main functions built with the
*Lambda { } entry points listed for each module.
The coroutine foundation: lambdaContext, lambdaJson and, on the JVM, the handler base classes
SuspendingLambda, IOLambda<T, R> and InputLambda<T>.
class MyHandler : IOLambda<Request, Response>(Request.serializer(), Response.serializer()) {
override suspend fun handle(input: Request) = Response("ok ${input.id}")
}The Kotlin/Native custom runtime for provided.al2023. Native targets only.
fun main() = nativeLambdaIO<Request, Response> { request -> Response("ok ${request.id}") }nativeLambdaIO and nativeLambdaInput log each event as Request received (payload under
input) and nativeLambdaIO logs each response as Lambda response sent (payload under event),
with the request id, as the JVM handlers do. logIncoming = false, or LogIncomingData=false in the
environment, suppresses the request line unless the handler fails; logOutgoing = false suppresses
the response line.
Request logging never writes credentials for API Gateway or AppSync. redactCredentials (in
lambda-coroutines) replaces the value of any field named Authorization, Cookie, Set-Cookie,
X-Api-Key and the like with [REDACTED], at any depth, and the API Gateway handlers (JVM and
native, Ktor or not) and both AppSync entry points apply it. Pass logRedactor = { redactCredentials(it) }
to nativeLambdaIO or nativeLambdaInput to do the same elsewhere, or override logRedactor on an
IOLambda or InputLambda. Request bodies are strings and are not redacted.
The com.steamstreet.awskt.native-lambda Gradle plugin, from the gradle-plugin included build,
packages a linuxArm64 executable as a Lambda bootstrap zip.
JVM only. @AWSLambdaConstructor, MockLambdaContext for tests, and the aws-lambda-java-core
dependency.
Serializable API Gateway models for REST proxy (ApiGatewayProxyRequest/Response) and HTTP API
v2 (ApiGatewayV2HttpRequest/Response), with JVM base classes ApiGatewayProxyHandler and
ApiGatewayV2HttpHandler.
class ApiHandler : ApiGatewayProxyHandler() {
override suspend fun handle(input: ApiGatewayProxyRequest) =
ApiGatewayProxyResponse(statusCode = 200, body = """{"result":"success"}""")
}Runs a Ktor Application behind API Gateway, REST or HTTP API, with no server engine. JVM:
APIGatewayLambdaServer and APIGatewayV2LambdaServer. Native: apiGatewayKtorLambda { } and
apiGatewayV2KtorLambda { }.
class KtorHandler : APIGatewayLambdaServer() {
override fun Application.module() {
routing { get("/users/{id}") { call.respondText(call.parameters["id"]!!) } }
}
}From 3.1.11. ApiGatewayPrincipal and the ApiGatewayAuth plugin, which make the claims API Gateway
has already verified available as a Ktor principal, on the JVM and on native. It does not validate
tokens itself: its provider accepts every request, and sets a principal with null claims when the
request carried none. It depends on ktor-server-auth only.
apiGatewayKtorLambda {
install(ApiGatewayAuth)
routing {
authenticate("api-gateway-jwt") {
get("/me") {
val principal = call.principal<ApiGatewayPrincipal>()
call.respondText("${principal?.subject} ${principal?.claim("email")} ${principal?.sourceIp}")
}
}
}
}claim(name) returns a string, number or boolean claim as its content string, and null for a claim
that is missing, JSON null, an object or an array. claims is the raw JsonObject. The plugin reads
a Cognito user pool authorizer's claims under a REST API and a JWT authorizer's claims under an HTTP
API (apiGatewayV2KtorLambda).
install(ApiGatewayAuth) { name = "cognito" } changes the provider name, and name = null makes it
the default provider. In a test, mockPrincipal replaces the principal read from the request:
install(ApiGatewayAuth) {
mockPrincipal = { ApiGatewayPrincipal(buildJsonObject { put("sub", "test-user") }) }
}ApiGatewayPrincipal(claims) also wraps claims from another source, such as the response of
Cognito's oauth2/userInfo endpoint.
To move from ApiGatewayJWT, replace the lambda-api-gateway-ktor-jwt dependency with
lambda-api-gateway-ktor-auth and change two names. The provider name is the same, so
authenticate("api-gateway-jwt") { } blocks do not change, and the package is the same
(com.steamstreet.aws.lambda.apigateway.ktor).
// before (JVM only)
install(ApiGatewayJWT)
val user = call.principal<JWTPrincipal>()?.subject
// after (JVM and native)
install(ApiGatewayAuth)
val user = call.principal<ApiGatewayPrincipal>()?.subjectJWTPrincipal |
ApiGatewayPrincipal |
|---|---|
payload.getClaim("email").asString() |
claim("email") |
payload.getClaim("sourceIp").asString() |
sourceIp |
subject, which throws when there is no sub claim |
subject, which is null |
mockPrinciple = { ... }, a JWTPrincipal
|
mockPrincipal = { ... }, an ApiGatewayPrincipal
|
JVM only. JWTPrincipal and the ApiGatewayJWT plugin, which build a Ktor principal from the
Cognito claims API Gateway has already verified. It does not validate tokens itself. For a Lambda
that also runs on native, use lambda-api-gateway-ktor-auth.
class KtorHandler : APIGatewayLambdaServer() {
override fun Application.module() {
install(ApiGatewayJWT)
routing {
authenticate("api-gateway-jwt") {
get("/me") { call.respond(call.principal<JWTPrincipal>()!!.subject) }
}
}
}
}These declarations shipped inside lambda-api-gateway-ktor before 3.0. They are now in this
separate artifact, in the same package:
implementation("com.steamstreet.awskt:lambda-api-gateway-ktor-jwt:VERSION").
Direct-Lambda AppSync resolvers routed by type and field. JVM: appSync(input, output, context) { }.
Native: appSyncLambda { }.
appSync(input, output, context) {
type("Query") {
field("getUser", GetUserArgs.serializer(), User.serializer()) { args -> loadUser(args.id) }
}
}DynamoDB stream handlers that accept direct, Kinesis-wrapped and SQS-redriven batches. JVM:
DynamoStreamHandler and DynamoKtStreamHandler. Native: dynamoStreamLambda,
dynamoStreamBatchLambda and dynamoKtStreamLambda.
class StreamHandler : DynamoStreamHandler() {
override suspend fun handleRecord(record: DynamoStreamEvent) {
when (record.eventName) { "INSERT" -> onInsert(record); "REMOVE" -> onRemove(record) }
}
}Routes EventBridge events by detail-type through an EventSchema. JVM: EventBridgeFunction.
Native: eventBridgeLambda { }.
class OrderHandler : EventBridgeFunction {
override suspend fun EventBridgeHandlerConfig.onEvent() {
on(OrderCompleted) { order -> finalizeOrder(order) }
}
}Kinesis event models and batch processing with partial failures. JVM: KinesisHandler. Native:
kinesisLambda and kinesisBatchLambda.
class Processor : KinesisHandler() {
override suspend fun processRecord(record: KinesisRecord) {
val data = Json.decodeFromString<SensorData>(record.kinesis.decodedData())
}
}Typed or raw SNS message processing. JVM: SNSHandler<T>. Native: snsLambda and snsRawLambda.
class NotificationHandler : SNSHandler<Notification>(Notification.serializer()) {
context(record: SnsRecord)
override suspend fun handleMessage(message: Notification) = sendEmail(message)
}SQS handlers. JVM: SQSRawHandler, SQSHandler<T>, and SQSBatchHandler<T>, which reports
partial batch failures. Native: sqsLambda, sqsBatchLambda and sqsRawLambda.
class QueueProcessor : SQSBatchHandler<Job>(Job.serializer()) {
context(record: SQSRecord)
override suspend fun handleMessage(message: Job) {
executeJob(message) // a throw fails only this record
}
}JVM only. A logback appender that writes through the Lambda runtime logger, and a default
logback.xml that uses it with a JSON encoder. Adding the dependency is enough.
JVM only. A bundle of env, standards, logging, lambda-core and lambda-logging.
Serializable AppSync resolver event models (AppSyncContext, AppSyncIdentity, AppSyncInfo) and
String.appSyncContext().
JVM only. Claims and JsonElementClaim, which read Cognito claims from API Gateway's authorizer
context. This is the support code for lambda-api-gateway-ktor-jwt. On native, ApiGatewayPrincipal
in lambda-api-gateway-ktor-auth does the job of Claims.
JVM only. Local and mock AWS for tests: DynamoLocalBuilder (in-memory DynamoDB Local, exposing an
SDK DynamoDbClient), EventBridgeMock (which implements awskt's EventBridge, with pattern
matching), DynamoStreamRunner, SqsMock, S3Local and LambdaMock.
val dynamo = DynamoLocalBuilder().apply { start() }
val db = DynamoKt("table", builder = { SdkBackedDynamoDb(dynamo.client) })
val events = EventBridgeMock()./gradlew check # every target, tests and ABI dumps
./gradlew :aws:aws-core:allTests # one module
./gradlew publishToMavenLocalReleases are cut with scripts/release.sh; see "Releasing" in AGENTS.md.
MIT. See LICENSE.
Kotlin libraries for building AWS applications, with a focus on AWS Lambda. Most modules are Kotlin
Multiplatform and target the JVM and Kotlin/Native (linuxX64, linuxArm64, macosArm64), so the
same handler code runs on the JVM or as a native provided.al2023 Lambda.
Since 3.0, awskt does not use the AWS SDK for Kotlin at runtime. The aws/* modules are a small
hand-written client: SigV4 signing plus a Ktor transport.
Moving from 2.x? Read Migrating from awskt 2.x to 3.x. The coordinates changed, and so did the default credentials chain.
Every release is published to the Steamstreet repository; some are also on Maven Central. Add the repository, limited to the awskt group so that nothing else is looked up there:
repositories {
mavenCentral()
maven("https://repo.steamstreet.com") {
content { includeGroup("com.steamstreet.awskt") }
}
}
dependencies {
implementation("com.steamstreet.awskt:dynamokt:VERSION")
implementation("com.steamstreet.awskt:lambda-api-gateway-ktor:VERSION")
implementation("com.steamstreet.awskt:aws-sqs:VERSION")
// Add other modules as needed
}Artifacts publish as com.steamstreet.awskt:<module> from 3.1.0 on. Versions through 3.0.0 used
com.steamstreet:awskt-<module>; those coordinates are no longer maintained, and 3.0.0 should not be
used. Maven and other non-Gradle builds must name the platform variant of a multiplatform module,
for example dynamokt-jvm.
Requires Kotlin 2.3 and Ktor 3.5 or later. Handlers in lambda-sqs and lambda-sns declare context
parameters, so a build that subclasses them needs -Xcontext-parameters.
Each service module exposes an interface, a factory function that takes a configuration lambda, and
suspending calls. Every service module depends only on aws-core.
val sqs = Sqs { region = "us-west-2" }
sqs.sendMessage(SendMessageRequest(queueUrl = queueUrl, messageBody = body))| Module | Provides |
|---|---|
aws-signing |
SigV4 signing (SigV4, AwsCredentials) |
aws-core |
The signed transport: credentials, endpoints, retries, error mapping, AwsServiceClient
|
aws-dynamodb |
DynamoDb: items, query, scan, batch and transact operations, table create/describe/delete |
aws-eventbridge |
EventBridge.putEvents |
aws-kinesis |
Put records, shards, iterators, get records |
aws-kms |
Encrypt, decrypt, re-encrypt, data keys, random, sign, verify |
aws-lambda |
invoke, invokeWithResponseStream, and listFunctions / listFunctionsPaginated
|
aws-opensearch |
A signed OpenSearch transport: request, search, bulk, getDocument
|
aws-s3 |
Get, put, head and delete object, and S3Presigner
|
aws-scheduler |
EventBridge Scheduler schedules |
aws-secretsmanager |
Get, put and batch-get secret values |
aws-ses |
SES v2 sendEmail
|
aws-sns |
Publish, batch publish, mobile push endpoints and APNs/FCM payloads |
aws-sqs |
Send, receive and delete (single and batch), visibility, queue URLs |
aws-sts |
assumeRole, getCallerIdentity, and AssumeRoleCredentialsProvider
|
aws-bedrock-runtime |
converse and converseStream
|
aws-cloudwatch-logs |
Logs Insights queries |
aws-dynamodb-sdk-adapter (JVM) |
SdkBackedDynamoDb: the DynamoDb interface backed by the SDK's DynamoDbClient
|
aws-sdk-credentials (JVM) |
The AWS SDK's default credential chain for every awskt client |
By default, clients read credentials from environment variables only, which is what Lambda
provides. On ECS or EC2, or on a machine that uses ~/.aws profiles or SSO, add
aws-sdk-credentials and install the SDK's chain once at startup:
AwsCredentialsDefaults.provider = sdkDefaultChainCredentialsProvider()To act as another role, add aws-sts. AssumeRoleCredentialsProvider calls AssumeRole, caches
the result and refreshes it five minutes before it expires. It works on native as well as the JVM,
and can be given to one client or installed for all of them:
AwsCredentialsDefaults.provider =
AssumeRoleCredentialsProvider(AssumeRoleRequest(roleArn, roleSessionName = "reports"))The provider calls STS with credentials from the environment, not from
AwsCredentialsDefaults.provider, so installing it there does not make it its own source.
A client can also be given its own provider through credentialsProvider in its configuration. The
region comes from configuration, AWS_REGION, AWS_DEFAULT_REGION or the aws.region system
property, never from ~/.aws/config.
General Kotlin helpers: asyncLazy, mutableLazy, cached(Duration), whenNotNull,
string and collection extensions, and exception types such as NotFoundException. Also targets JS,
Wasm and iOS.
val config = asyncLazy { loadConfig() } // config.get() suspends
var token: String by cached(5.minutes) { fetchToken() }Reads environment variables through Env and the env() delegate. A value of the form
Secret_<id>[.jsonKey], or a separate Secret_<KEY> variable, is resolved from Secrets Manager: on
the JVM through the AWS SDK, and on native through aws-secretsmanager. Resolved values are not
cached, so read them once. On the JVM only, AppConfig.<app>.<env>.<config>.<key> values are
resolved from AppConfig.
val tableName = Env["TABLE_NAME"] // throws IllegalStateException naming TABLE_NAME if missing
val region: String? = Env.optional("AWS_REGION")
val apiKey by env("API_KEY")Structured JSON logging. The suspending log object carries its context through coroutines:
log.ctx({ "requestId" `is` id }) {
log.info("Processing")
log.data("User created", user, field = "user")
}The non-suspending helpers logInfo, logWarning, logError (each with vararg metadata, and the
last two with a throwable), logValue, logJson and mdcContext are available on every platform:
mdcContext("requestId" to id) {
logInfo("Order placed", "orderId" to order.id)
logValue("Order", "order", order)
}On the JVM they go through slf4j's MDC and the Logstash encoder, and their output is unchanged
from 3.1.5. Everywhere else each call writes one JSON object per line, in JsonLogPublisher's
format. Metadata values are written as strings and nulls are left out, on every platform.
One difference to know about: Kotlin/Native has no ThreadContextElement, so off the JVM a
non-suspending call cannot read its own coroutine's context. mdcContext fields reach the
suspending log exactly, through the coroutine context. The non-suspending helpers read a
process-wide registry of the mdcContext blocks currently running, so two coroutines that run
concurrently under different values can see each other's fields. Nothing outlives its block, and a
native Lambda runs one invocation at a time. The JVM-only extras (logInfo with a builder, the
Logger extensions, logEvent, and the JsonElement-context overloads) remain JVM-only.
JsonObject helpers: copy, copyOrBuild, diff, comprehensiveDiff and deepEquals.
Typed EventBridge publishing. The default poster is an EventBridgeSubmitter built on
aws-eventbridge, configured from the EventBusArn and EventPosterSource environment variables.
@Serializable data class OrderEvent(val orderId: String, val status: String)
val OrderCompleted = eventSchema<OrderEvent>("OrderCompleted")
OrderCompleted.post(OrderEvent("123", "completed"))JWT verification and signing on the JVM and native, where com.auth0:java-jwt cannot run.
Verifies RS256 and ES256 against a cached JWKS, and signs ES256, for example a Sign in with Apple
client secret. Built on cryptography-kotlin: the JDK's providers on the JVM, and a statically linked
OpenSSL 3 on native.
val apple = JwtVerifier(
keys = JwksKeySource("https://appleid.apple.com/auth/keys"),
issuers = setOf("https://appleid.apple.com"),
audiences = setOf("com.example.app"),
)
val userId = apple.verify(idToken).claims.subject
val clientSecret = Es256Signer.fromPrivateKey(p8, keyId = keyId).sign {
issuer = teamId; subject = clientId
audience("https://appleid.apple.com")
issuedAt = now; expiresAt = now + 180.days
}JwtVerifier requires both issuers and an audience rule, and takes the accepted algorithms from
its own configuration, never from the token.
Cognito access tokens carry no aud. They name the app client in client_id and set token_use
to access, and cognitoAccessToken checks both, along with the pool's issuer and keys:
val cognito = JwtVerifier.cognitoAccessToken("us-east-1", "us-east-1_AbCdEf123", setOf(appClientId))
val userId = cognito.verify(accessToken).claims.subjectFor other issuers, JwtAudience.Claim checks a claim other than aud, and requiredClaims requires
exact string values.
A single-table DynamoDB library built on aws-dynamodb. DynamoKt holds the table configuration,
and sessions provide get, put, update, query, scan and transactions over Item and MutableItem,
with typed attribute delegates.
val db = DynamoKt(table = "app", pkName = "pk", skName = "sk")
val session = db.session()
session.put("user#1", "profile") { set("name", "Alice") }
val name = session.get("user#1", "profile").getString("name")AttributeValue and its serializer, and the DynamoDB stream models, shared by dynamokt and
aws-dynamodb.
An Exposed-style typed DSL for DynamoDB: Table, typed columns, Op expressions and Database.
See its own README.
JVM handlers are abstract classes you subclass. Native handlers are main functions built with the
*Lambda { } entry points listed for each module.
The coroutine foundation: lambdaContext, lambdaJson and, on the JVM, the handler base classes
SuspendingLambda, IOLambda<T, R> and InputLambda<T>.
class MyHandler : IOLambda<Request, Response>(Request.serializer(), Response.serializer()) {
override suspend fun handle(input: Request) = Response("ok ${input.id}")
}The Kotlin/Native custom runtime for provided.al2023. Native targets only.
fun main() = nativeLambdaIO<Request, Response> { request -> Response("ok ${request.id}") }nativeLambdaIO and nativeLambdaInput log each event as Request received (payload under
input) and nativeLambdaIO logs each response as Lambda response sent (payload under event),
with the request id, as the JVM handlers do. logIncoming = false, or LogIncomingData=false in the
environment, suppresses the request line unless the handler fails; logOutgoing = false suppresses
the response line.
Request logging never writes credentials for API Gateway or AppSync. redactCredentials (in
lambda-coroutines) replaces the value of any field named Authorization, Cookie, Set-Cookie,
X-Api-Key and the like with [REDACTED], at any depth, and the API Gateway handlers (JVM and
native, Ktor or not) and both AppSync entry points apply it. Pass logRedactor = { redactCredentials(it) }
to nativeLambdaIO or nativeLambdaInput to do the same elsewhere, or override logRedactor on an
IOLambda or InputLambda. Request bodies are strings and are not redacted.
The com.steamstreet.awskt.native-lambda Gradle plugin, from the gradle-plugin included build,
packages a linuxArm64 executable as a Lambda bootstrap zip.
JVM only. @AWSLambdaConstructor, MockLambdaContext for tests, and the aws-lambda-java-core
dependency.
Serializable API Gateway models for REST proxy (ApiGatewayProxyRequest/Response) and HTTP API
v2 (ApiGatewayV2HttpRequest/Response), with JVM base classes ApiGatewayProxyHandler and
ApiGatewayV2HttpHandler.
class ApiHandler : ApiGatewayProxyHandler() {
override suspend fun handle(input: ApiGatewayProxyRequest) =
ApiGatewayProxyResponse(statusCode = 200, body = """{"result":"success"}""")
}Runs a Ktor Application behind API Gateway, REST or HTTP API, with no server engine. JVM:
APIGatewayLambdaServer and APIGatewayV2LambdaServer. Native: apiGatewayKtorLambda { } and
apiGatewayV2KtorLambda { }.
class KtorHandler : APIGatewayLambdaServer() {
override fun Application.module() {
routing { get("/users/{id}") { call.respondText(call.parameters["id"]!!) } }
}
}From 3.1.11. ApiGatewayPrincipal and the ApiGatewayAuth plugin, which make the claims API Gateway
has already verified available as a Ktor principal, on the JVM and on native. It does not validate
tokens itself: its provider accepts every request, and sets a principal with null claims when the
request carried none. It depends on ktor-server-auth only.
apiGatewayKtorLambda {
install(ApiGatewayAuth)
routing {
authenticate("api-gateway-jwt") {
get("/me") {
val principal = call.principal<ApiGatewayPrincipal>()
call.respondText("${principal?.subject} ${principal?.claim("email")} ${principal?.sourceIp}")
}
}
}
}claim(name) returns a string, number or boolean claim as its content string, and null for a claim
that is missing, JSON null, an object or an array. claims is the raw JsonObject. The plugin reads
a Cognito user pool authorizer's claims under a REST API and a JWT authorizer's claims under an HTTP
API (apiGatewayV2KtorLambda).
install(ApiGatewayAuth) { name = "cognito" } changes the provider name, and name = null makes it
the default provider. In a test, mockPrincipal replaces the principal read from the request:
install(ApiGatewayAuth) {
mockPrincipal = { ApiGatewayPrincipal(buildJsonObject { put("sub", "test-user") }) }
}ApiGatewayPrincipal(claims) also wraps claims from another source, such as the response of
Cognito's oauth2/userInfo endpoint.
To move from ApiGatewayJWT, replace the lambda-api-gateway-ktor-jwt dependency with
lambda-api-gateway-ktor-auth and change two names. The provider name is the same, so
authenticate("api-gateway-jwt") { } blocks do not change, and the package is the same
(com.steamstreet.aws.lambda.apigateway.ktor).
// before (JVM only)
install(ApiGatewayJWT)
val user = call.principal<JWTPrincipal>()?.subject
// after (JVM and native)
install(ApiGatewayAuth)
val user = call.principal<ApiGatewayPrincipal>()?.subjectJWTPrincipal |
ApiGatewayPrincipal |
|---|---|
payload.getClaim("email").asString() |
claim("email") |
payload.getClaim("sourceIp").asString() |
sourceIp |
subject, which throws when there is no sub claim |
subject, which is null |
mockPrinciple = { ... }, a JWTPrincipal
|
mockPrincipal = { ... }, an ApiGatewayPrincipal
|
JVM only. JWTPrincipal and the ApiGatewayJWT plugin, which build a Ktor principal from the
Cognito claims API Gateway has already verified. It does not validate tokens itself. For a Lambda
that also runs on native, use lambda-api-gateway-ktor-auth.
class KtorHandler : APIGatewayLambdaServer() {
override fun Application.module() {
install(ApiGatewayJWT)
routing {
authenticate("api-gateway-jwt") {
get("/me") { call.respond(call.principal<JWTPrincipal>()!!.subject) }
}
}
}
}These declarations shipped inside lambda-api-gateway-ktor before 3.0. They are now in this
separate artifact, in the same package:
implementation("com.steamstreet.awskt:lambda-api-gateway-ktor-jwt:VERSION").
Direct-Lambda AppSync resolvers routed by type and field. JVM: appSync(input, output, context) { }.
Native: appSyncLambda { }.
appSync(input, output, context) {
type("Query") {
field("getUser", GetUserArgs.serializer(), User.serializer()) { args -> loadUser(args.id) }
}
}DynamoDB stream handlers that accept direct, Kinesis-wrapped and SQS-redriven batches. JVM:
DynamoStreamHandler and DynamoKtStreamHandler. Native: dynamoStreamLambda,
dynamoStreamBatchLambda and dynamoKtStreamLambda.
class StreamHandler : DynamoStreamHandler() {
override suspend fun handleRecord(record: DynamoStreamEvent) {
when (record.eventName) { "INSERT" -> onInsert(record); "REMOVE" -> onRemove(record) }
}
}Routes EventBridge events by detail-type through an EventSchema. JVM: EventBridgeFunction.
Native: eventBridgeLambda { }.
class OrderHandler : EventBridgeFunction {
override suspend fun EventBridgeHandlerConfig.onEvent() {
on(OrderCompleted) { order -> finalizeOrder(order) }
}
}Kinesis event models and batch processing with partial failures. JVM: KinesisHandler. Native:
kinesisLambda and kinesisBatchLambda.
class Processor : KinesisHandler() {
override suspend fun processRecord(record: KinesisRecord) {
val data = Json.decodeFromString<SensorData>(record.kinesis.decodedData())
}
}Typed or raw SNS message processing. JVM: SNSHandler<T>. Native: snsLambda and snsRawLambda.
class NotificationHandler : SNSHandler<Notification>(Notification.serializer()) {
context(record: SnsRecord)
override suspend fun handleMessage(message: Notification) = sendEmail(message)
}SQS handlers. JVM: SQSRawHandler, SQSHandler<T>, and SQSBatchHandler<T>, which reports
partial batch failures. Native: sqsLambda, sqsBatchLambda and sqsRawLambda.
class QueueProcessor : SQSBatchHandler<Job>(Job.serializer()) {
context(record: SQSRecord)
override suspend fun handleMessage(message: Job) {
executeJob(message) // a throw fails only this record
}
}JVM only. A logback appender that writes through the Lambda runtime logger, and a default
logback.xml that uses it with a JSON encoder. Adding the dependency is enough.
JVM only. A bundle of env, standards, logging, lambda-core and lambda-logging.
Serializable AppSync resolver event models (AppSyncContext, AppSyncIdentity, AppSyncInfo) and
String.appSyncContext().
JVM only. Claims and JsonElementClaim, which read Cognito claims from API Gateway's authorizer
context. This is the support code for lambda-api-gateway-ktor-jwt. On native, ApiGatewayPrincipal
in lambda-api-gateway-ktor-auth does the job of Claims.
JVM only. Local and mock AWS for tests: DynamoLocalBuilder (in-memory DynamoDB Local, exposing an
SDK DynamoDbClient), EventBridgeMock (which implements awskt's EventBridge, with pattern
matching), DynamoStreamRunner, SqsMock, S3Local and LambdaMock.
val dynamo = DynamoLocalBuilder().apply { start() }
val db = DynamoKt("table", builder = { SdkBackedDynamoDb(dynamo.client) })
val events = EventBridgeMock()./gradlew check # every target, tests and ABI dumps
./gradlew :aws:aws-core:allTests # one module
./gradlew publishToMavenLocalReleases are cut with scripts/release.sh; see "Releasing" in AGENTS.md.
MIT. See LICENSE.