diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6802fb251..689a163e7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,12 +1,14 @@ name: CI on: push: - branches-ignore: - - 'generated' - - 'codegen/**' - - 'integrated/**' - - 'stl-preview-head/**' - - 'stl-preview-base/**' + branches: + - '**' + - '!integrated/**' + - '!stl-preview-head/**' + - '!stl-preview-base/**' + - '!generated' + - '!codegen/**' + - 'codegen/stl/**' pull_request: branches-ignore: - 'stl-preview-head/**' diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 4e7fded39..dadc0bb93 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.480.0" + ".": "0.481.0" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 182b18fc6..55652b8c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## 0.481.0 (2026-03-17) + +Full Changelog: [v0.480.0...v0.481.0](https://github.com/Increase/increase-java/compare/v0.480.0...v0.481.0) + +### Features + +* **webhooks:** replace hand-written webhook services with codegen events ([9fdb89d](https://github.com/Increase/increase-java/commit/9fdb89d43911e4b931159df63672dc37ea865a7c)) + + +### Chores + +* **internal:** tweak CI branches ([58afbd1](https://github.com/Increase/increase-java/commit/58afbd14594edc9c338068523c1d02906507762e)) + ## 0.480.0 (2026-03-16) Full Changelog: [v0.479.0...v0.480.0](https://github.com/Increase/increase-java/compare/v0.479.0...v0.480.0) diff --git a/README.md b/README.md index adde12ba1..9d1a5bdac 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,8 @@ -[![Maven Central](https://img.shields.io/maven-central/v/com.increase.api/increase-java)](https://central.sonatype.com/artifact/com.increase.api/increase-java/0.480.0) -[![javadoc](https://javadoc.io/badge2/com.increase.api/increase-java/0.480.0/javadoc.svg)](https://javadoc.io/doc/com.increase.api/increase-java/0.480.0) +[![Maven Central](https://img.shields.io/maven-central/v/com.increase.api/increase-java)](https://central.sonatype.com/artifact/com.increase.api/increase-java/0.481.0) +[![javadoc](https://javadoc.io/badge2/com.increase.api/increase-java/0.481.0/javadoc.svg)](https://javadoc.io/doc/com.increase.api/increase-java/0.481.0) @@ -13,7 +13,7 @@ The Increase Java SDK is similar to the Increase Kotlin SDK but with minor diffe -The REST API documentation can be found on [increase.com](https://increase.com/documentation). Javadocs are available on [javadoc.io](https://javadoc.io/doc/com.increase.api/increase-java/0.480.0). +The REST API documentation can be found on [increase.com](https://increase.com/documentation). Javadocs are available on [javadoc.io](https://javadoc.io/doc/com.increase.api/increase-java/0.481.0). @@ -24,7 +24,7 @@ The REST API documentation can be found on [increase.com](https://increase.com/d ### Gradle ```kotlin -implementation("com.increase.api:increase-java:0.480.0") +implementation("com.increase.api:increase-java:0.481.0") ``` ### Maven @@ -33,7 +33,7 @@ implementation("com.increase.api:increase-java:0.480.0") com.increase.api increase-java - 0.480.0 + 0.481.0 ``` @@ -430,11 +430,9 @@ export INCREASE_LOG=debug We provide helper methods for verifying that a webhook request came from Increase, and not a malicious third party. -You can use `increase.webhooks().verifySignature(body, headers, secret?)` or `increase.webhooks().unwrap(body, headers, secret?)`, -both of which will raise an error if the signature is invalid. If secret is omitted, the body will be unwrapped without any validation. +You can use `increase.events().unwrap(UnwrapWebhookParams)` to both verify the signature and parse the event body into a typed `UnwrapWebhookEvent`. An `IncreaseWebhookException` will be thrown if the signature is invalid. -Note that the "body" parameter must be the raw JSON string sent from the server (do not parse it first). -The `.unwrap()` method can parse this JSON for you. +Note that the `body` parameter must be the raw JSON string sent from the server (do not parse it first). If `headers` is omitted, the body will be unwrapped without any signature validation. ## ProGuard and R8 diff --git a/build.gradle.kts b/build.gradle.kts index 7d0f8ab9b..c3c9365c9 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -8,7 +8,7 @@ repositories { allprojects { group = "com.increase.api" - version = "0.480.0" // x-release-please-version + version = "0.481.0" // x-release-please-version } subprojects { diff --git a/increase-java-core/src/main/kotlin/com/increase/api/client/IncreaseClient.kt b/increase-java-core/src/main/kotlin/com/increase/api/client/IncreaseClient.kt index 5628c7f29..c9f7ae440 100644 --- a/increase-java-core/src/main/kotlin/com/increase/api/client/IncreaseClient.kt +++ b/increase-java-core/src/main/kotlin/com/increase/api/client/IncreaseClient.kt @@ -59,7 +59,6 @@ import com.increase.api.services.blocking.SimulationService import com.increase.api.services.blocking.SupplementalDocumentService import com.increase.api.services.blocking.SwiftTransferService import com.increase.api.services.blocking.TransactionService -import com.increase.api.services.blocking.WebhookService import com.increase.api.services.blocking.WireDrawdownRequestService import com.increase.api.services.blocking.WireTransferService import java.util.function.Consumer @@ -200,8 +199,6 @@ interface IncreaseClient { fun oauthConnections(): OAuthConnectionService - fun webhooks(): WebhookService - fun oauthTokens(): OAuthTokenService fun intrafiAccountEnrollments(): IntrafiAccountEnrollmentService diff --git a/increase-java-core/src/main/kotlin/com/increase/api/client/IncreaseClientAsync.kt b/increase-java-core/src/main/kotlin/com/increase/api/client/IncreaseClientAsync.kt index e20bc342c..d414e0d64 100644 --- a/increase-java-core/src/main/kotlin/com/increase/api/client/IncreaseClientAsync.kt +++ b/increase-java-core/src/main/kotlin/com/increase/api/client/IncreaseClientAsync.kt @@ -59,7 +59,6 @@ import com.increase.api.services.async.SimulationServiceAsync import com.increase.api.services.async.SupplementalDocumentServiceAsync import com.increase.api.services.async.SwiftTransferServiceAsync import com.increase.api.services.async.TransactionServiceAsync -import com.increase.api.services.async.WebhookServiceAsync import com.increase.api.services.async.WireDrawdownRequestServiceAsync import com.increase.api.services.async.WireTransferServiceAsync import java.util.function.Consumer @@ -200,8 +199,6 @@ interface IncreaseClientAsync { fun oauthConnections(): OAuthConnectionServiceAsync - fun webhooks(): WebhookServiceAsync - fun oauthTokens(): OAuthTokenServiceAsync fun intrafiAccountEnrollments(): IntrafiAccountEnrollmentServiceAsync diff --git a/increase-java-core/src/main/kotlin/com/increase/api/client/IncreaseClientAsyncImpl.kt b/increase-java-core/src/main/kotlin/com/increase/api/client/IncreaseClientAsyncImpl.kt index 0e6801386..e7352caae 100644 --- a/increase-java-core/src/main/kotlin/com/increase/api/client/IncreaseClientAsyncImpl.kt +++ b/increase-java-core/src/main/kotlin/com/increase/api/client/IncreaseClientAsyncImpl.kt @@ -116,8 +116,6 @@ import com.increase.api.services.async.SwiftTransferServiceAsync import com.increase.api.services.async.SwiftTransferServiceAsyncImpl import com.increase.api.services.async.TransactionServiceAsync import com.increase.api.services.async.TransactionServiceAsyncImpl -import com.increase.api.services.async.WebhookServiceAsync -import com.increase.api.services.async.WebhookServiceAsyncImpl import com.increase.api.services.async.WireDrawdownRequestServiceAsync import com.increase.api.services.async.WireDrawdownRequestServiceAsyncImpl import com.increase.api.services.async.WireTransferServiceAsync @@ -338,8 +336,6 @@ class IncreaseClientAsyncImpl(private val clientOptions: ClientOptions) : Increa OAuthConnectionServiceAsyncImpl(clientOptionsWithUserAgent) } - private val webhooks: WebhookServiceAsync by lazy { WebhookServiceAsyncImpl(clientOptions) } - private val oauthTokens: OAuthTokenServiceAsync by lazy { OAuthTokenServiceAsyncImpl(clientOptionsWithUserAgent) } @@ -493,8 +489,6 @@ class IncreaseClientAsyncImpl(private val clientOptions: ClientOptions) : Increa override fun intrafiExclusions(): IntrafiExclusionServiceAsync = intrafiExclusions - override fun webhooks(): WebhookServiceAsync = webhooks - override fun cardTokens(): CardTokenServiceAsync = cardTokens override fun cardPushTransfers(): CardPushTransferServiceAsync = cardPushTransfers diff --git a/increase-java-core/src/main/kotlin/com/increase/api/client/IncreaseClientImpl.kt b/increase-java-core/src/main/kotlin/com/increase/api/client/IncreaseClientImpl.kt index b4d9db6a8..a2aca3a2e 100644 --- a/increase-java-core/src/main/kotlin/com/increase/api/client/IncreaseClientImpl.kt +++ b/increase-java-core/src/main/kotlin/com/increase/api/client/IncreaseClientImpl.kt @@ -116,8 +116,6 @@ import com.increase.api.services.blocking.SwiftTransferService import com.increase.api.services.blocking.SwiftTransferServiceImpl import com.increase.api.services.blocking.TransactionService import com.increase.api.services.blocking.TransactionServiceImpl -import com.increase.api.services.blocking.WebhookService -import com.increase.api.services.blocking.WebhookServiceImpl import com.increase.api.services.blocking.WireDrawdownRequestService import com.increase.api.services.blocking.WireDrawdownRequestServiceImpl import com.increase.api.services.blocking.WireTransferService @@ -339,8 +337,6 @@ class IncreaseClientImpl(private val clientOptions: ClientOptions) : IncreaseCli IntrafiExclusionServiceImpl(clientOptionsWithUserAgent) } - private val webhooks: WebhookService by lazy { WebhookServiceImpl(clientOptionsWithUserAgent) } - private val cardTokens: CardTokenService by lazy { CardTokenServiceImpl(clientOptionsWithUserAgent) } @@ -476,8 +472,6 @@ class IncreaseClientImpl(private val clientOptions: ClientOptions) : IncreaseCli override fun intrafiExclusions(): IntrafiExclusionService = intrafiExclusions - override fun webhooks(): WebhookService = webhooks - override fun cardTokens(): CardTokenService = cardTokens override fun cardPushTransfers(): CardPushTransferService = cardPushTransfers diff --git a/increase-java-core/src/main/kotlin/com/increase/api/services/async/WebhookServiceAsync.kt b/increase-java-core/src/main/kotlin/com/increase/api/services/async/WebhookServiceAsync.kt deleted file mode 100644 index b22b16f01..000000000 --- a/increase-java-core/src/main/kotlin/com/increase/api/services/async/WebhookServiceAsync.kt +++ /dev/null @@ -1,13 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. - -package com.increase.api.services.async - -import com.increase.api.core.JsonValue -import com.increase.api.core.http.Headers - -interface WebhookServiceAsync { - - fun unwrap(payload: String, headers: Headers, secret: String?): JsonValue - - fun verifySignature(payload: String, headers: Headers, secret: String?) -} diff --git a/increase-java-core/src/main/kotlin/com/increase/api/services/async/WebhookServiceAsyncImpl.kt b/increase-java-core/src/main/kotlin/com/increase/api/services/async/WebhookServiceAsyncImpl.kt deleted file mode 100644 index 64ed690aa..000000000 --- a/increase-java-core/src/main/kotlin/com/increase/api/services/async/WebhookServiceAsyncImpl.kt +++ /dev/null @@ -1,62 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. - -package com.increase.api.services.async - -import com.fasterxml.jackson.core.JsonProcessingException -import com.google.common.io.BaseEncoding -import com.increase.api.core.ClientOptions -import com.increase.api.core.JsonValue -import com.increase.api.core.getRequiredHeader -import com.increase.api.core.http.Headers -import com.increase.api.errors.IncreaseException -import java.time.Instant -import javax.crypto.Mac -import javax.crypto.spec.SecretKeySpec -import kotlin.jvm.optionals.getOrNull - -class WebhookServiceAsyncImpl(private val clientOptions: ClientOptions) : WebhookServiceAsync { - - override fun unwrap(payload: String, headers: Headers, secret: String?): JsonValue { - if (secret != null) { - verifySignature(payload, headers, secret) - } - - return try { - clientOptions.jsonMapper.readValue(payload, JsonValue::class.java) - } catch (e: JsonProcessingException) { - throw IncreaseException("Invalid event payload", e) - } - } - - override fun verifySignature(payload: String, headers: Headers, secret: String?) { - val webhookSecret = - secret - ?: clientOptions.webhookSecret().getOrNull() - ?: throw IncreaseException( - "The webhook secret must either be set using the env var, INCREASE_WEBHOOK_SECRET, on the client class, or passed to this method" - ) - - val msgSignatureRaw = headers.getRequiredHeader("Increase-Webhook-Signature") - val msgSignatureParts = - msgSignatureRaw.split(",", "=").chunked(2) { it[0] to it[1] }.toMap() - val msgTimestamp = msgSignatureParts["t"] - val msgSignature = msgSignatureParts["v1"] - - try { - Instant.parse(msgTimestamp) - } catch (e: RuntimeException) { - throw IncreaseException("Invalid signature headers", e) - } - - val mac = Mac.getInstance("HmacSHA256") - mac.init(SecretKeySpec(webhookSecret.toByteArray(), "HmacSHA256")) - val expectedSignature = mac.doFinal("$msgTimestamp.$payload".toByteArray()) - val expectedSignatureStr = BaseEncoding.base16().lowerCase().encode(expectedSignature) - - if (msgSignature == expectedSignatureStr) { - return - } - - throw IncreaseException("The given webhook signature does not match the expected signature") - } -} diff --git a/increase-java-core/src/main/kotlin/com/increase/api/services/blocking/WebhookService.kt b/increase-java-core/src/main/kotlin/com/increase/api/services/blocking/WebhookService.kt deleted file mode 100644 index d4dcbe876..000000000 --- a/increase-java-core/src/main/kotlin/com/increase/api/services/blocking/WebhookService.kt +++ /dev/null @@ -1,13 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. - -package com.increase.api.services.blocking - -import com.increase.api.core.JsonValue -import com.increase.api.core.http.Headers - -interface WebhookService { - - fun unwrap(payload: String, headers: Headers, secret: String?): JsonValue - - fun verifySignature(payload: String, headers: Headers, secret: String?) -} diff --git a/increase-java-core/src/main/kotlin/com/increase/api/services/blocking/WebhookServiceImpl.kt b/increase-java-core/src/main/kotlin/com/increase/api/services/blocking/WebhookServiceImpl.kt deleted file mode 100644 index 5674cc5cf..000000000 --- a/increase-java-core/src/main/kotlin/com/increase/api/services/blocking/WebhookServiceImpl.kt +++ /dev/null @@ -1,62 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. - -package com.increase.api.services.blocking - -import com.fasterxml.jackson.core.JsonProcessingException -import com.google.common.io.BaseEncoding -import com.increase.api.core.ClientOptions -import com.increase.api.core.JsonValue -import com.increase.api.core.getRequiredHeader -import com.increase.api.core.http.Headers -import com.increase.api.errors.IncreaseException -import java.time.Instant -import javax.crypto.Mac -import javax.crypto.spec.SecretKeySpec -import kotlin.jvm.optionals.getOrNull - -class WebhookServiceImpl(private val clientOptions: ClientOptions) : WebhookService { - - override fun unwrap(payload: String, headers: Headers, secret: String?): JsonValue { - if (secret != null) { - verifySignature(payload, headers, secret) - } - - return try { - clientOptions.jsonMapper.readValue(payload, JsonValue::class.java) - } catch (e: JsonProcessingException) { - throw IncreaseException("Invalid event payload", e) - } - } - - override fun verifySignature(payload: String, headers: Headers, secret: String?) { - val webhookSecret = - secret - ?: clientOptions.webhookSecret().getOrNull() - ?: throw IncreaseException( - "The webhook secret must either be set using the env var, INCREASE_WEBHOOK_SECRET, on the client class, or passed to this method" - ) - - val msgSignatureRaw = headers.getRequiredHeader("Increase-Webhook-Signature") - val msgSignatureParts = - msgSignatureRaw.split(",", "=").chunked(2) { it[0] to it[1] }.toMap() - val msgTimestamp = msgSignatureParts["t"] - val msgSignature = msgSignatureParts["v1"] - - try { - Instant.parse(msgTimestamp) - } catch (e: RuntimeException) { - throw IncreaseException("Invalid signature headers", e) - } - - val mac = Mac.getInstance("HmacSHA256") - mac.init(SecretKeySpec(webhookSecret.toByteArray(), "HmacSHA256")) - val expectedSignature = mac.doFinal("$msgTimestamp.$payload".toByteArray()) - val expectedSignatureStr = BaseEncoding.base16().lowerCase().encode(expectedSignature) - - if (msgSignature == expectedSignatureStr) { - return - } - - throw IncreaseException("The given webhook signature does not match the expected signature") - } -} diff --git a/increase-java-core/src/test/kotlin/com/increase/api/services/blocking/WebhookServiceTest.kt b/increase-java-core/src/test/kotlin/com/increase/api/services/blocking/WebhookServiceTest.kt deleted file mode 100644 index 086d4886a..000000000 --- a/increase-java-core/src/test/kotlin/com/increase/api/services/blocking/WebhookServiceTest.kt +++ /dev/null @@ -1,116 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. - -package com.increase.api.services.blocking - -import com.increase.api.TestServerExtension -import com.increase.api.client.okhttp.IncreaseOkHttpClient -import com.increase.api.core.http.Headers -import com.increase.api.errors.IncreaseException -import com.increase.api.models.* -import java.time.Clock -import java.time.Instant -import java.time.ZoneOffset -import org.assertj.core.api.Assertions.* -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith - -@ExtendWith(TestServerExtension::class) -class WebhookServiceTest { - - @Test - fun unwrapConfiguredSecret() { - val client = - IncreaseOkHttpClient.builder() - .apiKey("test-api-key") - .webhookSecret("whsec_zlFsbBZ8Xcodlpcu6NDTdSzZRLSdhkst") - .clock(Clock.fixed(Instant.parse("2022-01-31T23:59:59Z"), ZoneOffset.UTC)) - .build() - - val payload = "{\"id\":\"event_123abc\",\"created_at\":\"2020-01-31T23:59:59Z\"}" - val headers = - Headers.builder() - .put( - "Increase-Webhook-Signature", - "t=2022-01-31T23:59:59Z,v1=3f9c3dcc820ca3adfae8e196d05b09dfef63b91db5ce5ac1407090f2aa424a6f", - ) - .build() - - val event = client.webhooks().unwrap(payload, headers, null) - - assertThat(event).isNotNull() - } - - @Test - fun unwrapPassedSecret() { - val client = - IncreaseOkHttpClient.builder() - .apiKey("test-api-key") - .clock(Clock.fixed(Instant.parse("2022-01-31T23:59:59Z"), ZoneOffset.UTC)) - .build() - - val payload = "{\"id\":\"event_123abc\",\"created_at\":\"2020-01-31T23:59:59Z\"}" - val headers = - Headers.builder() - .put( - "Increase-Webhook-Signature", - "t=2022-01-31T23:59:59Z,v1=3f9c3dcc820ca3adfae8e196d05b09dfef63b91db5ce5ac1407090f2aa424a6f", - ) - .build() - - val event = - client.webhooks().unwrap(payload, headers, "whsec_zlFsbBZ8Xcodlpcu6NDTdSzZRLSdhkst") - - assertThat(event).isNotNull() - } - - @Test - fun noSecret() { - val client = - IncreaseOkHttpClient.builder() - .apiKey("test-api-key") - .clock(Clock.fixed(Instant.parse("2022-01-31T23:59:59Z"), ZoneOffset.UTC)) - .build() - - val payload = "{\"id\":\"event_123abc\",\"created_at\":\"2020-01-31T23:59:59Z\"}" - val headers = - Headers.builder() - .put( - "Increase-Webhook-Signature", - "t=2022-01-31T23:59:59Z,v1=3f9c3dcc820ca3adfae8e196d05b09dfef63b91db5ce5ac1407090f2aa424a6f", - ) - .build() - - val event = client.webhooks().unwrap(payload, headers, null) - - assertThat(event).isNotNull() - } - - @Test - fun verifySignature() { - val client = - IncreaseOkHttpClient.builder() - .apiKey("test-api-key") - .clock(Clock.fixed(Instant.parse("2022-01-31T23:59:59Z"), ZoneOffset.UTC)) - .build() - - val payload = "{\"id\":\"event_123abc\",\"created_at\":\"2020-01-31T23:59:59Z\"}" - val webhookTimestamp = "2022-01-31T23:59:59Z" - val webhookSignature = "bf127c54744439c2890b36028a8c734856776db3ed1e2632fc548e5f834a1f57" - val headers = - Headers.builder() - .put("Increase-Webhook-Signature", "t=$webhookTimestamp,v1=$webhookSignature") - .build() - - assertThatCode { - client - .webhooks() - .verifySignature(payload, headers, "5WbX5kEWLlfzsGNjH64I8lOOqUB6e8FH") - } - .doesNotThrowAnyException() - - assertThatThrownBy { client.webhooks().verifySignature(payload, headers, "other") } - .isInstanceOf(IncreaseException::class.java) - .hasMessage("The given webhook signature does not match the expected signature") - } -}