feat(sdk): AuthConfigs, JSON Schema, examples, and release checksum CI

Add craftlabs-auth-config.schema.json, Java AuthConfigs model with tests,
example configs aligned to BP-10, C/Java/auth-config documentation,
native header notes, RELEASING guide, and workflow to verify SDK
artifact checksums on release tags.

Made-with: Cursor
This commit is contained in:
2026-04-06 21:05:12 +08:00
parent 65eb983035
commit f94f03bcc2
31 changed files with 1219 additions and 22 deletions
@@ -0,0 +1,78 @@
package cn.craftlabs.auth.config;
import static org.junit.jupiter.api.Assertions.*;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import org.junit.jupiter.api.Test;
class AuthConfigsTest {
@Test
void parseWharfExampleFromClasspath() throws Exception {
String json = readClasspath("examples/config/wharf.bitanswer.json");
AuthConfig c = AuthConfigs.parse(json);
assertEquals(1, c.schemaVersion());
assertEquals("bitanswer", c.provider());
assertEquals("wharf", c.scenario());
assertNotNull(c.bitanswer());
assertTrue(c.bitanswer().url().startsWith("bit://"));
assertEquals(101, c.bitanswerFeatureId("container_number_detect"));
assertEquals("group", c.wharf().topology());
}
@Test
void parseSchoolExample() throws Exception {
String json = readClasspath("examples/config/school.bitanswer.json");
AuthConfig c = AuthConfigs.parse(json);
assertEquals("school", c.scenario());
assertEquals(201, c.bitanswerFeatureId("face"));
assertEquals("classroom-gate-01", c.school().edgeDeviceId());
}
@Test
void parseFloatingExample_requiresProjectId() throws Exception {
String json = readClasspath("examples/config/floating.bitanswer.json");
AuthConfig c = AuthConfigs.parse(json);
assertEquals("floating", c.scenario());
assertEquals("migrant-flow-prj-2026-001", c.floating().projectId());
}
@Test
void validateFails_whenFloatingWithoutProject() {
String json =
"""
{"schemaVersion":1,"provider":"bitanswer","scenario":"floating",\
"bitanswer":{"url":"http://x"},"floating":{}}\
""";
AuthConfigException ex = assertThrows(AuthConfigException.class, () -> AuthConfigs.parse(json));
assertTrue(ex.getMessage().contains("projectId"));
}
@Test
void validateFails_whenBitanswerWithoutUrl() {
String json =
"""
{"schemaVersion":1,"provider":"bitanswer","scenario":"school",\
"bitanswer":{"url":""}}\
""";
assertThrows(AuthConfigException.class, () -> AuthConfigs.parse(json));
}
@Test
void roundTrip_toJson() throws Exception {
String json = readClasspath("examples/config/school.bitanswer.json");
AuthConfig c = AuthConfigs.parse(json);
String out = AuthConfigs.toJson(c);
AuthConfig again = AuthConfigs.parse(out);
assertEquals(c.scenario(), again.scenario());
assertEquals(c.bitanswerFeatureId("expression"), again.bitanswerFeatureId("expression"));
}
private static String readClasspath(String path) throws Exception {
try (InputStream in = AuthConfigsTest.class.getClassLoader().getResourceAsStream(path)) {
assertNotNull(in, "missing classpath resource: " + path);
return new String(in.readAllBytes(), StandardCharsets.UTF_8);
}
}
}
@@ -0,0 +1,69 @@
package cn.craftlabs.auth.config;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.networknt.schema.JsonSchema;
import com.networknt.schema.JsonSchemaFactory;
import com.networknt.schema.SpecVersion;
import com.networknt.schema.ValidationMessage;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Set;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* BP-10 / I5{@code examples/config/*.json} 与仓库根 {@code schemas/craftlabs-auth-config.schema.json} 一致。
*/
class ExamplesConfigSchemaValidationTest {
private static final ObjectMapper MAPPER = new ObjectMapper();
@Test
void allExampleConfigsValidateAgainstSchema() throws Exception {
Path repoRoot = findRepoRoot();
Path schemaPath = repoRoot.resolve("schemas/craftlabs-auth-config.schema.json");
Path examplesDir = repoRoot.resolve("examples/config");
assertTrue(Files.isRegularFile(schemaPath), "schema missing: " + schemaPath);
assertTrue(Files.isDirectory(examplesDir), "examples dir missing: " + examplesDir);
JsonNode schemaNode = MAPPER.readTree(schemaPath.toFile());
JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V202012);
JsonSchema schema = factory.getSchema(schemaNode);
try (Stream<Path> stream = Files.list(examplesDir)) {
stream.filter(p -> p.toString().endsWith(".json")).forEach(jsonFile -> {
try {
JsonNode instance = MAPPER.readTree(jsonFile.toFile());
Set<ValidationMessage> errors = schema.validate(instance);
assertTrue(
errors.isEmpty(),
() -> jsonFile.getFileName() + ": " + errors);
} catch (IOException e) {
throw new AssertionError(jsonFile.toString(), e);
}
});
}
}
/**
* 自 {@code user.dir} 向上查找含 {@code schemas/craftlabs-auth-config.schema.json} 的目录(兼容模块目录或仓库根执行)。
*/
static Path findRepoRoot() {
Path p = Path.of(System.getProperty("user.dir", ".")).toAbsolutePath().normalize();
for (int i = 0; i < 8 && p != null; i++) {
Path candidate = p.resolve("schemas/craftlabs-auth-config.schema.json");
if (Files.isRegularFile(candidate)) {
return p;
}
p = p.getParent();
}
throw new IllegalStateException(
"Could not find repo root (schemas/craftlabs-auth-config.schema.json) from user.dir="
+ System.getProperty("user.dir"));
}
}
@@ -0,0 +1,17 @@
{
"schemaVersion": 1,
"provider": "bitanswer",
"scenario": "floating",
"bitanswer": {
"url": "https://cloud.bitanswer.example/e3",
"loginMode": "REMOTE"
},
"features": {
"face": { "bitanswerFeatureId": 301 }
},
"floating": {
"projectId": "migrant-flow-prj-2026-001",
"projectName": "某市流动人口人像核验",
"contractRef": "PO-2026-8848"
}
}
@@ -0,0 +1,18 @@
{
"schemaVersion": 1,
"provider": "bitanswer",
"scenario": "school",
"bitanswer": {
"url": "https://cloud.bitanswer.example/e3",
"loginMode": "AUTO",
"sn": ""
},
"features": {
"face": { "bitanswerFeatureId": 201 },
"expression": { "bitanswerFeatureId": 202 }
},
"school": {
"edgeDeviceId": "classroom-gate-01",
"tenantId": "school-district-demo"
}
}
@@ -0,0 +1,21 @@
{
"schemaVersion": 1,
"provider": "bitanswer",
"scenario": "wharf",
"bitanswer": {
"url": "bit://license.example.com:8273",
"loginMode": "AUTO",
"rootPath": "/var/lib/craftlabs/bitanswer"
},
"features": {
"container_number_detect": { "bitanswerFeatureId": 101 },
"container_number_recognize": { "bitanswerFeatureId": 102 },
"iso_detect": { "bitanswerFeatureId": 103 },
"iso_recognize": { "bitanswerFeatureId": 104 }
},
"wharf": {
"topology": "group",
"groupServiceUrl": "bit://license.example.com:8273",
"notes": "边设备集中连集团服务;特征项与控制台产品一致后替换 id。"
}
}