Merge remote-tracking branch 'origin/main'

This commit is contained in:
Weblate
2026-09-24 01:41:35 +00:00
19 changed files with 306 additions and 8 deletions
+19 -2
View File
@@ -49,7 +49,7 @@ On Linux, prefer a repository over a single file so Fluxer updates with the rest
## Linux package repositories
Every repository serves both channels. The package is `fluxer` for stable, `fluxer-canary` for canary.
The package is `fluxer` for stable and `fluxer-canary` for canary. apt and dnf subscribe to one channel per entry file. pacman and Flatpak serve both from one repository.
### Flatpak
@@ -59,7 +59,7 @@ Stable is on [Flathub][flathub], the easiest route on most desktops:
flatpak install flathub app.fluxer.Fluxer
```
Flathub has stable only. For canary, or to use Fluxer's own repository, open [this reference file][flatpak-ref] and your software manager takes over. Some desktops also accept `flatpak+https://pkgs.fluxer.com/flatpak/fluxer.flatpakref` in the address bar.
Flathub has stable only. To use Fluxer's own repository, open [the stable][flatpak-ref] or [the canary][flatpak-canary-ref] reference file and your software manager takes over. Some desktops also accept `flatpak+https://pkgs.fluxer.com/flatpak/fluxer.flatpakref` in the address bar.
From a terminal:
@@ -76,6 +76,15 @@ sudo curl -fsSL -o /etc/apt/sources.list.d/fluxer.sources https://pkgs.fluxer.co
sudo apt update && sudo apt install fluxer
```
For canary, use the canary entry file and package.
```sh
sudo curl -fsSL -o /etc/apt/sources.list.d/fluxer-canary.sources https://pkgs.fluxer.com/deb/fluxer-canary.sources
sudo apt update && sudo apt install fluxer-canary
```
A `.deb` installed from a download only updates once its channel's entry is added.
### Fedora and RHEL
```sh
@@ -83,6 +92,13 @@ sudo curl -fsSL -o /etc/yum.repos.d/fluxer.repo https://pkgs.fluxer.com/rpm/flux
sudo dnf install fluxer
```
For canary, use the canary entry file and package.
```sh
sudo curl -fsSL -o /etc/yum.repos.d/fluxer-canary.repo https://pkgs.fluxer.com/rpm/fluxer-canary.repo
sudo dnf install fluxer-canary
```
RHEL, Rocky, Alma and CentOS Stream need `sudo dnf install epel-release` first, because their base repositories lack `libXScrnSaver`. Fedora does not.
### Arch Linux
@@ -150,6 +166,7 @@ endorsement rights.
[linux-targz-x64]: https://pkgs.fluxer.com/desktop/stable/linux/x64/latest/tar_gz
[linux-targz-arm64]: https://pkgs.fluxer.com/desktop/stable/linux/arm64/latest/tar_gz
[flatpak-ref]: https://pkgs.fluxer.com/flatpak/fluxer.flatpakref
[flatpak-canary-ref]: https://pkgs.fluxer.com/flatpak/fluxer-canary.flatpakref
[flathub]: https://flathub.org/apps/app.fluxer.Fluxer
[android-apk]: https://github.com/fluxerapp/flutter_client/releases
[obtainium]: https://obtainium.imranr.dev/
+4
View File
@@ -476,6 +476,10 @@ export function buildAPIConfigFromMaster(master: MasterConfig): APIConfig {
},
abusePolicy: {
inboundPhoneCountryCodes: master.instance.abuse_policy.inbound_phone_country_codes,
phoneFlagging: {
enabled: master.instance.abuse_policy.phone_flagging.enabled,
exemptCountryCodes: master.instance.abuse_policy.phone_flagging.exempt_country_codes,
},
phoneVerification: {
inboundRequiredPrefixes: master.instance.abuse_policy.phone_verification.inbound_required_prefixes,
},
+4 -2
View File
@@ -23,7 +23,7 @@ import {profileSubstringBlocklistCache} from '@app/api/middleware/ProfileSubstri
import type {RequestCache} from '@app/api/middleware/RequestCacheMiddleware';
import type {User} from '@app/api/models/User';
import {UserSettings} from '@app/api/models/UserSettings';
import {countryRequiresInboundPhoneVerification} from '@app/api/risk/AbusePolicy';
import {countryRequiresInboundPhoneVerification, stripDisallowedPhoneFlags} from '@app/api/risk/AbusePolicy';
import {
type IAccountPolicyEvaluator,
isAssessmentThresholdAuditEvent,
@@ -362,7 +362,9 @@ export async function register(
action: riskResult.recommendedAction,
},
});
const combinedFlags = await deferPhoneFlagsUntilCommunityJoin(policyDecision.flagBits);
const combinedFlags = await deferPhoneFlagsUntilCommunityJoin(
await stripDisallowedPhoneFlags(policyDecision.flagBits, async () => countryCode),
);
const createdAt = new Date();
const riskContext = deriveLatestRiskContext({
userId: userId.toString(),
@@ -9,6 +9,7 @@ import {
loginAccount,
registerUser,
} from '@app/api/auth/tests/AuthTestUtils';
import {Config} from '@app/api/Config';
import {setInjectedRegistrationRiskEvaluator} from '@app/api/middleware/ServiceMiddleware';
import {getInstanceConfigRepository} from '@app/api/middleware/ServiceSingletons';
import {
@@ -33,7 +34,7 @@ import {
SuspiciousActivityFlags,
} from '@fluxer/constants/src/UserConstants';
import type {GuildResponse} from '@fluxer/schema/src/domains/guild/GuildResponseSchemas';
import {afterAll, beforeAll, beforeEach, describe, expect, it, vi} from 'vitest';
import {afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi} from 'vitest';
function phoneRiskEvaluator(level: RiskLevelType, riskScore: number): IRegistrationRiskEvaluator {
return {
@@ -241,6 +242,59 @@ describe('Deferred phone verification gate', () => {
expect(flags & SuspiciousActivityFlags.REQUIRE_VERIFIED_PHONE).not.toBe(0);
});
describe('with phone flagging disabled', () => {
const originalPhoneFlagging = {...Config.abusePolicy.phoneFlagging};
afterEach(() => {
Config.abusePolicy.phoneFlagging = originalPhoneFlagging;
});
it('sets no phone requirement and no deferral at registration', async () => {
await getInstanceConfigRepository().setInstancePolicyConfig({deferred_phone_gate_enabled: true});
Config.abusePolicy.phoneFlagging = {enabled: false, exemptCountryCodes: []};
setInjectedRegistrationRiskEvaluator(phoneRiskEvaluator(RiskLevel.High, 70));
const registration = await registerUser(harness, {
email: createUniqueEmail('flagging-off'),
username: createUniqueUsername('flagging_off'),
global_name: 'Flagging Off',
password: 'StrongPassword!123',
date_of_birth: '2000-01-01',
consent: true,
});
const flags = await readFlags(registration.user_id);
expect(flags & SuspiciousActivityFlags.REQUIRE_VERIFIED_PHONE).toBe(0);
expect(flags & DEFERRED_PHONE_ON_COMMUNITY_JOIN).toBe(0);
});
it('keeps an existing deferral dormant on a qualifying join', async () => {
await getInstanceConfigRepository().setInstancePolicyConfig({
deferred_phone_gate_enabled: true,
deferred_phone_gate_member_threshold: 1,
deferred_phone_gate_window_hours: 24,
});
const {inviteCode} = await createGuildWithInvite(harness);
const filler = await createTestAccount(harness);
await createBuilder(harness, filler.token).post(`/invites/${inviteCode}`).expect(200).execute();
setInjectedRegistrationRiskEvaluator(phoneRiskEvaluator(RiskLevel.High, 70));
const registration = await registerUser(harness, {
email: createUniqueEmail('flagging-off-join'),
username: createUniqueUsername('flagging_off_join'),
global_name: 'Flagging Off Join',
password: 'StrongPassword!123',
date_of_birth: '2000-01-01',
consent: true,
});
setInjectedRegistrationRiskEvaluator(undefined);
expect((await readFlags(registration.user_id)) & DEFERRED_PHONE_ON_COMMUNITY_JOIN).not.toBe(0);
Config.abusePolicy.phoneFlagging = {enabled: false, exemptCountryCodes: []};
await createBuilder(harness, registration.token).post(`/invites/${inviteCode}`).expect(200).execute();
const flags = await readFlags(registration.user_id);
expect(flags & DEFERRED_PHONE_ON_COMMUNITY_JOIN).not.toBe(0);
expect(flags & PHONE_GATE_PROMOTED_FROM_DEFERRAL).toBe(0);
});
});
describe('phone gate escape', () => {
async function configurePhoneGate(
overrides: {
+4
View File
@@ -336,6 +336,10 @@ export interface APIConfig {
};
abusePolicy: {
inboundPhoneCountryCodes: Array<string>;
phoneFlagging: {
enabled: boolean;
exemptCountryCodes: Array<string>;
};
phoneVerification: {
inboundRequiredPrefixes: Array<string>;
};
@@ -3,6 +3,7 @@
import {requireEmailVerified} from '@app/api/auth/EmailVerificationUtils';
import type {GuildID, InviteCode, RoleID, UserID} from '@app/api/BrandedTypes';
import {createChannelID, createRoleID} from '@app/api/BrandedTypes';
import {Config} from '@app/api/Config';
import type {ChannelService} from '@app/api/channel/services/ChannelService';
import {assertMutableUserId} from '@app/api/constants/Core';
import type {GuildMemberRow} from '@app/api/database/types/GuildTypes';
@@ -421,6 +422,13 @@ export class GuildMemberOperationsService {
memberCount: guild.memberCount,
accountAgeMs: Date.now() - snowflakeToDate(BigInt(user.id)).getTime(),
};
if (
!Config.abusePolicy.phoneFlagging.enabled &&
(getEffectiveSuspiciousFlags(user) & PHONE_REQUIREMENT_FLAGS) === 0
) {
Logger.info(logContext, 'deferred_phone_gate.skipped_phone_flagging_disabled');
return;
}
if (status !== 'ok') {
const undeferredFlags = getEffectiveSuspiciousFlags({
...user,
+42
View File
@@ -1,6 +1,29 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {Config} from '@app/api/Config';
import {PHONE_REQUIREMENT_FLAGS, SuspiciousActivityFlags} from '@fluxer/constants/src/UserConstants';
const EMAIL_ONLY_EQUIVALENTS: ReadonlyArray<readonly [number, number]> = [
[SuspiciousActivityFlags.REQUIRE_VERIFIED_EMAIL_OR_VERIFIED_PHONE, SuspiciousActivityFlags.REQUIRE_VERIFIED_EMAIL],
[SuspiciousActivityFlags.REQUIRE_VERIFIED_EMAIL_OR_REVERIFIED_PHONE, SuspiciousActivityFlags.REQUIRE_VERIFIED_EMAIL],
[
SuspiciousActivityFlags.REQUIRE_REVERIFIED_EMAIL_OR_VERIFIED_PHONE,
SuspiciousActivityFlags.REQUIRE_REVERIFIED_EMAIL,
],
[
SuspiciousActivityFlags.REQUIRE_REVERIFIED_EMAIL_OR_REVERIFIED_PHONE,
SuspiciousActivityFlags.REQUIRE_REVERIFIED_EMAIL,
],
];
const PHONE_OFFERING_FLAGS = EMAIL_ONLY_EQUIVALENTS.reduce((mask, [either]) => mask | either, PHONE_REQUIREMENT_FLAGS);
function withoutPhoneOfferingFlags(flagBits: number): number {
return EMAIL_ONLY_EQUIVALENTS.reduce(
(next, [either, emailOnly]) => ((flagBits & either) !== 0 ? next | emailOnly : next),
flagBits & ~PHONE_OFFERING_FLAGS,
);
}
function normalizeCountryCode(countryCode: string | null | undefined): string | null {
const trimmed = countryCode?.trim();
@@ -17,6 +40,25 @@ export function countryRequiresInboundPhoneVerification(countryCode: string | nu
return configuredCountrySet(Config.abusePolicy.inboundPhoneCountryCodes).has(normalized);
}
export function phoneFlaggingAllowedForCountry(countryCode: string | null | undefined): boolean {
const {enabled, exemptCountryCodes} = Config.abusePolicy.phoneFlagging;
if (!enabled) return false;
const normalized = normalizeCountryCode(countryCode);
if (!normalized) return true;
return !configuredCountrySet(exemptCountryCodes).has(normalized);
}
export async function stripDisallowedPhoneFlags(
flagBits: number,
resolveCountryCode: () => Promise<string | null>,
): Promise<number> {
if ((flagBits & PHONE_OFFERING_FLAGS) === 0) return flagBits;
const {enabled, exemptCountryCodes} = Config.abusePolicy.phoneFlagging;
if (enabled && exemptCountryCodes.length === 0) return flagBits;
if (enabled && phoneFlaggingAllowedForCountry(await resolveCountryCode())) return flagBits;
return withoutPhoneOfferingFlags(flagBits);
}
export function phoneRequiresInboundVerification(
phone: string,
prefixes: ReadonlyArray<string> = Config.abusePolicy.phoneVerification.inboundRequiredPrefixes,
@@ -0,0 +1,86 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {Config} from '@app/api/Config';
import {phoneFlaggingAllowedForCountry, stripDisallowedPhoneFlags} from '@app/api/risk/AbusePolicy';
import {SuspiciousActivityFlags} from '@fluxer/constants/src/UserConstants';
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
const PHONE_AND_EMAIL =
SuspiciousActivityFlags.REQUIRE_VERIFIED_EMAIL |
SuspiciousActivityFlags.REQUIRE_VERIFIED_PHONE |
SuspiciousActivityFlags.REQUIRE_INBOUND_PHONE_VERIFICATION;
describe('phone flagging policy', () => {
const original = {...Config.abusePolicy.phoneFlagging};
beforeEach(() => {
Config.abusePolicy.phoneFlagging = {enabled: true, exemptCountryCodes: []};
});
afterEach(() => {
Config.abusePolicy.phoneFlagging = original;
});
it('keeps phone flags by default without resolving the country', async () => {
const resolveCountryCode = vi.fn(async () => 'NG');
expect(await stripDisallowedPhoneFlags(PHONE_AND_EMAIL, resolveCountryCode)).toBe(PHONE_AND_EMAIL);
expect(resolveCountryCode).not.toHaveBeenCalled();
expect(phoneFlaggingAllowedForCountry('NG')).toBe(true);
});
it('strips only phone flags when disabled', async () => {
Config.abusePolicy.phoneFlagging = {enabled: false, exemptCountryCodes: []};
const resolveCountryCode = vi.fn(async () => 'NG');
expect(await stripDisallowedPhoneFlags(PHONE_AND_EMAIL, resolveCountryCode)).toBe(
SuspiciousActivityFlags.REQUIRE_VERIFIED_EMAIL,
);
expect(resolveCountryCode).not.toHaveBeenCalled();
expect(phoneFlaggingAllowedForCountry('NG')).toBe(false);
expect(phoneFlaggingAllowedForCountry(null)).toBe(false);
});
it('strips phone flags for exempt countries only', async () => {
Config.abusePolicy.phoneFlagging = {enabled: true, exemptCountryCodes: [' br', 'PT']};
expect(await stripDisallowedPhoneFlags(PHONE_AND_EMAIL, async () => 'BR')).toBe(
SuspiciousActivityFlags.REQUIRE_VERIFIED_EMAIL,
);
expect(await stripDisallowedPhoneFlags(PHONE_AND_EMAIL, async () => 'ng')).toBe(PHONE_AND_EMAIL);
expect(await stripDisallowedPhoneFlags(PHONE_AND_EMAIL, async () => null)).toBe(PHONE_AND_EMAIL);
expect(phoneFlaggingAllowedForCountry('pt')).toBe(false);
expect(phoneFlaggingAllowedForCountry('NG')).toBe(true);
});
it('replaces email or phone flags with their email only equivalent', async () => {
Config.abusePolicy.phoneFlagging = {enabled: false, exemptCountryCodes: []};
expect(
await stripDisallowedPhoneFlags(
SuspiciousActivityFlags.REQUIRE_VERIFIED_EMAIL_OR_VERIFIED_PHONE |
SuspiciousActivityFlags.REQUIRE_VERIFIED_EMAIL_OR_REVERIFIED_PHONE,
async () => null,
),
).toBe(SuspiciousActivityFlags.REQUIRE_VERIFIED_EMAIL);
expect(
await stripDisallowedPhoneFlags(
SuspiciousActivityFlags.REQUIRE_REVERIFIED_EMAIL_OR_VERIFIED_PHONE |
SuspiciousActivityFlags.REQUIRE_INBOUND_PHONE_VERIFICATION,
async () => null,
),
).toBe(SuspiciousActivityFlags.REQUIRE_REVERIFIED_EMAIL);
Config.abusePolicy.phoneFlagging = {enabled: true, exemptCountryCodes: ['BR']};
expect(
await stripDisallowedPhoneFlags(
SuspiciousActivityFlags.REQUIRE_REVERIFIED_EMAIL_OR_REVERIFIED_PHONE,
async () => 'BR',
),
).toBe(SuspiciousActivityFlags.REQUIRE_REVERIFIED_EMAIL);
});
it('skips the country lookup when no phone flags are present', async () => {
Config.abusePolicy.phoneFlagging = {enabled: true, exemptCountryCodes: ['BR']};
const resolveCountryCode = vi.fn(async () => 'BR');
expect(await stripDisallowedPhoneFlags(SuspiciousActivityFlags.REQUIRE_VERIFIED_EMAIL, resolveCountryCode)).toBe(
SuspiciousActivityFlags.REQUIRE_VERIFIED_EMAIL,
);
expect(resolveCountryCode).not.toHaveBeenCalled();
});
});
@@ -10,7 +10,7 @@ import type {UserCacheService} from '@app/api/infrastructure/UserCacheService';
import {Logger} from '@app/api/Logger';
import type {RequestCache} from '@app/api/middleware/RequestCacheMiddleware';
import type {User} from '@app/api/models/User';
import {countryRequiresInboundPhoneVerification} from '@app/api/risk/AbusePolicy';
import {countryRequiresInboundPhoneVerification, phoneFlaggingAllowedForCountry} from '@app/api/risk/AbusePolicy';
import {
createRpcTimingNode,
RpcTimingRecorder,
@@ -311,6 +311,17 @@ export class RpcSessionStartService {
) {
return null;
}
if (
!timeRpcStepSync(timingSteps, 'check_phone_flagging_allowed', () =>
phoneFlaggingAllowedForCountry(geoipCountryIso),
)
) {
Logger.info(
{userId: user.id.toString(), countryIso: geoipCountryIso},
'Skipping configured-country inbound phone requirement: phone flagging disabled for this country',
);
return null;
}
if (
timeRpcStepSync(timingSteps, 'check_not_suspicious_flag', () => (user.flags & UserFlags.NOT_SUSPICIOUS) !== 0n)
) {
@@ -1,5 +1,6 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {Config} from '@app/api/Config';
import type {User} from '@app/api/models/User';
import {setInjectedAccountPolicyEvaluator} from '@app/api/risk/AccountPolicyService';
import {setCachedDeferredPhoneGateEnabled} from '@app/api/risk/DeferredPhoneGateCache';
@@ -52,6 +53,20 @@ describe('deferred phone gate marker', () => {
});
expect(getRequiredActions(user)).toEqual(['REQUIRE_VERIFIED_PHONE']);
});
it('keeps a deferral suppressed when the gate reads off but phone flagging is disabled', () => {
setCachedDeferredPhoneGateEnabled(false);
const original = {...Config.abusePolicy.phoneFlagging};
Config.abusePolicy.phoneFlagging = {enabled: false, exemptCountryCodes: []};
try {
const user = createUser({
suspiciousActivityFlags: SuspiciousActivityFlags.REQUIRE_VERIFIED_PHONE | DEFERRED_PHONE_ON_COMMUNITY_JOIN,
});
expect(getRequiredActions(user)).toEqual([]);
expect(getEffectiveSuspiciousFlags(user)).toBe(0);
} finally {
Config.abusePolicy.phoneFlagging = original;
}
});
it('suppresses a deferred phone requirement so the account is not locked out', () => {
const user = createUser({
suspiciousActivityFlags: SuspiciousActivityFlags.REQUIRE_VERIFIED_PHONE | DEFERRED_PHONE_ON_COMMUNITY_JOIN,
+1 -1
View File
@@ -134,7 +134,7 @@ function suppressDeferredPhoneFlags(rawFlags: number): number {
if ((rawFlags & DEFERRED_PHONE_ON_COMMUNITY_JOIN) === 0) {
return rawFlags;
}
if (getCachedDeferredPhoneGateEnabled() === false) {
if (getCachedDeferredPhoneGateEnabled() === false && Config.abusePolicy.phoneFlagging.enabled) {
return rawFlags & ~DEFERRED_PHONE_ON_COMMUNITY_JOIN;
}
return rawFlags & ~DEFERRABLE_PHONE_FLAGS;
@@ -12,6 +12,7 @@ import {Logger} from '@app/api/Logger';
import type {RequestCache} from '@app/api/middleware/RequestCacheMiddleware';
import type {AuthSession} from '@app/api/models/AuthSession';
import type {User} from '@app/api/models/User';
import {stripDisallowedPhoneFlags} from '@app/api/risk/AbusePolicy';
import {createAccountPolicyContactContext, type IAccountPolicyEvaluator} from '@app/api/risk/AccountPolicyEvaluator';
import type {IRegistrationEventsRepository} from '@app/api/risk/adapters/VelocityAdapter';
import type {IRiskHistoryRepository} from '@app/api/risk/HistoricalOutcomeRepository';
@@ -42,6 +43,7 @@ import {
mapUserToPrivateResponse,
mapUserToProfileResponse,
} from '@app/api/user/UserMappers';
import {lookupGeoip} from '@app/api/utils/IpUtils';
import {DEFERRED_PHONE_ON_COMMUNITY_JOIN, imposePhoneRequirements} from '@fluxer/constants/src/UserConstants';
import {ValidationErrorCodes} from '@fluxer/constants/src/ValidationErrorCodes';
import {getCurrentTimeZoneOffsetMinutes} from '@fluxer/date_utils/src/TimeZoneUtils';
@@ -302,7 +304,11 @@ export class UserAccountRequestService {
action: emailSetRecommendedAction,
},
});
nextSuspiciousFlags = imposePhoneRequirements(nextSuspiciousFlags, policyDecision.flagBits);
const policyFlagBits = await stripDisallowedPhoneFlags(
policyDecision.flagBits,
async () => (await lookupGeoip(request)).countryCode,
);
nextSuspiciousFlags = imposePhoneRequirements(nextSuspiciousFlags, policyFlagBits);
if (nextSuspiciousFlags !== currentSuspiciousFlags) {
user = await this.userRepository.patchUpsert(
user.id,
@@ -1680,6 +1680,7 @@ module.exports = {
},
deb: {
packageCategory: 'net',
synopsis: 'Instant messaging and VoIP',
desktop: {
entry: linuxDesktopEntryWithActions,
desktopActions: linuxDesktopActions,
@@ -19,6 +19,16 @@ sudo curl -fsSL -o /etc/apt/sources.list.d/fluxer.sources \
sudo apt update && sudo apt install fluxer
```
For the canary channel, add `fluxer-canary.sources` and install `fluxer-canary`.
```
sudo curl -fsSL -o /etc/apt/sources.list.d/fluxer-canary.sources \
https://pkgs.fluxer.com/deb/fluxer-canary.sources
sudo apt update && sudo apt install fluxer-canary
```
The stable entry does not list `fluxer-canary`. A canary `.deb` installed from a download stays on its version until the canary entry is added.
The `.sources` entry uses `Signed-By` rather than `Trusted: yes`, so `apt update` verifies the repository and prints nothing.
## dnf
@@ -34,6 +44,14 @@ sudo dnf install fluxer
The `.repo` file names the signing key by URL, so dnf fetches it rather than needing the keyring step the apt entry has. Without the `rpm --import` line dnf asks to import twice on a first install, once for the repository metadata and once for the package. With it dnf asks once, for the metadata, which dnf keeps in its own key store. Both prompts print the fingerprint, which reads `09D01339EE128925F75E675C855C5BDE34D205D2`.
For the canary channel, add `fluxer-canary.repo` and install `fluxer-canary`.
```
sudo curl -fsSL -o /etc/yum.repos.d/fluxer-canary.repo \
https://pkgs.fluxer.com/rpm/fluxer-canary.repo
sudo dnf install fluxer-canary
```
Metadata expires after six hours, so a freshly published build becomes visible within that window, or immediately with `dnf --refresh upgrade`.
:::caution[RHEL, Rocky, Alma and CentOS Stream need EPEL]
@@ -991,6 +991,14 @@ No default. The account risk policy. JSON. Malformed JSON fails startup, and an
Default empty. Allowed inbound phone countries. Comma separated, passed through unvalidated.
#### `FLUXER_ABUSE_PHONE_FLAGGING_ENABLED`
Default `true`. Automatic phone requirements. With this off, registration, setting an email and gateway session start never add a requirement that offers phone verification. An email or phone requirement becomes its email only form. A deferred phone requirement stays dormant on a community join. Requirements already on an account and flags set by an Admin are untouched.
#### `FLUXER_ABUSE_PHONE_FLAGGING_EXEMPT_COUNTRY_CODES`
Default empty. Countries that never get an automatic phone requirement, matched against the request's GeoIP country. An email or phone requirement becomes its email only form. Comma separated, unvalidated.
#### `FLUXER_ABUSE_PHONE_INBOUND_REQUIRED_PREFIXES`
Default empty. Required inbound prefixes. Comma separated.
+4
View File
@@ -277,6 +277,10 @@ function defaultConfig(): MasterConfig {
},
abuse_policy: {
inbound_phone_country_codes: [],
phone_flagging: {
enabled: true,
exempt_country_codes: [],
},
phone_verification: {
inbound_required_prefixes: [],
},
+4
View File
@@ -341,6 +341,10 @@ export interface MasterConfig {
};
abuse_policy: {
inbound_phone_country_codes: Array<string>;
phone_flagging: {
enabled: boolean;
exempt_country_codes: Array<string>;
};
phone_verification: {
inbound_required_prefixes: Array<string>;
};
@@ -464,6 +464,8 @@ describe('ConfigLoader', () => {
FLUXER_APP_STATUS_PAGE_INCIDENT_HISTORY_URL: 'https://status.example/history',
FLUXER_INSTANCE_SETUP_CONFIGURED: 'true',
FLUXER_ABUSE_INBOUND_PHONE_COUNTRY_CODES: 'AA,BB',
FLUXER_ABUSE_PHONE_FLAGGING_ENABLED: 'false',
FLUXER_ABUSE_PHONE_FLAGGING_EXEMPT_COUNTRY_CODES: 'CC,DD',
FLUXER_ABUSE_PHONE_INBOUND_REQUIRED_PREFIXES: '+101,+202',
FLUXER_ABUSE_DIRECT_CONTACT_SPAM_ENABLED: 'true',
FLUXER_ABUSE_DIRECT_CONTACT_SPAM_COUNTRY_CODES: 'AA,BB',
@@ -492,6 +494,10 @@ describe('ConfigLoader', () => {
expect(config.instance.setup.configured).toBe(true);
expect(config.instance.abuse_policy).toEqual({
inbound_phone_country_codes: ['AA', 'BB'],
phone_flagging: {
enabled: false,
exempt_country_codes: ['CC', 'DD'],
},
phone_verification: {
inbound_required_prefixes: ['+101', '+202'],
},
@@ -369,6 +369,14 @@ const NAMED_FLUXER_ENV_OVERRIDES: Record<string, NamedEnvOverride> = {
path: ['instance', 'abuse_policy', 'inbound_phone_country_codes'],
parse: parseCsv,
},
FLUXER_ABUSE_PHONE_FLAGGING_ENABLED: {
path: ['instance', 'abuse_policy', 'phone_flagging', 'enabled'],
parse: parseBoolean,
},
FLUXER_ABUSE_PHONE_FLAGGING_EXEMPT_COUNTRY_CODES: {
path: ['instance', 'abuse_policy', 'phone_flagging', 'exempt_country_codes'],
parse: parseCsv,
},
FLUXER_ABUSE_PHONE_INBOUND_REQUIRED_PREFIXES: {
path: ['instance', 'abuse_policy', 'phone_verification', 'inbound_required_prefixes'],
parse: parseCsv,