diff --git a/fluxer_api/src/api/auth/AuthLogin.ts b/fluxer_api/src/api/auth/AuthLogin.ts index 05483a226..ca993857d 100644 --- a/fluxer_api/src/api/auth/AuthLogin.ts +++ b/fluxer_api/src/api/auth/AuthLogin.ts @@ -37,7 +37,6 @@ import * as AuthMfa from './AuthMfa'; import * as AuthPassword from './AuthPassword'; import * as AuthSession from './AuthSession'; import * as AuthUtility from './AuthUtility'; -import {assertFlutterClientLoginAllowed, type FlutterClientGateMemberRepository} from './FlutterClientGate'; function createRequestCache(): RequestCache { const userPartials = new Map(); @@ -73,7 +72,6 @@ interface LoginMfaWebAuthnParams { export interface LoginDependencies { inviteService: InviteService | null; kvDeletionQueue: KVAccountDeletionQueueService; - flutterClientGateMemberRepository: FlutterClientGateMemberRepository; } interface LoginTokenResult { @@ -219,7 +217,7 @@ export async function login( {data, request}: LoginParams, ): Promise { const {users, cache, rateLimit, email, config} = ctx.services; - const {inviteService, kvDeletionQueue, flutterClientGateMemberRepository} = deps; + const {inviteService, kvDeletionQueue} = deps; const skipRateLimits = config.dev.testModeEnabled || config.dev.disableRateLimits; const emailRateLimit = await rateLimit.checkLimit({ identifier: `login:email:${data.email}`, @@ -259,7 +257,6 @@ export async function login( {path: 'password', code: ValidationErrorCodes.INVALID_EMAIL_OR_PASSWORD}, ]); } - await assertFlutterClientLoginAllowed(request, user, flutterClientGateMemberRepository); let currentUser = await AuthUtility.handleBanStatus(ctx, user); if ((currentUser.flags & UserFlags.DISABLED) !== 0n && !currentUser.tempBannedUntil) { const updatedFlags = currentUser.flags & ~UserFlags.DISABLED; @@ -380,7 +377,6 @@ const MFA_USER_ATTEMPTS_WINDOW = seconds('15 minutes'); export async function loginMfaTotp( ctx: ApiContext, - deps: Pick, {code, ticket, request}: LoginMfaTotpParams, ): Promise { const {users, cache} = ctx.services; @@ -393,7 +389,6 @@ export async function loginMfaTotp( throw new UnknownUserError(); } AuthUtility.assertNonBotUser(ctx, user); - await assertFlutterClientLoginAllowed(request, user, deps.flutterClientGateMemberRepository); if (!user.totpSecret || !user.authenticatorTypes?.has(UserAuthenticatorTypes.TOTP)) { throw InputValidationError.fromCode('code', ValidationErrorCodes.TOTP_NOT_ENABLED); } @@ -429,7 +424,6 @@ export async function loginMfaTotp( export async function loginMfaWebAuthn( ctx: ApiContext, - deps: Pick, {response, challenge, ticket, request}: LoginMfaWebAuthnParams, ): Promise { const {users, cache} = ctx.services; @@ -442,7 +436,6 @@ export async function loginMfaWebAuthn( throw new UnknownUserError(); } AuthUtility.assertNonBotUser(ctx, user); - await assertFlutterClientLoginAllowed(request, user, deps.flutterClientGateMemberRepository); await AuthMfa.verifyWebAuthnAuthentication(ctx, user.id, response, challenge, 'mfa', ticket); await cache.delete(`mfa-ticket:${ticket}`); const [token] = await AuthSession.createAuthSession(ctx, {user, request}); diff --git a/fluxer_api/src/api/auth/AuthRegistration.ts b/fluxer_api/src/api/auth/AuthRegistration.ts index 6335fbd6b..12cdf5a88 100644 --- a/fluxer_api/src/api/auth/AuthRegistration.ts +++ b/fluxer_api/src/api/auth/AuthRegistration.ts @@ -51,7 +51,6 @@ import {deriveUsernameFromDisplayName} from '../utils/UsernameSuggestionUtils'; import * as AuthPassword from './AuthPassword'; import * as AuthSession from './AuthSession'; import * as AuthUtility from './AuthUtility'; -import {assertFlutterClientRegistrationAllowed} from './FlutterClientGate'; import type {IRegistrationRiskEvaluator} from './services/IRegistrationRiskEvaluator'; const DEFAULT_MINIMUM_AGE = 13; @@ -133,7 +132,6 @@ export async function register( riskAssessmentRepository, riskHistoryRepository, } = deps; - assertFlutterClientRegistrationAllowed(request, data.email ?? null); const appPublicConfig = await instanceConfigRepository.getAppPublicConfig(); const emailEnabled = await instanceConfigRepository.isEmailEnabled(); const requiresTermsConsent = shouldRequireHostedLegalConsent(config) || appPublicConfig.legal.terms_url !== null; diff --git a/fluxer_api/src/api/auth/AuthRequestService.ts b/fluxer_api/src/api/auth/AuthRequestService.ts index 53fd64d08..0a06b696a 100644 --- a/fluxer_api/src/api/auth/AuthRequestService.ts +++ b/fluxer_api/src/api/auth/AuthRequestService.ts @@ -179,7 +179,7 @@ export class AuthRequestService { } async loginMfaTotp({code, ticket, request}: AuthLoginMfaRequest): Promise { - const result = await AuthLogin.loginMfaTotp(this.apiContext, this.loginDependencies, {code, ticket, request}); + const result = await AuthLogin.loginMfaTotp(this.apiContext, {code, ticket, request}); return await this.toAuthTokenResponse(result); } @@ -285,7 +285,7 @@ export class AuthRequestService { } async loginMfaWebAuthn({data, request}: AuthWebAuthnMfaRequest): Promise { - const result = await AuthLogin.loginMfaWebAuthn(this.apiContext, this.loginDependencies, { + const result = await AuthLogin.loginMfaWebAuthn(this.apiContext, { response: data.response, challenge: data.challenge, ticket: data.ticket, diff --git a/fluxer_api/src/api/auth/FlutterClientGate.ts b/fluxer_api/src/api/auth/FlutterClientGate.ts deleted file mode 100644 index a44796634..000000000 --- a/fluxer_api/src/api/auth/FlutterClientGate.ts +++ /dev/null @@ -1,46 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import {ValidationErrorCodes} from '@fluxer/constants/src/ValidationErrorCodes'; -import {InputValidationError} from '@fluxer/errors/src/domains/core/InputValidationError'; -import {createGuildID} from '../BrandedTypes'; -import {Config} from '../Config'; -import type {IGuildMemberRepository} from '../guild/repositories/IGuildMemberRepository'; -import type {User} from '../models/User'; -import {accountPolicyContactHasCapability} from '../risk/AccountPolicyService'; -import {checkHasActivePaidPremium} from '../user/UserHelpers'; -import {isFluxerFlutterAndroidClient, isFluxerFlutterClient, isFluxerFlutterIosClient} from '../utils/UserAgentUtils'; - -const FLUTTER_CLIENT_ALLOWED_GUILD_ID = createGuildID(1489322182823577203n); -const ANDROID_FLUTTER_OPEN_ACCESS_AT_MS = Date.parse('2026-06-15T21:00:00.000Z'); - -export type FlutterClientGateMemberRepository = Pick; - -export async function assertFlutterClientLoginAllowed( - request: Request, - user: User, - memberRepository: FlutterClientGateMemberRepository, -): Promise { - if (!isFluxerFlutterClient(request)) return; - if (Config.instance.selfHosted) return; - if (accountPolicyContactHasCapability(user.email, 'client_gate_exempt')) return; - if (isFluxerFlutterAndroidClient(request) && isAndroidOpenAccessActive()) return; - if (isFluxerFlutterIosClient(request) && checkHasActivePaidPremium(user)) return; - const member = await memberRepository.getMember(FLUTTER_CLIENT_ALLOWED_GUILD_ID, user.id); - if (member) return; - throw InputValidationError.fromCodes([ - {path: 'email', code: ValidationErrorCodes.INVALID_EMAIL_OR_PASSWORD}, - {path: 'password', code: ValidationErrorCodes.INVALID_EMAIL_OR_PASSWORD}, - ]); -} - -export function assertFlutterClientRegistrationAllowed(request: Request, email: string | null | undefined): void { - if (!isFluxerFlutterClient(request)) return; - if (Config.instance.selfHosted) return; - if (accountPolicyContactHasCapability(email, 'client_gate_exempt')) return; - if (isFluxerFlutterAndroidClient(request) && isAndroidOpenAccessActive()) return; - throw InputValidationError.fromCode('email', ValidationErrorCodes.INVALID_EMAIL_ADDRESS); -} - -function isAndroidOpenAccessActive(): boolean { - return Date.now() >= ANDROID_FLUTTER_OPEN_ACCESS_AT_MS; -} diff --git a/fluxer_api/src/api/middleware/ServiceMiddleware.ts b/fluxer_api/src/api/middleware/ServiceMiddleware.ts index 74ab80363..425b8c94e 100644 --- a/fluxer_api/src/api/middleware/ServiceMiddleware.ts +++ b/fluxer_api/src/api/middleware/ServiceMiddleware.ts @@ -482,7 +482,6 @@ export const ServiceMiddleware = createMiddleware(async (ctx, next) => { inviteService, kvDeletionQueue: getKVAccountDeletionQueue(), - flutterClientGateMemberRepository: guildRepository, }, ); const reportService = getReportServiceInstance(); diff --git a/fluxer_api/src/api/risk/AccountPolicyEvaluator.ts b/fluxer_api/src/api/risk/AccountPolicyEvaluator.ts index 2a7d2df0e..346f9b4cf 100644 --- a/fluxer_api/src/api/risk/AccountPolicyEvaluator.ts +++ b/fluxer_api/src/api/risk/AccountPolicyEvaluator.ts @@ -18,7 +18,6 @@ type AccountPolicyAuditLevel = 'debug' | 'info' | 'warn' | 'error'; type AccountPolicyRiskHistoryOutcome = 'challenged'; export type AccountPolicyContactCapability = | 'captcha_exempt' - | 'client_gate_exempt' | 'followup_risk_exempt' | 'reputation_checks_exempt' | 'required_actions_exempt'; @@ -48,7 +47,6 @@ const CONTACT_CAPABILITY_FACT_PATHS = new Set([ ]); const CONTACT_CAPABILITIES = new Set([ 'captcha_exempt', - 'client_gate_exempt', 'followup_risk_exempt', 'reputation_checks_exempt', 'required_actions_exempt', diff --git a/fluxer_api/src/api/risk/__tests__/AccountPolicyEvaluator.test.ts b/fluxer_api/src/api/risk/__tests__/AccountPolicyEvaluator.test.ts index 570cb8830..93c6f191e 100644 --- a/fluxer_api/src/api/risk/__tests__/AccountPolicyEvaluator.test.ts +++ b/fluxer_api/src/api/risk/__tests__/AccountPolicyEvaluator.test.ts @@ -137,7 +137,6 @@ describe('AccountPolicyEvaluator', () => { const evaluator = createCurrentBehaviorTestAccountPolicyEvaluator(); const decision = evaluator.evaluateContact(createAccountPolicyContactContext(`test@${TEST_POLICY_CONTACT_DOMAIN}`)); expect(decision.hasCapability('captcha_exempt')).toBe(true); - expect(decision.hasCapability('client_gate_exempt')).toBe(true); expect(decision.hasCapability('followup_risk_exempt')).toBe(true); expect(decision.hasCapability('required_actions_exempt')).toBe(true); diff --git a/fluxer_api/src/api/test/AccountPolicyTestEvaluator.ts b/fluxer_api/src/api/test/AccountPolicyTestEvaluator.ts index c8ff884bf..3246b0efb 100644 --- a/fluxer_api/src/api/test/AccountPolicyTestEvaluator.ts +++ b/fluxer_api/src/api/test/AccountPolicyTestEvaluator.ts @@ -145,7 +145,7 @@ const CURRENT_BEHAVIOR_TEST_POLICY_DSL = { fact: 'contact.domain', in_set: 'contact_domain_exclusions', }, - capabilities: ['captcha_exempt', 'client_gate_exempt', 'followup_risk_exempt', 'required_actions_exempt'], + capabilities: ['captcha_exempt', 'followup_risk_exempt', 'required_actions_exempt'], }, { id: 'contact_domain_reputation_profile', diff --git a/fluxer_api/src/api/utils/UserAgentUtils.ts b/fluxer_api/src/api/utils/UserAgentUtils.ts index 6599663a3..17a8418b0 100644 --- a/fluxer_api/src/api/utils/UserAgentUtils.ts +++ b/fluxer_api/src/api/utils/UserAgentUtils.ts @@ -2,15 +2,12 @@ import Bowser from 'bowser'; import {Logger} from '../Logger'; -import {parseJsonRecord} from './JsonBoundaryUtils'; interface UserAgentInfo { clientOs: string; detectedPlatform: string; } -type FluxerClientProperties = Record; - const UNKNOWN_LABEL = 'Unknown'; function formatName(name?: string | null): string { @@ -33,57 +30,6 @@ function parseUserAgentSafe(userAgentRaw: string): UserAgentInfo { } } -export function isFluxerFlutterClient(request: Request): boolean { - const parsed = parseFluxerClientProperties(request); - return parsed?.client_runtime === 'flutter'; -} - -export function isFluxerFlutterAndroidClient(request: Request): boolean { - return isFluxerFlutterPlatformClient(request, 'android'); -} - -export function isFluxerFlutterIosClient(request: Request): boolean { - return ( - isFluxerFlutterPlatformClient(request, 'ios') || - isFluxerFlutterPlatformClient(request, 'iphone') || - isFluxerFlutterPlatformClient(request, 'ipad') || - isFluxerFlutterPlatformClient(request, 'ipod') - ); -} - -function isFluxerFlutterPlatformClient(request: Request, platform: string): boolean { - const platformCandidates = getFluxerFlutterPlatformCandidates(request); - if (!platformCandidates) return false; - const needle = platform.toLowerCase(); - return platformCandidates.some((value) => typeof value === 'string' && value.toLowerCase().includes(needle)); -} - -function getFluxerFlutterPlatformCandidates(request: Request): Array | null { - const parsed = parseFluxerClientProperties(request); - if (parsed?.client_runtime !== 'flutter') return null; - return [ - parsed.client_os, - parsed.os, - parsed.operating_system, - parsed.platform, - parsed.device_platform, - parsed.client_platform, - parsed.user_agent, - request.headers.get('user-agent'), - ]; -} - -function parseFluxerClientProperties(request: Request): FluxerClientProperties | null { - const encoded = request.headers.get('x-fluxer-client-properties'); - if (!encoded) return null; - try { - const decoded = Buffer.from(encoded, 'base64').toString('utf8'); - return parseJsonRecord(decoded); - } catch { - return null; - } -} - export function resolveSessionClientInfo(args: {userAgent: string | null; isDesktopClient: boolean | null}): { clientOs: string; clientPlatform: string;