4c9e5feec0
Preserve the successor session for legitimate refresh retries so transient failures no longer revoke the user's session family.
462 lines
22 KiB
Kotlin
462 lines
22 KiB
Kotlin
package com.osglab.account.config
|
|
|
|
import io.kotest.core.spec.style.FunSpec
|
|
import io.kotest.matchers.shouldBe
|
|
import io.kotest.matchers.string.shouldContain
|
|
import io.kotest.matchers.string.shouldNotContain
|
|
import java.nio.file.Files
|
|
import java.nio.file.Path
|
|
|
|
class DeploymentConsistencyTest : FunSpec({
|
|
val root = Path.of(System.getProperty("user.dir"))
|
|
|
|
test("OpenAPI documents every mounted API route") {
|
|
val openApi = root.read("docs/openapi.yaml")
|
|
val documentedPaths = Regex("""(?m)^ (/[^:]+):\s*$""")
|
|
.findAll(openApi)
|
|
.map { it.groupValues[1] }
|
|
.toSet()
|
|
|
|
documentedPaths shouldBe EXPECTED_PUBLIC_PATHS
|
|
}
|
|
|
|
test("session refresh idempotency stays aligned across API, schema, and deployment") {
|
|
val openApi = root.read("docs/openapi.yaml")
|
|
val migration = root.read(
|
|
"src/main/resources/db/migration/V29__idempotent_session_refresh.sql",
|
|
)
|
|
|
|
openApi shouldContain "refreshOperationId"
|
|
migration shouldContain "encrypted_replacement_refresh_token"
|
|
migration shouldContain "refresh_replay_until"
|
|
listOf(
|
|
root.read("src/main/resources/application.yaml"),
|
|
root.read(".env.example"),
|
|
root.read("compose.yaml"),
|
|
).forEach { configuration ->
|
|
configuration shouldContain "LEGACY_REFRESH_REPLAY_SECONDS"
|
|
}
|
|
}
|
|
|
|
test("OpenAPI defines admin pagination and response contracts") {
|
|
val openApi = root.read("docs/openapi.yaml")
|
|
val sessionSchema = openApi
|
|
.substringAfter(" AdminSessionState:")
|
|
.substringBefore(" AdminLoginResponse:")
|
|
|
|
sessionSchema shouldNotContain "csrfToken"
|
|
openApi shouldContain "schema: { \$ref: \"#/components/schemas/AdminOverview\" }"
|
|
openApi shouldContain "schema: { \$ref: \"#/components/schemas/AdminReferralOverview\" }"
|
|
openApi shouldContain "schema: { \$ref: \"#/components/schemas/AdminUserPage\" }"
|
|
openApi shouldContain "schema: { \$ref: \"#/components/schemas/AdminUserDetail\" }"
|
|
openApi shouldContain "schema: { \$ref: \"#/components/schemas/AdminLedgerPage\" }"
|
|
openApi shouldContain "schema: { \$ref: \"#/components/schemas/AdminAuditPage\" }"
|
|
openApi shouldContain "pendingBindings"
|
|
openApi shouldContain "ineligibleBindings"
|
|
openApi shouldContain "chargedCredits"
|
|
openApi shouldContain "referralCode"
|
|
}
|
|
|
|
test("official content contract migration and runtime grants stay aligned") {
|
|
val openApi = root.read("docs/openapi.yaml")
|
|
val migration = root.read(
|
|
"src/main/resources/db/migration/V22__official_content_management.sql",
|
|
)
|
|
val hintFeedMigration = root.read(
|
|
"src/main/resources/db/migration/V24__hint_feed_generation.sql",
|
|
)
|
|
val privileges = root.read("docs/mysql-minimum-privileges.sql")
|
|
val smokePrivileges = root.read("deploy/smoke/runtime-grants.sql")
|
|
|
|
migration shouldContain "CREATE TABLE official_content_catalog"
|
|
migration shouldContain "CREATE TABLE official_skills"
|
|
migration shouldContain "CREATE TABLE official_skill_localizations"
|
|
migration shouldContain "CREATE TABLE official_hint_packs"
|
|
migration shouldContain "locale IN ('zh', 'en')"
|
|
hintFeedMigration shouldContain "CREATE TABLE hint_feed_settings"
|
|
hintFeedMigration shouldContain "CREATE TABLE hint_feed_generation_state"
|
|
hintFeedMigration shouldContain "generation_interval_hours BETWEEN 1 AND 168"
|
|
openApi shouldContain "schemaVersion: { type: integer, const: 1 }"
|
|
openApi shouldContain "pattern: \"^official\\\\."
|
|
openApi shouldContain "Cache-Control: { schema: { type: string, const: \"public,max-age=300\" } }"
|
|
val skillSchemas = openApi
|
|
.substringAfter(" SkillLocalization:")
|
|
.substringBefore(" AIHintCard:")
|
|
skillSchemas shouldContain "name: { type: string, minLength: 1, maxLength: 40 }"
|
|
skillSchemas shouldContain "summary: { type: string, minLength: 1, maxLength: 200 }"
|
|
skillSchemas shouldContain "prompt: { type: string, minLength: 1, maxLength: 6000 }"
|
|
skillSchemas shouldContain "systemImage: { type: string, minLength: 1, maxLength: 100 }"
|
|
skillSchemas shouldContain "sortOrder: { type: integer, minimum: 0, maximum: 100000 }"
|
|
skillSchemas shouldContain "maxItems: 100"
|
|
listOf(privileges, smokePrivileges).forEach { grants ->
|
|
grants shouldContain "SELECT ON osg_account"
|
|
grants shouldContain "official_content_catalog"
|
|
grants shouldContain "official_skills"
|
|
grants shouldContain "official_skill_localizations"
|
|
grants shouldContain "official_hint_packs"
|
|
grants shouldContain "hint_feed_settings"
|
|
grants shouldContain "hint_feed_generation_state"
|
|
}
|
|
}
|
|
|
|
test("admin ledger operations stay indexed exact and privacy minimized") {
|
|
val migration = root.read(
|
|
"src/main/resources/db/migration/V19__admin_ledger_operations.sql",
|
|
)
|
|
migration shouldContain
|
|
"MODIFY reference_id CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NULL"
|
|
migration shouldContain
|
|
"MODIFY reservation_id CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NULL"
|
|
migration shouldContain "ON credit_ledger (amount_delta, id)"
|
|
migration shouldContain "ON credit_ledger (user_id, amount_delta, id)"
|
|
migration shouldContain "ON provider_requests (capability, request_source, reservation_id)"
|
|
migration shouldContain "ON provider_requests (request_source, capability, reservation_id)"
|
|
|
|
val openApi = root.read("docs/openapi.yaml")
|
|
val ledgerSchema = openApi
|
|
.substringAfter(" LedgerEntryType:")
|
|
.substringBefore(" AdminLedgerPage:")
|
|
LEDGER_ENTRY_TYPE_NAMES.forEach(ledgerSchema::shouldContain)
|
|
ledgerSchema shouldContain "deprecated: true"
|
|
ledgerSchema shouldContain "referenceId"
|
|
ledgerSchema shouldContain "reservationId"
|
|
ledgerSchema shouldContain
|
|
"required: [entryId, userId, type, entryType, amount, balanceAfter, reasonCode, createdAt]"
|
|
ledgerSchema shouldNotContain "enum: [grant, reserve, settle, refund, adjustment]"
|
|
ledgerSchema shouldNotContain "idempotencyKey"
|
|
ledgerSchema shouldNotContain "appAccountToken"
|
|
ledgerSchema shouldNotContain "signedTransaction"
|
|
ledgerSchema shouldNotContain "appleSubject"
|
|
}
|
|
|
|
test("provider defaults and Apple integrity contract stay production compatible") {
|
|
val providerConfigurations = listOf(
|
|
root.read("src/main/kotlin/com/osglab/account/config/AppConfig.kt"),
|
|
root.read("src/main/resources/application.yaml"),
|
|
root.read(".env.example"),
|
|
root.read("compose.yaml"),
|
|
)
|
|
providerConfigurations.forEach { configuration ->
|
|
configuration shouldContain "wss://openspeech.bytedance.com/api/v3/sauc/bigmodel"
|
|
configuration shouldNotContain "bigmodel_async"
|
|
}
|
|
|
|
val openApi = root.read("docs/openapi.yaml")
|
|
openApi shouldContain "osg-app-attest-v1"
|
|
openApi shouldContain "purpose=apple-sign-in"
|
|
openApi shouldContain "identity_token_sha256=<identity-token-digest>"
|
|
openApi shouldContain "authorization_code_sha256=<authorization-code-digest>"
|
|
openApi shouldContain "nonce_sha256=<raw-nonce-digest>"
|
|
openApi shouldContain "including the final line feed"
|
|
openApi shouldContain "unpadded Base64URL"
|
|
}
|
|
|
|
test("StoreKit catalog and smaller immutable rates stay aligned") {
|
|
listOf(root.read(".env.example"), root.read("compose.yaml")).forEach { configuration ->
|
|
configuration shouldContain "STOREKIT_PRODUCTS"
|
|
configuration shouldContain "500tks:500,1500tks:1500,3000tks:3000"
|
|
configuration shouldNotContain "STOREKIT_PRODUCT_CREDITS"
|
|
configuration shouldContain "SIGNUP_TRIAL_CREDITS"
|
|
configuration shouldContain "REFERRAL_INVITER_CREDITS"
|
|
}
|
|
|
|
val rates = root.read("src/main/resources/db/migration/V10__smaller_credit_units.sql")
|
|
rates shouldContain "'10000000-0000-0000-0000-000000000003'"
|
|
rates shouldContain "'10000000-0000-0000-0000-000000000004'"
|
|
rates shouldContain "1,\n 3000,"
|
|
rates shouldContain "1,\n 1000,\n 1,\n 400,"
|
|
}
|
|
|
|
test("StoreKit history remains ledger backed, indexed, and privacy minimized") {
|
|
val historyIndex = root.read(
|
|
"src/main/resources/db/migration/V15__storekit_purchase_history_index.sql",
|
|
)
|
|
historyIndex shouldContain
|
|
"ON storekit_credit_purchases (user_id, purchased_at, transaction_id)"
|
|
|
|
val openApi = root.read("docs/openapi.yaml")
|
|
val historySchema = openApi
|
|
.substringAfter(" StoreKitTransactionHistoryItem:")
|
|
.substringBefore(" AppleAppSiteAssociation:")
|
|
historySchema shouldContain "purchasedAt"
|
|
historySchema shouldContain "status"
|
|
historySchema shouldContain "nextCursor"
|
|
historySchema shouldNotContain "signedTransaction"
|
|
historySchema shouldNotContain "appAccountToken"
|
|
historySchema shouldNotContain "userId"
|
|
historySchema shouldNotContain "accountId"
|
|
}
|
|
|
|
test("account profiles cascade on deletion and grants stay aligned") {
|
|
val profileMigration = root.read(
|
|
"src/main/resources/db/migration/V11__account_profiles.sql",
|
|
)
|
|
val referralMigration = root.read(
|
|
"src/main/resources/db/migration/V12__align_referral_rewards.sql",
|
|
)
|
|
val ledgerTimelineMigration = root.read(
|
|
"src/main/resources/db/migration/V13__credit_ledger_global_timeline.sql",
|
|
)
|
|
profileMigration shouldContain "encrypted_display_name MEDIUMTEXT NOT NULL"
|
|
profileMigration shouldContain "REFERENCES accounts (id) ON DELETE CASCADE"
|
|
referralMigration shouldContain "inviter_reward_credits = 1000"
|
|
referralMigration shouldContain "invitee_reward_credits = 1000"
|
|
ledgerTimelineMigration shouldContain "ON credit_ledger (created_at, id)"
|
|
|
|
listOf(
|
|
root.read("src/main/resources/application.yaml"),
|
|
root.read(".env.example"),
|
|
root.read("compose.yaml"),
|
|
).forEach { configuration ->
|
|
configuration shouldContain "1000"
|
|
configuration shouldNotContain "SIGNUP_TRIAL_CREDITS=334"
|
|
}
|
|
}
|
|
|
|
test("product analytics contract stays allowlisted and account deletions cascade") {
|
|
val migration = root.read(
|
|
"src/main/resources/db/migration/V16__product_analytics_events.sql",
|
|
)
|
|
migration shouldContain "product_analytics_installations"
|
|
migration shouldContain "product_analytics_events"
|
|
migration shouldContain "REFERENCES accounts (id) ON DELETE CASCADE"
|
|
migration shouldContain "PRIMARY KEY (installation_hash, client_event_id)"
|
|
|
|
val openApi = root.read("docs/openapi.yaml")
|
|
val eventSchema = openApi
|
|
.substringAfter(" ProductAnalyticsEvent:")
|
|
.substringBefore(" SkillLocalization:")
|
|
eventSchema shouldContain "additionalProperties: false"
|
|
eventSchema shouldContain "AI_FEATURE_SUCCEEDED"
|
|
eventSchema shouldContain "INSUFFICIENT_CREDITS"
|
|
eventSchema shouldNotContain "prompt"
|
|
eventSchema shouldNotContain "transcript"
|
|
eventSchema shouldNotContain "audio"
|
|
eventSchema shouldNotContain "modelOutput"
|
|
openApi shouldContain "schema: { \$ref: \"#/components/schemas/AdminProductAnalytics\" }"
|
|
|
|
val keyboardMigration = root.read(
|
|
"src/main/resources/db/migration/V23__keyboard_usage_daily_summaries.sql",
|
|
)
|
|
keyboardMigration shouldContain "keyboard_usage_daily_summaries"
|
|
keyboardMigration shouldContain
|
|
"UNIQUE KEY uq_keyboard_usage_installation_date (installation_hash, summary_date)"
|
|
keyboardMigration shouldContain "ON DELETE CASCADE"
|
|
keyboardMigration shouldNotContain "input_text"
|
|
keyboardMigration shouldNotContain "host_application"
|
|
|
|
val keyboardSchema = openApi
|
|
.substringAfter(" KeyboardUsageSummary:")
|
|
.substringBefore(" ProductAnalyticsBatchResponse:")
|
|
keyboardSchema shouldContain "additionalProperties: false"
|
|
keyboardSchema shouldContain "chineseCharacterCount"
|
|
keyboardSchema shouldContain "mixedLanguageSessionCount"
|
|
keyboardSchema shouldNotContain "userText"
|
|
keyboardSchema shouldNotContain "keystrokes"
|
|
keyboardSchema shouldNotContain "hostApplication"
|
|
}
|
|
|
|
test("anonymous OOBE feature claims are scoped to each guided session") {
|
|
val migration = root.read(
|
|
"src/main/resources/db/migration/V27__scope_oobe_claims_to_grant.sql",
|
|
)
|
|
val openApi = root.read("docs/openapi.yaml")
|
|
|
|
migration shouldContain "PRIMARY KEY (grant_id, feature)"
|
|
migration shouldContain
|
|
"FOREIGN KEY (grant_id) REFERENCES oobe_gateway_grants (id) ON DELETE CASCADE"
|
|
migration shouldNotContain "PRIMARY KEY (subject_id, feature)"
|
|
openApi shouldContain "Each feature can"
|
|
openApi shouldContain "succeed once within this short-lived grant"
|
|
}
|
|
|
|
test("production Compose reuses private MySQL and hardens the application container") {
|
|
val compose = root.read("compose.yaml")
|
|
|
|
compose shouldContain "127.0.0.1:\${ACCOUNT_BIND_PORT:-18080}:8080"
|
|
compose shouldContain "ghcr.io/hkgood/osg-account-server"
|
|
compose shouldContain "pull_policy: always"
|
|
compose shouldContain "external: true"
|
|
compose shouldContain "account-egress:"
|
|
compose shouldContain "user: \"10001:10001\""
|
|
compose shouldContain "read_only: true"
|
|
compose shouldContain "cap_drop:"
|
|
compose shouldContain "no-new-privileges:true"
|
|
compose shouldNotContain "image: mysql"
|
|
compose shouldNotContain "3306:3306"
|
|
compose shouldNotContain "0.0.0.0:"
|
|
}
|
|
|
|
test("admin bootstrap is one-time and runtime database grants stay explicit") {
|
|
val compose = root.read("compose.yaml")
|
|
val privileges = root.read("docs/mysql-minimum-privileges.sql")
|
|
val smokePrivileges = root.read("deploy/smoke/runtime-grants.sql")
|
|
|
|
compose shouldContain "ADMIN_BOOTSTRAP_ENABLED: \${ADMIN_BOOTSTRAP_ENABLED:-false}"
|
|
compose shouldContain "ADMIN_MTLS_REQUIRED: \${ADMIN_MTLS_REQUIRED:-true}"
|
|
privileges shouldContain "GRANT SELECT ON osg_account.admin_operators"
|
|
privileges shouldContain "GRANT INSERT, UPDATE ON osg_account.admin_operators"
|
|
privileges shouldContain "GRANT SELECT ON osg_account.admin_sessions"
|
|
privileges shouldContain "GRANT INSERT, UPDATE, DELETE ON osg_account.admin_sessions"
|
|
privileges shouldContain "GRANT SELECT ON osg_account.gateway_grant_scopes"
|
|
privileges shouldContain "GRANT INSERT ON osg_account.gateway_grant_scopes"
|
|
privileges shouldContain "GRANT SELECT ON osg_account.gateway_refresh_tokens"
|
|
privileges shouldContain "GRANT INSERT, UPDATE ON osg_account.gateway_refresh_tokens"
|
|
privileges shouldContain "GRANT SELECT ON osg_account.gateway_complimentary_requests"
|
|
privileges shouldContain
|
|
"GRANT INSERT, UPDATE, DELETE ON osg_account.gateway_complimentary_requests"
|
|
privileges shouldContain "GRANT SELECT ON osg_account.account_profiles"
|
|
privileges shouldContain "GRANT INSERT, UPDATE ON osg_account.account_profiles"
|
|
privileges shouldContain "GRANT INSERT ON osg_account.admin_audit_log"
|
|
privileges shouldContain "GRANT INSERT ON osg_account.admin_credit_grants"
|
|
privileges shouldContain "GRANT SELECT ON osg_account.storekit_credit_purchases"
|
|
privileges shouldContain "GRANT INSERT ON osg_account.storekit_credit_purchases"
|
|
privileges shouldNotContain "UPDATE ON osg_account.admin_audit_log"
|
|
privileges shouldNotContain "DELETE ON osg_account.admin_credit_grants"
|
|
smokePrivileges shouldContain "GRANT SELECT ON osg_account_smoke.account_profiles"
|
|
smokePrivileges shouldContain "GRANT INSERT, UPDATE ON osg_account_smoke.account_profiles"
|
|
}
|
|
|
|
test("container image remains non-root and read-only compatible") {
|
|
val dockerfile = root.read("Dockerfile")
|
|
val build = root.read("build.gradle.kts")
|
|
|
|
dockerfile shouldContain "USER 10001:10001"
|
|
dockerfile shouldContain "ENV HOME=/tmp"
|
|
dockerfile shouldContain "http://127.0.0.1:8080/health/ready"
|
|
dockerfile shouldNotContain "ENTRYPOINT [\"sh\""
|
|
dockerfile shouldNotContain "jansi.tmpdir"
|
|
build shouldContain "configurations.configureEach"
|
|
build shouldContain "exclude(group = \"org.fusesource.jansi\", module = \"jansi\")"
|
|
build shouldContain "tasks.register(\"verifyRuntimeClasspath\")"
|
|
}
|
|
|
|
test("OpenResty proxies HTTP WebSocket invitations and both AASA paths safely") {
|
|
val openResty = root.read("deploy/openresty-account.conf")
|
|
|
|
openResty shouldContain "proxy_set_header X-OSG-mTLS-Verified \$ssl_client_verify;"
|
|
openResty shouldNotContain "proxy_set_header X-OSG-mTLS-Verified \"SUCCESS\";"
|
|
openResty shouldContain "proxy_set_header Upgrade \$http_upgrade;"
|
|
openResty shouldContain "proxy_set_header Connection \$connection_upgrade;"
|
|
openResty shouldContain "location = /.well-known/apple-app-site-association"
|
|
openResty shouldContain "location = /apple-app-site-association"
|
|
openResty shouldContain "location ^~ /i/"
|
|
Regex("""location \^~ /i/ \{\s+access_log off;""").containsMatchIn(openResty) shouldBe true
|
|
openResty shouldNotContain "alias /www/wwwroot/osglab.com/apple-app-site-association"
|
|
openResty shouldNotContain "unsafe-inline"
|
|
}
|
|
|
|
test("CI definition is singular and leaves MySQL lifecycle to Testcontainers") {
|
|
val ci = root.read(".github/workflows/ci.yml")
|
|
|
|
Regex("""(?m)^name: CI$""").findAll(ci).count() shouldBe 1
|
|
Regex("""(?m)^jobs:$""").findAll(ci).count() shouldBe 1
|
|
ci shouldContain "docker compose -f compose.yaml config --quiet"
|
|
ci shouldContain "./gradlew --no-daemon clean test"
|
|
ci shouldContain "./gradlew --no-daemon buildFatJar"
|
|
ci shouldContain "docker/build-push-action@v6"
|
|
ci shouldContain "packages: write"
|
|
ci shouldNotContain "3306:3306"
|
|
ci shouldNotContain "TEST_DB_"
|
|
}
|
|
|
|
test("AASA has one runtime template and no deploy-time identifier placeholder") {
|
|
val aasa = root.read("src/main/resources/invite/apple-app-site-association.json")
|
|
|
|
aasa shouldContain "\"{{APPLE_APP_ID}}\""
|
|
aasa shouldContain "\"/i/*\""
|
|
Files.exists(root.resolve("deploy/apple-app-site-association")) shouldBe false
|
|
}
|
|
})
|
|
|
|
private fun Path.read(relativePath: String): String =
|
|
Files.readString(resolve(relativePath))
|
|
|
|
private val LEDGER_ENTRY_TYPE_NAMES = listOf(
|
|
"SIGNUP_TRIAL",
|
|
"MANUAL_GRANT",
|
|
"USAGE_RESERVE",
|
|
"USAGE_SETTLE",
|
|
"USAGE_RELEASE",
|
|
"USAGE_REFUND",
|
|
"REFERRAL_INVITER",
|
|
"REFERRAL_INVITEE",
|
|
"STOREKIT_PURCHASE",
|
|
"SUBSCRIPTION_GRANT",
|
|
)
|
|
|
|
private val EXPECTED_PUBLIC_PATHS = setOf(
|
|
"/health",
|
|
"/health/live",
|
|
"/health/ready",
|
|
"/v1/auth/apple",
|
|
"/v1/auth/refresh",
|
|
"/v1/auth/logout",
|
|
"/v1/account",
|
|
"/v1/apple/events",
|
|
"/v1/analytics/events",
|
|
"/v1/analytics/keyboard-usage",
|
|
"/v1/credits/balance",
|
|
"/v1/credits/ledger",
|
|
"/v1/credits/rates",
|
|
"/v1/storekit/products",
|
|
"/v1/storekit/transactions",
|
|
"/v1/referrals",
|
|
"/v1/referrals/me",
|
|
"/v1/referrals/code",
|
|
"/v1/referrals/redeem",
|
|
"/v1/referrals/bind",
|
|
"/v1/referrals/campaigns",
|
|
"/v1/integrity/challenges",
|
|
"/v1/integrity/attest",
|
|
"/v1/integrity/assert",
|
|
"/v1/oobe/grants",
|
|
"/v1/oobe/grants/refresh",
|
|
"/v1/gateway/catalog",
|
|
"/v1/gateway/grants",
|
|
"/v1/gateway/grants/refresh",
|
|
"/v1/gateway/grants/{grantId}",
|
|
"/v1/gateway/llm/{capability}",
|
|
"/v1/gateway/asr",
|
|
"/v1/gateway/asr/sessions",
|
|
"/v1/gateway/asr/sessions/{sessionId}/stream",
|
|
"/v1/content/skills",
|
|
"/v1/content/hints/manifest",
|
|
"/v1/content/hints/{locale}",
|
|
"/hints/manifest.json",
|
|
"/hints/hints-{locale}.json",
|
|
"/v1/admin/content/skills",
|
|
"/v1/admin/content/skills/{id}",
|
|
"/v1/admin/content/skills/{id}/enable",
|
|
"/v1/admin/content/skills/{id}/disable",
|
|
"/v1/admin/content/hints/generation/settings",
|
|
"/v1/admin/content/hints/generation/status",
|
|
"/v1/admin/content/hints/generation/regenerate",
|
|
"/v1/admin/content/hints/{locale}",
|
|
"/v1/admin/auth/session",
|
|
"/v1/admin/auth/login",
|
|
"/v1/admin/auth/logout",
|
|
"/v1/admin/providers",
|
|
"/v1/admin/providers/{providerId}/api-key",
|
|
"/v1/admin/providers/{providerId}/api-key/reveal",
|
|
"/v1/admin/overview",
|
|
"/v1/admin/referrals",
|
|
"/v1/admin/analytics",
|
|
"/v1/admin/users",
|
|
"/v1/admin/users/{userId}",
|
|
"/v1/admin/users/{userId}/ledger",
|
|
"/v1/admin/credits/ledger",
|
|
"/v1/admin/credits/grants",
|
|
"/v1/admin/operators/summary",
|
|
"/v1/admin/operators",
|
|
"/v1/admin/operators/{operatorId}/enable",
|
|
"/v1/admin/operators/{operatorId}/disable",
|
|
"/v1/admin/operators/{operatorId}/unlock",
|
|
"/v1/admin/operators/{operatorId}/credentials/reset",
|
|
"/v1/admin/operators/{operatorId}/sessions/revoke",
|
|
"/v1/admin/audit",
|
|
"/.well-known/apple-app-site-association",
|
|
"/apple-app-site-association",
|
|
"/i/{code}",
|
|
)
|