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.admin.grants.services.AdminGrantService import com.osglab.account.features.admin.repositories.AdminRepository import com.osglab.account.features.admin.repositories.ExposedAdminRepository import com.osglab.account.features.admin.routes.adminApiRoutes import com.osglab.account.features.admin.routes.adminWebRoutes import com.osglab.account.features.admin.security.AdminPasswordHasher import com.osglab.account.features.admin.security.AdminTotpVerifier import com.osglab.account.features.admin.security.BouncyCastleArgon2idPasswordHasher import com.osglab.account.features.admin.security.HmacTotpVerifier import com.osglab.account.features.admin.services.AdminAuthService import com.osglab.account.features.admin.services.AdminAuditService import com.osglab.account.features.admin.services.AdminBootstrapConfig import com.osglab.account.features.admin.services.AdminBootstrapService import com.osglab.account.features.admin.services.AdminOperatorService import com.osglab.account.features.admin.services.AdminSessionService import com.osglab.account.features.admin.stats.repositories.AdminStatsRepository import com.osglab.account.features.admin.stats.repositories.AdminProductAnalyticsRepository import com.osglab.account.features.admin.stats.repositories.ExposedAdminStatsRepository import com.osglab.account.features.admin.stats.repositories.ExposedAdminProductAnalyticsRepository import com.osglab.account.features.admin.stats.services.AdminProductAnalyticsService import com.osglab.account.features.admin.stats.services.AdminStatsService import com.osglab.account.features.admin.users.repositories.AdminUsersRepository import com.osglab.account.features.admin.users.repositories.ExposedAdminUsersRepository import com.osglab.account.features.admin.users.services.AdminUsersService import com.osglab.account.features.account.AccountOperations import com.osglab.account.features.account.AccountReauthenticator import com.osglab.account.features.account.AccountRepository 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.analytics.repositories.AnalyticsRepository import com.osglab.account.features.analytics.repositories.ExposedAnalyticsRepository import com.osglab.account.features.analytics.routes.analyticsRoutes import com.osglab.account.features.analytics.services.AnalyticsMaintenanceService import com.osglab.account.features.analytics.services.AnalyticsService import com.osglab.account.features.analytics.services.DefaultAnalyticsService 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.CreditOperations import com.osglab.account.features.credits.services.CreditService import com.osglab.account.features.credits.services.ReferralRewardConfig import com.osglab.account.features.credits.services.signupTrialIdempotencyKey import com.osglab.account.features.content.repositories.ContentRepository import com.osglab.account.features.content.repositories.ExposedContentRepository import com.osglab.account.features.content.routes.contentRoutes import com.osglab.account.features.content.services.ContentService import com.osglab.account.features.content.feed.ExposedHintFeedRepository import com.osglab.account.features.content.feed.HintFeedRepository import com.osglab.account.features.content.feed.HintFeedGenerationLock import com.osglab.account.features.content.feed.HintFeedScheduler import com.osglab.account.features.content.feed.HintFeedService import com.osglab.account.features.content.feed.MysqlHintFeedGenerationLock import com.osglab.account.features.content.feed.sources.BaselineHintSource import com.osglab.account.features.content.feed.sources.GoogleFeedHintSource import com.osglab.account.features.content.feed.sources.HolidayHintSource import com.osglab.account.features.content.feed.sources.TopHubHintSource import com.osglab.account.features.content.feed.sources.WeatherHintSource 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.credentials.DatabaseProviderApiKeyResolver import com.osglab.account.features.gateway.credentials.EnvironmentProviderCredentials import com.osglab.account.features.gateway.credentials.ExposedGatewayCredentialRepository import com.osglab.account.features.gateway.credentials.GatewayCredentialRepository import com.osglab.account.features.gateway.credentials.GatewayCredentialService import com.osglab.account.features.gateway.credentials.ProviderApiKeyResolver import com.osglab.account.features.gateway.ports.CreditReservationPort import com.osglab.account.features.gateway.ports.ComplimentaryRequestPort 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.health.healthRoutes 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.InviteOpenRecorder import com.osglab.account.features.inviteweb.ReferralLookupPort import com.osglab.account.features.inviteweb.configureInviteWebRoutes import com.osglab.account.features.oobe.ExposedOobeRepository import com.osglab.account.features.oobe.OobeGrantService import com.osglab.account.features.oobe.OobeRepository import com.osglab.account.features.oobe.OobeTokenSettings import com.osglab.account.features.oobe.oobeRoutes import com.osglab.account.features.referrals.routes.referralRoutes import com.osglab.account.features.referrals.services.ReferralOperations 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 com.osglab.account.features.storekit.domain.StoreKitUnavailable import com.osglab.account.features.storekit.routes.storeKitRoutes import com.osglab.account.features.storekit.services.StoreKitService import com.osglab.account.features.storekit.verification.AppleStoreKitTransactionVerifier import com.osglab.account.features.storekit.verification.StoreKitTransactionVerifier 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.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.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 kotlinx.coroutines.runBlocking 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) } register(ADMIN_AUTH_RATE_LIMIT) { rateLimiter(limit = 5, refillPeriod = 1.minutes) } register(ADMIN_API_RATE_LIMIT) { rateLimiter(limit = 60, 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().database if (appConfig.admin.bootstrapEnabled) { runBlocking { koin.get().initialize( AdminBootstrapConfig( enabled = true, operatorId = appConfig.admin.bootstrapOperatorId, username = appConfig.admin.bootstrapUsername, passwordHash = appConfig.admin.bootstrapPasswordHash, totpSecretBase32 = appConfig.admin.bootstrapTotpSecretBase32, ), ) } } val sessionAuthenticator = koin.get() installSessionAuthentication(sessionAuthenticator::authenticate) val asrStreaming = if (appConfig.providers.volcengine.credentialsAvailable) { val providerConfig = appConfig.providers.volcengine.toProviderConfig() AsrStreamingService( gateway = koin.get(), upstream = KtorVolcengineAsrTransport( client = koin.get(), config = providerConfig, credentialResolver = koin.get(), ), scope = this, ) } else { null } if (appConfig.hintFeed.enabled) { launch { koin.get().run() } } launch { while (isActive) { try { koin.get().processPending() } catch (exception: CancellationException) { throw exception } catch (_: Exception) { // Durable outbox state is retried; never log sensitive token material. } try { koin.get().purgeStaleAnonymousInstallations() } catch (exception: CancellationException) { throw exception } catch (_: Exception) { // Anonymous analytics cleanup is bounded and retried on the next cycle. } try { koin.get().reconcile() } catch (exception: CancellationException) { throw exception } catch (_: Exception) { // Durable settlement state is retried without logging provider data. } if (appConfig.admin.enabled) { try { koin.get().cleanupInactive() } catch (exception: CancellationException) { throw exception } catch (_: Exception) { // Expired sessions are retried in bounded batches on the next cycle. } } delay(60_000) } } monitor.subscribe(ApplicationStopped) { koin.get().close() koin.get().close() } routing { healthRoutes(koin.get()) rateLimit(AUTH_RATE_LIMIT) { authRoutes(koin.get()) oobeRoutes(koin.get()) } rateLimit(ACCOUNT_RATE_LIMIT) { accountRoutes(koin.get()) creditRoutes(koin.get(), koin.get()) referralRoutes(koin.get(), appConfig.inviteBaseUrl, koin.get()) storeKitRoutes(koin.get()) } rateLimit(GATEWAY_RATE_LIMIT) { configureGatewayRoutes( service = koin.get(), appIdentity = koin.get(), gatewayIdentity = koin.get(), grantService = koin.get(), asrStreaming = asrStreaming, ) } rateLimit(PUBLIC_RATE_LIMIT) { appleEventRoutes(koin.get()) analyticsRoutes(koin.get()) configureInviteWebRoutes(koin.get(), koin.get(), koin.get()) integrityRoutes(koin.get()) contentRoutes(koin.get()) } if (appConfig.admin.enabled) { adminWebRoutes(appConfig) rateLimit(ADMIN_API_RATE_LIMIT) { adminApiRoutes( config = appConfig, authService = koin.get(), sessionService = koin.get(), statsService = koin.get(), productAnalyticsService = koin.get(), usersService = koin.get(), grantService = koin.get(), operatorService = koin.get(), auditService = koin.get(), credentialService = koin.get(), contentService = koin.get(), hintFeedService = koin.get(), ) } } } } fun accountServerModule(config: AppConfig): Module = module { single { config } single { DatabaseFactory(config.database) } single { get().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 { ExposedGatewayCredentialRepository(get()) } single { EnvironmentProviderCredentials( deepSeekApiKey = config.providers.deepSeek.apiKey, volcengineApiKey = config.providers.volcengine.apiKey, volcengineLegacyConfigured = !config.providers.volcengine.appId.isNullOrBlank() && !config.providers.volcengine.accessToken.isNullOrBlank(), ) } single { DatabaseProviderApiKeyResolver(get(), get(), get()) } single { get() } single { GatewayCredentialService(get(), get(), get()) } single { ExposedAdminRepository(get()) } single { BouncyCastleArgon2idPasswordHasher() } single { HmacTotpVerifier() } single { val dummyPassword = "invalid-admin-password-constant-work".toCharArray() try { AdminAuthService( repository = get(), passwordHasher = get(), dummyPasswordHash = get().hash(dummyPassword), totpVerifier = get(), fieldEncryptor = get(), sessionTtl = Duration.ofHours(config.admin.sessionHours), ) } finally { dummyPassword.fill('\u0000') } } single { AdminSessionService(get()) } single { AdminBootstrapService(get(), get()) } single { AdminOperatorService(get(), get(), get()) } single { AdminAuditService(get()) } single { ExposedAdminStatsRepository(get()) } single { AdminStatsService(get()) } single { ExposedAdminProductAnalyticsRepository(get()) } single { AdminProductAnalyticsService(get()) } single { ExposedAdminUsersRepository(get()) } single { AdminUsersService(get()) } single { AdminGrantService(get()) } single { ExposedContentRepository(get()) } single { ContentService(get()) } single { ExposedHintFeedRepository(get()) } single { MysqlHintFeedGenerationLock(get()) } single { val client = get() HintFeedService( repository = get(), contentService = get(), generationLock = get(), sources = listOf( BaselineHintSource(), HolidayHintSource(client), WeatherHintSource(client), TopHubHintSource(client, config.hintFeed.topHubApiKey), GoogleFeedHintSource(client), ), config = config.hintFeed, ) } single { HintFeedScheduler(get()) } single { RemoteAppleJwksProvider(get(), config.apple.jwksUrl) } single { AppleIdentityTokenVerifier(config.apple, get()) } single { createAppleTokenClient(get(), config.apple) } single { createDeviceCheckClient(get(), config.apple, config.integrity.appleEnvironment) ?: UnavailableAppleDeviceCheckClient() } single { RemoteDeviceCheckVerifier(get()) } single { ExposedAppAttestRepository(get()) } single { LibraryAppAttestCrypto( config = config.integrity, certificateValidator = BundledAppleAppAttestTrust.validator(), ) } single { AppAttestService(get(), get(), config.integrity) } single { get() } single { IntegrityService(config.integrity, get(), get()) } single { ExposedDeviceCheckTrialClaimRepository(get()) } single { MysqlDeviceCheckTrialMutex(get()) } single { ExposedAuthRepository(get()) } single { SessionAccessAuthenticator(get(), get()) } single { ExposedAccountRepository(get(), get()) } single { ExposedAnalyticsRepository(get()) } single { DefaultAnalyticsService(get()) } single { AnalyticsMaintenanceService(get()) } single { InviteOpenRecorder { get().recordInvitePageOpen(Instant.now()) } } single { ExposedAppleEventRepository(get(), get(), config.antiAbuse) } single { AppleEventVerifier(config.apple, get()) } single { AppleEventService(get(), get()) } single { ExposedBillingTransactionRunner(get()) } single { CreditService( transactions = get(), referralRewards = ReferralRewardConfig( inviterCredits = config.credits.referralInviter, inviteeCredits = config.credits.referralInvitee, ), ) } single { get() } single { if (config.storeKit.enabled) { AppleStoreKitTransactionVerifier( bundleId = config.storeKit.bundleId, appAppleId = requireNotNull(config.storeKit.appAppleId), ) } else { StoreKitTransactionVerifier { throw StoreKitUnavailable() } } } single { StoreKitService( products = if (config.storeKit.enabled) config.storeKit.products else emptyList(), verifier = get(), transactions = get(), ) } single { val creditService = get() object : TrialCreditGranter { override suspend fun grant(accountId: UUID) { creditService.grantSignupTrial( userId = accountId, credits = config.credits.signupTrial, idempotencyKey = signupTrialIdempotencyKey(accountId), ) } override suspend fun wasGranted(accountId: UUID): Boolean = creditService.hasSignupTrial(accountId) } } single { DeviceCheckTrialService( repository = get(), client = get(), creditGranter = get(), policy = config.integrity.deviceCheckPolicy, mutex = get(), ) } single { ExposedGatewayRepository(get()) } single { get() } single { get() } single { get() } single { get() } single { AccountProvisioner { accountId, deviceCheckToken, displayName -> val trial = get().claimAndGrant(accountId, deviceCheckToken) if (trial.shouldRestrictAccount) { get().restrictAccountForAntiAbuse( accountId, java.time.Instant.now(), ) } get().seedDisplayName(accountId, displayName) // Referral provisioning is intentionally handled by /v1/referrals/me // after authentication so referral storage can never block sign-in. } } 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 { AppleAccountReauthenticator( identityVerifier = get(), appleTokenClient = get(), identityFingerprint = get(), ) } single { AccountService( repository = get(), fieldEncryptor = get(), antiAbuseConfig = config.antiAbuse, revocationProcessor = get(), reauthenticator = get(), ) } single { get() } single { UserRegistrationTimeProvider { accountId -> get().findById(accountId)?.createdAt } } single { ReferralRiskProvider { accountId -> get().findById(accountId)?.let { ReferralRiskIdentity(it.identityFingerprint, it.antiAbuseRestricted) } } } single { ReferralService( transactions = get(), registrationTimeProvider = get(), riskProvider = get(), bindingWindow = Duration.ofDays(config.credits.referralBindingDays), ) } single { get() } single { val transactions = get() ReferralLookupPort { code -> transactions.inTransaction { unit -> // Invitation codes are permanent account identifiers. Campaign // availability is evaluated only when an invitee redeems one. unit.referrals.findCode(code) != null } } } single { SessionIdentityAdapter(get()) } single { get() } single { get() } 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 { ExposedOobeRepository(get()) } single { OobeTokenSettings( issuer = config.session.issuer, audience = "${config.session.audience}-gateway", accessTokenHmacSecret = deriveGatewaySecret( config.session.hmacSecret, "oobe-gateway-access", ), refreshTokenHmacSecret = deriveGatewaySecret( config.session.hmacSecret, "oobe-gateway-refresh", ), ) } single { OobeGrantService(get(), get(), get()) } single { GatewayGrantService(get(), get()) } single { val oobeGrants = get() GatewayBearerIdentity(get(), oobeGrants::authenticate) } single { CreditReservationAdapter( creditService = get(), llmModel = config.providers.deepSeek.model, asrModel = config.providers.volcengine.resourceId, ) } single { ProviderCatalog(configuredProviders(config, get(), get())) } single { GatewayService(get(), get(), 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, credentialResolver: ProviderApiKeyResolver, ): List = buildList { config.providers.deepSeek.apiKey?.let { apiKey -> add( DeepSeekProvider( client, DeepSeekConfig( endpoint = config.providers.deepSeek.endpoint, apiKey = apiKey, model = config.providers.deepSeek.model, reasoningModel = config.providers.deepSeek.reasoningModel, ), credentialResolver = credentialResolver, ), ) } if (config.providers.volcengine.credentialsAvailable) { val providerConfig = config.providers.volcengine.toProviderConfig() add( VolcengineAsrProvider( KtorVolcengineAsrTransport( client = client, config = providerConfig, credentialResolver = credentialResolver, ), ), ) } } 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") private val ADMIN_AUTH_RATE_LIMIT = RateLimitName("admin-auth") private val ADMIN_API_RATE_LIMIT = RateLimitName("admin-api")