Skip to content

Commit d11a932

Browse files
feat(indonesia): add pii-indonesia category, cross-border audit fields
Add PII_INDONESIA to PolicyCategory enum ("pii-indonesia") for Indonesian PII detection (NIK, KK, NPWP, BPJS). Add dataResidency and transferBasis nullable fields to AuditLogEntry for cross-border data transfer logging. Wire fields are data_residency and transfer_basis; both nullable for backward compatibility. Signed-off-by: Saurabh Jain <dev@getaxonflow.com> Signed-off-by: Saurabh Jain <saurabh.jain@getaxonflow.com>
1 parent d003a99 commit d11a932

4 files changed

Lines changed: 185 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,19 @@ All notable changes to the AxonFlow Java SDK will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [Unreleased]
9+
10+
### Added
11+
12+
- **`PII_INDONESIA` policy category constant** (`"pii-indonesia"`).
13+
Enables filtering and creating policies for Indonesian PII detection
14+
(NIK, KK, NPWP, BPJS) alongside the existing per-jurisdiction categories.
15+
- **`dataResidency` and `transferBasis` fields on `AuditLogEntry`.**
16+
Optional string fields supporting cross-border data transfer logging.
17+
`dataResidency` is an ISO 3166-1 alpha-2 country code;
18+
`transferBasis` is one of `adequacy`, `safeguards`, or `consent`.
19+
Both are nullable for backward compatibility with older platform versions.
20+
821
## [8.2.0] - 2026-05-23 — `createHITLRequest` for explicit HITL row creation
922

1023
Enables agent-framework callers (Google ADK, n8n, OpenAI Agents SDK) to

src/main/java/com/getaxonflow/sdk/types/AuditLogEntry.java

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,12 @@ public final class AuditLogEntry {
7878
@JsonProperty("metadata")
7979
private final Map<String, Object> metadata;
8080

81+
@JsonProperty("data_residency")
82+
private final String dataResidency;
83+
84+
@JsonProperty("transfer_basis")
85+
private final String transferBasis;
86+
8187
public AuditLogEntry(
8288
@JsonProperty("id") String id,
8389
@JsonProperty("request_id") String requestId,
@@ -95,7 +101,9 @@ public AuditLogEntry(
95101
@JsonProperty("tokens_used") Integer tokensUsed,
96102
@JsonProperty("latency_ms") Integer latencyMs,
97103
@JsonProperty("policy_violations") List<String> policyViolations,
98-
@JsonProperty("metadata") Map<String, Object> metadata) {
104+
@JsonProperty("metadata") Map<String, Object> metadata,
105+
@JsonProperty("data_residency") String dataResidency,
106+
@JsonProperty("transfer_basis") String transferBasis) {
99107
this.id = id != null ? id : "";
100108
this.requestId = requestId != null ? requestId : "";
101109
this.timestamp = timestamp != null ? timestamp : Instant.now();
@@ -113,6 +121,8 @@ public AuditLogEntry(
113121
this.latencyMs = latencyMs != null ? latencyMs : 0;
114122
this.policyViolations = policyViolations != null ? policyViolations : Collections.emptyList();
115123
this.metadata = metadata != null ? metadata : Collections.emptyMap();
124+
this.dataResidency = dataResidency;
125+
this.transferBasis = transferBasis;
116126
}
117127

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

213+
/** Returns the ISO 3166-1 alpha-2 data residency country code, or null if not set. */
214+
public String getDataResidency() {
215+
return dataResidency;
216+
}
217+
218+
/** Returns the cross-border transfer basis (adequacy, safeguards, or consent), or null if not set. */
219+
public String getTransferBasis() {
220+
return transferBasis;
221+
}
222+
203223
@Override
204224
public boolean equals(Object o) {
205225
if (this == o) return true;
@@ -221,7 +241,9 @@ public boolean equals(Object o) {
221241
&& Objects.equals(provider, that.provider)
222242
&& Objects.equals(model, that.model)
223243
&& Objects.equals(policyViolations, that.policyViolations)
224-
&& Objects.equals(metadata, that.metadata);
244+
&& Objects.equals(metadata, that.metadata)
245+
&& Objects.equals(dataResidency, that.dataResidency)
246+
&& Objects.equals(transferBasis, that.transferBasis);
225247
}
226248

227249
@Override
@@ -243,7 +265,9 @@ public int hashCode() {
243265
tokensUsed,
244266
latencyMs,
245267
policyViolations,
246-
metadata);
268+
metadata,
269+
dataResidency,
270+
transferBasis);
247271
}
248272

249273
@Override
@@ -269,6 +293,12 @@ public String toString() {
269293
+ blocked
270294
+ ", riskScore="
271295
+ riskScore
296+
+ ", dataResidency='"
297+
+ dataResidency
298+
+ '\''
299+
+ ", transferBasis='"
300+
+ transferBasis
301+
+ '\''
272302
+ '}';
273303
}
274304
}

src/main/java/com/getaxonflow/sdk/types/policies/PolicyTypes.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ public enum PolicyCategory {
5858
PII_EU("pii-eu"),
5959
PII_INDIA("pii-india"),
6060
PII_SINGAPORE("pii-singapore"),
61+
PII_INDONESIA("pii-indonesia"),
6162

6263
// Static policy categories - Code Governance
6364
CODE_SECRETS("code-secrets"),
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
/*
2+
* Copyright 2025 AxonFlow
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
*/
7+
package com.getaxonflow.sdk.types;
8+
9+
import static org.assertj.core.api.Assertions.*;
10+
11+
import com.fasterxml.jackson.databind.ObjectMapper;
12+
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
13+
import com.getaxonflow.sdk.types.policies.PolicyTypes.PolicyCategory;
14+
import org.junit.jupiter.api.DisplayName;
15+
import org.junit.jupiter.api.Nested;
16+
import org.junit.jupiter.api.Test;
17+
18+
@DisplayName("Indonesia PII + AuditLogEntry cross-border fields")
19+
class IndonesiaPiiAuditTest {
20+
21+
private static final ObjectMapper MAPPER =
22+
new ObjectMapper().registerModule(new JavaTimeModule());
23+
24+
@Nested
25+
@DisplayName("PolicyCategory.PII_INDONESIA")
26+
class PiiIndonesiaCategory {
27+
28+
@Test
29+
@DisplayName("PII_INDONESIA value should be 'pii-indonesia'")
30+
void piiIndonesiaValueShouldBePiiIndonesia() {
31+
assertThat(PolicyCategory.PII_INDONESIA.getValue()).isEqualTo("pii-indonesia");
32+
}
33+
}
34+
35+
@Nested
36+
@DisplayName("AuditLogEntry cross-border fields")
37+
class AuditLogEntryCrossBorderFields {
38+
39+
@Test
40+
@DisplayName("should deserialize with data_residency and transfer_basis")
41+
void shouldDeserializeWithCrossBorderFields() throws Exception {
42+
String json =
43+
"{"
44+
+ "\"id\": \"audit-indo-1\","
45+
+ "\"request_id\": \"req-1\","
46+
+ "\"timestamp\": \"2026-05-26T10:00:00Z\","
47+
+ "\"user_email\": \"user@example.com\","
48+
+ "\"client_id\": \"client-1\","
49+
+ "\"tenant_id\": \"tenant-1\","
50+
+ "\"request_type\": \"llm_chat\","
51+
+ "\"query_summary\": \"Test query\","
52+
+ "\"success\": true,"
53+
+ "\"blocked\": false,"
54+
+ "\"risk_score\": 0.1,"
55+
+ "\"provider\": \"openai\","
56+
+ "\"model\": \"gpt-4\","
57+
+ "\"tokens_used\": 150,"
58+
+ "\"latency_ms\": 250,"
59+
+ "\"policy_violations\": [],"
60+
+ "\"metadata\": {},"
61+
+ "\"data_residency\": \"ID\","
62+
+ "\"transfer_basis\": \"consent\""
63+
+ "}";
64+
65+
AuditLogEntry entry = MAPPER.readValue(json, AuditLogEntry.class);
66+
67+
assertThat(entry.getId()).isEqualTo("audit-indo-1");
68+
assertThat(entry.getDataResidency()).isEqualTo("ID");
69+
assertThat(entry.getTransferBasis()).isEqualTo("consent");
70+
}
71+
72+
@Test
73+
@DisplayName("should deserialize without cross-border fields (backward compat)")
74+
void shouldDeserializeWithoutCrossBorderFields() throws Exception {
75+
String json =
76+
"{"
77+
+ "\"id\": \"audit-old-1\","
78+
+ "\"request_id\": \"req-2\","
79+
+ "\"timestamp\": \"2026-05-26T10:00:00Z\","
80+
+ "\"user_email\": \"user@example.com\","
81+
+ "\"client_id\": \"client-1\","
82+
+ "\"tenant_id\": \"tenant-1\","
83+
+ "\"request_type\": \"llm_chat\","
84+
+ "\"query_summary\": \"Old platform query\","
85+
+ "\"success\": true,"
86+
+ "\"blocked\": false,"
87+
+ "\"risk_score\": 0.2,"
88+
+ "\"provider\": \"openai\","
89+
+ "\"model\": \"gpt-4\","
90+
+ "\"tokens_used\": 100,"
91+
+ "\"latency_ms\": 200,"
92+
+ "\"policy_violations\": [],"
93+
+ "\"metadata\": {}"
94+
+ "}";
95+
96+
AuditLogEntry entry = MAPPER.readValue(json, AuditLogEntry.class);
97+
98+
assertThat(entry.getId()).isEqualTo("audit-old-1");
99+
assertThat(entry.getDataResidency()).isNull();
100+
assertThat(entry.getTransferBasis()).isNull();
101+
}
102+
103+
@Test
104+
@DisplayName("equals and hashCode should include cross-border fields")
105+
void equalsAndHashCodeShouldIncludeCrossBorderFields() throws Exception {
106+
String jsonWithFields =
107+
"{"
108+
+ "\"id\": \"audit-1\","
109+
+ "\"data_residency\": \"ID\","
110+
+ "\"transfer_basis\": \"adequacy\""
111+
+ "}";
112+
String jsonWithoutFields = "{\"id\": \"audit-1\"}";
113+
114+
AuditLogEntry with = MAPPER.readValue(jsonWithFields, AuditLogEntry.class);
115+
AuditLogEntry without = MAPPER.readValue(jsonWithoutFields, AuditLogEntry.class);
116+
117+
assertThat(with).isNotEqualTo(without);
118+
assertThat(with.hashCode()).isNotEqualTo(without.hashCode());
119+
}
120+
121+
@Test
122+
@DisplayName("toString should include cross-border fields")
123+
void toStringShouldIncludeCrossBorderFields() throws Exception {
124+
String json =
125+
"{"
126+
+ "\"id\": \"audit-1\","
127+
+ "\"data_residency\": \"SG\","
128+
+ "\"transfer_basis\": \"safeguards\""
129+
+ "}";
130+
131+
AuditLogEntry entry = MAPPER.readValue(json, AuditLogEntry.class);
132+
String str = entry.toString();
133+
134+
assertThat(str).contains("dataResidency='SG'");
135+
assertThat(str).contains("transferBasis='safeguards'");
136+
}
137+
}
138+
}

0 commit comments

Comments
 (0)