Files
OSGAccountServer/src/main/kotlin/com/osglab/account/Application.kt
T
Rocky 231c5040a5
CI / verify (push) Has been cancelled
CI / publish (push) Has been cancelled
Add StoreKit history and modernize admin console
Expose ledger-backed cross-device purchase history while shipping the tested React admin redesign in the same reproducible deployment revision.
2026-08-19 22:13:13 +08:00

645 lines
28 KiB
Kotlin

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.ExposedAdminStatsRepository
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.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.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.domain.ReferralException
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.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 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<DatabaseFactory>().database
if (appConfig.admin.bootstrapEnabled) {
runBlocking {
koin.get<AdminBootstrapService>().initialize(
AdminBootstrapConfig(
enabled = true,
operatorId = appConfig.admin.bootstrapOperatorId,
username = appConfig.admin.bootstrapUsername,
passwordHash = appConfig.admin.bootstrapPasswordHash,
totpSecretBase32 = appConfig.admin.bootstrapTotpSecretBase32,
),
)
}
}
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.
}
if (appConfig.admin.enabled) {
try {
koin.get<AdminSessionService>().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<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())
storeKitRoutes(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())
}
if (appConfig.admin.enabled) {
adminWebRoutes()
rateLimit(ADMIN_API_RATE_LIMIT) {
adminApiRoutes(
config = appConfig,
authService = koin.get(),
sessionService = koin.get(),
statsService = koin.get(),
usersService = koin.get(),
grantService = koin.get(),
operatorService = koin.get(),
auditService = 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<AdminRepository> { ExposedAdminRepository(get()) }
single<AdminPasswordHasher> { BouncyCastleArgon2idPasswordHasher() }
single<AdminTotpVerifier> { HmacTotpVerifier() }
single {
val dummyPassword = "invalid-admin-password-constant-work".toCharArray()
try {
AdminAuthService(
repository = get(),
passwordHasher = get(),
dummyPasswordHash = get<AdminPasswordHasher>().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<AdminStatsRepository> { ExposedAdminStatsRepository(get()) }
single { AdminStatsService(get()) }
single<AdminUsersRepository> { ExposedAdminUsersRepository(get()) }
single { AdminUsersService(get()) }
single { AdminGrantService(get()) }
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<CreditOperations> { get<CreditService>() }
single<StoreKitTransactionVerifier> {
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<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, displayName ->
val granted = get<DeviceCheckTrialService>().claimAndGrant(accountId, deviceCheckToken)
if (deviceCheckToken != null && !granted) {
get<AuthRepository>().restrictAccountForAntiAbuse(
accountId,
java.time.Instant.now(),
)
}
get<AccountService>().seedDisplayName(accountId, displayName)
try {
get<ReferralOperations>().getOrCreateCode(accountId)
} catch (exception: CancellationException) {
throw exception
} catch (_: ReferralException) {
// Referral eligibility must not make account sign-in unavailable.
}
}
}
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<AccountOperations> { get<AccountService>() }
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<ReferralOperations> { get<ReferralService>() }
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,
reasoningModel = config.providers.deepSeek.reasoningModel,
),
),
)
}
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")
private val ADMIN_AUTH_RATE_LIMIT = RateLimitName("admin-auth")
private val ADMIN_API_RATE_LIMIT = RateLimitName("admin-api")