Backend, sync, infra, docs: ETag, API versioning, k8s, web scaffold, Android 16, domain stubs

- Backend: ShallowEtagHeaderFilter for /api/v1/*, API-VERSIONING.md, README (tenant, CORS, Flyway, ETag)
- k8s: backend-deployment.yaml (Deployment, Service, Secret/ConfigMap)
- Web: scaffold with directory pull, 304 handling, touch-friendly UI
- Android 16: ANDROID-16-TARGET.md; BuildConfig STUN/signaling, SMOAApplication configures InfrastructureManager
- Domain: CertificateManager revocation stub, ReportService signReports, ZeroTrust/ThreatDetection minimal docs
- TODO.md and IMPLEMENTATION_STATUS.md updated; communications README for endpoint config

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
defiQUG
2026-02-10 20:37:01 -08:00
parent 97f75e144f
commit 5a8c26cf5d
101 changed files with 4923 additions and 103 deletions

View File

@@ -46,10 +46,9 @@ class CertificateManager @Inject constructor() {
/**
* Check certificate revocation status via OCSP/CRL.
* TODO: Implement actual OCSP/CRL checking
* Minimal implementation: returns UNKNOWN. Extend with an OCSP client or CRL fetcher for production.
*/
suspend fun checkRevocationStatus(certificate: X509Certificate): RevocationStatus {
// Placeholder - actual implementation will query OCSP responder or CRL
return RevocationStatus.UNKNOWN
}
}

View File

@@ -37,6 +37,7 @@ dependencies {
implementation(Dependencies.hiltAndroid)
kapt(Dependencies.hiltAndroidCompiler)
implementation("com.google.code.gson:gson:2.10.1")
// Testing
testImplementation(Dependencies.junit)

View File

@@ -0,0 +1,71 @@
package com.smoa.core.common
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import javax.inject.Inject
import javax.inject.Singleton
/**
* Circuit breaker for an endpoint or resource to improve system stability.
* After [failureThreshold] failures, the circuit opens and calls fail fast until [resetTimeoutMs] elapses.
*/
@Singleton
class CircuitBreaker @Inject constructor() {
private val mutex = Mutex()
private val state = mutableMapOf<String, EndpointState>()
/**
* Execute [block] if the circuit for [endpointId] is closed; otherwise throw [CircuitOpenException].
*/
suspend fun <T> execute(endpointId: String, failureThreshold: Int, resetTimeoutMs: Long, block: suspend () -> T): T {
mutex.withLock {
val s = state.getOrPut(endpointId) { EndpointState() }
if (s.failures >= failureThreshold && (System.currentTimeMillis() - s.lastFailureAt) < resetTimeoutMs) {
throw CircuitOpenException("Circuit open for $endpointId")
}
if ((System.currentTimeMillis() - s.lastFailureAt) >= resetTimeoutMs) {
s.failures = 0
}
}
return try {
block().also {
mutex.withLock {
state[endpointId]?.failures = 0
}
}
} catch (e: Exception) {
mutex.withLock {
val s = state.getOrPut(endpointId) { EndpointState() }
s.failures++
s.lastFailureAt = System.currentTimeMillis()
}
throw e
}
}
/** Check if circuit is open for [endpointId] (caller can skip calling the endpoint). */
fun isOpen(endpointId: String, failureThreshold: Int, resetTimeoutMs: Long): Boolean {
val s = state[endpointId] ?: return false
return s.failures >= failureThreshold && (System.currentTimeMillis() - s.lastFailureAt) < resetTimeoutMs
}
/** Reset failure count for [endpointId]. */
suspend fun reset(endpointId: String) {
mutex.withLock {
state.remove(endpointId)
}
}
/** Record a failure for [endpointId] (e.g. when a call to the endpoint fails). */
suspend fun recordFailure(endpointId: String) {
mutex.withLock {
val s = state.getOrPut(endpointId) { EndpointState() }
s.failures++
s.lastFailureAt = System.currentTimeMillis()
}
}
private data class EndpointState(var failures: Int = 0, var lastFailureAt: Long = 0L)
}
class CircuitOpenException(message: String) : Exception(message)

View File

@@ -3,6 +3,8 @@ package com.smoa.core.common
import android.content.Context
import android.net.Network
import android.net.NetworkCapabilities
import android.os.Build
import android.telephony.TelephonyManager
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -19,10 +21,15 @@ class ConnectivityManager @Inject constructor(
) {
private val systemConnectivityManager =
context.getSystemService(Context.CONNECTIVITY_SERVICE) as android.net.ConnectivityManager
private val telephonyManager: TelephonyManager? =
try { context.getSystemService(Context.TELEPHONY_SERVICE) as? TelephonyManager } catch (_: Exception) { null }
private val _connectivityState = MutableStateFlow<ConnectivityState>(ConnectivityState.Unknown)
val connectivityState: StateFlow<ConnectivityState> = _connectivityState.asStateFlow()
@Volatile
private var currentCapabilities: NetworkCapabilities? = null
private val networkCallback = object : android.net.ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
updateConnectivityState()
@@ -64,6 +71,7 @@ class ConnectivityManager @Inject constructor(
val capabilities = activeNetwork?.let {
systemConnectivityManager.getNetworkCapabilities(it)
}
currentCapabilities = capabilities
_connectivityState.value = when {
capabilities == null -> ConnectivityState.Offline
@@ -117,6 +125,49 @@ class ConnectivityManager @Inject constructor(
return _connectivityState.value
}
/**
* Get active network transport type for smart routing (QoS, lag reduction).
*/
fun getActiveTransportType(): NetworkTransportType {
val cap = currentCapabilities ?: return NetworkTransportType.UNKNOWN
return when {
cap.hasTransport(NetworkCapabilities.TRANSPORT_VPN) -> NetworkTransportType.VPN
cap.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> NetworkTransportType.WIFI
cap.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> NetworkTransportType.ETHERNET
cap.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> NetworkTransportType.CELLULAR
else -> NetworkTransportType.UNKNOWN
}
}
/**
* When active transport is cellular, returns 4G LTE, 5G, or 5G MW (millimeter wave).
* Requires READ_PHONE_STATE or READ_BASIC_PHONE_STATE for full accuracy on API 29+.
*/
fun getCellularGeneration(): CellularGeneration? {
if (getActiveTransportType() != NetworkTransportType.CELLULAR) return null
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) return CellularGeneration.LTE_4G
val tm = telephonyManager ?: return null
@Suppress("DEPRECATION")
val networkType = tm.dataNetworkType
return when (networkType) {
TelephonyManager.NETWORK_TYPE_LTE -> CellularGeneration.LTE_4G
TelephonyManager.NETWORK_TYPE_NR -> cellularGenerationFrom5G(tm)
else -> CellularGeneration.UNKNOWN
}
}
@Suppress("DEPRECATION")
private fun cellularGenerationFrom5G(tm: TelephonyManager): CellularGeneration {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return CellularGeneration.NR_5G
return try {
val displayInfo = tm.telephonyDisplayInfo
val override = displayInfo.overrideNetworkType
if (override == 5) CellularGeneration.NR_5G_MW else CellularGeneration.NR_5G
} catch (_: Throwable) {
CellularGeneration.NR_5G
}
}
enum class ConnectivityState {
Online,
Offline,
@@ -125,3 +176,25 @@ class ConnectivityManager @Inject constructor(
}
}
/**
* Network transport type for path selection and QoS.
*/
enum class NetworkTransportType {
WIFI,
CELLULAR,
VPN,
ETHERNET,
UNKNOWN
}
/**
* Cellular generation when transport is CELLULAR: 4G LTE, 5G NR, or 5G MW (millimeter wave).
* Used by smart routing to prefer 5G / 5G MW over 4G for lower latency and higher capacity.
*/
enum class CellularGeneration {
LTE_4G,
NR_5G,
NR_5G_MW,
UNKNOWN
}

View File

@@ -0,0 +1,24 @@
package com.smoa.core.common
/**
* API for pulling (GET) directory, orders, evidence, credentials, and reports from the backend.
* Used when connectivity is restored to refresh local data.
*/
interface PullAPI {
suspend fun pullDirectory(unit: String? = null): Result<ByteArray>
suspend fun pullOrders(since: Long? = null, limit: Int = 100, jurisdiction: String? = null): Result<ByteArray>
suspend fun pullEvidence(since: Long? = null, limit: Int = 100, caseNumber: String? = null): Result<ByteArray>
suspend fun pullCredentials(since: Long? = null, limit: Int = 100, holderId: String? = null): Result<ByteArray>
suspend fun pullReports(since: Long? = null, limit: Int = 100): Result<ByteArray>
}
/**
* No-op implementation when backend is not configured.
*/
class DefaultPullAPI : PullAPI {
override suspend fun pullDirectory(unit: String?) = Result.Success(ByteArray(0))
override suspend fun pullOrders(since: Long?, limit: Int, jurisdiction: String?) = Result.Success(ByteArray(0))
override suspend fun pullEvidence(since: Long?, limit: Int, caseNumber: String?) = Result.Success(ByteArray(0))
override suspend fun pullCredentials(since: Long?, limit: Int, holderId: String?) = Result.Success(ByteArray(0))
override suspend fun pullReports(since: Long?, limit: Int) = Result.Success(ByteArray(0))
}

View File

@@ -0,0 +1,31 @@
package com.smoa.core.common
/**
* Traffic classification for QoS (Quality of Service).
* Used by smart routing and media stack to prioritize voice, video, signaling, and data.
*/
enum class TrafficClass(val priority: Int, val description: String) {
/** Real-time voice; highest priority for lag reduction. */
VOICE(4, "Real-time voice"),
/** Real-time video; high priority. */
VIDEO(3, "Real-time video"),
/** Signaling (ICE, SDP, etc.); must not be delayed. */
SIGNALING(2, "Signaling"),
/** Best-effort data (file transfer, presence). */
DATA(1, "Best-effort data")
}
/**
* QoS policy for media routing: which traffic class to prefer under congestion,
* and optional caps for system stability.
*/
data class QoSPolicy(
val voicePriority: Int = 4,
val videoPriority: Int = 3,
val signalingPriority: Int = 2,
val dataPriority: Int = 1,
/** Max concurrent media sessions (0 = unlimited). */
val maxConcurrentSessions: Int = 0,
/** Max total send bitrate in bps (0 = unlimited). */
val maxTotalSendBitrateBps: Int = 0
)

View File

@@ -29,6 +29,31 @@ interface SyncAPI {
* Sync report to backend.
*/
suspend fun syncReport(reportData: ByteArray): Result<SyncResponse>
/**
* Delete directory entry on backend (SyncOperation.Delete).
*/
suspend fun deleteDirectory(id: String): Result<SyncResponse>
/**
* Delete order on backend.
*/
suspend fun deleteOrder(orderId: String): Result<SyncResponse>
/**
* Delete evidence on backend.
*/
suspend fun deleteEvidence(evidenceId: String): Result<SyncResponse>
/**
* Delete credential on backend.
*/
suspend fun deleteCredential(credentialId: String): Result<SyncResponse>
/**
* Delete report on backend.
*/
suspend fun deleteReport(reportId: String): Result<SyncResponse>
}
/**
@@ -103,5 +128,20 @@ class DefaultSyncAPI : SyncAPI {
)
)
}
override suspend fun deleteDirectory(id: String): Result<SyncResponse> =
Result.Success(SyncResponse(success = true, itemId = id, serverTimestamp = System.currentTimeMillis()))
override suspend fun deleteOrder(orderId: String): Result<SyncResponse> =
Result.Success(SyncResponse(success = true, itemId = orderId, serverTimestamp = System.currentTimeMillis()))
override suspend fun deleteEvidence(evidenceId: String): Result<SyncResponse> =
Result.Success(SyncResponse(success = true, itemId = evidenceId, serverTimestamp = System.currentTimeMillis()))
override suspend fun deleteCredential(credentialId: String): Result<SyncResponse> =
Result.Success(SyncResponse(success = true, itemId = credentialId, serverTimestamp = System.currentTimeMillis()))
override suspend fun deleteReport(reportId: String): Result<SyncResponse> =
Result.Success(SyncResponse(success = true, itemId = reportId, serverTimestamp = System.currentTimeMillis()))
}

View File

@@ -1,6 +1,7 @@
package com.smoa.core.common
import android.content.Context
import com.google.gson.Gson
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
@@ -12,15 +13,25 @@ import javax.inject.Singleton
* Offline synchronization service.
* Handles data synchronization when connectivity is restored.
*/
/**
* Emitted when pull-on-connect runs; observers can merge into local DB.
*/
data class PullResultData(val resourceType: String, val data: ByteArray)
@Singleton
class SyncService @Inject constructor(
private val context: Context,
private val connectivityManager: ConnectivityManager,
private val syncAPI: SyncAPI = DefaultSyncAPI()
private val syncAPI: SyncAPI = DefaultSyncAPI(),
private val pullAPI: PullAPI = DefaultPullAPI()
) {
private val gson = Gson()
private val _syncState = MutableStateFlow<SyncState>(SyncState.Idle)
val syncState: StateFlow<SyncState> = _syncState.asStateFlow()
private val _pullResults = MutableStateFlow<List<PullResultData>>(emptyList())
val pullResults: StateFlow<List<PullResultData>> = _pullResults.asStateFlow()
private val syncQueue = mutableListOf<SyncItem>()
private val conflictResolver = ConflictResolver()
@@ -36,7 +47,7 @@ class SyncService @Inject constructor(
}
/**
* Start synchronization process.
* Start synchronization process. When online and pullAPI is set, runs pull first and emits to pullResults for merge.
*/
suspend fun startSync() {
if (!connectivityManager.isOnline()) {
@@ -44,6 +55,16 @@ class SyncService @Inject constructor(
return
}
if (pullAPI !is DefaultPullAPI) {
val results = mutableListOf<PullResultData>()
pullAPI.pullDirectory(null).let { if (it is Result.Success) results.add(PullResultData("directory", it.data)) }
pullAPI.pullOrders(null, 100, null).let { if (it is Result.Success) results.add(PullResultData("orders", it.data)) }
pullAPI.pullEvidence(null, 100, null).let { if (it is Result.Success) results.add(PullResultData("evidence", it.data)) }
pullAPI.pullCredentials(null, 100, null).let { if (it is Result.Success) results.add(PullResultData("credentials", it.data)) }
pullAPI.pullReports(null, 100).let { if (it is Result.Success) results.add(PullResultData("reports", it.data)) }
if (results.isNotEmpty()) _pullResults.value = results
}
if (syncQueue.isEmpty()) {
_syncState.value = SyncState.Idle
return
@@ -83,27 +104,29 @@ class SyncService @Inject constructor(
}
/**
* Sync a single item.
* Sync a single item (create/update or delete).
*/
private suspend fun syncItem(item: SyncItem) {
// Implement sync logic based on item type
// In a full implementation, this would call appropriate service methods
if (item.operation == SyncOperation.Delete) {
val result = when (item.type) {
SyncItemType.Order -> syncAPI.deleteOrder(item.id)
SyncItemType.Evidence -> syncAPI.deleteEvidence(item.id)
SyncItemType.Credential -> syncAPI.deleteCredential(item.id)
SyncItemType.Directory -> syncAPI.deleteDirectory(item.id)
SyncItemType.Report -> syncAPI.deleteReport(item.id)
}
when (result) {
is Result.Error -> throw result.exception
else -> Unit
}
return
}
when (item.type) {
SyncItemType.Order -> {
syncOrder(item)
}
SyncItemType.Evidence -> {
syncEvidence(item)
}
SyncItemType.Credential -> {
syncCredential(item)
}
SyncItemType.Directory -> {
syncDirectoryEntry(item)
}
SyncItemType.Report -> {
syncReport(item)
}
SyncItemType.Order -> syncOrder(item)
SyncItemType.Evidence -> syncEvidence(item)
SyncItemType.Credential -> syncCredential(item)
SyncItemType.Directory -> syncDirectoryEntry(item)
SyncItemType.Report -> syncReport(item)
}
}
@@ -258,45 +281,14 @@ class SyncService @Inject constructor(
}
/**
* Serialize order data for transmission.
* Serialize data for transmission. Expects data to be a Map or object with property names
* matching backend DTOs (camelCase). Uses Gson for JSON serialization.
*/
private fun serializeOrderData(data: Any): ByteArray {
// TODO: Use proper JSON serialization (e.g., Jackson, Gson)
// For now, return empty array as placeholder
return ByteArray(0)
}
/**
* Serialize evidence data for transmission.
*/
private fun serializeEvidenceData(data: Any): ByteArray {
// TODO: Use proper JSON serialization
return ByteArray(0)
}
/**
* Serialize credential data for transmission.
*/
private fun serializeCredentialData(data: Any): ByteArray {
// TODO: Use proper JSON serialization
return ByteArray(0)
}
/**
* Serialize directory entry data for transmission.
*/
private fun serializeDirectoryEntryData(data: Any): ByteArray {
// TODO: Use proper JSON serialization
return ByteArray(0)
}
/**
* Serialize report data for transmission.
*/
private fun serializeReportData(data: Any): ByteArray {
// TODO: Use proper JSON serialization
return ByteArray(0)
}
private fun serializeOrderData(data: Any): ByteArray = gson.toJson(data).toByteArray(Charsets.UTF_8)
private fun serializeEvidenceData(data: Any): ByteArray = gson.toJson(data).toByteArray(Charsets.UTF_8)
private fun serializeCredentialData(data: Any): ByteArray = gson.toJson(data).toByteArray(Charsets.UTF_8)
private fun serializeDirectoryEntryData(data: Any): ByteArray = gson.toJson(data).toByteArray(Charsets.UTF_8)
private fun serializeReportData(data: Any): ByteArray = gson.toJson(data).toByteArray(Charsets.UTF_8)
/**
* Check if offline duration threshold has been exceeded.

View File

@@ -31,9 +31,11 @@ object CommonModule {
@Singleton
fun provideSyncService(
@ApplicationContext context: Context,
connectivityManager: ConnectivityManager
connectivityManager: ConnectivityManager,
syncAPI: com.smoa.core.common.SyncAPI,
pullAPI: com.smoa.core.common.PullAPI
): com.smoa.core.common.SyncService {
return com.smoa.core.common.SyncService(context, connectivityManager)
return com.smoa.core.common.SyncService(context, connectivityManager, syncAPI, pullAPI)
}
@Provides

View File

@@ -3,6 +3,7 @@ package com.smoa.core.common
import com.smoa.core.common.SyncAPI
import com.smoa.core.common.SyncResponse
import io.mockk.coEvery
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.test.runTest
import org.junit.Assert.*
@@ -16,7 +17,8 @@ class SyncServiceTest {
private val context = mockk<android.content.Context>(relaxed = true)
private val connectivityManager = mockk<ConnectivityManager>(relaxed = true)
private val syncAPI = mockk<SyncAPI>(relaxed = true)
private val syncService = SyncService(context, connectivityManager, syncAPI)
private val pullAPI = com.smoa.core.common.DefaultPullAPI()
private val syncService = SyncService(context, connectivityManager, syncAPI, pullAPI)
@Test
fun `queueSync should add item to queue`() = runTest {
@@ -45,7 +47,7 @@ class SyncServiceTest {
data = "test data"
)
every { connectivityManager.isOnline() } returns true
coEvery { syncAPI.syncOrder(any()) } returns Result.success(
coEvery { syncAPI.syncOrder(any()) } returns Result.Success(
SyncResponse(
success = true,
itemId = "test1",

View File

@@ -16,7 +16,7 @@ class ThreatDetection @Inject constructor(
* Detect anomalies in user behavior.
*/
suspend fun detectAnomalies(userId: String, activity: UserActivity): Result<ThreatAssessment> {
// TODO: Implement machine learning-based anomaly detection
// Minimal implementation; extend for production (e.g. ML-based anomaly detection).
return Result.success(ThreatAssessment.NORMAL)
}
@@ -24,7 +24,7 @@ class ThreatDetection @Inject constructor(
* Analyze security events for threats.
*/
suspend fun analyzeSecurityEvents(events: List<SecurityEvent>): Result<ThreatReport> {
// TODO: Implement threat analysis
// Minimal implementation; extend for production (e.g. SIEM integration, rule engine).
return Result.success(ThreatReport(emptyList(), ThreatLevel.LOW))
}
}

View File

@@ -20,8 +20,7 @@ class ZeroTrustFramework @Inject constructor(
resource: String,
action: String
): Result<TrustVerification> {
// Zero-trust: verify every access attempt
// TODO: Implement comprehensive trust verification
// Minimal implementation; extend for production (e.g. device posture, MFA, policy engine).
return Result.success(TrustVerification(trusted = true, verificationLevel = VerificationLevel.MULTI_FACTOR))
}