feat: add ABSDK 0630 bridge #3

Merged
KremeCN merged 3 commits from feat/absdk-bridge into main 2026-08-23 07:19:05 +08:00
7 changed files with 80 additions and 11 deletions
Showing only changes of commit 0e9b7dfd4b - Show all commits
@@ -208,13 +208,18 @@ class AbsdkBridge internal constructor(
private inline fun <T> withActiveSession( private inline fun <T> withActiveSession(
profileId: ProviderProfileId, profileId: ProviderProfileId,
block: () -> DomainResult<T>, block: () -> DomainResult<T>,
): DomainResult<T> = if (hasActiveSession(profileId)) { ): DomainResult<T> {
block() if (!hasActiveSession(profileId)) {
} else { return DomainResult.Failure(
DomainResult.Failure(
DomainError.TokenInvalid(detail = "No active ABSDK session for this provider"), DomainError.TokenInvalid(detail = "No active ABSDK session for this provider"),
) )
} }
return block().also { result ->
if (result is DomainResult.Failure && result.error is DomainError.TokenInvalid) {
activeSession.value = null
}
}
}
private fun hasActiveSession(profileId: ProviderProfileId): Boolean = private fun hasActiveSession(profileId: ProviderProfileId): Boolean =
activeSession.value?.profileId == profileId activeSession.value?.profileId == profileId
@@ -299,11 +304,17 @@ class AbsdkBridge internal constructor(
private const val DISABLED_STATUS = "0" private const val DISABLED_STATUS = "0"
private const val ENABLED_STATUS = "1" private const val ENABLED_STATUS = "1"
fun create(): AbsdkBridge = AbsdkBridge( @OptIn(CleartextAbsdkTransport::class)
private val processInstance: AbsdkBridge by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
AbsdkBridge(
client = VendorAbsdkClient.create(), client = VendorAbsdkClient.create(),
dispatcher = Dispatchers.IO, dispatcher = Dispatchers.IO,
) )
} }
@CleartextAbsdkTransport
fun create(): AbsdkBridge = processInstance
}
} }
private const val SUCCESS_CODE = "00000" private const val SUCCESS_CODE = "00000"
@@ -0,0 +1,22 @@
package com.flagship.abox.manager.absdk.bridge
/**
* Marks construction of the original ABSDK 0630 client, whose base endpoint is hardcoded to HTTP.
*
* Opting in acknowledges that transport confidentiality and integrity are not provided by the
* vendor JAR. App integration must remain disabled until the deployment has an approved transport
* control and the security limitation is presented to the responsible integrator.
*/
@RequiresOptIn(
message = "ABSDK 0630 uses a hardcoded cleartext HTTP transport. " +
"Do not enable it without an approved transport control.",
level = RequiresOptIn.Level.ERROR,
)
@Retention(AnnotationRetention.BINARY)
@Target(
AnnotationTarget.CLASS,
AnnotationTarget.CONSTRUCTOR,
AnnotationTarget.FUNCTION,
AnnotationTarget.PROPERTY,
)
annotation class CleartextAbsdkTransport
@@ -4,6 +4,7 @@ import com.dhc.absdk.ABRet
import com.dhc.absdk.ABSDK import com.dhc.absdk.ABSDK
import com.dhc.absdk.IrInfo import com.dhc.absdk.IrInfo
@CleartextAbsdkTransport
internal class VendorAbsdkClient private constructor( internal class VendorAbsdkClient private constructor(
private val sdk: ABSDK, private val sdk: ABSDK,
) : AbsdkClient { ) : AbsdkClient {
@@ -0,0 +1,12 @@
package com.flagship.abox.manager.absdk.bridge
import org.junit.Assert.assertSame
import org.junit.Test
@OptIn(CleartextAbsdkTransport::class)
class AbsdkFactoryTest {
@Test
fun productionFactoryReturnsOneProcessInstance() {
assertSame(AbsdkBridge.create(), AbsdkBridge.create())
}
}
@@ -91,6 +91,25 @@ class AbsdkSessionBridgeTest {
assertNull(bridge.observeSession(profile).first()) assertNull(bridge.observeSession(profile).first())
} }
@Test
fun deviceTokenErrorClearsSessionAndBlocksFurtherCalls() = runTest {
val client = ScriptedAbsdkClient().apply {
socketStatusResponses = ArrayDeque(
listOf(ScriptedAbsdkClient.failure("10002", "expired")),
)
}
val bridge = bridge(client)
bridge.login(profile, credentials)
val first = bridge.getStatus(profile, DeviceName("socket"))
assertTrue((first as DomainResult.Failure).error is DomainError.TokenInvalid)
assertNull(bridge.observeSession(profile).first())
val second = bridge.getStatus(profile, DeviceName("socket"))
assertTrue((second as DomainResult.Failure).error is DomainError.TokenInvalid)
assertEquals(listOf("login:demo", "socket-status:socket"), client.calls)
}
private fun bridge(client: ScriptedAbsdkClient): AbsdkBridge = private fun bridge(client: ScriptedAbsdkClient): AbsdkBridge =
AbsdkBridge(client, Dispatchers.Unconfined) AbsdkBridge(client, Dispatchers.Unconfined)
} }
+3 -1
View File
@@ -102,9 +102,11 @@ Fake 只用于开发和测试,不能作为真实 ABox 或真实硬件验收证
- 不提供设备目录; - 不提供设备目录;
- 不调用未确认的 `setHostInfo`; - 不调用未确认的 `setHostInfo`;
- logout 只清除 Bridge 本地会话,因为 JAR 没有公开 logout 或 Token setter; - logout 只清除 Bridge 本地会话,因为 JAR 没有公开 logout 或 Token setter;
- 生产工厂始终返回进程内同一个 Bridge,使互斥锁和本地会话与厂商单例保持一致;
- 真实工厂标记为 `@CleartextAbsdkTransport`,调用方必须显式 opt-in 承认厂商 JAR 的硬编码 HTTP 风险;
- 当前直接引用归档 Sample 目录中的唯一一份 JAR,不复制厂商二进制。 - 当前直接引用归档 Sample 目录中的唯一一份 JAR,不复制厂商二进制。
Bridge 本轮尚未加入 `android/app` 依赖,App debug 仍绑定 Fake。提供方选择、安全凭据和真实 Bridge 绑定由后续 `feat/provider-login` 完成。协议证据与限制记录在 `docs/protocol/absdk-0630.md`。 Bridge 本轮尚未加入 `android/app` 依赖,App debug 仍绑定 Fake。提供方选择、安全凭据和真实 Bridge 绑定由后续 `feat/provider-login` 完成;在获批的传输控制方案明确前,不得在 App 中启用真实 Bridge。协议证据与限制记录在 `docs/protocol/absdk-0630.md`。
## 9. 后续边界 ## 9. 后续边界
+2
View File
@@ -108,6 +108,8 @@ commons-logging-1.1.3.jar
JAR 内含宽松的 TLS 信任实现。正式产品不得把这一历史传输行为扩展为 App 其他网络请求的安全策略,也不得记录密码、Token、API Key 或敏感红外载荷。 JAR 内含宽松的 TLS 信任实现。正式产品不得把这一历史传输行为扩展为 App 其他网络请求的安全策略,也不得记录密码、Token、API Key 或敏感红外载荷。
真实 Bridge 工厂使用 `@CleartextAbsdkTransport` 作为编译期 opt-in 门禁,并且当前没有接入 Android App。该标记只防止误接入,不会为链路提供加密。生产集成前必须确定并批准 HTTPS、受控加密代理或其他能够提供机密性与完整性的传输控制;在此之前不得启用真实 ABox 入口。
## 6. `setHostInfo` 的已确认行为 ## 6. `setHostInfo` 的已确认行为
`setHostInfo(url, hotelName, roomNo, apikey)` 向以下端点发送初始化请求: `setHostInfo(url, hotelName, roomNo, apikey)` 向以下端点发送初始化请求: