package com.ruoyi.shadow.startup; import com.fasterxml.jackson.databind.ObjectMapper; import com.ruoyi.shadow.config.ShadowProperties; import com.ruoyi.shadow.kernel.ShadowDispatchSnapshotAssembler; import com.ruoyi.shadow.evidence.ShadowInputPayloadCodec; import org.springframework.core.env.Environment; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.util.Collections; import java.util.Locale; import java.util.Map; import java.util.TreeMap; /** Identifies this source revision and the nonsecret dispatch/storage settings, not a built artifact. */ public final class ShadowRuntimeIdentity { public static final String BUILD_ID = "ft546-shadow-20260910-source-recovery-v1"; private ShadowRuntimeIdentity() { } public static Map describe(ShadowProperties properties, Environment env) { Map settings = new TreeMap(); settings.put("enabled", properties.isEnabled()); settings.put("cycleDeadlineMillis", env.getProperty("shadow.cycle.deadline-ms", Long.class, 10000L)); settings.put("pollDelayMillis", env.getProperty("shadow.poll.fixed-delay-ms", Long.class, 5000L)); settings.put("initialDelayMillis", env.getProperty("shadow.poll.initial-delay-ms", Long.class, 5000L)); settings.put("maximumDatabaseBytes", properties.getEvidence().getMaximumDatabaseBytes()); settings.put("maximumBatchPayloadBytes", properties.getEvidence().getMaximumBatchPayloadBytes()); settings.put("minimumFreeBytes", properties.getEvidence().getMinimumFreeBytes()); settings.put("statusAddress", env.getProperty("server.address", "127.0.0.1")); settings.put("statusPort", env.getProperty("server.port", Integer.class, 18091)); settings.put("safeWaitSites", ShadowDispatchSnapshotAssembler.safeSites(properties.getTraffic().getSafeWaitSites())); settings.put("safeYieldSites", ShadowDispatchSnapshotAssembler.safeSites(properties.getTraffic().getSafeYieldSites())); settings.put("inputStorageFormat", ShadowInputPayloadCodec.GZIP); Map result = new TreeMap(); result.put("buildId", BUILD_ID); result.put("effectiveSettings", Collections.unmodifiableMap(settings)); result.put("effectiveSettingsSha256", digest(settings)); result.put("identityScope", "SOURCE_BUILD_LABEL_AND_NONSECRET_DISPATCH_STORAGE_SETTINGS"); result.put("operatorArtifactSha256", properties.getEvidence().getRun().getArtifactSha256()); result.put("operatorConfigurationSha256", properties.getEvidence().getRun().getConfigurationSha256()); result.put("safePointConfiguration", properties.getTraffic().getSafeWaitSites().isEmpty() && properties.getTraffic().getSafeYieldSites().isEmpty() ? "UNCONFIGURED" : "CONFIGURED_REQUIRES_TOPOLOGY_VALIDATION"); return Collections.unmodifiableMap(result); } private static String digest(Map settings) { try { byte[] bytes = new ObjectMapper().writeValueAsString(settings).getBytes(StandardCharsets.UTF_8); StringBuilder result = new StringBuilder(); for (byte b : MessageDigest.getInstance("SHA-256").digest(bytes)) result.append(String.format(Locale.ROOT, "%02x", b & 255)); return result.toString(); } catch (Exception impossible) { throw new IllegalStateException("RUNTIME_IDENTITY_FAILED", impossible); } } }