Establish secure account and managed AI backend

Provide the production foundation for Apple identity, immutable credits, referrals, integrity checks, managed providers, and hardened Docker deployment.
This commit is contained in:
Rocky
2026-08-16 14:46:23 +08:00
commit 0af35d44f4
124 changed files with 21052 additions and 0 deletions
@@ -0,0 +1,511 @@
package com.osglab.account
import com.osglab.account.common.api.installApiStatusPages
import com.osglab.account.common.security.FieldEncryptor
import com.osglab.account.common.security.IdentityFingerprint
import com.osglab.account.common.security.SessionJwt
import com.osglab.account.common.security.installSessionAuthentication
import com.osglab.account.config.AppConfig
import com.osglab.account.config.DatabaseFactory
import com.osglab.account.features.account.AccountRepository
import com.osglab.account.features.account.AccountReauthenticator
import com.osglab.account.features.account.AccountService
import com.osglab.account.features.account.AppleAccountReauthenticator
import com.osglab.account.features.account.AppleRevocationOutboxProcessor
import com.osglab.account.features.account.ExposedAccountRepository
import com.osglab.account.features.account.accountRoutes
import com.osglab.account.features.appleevents.AppleEventService
import com.osglab.account.features.appleevents.AppleEventRepository
import com.osglab.account.features.appleevents.AppleEventVerifier
import com.osglab.account.features.appleevents.ExposedAppleEventRepository
import com.osglab.account.features.appleevents.appleEventRoutes
import com.osglab.account.features.auth.AccountProvisioner
import com.osglab.account.features.auth.AppleIdentityTokenVerifier
import com.osglab.account.features.auth.AppleJwksProvider
import com.osglab.account.features.auth.AppleTokenClient
import com.osglab.account.features.auth.AuthRepository
import com.osglab.account.features.auth.ExposedAuthRepository
import com.osglab.account.features.auth.RemoteAppleJwksProvider
import com.osglab.account.features.auth.SessionService
import com.osglab.account.features.auth.SessionAccessAuthenticator
import com.osglab.account.features.auth.authRoutes
import com.osglab.account.features.auth.createAppleTokenClient
import com.osglab.account.features.credits.repositories.BillingTransactionRunner
import com.osglab.account.features.credits.repositories.ExposedBillingTransactionRunner
import com.osglab.account.features.credits.routes.AuthenticatedUserExtractor
import com.osglab.account.features.credits.routes.creditRoutes
import com.osglab.account.features.credits.services.CreditService
import com.osglab.account.features.credits.services.ReferralRewardConfig
import com.osglab.account.features.gateway.adapters.CreditReservationAdapter
import com.osglab.account.features.gateway.adapters.SessionIdentityAdapter
import com.osglab.account.features.gateway.GatewaySettings
import com.osglab.account.features.gateway.asr.AsrStreamingService
import com.osglab.account.features.gateway.ports.CreditReservationPort
import com.osglab.account.features.gateway.ports.GatewayAccessTokenPort
import com.osglab.account.features.gateway.ports.GatewayGrantPort
import com.osglab.account.features.gateway.ports.GatewayGrantRepository
import com.osglab.account.features.gateway.ports.GatewayIdentityPort
import com.osglab.account.features.gateway.ports.GatewayUsagePort
import com.osglab.account.features.gateway.providers.GatewayProvider
import com.osglab.account.features.gateway.providers.ProviderCatalog
import com.osglab.account.features.gateway.providers.deepseek.DeepSeekConfig
import com.osglab.account.features.gateway.providers.deepseek.DeepSeekProvider
import com.osglab.account.features.gateway.providers.volcengine.KtorVolcengineAsrTransport
import com.osglab.account.features.gateway.providers.volcengine.VolcengineAsrConfig
import com.osglab.account.features.gateway.providers.volcengine.VolcengineAsrProvider
import com.osglab.account.features.gateway.repositories.ExposedGatewayRepository
import com.osglab.account.features.gateway.routes.configureGatewayRoutes
import com.osglab.account.features.gateway.services.GatewayBearerIdentity
import com.osglab.account.features.gateway.services.GatewayGrantService
import com.osglab.account.features.gateway.services.GatewayReconciliationService
import com.osglab.account.features.gateway.services.GatewayService
import com.osglab.account.features.integrity.AppAttestCrypto
import com.osglab.account.features.integrity.AppAttestRepository
import com.osglab.account.features.integrity.AppAttestService
import com.osglab.account.features.integrity.AppAttestVerifier
import com.osglab.account.features.integrity.AppleDeviceCheckClient
import com.osglab.account.features.integrity.BundledAppleAppAttestTrust
import com.osglab.account.features.integrity.DeviceCheckTrialClaimRepository
import com.osglab.account.features.integrity.DeviceCheckTrialService
import com.osglab.account.features.integrity.DeviceCheckVerifier
import com.osglab.account.features.integrity.ExposedAppAttestRepository
import com.osglab.account.features.integrity.ExposedDeviceCheckTrialClaimRepository
import com.osglab.account.features.integrity.IntegrityService
import com.osglab.account.features.integrity.LibraryAppAttestCrypto
import com.osglab.account.features.integrity.MysqlDeviceCheckTrialMutex
import com.osglab.account.features.integrity.RemoteDeviceCheckVerifier
import com.osglab.account.features.integrity.TrialCreditGranter
import com.osglab.account.features.integrity.UnavailableAppleDeviceCheckClient
import com.osglab.account.features.integrity.createDeviceCheckClient
import com.osglab.account.features.integrity.integrityRoutes
import com.osglab.account.features.inviteweb.InviteWebConfig
import com.osglab.account.features.inviteweb.ReferralLookupPort
import com.osglab.account.features.inviteweb.configureInviteWebRoutes
import com.osglab.account.features.referrals.routes.referralRoutes
import com.osglab.account.features.referrals.services.ReferralService
import com.osglab.account.features.referrals.services.ReferralRiskIdentity
import com.osglab.account.features.referrals.services.ReferralRiskProvider
import com.osglab.account.features.referrals.services.UserRegistrationTimeProvider
import io.ktor.client.HttpClient
import io.ktor.client.engine.cio.CIO
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation as ClientContentNegotiation
import io.ktor.client.plugins.HttpTimeout
import io.ktor.client.plugins.websocket.WebSockets as ClientWebSockets
import io.ktor.http.ContentType
import io.ktor.http.HttpStatusCode
import io.ktor.serialization.kotlinx.json.json
import io.ktor.server.application.Application
import io.ktor.server.application.ApplicationStopped
import io.ktor.server.application.install
import io.ktor.server.plugins.callid.CallId
import io.ktor.server.plugins.callid.callIdMdc
import io.ktor.server.plugins.calllogging.CallLogging
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
import io.ktor.server.plugins.defaultheaders.DefaultHeaders
import io.ktor.server.plugins.forwardedheaders.XForwardedHeaders
import io.ktor.server.plugins.ratelimit.RateLimit
import io.ktor.server.plugins.ratelimit.RateLimitName
import io.ktor.server.plugins.ratelimit.rateLimit
import io.ktor.server.request.httpMethod
import io.ktor.server.response.respond
import io.ktor.server.response.respondText
import io.ktor.server.routing.get
import io.ktor.server.routing.Route
import io.ktor.server.routing.routing
import io.ktor.server.websocket.WebSockets
import kotlinx.serialization.json.Json
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import org.koin.core.module.Module
import org.koin.dsl.module
import org.koin.ktor.ext.getKoin
import org.koin.ktor.plugin.Koin
import org.koin.logger.slf4jLogger
import java.time.Duration
import java.time.Instant
import java.util.UUID
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
import kotlin.time.Duration.Companion.minutes
private val JSON = Json {
ignoreUnknownKeys = true
explicitNulls = false
encodeDefaults = true
}
fun Application.module() {
val appConfig = AppConfig.from(environment.config)
install(ContentNegotiation) {
json(JSON)
}
install(DefaultHeaders) {
header("X-Content-Type-Options", "nosniff")
header("X-Frame-Options", "DENY")
header("Referrer-Policy", "no-referrer")
if (appConfig.isProduction) {
header("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
}
}
install(CallId) {
retrieveFromHeader("X-Request-ID")
verify { it.matches(REQUEST_ID) }
generate { UUID.randomUUID().toString() }
}
install(CallLogging) {
callIdMdc("requestId")
// Never include paths, headers or bodies: they may contain invitation or session tokens.
format { call -> "${call.request.httpMethod.value} status=${call.response.status()}" }
}
install(WebSockets) {
maxFrameSize = 4L * 1024 * 1024
masking = false
}
install(XForwardedHeaders)
install(RateLimit) {
register(AUTH_RATE_LIMIT) {
rateLimiter(limit = 10, refillPeriod = 1.minutes)
}
register(ACCOUNT_RATE_LIMIT) {
rateLimiter(limit = 60, refillPeriod = 1.minutes)
}
register(GATEWAY_RATE_LIMIT) {
rateLimiter(limit = 120, refillPeriod = 1.minutes)
}
register(PUBLIC_RATE_LIMIT) {
rateLimiter(limit = 120, refillPeriod = 1.minutes)
}
}
installApiStatusPages()
install(Koin) {
slf4jLogger()
modules(accountServerModule(appConfig))
}
val koin = getKoin()
// Fail startup before accepting traffic if migrations or database connectivity fail.
koin.get<DatabaseFactory>().database
val sessionAuthenticator = koin.get<SessionAccessAuthenticator>()
installSessionAuthentication(sessionAuthenticator::authenticate)
val asrStreaming = if (appConfig.providers.volcengine.credentialsAvailable) {
val providerConfig = appConfig.providers.volcengine.toProviderConfig()
AsrStreamingService(
gateway = koin.get(),
upstream = KtorVolcengineAsrTransport(koin.get(), providerConfig),
scope = this,
)
} else {
null
}
launch {
while (isActive) {
try {
koin.get<AppleRevocationOutboxProcessor>().processPending()
} catch (exception: CancellationException) {
throw exception
} catch (_: Exception) {
// Durable outbox state is retried; never log sensitive token material.
}
try {
koin.get<GatewayReconciliationService>().reconcile()
} catch (exception: CancellationException) {
throw exception
} catch (_: Exception) {
// Durable settlement state is retried without logging provider data.
}
delay(60_000)
}
}
monitor.subscribe(ApplicationStopped) {
koin.get<HttpClient>().close()
koin.get<DatabaseFactory>().close()
}
routing {
healthRoutes(koin.get())
rateLimit(AUTH_RATE_LIMIT) {
authRoutes(koin.get())
}
rateLimit(ACCOUNT_RATE_LIMIT) {
accountRoutes(koin.get())
creditRoutes(koin.get(), koin.get())
referralRoutes(koin.get(), koin.get())
}
rateLimit(GATEWAY_RATE_LIMIT) {
configureGatewayRoutes(
service = koin.get(),
appIdentity = koin.get<GatewayIdentityPort>(),
gatewayIdentity = koin.get<GatewayAccessTokenPort>(),
grantService = koin.get(),
asrStreaming = asrStreaming,
)
}
rateLimit(PUBLIC_RATE_LIMIT) {
appleEventRoutes(koin.get())
configureInviteWebRoutes(koin.get(), koin.get())
integrityRoutes(koin.get())
}
}
}
fun Route.healthRoutes(databaseFactory: DatabaseFactory? = null) {
get("/health") { call.respondText("""{"status":"UP"}""", ContentType.Application.Json) }
get("/health/live") { call.respondText("""{"status":"UP"}""", ContentType.Application.Json) }
get("/health/ready") {
if (databaseFactory?.isReady() == true) {
call.respondText("""{"status":"UP"}""", ContentType.Application.Json)
} else {
call.respondText(
"""{"status":"DOWN"}""",
ContentType.Application.Json,
HttpStatusCode.ServiceUnavailable,
)
}
}
}
fun accountServerModule(config: AppConfig): Module = module {
single { config }
single { DatabaseFactory(config.database) }
single { get<DatabaseFactory>().database }
single {
HttpClient(CIO) {
followRedirects = false
install(HttpTimeout) {
connectTimeoutMillis = 10_000
socketTimeoutMillis = 60_000
requestTimeoutMillis = 360_000
}
install(ClientContentNegotiation) {
json(JSON)
}
install(ClientWebSockets) {
maxFrameSize = 4L * 1024 * 1024
}
}
}
single { SessionJwt(config.session) }
single { FieldEncryptor(config.encryption.key) }
single { IdentityFingerprint(config.antiAbuse.identityHmacKey) }
single<AppleJwksProvider> {
RemoteAppleJwksProvider(get(), config.apple.jwksUrl)
}
single { AppleIdentityTokenVerifier(config.apple, get()) }
single<AppleTokenClient> { createAppleTokenClient(get(), config.apple) }
single<AppleDeviceCheckClient> {
createDeviceCheckClient(get(), config.apple, config.integrity.appleEnvironment)
?: UnavailableAppleDeviceCheckClient()
}
single<DeviceCheckVerifier> { RemoteDeviceCheckVerifier(get()) }
single<AppAttestRepository> { ExposedAppAttestRepository(get()) }
single<AppAttestCrypto> {
LibraryAppAttestCrypto(
config = config.integrity,
certificateValidator = BundledAppleAppAttestTrust.validator(),
)
}
single { AppAttestService(get(), get(), config.integrity) }
single<AppAttestVerifier> { get<AppAttestService>() }
single { IntegrityService(config.integrity, get(), get()) }
single<DeviceCheckTrialClaimRepository> { ExposedDeviceCheckTrialClaimRepository(get()) }
single { MysqlDeviceCheckTrialMutex(get()) }
single<AuthRepository> { ExposedAuthRepository(get()) }
single { SessionAccessAuthenticator(get(), get()) }
single<AccountRepository> { ExposedAccountRepository(get(), get()) }
single<AppleEventRepository> { ExposedAppleEventRepository(get(), get(), config.antiAbuse) }
single { AppleEventVerifier(config.apple, get()) }
single { AppleEventService(get(), get()) }
single<BillingTransactionRunner> {
ExposedBillingTransactionRunner(get())
}
single {
CreditService(
transactions = get(),
referralRewards = ReferralRewardConfig(
inviterCredits = config.credits.referralInviter,
inviteeCredits = config.credits.referralInvitee,
),
)
}
single<TrialCreditGranter> {
TrialCreditGranter { accountId ->
get<CreditService>().grantSignupTrial(
userId = accountId,
credits = config.credits.signupTrial,
idempotencyKey = "internal:signup-trial:$accountId",
)
}
}
single {
DeviceCheckTrialService(
repository = get(),
client = get(),
creditGranter = get(),
policy = config.integrity.deviceCheckPolicy,
mutex = get<MysqlDeviceCheckTrialMutex>(),
)
}
single { ExposedGatewayRepository(get()) }
single<GatewayGrantRepository> { get<ExposedGatewayRepository>() }
single<GatewayGrantPort> { get<ExposedGatewayRepository>() }
single<GatewayUsagePort> { get<ExposedGatewayRepository>() }
single<AccountProvisioner> {
AccountProvisioner { accountId, deviceCheckToken ->
val granted = get<DeviceCheckTrialService>().claimAndGrant(accountId, deviceCheckToken)
if (deviceCheckToken != null && !granted) {
get<AuthRepository>().restrictAccountForAntiAbuse(
accountId,
java.time.Instant.now(),
)
}
}
}
single {
SessionService(
repository = get(),
appleIdentityVerifier = get(),
appleTokenClient = get(),
integrityService = get(),
sessionJwt = get(),
fieldEncryptor = get(),
identityFingerprint = get(),
sessionConfig = config.session,
accountProvisioner = get(),
)
}
single { AppleRevocationOutboxProcessor(get(), get(), get()) }
single<AccountReauthenticator> {
AppleAccountReauthenticator(
identityVerifier = get(),
appleTokenClient = get(),
identityFingerprint = get(),
)
}
single {
AccountService(
repository = get(),
fieldEncryptor = get(),
antiAbuseConfig = config.antiAbuse,
revocationProcessor = get(),
reauthenticator = get(),
)
}
single<UserRegistrationTimeProvider> {
UserRegistrationTimeProvider { accountId ->
get<AccountRepository>().findById(accountId)?.createdAt
}
}
single<ReferralRiskProvider> {
ReferralRiskProvider { accountId ->
get<AccountRepository>().findById(accountId)?.let {
ReferralRiskIdentity(it.identityFingerprint, it.antiAbuseRestricted)
}
}
}
single {
ReferralService(
transactions = get(),
registrationTimeProvider = get(),
riskProvider = get(),
bindingWindow = Duration.ofDays(config.credits.referralBindingDays),
)
}
single<ReferralLookupPort> {
val transactions = get<BillingTransactionRunner>()
ReferralLookupPort { code ->
transactions.inTransaction { unit ->
val referralCode = unit.referrals.findCode(code)
referralCode?.campaignId
?.let(unit.referrals::findCampaign)
?.isActive(Instant.now()) == true
}
}
}
single { SessionIdentityAdapter(get()) }
single<AuthenticatedUserExtractor> { get<SessionIdentityAdapter>() }
single<GatewayIdentityPort> { get<SessionIdentityAdapter>() }
single {
GatewaySettings(
issuer = config.session.issuer,
audience = "${config.session.audience}-gateway",
accessTokenHmacSecret = deriveGatewaySecret(config.session.hmacSecret, "gateway-access"),
refreshTokenHmacSecret = deriveGatewaySecret(config.session.hmacSecret, "gateway-refresh"),
accessTokenLifetime = Duration.ofMinutes(5),
refreshTokenLifetime = Duration.ofDays(config.session.refreshDays),
maximumGrantLifetime = Duration.ofDays(config.session.gatewayGrantDays),
)
}
single { GatewayGrantService(get(), get()) }
single<GatewayAccessTokenPort> { GatewayBearerIdentity(get()) }
single<CreditReservationPort> {
CreditReservationAdapter(
creditService = get(),
llmModel = config.providers.deepSeek.model,
asrModel = config.providers.volcengine.resourceId,
)
}
single {
ProviderCatalog(configuredProviders(config, get()))
}
single { GatewayService(get(), get(), get(), get()) }
single { GatewayReconciliationService(get(), get()) }
single {
InviteWebConfig(
appStoreUrl = config.appStoreUrl,
appleAppId = "${config.integrity.appAttestTeamId}.${config.integrity.appAttestBundleId}",
universalLinkBaseUrl = config.inviteBaseUrl,
)
}
}
private fun configuredProviders(config: AppConfig, client: HttpClient): List<GatewayProvider> =
buildList {
config.providers.deepSeek.apiKey?.let { apiKey ->
add(
DeepSeekProvider(
client,
DeepSeekConfig(
endpoint = config.providers.deepSeek.endpoint,
apiKey = apiKey,
model = config.providers.deepSeek.model,
),
),
)
}
if (config.providers.volcengine.credentialsAvailable) {
val providerConfig = config.providers.volcengine.toProviderConfig()
add(VolcengineAsrProvider(KtorVolcengineAsrTransport(client, providerConfig)))
}
}
private fun com.osglab.account.config.VolcengineConfig.toProviderConfig() =
VolcengineAsrConfig(
endpoint = endpoint,
resourceId = resourceId,
appId = appId,
accessToken = accessToken,
apiKey = apiKey,
)
private fun deriveGatewaySecret(master: ByteArray, context: String): ByteArray =
Mac.getInstance("HmacSHA256").run {
init(SecretKeySpec(master, "HmacSHA256"))
doFinal("osg-account-server:$context".toByteArray(Charsets.UTF_8))
}
private val REQUEST_ID = Regex("[A-Za-z0-9_-]{8,64}")
private val AUTH_RATE_LIMIT = RateLimitName("auth")
private val ACCOUNT_RATE_LIMIT = RateLimitName("account")
private val GATEWAY_RATE_LIMIT = RateLimitName("gateway")
private val PUBLIC_RATE_LIMIT = RateLimitName("public")