Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,19 @@ 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).

## [8.3.0] - 2026-05-26 — Indonesia PII category + cross-border audit fields

### Added

- **`PII_INDONESIA` policy category constant** (`"pii-indonesia"`).
Enables filtering and creating policies for Indonesian PII detection
(NIK, KK, NPWP, BPJS) alongside the existing per-jurisdiction categories.
- **`dataResidency` and `transferBasis` fields on `AuditLogEntry`.**
Optional string fields supporting cross-border data transfer logging.
`dataResidency` is an ISO 3166-1 alpha-2 country code;
`transferBasis` is one of `adequacy`, `safeguards`, or `consent`.
Both are nullable for backward compatibility with older platform versions.

## [8.2.0] - 2026-05-23 — `createHITLRequest` for explicit HITL row creation

Enables agent-framework callers (Google ADK, n8n, OpenAI Agents SDK) to
Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

<groupId>com.getaxonflow</groupId>
<artifactId>axonflow-sdk</artifactId>
<version>8.2.0</version>
<version>8.3.0</version>
<packaging>jar</packaging>

<name>AxonFlow Java SDK</name>
Expand Down
36 changes: 33 additions & 3 deletions src/main/java/com/getaxonflow/sdk/types/AuditLogEntry.java
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,12 @@ public final class AuditLogEntry {
@JsonProperty("metadata")
private final Map<String, Object> metadata;

@JsonProperty("data_residency")
private final String dataResidency;

@JsonProperty("transfer_basis")
private final String transferBasis;

public AuditLogEntry(
@JsonProperty("id") String id,
@JsonProperty("request_id") String requestId,
Expand All @@ -95,7 +101,9 @@ public AuditLogEntry(
@JsonProperty("tokens_used") Integer tokensUsed,
@JsonProperty("latency_ms") Integer latencyMs,
@JsonProperty("policy_violations") List<String> policyViolations,
@JsonProperty("metadata") Map<String, Object> metadata) {
@JsonProperty("metadata") Map<String, Object> metadata,
@JsonProperty("data_residency") String dataResidency,
@JsonProperty("transfer_basis") String transferBasis) {
this.id = id != null ? id : "";
this.requestId = requestId != null ? requestId : "";
this.timestamp = timestamp != null ? timestamp : Instant.now();
Expand All @@ -113,6 +121,8 @@ public AuditLogEntry(
this.latencyMs = latencyMs != null ? latencyMs : 0;
this.policyViolations = policyViolations != null ? policyViolations : Collections.emptyList();
this.metadata = metadata != null ? metadata : Collections.emptyMap();
this.dataResidency = dataResidency;
this.transferBasis = transferBasis;
}

/** Returns the unique audit log ID. */
Expand Down Expand Up @@ -200,6 +210,16 @@ public Map<String, Object> getMetadata() {
return metadata;
}

/** Returns the ISO 3166-1 alpha-2 data residency country code, or null if not set. */
public String getDataResidency() {
return dataResidency;
}

/** Returns the cross-border transfer basis (adequacy, safeguards, or consent), or null if not set. */
public String getTransferBasis() {
return transferBasis;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
Expand All @@ -221,7 +241,9 @@ public boolean equals(Object o) {
&& Objects.equals(provider, that.provider)
&& Objects.equals(model, that.model)
&& Objects.equals(policyViolations, that.policyViolations)
&& Objects.equals(metadata, that.metadata);
&& Objects.equals(metadata, that.metadata)
&& Objects.equals(dataResidency, that.dataResidency)
&& Objects.equals(transferBasis, that.transferBasis);
}

@Override
Expand All @@ -243,7 +265,9 @@ public int hashCode() {
tokensUsed,
latencyMs,
policyViolations,
metadata);
metadata,
dataResidency,
transferBasis);
}

@Override
Expand All @@ -269,6 +293,12 @@ public String toString() {
+ blocked
+ ", riskScore="
+ riskScore
+ ", dataResidency='"
+ dataResidency
+ '\''
+ ", transferBasis='"
+ transferBasis
+ '\''
+ '}';
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ public enum PolicyCategory {
PII_EU("pii-eu"),
PII_INDIA("pii-india"),
PII_SINGAPORE("pii-singapore"),
PII_INDONESIA("pii-indonesia"),

// Static policy categories - Code Governance
CODE_SECRETS("code-secrets"),
Expand Down
138 changes: 138 additions & 0 deletions src/test/java/com/getaxonflow/sdk/types/IndonesiaPiiAuditTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
/*
* 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.
*/
package com.getaxonflow.sdk.types;

import static org.assertj.core.api.Assertions.*;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.getaxonflow.sdk.types.policies.PolicyTypes.PolicyCategory;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;

@DisplayName("Indonesia PII + AuditLogEntry cross-border fields")
class IndonesiaPiiAuditTest {

private static final ObjectMapper MAPPER =
new ObjectMapper().registerModule(new JavaTimeModule());

@Nested
@DisplayName("PolicyCategory.PII_INDONESIA")
class PiiIndonesiaCategory {

@Test
@DisplayName("PII_INDONESIA value should be 'pii-indonesia'")
void piiIndonesiaValueShouldBePiiIndonesia() {
assertThat(PolicyCategory.PII_INDONESIA.getValue()).isEqualTo("pii-indonesia");
}
}

@Nested
@DisplayName("AuditLogEntry cross-border fields")
class AuditLogEntryCrossBorderFields {

@Test
@DisplayName("should deserialize with data_residency and transfer_basis")
void shouldDeserializeWithCrossBorderFields() throws Exception {
String json =
"{"
+ "\"id\": \"audit-indo-1\","
+ "\"request_id\": \"req-1\","
+ "\"timestamp\": \"2026-05-26T10:00:00Z\","
+ "\"user_email\": \"user@example.com\","
+ "\"client_id\": \"client-1\","
+ "\"tenant_id\": \"tenant-1\","
+ "\"request_type\": \"llm_chat\","
+ "\"query_summary\": \"Test query\","
+ "\"success\": true,"
+ "\"blocked\": false,"
+ "\"risk_score\": 0.1,"
+ "\"provider\": \"openai\","
+ "\"model\": \"gpt-4\","
+ "\"tokens_used\": 150,"
+ "\"latency_ms\": 250,"
+ "\"policy_violations\": [],"
+ "\"metadata\": {},"
+ "\"data_residency\": \"ID\","
+ "\"transfer_basis\": \"consent\""
+ "}";

AuditLogEntry entry = MAPPER.readValue(json, AuditLogEntry.class);

assertThat(entry.getId()).isEqualTo("audit-indo-1");
assertThat(entry.getDataResidency()).isEqualTo("ID");
assertThat(entry.getTransferBasis()).isEqualTo("consent");
}

@Test
@DisplayName("should deserialize without cross-border fields (backward compat)")
void shouldDeserializeWithoutCrossBorderFields() throws Exception {
String json =
"{"
+ "\"id\": \"audit-old-1\","
+ "\"request_id\": \"req-2\","
+ "\"timestamp\": \"2026-05-26T10:00:00Z\","
+ "\"user_email\": \"user@example.com\","
+ "\"client_id\": \"client-1\","
+ "\"tenant_id\": \"tenant-1\","
+ "\"request_type\": \"llm_chat\","
+ "\"query_summary\": \"Old platform query\","
+ "\"success\": true,"
+ "\"blocked\": false,"
+ "\"risk_score\": 0.2,"
+ "\"provider\": \"openai\","
+ "\"model\": \"gpt-4\","
+ "\"tokens_used\": 100,"
+ "\"latency_ms\": 200,"
+ "\"policy_violations\": [],"
+ "\"metadata\": {}"
+ "}";

AuditLogEntry entry = MAPPER.readValue(json, AuditLogEntry.class);

assertThat(entry.getId()).isEqualTo("audit-old-1");
assertThat(entry.getDataResidency()).isNull();
assertThat(entry.getTransferBasis()).isNull();
}

@Test
@DisplayName("equals and hashCode should include cross-border fields")
void equalsAndHashCodeShouldIncludeCrossBorderFields() throws Exception {
String jsonWithFields =
"{"
+ "\"id\": \"audit-1\","
+ "\"data_residency\": \"ID\","
+ "\"transfer_basis\": \"adequacy\""
+ "}";
String jsonWithoutFields = "{\"id\": \"audit-1\"}";

AuditLogEntry with = MAPPER.readValue(jsonWithFields, AuditLogEntry.class);
AuditLogEntry without = MAPPER.readValue(jsonWithoutFields, AuditLogEntry.class);

assertThat(with).isNotEqualTo(without);
assertThat(with.hashCode()).isNotEqualTo(without.hashCode());
}

@Test
@DisplayName("toString should include cross-border fields")
void toStringShouldIncludeCrossBorderFields() throws Exception {
String json =
"{"
+ "\"id\": \"audit-1\","
+ "\"data_residency\": \"SG\","
+ "\"transfer_basis\": \"safeguards\""
+ "}";

AuditLogEntry entry = MAPPER.readValue(json, AuditLogEntry.class);
String str = entry.toString();

assertThat(str).contains("dataResidency='SG'");
assertThat(str).contains("transferBasis='safeguards'");
}
}
}
4 changes: 3 additions & 1 deletion tests/fixtures/wire-shape-baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -186,9 +186,11 @@
"per_type_drift": {
"AuditLogEntry": {
"sdk_only": [
"data_residency",
"metadata",
"model",
"policy_violations"
"policy_violations",
"transfer_basis"
],
"spec_only": []
},
Expand Down
Loading