diff --git a/CHANGELOG.md b/CHANGELOG.md
index c1eb2df..11f434d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
diff --git a/pom.xml b/pom.xml
index db692af..dc68b66 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,7 +6,7 @@
com.getaxonflow
axonflow-sdk
- 8.2.0
+ 8.3.0
jar
AxonFlow Java SDK
diff --git a/src/main/java/com/getaxonflow/sdk/types/AuditLogEntry.java b/src/main/java/com/getaxonflow/sdk/types/AuditLogEntry.java
index 6e60e3f..9df545e 100644
--- a/src/main/java/com/getaxonflow/sdk/types/AuditLogEntry.java
+++ b/src/main/java/com/getaxonflow/sdk/types/AuditLogEntry.java
@@ -78,6 +78,12 @@ public final class AuditLogEntry {
@JsonProperty("metadata")
private final Map 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,
@@ -95,7 +101,9 @@ public AuditLogEntry(
@JsonProperty("tokens_used") Integer tokensUsed,
@JsonProperty("latency_ms") Integer latencyMs,
@JsonProperty("policy_violations") List policyViolations,
- @JsonProperty("metadata") Map metadata) {
+ @JsonProperty("metadata") Map 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();
@@ -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. */
@@ -200,6 +210,16 @@ public Map 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;
@@ -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
@@ -243,7 +265,9 @@ public int hashCode() {
tokensUsed,
latencyMs,
policyViolations,
- metadata);
+ metadata,
+ dataResidency,
+ transferBasis);
}
@Override
@@ -269,6 +293,12 @@ public String toString() {
+ blocked
+ ", riskScore="
+ riskScore
+ + ", dataResidency='"
+ + dataResidency
+ + '\''
+ + ", transferBasis='"
+ + transferBasis
+ + '\''
+ '}';
}
}
diff --git a/src/main/java/com/getaxonflow/sdk/types/policies/PolicyTypes.java b/src/main/java/com/getaxonflow/sdk/types/policies/PolicyTypes.java
index f18f248..29aa39f 100644
--- a/src/main/java/com/getaxonflow/sdk/types/policies/PolicyTypes.java
+++ b/src/main/java/com/getaxonflow/sdk/types/policies/PolicyTypes.java
@@ -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"),
diff --git a/src/test/java/com/getaxonflow/sdk/types/IndonesiaPiiAuditTest.java b/src/test/java/com/getaxonflow/sdk/types/IndonesiaPiiAuditTest.java
new file mode 100644
index 0000000..2c48e0d
--- /dev/null
+++ b/src/test/java/com/getaxonflow/sdk/types/IndonesiaPiiAuditTest.java
@@ -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'");
+ }
+ }
+}
diff --git a/tests/fixtures/wire-shape-baseline.json b/tests/fixtures/wire-shape-baseline.json
index 7644281..3cfa678 100644
--- a/tests/fixtures/wire-shape-baseline.json
+++ b/tests/fixtures/wire-shape-baseline.json
@@ -186,9 +186,11 @@
"per_type_drift": {
"AuditLogEntry": {
"sdk_only": [
+ "data_residency",
"metadata",
"model",
- "policy_violations"
+ "policy_violations",
+ "transfer_basis"
],
"spec_only": []
},