Skip to content
Merged
Show file tree
Hide file tree
Changes from 44 commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
3cf2d96
feat(data): AppSync Events Library Shells (#3009)
tylerjroach Apr 1, 2025
7be8e23
feat(data): AppSync Authorizers (#3011)
tylerjroach Apr 2, 2025
cbc383c
feat(data): Events API Contract shells (#3012)
tylerjroach Apr 3, 2025
3ca0692
feat(data): Event REST Publish for 200 codes (#3013)
tylerjroach Apr 8, 2025
880b91d
feat(data): Complete events class (#3016)
tylerjroach Apr 9, 2025
9ee12ba
feat(data): Events WebSocket Connection Logic + Subscribe (#3018)
tylerjroach Apr 11, 2025
d28f9e9
feat(data): Events API Connection ack timeout handling (#3020)
tylerjroach Apr 17, 2025
cd3110f
feat(data): Events Lib WS Publishing (#3027)
tylerjroach Apr 22, 2025
6d820c5
feat(data): Events Lib consistent exception handling (#3028)
tylerjroach Apr 23, 2025
ac4a7a4
feat(data): Update Events API Contract (#3029)
tylerjroach Apr 24, 2025
f6d81ee
feat(data): Fix Data event parsing + unit tests (#3031)
tylerjroach Apr 29, 2025
356f8ad
feat(data): Rename events packages (#3034)
tylerjroach Apr 30, 2025
ef3e097
feat(data): Events logs data removal (#3036)
tylerjroach Apr 30, 2025
140dea2
feat(data) Events Publish upate (#3037)
tylerjroach May 1, 2025
d60d032
feat(data) Events api contract lock (#3038)
tylerjroach May 1, 2025
cd7c149
feat(data): Update Events API Lib to new agreement (#3040)
tylerjroach May 6, 2025
01cc218
feat(data) AppSync Events WebSocket Unit Tests + Integration Tests (#…
tylerjroach May 9, 2025
33ea252
fix(data): Ignore test (#3050)
tylerjroach May 9, 2025
86326e6
fix(data): Events licenses (#3051)
tylerjroach May 9, 2025
7bdf665
feat(data): Pull Events Integration Test Data (#3052)
tylerjroach May 9, 2025
bfaec73
fix(data) Events fix test lockup (#3053)
tylerjroach May 9, 2025
3aa14dc
Merge branch 'main' into feat/appsync-events
tylerjroach May 12, 2025
f300d1e
feat(data): Add Events Unit Tests (#3055)
tylerjroach May 12, 2025
ace1910
fix(data) Events timeout fix (#3058)
tylerjroach May 13, 2025
bdbef6f
run integration tests
tylerjroach May 13, 2025
7814a4e
attempt to run events integration tests
tylerjroach May 13, 2025
67d9459
attempt to run events integration tests
tylerjroach May 13, 2025
3deeb5c
attempt to run events integration tests
tylerjroach May 13, 2025
cd05372
attempt to run events integration tests and stabilize unit tests
tylerjroach May 13, 2025
33d00b0
lint
tylerjroach May 13, 2025
18aecdd
lint
tylerjroach May 13, 2025
ac25f2a
lint
tylerjroach May 13, 2025
a4467e2
test update
tylerjroach May 13, 2025
19c9d90
test update
tylerjroach May 13, 2025
ee41e1d
fix(data) Modify unsubscribe event scenarios (#3059)
tylerjroach May 13, 2025
19f7fa6
set companion object to internal
tylerjroach May 13, 2025
356ec33
apiCheck fix
tylerjroach May 14, 2025
6579497
remove duplicate code
tylerjroach May 14, 2025
4bf0e9b
Merge branch 'main' into feat/appsync-events
tylerjroach May 15, 2025
7f9d7f3
Add README, CHANGELOG, and publishing prep
tylerjroach May 15, 2025
7873c53
Update scopes for Events
tylerjroach May 15, 2025
90533ec
ignore Events scopes on Amplify
tylerjroach May 15, 2025
27082fc
Update appsync/README.md
tylerjroach May 15, 2025
f573683
Update appsync/README.md
tylerjroach May 15, 2025
b157783
Improve test feedback
tylerjroach May 15, 2025
20ee84f
Merge remote-tracking branch 'origin/feat/appsync-events' into feat/a…
tylerjroach May 15, 2025
b1844ee
stabilize tests
tylerjroach May 15, 2025
d53cfe6
Fix events test lockups (#3067)
tylerjroach May 20, 2025
a65d62e
Merge branch 'main' into feat/appsync-events
tylerjroach May 20, 2025
659bd37
lint
tylerjroach May 20, 2025
5cc326d
fix test where order doesn't matter on received events
tylerjroach May 21, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/release_pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ on:
options:
- AmplifyAndroid
- ApolloExtensions
- AppSyncEvents
release_tag:
description: 'Previous Release Tag'
required: false
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ __pycache__/
#amplify
.amplify/
amplify/
#keep amplify gen2 backend projects
!**/androidTest/backend/amplify/
build/
dist/
node_modules/
Expand Down
3 changes: 3 additions & 0 deletions appsync/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Release 1.0.0

Initial Release
43 changes: 43 additions & 0 deletions appsync/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<img src="https://s3.amazonaws.com/aws-mobile-hub-images/aws-amplify-logo.png" alt="AWS Amplify" width="225">

---

# AWS AppSync Events for Android

These libraries allow you to connect to [AWS AppSync Events](https://docs.aws.amazon.com/appsync/latest/eventapi/event-api-welcome.html).
AWS AppSync Events lets you create secure and performant serverless WebSocket APIs that can broadcast real-time event data to millions of subscribers, without you having to manage connections or resource scaling. With this feature, you can build multi-user features such as a collaborative document editors, chat apps, and live polling systems.

Learn more about AWS AppSync Events by visiting the [Developer Guide](https://docs.aws.amazon.com/appsync/latest/eventapi/event-api-welcome.html).

There are two libraries available:

- `aws-sdk-appsync-events`: This is a standalone library that allows developers to connect to AWS AppSync Events. It does not depend on Amplify, and instead leaves it to the application developer to supply tokens or signatures when using Owner or IAM-based authorization. This is the recommended library if your application does not already use Amplify.
- `aws-sdk-appsync-amplify`: This library depends on `Amplify Android`, and contains some glue classes to use Amplify to implement the authorizers for `aws-sdk-appsync-events`. This additional library is recommended if your application is already using Amplify, and you want to authorize calls with AWS Cognito User Pool tokens or IAM.

## Installation

Add the dependency you prefer to your `build.gradle.kts` file.

```kotlin
// Standalone AWS AppSync Events library that does not require Amplify
implementation("com.amazonaws:aws-sdk-appsync-events:1.0.0")

// Allows Amplify to Authorize Events calls through UserPool tokens or IAM
implementation("com.amazonaws:aws-sdk-appsync-amplify:1.0.0")
```

## Usage Guide

See [AWS AppSync Events for Android Documentation](https://docs.amplify.aws/android/build-a-backend/data/connect-event-api/) for a complete implementation guide.

## Contributing

- [CONTRIBUTING.md](../CONTRIBUTING.md)

## Security

See [CONTRIBUTING](../CONTRIBUTING.md#security-issue-notifications) for more information.

## License

This project is licensed under the Apache-2.0 License.
1 change: 1 addition & 0 deletions appsync/aws-sdk-appsync-amplify/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
18 changes: 18 additions & 0 deletions appsync/aws-sdk-appsync-amplify/api/aws-sdk-appsync-amplify.api
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
public final class com/amazonaws/sdk/appsync/amplify/BuildConfig {
public static final field BUILD_TYPE Ljava/lang/String;
public static final field DEBUG Z
public static final field LIBRARY_PACKAGE_NAME Ljava/lang/String;
public static final field VERSION_NAME Ljava/lang/String;
public fun <init> ()V
}

public final class com/amazonaws/sdk/appsync/amplify/authorizers/AmplifyIamAuthorizer : com/amazonaws/sdk/appsync/core/AppSyncAuthorizer {
public fun <init> (Ljava/lang/String;)V
public fun getAuthorizationHeaders (Lcom/amazonaws/sdk/appsync/core/AppSyncRequest;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}

public final class com/amazonaws/sdk/appsync/amplify/authorizers/AmplifyUserPoolAuthorizer : com/amazonaws/sdk/appsync/core/AppSyncAuthorizer {
public fun <init> ()V
public fun getAuthorizationHeaders (Lcom/amazonaws/sdk/appsync/core/AppSyncRequest;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}

50 changes: 50 additions & 0 deletions appsync/aws-sdk-appsync-amplify/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/

import java.util.Properties

plugins {
id("com.android.library")
id("kotlin-android")
}

apply(from = rootProject.file("configuration/publishing.gradle"))

fun readVersion() = Properties().run {
file("../version.properties").inputStream().use { load(it) }
get("VERSION_NAME").toString()
}

project.setProperty("VERSION_NAME", readVersion())
group = properties["POM_GROUP"].toString()

android {
namespace = "com.amazonaws.sdk.appsync.amplify"
}

dependencies {

api(project(":aws-sdk-appsync-core"))
api(project(":core"))

implementation(project(":aws-auth-cognito"))
implementation(project(":aws-core"))
implementation(libs.aws.signing)

testImplementation(libs.test.junit)
testImplementation(libs.test.mockk)
testImplementation(libs.test.kotlin.coroutines)
testImplementation(libs.test.kotest.assertions)
}
5 changes: 5 additions & 0 deletions appsync/aws-sdk-appsync-amplify/gradle.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
POM_GROUP=com.amazonaws
POM_ARTIFACT_ID=aws-sdk-appsync-amplify
POM_NAME=Amplify Extensions for AWS AppSync
POM_DESCRIPTION=Amplify Extensions for AWS AppSync
POM_PACKAGING=aar
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
package com.amazonaws.sdk.appsync.amplify.authorizers

import com.amazonaws.sdk.appsync.amplify.util.AppSyncRequestSigner
import com.amazonaws.sdk.appsync.core.AppSyncAuthorizer
import com.amazonaws.sdk.appsync.core.AppSyncRequest
import com.amazonaws.sdk.appsync.core.authorizers.IamAuthorizer
import org.jetbrains.annotations.VisibleForTesting

/**
* Authorizer implementation that provides IAM signing through Amplify & AWS Kotlin SDK (Smithy).
*/
class AmplifyIamAuthorizer @VisibleForTesting internal constructor(
private val region: String,
private val requestSigner: AppSyncRequestSigner
) : AppSyncAuthorizer {

constructor(region: String) : this(region, requestSigner = AppSyncRequestSigner())

private val iamAuthorizer = IamAuthorizer { requestSigner.signAppSyncRequest(it, region) }

override suspend fun getAuthorizationHeaders(request: AppSyncRequest) =
iamAuthorizer.getAuthorizationHeaders(request)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
package com.amazonaws.sdk.appsync.amplify.authorizers

import com.amazonaws.sdk.appsync.core.AppSyncAuthorizer
import com.amazonaws.sdk.appsync.core.AppSyncRequest
import com.amazonaws.sdk.appsync.core.authorizers.AuthTokenAuthorizer
import com.amplifyframework.auth.AuthCredentialsProvider
import com.amplifyframework.auth.CognitoCredentialsProvider
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
import org.jetbrains.annotations.VisibleForTesting

/**
* Authorizer implementation that provides Cognito User Pool tokens via Amplify Auth.
*/
class AmplifyUserPoolAuthorizer @VisibleForTesting internal constructor(
private val accessTokenProvider: AccessTokenProvider
) : AppSyncAuthorizer {

constructor() : this(accessTokenProvider = AccessTokenProvider())

private val authTokenAuthorizer = AuthTokenAuthorizer(
fetchLatestAuthToken = accessTokenProvider::fetchLatestCognitoAuthToken
)

override suspend fun getAuthorizationHeaders(request: AppSyncRequest) =
authTokenAuthorizer.getAuthorizationHeaders(request)
}

internal class AccessTokenProvider(
private val credentialsProvider: AuthCredentialsProvider = CognitoCredentialsProvider()
) {
suspend fun fetchLatestCognitoAuthToken() = credentialsProvider.getAccessToken()

private suspend fun AuthCredentialsProvider.getAccessToken() = suspendCoroutine { continuation ->
getAccessToken(
{ token -> continuation.resume(token) },
{ error -> continuation.resumeWithException(error) }
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
* Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
package com.amazonaws.sdk.appsync.amplify.util

import aws.smithy.kotlin.runtime.InternalApi
import aws.smithy.kotlin.runtime.auth.awscredentials.Credentials
import aws.smithy.kotlin.runtime.auth.awssigning.AwsSignedBodyHeader
import aws.smithy.kotlin.runtime.auth.awssigning.AwsSigner
import aws.smithy.kotlin.runtime.auth.awssigning.AwsSigningConfig
import aws.smithy.kotlin.runtime.auth.awssigning.DefaultAwsSigner
import aws.smithy.kotlin.runtime.http.Headers
import aws.smithy.kotlin.runtime.http.HttpBody
import aws.smithy.kotlin.runtime.http.request.HttpRequest
import aws.smithy.kotlin.runtime.http.toHttpBody
import aws.smithy.kotlin.runtime.net.url.Url
import com.amazonaws.sdk.appsync.core.AppSyncRequest
import com.amplifyframework.auth.AuthCredentialsProvider
import com.amplifyframework.auth.CognitoCredentialsProvider

internal class AppSyncRequestSigner(
private val credentialsProvider: AuthCredentialsProvider = CognitoCredentialsProvider(),
private val awsSigner: AwsSigner = DefaultAwsSigner
) {

suspend fun signAppSyncRequest(request: AppSyncRequest, region: String): Map<String, String> {
// First translate the Apollo request to a Smithy request so it can be signed
val smithyRequest = request.toSmithyRequest()

// Sign the Smithy request
val signedRequest = signRequest(
awsRegion = region,
credentials = credentialsProvider.resolve(),
service = "appsync",
request = smithyRequest
)

// Return the headers from the signed request
return signedRequest.headers.entries().associate { it.key to it.value.first() }
}

@OptIn(InternalApi::class)
private suspend fun signRequest(
awsRegion: String,
credentials: Credentials,
service: String,
request: HttpRequest
): HttpRequest {
val signingConfig = AwsSigningConfig {
region = awsRegion
useDoubleUriEncode = true
this.service = service
this.credentials = credentials
signedBodyHeader = AwsSignedBodyHeader.X_AMZ_CONTENT_SHA256
}

// Generate the signature for the smithy request
return awsSigner.sign(request, signingConfig).output
}

private fun AppSyncRequest.toSmithyRequest() = HttpRequest(
method = method.toSmithyMethod(),
url = Url.parse(url),
headers = createSmithyHeaders(headers),
body = body?.toHttpBody() ?: HttpBody.Empty
)

private fun AppSyncRequest.HttpMethod.toSmithyMethod() = when (this) {
AppSyncRequest.HttpMethod.GET -> aws.smithy.kotlin.runtime.http.HttpMethod.GET
AppSyncRequest.HttpMethod.POST -> aws.smithy.kotlin.runtime.http.HttpMethod.POST
}

private fun createSmithyHeaders(headers: Map<String, String>) = Headers {
headers.forEach {
this.append(it.key, it.value)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
package com.amazonaws.sdk.appsync.amplify.authorizers

import com.amazonaws.sdk.appsync.amplify.util.AppSyncRequestSigner
import com.amazonaws.sdk.appsync.core.AppSyncRequest
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.matchers.maps.shouldContainExactly
import io.mockk.coEvery
import io.mockk.mockk
import kotlinx.coroutines.test.runTest
import org.junit.Test

class AmplifyIamAuthorizerTest {
private val region = "us-east-1"

@Test
fun `iam authorizer gets token from amplify`() = runTest {
val request = mockk<AppSyncRequest>()
val signer = mockk<AppSyncRequestSigner> {
coEvery {
signAppSyncRequest(request, region)
} returns mapOf("Authorization" to "test-signature")
}

val authorizer = AmplifyIamAuthorizer(region, signer)

authorizer.getAuthorizationHeaders(request) shouldContainExactly mapOf("Authorization" to "test-signature")
}

@Test
fun `iam authorizer throws if failed to fetch token from amplify`() = runTest {
val request = mockk<AppSyncRequest>()
val signer = mockk<AppSyncRequestSigner> {
coEvery {
signAppSyncRequest(request, region)
} throws IllegalStateException()
}

val authorizer = AmplifyIamAuthorizer(region, signer)

shouldThrow<IllegalStateException> {
authorizer.getAuthorizationHeaders(request)
}
}
}
Loading