refactor(api): emit moderation events and apply account actions (#3031)

This commit is contained in:
Hampus
2026-09-29 12:24:15 +02:00
committed by GitHub
parent c488906131
commit 364084c819
385 changed files with 8994 additions and 22221 deletions
+3 -18
View File
@@ -126,7 +126,7 @@ const OUT_OF_BAND_CREDENTIAL = new Map<string, OutOfBandRoute>([
'POST /webhooks/twilio/sms',
{
reason:
'installed only when config.sms.enabled, and then only when the inbound webhook token and public URL are both set, so a default or self-hosted instance never registers it',
'a provider callback that is forwarded untouched to the internal event bus and answers 500 while that bus is unavailable',
documentedIn: null,
},
],
@@ -208,7 +208,7 @@ const EXEMPTION_RULES: ReadonlyArray<ExemptionRule> = [
name: 'out-of-band credential',
justification:
'no ordinary client holds the credential. Each entry states its guard, and three are covered in prose',
anchors: [{file: 'fluxer_api/src/api/app/ControllerRegistry.ts', anchor: 'if (config.sms.enabled) {'}],
anchors: [{file: 'fluxer_api/src/api/app/ControllerRegistry.ts', anchor: 'installSmsWebhookForwarder(routes'}],
covers: (shape) => OUT_OF_BAND_CREDENTIAL.has(shape),
},
];
@@ -1699,7 +1699,7 @@ console.log('unthrottled routes and global bucket claims');
failures += section('rate limit prose disagreements', problems);
}
console.log('error registry and abuse signal weights');
console.log('error registry');
{
const errorsPage = await readFile(path.join(DOCS_ROOT, 'http-api/errors.md'), 'utf8');
const documentedCodes = new Set<string>();
@@ -1732,20 +1732,6 @@ console.log('error registry and abuse signal weights');
}
}
const banner = await readFile(path.join(REPO_ROOT, 'fluxer_api/src/api/middleware/AbusiveIpAutoBanner.ts'), 'utf8');
const weights = new Map<string, string>();
for (const rule of banner.matchAll(/if \(status === (\d{3})\) return ([\d.]+);/gu)) {
weights.set(rule[1], rule[2]);
}
for (const [status, weight] of weights) {
if (status === '404') {
continue;
}
if (!errorsPage.includes(`A ${status} weighs ${weight}`) && !errorsPage.includes(`a ${status} weighs ${weight}`)) {
problems.push(`errors.md does not state that a ${status} weighs ${weight}`);
}
}
const documentedRegistryCodes = [...registryCodes].filter((c) => documentedCodes.has(c)).length;
const UNDOCUMENTED_VALIDATION_CODES = new Set(['EMAIL_DOMAIN_CANNOT_RECEIVE_MAIL']);
const expectedEntries = registryCodes.size + validationCodes.size - UNDOCUMENTED_VALIDATION_CODES.size;
@@ -1765,7 +1751,6 @@ console.log('error registry and abuse signal weights');
);
}
console.log(` registry codes: ${registryCodes.size.toString()}, documented: ${documentedRegistryCodes.toString()}`);
console.log(` abuse signal weights compared: ${weights.size.toString()}`);
failures += section('error registry disagreements', problems);
}
+2 -2
View File
@@ -270,11 +270,11 @@ const ACCEPTED_TABLE_FINDINGS = new Map<string, Readonly<Partial<Record<TableRul
['admin-api/bulk-jobs.mdx', {'table-fit': 1}],
['admin-api/discovery.mdx', {'table-identifier': 1}],
['admin-api/guilds.mdx', {'table-identifier': 3}],
['admin-api/index.mdx', {'table-fit': 1, 'table-identifier': 3}],
['admin-api/index.mdx', {'table-identifier': 3}],
['admin-api/instance.mdx', {'table-identifier': 8}],
['admin-api/messages.mdx', {'table-identifier': 1}],
['admin-api/reports.mdx', {'table-fit': 1, 'table-identifier': 2}],
['admin-api/users.mdx', {'table-fit': 1, 'table-identifier': 1}],
['admin-api/users.mdx', {'table-fit': 1, 'table-identifier': 3}],
['admin-api/voice.mdx', {'table-identifier': 3}],
['gateway/event-filtering.md', {'table-cell': 2}],
['gateway/events.md', {'table-identifier': 1}],
@@ -6,49 +6,42 @@ description: The safety blocklists, their value forms, and the operations that m
import RouteHeader from '@/components/RouteHeader.astro';
Fluxer has nine blocklists for account access and user content. Each defines its accepted values, matching rules and metadata. Fluxer normalises a value when it adds the value and again when it checks the value.
Fluxer has eight blocklists for account access and user content. Each defines its accepted values, matching rules and metadata. Fluxer normalises a value when it adds the value and again when it checks the value.
Each list has its own [Admin ACLs](/admin-api/#acl-registry). A read needs the selected list's `check` permission, an addition or an update needs its `add` permission, and a removal needs its `remove` permission. An account holding `ban:ip:add` writes to the `ip` list and to no other. Every operation records the audit reason on the [Admin audit entries](/admin-api/#admin-audit-entry-object) it produces, and each read produces one entry. An entry about one list has a target type built from the list name with each hyphen written as an underscore, so `url-domain` entries use `url_domain`. `email-domain-suspicious` is the one exception and uses `email_domain`.
Each list has its own [Admin ACLs](/admin-api/#acl-registry). A read needs the selected list's `check` permission, an addition or an update needs its `add` permission, and a removal needs its `remove` permission. An account holding `ban:ip:add` writes to the `ip` list and to no other. Every operation records the audit reason on the [Admin audit entries](/admin-api/#admin-audit-entry-object) it produces, and each read produces one entry. An entry about one list has a target type built from the list name with each hyphen written as an underscore, so `url-domain` entries use `url_domain`.
Fluxer builds each permission name from `ban:`, the list name with each hyphen written as an underscore, and the verb, so the `url-domain` list uses `ban:url_domain:check`, `ban:url_domain:add`, and `ban:url_domain:remove`. The `email-domain-suspicious` list is the one exception and uses `suspicious_email_domain:check`, `suspicious_email_domain:add`, and `suspicious_email_domain:remove`.
:::note[The Admin API exposes no other blocklist]
Fluxer synchronises disposable email domains from external feeds every six hours when `FLUXER_BLOCKLIST_FEEDS_ENABLED` is on. No operation on this page reads or writes them.
:::
Fluxer builds each permission name from `ban:`, the list name with each hyphen written as an underscore, and the verb, so the `url-domain` list uses `ban:url_domain:check`, `ban:url_domain:add`, and `ban:url_domain:remove`.
## Blocklist types
`ip`, `email`, and `email-domain-suspicious` gate account access and registration. `phrase`, `url`, `url-domain`, `file-sha`, `avatar-hash`, and `profile-substring` gate what an account may post, link, upload, or display. `email` and `email-domain-suspicious` store nothing but the value. An `ip` row also stores a ban kind, a reason, an expiry, and its creation time.
`ip` and `email` gate account access and registration. `phrase`, `url`, `url-domain`, `file-sha`, `avatar-hash`, and `profile-substring` gate what an account may post, link, upload, or display. `email` stores nothing but the value. An `ip` row also stores a ban kind, a reason, an expiry, and its creation time.
| Value | Description |
| --- | --- |
| ip<sup>1</sup> | IPv4 addresses, IPv6 addresses, and CIDR ranges denied service |
| email<sup>2</sup> | Exact email addresses blocked from registration and from being set on an account |
| email-domain-suspicious<sup>2</sup> <sup>3</sup> | Email domains that allow registration and require the new account to verify a phone number first |
| phrase<sup>4</sup> | Phrases blocked from content |
| url<sup>5</sup> | Absolute `http` and `https` URLs blocked from being posted |
| url-domain<sup>6</sup> | Domains blocked from being linked |
| file-sha<sup>7</sup> | SHA-256 hashes blocked from being uploaded |
| avatar-hash<sup>8</sup> | Avatar hashes blocked from being set |
| profile-substring<sup>4</sup> <sup>9</sup> | Substrings blocked from one named profile field |
| phrase<sup>3</sup> | Phrases blocked from content |
| url<sup>4</sup> | Absolute `http` and `https` URLs blocked from being posted |
| url-domain<sup>5</sup> | Domains blocked from being linked |
| file-sha<sup>6</sup> | SHA-256 hashes blocked from being uploaded |
| avatar-hash<sup>7</sup> | Avatar hashes blocked from being set |
| profile-substring<sup>3</sup> <sup>8</sup> | Substrings blocked from one named profile field |
<sup>1</sup> Fluxer refuses an address with 400 `IP_BAN_DECLINED` when it is on the instance exemption list, or when IP lookup data shows that a single address is on a mobile carrier network, and records both refusals in the Admin audit log
<sup>2</sup> Stored lowercased, so a mixed-case value does not create a second row
<sup>3</sup> Fluxer never shows the list to the account holder, who sees only the verified-phone gate. A domain must match `^[a-zA-Z0-9][a-zA-Z0-9\-.]*\.[a-zA-Z]{2,}$`
<sup>3</sup> Canonicalised by NFKC normalisation, removal of control, format and variation-selector characters, lowercasing, and trimming. When Fluxer matches a value, it also normalises inserted whitespace, punctuation, and compatibility characters
<sup>4</sup> Canonicalised by NFKC normalisation, removal of control, format and variation-selector characters, lowercasing, and trimming. When Fluxer matches a value, it also normalises inserted whitespace, punctuation, and compatibility characters
<sup>4</sup> Canonicalised before storage. A value Fluxer cannot canonicalise returns 400 `INVALID_FORM_BODY` naming `url`
<sup>5</sup> Canonicalised before storage. A value Fluxer cannot canonicalise returns 400 `INVALID_FORM_BODY` naming `url`
<sup>5</sup> Stored lowercased and matched against the lowercased hostname of a submitted URL. `match_subdomains` is stored on the row and defaults to true, and the hostname match is exact whatever its value
<sup>6</sup> Stored lowercased and matched against the lowercased hostname of a submitted URL. `match_subdomains` is stored on the row and defaults to true, and the hostname match is exact whatever its value
<sup>6</sup> Stored as lowercase hexadecimal
<sup>7</sup> Stored as lowercase hexadecimal
<sup>7</sup> Stored as the 8-character hash with any `a_` animation prefix stripped and the rest lowercased, so the animated and static forms of one avatar are the same row
<sup>8</sup> Stored as the 8-character hash with any `a_` animation prefix stripped and the rest lowercased, so the animated and static forms of one avatar are the same row
<sup>9</sup> The only scoped list. Scope and substring together identify a row, so the same substring can be stored once per [scope](#profile-substring-scopes)
<sup>8</sup> The only scoped list. Scope and substring together identify a row, so the same substring can be stored once per [scope](#profile-substring-scopes)
The fields and the operations a list accepts differ from list to list. Read the `fields` array and the `supports_` flags of a [blocklist object](#blocklist-object) before writing to a list.
@@ -109,9 +102,9 @@ One entry of the blocklist catalogue returned by [List blocklists](#list-blockli
| supports_bulk_delete<sup>5</sup> | boolean | Whether [Bulk remove blocklist entries](#bulk-remove-blocklist-entries) is accepted |
| supports_update<sup>6</sup> | boolean | Whether [Update blocklist entry](#update-blocklist-entry) is accepted |
<sup>1</sup> `ip` names `ip`, `email` names `email`, `email-domain-suspicious` and `url-domain` name `domain`, `phrase` names `phrase`, `url` names `url`, `file-sha` names `sha256_hex`, `avatar-hash` names `hashes`, and `profile-substring` names `substrings`
<sup>1</sup> `ip` names `ip`, `email` names `email`, `url-domain` names `domain`, `phrase` names `phrase`, `url` names `url`, `file-sha` names `sha256_hex`, `avatar-hash` names `hashes`, and `profile-substring` names `substrings`
<sup>2</sup> Empty for `ip`, `email`, `email-domain-suspicious`, and `phrase`. `url` names `category`, `severity`, `source_url`, and `notes`. `url-domain` adds `match_subdomains`, `file-sha` adds `content_type`, and `avatar-hash` adds `reason`. `profile-substring` names `scope`, `reason`, and `notes`
<sup>2</sup> Empty for `ip`, `email`, and `phrase`. `url` names `category`, `severity`, `source_url`, and `notes`. `url-domain` adds `match_subdomains`, `file-sha` adds `content_type`, and `avatar-hash` adds `reason`. `profile-substring` names `scope`, `reason`, and `notes`
<sup>3</sup> True only for `profile-substring`
@@ -172,7 +165,7 @@ One stored row of one blocklist. Every field is present on every entry, and a fi
<sup>7</sup> Non-null only on `ip`. An address added through [Add blocklist entry](#add-blocklist-entry) is written as a permanent ban, so it reads back with the reason `platform_admin_enforcement` and a null `expires_at`
<sup>8</sup> Both null on `email`, `email-domain-suspicious`, and `phrase`, which store nothing but the value. `created_by_user_id` is also null on `ip`
<sup>8</sup> Both null on `email` and `phrase`, which store nothing but the value. `created_by_user_id` is also null on `ip`
:::note[The audit entry records the accepted reason]
An `avatar-hash` or `profile-substring` write accepts `reason`, and both lists then report `reason` as null in this object.
@@ -224,7 +217,6 @@ The body of [Add blocklist entry](#add-blocklist-entry) has one shape per blockl
| --- | --- | --- |
| ip | ip | IPv4 address, IPv6 address, or CIDR range of 1 through 45 characters |
| email | email | Email address of 1 through 254 characters |
| email-domain-suspicious | domain | Domain of 1 through 253 characters matching `^[a-zA-Z0-9][a-zA-Z0-9\-.]*\.[a-zA-Z]{2,}$` |
| phrase | phrase | Phrase of 1 through 500 characters |
| url | url | Absolute `http` or `https` URL of 1 through 2048 characters |
| url-domain | domain | Domain of 1 through 253 characters |
@@ -375,13 +367,13 @@ Every write is an upsert on the canonical value. An omitted optional field is wr
A body field the selected blocklist does not accept is stripped and never produces that 400. A `url` Fluxer cannot canonicalise returns 400 `INVALID_FORM_BODY` naming `url` in the `errors` array.
Fluxer refuses to add an `ip` with 400 `IP_BAN_DECLINED` when the address is on the instance exemption list, and when a single address is classified as a high blast-radius mobile or carrier network. Fluxer runs that carrier network check only for a single address, so the check never refuses a CIDR range. When the IP lookup for the check fails, Fluxer treats the address as low risk and writes it.
Fluxer refuses to add an `ip` with 400 `IP_BAN_DECLINED` when the address is on the instance exemption list, and when a single address is classified as a high blast-radius mobile or carrier network. Fluxer runs that carrier network check only for a single address, so the check never refuses a CIDR range. When the IP lookup for the check fails, Fluxer writes the address.
The response has no body, so it does not report the canonical form that was stored. Read it back with [List blocklist entries](#list-blocklist-entries).
### Side effects
Fluxer checks later requests against the written rows. For every list except `email` and `email-domain-suspicious`, other nodes see the rows after a short propagation delay. No Gateway Dispatch is emitted.
Fluxer checks later requests against the written rows. For every list except `email`, other nodes see the rows after a short propagation delay. No Gateway Dispatch is emitted.
Fluxer records one [Admin audit entry](/admin-api/#admin-audit-entry-object) per written value, with that value in its metadata. It records an entry for a refused `ip` too, under the action `ban_ip_skipped_exempt` or `ban_ip_skipped_cgnat`, before it returns the 400.
@@ -518,9 +510,8 @@ Reports whether one value is currently blocked by the selected blocklist and ret
| --- | --- |
| ip<sup>3</sup> <sup>4</sup> | The address itself, any stored CIDR range containing it, and any stored address the instance treats as the same origin |
| email | Exact match on the lowercased address |
| email-domain-suspicious<sup>5</sup> | Exact match on the lowercased domain |
| phrase | Normalised phrase matching, so a disguised form of a stored phrase still reads as blocked |
| url<sup>6</sup> | Exact match on the canonicalised URL |
| url<sup>5</sup> | Exact match on the canonicalised URL |
| url-domain | Exact match on the lowercased hostname |
| file-sha | Exact match on the lowercased hexadecimal digest |
| avatar-hash | Exact match after the `a_` prefix is stripped and the hash is lowercased |
@@ -530,9 +521,7 @@ Reports whether one value is currently blocked by the selected blocklist and ret
<sup>4</sup> Two addresses are the same origin when they share a decision key, which is the exact address for IPv4 and the `/64` prefix for IPv6
<sup>5</sup> A domain that the instance account policy marks as exempt from reputation checks reads as not blocked even while a row exists
<sup>6</sup> This check does not read the `url-domain` list, so a URL that a stored domain blocks reads as not blocked. Check the hostname separately
<sup>5</sup> This check does not read the `url-domain` list, so a URL that a stored domain blocks reads as not blocked. Check the hostname separately
### Response
@@ -543,7 +532,7 @@ Reports whether one value is currently blocked by the selected blocklist and ret
A value with no row and no covering match returns 200 with `banned` false, so this operation never reports whether a specific row exists. Use [List blocklist entries](#list-blocklist-entries) for that.
For every list except `email` and `email-domain-suspicious`, a row written on another node becomes visible here after a short propagation delay.
For every list except `email`, a row written on another node becomes visible here after a short propagation delay.
### Side effects
@@ -658,7 +647,7 @@ Removal is idempotent. A value with no stored row returns 204 and still records
### Side effects
The removed row stops affecting subsequent blocklist decisions. For every list except `email` and `email-domain-suspicious`, other nodes stop applying it after a short propagation delay. A value can remain blocked by another matching row, such as a single address covered by a stored CIDR range. No Gateway Dispatch is emitted.
The removed row stops affecting subsequent blocklist decisions. For every list except `email`, other nodes stop applying it after a short propagation delay. A value can remain blocked by another matching row, such as a single address covered by a stored CIDR range. No Gateway Dispatch is emitted.
The removal records one [Admin audit entry](/admin-api/#admin-audit-entry-object), with the canonical value in its metadata.
@@ -154,11 +154,11 @@ Every task writes one summary Admin audit entry when it finishes, with the actio
`update_user_flags` writes one `update_flags` entry for each account and dispatches [User Update](/gateway/events/#user-update) to the account's sessions. A change to a publicly visible flag also dispatches [Guild Member Update](/gateway/events/#guild-member-update) to every guild the account is in.
`update_suspicious_activity_flags` rewrites each account's verification requirements and dispatches [User Update](/gateway/events/#user-update). No [Guild Member Update](/gateway/events/#guild-member-update) follows. The task writes one `update_suspicious_activity_flags` entry for each account, with the audit reason, and records a risk outcome when the requirements become non-empty. An unknown flag name fails the job before any account is changed.
`update_suspicious_activity_flags` rewrites each account's verification requirements and dispatches [User Update](/gateway/events/#user-update). No [Guild Member Update](/gateway/events/#guild-member-update) follows. The task writes one `update_suspicious_activity_flags` entry for each account, with the audit reason. An unknown flag name fails the job before any account is changed.
`update_guild_features` writes one `update_features` entry for each guild, dispatches [Guild Update](/gateway/events/#guild-update), and reindexes the guild for search. Fluxer reconciles a guild that already has a discovery application record against the new feature set, so gaining `DISCOVERABLE` approves the record and losing it marks the record removed. A guild with no discovery record is left alone.
`add_guild_members` bypasses the ban check and the deferred phone verification check that a normal join runs for an account without a verified phone. The task suppresses the join system message, records the join source as an Admin force add, and dispatches [Guild Member Add](/gateway/events/#guild-member-add) to the guild and [Guild Create](/gateway/events/#guild-create) to the added account's sessions. The task still enforces the per-account guild cap and the guild member cap, so an account at either ceiling is counted as failed. An account that is already a member is left unchanged and counted as successful, with no second membership and no Dispatch. Adding a bot account also records a `BOT_ADD` guild audit log entry attributed to the acting Admin.
`add_guild_members` bypasses the ban check that a normal join runs. The task suppresses the join system message, records the join source as an Admin force add, and dispatches [Guild Member Add](/gateway/events/#guild-member-add) to the guild and [Guild Create](/gateway/events/#guild-create) to the added account's sessions. The task still enforces the per-account guild cap and the guild member cap, so an account at either ceiling is counted as failed. An account that is already a member is left unchanged and counted as successful, with no second membership and no Dispatch. Adding a bot account also records a `BOT_ADD` guild audit log entry attributed to the acting Admin.
`delete_user_messages` deletes every message each account wrote, across every channel. It writes one `delete_all_user_messages` entry for each account, with the audit reason, the account, and the deleted message count, and reports progress after every account.
@@ -527,7 +527,7 @@ Fluxer checks only the Admin ACL. The acting account needs no membership and no
### Side effects
Fluxer creates the membership with the Admin force-add join source and restores a communication timeout still in force from a previous membership. The guild ban list is not checked, so a banned user can be admitted. The [deferred phone gate](/admin-api/instance/#deferred-phone-gate-object) does not run. The per-user guild limit and the guild member limit are still enforced.
Fluxer creates the membership with the Admin force-add join source and restores a communication timeout still in force from a previous membership. The guild ban list is not checked, so a banned user can be admitted. The per-user guild limit and the guild member limit are still enforced.
[Guild Member Add](/gateway/events/#guild-member-add) fires to the guild, and the user's sessions are joined to the guild on the main Gateway. When the guild's members are already indexed for search, Fluxer adds the new member to guild member search. The ordinary join system message is created, and with it a [Message Create](/gateway/events/#message-create) Dispatch, unless the guild sets `SUPPRESS_JOIN_NOTIFICATIONS` or has no `system_channel_id`, or its `system_channel_id` names a channel that no longer exists. A bot target also records a `BOT_ADD` entry in the guild's own [audit log](/http-api/guild-audit-logs/#audit-actions).
@@ -228,9 +228,8 @@ An entry has no request method, request path, status, duration, IP address, or f
| system<sup>2</sup> | API process that served the request |
| voice_region<sup>2</sup> | Voice region |
| voice_server<sup>2</sup> | Voice server registration |
| ip<sup>2</sup> | IP blocklist entry or suspicious IP marker |
| ip<sup>2</sup> | IP blocklist entry |
| email<sup>2</sup> | Exact email blocklist entry |
| email_domain<sup>2</sup> | Suspicious email domain entry |
| phrase<sup>2</sup> | Phrase blocklist entry |
| url<sup>2</sup> | Exact URL blocklist entry |
| url_domain<sup>2</sup> | URL domain blocklist entry |
@@ -312,7 +311,6 @@ An entry with any other action has `access` set to `write`.
| Value | Description |
| --- | --- |
| NCMEC Report | An attachment was submitted through the NCMEC reporting integration |
| add_suspicious_email_domain | A domain was added to the suspicious email domain list |
| approve_discovery_application | A discovery application was approved |
| approve_registration | A pending registration was approved |
| auto_resolve_reports_on_deletion | Reports against an account were resolved automatically as part of its deletion |
@@ -357,11 +355,6 @@ An entry with any other action has `access` set to `write`.
| force_add_to_guild | An account was added to a guild while bypassing the ban check |
| generate_gift_codes | Gift codes were generated |
| kick_member | A member was removed from a guild |
| mark_suspicious_ip | An IP address was marked as suspicious |
| mark_suspicious_ip_skipped_high_blast_radius | IP intelligence reports the address as a mobile, anycast, satellite, or education network, so no marker was written |
| mark_suspicious_ip_skipped_invalid | The address could not be parsed, so no marker was written |
| mark_suspicious_ip_skipped_ipinfo_unavailable | IP intelligence was unavailable, so no marker was written |
| mark_suspicious_ip_skipped_trusted_commercial_privacy_provider | The address belongs to a trusted commercial privacy provider, so no marker was written |
| purge_asset | A stored media asset was purged |
| purge_guild_emoji_asset | A guild emoji asset was purged |
| purge_guild_sticker_asset | A guild sticker asset was purged |
@@ -375,7 +368,6 @@ An entry with any other action has `access` set to `write`.
| remove_discovery_listing | A guild was removed from discovery |
| remove_relationship | One relationship was removed from an account |
| remove_relationships_by_category | Every relationship in one category was removed from an account |
| remove_suspicious_email_domain | A domain was removed from the suspicious email domain list |
| resend_verification_email | An email verification message was requested again for an account |
| resolve_report | A report was updated or resolved |
| revoke_admin_api_key | An Admin API key was revoked |
@@ -530,9 +522,6 @@ The registry is returned in this order by [List ACLs](#list-acls). A value outsi
| ban:email:add | Adds and updates `email` [blocklist](/admin-api/blocklists/) entries |
| ban:email:check | Reads the `email` blocklist |
| ban:email:remove | Removes `email` blocklist entries |
| suspicious_email_domain:add | Adds and updates `email-domain-suspicious` blocklist entries |
| suspicious_email_domain:check | Reads the `email-domain-suspicious` blocklist |
| suspicious_email_domain:remove | Removes `email-domain-suspicious` blocklist entries |
| ban:phrase:add | Adds and updates `phrase` blocklist entries |
| ban:phrase:check | Reads the `phrase` blocklist |
| ban:phrase:remove | Removes `phrase` blocklist entries |
@@ -338,7 +338,6 @@ Community, direct message, premium and gating policy for the whole deployment.
| services | object | Operator overrides for `gif_enabled`, `youtube_enabled`, and `bluesky_enabled`. Each is nullable, and null means no override |
| services_resolved<sup>2</sup> | object | The same keys as concrete booleans, resolved from the override and the provider's own availability |
| services_available<sup>3</sup> | object | Provider availability for `gif`, `youtube`, and `bluesky`, with no operator override applied |
| deferred_phone_gate | [deferred phone gate](#deferred-phone-gate-object) object | Delayed phone verification policy |
<sup>1</sup> The lock is set each time direct messages are re-enabled. [Update instance configuration](#update-instance-configuration) clears it when the policy object has `direct_messages_locked` set to false
@@ -355,18 +354,6 @@ Community, direct message, premium and gating policy for the whole deployment.
A self-hosted deployment issues gift codes and sells premium only in `mirror`.
## Deferred phone gate object
A rule for accounts whose phone verification requirement was deferred. When such an account joins a guild within `window_hours` of registration, and the guild has the `DISCOVERABLE` feature or more than `member_threshold` members, Fluxer refuses the join until the account verifies a phone.
### Structure
| Field | Type | Description |
| --- | --- | --- |
| enabled | boolean | Whether the delayed phone requirement is applied (default false) |
| window_hours | number | Hours after registration in which the requirement can be applied (default 6) |
| member_threshold | number | Guild member count above which the requirement is applied (default 50) |
## Instance integrations object
Every provider reports its stored settings, a `_set` boolean in place of each secret, and the availability resolved from the stored settings and the deployment configuration.
@@ -701,14 +688,11 @@ Fluxer skips URL validation while the merged configuration leaves single sign-on
| direct_messages_locked?<sup>2</sup> | boolean | Whether the direct message setting stays locked |
| premium_mode? | string | [Premium mode](#premium-modes) |
| services? | object | Nullable `gif_enabled`, `youtube_enabled`, and `bluesky_enabled` overrides |
| deferred_phone_gate?<sup>3</sup> | object | `enabled`, `window_hours`, and `member_threshold` |
<sup>1</sup> Setting `single_community_enabled` to true adopts the already designated guild when one still exists. When none is designated or the designated guild was deleted, it creates a community using `single_community_name` or the configured product name. When the stored guild ID is not a valid ID, or the guild lookup fails for a reason other than an unknown guild, the operation fails and Fluxer creates no community. On a deployment whose setup is already complete, enabling it while no guild is designated fails with 400 `INSTANCE_POLICY_TRANSITION_NOT_ALLOWED`, as does enabling it when the acting Admin account cannot be resolved. Setting it to false only clears the flag and leaves the guild in place
<sup>2</sup> The setting can be changed only while `direct_messages_locked` is false, and a change attempted after the lock is set fails with 400 `INSTANCE_POLICY_TRANSITION_NOT_ALLOWED` unless the same request sets `direct_messages_locked` to false. Re-enabling direct messages sets the lock again
<sup>3</sup> `window_hours` is a positive number up to 8760 and `member_threshold` is a positive integer up to 1000000. An omitted key keeps its stored value
`direct_messages_locked` accepts only false, and a body that sets it to true fails with 400 `INVALID_FORM_BODY`.
On a self-hosted deployment, billing and the `everyone` premium mode exclude each other. A body that would leave billing switched on in `everyone` fails with 400 `INVALID_FORM_BODY` and writes nothing, whether it enables billing or switches the premium mode.
@@ -11,7 +11,7 @@ These routes list recorded background jobs and request cancellation. Create jobs
Reads require `jobs:view` and cancellation requires `jobs:cancel`. Every operation on this page shares the `admin:jobs:view` bucket. The three reads record no [Admin audit entry](/admin-api/#admin-audit-entry-object), and cancellation records one.
:::note[Job updates vary by task]
Mention processing, link previews, message delete audit log batching, payment reconciliation when a user connects, and every scheduled task except `syncDisposableEmailDomains`, `syncUrlBlocklists` and `syncFileShaBlocklists` run with no job record and never appear here. Fluxer deletes a job record about 90 days after the job was queued. A job also runs with no record when Fluxer fails to write that record. When a later write of status, progress or attempts fails, Fluxer logs the failure and the job continues, so the stored values can be behind the real run. Use the [archive routes](/admin-api/archives/) to track archive progress and failures.
Mention processing, link previews, message delete audit log batching, payment reconciliation when a user connects, and every scheduled task except `syncUrlBlocklists` and `syncFileShaBlocklists` run with no job record and never appear here. Fluxer deletes a job record about 90 days after the job was queued. A job also runs with no record when Fluxer fails to write that record. When a later write of status, progress or attempts fails, Fluxer logs the failure and the job continues, so the stored values can be behind the real run. Use the [archive routes](/admin-api/archives/) to track archive progress and failures.
:::
## Admin job object
@@ -261,7 +261,7 @@ Returns false for a missing or terminal job. Repeating a request for a queued or
### Side effects
Sets `cancel_requested` to true for a queued or running job. `sendSystemDm`, `syncDisposableEmailDomains`, `bulkBanFileShas`, `bulkDeleteMessagesForUsers`, and the tasks queued by [Bulk jobs](/admin-api/bulk-jobs/) check for the request while they run and stop at the next check. Other tasks run to the end. Completed work is not undone. A status of `cancelled` means the task stopped because of the request.
Sets `cancel_requested` to true for a queued or running job. `sendSystemDm`, `bulkBanFileShas`, `bulkDeleteMessagesForUsers`, and the tasks queued by [Bulk jobs](/admin-api/bulk-jobs/) check for the request while they run and stop at the next check. Other tasks run to the end. Completed work is not undone. A status of `cancelled` means the task stopped because of the request.
The operation records one [Admin audit entry](/admin-api/#admin-audit-entry-object) with action `cancel_job`, target type `bulk_job`, target ID equal to `job_id`, and metadata key `cancelled`. The entry uses target type `bulk_job` for every task type, and Fluxer records it when `cancelled` is false too.
@@ -51,12 +51,15 @@ When the caller lacks `user:view:email`, `user:view:dob`, or `user:view:ip`, Flu
| premium_grace_ends_at | ?ISO8601 timestamp | The time the payment grace period ends, or null when no grace period is running |
| premium_lifetime_sequence | ?integer | The sequence number of the lifetime purchase, or null when the account holds none |
| suspicious_activity_flags | integer | [Suspicious activity flags](#suspicious-activity-flags) |
| phone_verification_deferred<sup>5</sup> | boolean | Whether a stored phone requirement is deferred until the account joins a discoverable or large community |
| phone_verification_deferred<sup>5</sup> | boolean | Whether a stored phone requirement is deferred and not enforced |
| temp_banned_until<sup>6</sup> | ?ISO8601 timestamp | The time the ban expires, or null when no ban stands |
| pending_deletion_at | ?ISO8601 timestamp | The time the scheduled deletion runs, or null when none is scheduled |
| pending_bulk_message_deletion_at<sup>7</sup> | ?ISO8601 timestamp | The time the account's own scheduled bulk message deletion runs |
| deletion_reason_code | ?integer | [Deletion reason](#deletion-reasons) |
| deletion_public_reason | ?string | The reason shown to the account holder, or null when none was supplied |
| deletion_audit_log_reason | ?string | The private reason stored with the pending deletion, and null without `audit_log:view` |
| deletion_scheduled_by | ?snowflake | The ID of the account that scheduled the pending deletion, or null when it was not recorded |
| deletion_scheduled_at | ?ISO8601 timestamp | The time the pending deletion was scheduled, or null when it was not recorded |
| acls<sup>8</sup> | array[string] | Effective [Admin ACLs](/admin-api/#acl-registry), with at most 111 entries |
| traits<sup>9</sup> | array[string] | The free-form operator labels set on the account, with at most 100 entries |
| has_totp<sup>10</sup> | boolean | Whether a TOTP authenticator is registered |
@@ -151,7 +154,7 @@ The `flags` field of the [Admin user object](#admin-user-object) is a 64-bit bit
| 1 &lt;&lt; 53 | APP_STORE_REVIEWER | Account belongs to an app store reviewer |
| 1 &lt;&lt; 57 | STAFF_HIDDEN | Staff status is hidden from public flags |
| 1 &lt;&lt; 60 | AGE_VERIFIED_ADULT | Account has verified its age as an adult through card verification |
| 1 &lt;&lt; 61 | FORCE_INBOUND_PHONE_VERIFICATION | Account is forced through inbound phone verification regardless of prefix |
| 1 &lt;&lt; 61 | FORCE_INBOUND_PHONE_VERIFICATION | Account is forced through inbound phone verification |
| 1 &lt;&lt; 62 | NOT_SUSPICIOUS | Account is permanently exempt from automatic suspicious activity flagging |
## Premium flags
@@ -198,7 +201,7 @@ A 32-bit bitfield of verification requirements applied to an account. [Update su
<sup>3</sup> Deferrable alongside `REQUIRE_VERIFIED_PHONE`. [Update user phone verification](#update-user-phone-verification) leaves it set
Bit `1 << 16` sits outside this registry. It defers a phone requirement until the account joins a discoverable or large community, and the [Admin user object](#admin-user-object) reports it as `phone_verification_deferred`. [Update suspicious activity flags](#update-suspicious-activity-flags) and [Disable user for suspicious activity](#disable-user-for-suspicious-activity) bound `flags` only as a non-negative 32-bit integer, and neither masks the submitted value, so a request with that bit sets it directly.
Bit `1 << 16` sits outside this registry. It defers a phone requirement so it is stored but not enforced, and the [Admin user object](#admin-user-object) reports it as `phone_verification_deferred`. [Update suspicious activity flags](#update-suspicious-activity-flags) and [Disable user for suspicious activity](#disable-user-for-suspicious-activity) bound `flags` only as a non-negative 32-bit integer, and neither masks the submitted value, so a request with that bit sets it directly.
## Deletion reasons
@@ -1258,13 +1261,13 @@ The operation sets verification requirements without disabling the account. [Dis
| 200 | response body | Flags were replaced |
| 404 | [error response](/admin-api/#error-response) | `UNKNOWN_USER`, because the account does not exist |
The stored deferral bit `1 << 16` is preserved only when it was already set and the submitted value sets the same `REQUIRE_VERIFIED_PHONE` and `REQUIRE_REVERIFIED_PHONE` bits as the stored value, with at least one of them set. Any other submitted value clears the deferral bit, so any phone requirement in the submitted value stops waiting for a guild join.
The stored deferral bit `1 << 16` is preserved only when it was already set and the submitted value sets the same `REQUIRE_VERIFIED_PHONE` and `REQUIRE_REVERIFIED_PHONE` bits as the stored value, with at least one of them set. Any other submitted value clears the deferral bit, so any phone requirement in the submitted value is enforced immediately.
[Bulk jobs](/admin-api/bulk-jobs/) applies the same change to up to 1,000 accounts as a queued `update_suspicious_activity_flags` task.
### Side effects
[User Update](/gateway/events/#user-update) is emitted to the account's own sessions. Fluxer records a `challenged` risk outcome against the account when the write changes the set and leaves at least one registry flag set.
[User Update](/gateway/events/#user-update) is emitted to the account's own sessions.
The operation records one [Admin audit entry](/admin-api/#admin-audit-entry-object) with action `update_suspicious_activity_flags`, target type `user`, and a metadata key `flags` with the submitted value. A preserved deferral bit makes that value differ from the flags the account ends up with.
@@ -1315,7 +1318,7 @@ Fluxer adds the `DISABLED_SUSPICIOUS_ACTIVITY` [account flag](#account-flags), s
The account is marked with `DISABLED_SUSPICIOUS_ACTIVITY`, its suspicious activity flags are replaced, and its password hash is set to null. Every authentication session is then deleted, so the account is signed out on every device.
Fluxer records a `disabled_suspicious` risk outcome, together with a `challenged` outcome when the submitted `flags` is non-zero. Fluxer emails the account holder when the account has an email address.
Fluxer emails the account holder when the account has an email address.
[User Update](/gateway/events/#user-update) is emitted after the sessions have already been deleted, so no connection of the account remains to receive it. The operation records one [Admin audit entry](/admin-api/#admin-audit-entry-object) with action `disable_suspicious_activity`, target type `user`, and a metadata key `flags`.
@@ -1426,11 +1429,49 @@ The operation records one [Admin audit entry](/admin-api/#admin-audit-entry-obje
100 requests per minute for each authenticated user, on the `admin:user:modify` bucket.
## Add a note to a user ban
<RouteHeader method="POST" path="/v1/admin/users/{user_id}/ban/notes" />
Appends a note to the ban that currently stands on the account. Requires `user:temp_ban`. The ban entry and earlier notes are never changed.
### Path parameters
| Field | Type | Description |
| --- | --- | --- |
| user_id | snowflake | The ID of the target account |
### JSON body
| Field | Type | Description |
| --- | --- | --- |
| ban_audit_log_id<sup>1</sup> | snowflake | The ID of the `temp_ban` audit entry of the current ban |
| note | string | The note (1-512 characters) |
<sup>1</sup> The entry must target this account, and its `banned_until` metadata must equal the current `temp_banned_until`
### Response
| Status | Body | Condition |
| --- | --- | --- |
| 204 | none | The note was recorded |
| 400 | [error response](/admin-api/#error-response) | `INVALID_FORM_BODY`, because `ban_audit_log_id` does not name a ban of this account |
| 404 | [error response](/admin-api/#error-response) | `UNKNOWN_USER`, because the account does not exist |
| 409 | [error response](/admin-api/#error-response) | `CONFLICT`, because the named ban is no longer the current ban |
### Side effects
The operation records one [Admin audit entry](/admin-api/#admin-audit-entry-object) with action `annotate_ban`, target type `user`, the note as `audit_log_reason`, and a metadata key `ban_audit_log_id`. The `X-Audit-Log-Reason` header is not read.
### Rate limit
100 requests per minute for each authenticated user, on the `admin:user:modify` bucket.
## Schedule user deletion
<RouteHeader method="PUT" path="/v1/admin/users/{user_id}/deletion" auditReason />
Creates or replaces an account deletion schedule and returns the resulting account. Requires `user:delete`. A schedule cannot be changed once erasure starts.
Creates an account deletion schedule and returns the resulting account. Requires `user:delete`. A pending schedule is replaced only when the request names it in `replace_pending_deletion_at`, and a schedule cannot be changed once erasure starts.
Fluxer raises the submitted delay to the minimum for the [deletion reason](#deletion-reasons), so a request for one day under any reason other than `USER_REQUESTED` is stored as 60 days.
@@ -1449,6 +1490,7 @@ The `X-Audit-Log-Reason` value is also stored on the account as the private dele
| reason_code<sup>1</sup> | integer | [Deletion reason](#deletion-reasons) |
| public_reason?<sup>2</sup> | string | Statement of reasons shown to the account holder (at most 512 characters) |
| days_until_deletion?<sup>3</sup> | integer | Requested whole-day delay (1-365, default 60) |
| replace_pending_deletion_at?<sup>4</sup> | ISO8601 timestamp | The `pending_deletion_at` of the schedule this request replaces |
<sup>1</sup> Required, and validated against the [deletion reason](#deletion-reasons) registry. `USER_REQUESTED` selects the 14-day minimum, and every other value selects the 60-day minimum
@@ -1456,6 +1498,8 @@ The `X-Audit-Log-Reason` value is also stored on the account as the private dele
<sup>3</sup> The stored deadline is the request instant plus the greater of this value and the minimum for the reason code. There is no absolute-timestamp field
<sup>4</sup> Required when the account already has a pending deletion, including one the caller scheduled. It must equal the current `pending_deletion_at`
### Response body
| Field | Type | Description |
@@ -1468,21 +1512,21 @@ The `X-Audit-Log-Reason` value is also stored on the account as the private dele
| --- | --- | --- |
| 200 | response body | The deletion schedule was stored |
| 404 | [error response](/admin-api/#error-response) | `UNKNOWN_USER`, because the account does not exist |
| 409 | [error response](/admin-api/#error-response) | `CONFLICT`, because erasure has started or the deletion state changed during the request |
| 409 | [error response](/admin-api/#error-response) | `CONFLICT`, because a pending deletion was not named, erasure has started, or the deletion state changed |
`user:delete` also allows scheduling deletion of the acting Admin or an account with broader permissions.
### Side effects
The account can no longer authenticate, and its existing authentication sessions are deleted. Erasure is scheduled for the resulting deadline.
The account can no longer authenticate, and its existing authentication sessions and OAuth2 access and refresh tokens are deleted. The acting Admin and the time are stored as `deletion_scheduled_by` and `deletion_scheduled_at`. Erasure is scheduled for the resulting deadline.
Fluxer cancels a Stripe subscription on the account without proration and refunds the charge behind its latest invoice as fraudulent. When the cancellation or refund fails, Fluxer logs the failure and still keeps the deletion schedule.
The account holder is emailed the deadline and the supplied `public_reason` when the account has an email address.
For every reason other than `USER_REQUESTED`, Fluxer also blocks the account's email address. It marks the account's last active IP address, its authorised IP addresses, and the IP addresses of its active and terminated sessions as suspicious, and it resolves the pending reports against the account. These enforcement steps are best-effort and can fail without cancelling the deletion schedule.
For every reason other than `USER_REQUESTED`, Fluxer also blocks the account's email address and resolves the pending reports against the account. These enforcement steps are best-effort and can fail without cancelling the deletion schedule.
[User Update](/gateway/events/#user-update) is emitted after the sessions have already been deleted. The operation records one [Admin audit entry](/admin-api/#admin-audit-entry-object) with action `schedule_deletion`, target type `user`, and the metadata keys `days` and `reason_code`. The identifier bans record their own blocklist entries, and when Fluxer resolves at least one report, it records a second entry with action `auto_resolve_reports_on_deletion` and a metadata key `resolved_count`.
[User Update](/gateway/events/#user-update) is emitted after the sessions have already been deleted. The operation records one [Admin audit entry](/admin-api/#admin-audit-entry-object) with action `schedule_deletion`, target type `user`, and the metadata keys `days`, `reason_code`, and `pending_deletion_at`. A replacement adds `replaced_pending_deletion_at`, `replaced_scheduled_by`, `replaced_scheduled_at`, and `replaced_reason_code` for the schedule it replaced. The identifier bans record their own blocklist entries, and when Fluxer resolves at least one report, it records a second entry with action `auto_resolve_reports_on_deletion` and a metadata key `resolved_count`.
### Rate limit
@@ -1492,7 +1536,7 @@ For every reason other than `USER_REQUESTED`, Fluxer also blocks the account's e
<RouteHeader method="DELETE" path="/v1/admin/users/{user_id}/deletion" auditReason />
Cancels a scheduled deletion and returns the resulting account. Requires `user:delete`. Erasure cannot be cancelled once it starts.
Cancels the scheduled deletion named in the request and returns the resulting account. Requires `user:delete`. Erasure cannot be cancelled once it starts.
:::caution[Cancellation does not undo enforcement]
Clearing the deadline permits the account to authenticate again, but it does not restore deleted sessions, reinstate a cancelled subscription, lift the email and IP blocklist entries the schedule wrote, or reopen the reports it resolved.
@@ -1504,6 +1548,15 @@ Clearing the deadline permits the account to authenticate again, but it does not
| --- | --- | --- |
| user_id | snowflake | The ID of the target account |
### JSON body
| Field | Type | Description |
| --- | --- | --- |
| expected_pending_deletion_at<sup>1</sup> | ISO8601 timestamp | The `pending_deletion_at` of the deletion being cancelled |
| notify_user? | boolean | Whether to email the account holder (default false) |
<sup>1</sup> Required. It must equal the current `pending_deletion_at`, so a cancel written against one schedule never clears a different one
### Response body
| Field | Type | Description |
@@ -1514,17 +1567,18 @@ Clearing the deadline permits the account to authenticate again, but it does not
| Status | Body | Condition |
| --- | --- | --- |
| 200 | response body | The deletion was cancelled, or no deletion was scheduled |
| 200 | response body | The deletion was cancelled |
| 400 | [error response](/admin-api/#error-response) | `NO_PENDING_DELETION`, because no deletion is scheduled |
| 404 | [error response](/admin-api/#error-response) | `UNKNOWN_USER`, because the account does not exist |
| 409 | [error response](/admin-api/#error-response) | `CONFLICT`, because erasure has started or the deletion state changed during the request |
| 409 | [error response](/admin-api/#error-response) | `CONFLICT`, because a different deletion is pending, erasure has started, or the deletion state changed |
### Side effects
The deletion schedule and reasons are cleared, allowing the account to authenticate again unless another restriction applies.
The deletion schedule, its reasons, and the recorded scheduler are cleared, allowing the account to authenticate again unless another restriction applies.
Fluxer emails the account holder when the account has an email address. The email quotes the `X-Audit-Log-Reason` value verbatim and falls back to the literal text `deletion canceled` when the header is absent or resolves to null.
When `notify_user` is true and the account has an email address, Fluxer emails the account holder. The email never includes the `X-Audit-Log-Reason` value.
[User Update](/gateway/events/#user-update) is emitted to the account's own sessions. The operation records one [Admin audit entry](/admin-api/#admin-audit-entry-object) with action `cancel_deletion`, target type `user`, and no metadata.
[User Update](/gateway/events/#user-update) is emitted to the account's own sessions. The operation records one [Admin audit entry](/admin-api/#admin-audit-entry-object) with action `cancel_deletion`, target type `user`, the metadata keys `cancelled_pending_deletion_at`, `cancelled_scheduled_by`, `cancelled_scheduled_at`, and `cancelled_reason_code` for the cancelled schedule, and `notify_user`.
### Rate limit
@@ -294,7 +294,7 @@ An unclaimed account cannot create an application. An account is unclaimed while
| X-Captcha-Token?<sup>1</sup> | string | The CAPTCHA proof for the request |
| X-Captcha-Type?<sup>2</sup> | string | The CAPTCHA provider to verify against, either `hcaptcha` or `turnstile` |
<sup>1</sup> A missing proof returns 400 `CAPTCHA_REQUIRED` and a rejected proof returns 400 `INVALID_CAPTCHA`. Fluxer skips verification when CAPTCHA is disabled for the instance, when the account has the CAPTCHA exemption flag, or when the instance's account policy grants the caller's email address the CAPTCHA exemption capability, as described by [CAPTCHA handling](/topics/captcha/)
<sup>1</sup> A missing proof returns 400 `CAPTCHA_REQUIRED` and a rejected proof returns 400 `INVALID_CAPTCHA`. Fluxer skips verification when CAPTCHA is disabled for the instance, when the account has the CAPTCHA exemption flag, or when the caller's email address is on an exempt domain, as described by [CAPTCHA handling](/topics/captcha/)
<sup>2</sup> Fluxer verifies against the instance's configured provider when the header is absent
@@ -536,7 +536,7 @@ Registration verifies [CAPTCHA](/topics/captcha/) when CAPTCHA is enabled. It pe
| registration_url_code?<sup>6</sup> | ?string | The administrator-issued registration URL code (1-256 characters) |
| theme? | string | The initial theme preference, one of `dark`, `dark_legacy`, `coal`, `light`, or `system` |
<sup>1</sup> Omitting the email address creates an unclaimed account, which has no recovery path until it is claimed. An address whose domain has no usable DNS records, or whose top-level domain is blocked by account policy, returns the field code `INVALID_EMAIL_ADDRESS`, and an address already in use returns `EMAIL_ALREADY_IN_USE`
<sup>1</sup> Omitting the email address creates an unclaimed account, which has no recovery path until it is claimed. An address whose domain has no usable DNS records, or whose top-level domain the instance blocks, returns the field code `INVALID_EMAIL_ADDRESS`, and an address already in use returns `EMAIL_ALREADY_IN_USE`
<sup>2</sup> Omitting the username derives one from `global_name` when that value produces a permitted username, and otherwise allocates a generated username, in both cases with a server-allocated discriminator
@@ -548,7 +548,7 @@ Registration verifies [CAPTCHA](/topics/captcha/) when CAPTCHA is enabled. It pe
<sup>6</sup> A code supplied on an instance with administrator registration URLs disabled, and a code that does not resolve, both return 400 `REGISTRATION_URL_INVALID`. A valid code overrides the closed registration mode and replaces the instance approval mode with its own approval setting
Fluxer resolves the region from the client IP address, and an age below the minimum for that region returns the field code `MUST_BE_MINIMUM_AGE`. That minimum is 13 years unless account policy sets a different minimum for the region, and the applied minimum appears only in the localised message.
Fluxer resolves the region from the client IP address, and an age below the minimum for that region returns the field code `MUST_BE_MINIMUM_AGE`. That minimum is 13 years unless the region sets a different minimum, and the applied minimum appears only in the localised message.
Registration returns 403 `REGISTRATION_CLOSED` when the instance is closed and no valid registration URL was supplied. A username whose discriminator space is exhausted returns the field code `TOO_MANY_USERS_WITH_THIS_USERNAME`. A username or display name containing a blocked substring returns 403 `CONTENT_BLOCKED`.
@@ -573,9 +573,9 @@ There is no retry key. A repeated request with the same values creates a second
### Side effects
The operation applies the instance's registration, email-domain, breached-password, and regional policies before creating the account, and its risk policy can set suspicious activity flags on the created account. It records the accepted terms and privacy policy, authorises the registering client IP address, and sends an email verification message when the instance sends email. An instance that sends no email marks the address verified at creation instead.
The operation applies the instance's registration, email-domain, breached-password, and regional policies before creating the account. It records the accepted terms and privacy policy, authorises the registering client IP address, and sends an email verification message when the instance sends email. An instance that sends no email marks the address verified at creation instead.
Registration accepts the `invite_code` from the body, or the instance's configured auto-join invite when the body has none. On an instance with single-community mode enabled, the account also joins the community guild. Each join emits [Guild Member Add](/gateway/events/#guild-member-add) to the affected guild's sessions. Registration policy can suppress invite admission.
Registration accepts the `invite_code` from the body, or the instance's configured auto-join invite when the body has none. On an instance with single-community mode enabled, the account also joins the community guild. Each join emits [Guild Member Add](/gateway/events/#guild-member-add) to the affected guild's sessions.
Approval mode registration creates no guild membership and no authentication session, and the account cannot sign in until an administrator approves it. Every other successful registration creates one session and returns its token.
@@ -353,13 +353,11 @@ A pending, rejected, removed, or absent application fails with 400 `DISCOVERY_NO
| 400 | [error response](/http-api/#error-response) | The caller already holds the maximum number of guilds and the request returns `MAX_GUILDS` |
| 400 | [error response](/http-api/#error-response) | The guild is full and the request returns `MAX_GUILD_MEMBERS` |
| 403 | [error response](/http-api/#error-response) | Caller is a bot, presents a bearer credential, or holds a revoked account and the request returns `ACCESS_DENIED` |
| 403<sup>1</sup> | [error response](/http-api/#error-response) | The account has an outstanding required action and the request returns `ACCOUNT_SUSPICIOUS_ACTIVITY` |
| 403 | [error response](/http-api/#error-response) | The account has an outstanding required action and the request returns `ACCOUNT_SUSPICIOUS_ACTIVITY` |
| 403 | [error response](/http-api/#error-response) | The guild has the invites-disabled feature and the request returns `INVITES_DISABLED` |
| 403 | [error response](/http-api/#error-response) | The caller is banned from the guild directly or by address and the request returns `USER_BANNED_FROM_GUILD` or `USER_IP_BANNED_FROM_GUILD` |
| 404 | [error response](/http-api/#error-response) | The approved application names a guild whose record no longer exists and the request returns `UNKNOWN_GUILD` |
<sup>1</sup> An account without a verified phone number can have a phone requirement that Fluxer holds back until the account joins a guild. At join time Fluxer checks that requirement against the target guild. An account that passes the account-wide check can therefore still receive `ACCOUNT_SUSPICIOUS_ACTIVITY` here
:::note[A guild that never existed returns 400, not 404]
A guild with no approved listing returns `DISCOVERY_NOT_DISCOVERABLE`.
:::
@@ -110,7 +110,7 @@ Fluxer answers an unrecognised failure with 500 `INTERNAL_SERVER_ERROR` and a ge
## Client errors as an abuse signal
Repeated invalid requests or credentials can trigger a temporary IP ban. A `4xx` answer to a request with no authenticated user adds to that signal, weighted by status. A 429 weighs 3, a 401 weighs 0.75, a 403 weighs 0.5, and every other 4xx weighs 0.25. One request adds at most one signal, and a request from a private or exempt address adds none. Stop using a rejected credential. Change a rejected request before sending it again, and after a 429 wait `retry_after` before the next attempt.
Repeated invalid requests or credentials can trigger a temporary IP ban. Stop using a rejected credential. Change a rejected request before sending it again, and after a 429 wait `retry_after` before the next attempt.
:::caution[An automatic ban answers every request for 24 hours]
A temporary ban lasts 24 hours by default. Requests from the banned address return 403 `GLOBAL_IP_TEMPORARILY_BANNED`. Use `expires_at` from the response when available.
@@ -134,7 +134,7 @@ One redemption can be in flight for a code across the whole deployment, and a se
| X-Captcha-Token?<sup>1</sup> | string | The proof issued by the CAPTCHA provider |
| X-Captcha-Type?<sup>2</sup> | string | The CAPTCHA provider, either `hcaptcha` or `turnstile` |
<sup>1</sup> A missing proof returns 400 `CAPTCHA_REQUIRED` and a rejected proof returns 400 `INVALID_CAPTCHA`. Verification is skipped when CAPTCHA is disabled, when the account has the exemption flag, or when the account's email address has the `captcha_exempt` account policy capability, as described by [CAPTCHA handling](/topics/captcha/)
<sup>1</sup> A missing proof returns 400 `CAPTCHA_REQUIRED` and a rejected proof returns 400 `INVALID_CAPTCHA`. Verification is skipped when CAPTCHA is disabled, when the account has the exemption flag, or when the account's email address is on an exempt domain, as described by [CAPTCHA handling](/topics/captcha/)
<sup>2</sup> Any other value, including an omitted header, falls back to the instance's configured provider
@@ -220,7 +220,6 @@ Accepting an exhausted invite returns 404 `UNKNOWN_INVITE` and makes the code st
| 403 | [error response](/http-api/#error-response) | Instant invites are temporarily disabled for the guild and the request returns `FEATURE_TEMPORARILY_DISABLED` |
| 403 | [error response](/http-api/#error-response) | The caller is banned by account or email address and the request returns `USER_BANNED_FROM_GUILD` |
| 403 | [error response](/http-api/#error-response) | The caller is banned by network address and the request returns `USER_IP_BANNED_FROM_GUILD` |
| 403 | [error response](/http-api/#error-response) | A deferred phone verification requirement becomes due and the request returns `ACCOUNT_SUSPICIOUS_ACTIVITY` |
| 404 | [error response](/http-api/#error-response) | The named guild no longer exists and the request returns `UNKNOWN_GUILD` |
| 404 | [error response](/http-api/#error-response) | The target channel no longer exists and the request returns `UNKNOWN_CHANNEL` |
| 404 | [error response](/http-api/#error-response) | The code resolves to no record, the record is exhausted, a guild invite names no guild, or a group direct message invite names no channel or a channel that no longer exists, each returning `UNKNOWN_INVITE` |
@@ -835,7 +835,7 @@ The route requires a lifetime entitlement, so an account whose premium type is n
A join emits [Guild Create](/gateway/events/#guild-create), [Guild Member Add](/gateway/events/#guild-member-add) and, when the guild has a system channel and does not suppress join notifications, [Message Create](/gateway/events/#message-create). The role grant that follows emits [Guild Member Update](/gateway/events/#guild-member-update). An account that already belongs to the guild produces that Dispatch alone.
The operation is idempotent. For an account that already belongs to the guild, Fluxer grants the Visionary role again and changes nothing else. The join bypasses the guild ban check and the join risk gate, so a banned account is added anyway.
The operation is idempotent. For an account that already belongs to the guild, Fluxer grants the Visionary role again and changes nothing else. The join bypasses the guild ban check, so a banned account is added anyway.
Both size ceilings still apply. An account already at its maximum number of guilds receives 400 `MAX_GUILDS`, and a Visionary guild at its member ceiling receives 400 `MAX_GUILD_MEMBERS`. Neither check runs for an account that already belongs to the guild.
@@ -179,7 +179,7 @@ The private representation of the current account, returned by [Get current user
<sup>15</sup> The value drives the 24-hour cooldown on changing the default voice activity sharing state
<sup>16</sup> The array is empty when the account has no stored suspicious activity flag, when it has no email address, or when the account policy exempts its email address from required actions
<sup>16</sup> The array is empty when the account has no stored suspicious activity flag, when it has no email address, or when its email address is on an exempt domain
<sup>17</sup> A bot account is always permitted, and a user account is permitted only when its recorded date of birth places it at 18 years or older
@@ -449,7 +449,7 @@ This operation replaces the email address only. Nothing on the path revokes a cr
### Side effects
Fluxer replaces the account email, marks it verified, and removes every email-clearable suspicious activity flag, which can empty `required_actions` and restore ordinary access. The new address is also checked against the instance contact policy, and that check can add suspicious activity flags. The bounced marker is not cleared, so an account that has it recovers through [bounced email recovery](#bounced-email-recovery).
Fluxer replaces the account email, marks it verified, and removes every email-clearable suspicious activity flag, which can empty `required_actions` and restore ordinary access. The bounced marker is not cleared, so an account that has it recovers through [bounced email recovery](#bounced-email-recovery).
For an ordinary change from an existing address, Fluxer sends a revert email to the original address so its holder can use [Revert an email change](/http-api/authentication/#revert-an-email-change). Fluxer deletes the email token, records the contact change in the account's contact change log, and emits [User Update](/gateway/events/#user-update). No public member field changes, so no [Guild Member Update](/gateway/events/#guild-member-update) follows.
@@ -29,25 +29,20 @@ An account can qualify even when its [required actions](/http-api/users/#require
## Deferred phone requirement
At registration, an instance can defer a new account's phone requirement. The [deferred phone gate](/admin-api/instance/#deferred-phone-gate-object) policy sets the window after registration in which a qualifying join makes the requirement due, and the member count above which a guild is a qualifying one. A guild with the `DISCOVERABLE` [feature](/http-api/guilds/#guild-features) qualifies at any member count.
An account can hold a phone requirement together with a marker that defers it. A deferred requirement is absent from the [required actions](/http-api/users/#required-actions) array and restricts nothing. An account whose deferred requirement was later made due owes `REQUIRE_VERIFIED_PHONE` or `REQUIRE_REVERIFIED_PHONE`, and every ordinary user route refuses it with 403 `ACCOUNT_SUSPICIOUS_ACTIVITY`.
A deferred requirement is absent from the [required actions](/http-api/users/#required-actions) array and restricts nothing. An attempt to join a qualifying guild inside the window makes it due, and [Accept invite](/http-api/invites/#accept-invite) refuses that attempt with 403 `ACCOUNT_SUSPICIOUS_ACTIVITY`. The account then owes `REQUIRE_VERIFIED_PHONE` or `REQUIRE_REVERIFIED_PHONE`, and every ordinary user route refuses it with the same code.
[Get phone gate escape preview](#get-phone-gate-escape-preview) and [Run phone gate escape](#run-phone-gate-escape) set a due requirement aside again without verifying a number and without leaving any guild. Both admit an account the rest of the API refuses. The escape is available only while all of these hold.
[Get phone gate escape preview](#get-phone-gate-escape-preview) and [Run phone gate escape](#run-phone-gate-escape) set that requirement aside again without verifying a number. Both admit an account the rest of the API refuses. The escape is available only while all of these hold.
- The instance has the deferred phone gate enabled.
- The account holds no verified phone.
- The account owes `REQUIRE_VERIFIED_PHONE` or `REQUIRE_REVERIFIED_PHONE`.
- The account does not owe `REQUIRE_INBOUND_PHONE_VERIFICATION`.
- The requirement became due through a qualifying join and has not been deferred again.
- The requirement was deferred before it became due, and it has not been deferred again.
- The account has at least one visible required action.
## Number representation
The `phone` field is a number already in canonical E.164 form. Fluxer strips control and formatting characters, trims the value, and then matches it against `^\+[1-9]\d{1,14}$`. A value that fails returns 400 `INVALID_FORM_BODY` with one [validation error](/http-api/#validation-error-object) entry whose `path` is `phone` and whose `code` is `PHONE_NUMBER_INVALID_FORMAT`.
A number whose leading characters appear on the instance's banned prefix list is refused with 400 `INVALID_PHONE_NUMBER`. The list holds a built-in set of country prefixes plus operator additions, and one entry blocks every number that starts with it.
:::note[Only the verified state is exposed]
A successful verification sets a boolean on the account. The `phone` field of the [user object](/http-api/users/#user-object) is always null, and no route returns the submitted number.
:::
@@ -67,48 +62,15 @@ Keep phone numbers, verification codes, and challenge codes out of logs, analyti
<sup>2</sup> Requesting this value always yields an inbound challenge, and neither the lookup provider nor the outbound SMS provider is contacted
## Provider lookup verdicts
## Number checks
Outbound SMS requires the number to pass the following checks. Fluxer evaluates the rows in order and uses the verdict of the first row whose condition matches. A number routed directly to an inbound challenge does not need this lookup.
| Order | Condition | Verdict |
| --- | --- | --- |
| 1 | The lookup could not be done<sup>1</sup> | Refused with `INVALID_PHONE_NUMBER` |
| 2 | The provider reports the number as invalid | Refused with `INVALID_PHONE_NUMBER` |
| 3 | The line type is `fixedVoip` or `nonFixedVoip` | Inbound, reason `voip` |
| 4 | The number starts with `+1` and its numbering plan area is Canadian | Inbound, reason `canadian` |
| 5 | The line type is absent or `unknown` | Inbound, reason `unknown_line_type` |
| 6 | The line type is `landline`, `tollFree`, `premium`, `sharedCost`, `uan`, `voicemail`, or `pager` | Refused with `INVALID_PHONE_NUMBER` |
| 7 | The line type is anything other than `mobile` or `personal`<sup>2</sup> | Refused with `INVALID_PHONE_NUMBER` |
| 8 | The SMS pumping risk score reaches the threshold for the reported country<sup>3</sup> | Refused with `INVALID_PHONE_NUMBER` |
| 9 | No rule above matched | Accepted for outbound SMS |
<sup>1</sup> An instance whose lookup provider is unavailable or unconfigured refuses every number
<sup>2</sup> `mobile` and `personal` are the only accepted line types
<sup>3</sup> The threshold is 100 for `US` and `CA`, 70 for `GB`, `DE`, `FR`, `IT`, `ES`, `NL`, `SE`, `NO`, `DK`, `FI`, `AU`, `NZ`, `JP`, `KR`, `CH`, `AT`, `BE`, `IE`, and `PT`, and 35 for every other country and for a lookup reporting no country
Only [Send phone verification](#send-phone-verification) converts an inbound verdict into a challenge. [Verify phone code](#verify-phone-code) refuses the same verdicts with 400 `INVALID_PHONE_NUMBER`.
Outbound SMS requires the number to pass a line check with the instance's phone provider. A number the provider cannot reach, or one on a line type that cannot receive a code, is refused with 400 `INVALID_PHONE_NUMBER`. Some numbers that pass are routed to an inbound challenge instead. A number the check cannot be run for is refused.
## Inbound challenge reason values
A reason explains why the attempt could not complete over outbound SMS.
| Value | Description |
| --- | --- |
| voip | Provider lookup classified the destination as a VoIP line |
| canadian<sup>1</sup> | The destination is a Canadian numbering plan area |
| unknown_line_type | Provider lookup reported no line type or an unknown line type |
| expensive_destination<sup>2</sup> | The configured inbound-required prefix policy matches the destination |
| account_forced<sup>3</sup> | The account has the flag that forces inbound verification |
| behavioural_risk | Phone attempt risk controls forced this attempt onto the inbound channel |
<sup>1</sup> Determined from the numbering plan area of a `+1` number, and evaluated after the VoIP rule, so a Canadian VoIP number reports `voip` instead
<sup>2</sup> Also reported when the caller explicitly requests the `inbound_challenge` channel. The prefix policy itself applies only while the account holds no verified phone
<sup>3</sup> Reported in preference to `expensive_destination` when both would apply
| verification_required | The instance requires inbound verification for this attempt |
## SMS delivery object
@@ -132,7 +94,7 @@ The instructions for texting a code to Fluxer, returned by [Send phone verificat
| challenge_code<sup>1</sup> | string | The code the user texts to `our_number` |
| our_number | string | The Fluxer E.164 number that receives the code |
| expires_at<sup>2</sup> | ISO8601 timestamp | The moment the challenge stops being redeemable |
| reason | string | The [inbound challenge reason](#inbound-challenge-reason-values) that forced this channel |
| reason | string | The [inbound challenge reason](#inbound-challenge-reason-values), always `verification_required` |
<sup>1</sup> Six decimal digits
@@ -146,7 +108,7 @@ The instructions for texting a code to Fluxer, returned by [Send phone verificat
"challenge_code": "418207",
"our_number": "+15550000000",
"expires_at": "2026-03-04T18:15:00.000Z",
"reason": "voip"
"reason": "verification_required"
}
```
@@ -196,8 +158,8 @@ What [Run phone gate escape](#run-phone-gate-escape) would do for the current ac
| Field | Type | Description |
| --- | --- | --- |
| available | boolean | Whether the escape is open to this account right now |
| guilds | array[[phone gate escape guild](#phone-gate-escape-guild-object) object] | The qualifying guilds the escape leaves, empty while `available` is false |
| owned_guilds | array[[phone gate escape guild](#phone-gate-escape-guild-object) object] | The qualifying guilds the account owns, which the escape keeps |
| guilds | array[[phone gate escape guild](#phone-gate-escape-guild-object) object] | Always empty, the escape leaves no guild |
| owned_guilds | array[[phone gate escape guild](#phone-gate-escape-guild-object) object] | Always empty, the escape leaves no guild |
## Send phone verification
@@ -205,17 +167,9 @@ What [Run phone gate escape](#run-phone-gate-escape) would do for the current ac
Starts verification of the submitted number. Requires an account satisfying [eligibility](#eligibility). Returns an [SMS delivery](#sms-delivery-object) object or an [inbound challenge](#inbound-challenge-object) object on success.
Several independent policies can force the inbound channel, and each one overrides a requested `sms`. Fluxer routes the attempt inbound before it contacts the lookup and SMS providers when any of these holds.
The instance can route the attempt to an inbound challenge instead of outbound SMS, and that overrides a requested `sms`. Requesting `inbound_challenge` always yields a challenge.
- The caller asks for `inbound_challenge`.
- The account has the flag that forces inbound verification.
- The configured inbound-required prefix policy matches the number while the account holds no verified phone.
An attempt that survives that step passes through phone attempt risk controls keyed by account, by client IP address, and by /24 network. A hard block returns 429 `PHONE_RATE_LIMIT_EXCEEDED` with a `Retry-After` of 86400. A captcha decision returns 400 `CAPTCHA_REQUIRED`, and this route reads no [CAPTCHA](/topics/captcha/) solution header, so the attempt cannot be retried with a solution. An inbound decision returns a challenge with the `behavioural_risk` reason.
When the SMS provider throttles an earlier send or code check for the same account or number, Fluxer records a cooldown. A request during that cooldown returns 429 `PHONE_RATE_LIMIT_EXCEEDED`. The destination is then resolved through [provider lookup](#provider-lookup-verdicts). A number that has already completed verification twice is refused with 400 `PHONE_ALREADY_USED`.
Further controls bound outbound delivery to 3 sends per 6 hours for each account and 3 sends per 5 days for each number. Either denial returns 429 `PHONE_RATE_LIMIT_EXCEEDED`. The provider can also refuse the send. An invalid destination returns 400 `INVALID_PHONE_NUMBER`, and a throttle returns 429 `PHONE_RATE_LIMIT_EXCEEDED`. Every other refusal, an unreachable provider included, returns 400 `SMS_VERIFICATION_UNAVAILABLE`. On an instance where the inbound flow or its receiving number is unconfigured, an attempt routed inbound returns the same code.
The number then passes the [number checks](#number-checks). A number that has reached its verification limit is refused with 400 `PHONE_ALREADY_USED`. Sends are limited for each account and for each number, and a denial returns 429 `PHONE_RATE_LIMIT_EXCEEDED`. A request can also return 400 `CAPTCHA_REQUIRED`. This route reads no [CAPTCHA](/topics/captcha/) solution header, so the attempt cannot be retried with a solution. Every other refusal returns 400 `SMS_VERIFICATION_UNAVAILABLE`, as does an instance with no phone verification service.
### JSON body
@@ -224,16 +178,16 @@ Further controls bound outbound delivery to 3 sends per 6 hours for each account
| phone | string | The number in canonical E.164 form, matching `^\+[1-9]\d{1,14}$` |
| channel?<sup>1</sup> | string | The preferred [verification channel](#phone-verification-channel-values) |
<sup>1</sup> Omitting the field requests outbound SMS. Requesting `inbound_challenge` always yields a challenge reported with the `expensive_destination` reason, unless the account flag independently forces the channel and the reason becomes `account_forced`
<sup>1</sup> Omitting the field requests outbound SMS. Requesting `inbound_challenge` always yields a challenge
### Response
| Status | Body | Condition |
| --- | --- | --- |
| 200 | [SMS delivery](#sms-delivery-object) object \| [inbound challenge](#inbound-challenge-object) object | Verification was started on the channel named by the response |
| 400 | [error response](/http-api/#error-response) | The number is refused with `INVALID_PHONE_NUMBER`, was verified twice already and returns `PHONE_ALREADY_USED`, risk controls demand `CAPTCHA_REQUIRED`, or the send could not start and returns `SMS_VERIFICATION_UNAVAILABLE` |
| 400 | [error response](/http-api/#error-response) | The number is refused with `INVALID_PHONE_NUMBER`, has reached its verification limit and returns `PHONE_ALREADY_USED`, the attempt returns `CAPTCHA_REQUIRED`, or the send could not start and returns `SMS_VERIFICATION_UNAVAILABLE` |
| 403 | [error response](/http-api/#error-response) | The account is not eligible and the request returns `PHONE_ADD_NOT_ELIGIBLE` |
| 429<sup>1</sup> | [rate limit response](/topics/rate-limits/#rate-limit-response-object) | Risk controls, a provider throttle, or an account or per-number send control denies the request, which returns `PHONE_RATE_LIMIT_EXCEEDED` |
| 429<sup>1</sup> | [rate limit response](/topics/rate-limits/#rate-limit-response-object) | A send limit or a provider throttle denies the request, which returns `PHONE_RATE_LIMIT_EXCEEDED` |
<sup>1</sup> A phone denial uses the rate limit envelope with `code` set to `PHONE_RATE_LIMIT_EXCEEDED`. An ordinary route denial returns `RATE_LIMITED`
@@ -260,11 +214,11 @@ The [eligibility](#eligibility) check does not apply here. The challenge code is
| Status | Body | Condition |
| --- | --- | --- |
| 200 | [direct inbound challenge](#direct-inbound-challenge-object) object | A challenge was issued |
| 400 | [error response](/http-api/#error-response) | The instance has no receiving number for the inbound challenge and the request returns `SMS_VERIFICATION_UNAVAILABLE` |
| 400 | [error response](/http-api/#error-response) | The instance cannot issue an inbound challenge and the request returns `SMS_VERIFICATION_UNAVAILABLE` |
### Side effects
Fluxer creates a one-use challenge for the current account. The challenge completes out of band when the provider delivers the matching inbound message, and the HTTP response has no part of it. On completion, Fluxer sets the verified phone state, clears the phone requirements from the stored suspicious activity bitfield, and dispatches [User Update](/gateway/events/#user-update). When the sending number has already completed verification twice, Fluxer abandons the completion and produces no observable error.
Fluxer creates a one-use challenge for the current account. The challenge completes out of band when the provider delivers the matching inbound message, and the HTTP response has no part of it. On completion, Fluxer sets the verified phone state, clears the phone requirements from the stored suspicious activity bitfield, and dispatches [User Update](/gateway/events/#user-update). A completion from a number that has reached its verification limit does not verify the account.
### Rate limit
@@ -276,9 +230,9 @@ Fluxer creates a one-use challenge for the current account. The challenge comple
Verifies an outbound SMS code. Requires an account satisfying [eligibility](#eligibility). Returns a [phone verification result](#phone-verification-result-object) object on success. Emits a [User Update](/gateway/events/#user-update) Gateway event.
The number is resolved through [provider lookup](#provider-lookup-verdicts) again before the code is checked, so a number that has become ineligible since the code was sent is refused.
The number passes the [number checks](#number-checks) again before the code is checked, so a number that has become ineligible since the code was sent is refused.
The configured SMS provider checks the number and the code together, so the provider decides the code lifetime and the attempt allowance. A rejected code returns 400 `INVALID_PHONE_VERIFICATION_CODE`. A provider throttle returns 429 `PHONE_RATE_LIMIT_EXCEEDED` and records a cooldown that also denies [Send phone verification](#send-phone-verification) until it expires. An unreachable provider, or one answering with a server error, returns 400 `SMS_VERIFICATION_UNAVAILABLE`. Once the code has been accepted, an account record that has been deleted or can no longer be read fails with 400 `PHONE_VERIFICATION_REQUIRED`.
The phone provider checks the number and the code together, so the provider decides the code lifetime and the attempt allowance. A rejected code returns 400 `INVALID_PHONE_VERIFICATION_CODE`. A throttle returns 429 `PHONE_RATE_LIMIT_EXCEEDED`. An unavailable provider returns 400 `SMS_VERIFICATION_UNAVAILABLE`. A number that has reached its verification limit fails with 400 `PHONE_ALREADY_USED`. A deleted account, or one that can no longer be read, fails with 400 `PHONE_VERIFICATION_REQUIRED`.
### JSON body
@@ -289,10 +243,6 @@ The configured SMS provider checks the number and the code together, so the prov
<sup>1</sup> Stripped of control and formatting characters and trimmed before the length check, and the normalised value is what the provider checks
:::caution[A third verification consumes the code before refusing]
A number can complete verification twice, and each success restarts a 31-day window in which Fluxer counts successes for that number. A third attempt is refused with 400 `PHONE_ALREADY_USED`, but only after the provider has accepted the code.
:::
### Response
| Status | Body | Condition |
@@ -308,9 +258,9 @@ On the 429, `X-RateLimit-Scope` reports `shared` when a number-scoped provider t
### Side effects
Fluxer marks the account as holding a verified phone. It then clears every phone requirement from the stored suspicious activity bitfield: `REQUIRE_VERIFIED_PHONE`, `REQUIRE_REVERIFIED_PHONE`, the combined email-or-phone requirements, `REQUIRE_INBOUND_PHONE_VERIFICATION`, and the marker deferring a phone requirement until the account joins a qualifying community guild. Email-only requirements remain, so an account that also owes email verification stays restricted.
Fluxer marks the account as holding a verified phone. It then clears every phone requirement from the stored suspicious activity bitfield: `REQUIRE_VERIFIED_PHONE`, `REQUIRE_REVERIFIED_PHONE`, the combined email-or-phone requirements, `REQUIRE_INBOUND_PHONE_VERIFICATION`, and the markers of a deferred phone requirement. Email-only requirements remain, so an account that also owes email verification stays restricted.
Fluxer clears the spammer flag when present and dispatches [User Update](/gateway/events/#user-update). When the spammer flag was cleared, Fluxer also dispatches [Guild Member Update](/gateway/events/#guild-member-update) for the account in every guild it belongs to, before the HTTP response returns.
Fluxer dispatches [User Update](/gateway/events/#user-update) before the HTTP response returns.
### Rate limit
@@ -336,12 +286,11 @@ Returns a [phone gate escape preview](#phone-gate-escape-preview-object) object
<RouteHeader method="POST" path="/v1/users/@me/required-actions/phone-gate-escape" />
Leaves the qualifying guilds the account does not own and defers the phone requirement again. Returns the [user](/http-api/users/#user-object) object.
Defers the phone requirement again without leaving any guild. Returns the [user](/http-api/users/#user-object) object.
### Limitations
- The account is in the state [Deferred phone requirement](#deferred-phone-requirement) defines, and one outside it is rejected with 400 [`PHONE_GATE_ESCAPE_UNAVAILABLE`](/http-api/errors/).
- One request leaves at most 25 guilds.
### JSON body
@@ -351,14 +300,12 @@ The body is an empty object. Fluxer reads an absent or empty body as an empty ob
| Status | Body | Condition |
| --- | --- | --- |
| 200 | [user](/http-api/users/#user-object) object | The escape ran, fully or up to the per-request limit |
| 200 | [user](/http-api/users/#user-object) object | The requirement was deferred again |
| 400 | [error response](/http-api/#error-response) | The escape is unavailable and the request returns `PHONE_GATE_ESCAPE_UNAVAILABLE` |
### Side effects
Fluxer leaves each qualifying guild the account does not own, at most 25 in one request, with the same effects as [Leave guild](/http-api/guilds/#leave-guild). A qualifying guild the account owns is kept and does not block the escape.
A request that leaves the whole set then defers the requirement again and dispatches [User Update](/gateway/events/#user-update). One that stopped at the limit writes no account field, dispatches nothing, and returns the account with the requirement still due. Repeat the request until the returned `required_actions` has no phone entry.
Fluxer defers the requirement again and dispatches [User Update](/gateway/events/#user-update). Guild memberships are unchanged.
### Rate limit
@@ -171,13 +171,13 @@ A target the caller has blocked returns 400 `CANNOT_SEND_FRIEND_REQUEST_TO_BLOCK
<sup>1</sup> The friend source evaluation is skipped when the target has no stored settings, which admits the request as though the target permitted requests from anyone
:::caution[A flagged caller receives an indistinguishable success]
A caller with the `SPAMMER` [public user flag](/http-api/users/#public-user-flags), or one whose send triggers the deployment's direct contact spam policy, gets a one-sided request. Fluxer writes only the caller's `OUTGOING_REQUEST` record. [Relationship Add](/gateway/events/#relationship-add) reaches the caller alone. The response is an ordinary `OUTGOING_REQUEST` object.
A caller with the `SPAMMER` [public user flag](/http-api/users/#public-user-flags) gets a one-sided request. Fluxer writes only the caller's `OUTGOING_REQUEST` record. [Relationship Add](/gateway/events/#relationship-add) reaches the caller alone. The response is an ordinary `OUTGOING_REQUEST` object.
:::
The one-sided request path also skips the rules that follow it: the target's block of the caller, the target's [friend source flags](/http-api/users/#friend-source-flags), the app store reviewer rule, the [relationship limit](#relationship-limit), and friendly bot auto-acceptance. The caller's own block of the target is still enforced and still returns 400 `CANNOT_SEND_FRIEND_REQUEST_TO_BLOCKED_USER`. When the direct contact spam policy suppresses a send and its action is `flag_spammer`, Fluxer also sets the `SPAMMER` flag on the caller, which makes every later send one-sided.
The one-sided request path also skips the rules that follow it: the target's block of the caller, the target's [friend source flags](/http-api/users/#friend-source-flags), the app store reviewer rule, the [relationship limit](#relationship-limit), and friendly bot auto-acceptance. The caller's own block of the target is still enforced and still returns 400 `CANNOT_SEND_FRIEND_REQUEST_TO_BLOCKED_USER`.
:::note[The suppression entries enter at different points]
Fluxer checks for the `SPAMMER` flag before it looks for a pending request from the target. A caller that already has the flag gets a one-sided request even when the target has a pending request, so it can hold both an `INCOMING_REQUEST` and an `OUTGOING_REQUEST` record for one account. Fluxer applies the direct contact spam policy after that lookup, so a caller without the flag accepts a pending request from the target normally.
Fluxer checks for the `SPAMMER` flag before it looks for a pending request from the target. A caller that already has the flag gets a one-sided request even when the target has a pending request, so it can hold both an `INCOMING_REQUEST` and an `OUTGOING_REQUEST` record for one account. A caller without the flag accepts a pending request from the target normally.
:::
### Path parameters
@@ -987,92 +987,20 @@ Default `false`. Behaviour when the scanner is unreachable. With scanning on and
Default `0.7`. The API-side NSFW score cutoff. No range check. Distinct from the Media Proxy threshold, which defaults to `0.85`.
#### `FLUXER_RISK_INTEGRATION_ENABLED`
#### `FLUXER_IPINFO_API_KEY`
Default `false`. IP intelligence. Needs an ipinfo key to do anything.
#### `FLUXER_RISK_IPINFO_API_KEY`
Default empty. The ipinfo key. Paired with `FLUXER_RISK_INTEGRATION_ENABLED`.
#### `FLUXER_ACCOUNT_POLICY_DSL`
No default. The account risk policy. JSON. Malformed JSON fails startup, and an unknown key in a well-formed policy surfaces when the policy runs.
#### `FLUXER_ABUSE_INBOUND_PHONE_COUNTRY_CODES`
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.
#### `FLUXER_ABUSE_DIRECT_CONTACT_SPAM_ENABLED`
Default `false`. Direct contact spam detection.
#### `FLUXER_ABUSE_DIRECT_CONTACT_SPAM_COUNTRY_CODES`
Default empty. Countries the rule applies to. Comma separated, unvalidated.
#### `FLUXER_ABUSE_DIRECT_CONTACT_SPAM_DISTINCT_TARGET_THRESHOLD`
Default `25`. Distinct targets before the rule fires. Integer.
#### `FLUXER_ABUSE_DIRECT_CONTACT_SPAM_TARGET_WINDOW_MS`
Default `7200000`. The observation window. Milliseconds, two hours by default.
#### `FLUXER_ABUSE_DIRECT_CONTACT_SPAM_ACTION`
Default `flag_spammer`. What happens when it fires. `flag_spammer` or `suppress_delivery`. Anything else fails startup.
Default empty. The ipinfo key. Admin and guild IP bans use it to skip carrier-grade NAT and other shared addresses. Without it those checks treat every address as unknown. The previous name `FLUXER_RISK_IPINFO_API_KEY` is still read when this one is unset.
#### `FLUXER_BLOCKLIST_FEEDS_ENABLED`
Defaults to the inverse of `FLUXER_SELF_HOSTED`. Off by default on a self-hosted instance. With feeds on, the worker downloads disposable email domains and the URLhaus and PhishTank URL lists every six hours, and MalwareBazaar file hashes every twelve hours. Fluxer checks registrations and email changes against the domains, posted links against the URLs, and uploads against the hashes.
Defaults to the inverse of `FLUXER_SELF_HOSTED`. Off by default on a self-hosted instance. With feeds on, the worker downloads the URLhaus and PhishTank URL lists every six hours, and MalwareBazaar file hashes every twelve hours. Fluxer checks posted links against the URLs and uploads against the hashes.
With feeds off, Fluxer checks none of this data. The first worker start with feeds off removes every disposable email domain, the URL feed file, and every `malware_bazaar` file-SHA ban that no Admin added. File-SHA bans added through the Admin API stay. Turning feeds back on downloads the disposable domains at the next worker start, the URLs within six hours, and the hashes within twelve.
#### `FLUXER_TOR_EXIT_LIST_ENABLED`
Defaults to the inverse of `FLUXER_SELF_HOSTED`. Tor exit blocking. The API fetches the exit relay list from `onionoo.torproject.org` at startup and every 30 minutes. Off by default on a self-hosted instance.
With feeds off, Fluxer checks none of this data. The first worker start with feeds off removes the URL feed file and every `malware_bazaar` file-SHA ban that no Admin added. File-SHA bans added through the Admin API stay. Turning feeds back on downloads the URLs within six hours and the hashes within twelve.
#### `FLUXER_BREACHED_PASSWORD_CHECK_ENABLED`
Defaults to the inverse of `FLUXER_SELF_HOSTED`. Breached password rejection. Sends the first five characters of the password's SHA-1 hash to `api.pwnedpasswords.com`. Off by default on a self-hosted instance.
A second family, unrelated to the rules above, tunes the IP auto-banner: `FLUXER_ABUSE_WINDOW_MS`, the `FLUXER_ABUSE_THRESHOLD_` names, the `FLUXER_ABUSE_TOKEN_DIVERSITY_` names, `FLUXER_ABUSE_BAN_TTL_SEC`, `FLUXER_ABUSE_BATCH_FLUSH_MS`, `FLUXER_ABUSE_MAX_BATCH_TICKS`, `FLUXER_ABUSE_MAX_NEW_TOKENS_PER_TICK`, `FLUXER_ABUSE_MAX_TRACKED_IPS`, `FLUXER_ABUSE_MAX_TOKEN_HASHES_PER_IP`, `FLUXER_ABUSE_MIN_SCORE_FOR_LOOKUP`, `FLUXER_ABUSE_MIN_TOKENS_FOR_LOOKUP`, and `FLUXER_ABUSE_REQUIRED_SCORE_WINDOWS_FOR_AUTO_BAN`. All are read directly from the environment, none are in `.env.example` or the Compose file, and a non-finite value or one at or below zero falls back to the default.
`FLUXER_ABUSE_EXEMPT_ASNS` takes comma-separated ASN numbers that the auto-banner never bans. Non-numeric entries are dropped. Before it buys a classification the auto-banner resolves the IP against the local MaxMind ASN database, and an IP on a listed ASN is neither classified nor banned. With no MaxMind ASN database the list has no effect. It does not change admin or guild IP bans, and `FLUXER_API_IP_BAN_EXEMPT_IPS` remains the way to exempt single addresses and ranges from every kind of IP ban.
Other names in that family limit how often the auto-banner buys an IP classification from ipinfo. A shared claim lets one replica do the lookup for the others. With the claim off, every API replica looks up the same attacking IP at the same moment, so one IP costs one lookup per replica.
- `FLUXER_ABUSE_IP_CLASS_CLAIM_ENABLED` defaults to `1` and gates the shared claim. It is read as a string, and only `0` turns the claim off.
- `FLUXER_ABUSE_IP_CLASS_CLAIM_TTL_SEC` defaults to `15` seconds and sets how long a replica holds that claim.
- `FLUXER_ABUSE_IP_CLASS_PENDING_TTL_MS` defaults to `20000` milliseconds and sets how long a replica that lost the claim waits before it tries again.
- `FLUXER_ABUSE_IP_CLASS_NEGATIVE_TTL_MS` defaults to `300000` milliseconds and sets how long a failed classification is remembered.
- `FLUXER_ABUSE_IP_CLASS_HINT_TTL_MS` defaults to `600000` milliseconds and sets how long a class sent by another replica stays usable.
For these numeric settings, a non-finite value or one at or below zero falls back to the default.
A `FLUXER_IPINFO_BUDGET_` family caps what the instance spends at ipinfo. `FLUXER_IPINFO_BUDGET_ENABLED` defaults to `1`, and `0` turns off all shedding. `FLUXER_IPINFO_BUDGET_MONTHLY_MAX` defaults to `140000` and is the ceiling for one UTC calendar month. Lookups run at the priorities below, each with a share of that ceiling and a token bucket for bursts refilled once a minute.
- Admin IP bans and scheduled deletion checks are critical. They reach the full ceiling, with a burst of `60` from `FLUXER_IPINFO_BUDGET_CRITICAL_BURST` refilled at `60` a minute by `FLUXER_IPINFO_BUDGET_CRITICAL_REFILL_PER_MIN`.
- Registration risk is standard. It stops at `FLUXER_IPINFO_BUDGET_STANDARD_MONTHLY_PCT` percent of the ceiling, default `90`, with a burst of `240` from `FLUXER_IPINFO_BUDGET_STANDARD_BURST` refilled at `120` a minute by `FLUXER_IPINFO_BUDGET_STANDARD_REFILL_PER_MIN`.
- The IP auto-banner is background. It stops at `FLUXER_IPINFO_BUDGET_BACKGROUND_MONTHLY_PCT` percent, default `60`, with a burst of `120` from `FLUXER_IPINFO_BUDGET_BACKGROUND_BURST` refilled at `30` a minute by `FLUXER_IPINFO_BUDGET_BACKGROUND_REFILL_PER_MIN`.
Background lookups stop first and critical lookups stop last. A denied lookup returns an unavailable result. If the key-value store fails, lookups remain allowed at every priority, so an outage can increase usage beyond these budgets.
Local MaxMind data can reduce registration-risk lookups. Set `FLUXER_RISK_IPINFO_PRESCREEN_ENABLED` to `1` or `true`, in any letter case, and fill `FLUXER_RISK_IPINFO_PRESCREEN_ALLOW_ASNS` with comma-separated ASN numbers. Non-numeric entries are dropped. An IP skips ipinfo only when MaxMind supplies its country and an allowed ASN, and the organisation is not a commercial privacy provider, education network or cellular network.
## Stored instance policy
Use the [admin dashboard](#runtime-settings-in-the-admin-dashboard) or [Admin instance API](/admin-api/instance/) to change saved settings. Missing settings use their documented defaults. Invalid JSON, field types, identifiers or out-of-range values cause an error instead of silently resetting security or registration policy.
@@ -1149,7 +1077,7 @@ Defaults to `false` at the Gateway's environment layer. Gateway telemetry. Read
#### `HOSTNAME`
Set by Docker. Node identity in metrics and logs. Also used by the IP auto-banner.
Set by Docker. Node identity in metrics and logs.
#### `FLUXER_ENV`
@@ -1255,7 +1183,7 @@ Default `10000`. Purge request timeout. Accepts 1000 to 10000. The range is chec
#### `FLUXER_GEOIP_DB_PATH`
Default empty. The GeoIP database. A filesystem path, or an `s3://bucket/key` URL whose `download_path` query parameter is mandatory and must be absolute. `app-proxy` also accepts `MAXMIND_DB_PATH`. The API also reads an ASN database. On a filesystem path it looks for `GeoLite2-ASN.mmdb` in the same directory. On an `s3://` URL it needs an `asn_key` query parameter, and takes an optional absolute `asn_download_path` next to it. A missing ASN database is not an error, and the API falls back to ipinfo for ASN data.
Default empty. The GeoIP database. A filesystem path, or an `s3://bucket/key` URL whose `download_path` query parameter is mandatory and must be absolute. `app-proxy` also accepts `MAXMIND_DB_PATH`.
## Instance identity and branding
@@ -1733,7 +1661,7 @@ Branding is otherwise admin-dashboard only.
Supplies the initial setup state on a self-hosted instance, until the first write of the stored `app_public_config` row takes over.
#### Every `FLUXER_ABUSE_` auto-banner name, every `FLUXER_IPINFO_` budget name, and every Media Proxy performance knob
#### Every Media Proxy performance knob
No example coverage at all.
@@ -30,7 +30,7 @@ Create private channel is gated only on the group direct message path, where the
## Exemption
Fluxer skips the check in three cases, and the operation then proceeds with no CAPTCHA header. The instance account policy grants the `captcha_exempt` capability to the authenticated account's email address. The authenticated account holds the [`APP_STORE_REVIEWER`](/admin-api/users/#account-flags) flag. The request body has an `email` that belongs to an account holding that flag. Discovery does not report exemptions, so clients must handle a challenge on every gated operation.
Fluxer skips the check in three cases, and the operation then proceeds with no CAPTCHA header. The authenticated account's email address is on an exempt domain. The authenticated account holds the [`APP_STORE_REVIEWER`](/admin-api/users/#account-flags) flag. The request body has an `email` that belongs to an account holding that flag. Discovery does not report exemptions, so clients must handle a challenge on every gated operation.
## Request headers
@@ -79,6 +79,6 @@ A rejected solution or unavailable provider returns 400 `INVALID_CAPTCHA`. The r
| CAPTCHA_REQUIRED<sup>1</sup> | 400 | The operation is gated and the request has no solution |
| INVALID_CAPTCHA | 400 | The provider rejected the solution, or verification could not be completed |
<sup>1</sup> [Send phone verification](/http-api/users/phone-verification/#send-phone-verification) also answers this code when Fluxer's risk check on the phone attempt decides that the request needs a CAPTCHA. That operation is not gated and accepts no solution, so retrying it with `X-Captcha-Token` never helps
<sup>1</sup> [Send phone verification](/http-api/users/phone-verification/#send-phone-verification) can also answer this code. That operation is not gated and accepts no solution, so retrying it with `X-Captcha-Token` never helps
Both codes are defined in the [API error code registry](/http-api/errors/#api-error-code-registry), and the body of each is the ordinary [error response](/http-api/#error-response) envelope.