diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ec83e1..7402702 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,17 +5,13 @@ All notable changes to the AxonFlow Java SDK will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [5.3.0] - Release Pending (2026-04-09) +## [5.3.0] - 2026-04-09 -### Fixed +### Added -- **IPv6 endpoint classification.** `classifyEndpoint` now handles IPv6 private ranges and expanded loopback forms that previously fell through to `REMOTE`, matching the Python and Go SDK implementations: - - IPv6 ULA (`fc00::/7`, RFC 4193) → `private_network` - - IPv6 link-local (`fe80::/10`) → `private_network` - - Expanded IPv6 loopback (`0:0:0:0:0:0:0:1`, zero-padded forms) → `localhost` - - IPv6 unspecified (`::`) → `localhost` (symmetric with `0.0.0.0`) - - Public IPv6 addresses (`2001::/3` space) → `remote` -- A new `expandIPv6(addr)` helper expands `::` compression into a full 8-hextet form for prefix comparison. Not a general-purpose parser — assumes input came from `URI.getHost()` after brackets are stripped. +- `AXONFLOW_TRY=1` environment variable to connect to `try.getaxonflow.com` shared evaluation server +- `AxonFlowTry.register()` helper for self-registering a tenant +- Checkpoint telemetry reports `endpoint_type: "community-saas"` when try mode is active --- diff --git a/src/main/java/com/getaxonflow/sdk/AxonFlowConfig.java b/src/main/java/com/getaxonflow/sdk/AxonFlowConfig.java index 2d7033a..cb29c6d 100644 --- a/src/main/java/com/getaxonflow/sdk/AxonFlowConfig.java +++ b/src/main/java/com/getaxonflow/sdk/AxonFlowConfig.java @@ -79,6 +79,9 @@ private static String detectSdkVersion() { /** Default endpoint URL. */ public static final String DEFAULT_ENDPOINT = "http://localhost:8080"; + /** Try mode endpoint URL. */ + public static final String TRY_ENDPOINT = "https://try.getaxonflow.com"; + private final String endpoint; private final String clientId; private final String clientSecret; @@ -90,9 +93,14 @@ private static String detectSdkVersion() { private final CacheConfig cacheConfig; private final String userAgent; private final Boolean telemetry; + private final boolean tryMode; private AxonFlowConfig(Builder builder) { - this.endpoint = normalizeUrl(builder.endpoint != null ? builder.endpoint : DEFAULT_ENDPOINT); + this.tryMode = "1".equals(System.getenv("AXONFLOW_TRY")); + this.endpoint = + this.tryMode + ? normalizeUrl(TRY_ENDPOINT) + : normalizeUrl(builder.endpoint != null ? builder.endpoint : DEFAULT_ENDPOINT); this.clientId = builder.clientId; this.clientSecret = builder.clientSecret; this.mode = builder.mode != null ? builder.mode : Mode.PRODUCTION; @@ -112,6 +120,10 @@ private void validate() { if (endpoint == null || endpoint.isEmpty()) { throw new ConfigurationException("endpoint is required", "endpoint"); } + if (tryMode && (clientId == null || clientId.isEmpty())) { + throw new ConfigurationException( + "clientId is required in try mode (AXONFLOW_TRY=1)", "clientId"); + } // Credentials are optional for community/self-hosted deployments // Enterprise features require credentials (validated at method call time) } diff --git a/src/main/java/com/getaxonflow/sdk/AxonFlowTry.java b/src/main/java/com/getaxonflow/sdk/AxonFlowTry.java new file mode 100644 index 0000000..7406ee9 --- /dev/null +++ b/src/main/java/com/getaxonflow/sdk/AxonFlowTry.java @@ -0,0 +1,104 @@ +/* + * Copyright 2025 AxonFlow + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License 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.getaxonflow.sdk; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; + +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * Registration helper for try.getaxonflow.com shared evaluation server. + */ +public class AxonFlowTry { + + public static final String TRY_ENDPOINT = "https://try.getaxonflow.com"; + + private static final ObjectMapper MAPPER = new ObjectMapper(); + private static final HttpClient CLIENT = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(10)) + .build(); + + /** + * Register for a free evaluation tenant. Store the secret securely — it is shown only once. + */ + public static TryRegistration register() throws IOException, InterruptedException { + return register("", TRY_ENDPOINT); + } + + /** + * Register with an optional label. + */ + public static TryRegistration register(String label) throws IOException, InterruptedException { + return register(label, TRY_ENDPOINT); + } + + /** + * Register with a custom endpoint (for local testing). + */ + public static TryRegistration register(String label, String endpoint) throws IOException, InterruptedException { + String body = label != null && !label.isEmpty() + ? String.format("{\"label\":\"%s\"}", label) + : "{}"; + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(endpoint + "/api/v1/register")) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(body)) + .timeout(Duration.ofSeconds(10)) + .build(); + + HttpResponse response = CLIENT.send(request, HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() != 201) { + throw new IOException("Registration failed (" + response.statusCode() + "): " + response.body()); + } + + return MAPPER.readValue(response.body(), TryRegistration.class); + } + + /** + * Registration response from try.getaxonflow.com. + */ + public static class TryRegistration { + private String tenant_id; + private String secret; + private String secret_prefix; + private String expires_at; + private String endpoint; + private String note; + + // Getters + public String getTenantId() { return tenant_id; } + public String getSecret() { return secret; } + public String getSecretPrefix() { return secret_prefix; } + public String getExpiresAt() { return expires_at; } + public String getEndpoint() { return endpoint; } + public String getNote() { return note; } + + // Setters for Jackson + public void setTenant_id(String v) { this.tenant_id = v; } + public void setSecret(String v) { this.secret = v; } + public void setSecret_prefix(String v) { this.secret_prefix = v; } + public void setExpires_at(String v) { this.expires_at = v; } + public void setEndpoint(String v) { this.endpoint = v; } + public void setNote(String v) { this.note = v; } + } +} diff --git a/src/main/java/com/getaxonflow/sdk/telemetry/TelemetryReporter.java b/src/main/java/com/getaxonflow/sdk/telemetry/TelemetryReporter.java index 6e503c4..64409f6 100644 --- a/src/main/java/com/getaxonflow/sdk/telemetry/TelemetryReporter.java +++ b/src/main/java/com/getaxonflow/sdk/telemetry/TelemetryReporter.java @@ -231,6 +231,7 @@ public static final class EndpointType { public static final String LOCALHOST = "localhost"; public static final String PRIVATE_NETWORK = "private_network"; public static final String REMOTE = "remote"; + public static final String COMMUNITY_SAAS = "community-saas"; public static final String UNKNOWN = "unknown"; private EndpointType() {} @@ -248,6 +249,7 @@ private EndpointType() {} *

The raw URL is never sent — only the classification. */ public static String classifyEndpoint(String url) { + if ("1".equals(System.getenv("AXONFLOW_TRY"))) return EndpointType.COMMUNITY_SAAS; if (url == null || url.isEmpty()) { return EndpointType.UNKNOWN; }