diff --git a/fluxer_docs/src/content/docs/admin-api/api-keys.mdx b/fluxer_docs/src/content/docs/admin-api/api-keys.mdx
index 29d54843e..ea31eb8ae 100644
--- a/fluxer_docs/src/content/docs/admin-api/api-keys.mdx
+++ b/fluxer_docs/src/content/docs/admin-api/api-keys.mdx
@@ -1,7 +1,7 @@
---
# SPDX-License-Identifier: AGPL-3.0-or-later
title: Admin API keys
-description: Admin API key objects and the five operations that manage them.
+description: Admin API key objects and the operations that manage them.
---
import RouteHeader from '@/components/RouteHeader.astro';
diff --git a/fluxer_docs/src/content/docs/admin-api/archives.mdx b/fluxer_docs/src/content/docs/admin-api/archives.mdx
index c9c43d71e..c1b1a2a3a 100644
--- a/fluxer_docs/src/content/docs/admin-api/archives.mdx
+++ b/fluxer_docs/src/content/docs/admin-api/archives.mdx
@@ -20,7 +20,7 @@ Every operation needs an [ACL](/admin-api/#acl-evaluation) covering the subject
## Archive object
-An archive is in one of three lifecycle states. It is building while `completed_at` and `failed_at` are both null, complete once `completed_at` is set, and failed once `failed_at` is set. A retried attempt clears `failed_at` and `error_message` when it starts, so a failed archive reads as building again while the retry runs.
+An archive is in one of these lifecycle states. It is building while `completed_at` and `failed_at` are both null, complete once `completed_at` is set, and failed once `failed_at` is set. A retried attempt clears `failed_at` and `error_message` when it starts, so a failed archive reads as building again while the retry runs.
### Structure
diff --git a/fluxer_docs/src/content/docs/admin-api/blocklists.mdx b/fluxer_docs/src/content/docs/admin-api/blocklists.mdx
index 10201890f..0606d3cac 100644
--- a/fluxer_docs/src/content/docs/admin-api/blocklists.mdx
+++ b/fluxer_docs/src/content/docs/admin-api/blocklists.mdx
@@ -1,14 +1,14 @@
---
# SPDX-License-Identifier: AGPL-3.0-or-later
title: Blocklists
-description: The nine safety blocklists, their value forms, and the operations that manage them.
+description: The safety blocklists, their value forms, and the operations that manage them.
---
import RouteHeader from '@/components/RouteHeader.astro';
A blocklist is a stored set of values Fluxer checks account access and user content against. Nine lists exist. Each has one canonical value form, one matching rule, and its own stored fields. Fluxer canonicalises every value before storage and before every check.
-Each list has its own three [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 write records the audit reason on the [Admin audit entries](/admin-api/#admin-audit-entry-object) it produces. The reads record nothing.
+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 write records the audit reason on the [Admin audit entries](/admin-api/#admin-audit-entry-object) it produces. The reads record nothing.
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`.
@@ -50,7 +50,7 @@ Fluxer synchronises disposable email domains from external feeds every six hours
9 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 three `supports_` flags of a [blocklist object](#blocklist-object) before writing to a list.
+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.
## Content blocklist categories
@@ -67,7 +67,7 @@ Every `url`, `url-domain`, `file-sha`, and `avatar-hash` row has a category nami
| gifct | Imported from a GIFCT hash set |
| stop_ncii | Imported from a StopNCII hash set |
-The request field is a free string of 1 through 64 characters, so a value outside this registry is accepted and stored verbatim. Tolerate a stored category outside these eight.
+The request field is a free string of 1 through 64 characters, so a value outside this registry is accepted and stored verbatim. Tolerate a stored category outside the registry.
## Content blocklist severities
@@ -267,7 +267,7 @@ Each field is a string, except `hashes` and `substrings`, which are `array[strin
-Returns one [blocklist](#blocklist-object) object for every blocklist this instance keeps. Requires any one of the nine `check` permissions.
+Returns one [blocklist](#blocklist-object) object for every blocklist this instance keeps. Requires any one of the `check` permissions.
The response is identical for every Admin.
diff --git a/fluxer_docs/src/content/docs/admin-api/discovery.mdx b/fluxer_docs/src/content/docs/admin-api/discovery.mdx
index 0fb008208..fc0ae8b30 100644
--- a/fluxer_docs/src/content/docs/admin-api/discovery.mdx
+++ b/fluxer_docs/src/content/docs/admin-api/discovery.mdx
@@ -8,7 +8,7 @@ import RouteHeader from '@/components/RouteHeader.astro';
Admin discovery is the review side of the public guild directory. An Admin decides the application a guild manager submits, reads and edits the approved listing, moves it between categories, and removes the guild from the directory. A guild has at most one application, and the public [Discovery](/http-api/discovery/) resource drives the same lifecycle from the guild's side.
-Every operation except [Remove discovery listing](#remove-discovery-listing) requires `discovery:review`, which covers the four reads, [Review discovery application](#review-discovery-application), [Move discovery listings to a category](#move-discovery-listings-to-a-category), and [Update discovery listing](#update-discovery-listing). [Remove discovery listing](#remove-discovery-listing) requires `discovery:remove` instead. Neither implies the other.
+Every operation except [Remove discovery listing](#remove-discovery-listing) requires `discovery:review`, which covers the reads, [Review discovery application](#review-discovery-application), [Move discovery listings to a category](#move-discovery-listings-to-a-category), and [Update discovery listing](#update-discovery-listing). [Remove discovery listing](#remove-discovery-listing) requires `discovery:remove` instead. Neither implies the other.
No operation here reads `X-Audit-Log-Reason` or records an Admin audit entry. An operation that stores a reason takes it in its request body.
@@ -40,7 +40,7 @@ A guild's submitted application, together with the guild details Fluxer resolves
| custom_tags6 8 | array[string] | Normalised [custom tags](/http-api/discovery/#custom-tags) of the listing |
| applied_at | ISO8601 timestamp | Time at which the guild applied |
-1 Resolved from the guild at request time. A guild that can no longer be resolved yields the literal name `(unknown guild)`, a null icon, the owner ID `0`, a member count of 0, a null NSFW level, an empty feature array, and null for all three owner name fields
+1 Resolved from the guild at request time. A guild that can no longer be resolved yields the literal name `(unknown guild)`, a null icon, the owner ID `0`, a member count of 0, a null NSFW level, an empty feature array, and null for all owner name fields
2 Read from the owner account, so it is null when the guild resolved but the owner account did not
diff --git a/fluxer_docs/src/content/docs/admin-api/guilds.mdx b/fluxer_docs/src/content/docs/admin-api/guilds.mdx
index 13cc75d4e..135b75944 100644
--- a/fluxer_docs/src/content/docs/admin-api/guilds.mdx
+++ b/fluxer_docs/src/content/docs/admin-api/guilds.mdx
@@ -41,11 +41,11 @@ The compact guild representation returned by [List guilds](#list-guilds) and by
| approximate_member_count?4 | integer | The member count the main Gateway reports |
| approximate_presence_count?4 | integer | The connected member count the main Gateway reports |
-1 All three are null whenever the owner account is not resolved. [List guilds](#list-guilds) never resolves it, so all three are always null there
+1 All are null whenever the owner account is not resolved. [List guilds](#list-guilds) never resolves it, so all are always null there
2 A member connecting or disconnecting does not change it
-3 No operation populates these three, so they are absent from every current response
+3 No operation populates these, so they are absent from every current response
4 Present only on [List user guilds](/admin-api/users/#list-user-guilds) when that operation is asked for counts
@@ -105,9 +105,9 @@ The full guild representation returned by [Get guild](#get-guild). It embeds eve
| channels | array[[Admin guild channel](#admin-guild-channel-object) object] | Every channel in the guild |
| roles | array[[Admin guild role](#admin-guild-role-object) object] | Every role in the guild |
-1 All three are null when the owner account cannot be resolved
+1 All are null when the owner account cannot be resolved
-2 The operation does not populate these three, so they are absent from every current response
+2 The operation does not populate these, so they are absent from every current response
### Admin guild channel object
@@ -126,7 +126,7 @@ The full guild representation returned by [Get guild](#get-guild). It embeds eve
| content_warning_text?3 | ?string | The content warning text set on the channel |
| url4 | ?string | The external channel URL (1-2048 characters) |
-3 The operation does not populate these three, so they are absent from every current response
+3 The operation does not populate these, so they are absent from every current response
4 Null for every channel type that is not an external link channel
@@ -163,7 +163,7 @@ The guild state [Update guild](#update-guild) reads back after applying the requ
## Admin guild expression object
-A custom emoji or a sticker of a guild, with a resolvable media URL. The two listings return the same shape.
+A custom emoji or a sticker of a guild, with a resolvable media URL. The listings return the same shape.
### Structure
@@ -370,7 +370,7 @@ The channel references, idle timeout, and message history cutoff of a guild are
| --- | --- | --- |
| 200 | response body | Every selected field group was applied |
| 400 | [error response](/admin-api/#error-response) | The custom invite code is malformed or already claimed |
-| 403 | [error response](/admin-api/#error-response) | `MISSING_ACL`, because the account holds none of the five update ACLs, or lacks an ACL a supplied field selects |
+| 403 | [error response](/admin-api/#error-response) | `MISSING_ACL`, because the account holds none of the update ACLs, or lacks an ACL a supplied field selects |
| 404 | [error response](/admin-api/#error-response) | `UNKNOWN_GUILD`, because the guild does not exist |
:::caution[Field groups apply one at a time]
@@ -769,7 +769,7 @@ The page has the same shape and semantics as the public [List guild audit logs](
| user_id?2 | snowflake | Return only entries recorded for this actor |
| action_type?2 | integer | Return only entries with this [audit action](/http-api/guild-audit-logs/#audit-actions) value |
-1 The two cursors are mutually exclusive. Supplying both fails with 400 `INVALID_FORM_BODY` and the validation code `CANNOT_SPECIFY_BOTH_BEFORE_AND_AFTER` against `before`
+1 The cursors are mutually exclusive. Supplying both fails with 400 `INVALID_FORM_BODY` and the validation code `CANNOT_SPECIFY_BOTH_BEFORE_AND_AFTER` against `before`
2 Supplying either filter disables the consolidation of consecutive message deletion entries
diff --git a/fluxer_docs/src/content/docs/admin-api/index.mdx b/fluxer_docs/src/content/docs/admin-api/index.mdx
index 1f340f369..12fc83985 100644
--- a/fluxer_docs/src/content/docs/admin-api/index.mdx
+++ b/fluxer_docs/src/content/docs/admin-api/index.mdx
@@ -40,7 +40,7 @@ Where an operation stacks several requirements, Fluxer evaluates each separately
The effective ACL set of a session or an Admin OAuth2 bearer credential is the set stored on the account. The effective set of an Admin API key is the set stored on the key, subject to the owner check above. [Set user ACLs](/admin-api/users/#set-user-acls) writes an account's set. Holding `admin:authenticate` or `*` is the whole definition of an account that can reach the Admin API.
-Two operations derive their required ACLs from the validated request body.
+The operations below derive their required ACLs from the validated request body.
[Update guild](/admin-api/guilds/#update-guild) maps each present body field to one ACL and requires every ACL in that set.
@@ -413,7 +413,7 @@ The registry is returned in this order by [List ACLs](#list-acls). A value outsi
| acl:set:user | Replaces the Admin ACL set held by an account |
| admin_api_key:manage | Creates, reads, renames, and revokes [Admin API keys](/admin-api/api-keys/) |
| application:lookup | Reads [Admin application](/admin-api/applications/) resources, including the applications owned by one account |
-| application:list:by_owner2 | Reads the two application listings |
+| application:list:by_owner2 | Reads the application listings |
| application:transfer_ownership | Transfers OAuth2 application ownership |
| archive:trigger:guild | Creates guild [archives](/admin-api/archives/) |
| archive:trigger:user | Creates user archives |
@@ -526,11 +526,11 @@ The registry is returned in this order by [List ACLs](#list-acls). A value outsi
2 [List applications](/admin-api/applications/#list-applications) and [List user applications](/admin-api/users/#list-user-applications) accept it in place of `application:lookup`, and no other operation reads it
-3 The three archive reads accept `archive:view_all`, `archive:trigger:user`, or `archive:trigger:guild`, so an Admin who can create archives can also read them
+3 The archive reads accept `archive:view_all`, `archive:trigger:user`, or `archive:trigger:guild`, so an Admin who can create archives can also read them
4 [Block a user's current avatar](/admin-api/blocklists/#block-a-users-current-avatar) requires it as well
-5 [Update guild](/admin-api/guilds/#update-guild) names all five of these as one any-of requirement and then requires every one that the submitted body maps to
+5 [Update guild](/admin-api/guilds/#update-guild) names all of these as one any-of requirement and then requires every one that the submitted body maps to
6 The permission admits no operation of its own. It only widens what an already admitted read returns
diff --git a/fluxer_docs/src/content/docs/admin-api/instance.mdx b/fluxer_docs/src/content/docs/admin-api/instance.mdx
index 49acb76b0..4d5eae638 100644
--- a/fluxer_docs/src/content/docs/admin-api/instance.mdx
+++ b/fluxer_docs/src/content/docs/admin-api/instance.mdx
@@ -130,7 +130,7 @@ How often a client polls [Get experiment assignments](/http-api/experiments/#get
Every field is present on read. A deployment that has stored nothing reports the defaults above.
-Both fields are served to every account, whether or not any experiment targets that account, and neither one is versioned by `config_version`. A client that has never reached the experiments route holds the same two values as built-in defaults, 300 seconds and 15 percent, so neither field reaches a client that cannot read the route.
+Both fields are served to every account, whether or not any experiment targets that account, and neither one is versioned by `config_version`. A client that has never reached the experiments route holds the same values as built-in defaults, 300 seconds and 15 percent, so neither field reaches a client that cannot read the route.
## Registration configuration object
@@ -257,7 +257,7 @@ Community, direct message, premium and gating policy for the whole deployment.
| direct_messages_locked1 | boolean | Whether the direct message setting is locked against further change |
| premium_mode | string | [Premium mode](#premium-modes) |
| services | object | Operator overrides for `gif_enabled`, `youtube_enabled`, and `bluesky_enabled`. Each is nullable, and null means no override |
-| services_resolved2 | object | The same three keys as concrete booleans, resolved from the override and the provider's own availability |
+| services_resolved2 | object | The same keys as concrete booleans, resolved from the override and the provider's own availability |
| services_available3 | 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 |
@@ -449,7 +449,7 @@ Trait definitions and the ordered rules that decide each [limit key](/http-api/i
| traitDefinitions | array[string] | Trait names a rule filter can match |
| rules | array[[limit rule](#limit-rule-object) object] | Ordered limit rules |
-The two field names are camelCase, unlike the rest of the Admin API.
+The field names are camelCase, unlike the rest of the Admin API.
## Limit rule object
@@ -535,7 +535,7 @@ The body has one optional object for each section. Fluxer leaves an absent secti
3 A secret such as `klipy_api_key`, `api_key`, `hcaptcha_secret_key`, `turnstile_secret_key`, or the SMTP `password` is written when supplied and left alone when absent. `integrations.bluesky.keys` is the only way to write the Bluesky signing keys counted as `bluesky.key_count`. It takes up to 8 entries of `kid` (1-255 characters) and nullable `private_key` (up to 10000 characters), and replaces the stored key set outright
:::note[Single sign-on URL validation is conditional]
-Fluxer skips URL validation while the merged configuration leaves single sign-on disabled. A configuration both enabled and enforced fails validation against `sso` with `SSO_MISCONFIGURED` unless it resolves an authorisation endpoint, a token endpoint, a client identifier, and a claims source. `issuer` stands in for the two endpoints and the claims source.
+Fluxer skips URL validation while the merged configuration leaves single sign-on disabled. A configuration both enabled and enforced fails validation against `sso` with `SSO_MISCONFIGURED` unless it resolves an authorisation endpoint, a token endpoint, a client identifier, and a claims source. `issuer` stands in for the endpoints and the claims source.
:::
#### Instance policy update structure
diff --git a/fluxer_docs/src/content/docs/admin-api/jobs.mdx b/fluxer_docs/src/content/docs/admin-api/jobs.mdx
index 71d9dab8c..dbc06af0d 100644
--- a/fluxer_docs/src/content/docs/admin-api/jobs.mdx
+++ b/fluxer_docs/src/content/docs/admin-api/jobs.mdx
@@ -15,7 +15,7 @@ A large share of Fluxer's background work never gets a ledger row, and that work
:::
:::note[Fan-out, embeds, and most sweeps have no ledger row]
-Mention fan-out (`handleMentions` and `handleMentionChunk`), embed extraction (`extractEmbeds`), and every periodic sweep other than the three blocklist feed syncs run this way.
+Mention fan-out (`handleMentions` and `handleMentionChunk`), embed extraction (`extractEmbeds`), and every periodic sweep other than the blocklist feed syncs run this way.
:::
## Admin job object
@@ -48,7 +48,7 @@ One object is one row of the ledger. No operation on this page changes any field
| payload11 | ?string | The JSON rendering of the job input |
| result | ?string | Always null |
-1 The task reports progress at its own pace, and a task that never reports leaves all three null for its whole run. A bulk task reports once before it starts, again after every 25 entities, and once when it finishes, except for `schedule_user_deletion`, which reports after every 10 accounts, and the file SHA bulk ban, which reports after every 50 hashes
+1 The task reports progress at its own pace, and a task that never reports leaves all null for its whole run. A bulk task reports once before it starts, again after every 25 entities, and once when it finishes, except for `schedule_user_deletion`, which reports after every 10 accounts, and the file SHA bulk ban, which reports after every 50 hashes
2 Written only when the job is dead-lettered. A single failed delivery that is redelivered leaves it null
@@ -94,7 +94,7 @@ One object is one row of the ledger. No operation on this page changes any field
## Job cursor object
-The resume point [List jobs](#list-jobs) returns, split into the three cursor query parameters a caller sends back.
+The resume point [List jobs](#list-jobs) returns, split into the cursor query parameters a caller sends back.
### Structure
@@ -126,7 +126,7 @@ The resume point [List jobs](#list-jobs) returns, split into the three cursor qu
1 Only a task that aborts at the checkpoint settles here, as [Cancel job](#cancel-job) describes
-`queued` and `running` are the active statuses and the other three are terminal. [List active jobs](#list-active-jobs) returns exactly the two active statuses. Cancellation is recorded only while a job holds one of them.
+`queued` and `running` are the active statuses and the other three are terminal. [List active jobs](#list-active-jobs) returns exactly the active statuses. Cancellation is recorded only while a job holds one of them.
A job whose queue publish fails after its ledger row is written also settles as `deadletter`. That row reports `attempts` 0, `started_at` null, and `error_message` from the publish failure.
@@ -152,7 +152,7 @@ A lane is a consumer group with its own concurrency, acknowledgement deadline, a
- `lifecycle` runs `applicationProcessDeletion`, `batchGuildAuditLogMessageDeletes`, `bulkAddGuildMembers`, `bulkBanFileShas`, `bulkDeleteSelfMessagesImmediate`, `bulkDeleteUserMessages`, `bulkDeleteUserMessagesScoped`, `bulkScheduleUserDeletion`, `bulkUpdateGuildFeatures`, `bulkUpdateSuspiciousActivityFlags`, `bulkUpdateUserFlags`, `deleteUserMessagesInGuildByTime`, `finalizeNcmecAttachmentReport`, `harvestGuildData`, `harvestUserData`, `messageShred`, `processStripeWebhook`, `reconcileUserPayments`, `revalidateUserConnections`, `sendSystemDm`, `userProcessPendingDeletion`, and `userProcessPendingDeletions`.
- `batch` runs `expireAttachments`, `flushUserActivityBuffer`, `indexChannelMessages`, `indexGuildMembers`, `processAssetDeletionQueue`, `processBunnyPurgeQueue`, `processExpiredPremiumSweep`, `processInactivityDeletions`, `processPendingBulkMessageDeletions`, `processPremiumStateReconciliationQueue`, `prunePostgresKvTtl`, `refreshSearchIndex`, `syncDiscoveryIndex`, `syncDisposableEmailDomains`, `syncFileShaBlocklists`, and `syncUrlBlocklists`.
-The five tasks queued by [Bulk jobs](/admin-api/bulk-jobs/) are `bulkUpdateUserFlags`, `bulkUpdateSuspiciousActivityFlags`, `bulkUpdateGuildFeatures`, `bulkAddGuildMembers`, and `bulkScheduleUserDeletion`. `harvestUserData` and `harvestGuildData` build [archives](/admin-api/archives/), `sendSystemDm` delivers a [system DM broadcast](/admin-api/system-dms/), and `refreshSearchIndex` rebuilds a [search index](/admin-api/search-indexes/).
+The tasks queued by [Bulk jobs](/admin-api/bulk-jobs/) are `bulkUpdateUserFlags`, `bulkUpdateSuspiciousActivityFlags`, `bulkUpdateGuildFeatures`, `bulkAddGuildMembers`, and `bulkScheduleUserDeletion`. `harvestUserData` and `harvestGuildData` build [archives](/admin-api/archives/), `sendSystemDm` delivers a [system DM broadcast](/admin-api/system-dms/), and `refreshSearchIndex` rebuilds a [search index](/admin-api/search-indexes/).
## List jobs
@@ -175,7 +175,7 @@ The ledger is bucketed by UTC day and each bucket is ordered by creation time de
| task_type?3 | string | The [background job task type](#background-job-task-types) a returned job must run (1-128 characters) |
| requested_by_user_id? | snowflake | The ID of the account a returned job must record as its requester |
-1 The three cursor parameters are one value split into parts and are supplied together. A strict subset, a `cursor_bucket_day` that is not a calendar date, or a `cursor_created_at` that is not an ISO 8601 timestamp returns 400 `INVALID_FORM_BODY`, and each rejected part is named by the `path` of an element in `errors`
+1 The cursor parameters are one value split into parts and are supplied together. A strict subset, a `cursor_bucket_day` that is not a calendar date, or a `cursor_created_at` that is not an ISO 8601 timestamp returns 400 `INVALID_FORM_BODY`, and each rejected part is named by the `path` of an element in `errors`
2 The scan covers the starting bucket plus this many earlier buckets, so the default reads 15. The starting bucket is the current UTC day, or the cursor's bucket when one is supplied
@@ -193,7 +193,7 @@ The ledger is bucketed by UTC day and each bucket is ordered by creation time de
| Status | Body | Condition |
| --- | --- | --- |
| 200 | response body | The job page was returned |
-| 400 | [error response](/admin-api/#error-response) | `INVALID_FORM_BODY` because the cursor is a strict subset of its three parts or one part is malformed |
+| 400 | [error response](/admin-api/#error-response) | `INVALID_FORM_BODY` because the cursor is a strict subset of its parts or one part is malformed |
### Rate limit
diff --git a/fluxer_docs/src/content/docs/admin-api/messages.mdx b/fluxer_docs/src/content/docs/admin-api/messages.mdx
index 627c660c9..69fb81ddd 100644
--- a/fluxer_docs/src/content/docs/admin-api/messages.mdx
+++ b/fluxer_docs/src/content/docs/admin-api/messages.mdx
@@ -42,7 +42,7 @@ Every operation that returns Admin message objects also returns the matching pub
| attachments | array[[Admin message attachment](#admin-message-attachment-object) object] | The attachments of the message (at most 10) |
| user_prior_ncmec_report_ids?3 | array[string] | The NCMEC report IDs previously filed against the author, ascending (at most 100) |
-1 Every object in one response has the same values, taken from the channel named in the request path or query. All five are null when the channel no longer exists
+1 Every object in one response has the same values, taken from the channel named in the request path or query. All are null when the channel no longer exists
2 Always absent from the objects these operations return. The same member is set on the [report message context object](/admin-api/reports/#report-message-context-object)
@@ -285,7 +285,7 @@ One [Admin audit entry](/admin-api/#admin-audit-entry-object) is recorded with t
Searches the message index of one channel, or resolves one message in that channel by message ID or by attachment identity. Requires `message:lookup`.
-The three modes return two different response bodies, and the query decides which one arrives.
+The modes return a search body or a lookup body, and the query decides which one arrives.
### Query parameters
@@ -339,7 +339,7 @@ Returned when `message_id` or `attachment_id` was supplied. It is the same body
The result is empty for an unknown channel, for a message that does not exist, and for an attachment whose filename does not match. This operation never answers 404.
-With no message search service configured the search mode returns an empty `messages` array and `total` zero. The two lookup modes are unaffected.
+With no message search service configured the search mode returns an empty `messages` array and `total` zero. The lookup modes are unaffected.
:::note[A deleted message never appears in a result]
A hit whose message no longer exists is dropped from the page and excluded from `total`.
@@ -381,7 +381,7 @@ Shred jobs are created by [Shred user messages](/admin-api/users/#shred-user-mes
Submits one image or video attachment to NCMEC and starts the account enforcement workflow for its author. Requires `csam:submit_ncmec`, `message:delete`, `user:delete`, and `archive:trigger:user`.
-A caller missing more than one of the four is refused with 403 `MISSING_ACL` naming the first it does not hold, in the order listed. The wildcard satisfies all four.
+A caller missing more than one of the four is refused with 403 `MISSING_ACL` naming the first it does not hold, in the order listed. The wildcard satisfies them all.
This operation ignores `X-Audit-Log-Reason`. The reason on every audit entry the workflow produces is built from the assigned NCMEC report ID and the channel and attachment IDs, and it is returned as `audit_log_reason`.
diff --git a/fluxer_docs/src/content/docs/admin-api/reports.mdx b/fluxer_docs/src/content/docs/admin-api/reports.mdx
index bbf7f8ab7..27c11c755 100644
--- a/fluxer_docs/src/content/docs/admin-api/reports.mdx
+++ b/fluxer_docs/src/content/docs/admin-api/reports.mdx
@@ -132,7 +132,7 @@ A report is written once, when the reporter files it. [Update report](#update-re
## Admin report detail object
-The detail object extends the [Admin report object](#admin-report-object) with the evidence too large to return in a list page. [Get report](#get-report) emits these three members, and [List reports](#list-reports) omits them.
+The detail object extends the [Admin report object](#admin-report-object) with the evidence too large to return in a list page. [Get report](#get-report) emits these members, and [List reports](#list-reports) omits them.
### Structure
@@ -258,7 +258,7 @@ A report resolution is the reduced view [Update report](#update-report) returns.
Returns a page of [Admin report](#admin-report-object) objects without message context. Requires `report:view`.
-The operation has two branches. Supplying `q`, `report_type`, `category`, `reporter_id`, `reported_user_id`, `reported_guild_id`, `reported_channel_id`, `guild_context_id`, or `resolved_by_admin_id` searches the report index and returns `total`, `offset`, and `limit` beside the page. Supplying none of them lists by status alone and returns only `reports`.
+The operation has these branches. Supplying `q`, `report_type`, `category`, `reporter_id`, `reported_user_id`, `reported_guild_id`, `reported_channel_id`, `guild_context_id`, or `resolved_by_admin_id` searches the report index and returns `total`, `offset`, and `limit` beside the page. Supplying none of them lists by status alone and returns only `reports`.
The status-only branch defaults to PENDING, so a request with no query string returns pending reports only. The search branch applies no status filter when `status` is omitted. `sort_by` and `sort_order` reach the search branch only. The status-only branch always orders by `reported_at` descending.
diff --git a/fluxer_docs/src/content/docs/admin-api/search-indexes.mdx b/fluxer_docs/src/content/docs/admin-api/search-indexes.mdx
index fc1d3d73e..bce6d2f57 100644
--- a/fluxer_docs/src/content/docs/admin-api/search-indexes.mdx
+++ b/fluxer_docs/src/content/docs/admin-api/search-indexes.mdx
@@ -6,7 +6,7 @@ description: Administrative search index rebuilds, index names, and rebuild prog
import RouteHeader from '@/components/RouteHeader.astro';
-A search index holds the documents a search reads, and a rebuild writes those documents from current data. These two routes queue a rebuild and read its progress. The rebuild itself runs as the `refreshSearchIndex` background task described by [Jobs](/admin-api/jobs/).
+A search index holds the documents a search reads, and a rebuild writes those documents from current data. These routes queue a rebuild and read its progress. The rebuild itself runs as the `refreshSearchIndex` background task described by [Jobs](/admin-api/jobs/).
Both routes require the [ACL](/admin-api/#acl-evaluation) `guild:lookup`.
@@ -61,7 +61,7 @@ A receipt for one queued rebuild.
## Search index refresh progress object
-Progress for one queued rebuild. The object has two shapes, selected by `status`.
+Progress for one queued rebuild. The object shape is selected by `status`.
### Not found structure
diff --git a/fluxer_docs/src/content/docs/admin-api/users.mdx b/fluxer_docs/src/content/docs/admin-api/users.mdx
index 27bd8b388..fddd7f4f0 100644
--- a/fluxer_docs/src/content/docs/admin-api/users.mdx
+++ b/fluxer_docs/src/content/docs/admin-api/users.mdx
@@ -10,7 +10,7 @@ These routes read and edit any account on the instance. An account has more fiel
Each mutable field group has its own route, its own [ACL](/admin-api/#acl-registry), and its own [audit action](/admin-api/#audit-actions), so there is no combined update.
-A read records no audit entry and reads no audit reason. [List user sessions](#list-user-sessions) and [List user WebAuthn credentials](#list-user-webauthn-credentials) are the two exceptions. `POST /v1/admin/users/{user_id}/avatar-block` addresses a user path but belongs to [Blocklists](/admin-api/blocklists/#block-a-users-current-avatar).
+A read records no audit entry and reads no audit reason. [List user sessions](#list-user-sessions) and [List user WebAuthn credentials](#list-user-webauthn-credentials) are the exceptions. `POST /v1/admin/users/{user_id}/avatar-block` addresses a user path but belongs to [Blocklists](/admin-api/blocklists/#block-a-users-current-avatar).
:::caution[A missing PII ACL nulls the protected fields]
The read still succeeds, so a null field is no evidence that the account has none.
@@ -20,7 +20,7 @@ The read still succeeds, so a null field is no evidence that the account has non
The complete administrative view of one account. It has every stored flag, the private lifecycle fields, and the contact and network fields that the public [user object](/http-api/users/#user-object) never exposes.
-When the caller lacks the matching ACL, Fluxer redacts three field groups and still returns every key, so the object shape is identical for every caller.
+When the caller lacks the matching ACL, Fluxer redacts field groups and still returns every key, so the object shape is identical for every caller.
### Structure
@@ -512,7 +512,7 @@ A `q` value that is entirely digits also resolves that exact account ID and plac
Returns the account the request authenticated as, wrapped in a `user` member. Requires `admin:authenticate`.
-Every Admin credential holds `admin:authenticate`, so every account that can reach the Admin API can read this route. `email`, `date_of_birth`, and the three IP fields are returned unredacted whatever the acting credential holds. `acls` reports the ACL set stored on the account. For an Admin API key credential that set can be wider than what the key itself can use.
+Every Admin credential holds `admin:authenticate`, so every account that can reach the Admin API can read this route. `email`, `date_of_birth`, and the IP fields are returned unredacted whatever the acting credential holds. `acls` reports the ACL set stored on the account. For an Admin API key credential that set can be wider than what the key itself can use.
### Response body
@@ -1463,7 +1463,7 @@ Fluxer cancels a Stripe subscription on the account without proration and refund
The account holder is emailed the deadline and the supplied `public_reason` when the account has an email address.
-For every reason code other than `USER_REQUESTED`, two more enforcement passes run. The first adds the account's email address to the email blocklist and marks its last active address, authorised addresses, live session addresses, and session tombstone addresses as suspicious IPs.
+For every reason code other than `USER_REQUESTED`, further enforcement passes run. The first adds the account's email address to the email blocklist and marks its last active address, authorised addresses, live session addresses, and session tombstone addresses as suspicious IPs.
The second resolves every pending report against the account, in pages of 100, and notifies each reporter through the ordinary [report](/admin-api/reports/) path. It runs only on an instance with a report search backend. Both passes log a failure and continue, so the request still succeeds.
@@ -1568,7 +1568,7 @@ The response has no paging state. Derive the next cursor from the last returned
Lists the direct message channels the account has taken part in. Requires `user:list:dm_channels`. The read does not open, reopen, or acknowledge a channel for the target account.
-:::note[The two channel classes are separate reads]
+:::note[The channel classes are separate reads]
`type` selects which channel class is returned. The default returns one-to-one direct message channels, and group direct messages need a second request with `type=group_dm`.
:::
@@ -1664,7 +1664,7 @@ Lists the friends, friend requests, and blocked accounts of the account, split i
| outgoing_requests1 | array[[Admin relationship](#admin-relationship-object) object] | The friend requests the account has sent |
| blocked1 | array[[Admin relationship](#admin-relationship-object) object] | The accounts this account has blocked |
-1 A stored [relationship type](/http-api/users/relationships/#relationship-types) outside the four categories is dropped from the response
+1 A stored [relationship type](/http-api/users/relationships/#relationship-types) outside the categories is dropped from the response
The operation accepts no cursor, category filter, or limit, and returns every category in full.
@@ -1808,7 +1808,7 @@ The `X-Audit-Log-Reason` header value is stored on the recorded entry.
1 Reachable only with `user:view:ip`, because no lookup is attempted without it. The audit entry is written after the lookups, so a request that fails this way records nothing
-Without `user:view:ip` the two derived fields are null and `client_ip` is the literal string `[redacted]`, so a redacted address is distinguishable from a failed lookup. With it, a partial reverse DNS or location failure yields null for that field alone.
+Without `user:view:ip` the derived fields are null and `client_ip` is the literal string `[redacted]`, so a redacted address is distinguishable from a failed lookup. With it, a partial reverse DNS or location failure yields null for that field alone.
### Side effects
diff --git a/fluxer_docs/src/content/docs/admin-api/voice.mdx b/fluxer_docs/src/content/docs/admin-api/voice.mdx
index d2b04100f..452c706c1 100644
--- a/fluxer_docs/src/content/docs/admin-api/voice.mdx
+++ b/fluxer_docs/src/content/docs/admin-api/voice.mdx
@@ -14,7 +14,7 @@ No operation here addresses a live session, a participant, or a track. [Get voic
Every write takes effect once each API node reloads its topology. No write moves, disconnects, or re-places a live session.
:::
-:::caution[Each write runs in three separate steps]
+:::caution[Each write runs in separate steps]
A write stores the record, notifies every node to reload, then records the audit entry with the audit reason. A failure at the second or third step answers 500 with the record already stored, so read the record back before retrying.
:::
@@ -32,13 +32,13 @@ A participant is one voice connection, identified as `user_{user_id}_{connection
## Placement eligibility
-Four stored fields decide whether a region or a server can be chosen for one placement. Fluxer evaluates the four identically on both records, and it also skips a server whenever `is_active` is false. It resolves a region first, then a server inside it, so a caller admitted to a region whose servers all refuse it cannot use that region.
+The stored fields below decide whether a region or a server can be chosen for one placement. Fluxer evaluates them identically on both records, and it also skips a server whenever `is_active` is false. It resolves a region first, then a server inside it, so a caller admitted to a region whose servers all refuse it cannot use that region.
-`allowed_user_ids` is evaluated first and on its own. A non-empty list admits only the accounts it names and refuses every other caller whatever the remaining three fields say. An empty list gates nothing.
+`allowed_user_ids` is evaluated first and on its own. A non-empty list admits only the accounts it names and refuses every other caller whatever the remaining fields say. An empty list gates nothing.
`vip_only`, `required_guild_features`, and `allowed_guild_ids` are guild gates. When none of the three is set, the record admits every caller that passed the user gate, including a private call, which has no guild. When any of the three is set, the record admits only a placement that has a guild. A [call](/http-api/calls/#modify-call-region) or a group direct message can then never use that region or server.
-With a guild present, a guild named by `allowed_guild_ids` is admitted at once and the other two gates are not checked. Otherwise `vip_only` requires the guild to hold the `VIP_VOICE` [guild feature](/http-api/guilds/#guild-features), and `required_guild_features` requires the guild to hold at least one of the features it names. A guild holding none of them is refused.
+With a guild present, a guild named by `allowed_guild_ids` is admitted at once and the other gates are not checked. Otherwise `vip_only` requires the guild to hold the `VIP_VOICE` [guild feature](/http-api/guilds/#guild-features), and `required_guild_features` requires the guild to hold at least one of the features it names. A guild holding none of them is refused.
## Admin voice region object
@@ -58,7 +58,7 @@ The operator supplies `id` on creation, and it is the primary key. A channel sto
| is_default1 | boolean | Whether automatic placement falls back to this region |
| vip_only2 | boolean | Whether the guild has to hold `VIP_VOICE` |
| required_guild_features2 | array[string] | [Guild features](/http-api/guilds/#guild-features) that admit a guild, any one of which is enough (max 100) |
-| allowed_guild_ids23 | array[snowflake] | The guilds admitted without checking the other two guild gates (max 1000) |
+| allowed_guild_ids23 | array[snowflake] | The guilds admitted without checking the other guild gates (max 1000) |
| allowed_user_ids3 | array[snowflake] | The accounts allowed to use the region at all (max 1000) |
| created_at | ?ISO8601 timestamp | Time the region record was created, or null when the stored row has none |
| updated_at | ?ISO8601 timestamp | Time the region record last changed, or null when the stored row has none |
@@ -114,7 +114,7 @@ A server can also have a soft connection limit, described under [soft connection
| soft_connection_limit5 | ?integer | The count at which placement starts preferring another server, or null when the server has no limit (1-2147483647) |
| vip_only | boolean | Whether the guild has to hold `VIP_VOICE` |
| required_guild_features | array[string] | [Guild features](/http-api/guilds/#guild-features) that admit a guild, any one of which is enough (max 100) |
-| allowed_guild_ids4 | array[snowflake] | The guilds admitted without checking the other two guild gates (max 1000) |
+| allowed_guild_ids4 | array[snowflake] | The guilds admitted without checking the other guild gates (max 1000) |
| allowed_user_ids4 | array[snowflake] | The accounts allowed to use the server at all (max 1000) |
| created_at | ?ISO8601 timestamp | Time the server record was created, or null when the stored row has none |
| updated_at | ?ISO8601 timestamp | Time the server record last changed, or null when the stored row has none |
@@ -253,7 +253,7 @@ Stores a region and returns it. Requires `voice:region:create`.
| is_default? | boolean | Whether automatic placement falls back to this region (default false) |
| vip_only? | boolean | Whether the guild has to hold `VIP_VOICE` (default false) |
| required_guild_features?2 | array[string] | [Guild features](/http-api/guilds/#guild-features) that admit a guild (max 100 items, default empty) |
-| allowed_guild_ids? | array[snowflake] | The guilds admitted without checking the other two guild gates (max 1000, default empty) |
+| allowed_guild_ids? | array[snowflake] | The guilds admitted without checking the other guild gates (max 1000, default empty) |
| allowed_user_ids? | array[snowflake] | The accounts allowed to use the region at all (max 1000, default empty) |
1 The identifier is not checked for collision. Reusing the identifier of an existing region overwrites that record in full, resets its creation time to now, and replaces every one of its collections
@@ -307,7 +307,7 @@ Every field is optional and an omitted field is left unchanged. An absent, empty
| is_default? | boolean | Whether automatic placement falls back to this region |
| vip_only? | boolean | Whether the guild has to hold `VIP_VOICE` |
| required_guild_features?1 | array[string] | [Guild features](/http-api/guilds/#guild-features) that admit a guild (max 100 items of 1-64 characters each) |
-| allowed_guild_ids?1 | array[snowflake] | The guilds admitted without checking the other two guild gates (max 1000) |
+| allowed_guild_ids?1 | array[snowflake] | The guilds admitted without checking the other guild gates (max 1000) |
| allowed_user_ids?1 | array[snowflake] | The accounts allowed to use the region at all (max 1000) |
1 A supplied collection replaces the stored collection outright, so removing one entry means sending the complete remaining set and clearing a collection means sending an empty array
@@ -465,7 +465,7 @@ Registers a voice server in a region and returns it. Requires `voice:server:crea
| soft_connection_limit?4 | ?integer | The count at which placement starts preferring another server, or null for no limit (1-2147483647, default null) |
| vip_only? | boolean | Whether the guild has to hold `VIP_VOICE` (default false) |
| required_guild_features? | array[string] | [Guild features](/http-api/guilds/#guild-features) that admit a guild (max 100 items of 1-64 characters each, default empty) |
-| allowed_guild_ids? | array[snowflake] | The guilds admitted without checking the other two guild gates (max 1000, default empty) |
+| allowed_guild_ids? | array[snowflake] | The guilds admitted without checking the other guild gates (max 1000, default empty) |
| allowed_user_ids? | array[snowflake] | The accounts allowed to use the server at all (max 1000, default empty) |
1 The pair of region and server identifier is not checked for collision. Reusing an existing pair overwrites that record in full, resets its creation time to now, and replaces both stored credentials
@@ -530,7 +530,7 @@ Every field is optional and an omitted field is left unchanged. An absent, empty
| soft_connection_limit?4 | ?integer | The count at which placement starts preferring another server, or null for no limit (1-2147483647) |
| vip_only? | boolean | Whether the guild has to hold `VIP_VOICE` |
| required_guild_features?3 | array[string] | [Guild features](/http-api/guilds/#guild-features) that admit a guild (max 100 items of 1-64 characters each) |
-| allowed_guild_ids?3 | array[snowflake] | The guilds admitted without checking the other two guild gates (max 1000) |
+| allowed_guild_ids?3 | array[snowflake] | The guilds admitted without checking the other guild gates (max 1000) |
| allowed_user_ids?3 | array[snowflake] | The accounts allowed to use the server at all (max 1000) |
1 An omitted credential is left unchanged and a supplied one replaces it. An empty string fails the length bound, so a credential can be replaced but never cleared
diff --git a/fluxer_docs/src/content/docs/authentication.md b/fluxer_docs/src/content/docs/authentication.md
index b186a2e36..06fcc65ea 100644
--- a/fluxer_docs/src/content/docs/authentication.md
+++ b/fluxer_docs/src/content/docs/authentication.md
@@ -4,7 +4,7 @@ title: Authentication
description: Credential syntax, token formats, authorisation outcomes, and sudo mode.
---
-An authenticated request has one credential in the `Authorization` header. Fluxer accepts four kinds. A client acting for a person sends a user session token, an application's bot sends a bot token, a client acting on a user's behalf under OAuth2 sends an access token, and the [Admin API](/admin-api/) takes an Admin API key. The kind decides who Fluxer treats as the caller and which [authorisation policy](#authorisation-outcomes) the matched operation applies.
+An authenticated request has one credential in the `Authorization` header. Fluxer accepts these kinds. A client acting for a person sends a user session token, an application's bot sends a bot token, a client acting on a user's behalf under OAuth2 sends an access token, and the [Admin API](/admin-api/) takes an Admin API key. The kind decides who Fluxer treats as the caller and which [authorisation policy](#authorisation-outcomes) the matched operation applies.
Fluxer returns every failure named here in the standard [error response](/http-api/#error-response) envelope. The operations that issue and revoke credentials belong to the [Authentication HTTP API](/http-api/authentication/) and the [OAuth2 HTTP API](/http-api/oauth2/).
@@ -100,7 +100,7 @@ The `Authorization` header holds a single credential. A [sudo mode](#sudo-mode)
A bot token is the owning application's [snowflake](/snowflakes/), a single full stop, and a secret. It is valid only while the application has an active bot user and the secret is current.
-The Gateway accepts a bot token in [Identify](/gateway/commands/#identify), and so do [`GET /v1/gateway/bot`](/http-api/gateway/#get-gateway-information) and [`GET /v1/applications/@me`](/http-api/applications/#get-bot-application). Those two operations match the scheme prefix without regard to case. `GET /v1/applications/@me` requires the `Bot` prefix and returns 401 `INVALID_TOKEN` for anything else.
+The Gateway accepts a bot token in [Identify](/gateway/commands/#identify), and so do [`GET /v1/gateway/bot`](/http-api/gateway/#get-gateway-information) and [`GET /v1/applications/@me`](/http-api/applications/#get-bot-application). Those operations match the scheme prefix without regard to case. `GET /v1/applications/@me` requires the `Bot` prefix and returns 401 `INVALID_TOKEN` for anything else.
A bot cannot use an operation restricted to ordinary user accounts, and such an operation returns 403 `ACCESS_DENIED`. An operation in [Authentication](/http-api/authentication/) that resolves an account from its request body or token, such as login, password recovery, email verification, email revert, and IP authorisation, returns 403 `BOT_USER_AUTH_ENDPOINT_ACCESS_DENIED` when that account is a bot.
@@ -134,7 +134,7 @@ On every Admin request the resolved user must hold the `admin:authenticate` ACL
## Authorisation outcomes
-An operation that requires a credential declares one of the four authorisation policies:
+An operation that requires a credential declares one of the authorisation policies:
- A user operation requires a resolved user and rejects an OAuth2 bearer credential it has not opted into. A user-only operation rejects a bot account as well.
- A bot operation accepts a bot token, which resolves the application's bot account as the request identity.
@@ -201,7 +201,7 @@ An unknown, expired, revoked, or malformed credential returns 401 `UNAUTHORIZED`
Fluxer records a malformed header and a credential that resolves nothing against the originating address. An Admin API key presented outside `/v1/admin` records nothing.
-Two triggers ban an address. Fluxer bans it on the first crossing of the distinct rejected token threshold inside the tracking window. A failure score over its threshold bans the address only after the score crosses that threshold in several separate windows. The window and both thresholds are instance configuration. Fluxer never applies an automatic ban to an address it classifies as mobile. A banned address is refused before the operation runs, as [Errors](/http-api/errors/) sets out.
+The triggers below ban an address. Fluxer bans it on the first crossing of the distinct rejected token threshold inside the tracking window. A failure score over its threshold bans the address only after the score crosses that threshold in several separate windows. The window and both thresholds are instance configuration. Fluxer never applies an automatic ban to an address it classifies as mobile. A banned address is refused before the operation runs, as [Errors](/http-api/errors/) sets out.
## Sudo mode
diff --git a/fluxer_docs/src/content/docs/conventions.md b/fluxer_docs/src/content/docs/conventions.md
index d5f81b916..5d4ebab92 100644
--- a/fluxer_docs/src/content/docs/conventions.md
+++ b/fluxer_docs/src/content/docs/conventions.md
@@ -136,7 +136,7 @@ A `json` example shows one valid or representative wire value, and a `text` exam
Inside inline code, angle brackets mark descriptive placeholder text standing for a real value, as in `Bot ` or `attachment://`. An ellipsis inside a JSON string value leaves out content the example does not need to show. Where inline code quotes an exact response body, the brackets are part of the literal value.
-A note states a consequence or a relationship to another rule. A caution states a detail a client would otherwise get wrong. That covers a contract that breaks the opposite assumption, a value the client must keep confidential, and an effect that cannot be undone. A danger is reserved for an outcome that destroys stored data, files an external report, revokes every credential on an account, or discloses a credential no later operation can return. All three are binding.
+A note states a consequence or a relationship to another rule. A caution states a detail a client would otherwise get wrong. That covers a contract that breaks the opposite assumption, a value the client must keep confidential, and an effect that cannot be undone. A danger is reserved for an outcome that destroys stored data, files an external report, revokes every credential on an account, or discloses a credential no later operation can return. All are binding.
## Independent protocol surfaces
diff --git a/fluxer_docs/src/content/docs/gateway/event-filtering.md b/fluxer_docs/src/content/docs/gateway/event-filtering.md
index 35df09113..fc928dca7 100644
--- a/fluxer_docs/src/content/docs/gateway/event-filtering.md
+++ b/fluxer_docs/src/content/docs/gateway/event-filtering.md
@@ -1,14 +1,14 @@
---
# SPDX-License-Identifier: AGPL-3.0-or-later
title: Event filtering
-description: The four gates a Dispatch passes before it reaches a socket, and the two client controls.
+description: The gates a Dispatch passes before it reaches a socket, and the client controls.
---
-A [Dispatch](/gateway/events/) is one event Fluxer sends to a connected client. Each one passes four independent gates on its way to a socket, and a client shapes its traffic with [Lazy Request](/gateway/commands/#lazy-request) subscriptions and the [Identify](/gateway/commands/#identify) `ignored_events` list.
+A [Dispatch](/gateway/events/) is one event Fluxer sends to a connected client. Each one passes independent gates on its way to a socket, and a client shapes its traffic with [Lazy Request](/gateway/commands/#lazy-request) subscriptions and the [Identify](/gateway/commands/#identify) `ignored_events` list.
-Fluxer has no `intents` field, no intent close code, and no privileged-intent approval. A client ported from a protocol that uses intents replaces its intent mask with those two mechanisms.
+Fluxer has no `intents` field, no intent close code, and no privileged-intent approval. A client ported from a protocol that uses intents replaces its intent mask with those mechanisms.
-## The four gates
+## The gates
Fluxer evaluates a guild-scoped Dispatch against these gates in order.
@@ -41,7 +41,7 @@ The guild resolves each event to one of these recipient sets.
Every one of those sets excludes a session that has not yet received the guild's initial state.
-Channel visibility is `VIEW_CHANNEL` on the channel, plus two extensions. A category is visible when at least one of its children is visible. A user with a live voice connection in a channel keeps virtual access to it whenever the channel would otherwise stop being visible. That covers a role or overwrite change removing `VIEW_CHANNEL`, and a move into a channel the user cannot view. Virtual access is keyed by user, so it applies to every session of that user. It is dropped when the user's voice connection to the channel ends.
+Channel visibility is `VIEW_CHANNEL` on the channel, plus extensions. A category is visible when at least one of its children is visible. A user with a live voice connection in a channel keeps virtual access to it whenever the channel would otherwise stop being visible. That covers a role or overwrite change removing `VIEW_CHANNEL`, and a move into a channel the user cannot view. Virtual access is keyed by user, so it applies to every session of that user. It is dropped when the user's voice connection to the channel ends.
Message access is `READ_MESSAGE_HISTORY` on the channel. Without that permission a session still receives events for messages newer than the guild's message history cutoff. A guild that sets no cutoff offers no such fallback, so a session without `READ_MESSAGE_HISTORY` receives none of the message-access filtered events there.
@@ -102,7 +102,7 @@ The override applies to every session, including a bot session. A bot suppresses
A session that no longer shares a viewable channel with the subject is dropped from that subject's subscriber set, so a client that regains access MUST resend `members` to restore delivery. Each `members` array replaces the session's previous subscription set for that guild.
-A session holds a presence back in two cases. It holds every presence that arrives before [Ready](/gateway/events/#ready), and releases the queue once it has dispatched Ready. A held presence whose subject already appears in the Ready `presences` array is dropped, and the session sends the rest in one burst. When Ready has not been dispatched within 10,000 milliseconds of session start, a fallback timer releases the queue. After that the session holds a guild presence whose `guild_id` names a guild it is not connected to, and an account-scoped presence for a user that is neither a friend nor a recipient of a group direct message it belongs to.
+A session holds a presence back in the cases below. It holds every presence that arrives before [Ready](/gateway/events/#ready), and releases the queue once it has dispatched Ready. A held presence whose subject already appears in the Ready `presences` array is dropped, and the session sends the rest in one burst. When Ready has not been dispatched within 10,000 milliseconds of session start, a fallback timer releases the queue. After that the session holds a guild presence whose `guild_id` names a guild it is not connected to, and an account-scoped presence for a user that is neither a friend nor a recipient of a group direct message it belongs to.
A bot session holds no friend or group direct message presence subscriptions, so a bot receives a presence through this guild path alone.
diff --git a/fluxer_docs/src/content/docs/gateway/events.md b/fluxer_docs/src/content/docs/gateway/events.md
index a76bef283..3a4ae7f70 100644
--- a/fluxer_docs/src/content/docs/gateway/events.md
+++ b/fluxer_docs/src/content/docs/gateway/events.md
@@ -34,7 +34,7 @@ Most guild-scoped Dispatches have a `guild_id` string. [Guild Create](#guild-cre
The originating session is excluded from a Dispatch only for [Message Reaction Add](#message-reaction-add) and [Message Reaction Remove](#message-reaction-remove) in a guild channel, and only when the request supplied a `session_id`. That field is removed from the payload. The same field on a direct message or group direct message reaction is forwarded to every recipient unchanged and excludes nobody. The actor that issues any other mutation receives the resulting Dispatch like every other eligible session.
-A Dispatch is buffered for [Resume](/gateway/commands/#resume) replay unless it is [Guild Sync](#guild-sync), [Guild Member List Update](#guild-member-list-update), or [Guild Members Chunk](#guild-members-chunk). Those three are delivered live and never retained. A single oversized Dispatch is delivered but not retained, as [Limits and rate limits](/gateway/limits-and-rate-limits/#replay-and-backpressure) describes. [Ready](#ready) and the guild burst that follows it for a bot session are also sent outside the replay buffer. The initial [Call Create](#call-create) events are retained like any other Dispatch and are replayed on Resume.
+A Dispatch is buffered for [Resume](/gateway/commands/#resume) replay unless it is [Guild Sync](#guild-sync), [Guild Member List Update](#guild-member-list-update), or [Guild Members Chunk](#guild-members-chunk). Those are delivered live and never retained. A single oversized Dispatch is delivered but not retained, as [Limits and rate limits](/gateway/limits-and-rate-limits/#replay-and-backpressure) describes. [Ready](#ready) and the guild burst that follows it for a bot session are also sent outside the replay buffer. The initial [Call Create](#call-create) events are retained like any other Dispatch and are replayed on Resume.
## Dispatch events
@@ -618,7 +618,7 @@ A group whose count is `0` is omitted. The `offline` group is also omitted once
#### Member list item object
-Each item has exactly one of the two fields.
+Each item has exactly one of the fields.
| Field | Type | Description |
| --- | --- | --- |
@@ -809,7 +809,7 @@ In a guild channel the session named by the request's `session_id` is excluded a
1 Present only on the [Message Reaction Add](#message-reaction-add) that creates the first reaction with that emoji on the message. An addition to an emoji that already has a reactor, a [Message Reaction Remove](#message-reaction-remove), and a [Message Reaction Remove Emoji](#message-reaction-remove-emoji) omit the field
-Neither `id` nor `animated` is ever null. A Unicode reaction omits both, so a client distinguishes the two forms by the presence of `id`. A client MUST NOT read an absent `animated` as `false`.
+Neither `id` nor `animated` is ever null. A Unicode reaction omits both, so a client distinguishes the forms by the presence of `id`. A client MUST NOT read an absent `animated` as `false`.
### MESSAGE_REACTION_ADD_MANY
@@ -997,7 +997,7 @@ A private channel call began, or became visible in the session's initial state.
2 Present only when the session pulled the call's state for itself
-A session pulls the call's state for itself in two cases. The first is the Call Create it receives shortly after [Ready](#ready) for a private channel that already has a call. The second is the Call Create that reattaches the session to a call it lost, whether or not that loss produced a [Call Delete](#call-delete).
+A session pulls the call's state for itself in the cases below. The first is the Call Create it receives shortly after [Ready](#ready) for a private channel that already has a call. The second is the Call Create that reattaches the session to a call it lost, whether or not that loss produced a [Call Delete](#call-delete).
Recipients are every recipient of the channel, whether or not they joined the call. The same set receives [Call Update](#call-update) and [Call Delete](#call-delete).
@@ -1067,6 +1067,6 @@ A channel the session cannot view, and a channel on which it lacks `VIEW_CHANNEL
Every resource object named on this page has the representation defined by the [HTTP API](/http-api/). A Dispatch payload with a resource object has the same fields, with the guild-scoped events adding `guild_id` and the message and reaction events adding `member`.
-Two reductions are specific to the Gateway and appear nowhere in the HTTP API. [Ready](#ready) strips `user` from each relationship and from each guild member and moves those accounts into its `users` array. The `member` added to a message event has its own `user` removed, and the account is in the message's `author`. A client MUST resolve those accounts from the surrounding payload.
+The reductions below are specific to the Gateway and appear nowhere in the HTTP API. [Ready](#ready) strips `user` from each relationship and from each guild member and moves those accounts into its `users` array. The `member` added to a message event has its own `user` removed, and the account is in the message's `author`. A client MUST resolve those accounts from the surrounding payload.
Every Dispatch payload also drops the fields the Gateway keeps for its own indexing: `recipient_ids`, `role_index`, `channel_index`, `member_role_index`, `role_perms_cache`, and `overwrite_perms_cache`.
diff --git a/fluxer_docs/src/content/docs/gateway/limits-and-rate-limits.md b/fluxer_docs/src/content/docs/gateway/limits-and-rate-limits.md
index ae13aeb24..1584227fd 100644
--- a/fluxer_docs/src/content/docs/gateway/limits-and-rate-limits.md
+++ b/fluxer_docs/src/content/docs/gateway/limits-and-rate-limits.md
@@ -34,11 +34,11 @@ The Gateway refuses a session start for draining, capacity, paused starts, the r
## Session start limit
-[`GET /v1/gateway/bot`](/http-api/gateway/#get-gateway-information) returns a [session start limit](/http-api/gateway/#session-start-limit-object) object for client compatibility. Its four values are constants. Admission is bounded by the source IP Identify budget, the per-user session count, the node's concurrent session-start bucket, and the rollout percentage.
+[`GET /v1/gateway/bot`](/http-api/gateway/#get-gateway-information) returns a [session start limit](/http-api/gateway/#session-start-limit-object) object for client compatibility. Its values are constants. Admission is bounded by the source IP Identify budget, the per-user session count, the node's concurrent session-start bucket, and the rollout percentage.
## Connection and command rate limits
-A Gateway node running with `FLUXER_DISABLE_RATE_LIMITS` set to `1`, `true`, or `TRUE` disables nine budgets together:
+A Gateway node running with `FLUXER_DISABLE_RATE_LIMITS` set to `1`, `true`, or `TRUE` disables these budgets together:
- Connection payload budget
- Session payload budget
@@ -54,7 +54,7 @@ The figures below are the enforced defaults.
One WebSocket accepts 600 client payloads in a rolling 60-second window. One authenticated session accepts 600 client payloads in each fixed 60-second bucket. One source IP address accepts 6,000 client payloads in each fixed 60-second bucket. Exceeding any of these budgets closes the current connection with `4008` and reason `Rate limited`.
-Fluxer evaluates the three payload budgets before any command-specific budget. The session budget is skipped while the connection is unauthenticated.
+Fluxer evaluates the payload budgets before any command-specific budget. The session budget is skipped while the connection is unauthenticated.
One source IP address holds 256 concurrent Gateway WebSockets. A further connection closes with `4008` and reason `Too many connections` before Hello is sent.
diff --git a/fluxer_docs/src/content/docs/gateway/opcodes-and-close-codes.md b/fluxer_docs/src/content/docs/gateway/opcodes-and-close-codes.md
index cbd7fbd2c..b7ca2b97b 100644
--- a/fluxer_docs/src/content/docs/gateway/opcodes-and-close-codes.md
+++ b/fluxer_docs/src/content/docs/gateway/opcodes-and-close-codes.md
@@ -134,7 +134,7 @@ Close `4007` follows one rule for [Heartbeat](/gateway/commands/#heartbeat) and
Heartbeat tests the value's type alone, and only once a session is attached. Before Identify or Resume attaches one, the Gateway accepts every `d` and answers with Opcode 11. With a session attached, the Gateway accepts a `d` that is `null` or any integer, and every other value closes with `4007`. A sequence below the acknowledged sequence leaves that bound unchanged, and any other integer sets it and trims the replay buffer. A heartbeat that arrives in the short window between the session process ending and the socket noticing also closes with `4007`.
-Resume tests two bounds and its `seq` must clear both.
+Resume tests the bounds below and its `seq` must clear both.
- The current sequence. A `seq` above the last sequence the session dispatched closes with `4007`.
- The acknowledged sequence. A `seq` below the last acknowledged sequence closes with `4007`. A heartbeat with a higher sequence moves this bound.
diff --git a/fluxer_docs/src/content/docs/gateway/overview.md b/fluxer_docs/src/content/docs/gateway/overview.md
index fc6b2917d..c27ede154 100644
--- a/fluxer_docs/src/content/docs/gateway/overview.md
+++ b/fluxer_docs/src/content/docs/gateway/overview.md
@@ -116,7 +116,7 @@ A connection cannot change its compression stream after the upgrade. Changing it
## Signalling state machine
-A connection moves through five states: Opening, Unauthenticated, Starting, Replaying, and Ready. The tables below give every event a state accepts, the action it triggers, and the state it lands in. Heartbeat is accepted in every open state, and Closed is terminal for that WebSocket.
+A connection moves through these states: Opening, Unauthenticated, Starting, Replaying, and Ready. The tables below give every event a state accepts, the action it triggers, and the state it lands in. Heartbeat is accepted in every open state, and Closed is terminal for that WebSocket.
### Opening
@@ -248,7 +248,7 @@ Opcode 6 supplies the original token, the Ready `session_id`, and the last proce
A successful Resume replays every retained Dispatch above `seq` in order and ends with [Resumed](/gateway/events/#resumed).
-All three fields are required. A missing field, a `token` or `session_id` that is not a string, or a `seq` that is not an integer closes with `4002` and reason `Invalid resume payload`.
+All fields are required. A missing field, a `token` or `session_id` that is not a string, or a `seq` that is not an integer closes with `4002` and reason `Invalid resume payload`.
The Gateway retains a disconnected session for 60,000 ms. An accepted `seq` is no greater than the session's current sequence and no less than the sequence the session has already acknowledged. A `seq` outside either bound closes with `4007` and reason `Invalid sequence`.
diff --git a/fluxer_docs/src/content/docs/http-api/applications.mdx b/fluxer_docs/src/content/docs/http-api/applications.mdx
index d54147445..ca99d95a0 100644
--- a/fluxer_docs/src/content/docs/http-api/applications.mdx
+++ b/fluxer_docs/src/content/docs/http-api/applications.mdx
@@ -12,7 +12,7 @@ An application is an OAuth2 client owned by one user account, and every bot acco
Every route except [Get public application](#get-public-application) requires a credential. A user-only route rejects a bot token and an OAuth2 bearer with 403 `ACCESS_DENIED`. Fluxer accepts an account with an outstanding required action everywhere here.
-The three sudo-gated routes read `X-Fluxer-Sudo-Mode-JWT`. A valid proof for the authenticated account satisfies [sudo mode](/http-api/users/mfa/#sudo-mode) on its own, and Fluxer echoes it back in the response header. Fluxer issues no token to an account with no authenticator, so that account proves sudo mode with `password` in the body.
+The sudo-gated routes read `X-Fluxer-Sudo-Mode-JWT`. A valid proof for the authenticated account satisfies [sudo mode](/http-api/users/mfa/#sudo-mode) on its own, and Fluxer echoes it back in the response header. Fluxer issues no token to an account with no authenticator, so that account proves sudo mode with `password` in the body.
:::caution[Credentials are shown once]
A client secret is returned only by [Create application](#create-application) and [Reset client secret](#reset-client-secret), and a [bot token](/authentication/#token-formats) only by [Create application](#create-application) and [Reset bot token](#reset-bot-token). Both are stored as one-way hashes.
@@ -83,7 +83,7 @@ The bot account an application owns. [Create application](#create-application) c
4 Declared optional by the schema and always emitted, except on the [public application](#public-application-object) object, which omits both. `authenticator_types` is an empty array when the account has no authenticator, and `mfa_enabled` is true exactly when that array is not empty
-5 The bitfield is the account's [public user flags](/http-api/users/#public-user-flags), and [Update bot profile](#update-bot-profile) sets only the two [bot flags](#bot-flags) in it
+5 The bitfield is the account's [public user flags](/http-api/users/#public-user-flags), and [Update bot profile](#update-bot-profile) sets only the [bot flags](#bot-flags) in it
:::note[Bot token format]
A bot token is the application ID, a full stop, and an opaque secret of 32 random bytes encoded as base64url.
@@ -442,7 +442,7 @@ Updates the bot account an application owns and returns the resulting [bot profi
4 Rejected with 403 `CONTENT_BLOCKED` when content moderation or the profile substring blocklist blocks the value
-5 Fluxer reads only the two [bot flags](#bot-flags) from the supplied bitfield and sets or clears each to match. It ignores every other bit
+5 Fluxer reads only the [bot flags](#bot-flags) from the supplied bitfield and sets or clears each to match. It ignores every other bit
Fluxer checks the decoded bytes of `avatar` and `banner` against the instance's avatar byte ceiling, which applies to both fields and defaults to 10 MiB. Each image also passes the format allowlist and the animation rules of the asset policy for the field it sets. Pixel dimensions are never checked.
diff --git a/fluxer_docs/src/content/docs/http-api/authentication.mdx b/fluxer_docs/src/content/docs/http-api/authentication.mdx
index f592033f8..35418730c 100644
--- a/fluxer_docs/src/content/docs/http-api/authentication.mdx
+++ b/fluxer_docs/src/content/docs/http-api/authentication.mdx
@@ -340,7 +340,7 @@ The returned `authorization_url` is the configured provider authorisation endpoi
1 Fluxer sanitises the value before binding it to the state and discards a value that does not survive, which the [SSO completion response](#sso-completion-response-object) reports as the empty string. Sanitisation keeps the trimmed value only when it begins with a single `/`, is at most 2,048 characters, and contains no carriage return or line feed
-2 The two accepted values are the instance default reported as `redirect_uri` by [get SSO status](#get-sso-status) and the mobile callback `fluxer://auth/sso/callback`, and any other value returns the field code `INVALID_URL_FORMAT`. The accepted value is bound to the state and reused at the token exchange
+2 The accepted values are the instance default reported as `redirect_uri` by [get SSO status](#get-sso-status) and the mobile callback `fluxer://auth/sso/callback`, and any other value returns the field code `INVALID_URL_FORMAT`. The accepted value is bound to the state and reused at the token exchange
### Response
@@ -533,7 +533,7 @@ Login verifies CAPTCHA when CAPTCHA is enabled. It also permits 10 attempts per
An unknown email address and an incorrect password both return the same paired field codes `INVALID_EMAIL_OR_PASSWORD` on `email` and `password`.
:::
-A new client IP address on an account that has neither a second factor nor the app store reviewer flag returns 403 `IP_AUTHORIZATION_REQUIRED`. That error body has `ip_authorization_required` set to true, the `ticket` used by the three IP authorisation operations, the account `email`, and `resend_available_in` set to 30 seconds.
+A new client IP address on an account that has neither a second factor nor the app store reviewer flag returns 403 `IP_AUTHORIZATION_REQUIRED`. That error body has `ip_authorization_required` set to true, the `ticket` used by the IP authorisation operations, the account `email`, and `resend_available_in` set to 30 seconds.
When the instance has disabled new-IP authorisation or sends no email, Fluxer authorises the client IP address silently and the login continues. An account that already has a second factor never enters IP authorisation.
@@ -869,7 +869,7 @@ Password recovery verifies CAPTCHA when CAPTCHA is enabled. Fluxer consumes both
1 An address whose domain has no usable DNS records returns the field code `INVALID_EMAIL_ADDRESS`, while an address that passes DNS validation but belongs to no account returns the ordinary success response
:::note[Account existence is not disclosed]
-An address that resolves to no account produces the same 204 response as an address that resolves to an ordinary account. Only DNS validation failure, the route bucket, the two extra allowances, and an address belonging to a bot account can produce a different outcome.
+An address that resolves to no account produces the same 204 response as an address that resolves to an ordinary account. Only DNS validation failure, the route bucket, the extra allowances, and an address belonging to a bot account can produce a different outcome.
:::
### Response
diff --git a/fluxer_docs/src/content/docs/http-api/billing.mdx b/fluxer_docs/src/content/docs/http-api/billing.mdx
index c8de5448b..dc529d554 100644
--- a/fluxer_docs/src/content/docs/http-api/billing.mdx
+++ b/fluxer_docs/src/content/docs/http-api/billing.mdx
@@ -36,7 +36,7 @@ One absolute URL that completes a billing operation in a browser. [Create subscr
## Localised card preapproval result object
-The state of one card preapproval flow. A localised recurring price is offered only to a card issued in the matching country. Fluxer creates the paid session only after a separate setup mode session has proven the card country. `status` says which of the four variants the object is, and each variant defines its own members.
+The state of one card preapproval flow. A localised recurring price is offered only to a card issued in the matching country. Fluxer creates the paid session only after a separate setup mode session has proven the card country. `status` says which of the variants the object is, and each variant defines its own members.
### Structure
diff --git a/fluxer_docs/src/content/docs/http-api/calls.mdx b/fluxer_docs/src/content/docs/http-api/calls.mdx
index 3f8fecffa..3480bd1d3 100644
--- a/fluxer_docs/src/content/docs/http-api/calls.mdx
+++ b/fluxer_docs/src/content/docs/http-api/calls.mdx
@@ -14,7 +14,7 @@ Every route on this page is user-only. Bot and OAuth2 credentials are rejected.
## Access rules
-Four routes share one boundary. [Get call eligibility](#get-call-eligibility), [Modify call region](#modify-call-region), [Ring call recipients](#ring-call-recipients), and [Stop ringing call recipients](#stop-ringing-call-recipients) each resolve the channel named by the path parameter first, then apply the same three checks in order.
+The routes below share one boundary. [Get call eligibility](#get-call-eligibility), [Modify call region](#modify-call-region), [Ring call recipients](#ring-call-recipients), and [Stop ringing call recipients](#stop-ringing-call-recipients) each resolve the channel named by the path parameter first, then apply the same checks in order.
Fluxer answers 404 `UNKNOWN_CHANNEL` for a channel ID that names no channel. A channel that exists but is neither a direct message nor a group direct message returns 400 `INVALID_CHANNEL_TYPE_FOR_CALL`. That type check runs before the membership check, so any authenticated account can learn that an arbitrary channel exists whenever that channel is not a private channel.
@@ -30,7 +30,7 @@ A private call has no moderator and no permission overwrites. No account other t
Silencing another participant happens in the client. A client MAY mute a participant or change per-participant volume. The client MUST keep each setting local to the listening device, so neither reaches the Gateway or any other participant.
-Two operations on this page name another recipient. [Ring call recipients](#ring-call-recipients) adds named recipients to the ringing set of a call, and [Stop ringing call recipients](#stop-ringing-call-recipients) removes them from it. Any current recipient MAY call either one. Neither changes anything for a recipient who has already connected, and neither writes a voice state.
+The operations below name another recipient. [Ring call recipients](#ring-call-recipients) adds named recipients to the ringing set of a call, and [Stop ringing call recipients](#stop-ringing-call-recipients) removes them from it. Any current recipient MAY call either one. Neither changes anything for a recipient who has already connected, and neither writes a voice state.
## Call eligibility object
diff --git a/fluxer_docs/src/content/docs/http-api/channels.mdx b/fluxer_docs/src/content/docs/http-api/channels.mdx
index 9cf097fdc..ef7f9faee 100644
--- a/fluxer_docs/src/content/docs/http-api/channels.mdx
+++ b/fluxer_docs/src/content/docs/http-api/channels.mdx
@@ -292,7 +292,7 @@ Returns the [RTC region objects](#rtc-region-object) the authenticated user can
- The caller needs the same access as [Get channel](#get-channel).
- The channel must be a guild voice channel.
-Fluxer returns a region only when the caller passes every restriction configured for it and at least one active voice server in that region is also accessible to the caller. The array can be empty. [Placement eligibility](/admin-api/voice/#placement-eligibility) states the four stored fields those restrictions come from.
+Fluxer returns a region only when the caller passes every restriction configured for it and at least one active voice server in that region is also accessible to the caller. The array can be empty. [Placement eligibility](/admin-api/voice/#placement-eligibility) states the stored fields those restrictions come from.
:::note[An unconfigured deployment skips the channel check]
On a deployment that configures no voice topology, a missing channel, an invisible channel, and a non-voice channel all return 200 with `[]`.
@@ -362,7 +362,7 @@ The stored channel type selects the body variant, and a `type` field in the requ
#### Guild channel body
-Every field is optional, and an omitted field preserves its current value. The four guild variants share one field set, and a field the stored type does not own is accepted and discarded.
+Every field is optional, and an omitted field preserves its current value. The guild variants share one field set, and a field the stored type does not own is accepted and discarded.
| Field | Type | Description |
| --- | --- | --- |
diff --git a/fluxer_docs/src/content/docs/http-api/connections.mdx b/fluxer_docs/src/content/docs/http-api/connections.mdx
index a320066e7..fe33992fa 100644
--- a/fluxer_docs/src/content/docs/http-api/connections.mdx
+++ b/fluxer_docs/src/content/docs/http-api/connections.mdx
@@ -6,7 +6,7 @@ description: External account connections, domain ownership proof, Bluesky autho
import RouteHeader from '@/components/RouteHeader.astro';
-A connection links a Fluxer account to an outside identity the account has proved it controls. The two [connection types](#connection-types) are a DNS domain and a Bluesky account authorised through atproto OAuth. A profile renders an account's connections as the `connected_accounts` field of the [full user profile object](/http-api/users/#full-user-profile-object).
+A connection links a Fluxer account to an outside identity the account has proved it controls. The [connection types](#connection-types) are a DNS domain and a Bluesky account authorised through atproto OAuth. A profile renders an account's connections as the `connected_accounts` field of the [full user profile object](/http-api/users/#full-user-profile-object).
Every route that reads or writes a connection is user-only. A bot token is rejected with 403 `ACCESS_DENIED`, and so is an OAuth2 bearer except on [List connections](#list-connections), which accepts a bearer with the `connections` [scope](/http-api/oauth2/#oauth2-scopes). An account with an outstanding required action is rejected with 403 `ACCOUNT_SUSPICIOUS_ACTIVITY`. No connection identifier is a snowflake.
@@ -98,7 +98,7 @@ The payload is ordinary base64url and has the verification token in the clear. T
Fluxer attempts a `domain` proof against the stored identifier exactly as it was submitted. [Initiate connection](#initiate-connection) accepts any string of 1 to 253 characters and applies no syntax check of its own, so a malformed value is accepted and then fails its proof.
-Fluxer first resolves the TXT records of `_fluxer.`. It issues the query in parallel to a fixed set of eight public DNS resolvers, each with a 2000 millisecond timeout and one attempt. The proof succeeds when any resolver returns a record whose concatenated strings equal `fluxer-verification=` exactly. A resolver that fails or times out is treated like an absent record and produces no separate error.
+Fluxer first resolves the TXT records of `_fluxer.`. It issues the query in parallel to a fixed set of public DNS resolvers, each with a 2000 millisecond timeout and one attempt. The proof succeeds when any resolver returns a record whose concatenated strings equal `fluxer-verification=` exactly. A resolver that fails or times out is treated like an absent record and produces no separate error.
When the DNS proof fails, Fluxer issues one HTTPS `GET` to `https:///.well-known/fluxer-verification` with a 5000 millisecond timeout. It follows up to 5 redirects. A sixth redirect fails the proof, as does a redirect response that has no `Location` header. The proof succeeds only when the status of the final response is in the 2xx range and the whitespace-trimmed body equals the token exactly. A response body over the 16384 byte ceiling fails the proof, and a declared `Content-Length` over that ceiling fails it before the body is read.
@@ -402,7 +402,7 @@ Fluxer trims surrounding whitespace, then removes a leading `https://bsky.app/pr
3 The comparison is case-insensitive and runs against the stored connection `name`, so an account whose upstream handle has since changed is not detected as a duplicate here and refreshes its existing connection at the callback
-:::note[Two independent availability gates]
+:::note[Independent availability gates]
This operation requires both a configured Bluesky OAuth client and the instance [service availability](/http-api/instance/#service-availability-object) flag `bluesky_enabled`. A missing client and a cleared flag both produce `BLUESKY_OAUTH_NOT_ENABLED`, and the response does not say which.
:::
@@ -468,7 +468,7 @@ The document is the same for every request. Its URLs are built from the `api_pub
| tos_uri?1 | string | The terms of service document offered to the account holder |
| policy_uri?1 | string | The privacy policy document offered to the account holder |
| redirect_uris | array[string] | The one URL an authorisation server may return to, `/connections/bluesky/callback` on `api_public` |
-| grant_types | array[string] | The two grants the client uses, `authorization_code` and `refresh_token` |
+| grant_types | array[string] | The grants the client uses, `authorization_code` and `refresh_token` |
| response_types | array[string] | The one response type the client accepts, `code` |
| scope | string | The scope requested at authorisation, `atproto` |
| application_type | string | The client type, `web` |
diff --git a/fluxer_docs/src/content/docs/http-api/deployment-availability.md b/fluxer_docs/src/content/docs/http-api/deployment-availability.md
index 9a93e2312..aa4f5f469 100644
--- a/fluxer_docs/src/content/docs/http-api/deployment-availability.md
+++ b/fluxer_docs/src/content/docs/http-api/deployment-availability.md
@@ -54,7 +54,7 @@ Without a provider client, the answer depends on the operation. An operation tha
| POST | /v1/premium/cancel-pending-subscription-change | [Cancel pending subscription change](/http-api/premium/#cancel-pending-subscription-change) |
| POST | /v1/premium/visionary/rejoin | [Rejoin Visionary guild](/http-api/premium/#rejoin-visionary-guild) |
-1 These two are the only `/users/@me` routes a self-hosted deployment does not serve
+1 These are the only `/users/@me` routes a self-hosted deployment does not serve
2 The webhook takes no credential and is authenticated by the provider signature header alone
diff --git a/fluxer_docs/src/content/docs/http-api/donations.mdx b/fluxer_docs/src/content/docs/http-api/donations.mdx
index b831bac5c..074e862bb 100644
--- a/fluxer_docs/src/content/docs/http-api/donations.mdx
+++ b/fluxer_docs/src/content/docs/http-api/donations.mdx
@@ -8,7 +8,7 @@ import RouteHeader from '@/components/RouteHeader.astro';
A donation is a one-off or recurring payment, taken on an externally hosted checkout page. Fluxer tracks it by email address, and it grants no [premium](/http-api/premium/). [Billing](/http-api/billing/) defines the payment provider webhook that completes one.
-None of the three routes here takes a credential, and all three are hosted-only, as [deployment availability](/http-api/deployment-availability/) describes. A valid credential presented anyway keys the rate limit to the account.
+None of the routes here takes a credential, and all are hosted-only, as [deployment availability](/http-api/deployment-availability/) describes. A valid credential presented anyway keys the rate limit to the account.
Fluxer uses a submitted address exactly as written and matches a donor by exact equality, so `Donor@example.com` and `donor@example.com` address two different donors.
diff --git a/fluxer_docs/src/content/docs/http-api/downloads.mdx b/fluxer_docs/src/content/docs/http-api/downloads.mdx
index 316e05a63..a7f180dec 100644
--- a/fluxer_docs/src/content/docs/http-api/downloads.mdx
+++ b/fluxer_docs/src/content/docs/http-api/downloads.mdx
@@ -10,9 +10,9 @@ The download resource serves the desktop application builds a deployment has sto
## Prefix and mounting
-Fluxer registers the routes under `/dl`, and the six desktop routes under `/dl/desktop`. A client builds the URL against `api_client` from the [instance endpoints object](/http-api/instance/#instance-endpoints-object).
+Fluxer registers the routes under `/dl`, and the desktop routes under `/dl/desktop`. A client builds the URL against `api_client` from the [instance endpoints object](/http-api/instance/#instance-endpoints-object).
-The prefix mounts at the root and at `/v1`, so `/v1/dl/desktop/...` resolves for the six routes that name their own segments.
+The prefix mounts at the root and at `/v1`, so `/v1/dl/desktop/...` resolves for the routes that name their own segments.
:::caution[The catch-all has no `/v1` form]
[Download stored object](#download-stored-object) builds its storage key from the raw request path and keeps only a path beginning `/dl`. A request to `/v1/dl/...` returns 404 while the same path without that prefix returns the object.
@@ -22,7 +22,7 @@ Every other route on this reference takes the `/v1` form, and [HTTP API](/http-a
## Methods
-Every route answers `GET`. The two checksum routes, the two artifact routes, and the catch-all also bind `HEAD`. [Get latest desktop version](#get-latest-desktop-version) and [List desktop versions](#list-desktop-versions) bind `GET` alone, and a `HEAD` still reaches them under the [shared rule for HEAD](/http-api/#request-format).
+Every route answers `GET`. The checksum routes, the artifact routes, and the catch-all also bind `HEAD`. [Get latest desktop version](#get-latest-desktop-version) and [List desktop versions](#list-desktop-versions) bind `GET` alone, and a `HEAD` still reaches them under the [shared rule for HEAD](/http-api/#request-format).
A `HEAD` on either JSON route runs the same resolution as the `GET` and returns its status and headers with no body. It can therefore answer 404.
@@ -185,7 +185,7 @@ A release feed filename is any of these:
## Redirects
-Two deployment settings answer a download with 302.
+The deployment settings below answer a download with 302.
A deployment that reports `self_hosted` false on the [instance features object](/http-api/instance/#instance-features-object) and configures a country list for GitHub redirects resolves the caller's country on every `desktop/` artifact request. When a complete and verified release descriptor names the file, a caller in a listed country receives 302 to that GitHub release asset. Every other outcome streams from storage, and every response under this setting has `Cache-Control: private, no-store`.
@@ -199,7 +199,7 @@ The client follows `Location` to read the file. Fluxer produces the redirect bef
Every route accepts `test` as a query parameter. The value `1` or `true`, matched without regard to case, resolves the object against the `desktop-test/` storage prefix. Any other value is read as false.
-On the six desktop routes the flag replaces the prefix outright. On [Download stored object](#download-stored-object) it rewrites a key beginning `desktop/` and leaves any other key unchanged, and a path that already names `desktop-test/` resolves there with no flag at all.
+On the desktop routes the flag replaces the prefix outright. On [Download stored object](#download-stored-object) it rewrites a key beginning `desktop/` and leaves any other key unchanged, and a path that already names `desktop-test/` resolves there with no flag at all.
A `url` and a `checksum_url` built for a request that sent the flag repeat `?test=1`, so a client following either one stays on the test prefix.
@@ -502,10 +502,10 @@ Streams one stored object addressed by its storage key. A release feed file has
| 206 | object bytes | The requested byte range was streamed |
| 302 | empty | The deployment [redirects](#redirects) this download |
| 400 | [error response](/http-api/#error-response) | The `test` value fails its schema and the request returns `INVALID_FORM_BODY` |
-| 404 | `Not Found` | The key is outside the two permitted prefixes, the request has the `/v1` prefix, or storage holds no such object |
+| 404 | `Not Found` | The key is outside the permitted prefixes, the request has the `/v1` prefix, or storage holds no such object |
| 416 | empty | The requested range is unsatisfiable |
-:::caution[The catch-all reaches two prefixes only]
+:::caution[The catch-all reaches the permitted prefixes only]
A key that does not begin `desktop/` or `desktop-test/` returns 404 before Fluxer reads storage.
:::
diff --git a/fluxer_docs/src/content/docs/http-api/errors.md b/fluxer_docs/src/content/docs/http-api/errors.md
index 59f4b5c4c..4bafcabc6 100644
--- a/fluxer_docs/src/content/docs/http-api/errors.md
+++ b/fluxer_docs/src/content/docs/http-api/errors.md
@@ -34,7 +34,7 @@ The error code determines which supplementary members a failure has, and most co
Fluxer answers a field-level failure with 400 and a top-level `errors` array. Each element identifies one failed input field. The [validation error object](/http-api/#validation-error-object) documents the element shape.
-A boundary schema validates one of four request targets: the JSON body, the form body, the query string, and the path parameters. A failure on any of them returns the top-level code `INVALID_FORM_BODY`, and each element has a `code` drawn from the [validation error code registry](#validation-error-code-registry) together with a localised `message`.
+A boundary schema validates one of the request targets: the JSON body, the form body, the query string, and the path parameters. A failure on any of them returns the top-level code `INVALID_FORM_BODY`, and each element has a `code` drawn from the [validation error code registry](#validation-error-code-registry) together with a localised `message`.
An operation can also report against a named field without the boundary schema. That failure returns `INVALID_FORM_BODY` as well. Its element has an enumerated `code` and localised `message` when the failure declares a registry code. Otherwise it has a fixed English `message` written at the failure site and no `code`. Every element `code` a client observes is a registry value.
@@ -62,7 +62,7 @@ The `path` of an element is the dot-joined position of the failed value, so a ne
An empty or whitespace-only body becomes `{}`, so the response reports the fields the schema then finds missing. A body that does not parse as JSON returns 400 `INVALID_FORM_BODY` with one element at path `body` and code `INVALID_FORMAT`.
:::
-Fluxer normalises empty values on all four targets before validation runs. An empty string becomes `null` wherever it appears, including inside an array element. A nested object becomes `null` when it holds no members. It also becomes `null` when every one of its members is `null` after Fluxer has applied the same rule to each of them. The top-level object itself is never replaced, so a request that sends nothing still reaches the schema as an object and fails on the fields the schema requires.
+Fluxer normalises empty values on all targets before validation runs. An empty string becomes `null` wherever it appears, including inside an array element. A nested object becomes `null` when it holds no members. It also becomes `null` when every one of its members is `null` after Fluxer has applied the same rule to each of them. The top-level object itself is never replaced, so a request that sends nothing still reaches the schema as an object and fails on the fields the schema requires.
:::caution[An enumerated validation failure answers 400 alone]
A validation failure whose elements have enumerated codes answers 400 with its elements in `errors`. Its top-level code is `INVALID_FORM_BODY` everywhere except [Modify current user settings](/http-api/users/settings/#modify-current-user-settings). Any other status, retry guidance, or response header from the original failure is dropped.
@@ -116,7 +116,7 @@ Every `4xx` response to a request that resolved no authenticated user contribute
Fluxer records a second signal of weight 1 for a credential that fails to resolve. A request that presents an unrecognised token and is answered 401 contributes both. That signal also records a hash of the credential presented, and Fluxer tracks the number of distinct hashes seen for one IP identity beside the score.
-The score and the credential hashes accumulate inside a fixed window, and the window restarts once it elapses. Two triggers fire an automatic ban. The credential trigger fires the first time the count of distinct rejected credentials reaches its threshold. The score trigger fires only after the score has crossed its threshold in three separate windows, which is the default. Both thresholds depend on the address classification, which is datacentre, anonymising, mobile, or residential. An unclassified address takes the residential thresholds. Fluxer never bans a mobile address automatically.
+The score and the credential hashes accumulate inside a fixed window, and the window restarts once it elapses. The triggers below fire an automatic ban. The credential trigger fires the first time the count of distinct rejected credentials reaches its threshold. The score trigger fires only after the score has crossed its threshold in three separate windows, which is the default. Both thresholds depend on the address classification, which is datacentre, anonymising, mobile, or residential. An unclassified address takes the residential thresholds. Fluxer never bans a mobile address automatically.
:::caution[An automatic ban answers every request for 24 hours]
The window length, both thresholds, and the number of windows the score trigger requires are instance configuration. A tripped ban lasts 24 hours by default. While it holds, Fluxer answers every request from that identity with 403 and the code `GLOBAL_IP_TEMPORARILY_BANNED`.
diff --git a/fluxer_docs/src/content/docs/http-api/experiments.mdx b/fluxer_docs/src/content/docs/http-api/experiments.mdx
index 9b0f540d8..872f57e22 100644
--- a/fluxer_docs/src/content/docs/http-api/experiments.mdx
+++ b/fluxer_docs/src/content/docs/http-api/experiments.mdx
@@ -22,7 +22,7 @@ One resolution of every defined experiment against one account. Every field is p
| poll_jitter_percent | integer | How far to spread the wait around the interval, from 0 through 50 |
| assignments | [assignment map](#assignment-map-object) object | One entry for each experiment the server defines |
-The two polling fields sit on the envelope rather than on any one experiment, because the cadence is a property of the route and not of a rollout. They are the operator's [experiment delivery configuration](/admin-api/instance/#experiment-delivery-configuration-object) read back unchanged, so they hold the same values for every account and every experiment, whatever those experiments resolve to.
+The polling fields sit on the envelope rather than on any one experiment, because the cadence is a property of the route and not of a rollout. They are the operator's [experiment delivery configuration](/admin-api/instance/#experiment-delivery-configuration-object) read back unchanged, so they hold the same values for every account and every experiment, whatever those experiments resolve to.
## Assignment map object
@@ -77,7 +77,7 @@ One resolution of the instance noise suppression rollout against one account. Ev
### Resolution outcomes
-Three outcomes set `user_targeted` to false, and they differ in what else they report.
+The outcomes below set `user_targeted` to false, and they differ in what else they report.
1. The rollout is off. `enabled` is false, `enabled_backends` and `guild_overrides` are empty, and `allow_user_override` and `stereo_enabled` are false.
2. The operator has excluded the caller. `enabled` is true, and every other field is as in the first outcome.
@@ -87,7 +87,7 @@ A caller is drawn either by the operator's allowlist, which sets `source` to `us
A client branches on `user_targeted` rather than on `enabled_backends`, because the third outcome keeps the array populated. A `guild_overrides` entry applies in its guild whether or not the caller was drawn.
-`config_version` reports the stored revision in all three outcomes, the rollout being off included.
+`config_version` reports the stored revision in all outcomes, the rollout being off included.
## Noise suppression guild override object
diff --git a/fluxer_docs/src/content/docs/http-api/gifs.mdx b/fluxer_docs/src/content/docs/http-api/gifs.mdx
index a332dcc73..7099fd5e3 100644
--- a/fluxer_docs/src/content/docs/http-api/gifs.mdx
+++ b/fluxer_docs/src/content/docs/http-api/gifs.mdx
@@ -32,7 +32,7 @@ Every response under `/gifs`, `/tenor`, and `/klipy` has these headers, includin
| X-Fluxer-GIF-Provider-Display-Name1 | string | The provider name a client displays |
| X-Fluxer-GIF-Provider-Attribution-Required1 | string | The literal `true` or `false`, stating whether a client shows the provider attribution mark |
-1 All three are absent together while the instance has bound no provider key, which is the same condition that answers 403 `FEATURE_TEMPORARILY_DISABLED`
+1 All are absent together while the instance has bound no provider key, which is the same condition that answers 403 `FEATURE_TEMPORARILY_DISABLED`
## Result freshness
@@ -126,7 +126,7 @@ To open the category, a client sends `name` as the `q` of [Search GIFs](#search-
## Deprecated vendor paths
-Two vendor-named prefixes serve the same handlers as `/gifs`. Each answers exactly as its successor does and has the same [provider headers](#provider-headers).
+The vendor-named prefixes serve the same handlers as `/gifs`. Each answers exactly as its successor does and has the same [provider headers](#provider-headers).
| Method | Deprecated path | Successor |
| --- | --- | --- |
@@ -142,7 +142,7 @@ Two vendor-named prefixes serve the same handlers as `/gifs`. Each answers exact
| POST | `/v1/klipy/register-share` | [Register a GIF share](#register-a-gif-share) |
:::caution[The vendor prefixes spell trending differently]
-Trending is `/trending` under `/gifs` and `/trending-gifs` under both vendor prefixes. The other paths keep the same last segment on all three prefixes.
+Trending is `/trending` under `/gifs` and `/trending-gifs` under both vendor prefixes. The other paths keep the same last segment on all prefixes.
:::
A response from a deprecated path has these further headers.
diff --git a/fluxer_docs/src/content/docs/http-api/gifts.mdx b/fluxer_docs/src/content/docs/http-api/gifts.mdx
index 66caea391..9d8efc906 100644
--- a/fluxer_docs/src/content/docs/http-api/gifts.mdx
+++ b/fluxer_docs/src/content/docs/http-api/gifts.mdx
@@ -114,7 +114,7 @@ Redeems a gift for the authenticated account and returns 204 with an empty body.
A consumed code receives 400 `GIFT_CODE_ALREADY_REDEEMED`. A code another request is redeeming receives 400 `STRIPE_GIFT_REDEMPTION_IN_PROGRESS`. A code that does not exist or has been revoked receives 404 `UNKNOWN_GIFT_CODE`.
:::note[The refusal order differs from checkout]
-The claimed account, verified email and purchase flag refusals are evaluated before the lifetime refusal here. [Create subscription checkout](/http-api/billing/#create-subscription-checkout) evaluates the same two groups in the opposite order.
+The claimed account, verified email and purchase flag refusals are evaluated before the lifetime refusal here. [Create subscription checkout](/http-api/billing/#create-subscription-checkout) evaluates the same groups in the opposite order.
:::
:::caution[Redemption is single-use per code]
diff --git a/fluxer_docs/src/content/docs/http-api/guild-audit-logs.mdx b/fluxer_docs/src/content/docs/http-api/guild-audit-logs.mdx
index e48a37520..876f7d80b 100644
--- a/fluxer_docs/src/content/docs/http-api/guild-audit-logs.mdx
+++ b/fluxer_docs/src/content/docs/http-api/guild-audit-logs.mdx
@@ -166,13 +166,13 @@ Every field is present only for the actions listed against it in the [audit acti
| temporary | boolean | Whether the recorded entity is temporary |
| uses3 | number | The invite use count recorded with the action |
-1 The value is the length of the consolidated run for [MESSAGE_BULK_DELETE](#audit-actions) and is always `1` for the two voice actions
+1 The value is the length of the consolidated run for [MESSAGE_BULK_DELETE](#audit-actions) and is always `1` for the voice actions
2 Read from the legacy `delete_message_days` body field of [Create or replace guild ban](/http-api/guild-moderation/#create-or-replace-guild-ban), so a ban that supplied only `delete_message_seconds` records `"0"`
3 The field is defined by the wire contract and no current guild operation records it
-4 The value is a [channel type](/http-api/channels/#channel-types) for the three channel actions and a [permission overwrite type](/http-api/channels/#permission-overwrite-types) for the three overwrite actions
+4 The value is a [channel type](/http-api/channels/#channel-types) for the channel actions and a [permission overwrite type](/http-api/channels/#permission-overwrite-types) for the overwrite actions
## Audit log change object
@@ -273,7 +273,7 @@ Recorded by [CHANNEL_CREATE](#audit-actions), [CHANNEL_UPDATE](#audit-actions),
#### Permission overwrite change fields
-Recorded by the three [channel overwrite actions](#audit-actions). Every value in this family is a string.
+Recorded by the [channel overwrite actions](#audit-actions). Every value in this family is a string.
| Field | Type | Description |
| --- | --- | --- |
@@ -443,7 +443,7 @@ A guild ID that names no guild returns 404 `UNKNOWN_GUILD`. A non-member of an e
1 A negative value and a value above 100 are both rejected, and the accepted value `0` is processed as `1`
-2 The two cursors are mutually exclusive, and supplying both returns 400 `INVALID_FORM_BODY` with [`CANNOT_SPECIFY_BOTH_BEFORE_AND_AFTER`](/http-api/errors/#validation-error-code-registry) against `before`
+2 The cursors are mutually exclusive, and supplying both returns 400 `INVALID_FORM_BODY` with [`CANNOT_SPECIFY_BOTH_BEFORE_AND_AFTER`](/http-api/errors/#validation-error-code-registry) against `before`
3 Supplying either filter disables message deletion consolidation for the request, so a page filtered by actor or action returns the unconsolidated entries
diff --git a/fluxer_docs/src/content/docs/http-api/guild-member-search.mdx b/fluxer_docs/src/content/docs/http-api/guild-member-search.mdx
index b0df781b7..6f3bac08c 100644
--- a/fluxer_docs/src/content/docs/http-api/guild-member-search.mdx
+++ b/fluxer_docs/src/content/docs/http-api/guild-member-search.mdx
@@ -55,7 +55,7 @@ A guild member search result is one matched membership as the search index store
2 Left-padded with zeroes to four digits
-3 The [guild member object](/http-api/guild-members/#guild-member-object) names these two fields `nick` and `roles`, so a client reusing a member renderer must map them
+3 The [guild member object](/http-api/guild-members/#guild-member-object) names these fields `nick` and `roles`, so a client reusing a member renderer must map them
4 The everyone role is never present
diff --git a/fluxer_docs/src/content/docs/http-api/guild-members.mdx b/fluxer_docs/src/content/docs/http-api/guild-members.mdx
index 36fc4a59e..a75e46b36 100644
--- a/fluxer_docs/src/content/docs/http-api/guild-members.mdx
+++ b/fluxer_docs/src/content/docs/http-api/guild-members.mdx
@@ -10,10 +10,10 @@ A guild member is an account that has joined a guild. The membership has a nickn
A guild that no Gateway process serves returns 404 `UNKNOWN_GUILD`. An existing guild where the caller is not a member returns 403 `MISSING_PERMISSIONS`. Fluxer refuses a guild an operator has marked unavailable with 403 `MISSING_ACCESS` before the route runs. [Transfer guild ownership](#transfer-guild-ownership) is the one exception to the 404, and returns 403 `ACCESS_DENIED` for a guild whose record survives without a Gateway process.
-The two modify operations return the resulting membership, [Transfer guild ownership](#transfer-guild-ownership) returns the updated guild, and every other mutation returns 204 with an empty body.
+The modify operations return the resulting membership, [Transfer guild ownership](#transfer-guild-ownership) returns the updated guild, and every other mutation returns 204 with an empty body.
:::note[Fluxer reads the target before authorising the caller]
-A request whose target is not a member returns 404 `UNKNOWN_MEMBER` whether or not the caller can see the guild. The rule covers the two modify operations, [Remove guild member](#remove-guild-member), [Add guild member role](#add-guild-member-role), and [Remove guild member role](#remove-guild-member-role).
+A request whose target is not a member returns 404 `UNKNOWN_MEMBER` whether or not the caller can see the guild. The rule covers the modify operations, [Remove guild member](#remove-guild-member), [Add guild member role](#add-guild-member-role), and [Remove guild member role](#remove-guild-member-role).
:::
## Guild member object
@@ -76,7 +76,7 @@ A client compares `communication_disabled_until` against the current time, becau
## Guild member profile flags
-A guild profile asset has three states. A membership with no flag and no stored hash inherits the account-level asset. A stored hash sets a guild-specific asset. The flag below blocks inheritance and renders the default.
+A guild profile asset has these states. A membership with no flag and no stored hash inherits the account-level asset. A stored hash sets a guild-specific asset. The flag below blocks inheritance and renders the default.
| Value | Name | Description |
| --- | --- | --- |
@@ -115,7 +115,7 @@ The request body shared by [Modify current guild member](#modify-current-guild-m
3 Base64 image data with an optional data URL prefix, which is not counted
-4 One of the three [reply mention preference](/http-api/users/#reply-mention-preferences) enum values, where 0 restores the account-level preference
+4 One of the [reply mention preference](/http-api/users/#reply-mention-preferences) enum values, where 0 restores the account-level preference
5 Stored on the membership, so it applies whether or not the member holds a voice connection
@@ -497,7 +497,7 @@ The guild owner receives 404 `UNKNOWN_ROLE` for a role that does not exist in th
### Side effects
-When the member does not already hold the role, the operation adds it and makes a temporary membership permanent. It then updates member search results, records a [`MEMBER_ROLE_UPDATE`](/http-api/guild-audit-logs/#audit-actions) audit entry, emits [Guild Audit Log Entry Create](/gateway/events/#guild-audit-log-entry-create), and emits [Guild Member Update](/gateway/events/#guild-member-update). All four run whether or not the role set changed. A request naming a role the member already holds still writes an audit entry with an empty change list, published without a `changes` member.
+When the member does not already hold the role, the operation adds it and makes a temporary membership permanent. It then updates member search results, records a [`MEMBER_ROLE_UPDATE`](/http-api/guild-audit-logs/#audit-actions) audit entry, emits [Guild Audit Log Entry Create](/gateway/events/#guild-audit-log-entry-create), and emits [Guild Member Update](/gateway/events/#guild-member-update). These all run whether or not the role set changed. A request naming a role the member already holds still writes an audit entry with an empty change list, published without a `changes` member.
The audit log response names the role only through the change list.
@@ -541,7 +541,7 @@ Removes one role from a member and returns 204 with an empty body. Requires memb
### Side effects
-When the member holds the role, the operation removes it. It then updates member search results, records a [`MEMBER_ROLE_UPDATE`](/http-api/guild-audit-logs/#audit-actions) audit entry, emits [Guild Audit Log Entry Create](/gateway/events/#guild-audit-log-entry-create), and emits [Guild Member Update](/gateway/events/#guild-member-update). All four run whether or not the role set changed. Removing a role does not change whether the membership is temporary.
+When the member holds the role, the operation removes it. It then updates member search results, records a [`MEMBER_ROLE_UPDATE`](/http-api/guild-audit-logs/#audit-actions) audit entry, emits [Guild Audit Log Entry Create](/gateway/events/#guild-audit-log-entry-create), and emits [Guild Member Update](/gateway/events/#guild-member-update). These all run whether or not the role set changed. Removing a role does not change whether the membership is temporary.
### Rate limit
diff --git a/fluxer_docs/src/content/docs/http-api/guild-stickers.mdx b/fluxer_docs/src/content/docs/http-api/guild-stickers.mdx
index 5dedcbd6b..b6044e2e4 100644
--- a/fluxer_docs/src/content/docs/http-api/guild-stickers.mdx
+++ b/fluxer_docs/src/content/docs/http-api/guild-stickers.mdx
@@ -242,7 +242,7 @@ The operation returns 200 whenever the top-level request was valid, including wh
A decoded image that is oversized, undecodable, in an unaccepted format, or matching a banned file hash fails one item.
-:::note[Two blocklist screens run with different floors]
+:::note[The blocklist screens run with different floors]
The global body screen runs before the batch starts and ignores strings shorter than 3 characters, so a blocked string there fails the whole request. The per-item scan applies the same blocklist with no floor, and only a shorter match becomes an item failure.
:::
diff --git a/fluxer_docs/src/content/docs/http-api/guilds.mdx b/fluxer_docs/src/content/docs/http-api/guilds.mdx
index 8c04d8f87..d55e942c1 100644
--- a/fluxer_docs/src/content/docs/http-api/guilds.mdx
+++ b/fluxer_docs/src/content/docs/http-api/guilds.mdx
@@ -91,9 +91,9 @@ A guild object contains the guild's configuration. The operation that returns it
13 Only [Get guild](#get-guild) populates this field, and the array is filtered to the channels the caller can currently view
-14 Only [Get guild](#get-guild) populates these two fields, and both are computed from the guild's live main Gateway state
+14 Only [Get guild](#get-guild) populates these fields, and both are computed from the guild's live main Gateway state
-15 Only [List current user guilds](#list-current-user-guilds) populates these two fields, and only when `with_counts` is true. A guild whose counts are not currently held reports 0 for both
+15 Only [List current user guilds](#list-current-user-guilds) populates these fields, and only when `with_counts` is true. A guild whose counts are not currently held reports 0 for both
:::note[An absent field means the operation omitted it]
`roles`, `emojis`, `stickers`, `channels`, and every count field arrive only from the operations named in the footnotes above. A guild returned without `roles` can still hold roles.
diff --git a/fluxer_docs/src/content/docs/http-api/index.md b/fluxer_docs/src/content/docs/http-api/index.md
index 684871518..03667fdb7 100644
--- a/fluxer_docs/src/content/docs/http-api/index.md
+++ b/fluxer_docs/src/content/docs/http-api/index.md
@@ -30,9 +30,9 @@ An operation documents its body under `JSON body`, `Form body`, or `Multipart bo
A JSON body is parsed from the raw request text without inspecting `Content-Type`. The instance content filter scans a `POST`, `PUT`, or `PATCH` body that parses as JSON against the banned-phrase and banned-URL blocklists, whatever the header declares. It skips a body whose `Content-Type` contains `multipart/form-data` or `application/x-www-form-urlencoded`. A client MUST send the canonical media type.
-Form bodies accept `application/x-www-form-urlencoded` and `multipart/form-data` interchangeably. The three [OAuth2](/http-api/oauth2/) token operations are the only ones that take one. A field that occurs once is a string or file. Repeating the same field name produces an array in occurrence order, and a name ending in `[]` also collects its values into an array.
+Form bodies accept `application/x-www-form-urlencoded` and `multipart/form-data` interchangeably. The [OAuth2](/http-api/oauth2/) token operations are the only ones that take one. A field that occurs once is a string or file. Repeating the same field name produces an array in occurrence order, and a name ending in `[]` also collects its values into an array.
-[Create message](/http-api/messages/#create-message), [Modify message](/http-api/messages/#modify-message), and [Execute webhook](/http-api/webhooks/#execute-webhook) are the only operations that define a multipart body of their own. Each selects the multipart parser when the request `Content-Type` contains `multipart/form-data` and parses the body as JSON otherwise. The three OAuth2 token operations also accept `multipart/form-data`, but read it as an ordinary form body.
+[Create message](/http-api/messages/#create-message), [Modify message](/http-api/messages/#modify-message), and [Execute webhook](/http-api/webhooks/#execute-webhook) are the only operations that define a multipart body of their own. Each selects the multipart parser when the request `Content-Type` contains `multipart/form-data` and parses the body as JSON otherwise. The OAuth2 token operations also accept `multipart/form-data`, but read it as an ordinary form body.
| Field | Type | Description |
| --- | --- | --- |
@@ -77,11 +77,11 @@ The shared validator normalises the JSON body, a form body, the query string, pa
A nested object whose members have all become `null` becomes `null` in turn. The root object itself is never collapsed this way. An empty request body is read as an empty object, so the caller sees the operation's own required-field failures. A body that is present but does not parse as JSON returns 400 `INVALID_FORM_BODY` with one element at path `body` and code `INVALID_FORMAT`.
-:::caution[Three operations bypass the shared validator]
+:::caution[These operations bypass the shared validator]
[Create message](/http-api/messages/#create-message), [Modify message](/http-api/messages/#modify-message), and [Execute webhook](/http-api/webhooks/#execute-webhook) read their own body and apply none of that normalisation.
:::
-An empty string stays an empty string and an empty nested object stays an empty object in those three. The first two reject a JSON body that does not parse. On a JSON body all three collapse every schema failure to one validation entry, and each operation names that entry on its own page.
+An empty string stays an empty string and an empty nested object stays an empty object in those three. The first two reject a JSON body that does not parse. On a JSON body all collapse every schema failure to one validation entry, and each operation names that entry on its own page.
## Authentication
@@ -170,7 +170,7 @@ An operation that sets its own `Cache-Control` keeps that value. A response whos
Every route consumes its own rate limit bucket and is also evaluated against one global bucket unless that bucket is exempt. A denial returns 429 `RATE_LIMITED`. [Rate limits](/topics/rate-limits/) defines the bucket scoping rules, the global allowance, the 429 body, the scope registry, and the complete `X-RateLimit-*` header contract.
-:::note[Two 429 responses have no `X-RateLimit-*` header]
+:::note[These 429 responses have no `X-RateLimit-*` header]
A 429 `RESOURCE_LOCKED` response has `Retry-After: 1`, and a 429 `IP_AUTHORIZATION_RESEND_COOLDOWN` response has the remaining cooldown in whole seconds. A client that reads the bucket headers branches on `code`.
:::
@@ -184,7 +184,7 @@ A small set of routes exists only on the hosted Fluxer deployment. A self-hosted
The CORS response policy is an allow-list of exactly two origins, the deployment's configured web application endpoint and its marketing endpoint. A request whose `Origin` matches one of them receives `Access-Control-Allow-Origin` set to that origin and `Vary: Origin`. Every other request, including one that sends no `Origin`, receives no `Access-Control-Allow-Origin` from that policy. Credentialed cross-origin requests are not enabled, so `Access-Control-Allow-Credentials` is never sent.
-Five paths are readable from any origin. `/v1/webhooks/{webhook_id}/{token}` and `/v1/webhooks/{webhook_id}/{token}/messages/{message_id}` have a second cross-origin policy that allows any origin. Four of the methods registered on them refuse the first-party web client outright, and that refusal is defined by [Origin refusal](/http-api/webhooks/#origin-refusal).
+The paths below are readable from any origin. `/v1/webhooks/{webhook_id}/{token}` and `/v1/webhooks/{webhook_id}/{token}/messages/{message_id}` have a second cross-origin policy that allows any origin. Four of the methods registered on them refuse the first-party web client outright, and that refusal is defined by [Origin refusal](/http-api/webhooks/#origin-refusal).
[Get instance discovery](/http-api/instance/#get-instance-discovery) on `/.well-known/fluxer`, [Get OpenAPI document](/http-api/instance/#get-openapi-document) on `/v1/openapi.json`, and [Get client geolocation](/http-api/instance/#get-client-geolocation) on `/v1/ip` set `Access-Control-Allow-Origin: *` in the operation itself. The wildcard stands for any origin outside the allow-list, and for an allowed origin the policy replaces it with that exact origin and sends `Vary: Origin`.
diff --git a/fluxer_docs/src/content/docs/http-api/instance.mdx b/fluxer_docs/src/content/docs/http-api/instance.mdx
index 5343e7ddb..a3a85081f 100644
--- a/fluxer_docs/src/content/docs/http-api/instance.mdx
+++ b/fluxer_docs/src/content/docs/http-api/instance.mdx
@@ -12,7 +12,7 @@ The Instance resource describes how one Fluxer deployment is set up. It publishe
Every route here is unauthenticated. A credential changes no response body. One that resolves to an account moves the [rate limit bucket](/topics/rate-limits/) from the client IP address onto that account.
-Each of the three routes answers with `Access-Control-Allow-Origin: *`, replaced by the request `Origin` when the deployment-wide [cross-origin policy](/http-api/#cross-origin-requests) allows that origin. The [Admin Instance API](/admin-api/instance/) defines the operator side of the same configuration.
+Each of the routes answers with `Access-Control-Allow-Origin: *`, replaced by the request `Origin` when the deployment-wide [cross-origin policy](/http-api/#cross-origin-requests) allows that origin. The [Admin Instance API](/admin-api/instance/) defines the operator side of the same configuration.
## Instance discovery object
@@ -245,7 +245,7 @@ The client starts from the default value of every key. It evaluates the rules fr
A rule that names no trait and no guild feature replaces the current value for each key it has. A rule that names at least one raises the current value to its own value when that is higher. Because a rebuilt rule has every key, an unfiltered rule returns each key it did not override to the default. A matching filtered rule raises each key it did not override to at least the default.
-Two evaluation contexts exist. A client reads the key's scope and the rule's filters to decide which keys an evaluation applies. The [limit key](#limit-keys) registry states the scope of each key.
+The evaluation contexts are listed below. A client reads the key's scope and the rule's filters to decide which keys an evaluation applies. The [limit key](#limit-keys) registry states the scope of each key.
| Scope | User evaluation | Guild evaluation |
| --- | --- | --- |
diff --git a/fluxer_docs/src/content/docs/http-api/memes.mdx b/fluxer_docs/src/content/docs/http-api/memes.mdx
index 3b75ff9c1..f44d70482 100644
--- a/fluxer_docs/src/content/docs/http-api/memes.mdx
+++ b/fluxer_docs/src/content/docs/http-api/memes.mdx
@@ -103,7 +103,7 @@ One descriptor describes one encoding of one provider GIF. A provider returns se
1 A non-negative value of at most 2147483647. A descriptor with an empty `src` or `proxy_src`, or a zero dimension, is unusable as a preview and is skipped
-A client submitting a format map to [Save meme from URL](#save-meme-from-url) supplies all four members of every descriptor, and a descriptor missing one fails validation with 400 `INVALID_FORM_BODY`.
+A client submitting a format map to [Save meme from URL](#save-meme-from-url) supplies all members of every descriptor, and a descriptor missing one fails validation with 400 `INVALID_FORM_BODY`.
## GIF media format names
@@ -255,7 +255,7 @@ A guild caller without [READ_MESSAGE_HISTORY](/http-api/permissions/) can select
| alt_text?3 | ?string | The accessibility description of the media (0-500 characters) |
| tags? | ?array[string] | The search tags to store, each 1-30 characters |
-1 At least one of the two keys must be present, and a body with neither fails validation. Both may be present as null, which selects nothing explicitly
+1 At least one of the keys must be present, and a body with neither fails validation. Both may be present as null, which selects nothing explicitly
2 `embed_index` takes precedence when both have a value, and an index outside the combined embed list fails with `EMBED_INDEX_OUT_OF_BOUNDS`
@@ -441,7 +441,7 @@ Fluxer derives a two-letter country from the requesting address by geolocation,
1 Exactly one entry per submitted URL, in the submitted order, so a client may index the result by position against its request
-Fluxer resolves a URL in three stages. It offers the URL to the configured GIF provider, then reads it as direct external media, and then unfurls it as a page when the direct read produced no renderable image or video. When a stage fails, Fluxer downgrades that URL's entry alone. A URL that none of the three stages resolves still returns an entry, with its signed proxy URL, an empty `media` map, and whatever the direct read reported. Dimensions are zero and `content_type` is the empty string when the direct read reported nothing.
+Fluxer resolves a URL in stages. It offers the URL to the configured GIF provider, then reads it as direct external media, and then unfurls it as a page when the direct read produced no renderable image or video. When a stage fails, Fluxer downgrades that URL's entry alone. A URL that none of the stages resolves still returns an entry, with its signed proxy URL, an empty `media` map, and whatever the direct read reported. Dimensions are zero and `content_type` is the empty string when the direct read reported nothing.
### Response
diff --git a/fluxer_docs/src/content/docs/http-api/messages.mdx b/fluxer_docs/src/content/docs/http-api/messages.mdx
index fa8e671d5..8b9201593 100644
--- a/fluxer_docs/src/content/docs/http-api/messages.mdx
+++ b/fluxer_docs/src/content/docs/http-api/messages.mdx
@@ -57,7 +57,7 @@ A message object is the full stored form of one post in a channel.
2 Echoed from the create request, so a message read through any other operation always reports false
-3 A user who became an active mention appears in `mentions`, and a user who is only referenced by rendered text appears in `users` instead, so the two arrays never contain the same user
+3 A user who became an active mention appears in `mentions`, and a user who is only referenced by rendered text appears in `users` instead, so the arrays never contain the same user
4 Empty while `SUPPRESS_EMBEDS` is set on the message
@@ -484,7 +484,7 @@ An allowed mentions object selects which mentions written in the message text be
| roles?2 | array[snowflake] | At most 100 role IDs permitted to become active mentions |
| replied_user?3 | boolean | Whether a reply mentions the referenced message author (default true) |
-1 Defaults to every category when the object is absent, to the empty set when `users` or `roles` is supplied without it, and to the empty set when the object is present with none of its four fields
+1 Defaults to every category when the object is absent, to the empty set when `users` or `roles` is supplied without it, and to the empty set when the object is present with none of its fields
2 A non-empty `parse` combined with a non-empty `users` or `roles` is rejected with the field code `PARSE_AND_USERS_OR_ROLES_CANNOT_BE_USED_TOGETHER`
@@ -1193,7 +1193,7 @@ Modifies a message. Returns the updated [message](#message-object) object. Emits
- The target must be a `DEFAULT` or `REPLY` message, and any other type fails with 400 `CANNOT_MODIFY_SYSTEM_WEBHOOK`.
- A message that has [message snapshots](#message-snapshot-object) cannot be edited by its author and fails with the field code `MESSAGES_WITH_SNAPSHOTS_CANNOT_BE_EDITED`. A non-author moderator holding `MANAGE_MESSAGES` can still toggle `SUPPRESS_EMBEDS` and change existing attachment metadata on a forwarded message.
- The author can modify every supported field, and a timed-out author is refused with 403 `COMMUNICATION_DISABLED`.
-- A caller who is not the author can act only in a guild channel, must hold [MANAGE_MESSAGES](/http-api/permissions/), and can change only `SUPPRESS_EMBEDS` and the metadata of attachments that already exist. A non-author edit that does not satisfy all three conditions fails with 403 `CANNOT_EDIT_OTHER_USER_MESSAGE`.
+- A caller who is not the author can act only in a guild channel, must hold [MANAGE_MESSAGES](/http-api/permissions/), and can change only `SUPPRESS_EMBEDS` and the metadata of attachments that already exist. A non-author edit that does not satisfy all these conditions fails with 403 `CANNOT_EDIT_OTHER_USER_MESSAGE`.
- `MANAGE_MESSAGES` is an [elevated permission](/http-api/permissions/#elevated-permissions), so a caller who holds it with no enrolled authenticator receives 400 [`TWO_FACTOR_REQUIRED`](/http-api/errors/) in a guild whose [MFA level](/http-api/guilds/#mfa-levels) is elevated, unless they own the guild.
- A caller who does not hold the bit at all is treated as any other non-author and receives 403 `CANNOT_EDIT_OTHER_USER_MESSAGE`.
- Embeds require [EMBED_LINKS](/http-api/permissions/) and adding an upload requires [ATTACH_FILES](/http-api/permissions/).
@@ -1729,7 +1729,7 @@ Returns a bare array of [partial user](/http-api/users/#partial-user-object) obj
- Authorisation, query parameters, and ordering are exactly those of [List reaction users](#list-reaction-users), and the array is the same page that operation returns in its `items` field.
-The pagination signal is in two response headers. `X-Has-More` is `true` or `false`. `X-Next-After` is the ID of the last user in the array, and it is absent when no further page exists. [List reaction users](#list-reaction-users) returns the same users with both values in the body.
+The pagination signal is in the response headers. `X-Has-More` is `true` or `false`. `X-Next-After` is the ID of the last user in the array, and it is absent when no further page exists. [List reaction users](#list-reaction-users) returns the same users with both values in the body.
### Path parameters
diff --git a/fluxer_docs/src/content/docs/http-api/oauth2.mdx b/fluxer_docs/src/content/docs/http-api/oauth2.mdx
index c374ccf4b..ba0f838ed 100644
--- a/fluxer_docs/src/content/docs/http-api/oauth2.mdx
+++ b/fluxer_docs/src/content/docs/http-api/oauth2.mdx
@@ -77,7 +77,7 @@ These members are the whole body, so the routine that reads the ordinary [error
The consent, token, introspection, and revocation routes return this object for a grant, client authentication, scope, redirect, response type, or permission mask failure. Every other failure, including request validation, authentication, authorisation, and rate limiting, uses the ordinary [error response](/http-api/#error-response) with an [API error code](/http-api/errors/).
-:::note[Two error envelopes share status 400]
+:::note[Both error envelopes share status 400]
The same route can return either one. [Authorise application](#authorise-application) returns neither, because it reports an OAuth2 failure as a query parameter on a redirect.
:::
@@ -401,7 +401,7 @@ A guild installation emits [Guild Create](/gateway/events/#guild-create), [Guild
3 The value must parse as a non-negative integer literal. Every bit outside the defined [permission](/http-api/permissions/) mask is cleared before use, and a caller without ADMINISTRATOR cannot request a retained bit it does not itself hold
-4 The two fields are mutually exclusive
+4 The fields are mutually exclusive
### Response
diff --git a/fluxer_docs/src/content/docs/http-api/premium.mdx b/fluxer_docs/src/content/docs/http-api/premium.mdx
index d7271ae10..3c9774ac1 100644
--- a/fluxer_docs/src/content/docs/http-api/premium.mdx
+++ b/fluxer_docs/src/content/docs/http-api/premium.mdx
@@ -61,7 +61,7 @@ Every amount field is in the minor unit of the currency field its description na
## Price IDs object
-The four checkout prices resolved for one country: monthly and yearly recurring, and one-month and one-year gift.
+The checkout prices resolved for one country: monthly and yearly recurring, and one-month and one-year gift.
### Structure
@@ -78,7 +78,7 @@ The four checkout prices resolved for one country: monthly and yearly recurring,
| currency | string | [Display currency](#display-currencies) of the recurring prices |
| gift_currency | string | [Display currency](#display-currencies) of the gift prices |
-1 Fluxer resolves the recurring pair and the gift pair separately. Each pair takes the first currency for which both of its prices are configured, so the two pairs can resolve to different currencies
+1 Fluxer resolves the recurring pair and the gift pair separately. Each pair takes the first currency for which both of its prices are configured, so the pairs can resolve to different currencies
2 Null when the amount cannot be resolved. [Get price IDs](#get-price-ids) reads it from the payment provider through a one-hour cache, while the copies inside [get premium state](#get-premium-state) read the mirrored price rows
@@ -276,7 +276,7 @@ The effective state decides whether premium features are available. It differs f
1 Decided in a fixed order. A self-hosted deployment in the `everyone` [premium mode](/admin-api/instance/#premium-modes) reports true, then a bot account reports true, then `premium_perks_disabled` reports false, then `premium_enabled_override` reports true, and otherwise the value follows `actual.has_active_paid_premium`
-2 The only two fields the effective state gates. They report the account value while `is_premium` is true, and `0` and null respectively while it is false
+2 The only fields the effective state gates. They report the account value while `is_premium` is true, and `0` and null respectively while it is false
3 Copied from the [actual premium state](#actual-premium-state-object) unchanged, so the value does not react to `is_premium`
@@ -488,7 +488,7 @@ The route answers from mirrored billing data, so it works when the payment provi
:::
:::note[The paired values are computed differently]
-This route derives `current_subscription_price` from the mirrored subscription and `refund_eligibility` from the mirrored invoices, while the two dedicated routes read the payment provider. The values can disagree while the mirror is behind.
+This route derives `current_subscription_price` from the mirrored subscription and `refund_eligibility` from the mirrored invoices, while the dedicated routes read the payment provider. The values can disagree while the mirror is behind.
:::
### Query parameters
diff --git a/fluxer_docs/src/content/docs/http-api/reports.mdx b/fluxer_docs/src/content/docs/http-api/reports.mdx
index 9607e4862..9e24cb53d 100644
--- a/fluxer_docs/src/content/docs/http-api/reports.mdx
+++ b/fluxer_docs/src/content/docs/http-api/reports.mdx
@@ -27,7 +27,7 @@ Fluxer copies the reported content when the report is submitted. A message repor
[Report message](#report-message), [Report user](#report-user), and [Report guild](#report-guild) accept a user session token only. A bot token and an OAuth2 bearer credential are both rejected with 403 `ACCESS_DENIED`. The account must also be claimed and email-verified. An account holding no password and no SSO identity is rejected with 400 `UNCLAIMED_ACCOUNT_CANNOT_SUBMIT_REPORTS`, and one whose address is unverified with 403 `REPORT_EMAIL_VERIFICATION_REQUIRED`. An account with the report ban flag is rejected with 403 `REPORT_BANNED`.
-The three Digital Services Act routes require no credential. Fluxer still resolves a valid credential presented anyway, so the request draws on that account's rate limit allowance.
+The Digital Services Act routes require no credential. Fluxer still resolves a valid credential presented anyway, so the request draws on that account's rate limit allowance.
Every route bucket is keyed by the authenticated user ID when a credential resolves and by client IP address otherwise. Report creation applies further policy allowances keyed by the reporter.
diff --git a/fluxer_docs/src/content/docs/http-api/search.mdx b/fluxer_docs/src/content/docs/http-api/search.mdx
index dd9051115..f6ff6089b 100644
--- a/fluxer_docs/src/content/docs/http-api/search.mdx
+++ b/fluxer_docs/src/content/docs/http-api/search.mdx
@@ -61,7 +61,7 @@ The other 200 body. Fluxer returns it when a channel the search needs has never
| --- | --- | --- |
| indexing | boolean | Whether an index the search needed is not queryable yet, always true on this body |
-Tell the two bodies apart by the presence of `indexing`, which the result object never has. The request queues the missing indexing work, and a later identical request returns ordinary results once that work completes.
+Tell the bodies apart by the presence of `indexing`, which the result object never has. The request queues the missing indexing work, and a later identical request returns ordinary results once that work completes.
### Example
@@ -135,7 +135,7 @@ A bot uses `current`. Supplying any other value with a bot token returns 400 `IN
2 An audio preview is indexed as `audio`. Use the `sound` [content type](#search-content-types) to find uploaded audio attachments
-The four values above are the only ones this filter accepts, so a rich, link, gifv, or bluesky embed cannot be selected or excluded by embed type.
+The values above are the only ones this filter accepts, so a rich, link, gifv, or bluesky embed cannot be selected or excluded by embed type.
## Search sort fields
@@ -258,7 +258,7 @@ The operation walks the set from the first page to skip stale hits, so a deep of
A plain text query pages past it. A page beyond that window fails with 500 `INTERNAL_SERVER_ERROR` when `contents` or `exact_phrases` is supplied.
:::
-The two backends read `min_id` and `max_id` differently. An Elasticsearch instance compares the message ID. A Meilisearch instance compares the whole creation second the bound encodes, so a message created in the same second as the bound is excluded there. An Elasticsearch instance requires at least one `contents` entry to match and replaces `content` with that group. A Meilisearch instance joins the group, `content`, and the quoted phrases into one query string, and requires no entry in particular. Supplying `exact_phrases` without `contents` narrows `content` to the message text alone on Elasticsearch.
+The backends read `min_id` and `max_id` differently. An Elasticsearch instance compares the message ID. A Meilisearch instance compares the whole creation second the bound encodes, so a message created in the same second as the bound is excluded there. An Elasticsearch instance requires at least one `contents` entry to match and replaces `content` with that group. A Meilisearch instance joins the group, `content`, and the quoted phrases into one query string, and requires no entry in particular. Supplying `exact_phrases` without `contents` narrows `content` to the message text alone on Elasticsearch.
The `current` scope requires one of `context_guild_id` and `context_channel_id`. Supplying neither returns 400 with the field code [CONTEXT_CHANNEL_OR_GUILD_ID_REQUIRED](/http-api/errors/) on the path `context`.
diff --git a/fluxer_docs/src/content/docs/http-api/streams.mdx b/fluxer_docs/src/content/docs/http-api/streams.mdx
index 2e42b1db1..d02e54621 100644
--- a/fluxer_docs/src/content/docs/http-api/streams.mdx
+++ b/fluxer_docs/src/content/docs/http-api/streams.mdx
@@ -36,13 +36,13 @@ A key with a different segment count, a non-numeric channel segment, an empty co
Fluxer checks the scope segment against the resolved channel. A guild-scoped key whose resolved channel is private returns 400 `STREAM_KEY_SCOPE_MISMATCH`, and so does a `dm` key whose resolved channel belongs to a guild. A guild-scoped key whose guild segment is not the resolved channel's guild returns the same code.
-The route then compares the channel segment against the resolved channel ID, and a mismatch returns 400 `STREAM_KEY_CHANNEL_MISMATCH`. On the two routes that have a body `channel_id`, a caller MUST send the same value in the body and in the key.
+The route then compares the channel segment against the resolved channel ID, and a mismatch returns 400 `STREAM_KEY_CHANNEL_MISMATCH`. On the routes that have a body `channel_id`, a caller MUST send the same value in the body and in the key.
That comparison is the last check of the read boundary. Channel lookup, the scope check, the channel type check, and the `CONNECT` check all run first, so a body `channel_id` naming a channel the caller cannot see answers 404 `UNKNOWN_CHANNEL` or 403.
## Access rules
-Two boundaries apply across this page.
+The boundaries below apply across this page.
Read access matches [Get channel](/http-api/channels/#get-channel) for the [resolved channel](#stream-key). A guild channel also requires the [CONNECT](/http-api/permissions/) permission. The caller does not have to own the stream, so any member who can join the channel can read its preview.
diff --git a/fluxer_docs/src/content/docs/http-api/users.mdx b/fluxer_docs/src/content/docs/http-api/users.mdx
index ccbf2b814..342c0e15c 100644
--- a/fluxer_docs/src/content/docs/http-api/users.mdx
+++ b/fluxer_docs/src/content/docs/http-api/users.mdx
@@ -557,7 +557,7 @@ The profile read returned by [Get user profile](#get-user-profile) for one targe
1 The pair requires the `guild_id` query parameter and requires both the caller and the target to be members of that guild
-2 All three are omitted while the profile is restricted or the target hides the premium badge, `premium_since` is also omitted while the target hides the activation time, and `premium_lifetime_sequence` while it hides the sequence
+2 All are omitted while the profile is restricted or the target hides the premium badge, `premium_since` is also omitted while the target hides the activation time, and `premium_lifetime_sequence` while it hides the sequence
3 The collection is omitted unless its query parameter is true, and always when the target is the caller
@@ -627,7 +627,7 @@ Returns a [full user profile](#full-user-profile-object) object for one target a
2 The value is trimmed and read as true only for the exact strings `true`, `True`, and `1`. Every other value, including absence, is false
:::note[Profile privacy is a field filter]
-A read that passes the access check can still be restricted. A restricted read nulls `bio` and `pronouns` in both profile objects, empties `connected_accounts`, nulls `timezone_offset`, and sets `profile_limited` to true. It also removes the three premium fields from the body.
+A read that passes the access check can still be restricted. A restricted read nulls `bio` and `pronouns` in both profile objects, empties `connected_accounts`, nulls `timezone_offset`, and sets `profile_limited` to true. It also removes the premium fields from the body.
:::
### Response
@@ -673,7 +673,7 @@ A reserved username fails validation with `USERNAME_RESERVED_VALUE` or `USERNAME
| --- | --- | --- |
| taken | boolean | Whether the normalised tag is unavailable |
-:::caution[`true` also comes from two transient conditions]
+:::caution[`true` also comes from transient conditions]
A tag claimed by a registration still in progress reads as taken for up to 30 seconds. A lookup that waits more than 10 seconds on a busy username also reads as taken.
:::
diff --git a/fluxer_docs/src/content/docs/http-api/users/content.mdx b/fluxer_docs/src/content/docs/http-api/users/content.mdx
index a3998cef9..a7378c60b 100644
--- a/fluxer_docs/src/content/docs/http-api/users/content.mdx
+++ b/fluxer_docs/src/content/docs/http-api/users/content.mdx
@@ -6,7 +6,7 @@ description: Recent mentions, saved messages, and deletion of the caller's own m
import RouteHeader from '@/components/RouteHeader.astro';
-Recent mentions and saved messages are private lists that only the account owning them can read. Two further routes delete the messages the caller authored, one filtered and immediate, the other unfiltered and delayed by a day. Data harvests live on [Data harvests](/http-api/users/data-harvest/).
+Recent mentions and saved messages are private lists that only the account owning them can read. Further routes delete the messages the caller authored, one filtered and immediate, the other unfiltered and delayed by a day. Data harvests live on [Data harvests](/http-api/users/data-harvest/).
These routes are user-only. Fluxer rejects a bot or OAuth2 bearer credential with 403 `ACCESS_DENIED`, and an account that has an outstanding required action with 403 `ACCOUNT_SUSPICIOUS_ACTIVITY`.
@@ -324,7 +324,7 @@ The caller proves sudo mode either with the sudo fields below or with an existin
5 These fields are the [sudo verification object](/http-api/users/mfa/#sudo-verification-object). Which combination is accepted depends on the account's configured authenticators
-The `selected` scope requires at least one of the four context toggles to be true, and a request that disables all four fails validation on `include_dms`. The `inaccessible_only` scope ignores the toggles and the guild filter.
+The `selected` scope requires at least one of the context toggles to be true, and a request that disables them all fails validation on `include_dms`. The `inaccessible_only` scope ignores the toggles and the guild filter.
Neither scope includes the caller's personal notes channel, so this operation cannot delete a personal note.
@@ -387,7 +387,7 @@ Any pending deletion for the account is removed from the queue first. The accoun
The updated counts and schedule are published to the caller through [User Update](/gateway/events/#user-update). No message is deleted by this request, so no channel receives a Dispatch until the scheduled work runs.
:::caution[Nothing announces that the deletion finished]
-When the scheduled work runs it clears the three stored fields directly and emits no [User Update](/gateway/events/#user-update).
+When the scheduled work runs it clears the stored fields directly and emits no [User Update](/gateway/events/#user-update).
:::
### Rate limit
diff --git a/fluxer_docs/src/content/docs/http-api/users/data-harvest.mdx b/fluxer_docs/src/content/docs/http-api/users/data-harvest.mdx
index e9cb7796d..7e9c87d9b 100644
--- a/fluxer_docs/src/content/docs/http-api/users/data-harvest.mdx
+++ b/fluxer_docs/src/content/docs/http-api/users/data-harvest.mdx
@@ -136,7 +136,7 @@ The filter chooses which messages a harvest collects. Every non-message section
| start_date?4 | ?ISO8601 timestamp | Inclusive lower bound on message timestamps, or null for no lower bound |
| end_date?4 | ?ISO8601 timestamp | Exclusive upper bound on message timestamps, or null for no upper bound |
-1 The `selected` scope requires at least one of the four include fields to be true, and a body disabling all four is rejected at path `include_dms`
+1 The `selected` scope requires at least one of the include fields to be true, and a body disabling them all is rejected at path `include_dms`
2 Independent of `include_dms`, so setting `include_dms` false and `include_dms_closed` true targets closed conversations only
@@ -150,7 +150,7 @@ Under the `selected` scope, each context has its own condition:
- A group DM is collected only while the caller is still a recipient and `include_group_dms` is true.
- A guild channel is collected only while the caller is still a member, `include_guilds` is true, and the guild is admitted by the configured list mode.
-The `inaccessible_only` scope selects messages in guilds the caller has left or been removed from and in group DMs the caller has left. It never selects one-to-one direct messages, and it ignores the four toggles, the guild filter mode, and both guild lists.
+The `inaccessible_only` scope selects messages in guilds the caller has left or been removed from and in group DMs the caller has left. It never selects one-to-one direct messages, and it ignores the toggles, the guild filter mode, and both guild lists.
Both scopes exclude the caller's personal notes channel, and both exclude a message whose channel no longer resolves or is neither a direct message, a group DM, nor a guild channel.
@@ -283,13 +283,13 @@ Fluxer removes no harvest record, so an ID that once resolved keeps resolving. A
Issues a temporary download URL for one completed harvest. Returns a [harvest download](#harvest-download-object) object on success.
-Fluxer evaluates four rejection cases in a fixed order, each with its own code. An unknown or foreign harvest is 404 `UNKNOWN_HARVEST`. A harvest whose latest attempt failed is 400 `HARVEST_FAILED`. A harvest with no recorded completion or no stored archive is 400 `HARVEST_NOT_READY`. A harvest past its download deadline is 400 `HARVEST_EXPIRED`.
+Fluxer evaluates the rejection cases in a fixed order, each with its own code. An unknown or foreign harvest is 404 `UNKNOWN_HARVEST`. A harvest whose latest attempt failed is 400 `HARVEST_FAILED`. A harvest with no recorded completion or no stored archive is 400 `HARVEST_NOT_READY`. A harvest past its download deadline is 400 `HARVEST_EXPIRED`.
:::danger[Each request issues a new bearer URL]
A URL is created for each call, its stated expiry is seven days later, and a later call revokes nothing. Treat every issued URL as a secret.
:::
-The URL takes one of two forms. When the instance has presigned harvest downloads enabled, which is the default, it is an object storage presigned URL. Otherwise it points at [Download data harvest archive](#download-data-harvest-archive) on this API with a signed `token` query parameter.
+The URL takes one of the forms below. When the instance has presigned harvest downloads enabled, which is the default, it is an object storage presigned URL. Otherwise it points at [Download data harvest archive](#download-data-harvest-archive) on this API with a signed `token` query parameter.
### Path parameters
diff --git a/fluxer_docs/src/content/docs/http-api/users/mfa.mdx b/fluxer_docs/src/content/docs/http-api/users/mfa.mdx
index 66ffb893a..ba95b06c0 100644
--- a/fluxer_docs/src/content/docs/http-api/users/mfa.mdx
+++ b/fluxer_docs/src/content/docs/http-api/users/mfa.mdx
@@ -8,7 +8,7 @@ import RouteHeader from '@/components/RouteHeader.astro';
Multi-factor authentication asks for a second proof of identity after the account password. Fluxer accepts a TOTP authenticator app and WebAuthn credentials such as a passkey. A one-use backup code works instead of a TOTP code, and [sudo mode](#sudo-mode) reuses these factors to protect every security-sensitive operation in the API.
-Every route here requires a non-bot user session. Fluxer rejects an account in suspicious activity state with 403 `ACCOUNT_SUSPICIOUS_ACTIVITY`, including on the two sudo routes.
+Every route here requires a non-bot user session. Fluxer rejects an account in suspicious activity state with 403 `ACCOUNT_SUSPICIOUS_ACTIVITY`, including on the sudo routes.
:::caution[A challenge is one-use and context-bound]
Fluxer consumes a backup code, a WebAuthn registration challenge, and a WebAuthn sudo challenge on first use. A challenge is bound to the issuing user and operation context and expires after five minutes, so a registration challenge cannot be redeemed as a sudo assertion.
@@ -32,9 +32,9 @@ Enabling TOTP issues 10 codes without deleting anything first. An account that a
[List MFA backup codes](#list-mfa-backup-codes) reads the current set back at any time and reports which codes are already consumed. Consuming a code is irreversible. An account that cannot prove [sudo mode](#sudo-mode) reads the same set through the [backup codes challenge](#backup-codes-challenge).
-A backup code has exactly three entry points: the `mfa_code` field of the [sudo verification object](#sudo-verification-object) with `mfa_method` set to `totp`, the `code` field of [Disable TOTP MFA](#disable-totp-mfa), and the `code` field of [complete login with TOTP](/http-api/authentication/#complete-login-with-totp). Every other code field rejects it. All three accept a current authenticator code or an unconsumed backup code. [Enable TOTP MFA](#enable-totp-mfa) validates only against the secret being enrolled.
+A backup code has only these entry points: the `mfa_code` field of the [sudo verification object](#sudo-verification-object) with `mfa_method` set to `totp`, the `code` field of [Disable TOTP MFA](#disable-totp-mfa), and the `code` field of [complete login with TOTP](/http-api/authentication/#complete-login-with-totp). Every other code field rejects it. All accept a current authenticator code or an unconsumed backup code. [Enable TOTP MFA](#enable-totp-mfa) validates only against the secret being enrolled.
-Every entry point requires the account to hold a TOTP secret. An account whose only authenticator is WebAuthn holds no TOTP secret, so none of the three entry points accepts a backup code, though the account can still regenerate the set.
+Every entry point requires the account to hold a TOTP secret. An account whose only authenticator is WebAuthn holds no TOTP secret, so none of the entry points accepts a backup code, though the account can still regenerate the set.
:::caution[An uppercase backup code with spaces still matches]
Fluxer lowercases the submitted value and each stored code, then removes every character outside `a-z` and `0-9`. `abcdefgh` and `AbCd EfGh` both match the issued code `abcd-efgh`.
@@ -71,7 +71,7 @@ Each operation passes through its own route bucket and, separately, through one
| Code verification | 5 attempts per 15 minutes for each ticket | [Verify MFA backup codes challenge code](#verify-mfa-backup-codes-challenge-code) |
| Regeneration | 5 attempts per 15 minutes for each ticket | [Regenerate MFA backup codes](#regenerate-mfa-backup-codes) |
-Fluxer refuses a resend for 30 seconds after the previous send on the same ticket. That refusal is HTTP 429 with a `Retry-After` computed from the moment the next send becomes available. It is independent of the route bucket and of the two 15-minute send controls.
+Fluxer refuses a resend for 30 seconds after the previous send on the same ticket. That refusal is HTTP 429 with a `Retry-After` computed from the moment the next send becomes available. It is independent of the route bucket and of the 15-minute send controls.
## Sudo mode
@@ -325,7 +325,7 @@ Fluxer accepts additional credential property fields.
## WebAuthn authentication options object
-Every operation that issues a WebAuthn authentication challenge returns this object: sudo verification here, and the two login option operations on [HTTP authentication](/http-api/authentication/).
+Every operation that issues a WebAuthn authentication challenge returns this object: sudo verification here, and the login option operations on [HTTP authentication](/http-api/authentication/).
### Structure
@@ -364,7 +364,7 @@ Fluxer emits no other PublicKeyCredential request options member, so `hints` and
| extensions6 | [WebAuthn client extension inputs](#webauthn-client-extension-inputs-object) object | Requested client extensions |
| hints7 | array[string] | Authenticator hints |
-1 Always the three COSE identifiers `-8`, `-7`, and `-257`, in that order, standing for EdDSA, ECDSA with SHA-256, and RSASSA-PKCS1-v1_5 with SHA-256
+1 Always the COSE identifiers `-8`, `-7`, and `-257`, in that order, standing for EdDSA, ECDSA with SHA-256, and RSASSA-PKCS1-v1_5 with SHA-256
2 Every registration emits the fixed value `60000`
diff --git a/fluxer_docs/src/content/docs/http-api/users/notes.mdx b/fluxer_docs/src/content/docs/http-api/users/notes.mdx
index cefa83c47..585c4f0e3 100644
--- a/fluxer_docs/src/content/docs/http-api/users/notes.mdx
+++ b/fluxer_docs/src/content/docs/http-api/users/notes.mdx
@@ -103,7 +103,7 @@ Creates, replaces, or deletes the caller's note for one target account. Returns
The supplied text becomes the complete stored note. An unresolved target ID returns 404 `UNKNOWN_USER`, even when the request would only delete a note. The caller can store a note for any account that exists, including a bot account, its own account, and an account it holds no relationship with.
:::caution[An omitted, null, or empty note deletes it]
-An empty string is [read as `null`](/http-api/#input-normalisation), so the three forms are indistinguishable. Any value that normalises to the empty string, such as one made only of whitespace, fails validation with `STRING_LENGTH_INVALID`. There is no separate delete route.
+An empty string is [read as `null`](/http-api/#input-normalisation), so the forms are indistinguishable. Any value that normalises to the empty string, such as one made only of whitespace, fails validation with `STRING_LENGTH_INVALID`. There is no separate delete route.
:::
A `note` of at least 3 characters matching the instance phrase blocklist, or with a URL matching the URL blocklist, is rejected with 403 `CONTENT_BLOCKED`. The blocklist check runs before the route's own credential and body checks, so a blocked value is refused with `CONTENT_BLOCKED` even when the request would also fail one of those checks.
diff --git a/fluxer_docs/src/content/docs/http-api/users/phone-verification.mdx b/fluxer_docs/src/content/docs/http-api/users/phone-verification.mdx
index be706222d..51a67d1f3 100644
--- a/fluxer_docs/src/content/docs/http-api/users/phone-verification.mdx
+++ b/fluxer_docs/src/content/docs/http-api/users/phone-verification.mdx
@@ -89,7 +89,7 @@ Keep phone numbers, verification codes, and challenge codes out of logs, analyti
3 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 three verdicts with 400 `INVALID_PHONE_NUMBER`.
+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`.
## Inbound challenge reason values
@@ -122,7 +122,7 @@ Confirmation that the SMS provider accepted a one-time code for delivery. [Send
## Inbound challenge object
-The instructions for texting a code to Fluxer, returned by [Send phone verification](#send-phone-verification) when policy routed the attempt inbound. `channel` tells the two response shapes apart.
+The instructions for texting a code to Fluxer, returned by [Send phone verification](#send-phone-verification) when policy routed the attempt inbound. `channel` tells the response shapes apart.
### Structure
@@ -310,7 +310,7 @@ 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 four 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 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 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.
diff --git a/fluxer_docs/src/content/docs/http-api/users/private-channels.mdx b/fluxer_docs/src/content/docs/http-api/users/private-channels.mdx
index 7771b5363..eff89535a 100644
--- a/fluxer_docs/src/content/docs/http-api/users/private-channels.mdx
+++ b/fluxer_docs/src/content/docs/http-api/users/private-channels.mdx
@@ -205,7 +205,7 @@ The body is the same as [Preload private channel messages](#preload-private-chan
### Rate limit
-40 requests per 10 seconds for each authenticated user, on the `user:preload_messages` bucket. The two paths share one allowance.
+40 requests per 10 seconds for each authenticated user, on the `user:preload_messages` bucket. The paths share one allowance.
## Pin private channel
diff --git a/fluxer_docs/src/content/docs/http-api/users/relationships.mdx b/fluxer_docs/src/content/docs/http-api/users/relationships.mdx
index 5eb7c32f0..7d799f4ed 100644
--- a/fluxer_docs/src/content/docs/http-api/users/relationships.mdx
+++ b/fluxer_docs/src/content/docs/http-api/users/relationships.mdx
@@ -176,7 +176,7 @@ A caller with the `SPAMMER` [public user flag](/http-api/users/#public-user-flag
That 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`. The policy path can also set the `SPAMMER` flag on the caller, which makes every later send one-sided.
-:::note[The two suppression entries enter at different points]
+:::note[The suppression entries enter at different points]
A caller that already has the flag enters before the pending request lookup, so it can hold both an `INCOMING_REQUEST` and an `OUTGOING_REQUEST` record for one account. A caller suppressed during the call enters after the lookup and accepts the pending request normally.
:::
@@ -247,7 +247,7 @@ The body can be omitted, in which case it is treated as an empty object and acce
| --- | --- | --- |
| type?1 | integer | The [relationship type](#relationship-types) selecting the operation |
-1 One of the four enumerated types when present. Only BLOCKED selects blocking, and every other accepted value selects acceptance
+1 One of the enumerated types when present. Only BLOCKED selects blocking, and every other accepted value selects acceptance
### Response
diff --git a/fluxer_docs/src/content/docs/http-api/users/settings-protobuf.md b/fluxer_docs/src/content/docs/http-api/users/settings-protobuf.md
index e34c9f533..7830ad41f 100644
--- a/fluxer_docs/src/content/docs/http-api/users/settings-protobuf.md
+++ b/fluxer_docs/src/content/docs/http-api/users/settings-protobuf.md
@@ -46,7 +46,7 @@ The `SyncedPreferences` message is the root of the snapshot. Every field is a pr
| Field | Type | Description |
| --- | --- | --- |
| accessibility? | [accessibility settings](#accessibility-settings-object) object | Accessibility, display, motion, media, and interaction preferences |
-| accessibility_overrides? | [accessibility overrides](#accessibility-overrides-object) object | Dirty flags for three media settings, with no live consumer |
+| accessibility_overrides? | [accessibility overrides](#accessibility-overrides-object) object | Dirty flags for media settings, with no live consumer |
| textual_preview? | [textual preview settings](#textual-preview-settings-object) object | Textual preview wrapping preferences |
| emoji_picker? | [emoji picker state](#emoji-picker-state-object) object | Emoji picker usage, favourites, and collapsed categories |
| sticker_picker? | [sticker picker state](#sticker-picker-state-object) object | Sticker picker usage, favourites, and collapsed categories |
@@ -207,7 +207,7 @@ Field numbers 42 and 43 are reserved, together with the names `attachment_media_
## Accessibility overrides object
-The `accessibility_overrides` field holds three dirty flags that no live surface reads or writes. The settings they name also appear in [accessibility settings](#accessibility-settings-object) as a `mobile_*_overridden` flag with a matching `mobile_*_value`.
+The `accessibility_overrides` field holds dirty flags that no live surface reads or writes. The settings they name also appear in [accessibility settings](#accessibility-settings-object) as a `mobile_*_overridden` flag with a matching `mobile_*_value`.
### Structure
@@ -418,7 +418,7 @@ One entry names one grouping in the favourites view. The client chooses the iden
## Recent mentions settings object
-The `recent_mentions` field controls which mentions appear in the recent mentions view. The three filters combine, and `include_guilds` selects by channel.
+The `recent_mentions` field controls which mentions appear in the recent mentions view. The filters combine, and `include_guilds` selects by channel.
### Structure
@@ -468,7 +468,7 @@ The `unread_channels` field stores which channels are collapsed in the unread vi
## Mention frecency state object
-The `mention_frecency` field records how often and how recently the account mentioned each user, one record set per guild, so a client can rank mention autocomplete without a server call. It nests two messages, `MentionFrecencyState.Scope` and `MentionFrecencyState.Entry`.
+The `mention_frecency` field records how often and how recently the account mentioned each user, one record set per guild, so a client can rank mention autocomplete without a server call. It nests the messages `MentionFrecencyState.Scope` and `MentionFrecencyState.Entry`.
### Structure
@@ -770,7 +770,7 @@ The `sudo_prompt` field stores the verification method the account used most rec
| --- | --- | --- |
| last_used_mfa_method?1 | int32 | [MFA method](#mfa-methods) most recently used for sudo verification |
-1 The two assigned values correspond to `totp` and `webauthn` in `mfa_method` of the [sudo verification object](/http-api/users/mfa/#sudo-verification-object)
+1 The assigned values correspond to `totp` and `webauthn` in `mfa_method` of the [sudo verification object](/http-api/users/mfa/#sudo-verification-object)
### MFA methods
diff --git a/fluxer_docs/src/content/docs/http-api/users/settings.mdx b/fluxer_docs/src/content/docs/http-api/users/settings.mdx
index 48b91f647..5dad49119 100644
--- a/fluxer_docs/src/content/docs/http-api/users/settings.mdx
+++ b/fluxer_docs/src/content/docs/http-api/users/settings.mdx
@@ -6,7 +6,7 @@ description: Account-wide settings, guild folders, notification settings, and vo
import RouteHeader from '@/components/RouteHeader.astro';
-User settings are the stored preferences of one account. Fluxer keeps them in two records: the account-wide [user settings object](/http-api/users/#user-settings-object), and a [user guild settings object](#user-guild-settings-object) for each guild and for private channels.
+User settings are the stored preferences of one account. Fluxer keeps them in these records: the account-wide [user settings object](/http-api/users/#user-settings-object), and a [user guild settings object](#user-guild-settings-object) for each guild and for private channels.
Every route here is user-only and rejects a bot or OAuth2 bearer credential with 403 `ACCESS_DENIED`. Every route except [Get current user settings](#get-current-user-settings) also rejects an account with an outstanding required action, with 403 `ACCOUNT_SUSPICIOUS_ACTIVITY`.
@@ -184,9 +184,9 @@ Neither field is read outside a guild. A direct message and a group DM collect o
The input form of the [user settings object](/http-api/users/#user-settings-object). Every field is optional, and an omitted field leaves the stored value unchanged. A field present with a value overwrites the stored value in full, including an array or object member.
-The three staff-only fields are the exception. Every update resets `suppress_unprivileged_self_mentions`, `suppress_unprivileged_self_mentions_bypass_user_ids`, and `staff_dm_access_user_ids` to false, empty, and empty for an ordinary account, including an update that names none of them.
+The staff-only fields are the exception. Every update resets `suppress_unprivileged_self_mentions`, `suppress_unprivileged_self_mentions_bypass_user_ids`, and `staff_dm_access_user_ids` to false, empty, and empty for an ordinary account, including an update that names none of them.
-Two further members are accepted and ignored. `guild_positions` is an array of at most 200 guild snowflakes, and nothing reads it. `default_share_voice_activity` belongs to [Modify voice activity sharing](#modify-voice-activity-sharing).
+Further members are accepted and ignored. `guild_positions` is an array of at most 200 guild snowflakes, and nothing reads it. `default_share_voice_activity` belongs to [Modify voice activity sharing](#modify-voice-activity-sharing).
### Structure
diff --git a/fluxer_docs/src/content/docs/http-api/webhooks.mdx b/fluxer_docs/src/content/docs/http-api/webhooks.mdx
index 53e3cdc2e..21ec0ce52 100644
--- a/fluxer_docs/src/content/docs/http-api/webhooks.mdx
+++ b/fluxer_docs/src/content/docs/http-api/webhooks.mdx
@@ -18,7 +18,7 @@ A management operation requires [MANAGE_WEBHOOKS](/http-api/permissions/) at gui
Fluxer resolves the guild before every management operation. A guild that does not exist returns 404 `UNKNOWN_GUILD`. A caller who is not a current member of an existing guild returns 403 `MISSING_PERMISSIONS`.
-[List guild webhooks](#list-guild-webhooks), [List channel webhooks](#list-channel-webhooks), and [Create webhook](#create-webhook) name a guild or a channel in their paths, so the availability gate applies. A guild with [UNAVAILABLE_FOR_EVERYONE](/http-api/guilds/#guild-features) refuses an authenticated request with 403 `MISSING_ACCESS` before the operation runs. [UNAVAILABLE_FOR_EVERYONE_BUT_STAFF](/http-api/guilds/#guild-features) does the same for an account without the instance staff flag. The three remaining management routes name only a webhook ID and are not gated.
+[List guild webhooks](#list-guild-webhooks), [List channel webhooks](#list-channel-webhooks), and [Create webhook](#create-webhook) name a guild or a channel in their paths, so the availability gate applies. A guild with [UNAVAILABLE_FOR_EVERYONE](/http-api/guilds/#guild-features) refuses an authenticated request with 403 `MISSING_ACCESS` before the operation runs. [UNAVAILABLE_FOR_EVERYONE_BUT_STAFF](/http-api/guilds/#guild-features) does the same for an account without the instance staff flag. The remaining management routes name only a webhook ID and are not gated.
Fluxer scans a submitted webhook `name` against the instance phrase and URL blocklists, and a match returns 403 `CONTENT_BLOCKED`. The same lists scan the resolved content and embed text of a created or edited message, and a match returns the same code. The `avatar` member is exempt from that scan, and Fluxer checks its decoded bytes against the banned asset hash list when it stores them.
@@ -1025,7 +1025,7 @@ The operation removes the webhook, frees its guild and channel webhook slot, and
## Token routes
-The ten routes below take the matching webhook ID and token in their paths as the complete credential. The `Authorization` header is neither required nor read on any of them.
+The routes below take the matching webhook ID and token in their paths as the complete credential. The `Authorization` header is neither required nor read on any of them.
## Get webhook with token
diff --git a/fluxer_docs/src/content/docs/index.md b/fluxer_docs/src/content/docs/index.md
index 9a2df6ae4..e7824c3ce 100644
--- a/fluxer_docs/src/content/docs/index.md
+++ b/fluxer_docs/src/content/docs/index.md
@@ -1,10 +1,10 @@
---
# SPDX-License-Identifier: AGPL-3.0-or-later
title: Fluxer API
-description: The four Fluxer protocol surfaces and the contracts they share.
+description: The Fluxer protocol surfaces and the contracts they share.
---
-Fluxer is a self-hostable chat platform. Its API has four surfaces, and all four share one identifier space.
+Fluxer is a self-hostable chat platform. Its API has the surfaces below, and they all share one identifier space.
- To build a client or a bot, start with the [HTTP API](/http-api/) and the [Gateway](/gateway/overview/).
- For voice or a screen share, read [Voice](/voice/).
@@ -22,14 +22,14 @@ Fluxer is a self-hostable chat platform. Its API has four surfaces, and all four
A client mutates a resource over the HTTP API and receives the resulting update as a Gateway [Dispatch](/gateway/events/). Each operation states the Dispatches it fires, and [Events](/gateway/events/) defines each payload and its recipient scope.
-A [snowflake](/snowflakes/) is the identifier all four surfaces share. Voice runs on LiveKit, and [Voice](/voice/) defines the placement protocol and the media transport.
+A [snowflake](/snowflakes/) is the identifier all surfaces share. Voice runs on LiveKit, and [Voice](/voice/) defines the placement protocol and the media transport.
## Shared contracts
| Read this | For |
| --- | --- |
| [Conventions](/conventions/) | Wire table notation, footnotes, omission and `null` |
-| [Authentication](/authentication/) | The `Authorization` grammar and the four credential kinds |
+| [Authentication](/authentication/) | The `Authorization` grammar and the credential kinds |
| [Snowflakes](/snowflakes/) | Identifiers, ordering, and pagination cursors |
| [Errors](/http-api/errors/) | The error envelope and the code registries |
| [Rate limits](/topics/rate-limits/) | Buckets, the 429 body, and the `X-RateLimit-*` headers |
@@ -60,4 +60,4 @@ GET https://api.example.com/v1/users/@me
Authorization: flx_ZDb1GURItsMuYl1zvrgxv2qLBxyNmgNSEaWT
```
-That credential is a user session token. [Log in with a password](/http-api/authentication/#log-in-with-a-password) issues one. A bot sends a bot token with the `Bot` prefix, issued by [Create application](/http-api/applications/#create-application). [Authentication](/authentication/) gives the exact form of all four kinds.
+That credential is a user session token. [Log in with a password](/http-api/authentication/#log-in-with-a-password) issues one. A bot sends a bot token with the `Bot` prefix, issued by [Create application](/http-api/applications/#create-application). [Authentication](/authentication/) gives the exact form of each kind.
diff --git a/fluxer_docs/src/content/docs/media-proxy/responses-and-limits.md b/fluxer_docs/src/content/docs/media-proxy/responses-and-limits.md
index b2296168e..e9f08619a 100644
--- a/fluxer_docs/src/content/docs/media-proxy/responses-and-limits.md
+++ b/fluxer_docs/src/content/docs/media-proxy/responses-and-limits.md
@@ -33,7 +33,7 @@ An unsuccessful response body is an English reason phrase under the content type
A retained external origin status reaches the client as an upstream fetch failure on the signed external read route and as the canonical reason phrase of its status on `/_metadata`. An object store error that maps to no case above uses the canonical reason phrase of its status.
-Every error a route produces uses `Cache-Control: no-store` and the standard [security headers](/media-proxy/overview/#representation-headers). Three failures have the security headers and set no cache policy, and [Cache policies](#cache-policies) names them. No plain-text error has CORS headers unless it came from the [upload relay](/media-proxy/upload-relay/), and no error has `Retry-After` or a request identifier.
+Every error a route produces uses `Cache-Control: no-store` and the standard [security headers](/media-proxy/overview/#representation-headers). The exceptions have the security headers and set no cache policy, and [Cache policies](#cache-policies) names them. No plain-text error has CORS headers unless it came from the [upload relay](/media-proxy/upload-relay/), and no error has `Retry-After` or a request identifier.
### Handling contract
@@ -90,7 +90,7 @@ Decoded images are limited to 16,384 pixels on either edge and 268,435,456 pixel
The upload relay limits a body to the smaller of the capability's declared maximum and the endpoint's configured body limit. That endpoint limit defaults to the same 500 MiB ceiling and can be configured from 1 byte through 5 GiB. A request that declares no `Content-Length` is spooled to disk first, and spooled bodies share an 8 GiB endpoint budget by default.
-An internal `/_metadata`, `/_thumbnail`, or `/_frames` request body is limited to the base64 expansion of the 500 MiB media bound plus 1 MiB. All three answer a larger body with 413.
+An internal `/_metadata`, `/_thumbnail`, or `/_frames` request body is limited to the base64 expansion of the 500 MiB media bound plus 1 MiB. All answer a larger body with 413.
## Work admission
diff --git a/fluxer_docs/src/content/docs/operator/configuration.mdx b/fluxer_docs/src/content/docs/operator/configuration.mdx
index 542d74a69..d7fea13ea 100644
--- a/fluxer_docs/src/content/docs/operator/configuration.mdx
+++ b/fluxer_docs/src/content/docs/operator/configuration.mdx
@@ -10,7 +10,7 @@ We are grateful to everyone supporting the project through [Fluxer Plutonium](ht
Fluxer reads its settings from environment variables. They live in a file named `.env`, in the same directory as `docker-compose.yml`.
-A first run touches two sections. [Core identity and public address](#core-identity-and-public-address) has `FLUXER_DOMAIN`, the hostname people type into a browser. [Secrets you must generate](#secrets-you-must-generate) has the values that ship as `CHANGE_ME`. Everything after those two is optional and already has a working value.
+A first run touches the sections below. [Core identity and public address](#core-identity-and-public-address) has `FLUXER_DOMAIN`, the hostname people type into a browser. [Secrets you must generate](#secrets-you-must-generate) has the values that ship as `CHANGE_ME`. Everything after those two is optional and already has a working value.
The installer in [Get started](/operator/get-started/) writes `.env` for you and fills in every secret. [Upgrading](/operator/upgrading/) covers moving between releases.
@@ -35,15 +35,15 @@ Precedence, highest first:
2. The environment variable.
3. The built-in default.
-Three runtimes read the environment, each with its own parser, so the same name can have a different default in different services.
+Each runtime reads the environment with its own parser, so the same name can have a different default in different services.
#### Node named overrides
-Read by `api` and `worker`. A fixed table of 278 `FLUXER_*` names. A name outside it is invisible.
+Read by `api` and `worker`. A fixed table of `FLUXER_*` names. A name outside it is invisible.
#### Rust direct reads
-Read by `media-proxy`, `app-proxy`, `admin`, and the five internal services. Each crate reads its own names. Blank usually counts as unset.
+Read by `media-proxy`, `app-proxy`, `admin`, and the internal services. Each crate reads its own names. Blank usually counts as unset.
#### Erlang direct reads
@@ -53,7 +53,7 @@ Parsing rules:
- Node treats an empty string as set. `FLUXER_EMAIL_FROM_NAME=` overrides the `Fluxer` default with an empty name.
- Node booleans accept only `true` and `false`, in any letter case. `FLUXER_POSTGRES_SSL=1` parses as the number `1` and fails startup with `FLUXER_POSTGRES_SSL must be true or false`.
-- Rust has three boolean parsers. `app-proxy` and `admin` treat anything other than `1`, `true`, `yes`, or `on` as false. `media-proxy` and the internal services reject an unrecognised value with a startup error.
+- Rust parses booleans differently by crate. `app-proxy` and `admin` treat anything other than `1`, `true`, `yes`, or `on` as false. `media-proxy` and the internal services reject an unrecognised value with a startup error.
- A value that starts with `{` or `[` is parsed as JSON. A failed parse fails startup with a message naming the variable.
- A name read as an integer fails startup on any other value, in Node and in the Gateway. An empty value takes the default.
- Comma separated lists are split on `,`, trimmed, and stripped of empty entries.
@@ -138,7 +138,7 @@ Base64url of the 65-byte uncompressed P-256 point, unpadded, which is 87 charact
The matching half of the pair. Base64url of the 32-byte P-256 scalar, unpadded, which is 43 characters. The API refuses to start when the scalar does not derive the public point. Both values are required even when nobody uses browser notifications.
-Two more values ship with a usable value. Both are required.
+The values below ship with a usable value. Both are required.
#### `FLUXER_S3_ACCESS_KEY`
@@ -146,7 +146,7 @@ Two more values ship with a usable value. Both are required.
#### `LIVEKIT_API_KEY`
-`.env.example` `fluxer`. The LiveKit API key. Compose passes it to LiveKit as `LIVEKIT_KEYS` and as the webhook signing key, and to the API as `FLUXER_LIVEKIT_API_KEY`, so one change in `.env` moves all three.
+`.env.example` `fluxer`. The LiveKit API key. Compose passes it to LiveKit as `LIVEKIT_KEYS` and as the webhook signing key, and to the API as `FLUXER_LIVEKIT_API_KEY`, so one change in `.env` moves them all.
## Endpoint derivation from FLUXER_BASE_DOMAIN
@@ -169,7 +169,7 @@ Nothing in the stack reads `X-Forwarded-Proto` or `X-Forwarded-Host`. Every abso
## The public origin
-`FLUXER_PUBLIC_ORIGIN` states the public address as one string, with no trailing slash: the scheme, the host, and the port when that port is not the default for the scheme. `FLUXER_PUBLIC_SCHEME`, `FLUXER_DOMAIN` and `FLUXER_PUBLIC_PORT` state the same address between them, so the two spellings have to agree.
+`FLUXER_PUBLIC_ORIGIN` states the public address as one string, with no trailing slash: the scheme, the host, and the port when that port is not the default for the scheme. `FLUXER_PUBLIC_SCHEME`, `FLUXER_DOMAIN` and `FLUXER_PUBLIC_PORT` state the same address between them, so the spellings have to agree.
`docker-compose.yml` substitutes it into every name that needs a full origin, among them `FLUXER_APP_ENDPOINT`, `FLUXER_ADMIN_ENDPOINT`, `FLUXER_ADMIN_OAUTH_REDIRECT_URI`, `FLUXER_MEDIA_ENDPOINT`, `FLUXER_MARKETING_ENDPOINT`, `FLUXER_MEDIA_PROXY_PUBLIC_ENDPOINT`, `FLUXER_MEDIA_PROXY_UPLOAD_RELAY_ENDPOINT`, `FLUXER_STATIC_CDN_ENDPOINT`, `FLUXER_LIVEKIT_URL`, `FLUXER_PASSKEY_ADDITIONAL_ALLOWED_ORIGINS`, `PUBLIC_BOOTSTRAP_API_PUBLIC_ENDPOINT`, and the Gateway's media and static endpoints. `grep FLUXER_PUBLIC_ORIGIN docker-compose.yml` is the whole list. Compose also puts the name itself into the shared `x-fluxer-env` block, and a service that reads it takes its base domain, scheme and port from it.
@@ -177,7 +177,7 @@ It ships commented out. When it is unset, Compose builds those names from `FLUXE
A non-default port therefore needs nothing here. `FLUXER_PUBLIC_PORT` sets the port of the public address, and the rest of the stack follows it. The comment block under the first three lines of `.env.example` says what each layout needs.
-Set `FLUXER_PUBLIC_ORIGIN` only to write the address out in one place, and then repeat the port `FLUXER_PUBLIC_PORT` names and the host `FLUXER_DOMAIN` names inside it. An `.env` whose two spellings disagree names two addresses. Half the instance answers on one and half on the other, and the web app sends no `Authorization` header to an API that is not on its own origin.
+Set `FLUXER_PUBLIC_ORIGIN` only to write the address out in one place, and then repeat the port `FLUXER_PUBLIC_PORT` names and the host `FLUXER_DOMAIN` names inside it. An `.env` whose spellings disagree names two addresses. Half the instance answers on one and half on the other, and the web app sends no `Authorization` header to an API that is not on its own origin.
## Endpoint overrides
@@ -201,11 +201,11 @@ Defaults to the derived endpoint. The public API base. The `admin` service reads
#### `FLUXER_API_CLIENT_ENDPOINT`
-Defaults to the derived endpoint. The API base handed to clients. Setting only one of the two API endpoints splits what clients see from what the instance advertises.
+Defaults to the derived endpoint. The API base handed to clients. Setting only one of the API endpoints splits what clients see from what the instance advertises.
#### `FLUXER_APP_ENDPOINT`
-Defaults to the derived endpoint. The web app origin. Also one of the two allowed CORS origins. Serving the client from another hostname requires setting this.
+Defaults to the derived endpoint. The web app origin. Also one of the allowed CORS origins. Serving the client from another hostname requires setting this.
#### `FLUXER_GATEWAY_ENDPOINT`
@@ -271,7 +271,7 @@ Falls back to `FLUXER_STATIC_CDN_ENDPOINT`. The static origin used by `unfurl`.
The `edge` container is the only HTTP entry point. Both layouts below work with the edge left alone.
-Bundled TLS is the default. `docker compose up -d` binds `80/tcp`, `443/tcp`, and `443/udp` and obtains its own certificate for `FLUXER_DOMAIN`. Point DNS at the host and set nothing else. The two `443` publishes follow `FLUXER_PUBLIC_PORT`, so a non-default public port moves them with it.
+Bundled TLS is the default. `docker compose up -d` binds `80/tcp`, `443/tcp`, and `443/udp` and obtains its own certificate for `FLUXER_DOMAIN`. Point DNS at the host and set nothing else. The `443` publishes follow `FLUXER_PUBLIC_PORT`, so a non-default public port moves them with it.
Put your own reverse proxy in front by adding `docker-compose.proxy.yml`, a second Compose file that overrides parts of the first. Load it with `-f` twice, or set `COMPOSE_FILE=docker-compose.yml:docker-compose.proxy.yml` in `.env` once. The edge then serves plain HTTP on one port, and the proxy in front terminates TLS. [Behind your own reverse proxy](/operator/reverse-proxy/) owns the switch, the requirements, and the per-proxy configuration.
@@ -333,7 +333,7 @@ The API rejects any request whose client-IP header is missing, empty, not a pars
## Images
-These three pick which container images Compose pulls. All are optional.
+These pick which container images Compose pulls. All are optional.
| Variable | Value in `.env.example` |
| --- | --- |
@@ -343,7 +343,7 @@ These three pick which container images Compose pulls. All are optional.
`FLUXER_REGISTRY_OWNER` is the owner segment of the image names and falls back to `fluxerapp`. `FLUXER_REGISTRY` is the registry images are pulled from and falls back to `ghcr.io/` followed by the owner. `FLUXER_IMAGE_TAG` is the tag on every Fluxer image and falls back to `v1`.
-These affect only the eleven Fluxer images. `docker-compose.yml` pins the `caddy`, `postgres`, `valkey`, `nats`, `meilisearch`, `seaweedfs`, and `livekit` images, and `.env` cannot change them.
+These affect only the Fluxer images. `docker-compose.yml` pins the `caddy`, `postgres`, `valkey`, `nats`, `meilisearch`, `seaweedfs`, and `livekit` images, and `.env` cannot change them.
## Database
@@ -387,7 +387,7 @@ Default empty. A CA certificate in PEM form. Used only when SSL is on.
#### `FLUXER_POSTGRES_MAX_CONNECTIONS`
-Default `20`. Pool size. Integer 1 to 1000, per process. The total is the sum across every container. Compose sets 25 for `api` and `worker` and 20 for the two database-backed shards.
+Default `20`. Pool size. Integer 1 to 1000, per process. The total is the sum across every container. Compose sets 25 for `api` and `worker` and 20 for the database-backed shards.
#### `FLUXER_POSTGRES_KV_TABLE`
@@ -459,7 +459,7 @@ Default `536870912`. GIF cache size. Must be at least 16777216. Set it to 134217
Default `300000`. How often the API refreshes its IP-ban cache. A non-finite value or one at or below zero disables the timer.
-Two sorted sets in the bundled Valkey have no expiry: `bulk_message_deletion_queue` and the account deletion queue. The worker rebuilds each of them from the users table whenever its state version is absent or older than a day, so a lost set costs one rebuild and up to a day of delay. `docker-compose.yml` still starts Valkey with `--appendonly yes`, `--appendfsync everysec`, a named volume and `noeviction`. An over-limit write then returns an error to the caller and drops no queued work.
+These sorted sets in the bundled Valkey have no expiry: `bulk_message_deletion_queue` and the account deletion queue. The worker rebuilds each of them from the users table whenever its state version is absent or older than a day, so a lost set costs one rebuild and up to a day of delay. `docker-compose.yml` still starts Valkey with `--appendonly yes`, `--appendfsync everysec`, a named volume and `noeviction`. An over-limit write then returns an error to the caller and drops no queued work.
Distributed locks in the same store all have a TTL, so they expire on their own. [Volumes and buckets](#volumes-and-buckets) states what losing `valkey-data` costs.
@@ -521,7 +521,7 @@ Default `static`. Static assets. Read by `media-proxy` only in `static` mode, wh
A separate downloads provider is available. `FLUXER_S3_DOWNLOADS_ENDPOINT`, `FLUXER_S3_DOWNLOADS_PUBLIC_ENDPOINT`, `FLUXER_S3_DOWNLOADS_FORCE_PATH_STYLE`, `FLUXER_S3_DOWNLOADS_REGION`, `FLUXER_S3_DOWNLOADS_ACCESS_KEY_ID`, and `FLUXER_S3_DOWNLOADS_SECRET_ACCESS_KEY` take effect only when `FLUXER_S3_DOWNLOADS_ENDPOINT` is non-empty, and they then replace the primary configuration for the downloads bucket.
-The Media Proxy read path has four overrides of its own. All are optional.
+The Media Proxy read path has overrides of its own. All are optional.
#### `FLUXER_S3_READ_ENDPOINT`
@@ -571,7 +571,7 @@ Default `true`. Certificate verification. Elasticsearch only. Never passed to th
## Message bus and internal services
-NATS is the message bus between Fluxer processes, and five small internal services sit behind it. All are optional.
+NATS is the message bus between Fluxer processes, and small internal services sit behind it. All are optional.
#### `FLUXER_NATS_URL`
@@ -587,21 +587,21 @@ Default `nats://127.0.0.1:4222`. The JetStream address. Read by `api` and `worke
#### `FLUXER_NATS_AUTH_TOKEN`
-Default empty. NATS authentication. Read by `api`, `worker`, `gateway`, and the five internal services. The shipped NATS runs without authentication, and Compose forwards this name to every container that connects to it.
+Default empty. NATS authentication. Read by `api`, `worker`, `gateway`, and the internal services. The shipped NATS runs without authentication, and Compose forwards this name to every container that connects to it.
#### `FLUXER_SVC_NATS_URL`
-Default `nats://127.0.0.1:4222`. The NATS address for the five internal services. A separate variable from `FLUXER_NATS_URL`.
+Default `nats://127.0.0.1:4222`. The NATS address for the internal services. A separate variable from `FLUXER_NATS_URL`.
#### `FLUXER_GATEWAY_API_RPC_ENDPOINT`
No default. Where the Gateway calls the API. Read by the Gateway.
-The five internal services read the same topology variables. All are optional.
+The internal services read the same topology variables. All are optional.
#### `FLUXER_SVC_NAME`
-Default `default`. The metrics prefix and the concurrency default. NATS subjects come from a hardcoded per-crate name. Compose sets it to the crate name on all ten containers.
+Default `default`. The metrics prefix and the concurrency default. NATS subjects come from a hardcoded per-crate name. Compose sets it to the crate name on every internal service container.
#### `FLUXER_SVC_MODE`
@@ -625,7 +625,7 @@ Default `8090`. The health and metrics port. Not published.
#### `FLUXER_SVC_MAX_CONCURRENT_REQUESTS`
-Defaults to 192 for messages, 320 for snowflakes, 64 otherwise. In-flight request ceiling. The built-in defaults key off `FLUXER_SVC_NAME`. Compose forwards `FLUXER_SVC_MAX_CONCURRENT_REQUESTS` to `users`, `users-shard`, `messages`, and `messages-shard` at a default of 20, which pairs with their 20-connection Postgres pools, and leaves the six other containers on the built-in defaults.
+Defaults to 192 for messages, 320 for snowflakes, 64 otherwise. In-flight request ceiling. The built-in defaults key off `FLUXER_SVC_NAME`. Compose forwards `FLUXER_SVC_MAX_CONCURRENT_REQUESTS` to `users`, `users-shard`, `messages`, and `messages-shard` at a default of 20, which pairs with their 20-connection Postgres pools, and leaves the other containers on the built-in defaults.
#### `POD_NAME`
@@ -697,7 +697,7 @@ LiveKit media does not go through the edge. Compose publishes both media ports d
Moving a media port is one line in `.env` followed by `docker compose up -d livekit`. Compose puts the same value on the host side of the mapping, on the container side, and on the `rtc` port LiveKit advertises in the ICE candidates it hands to clients.
-Moving the key pair takes two steps. Set both names in `.env`, then run `docker compose up -d livekit api worker` so LiveKit restarts on the new key and the API rebuilds its webhook receivers and upserts the stored voice server row.
+Moving the key pair takes these steps. Set both names in `.env`, then run `docker compose up -d livekit api worker` so LiveKit restarts on the new key and the API rebuilds its webhook receivers and upserts the stored voice server row.
The `Caddyfile` is a bind mount, so the edge reads the copy that sits on disk beside `docker-compose.yml`. Editing it takes `docker compose restart edge`, because `docker compose up -d` leaves a container alone when only a mounted file changed. An upgrade does that restart itself, which [What the script does](/operator/upgrading/#what-the-script-does) covers. A change to any LiveKit value in `.env` needs `docker compose up -d`, because `restart` reuses the existing container with its old environment.
@@ -733,9 +733,9 @@ Default `60000`. Grace before culling a LiveKit-only participant. Milliseconds.
## Email
-Email is off by default, and two conditions turn it on. The switch must be on, and the provider must be `smtp` with a complete SMTP configuration, meaning `FLUXER_EMAIL_FROM_EMAIL`, `FLUXER_EMAIL_SMTP_HOST`, `FLUXER_EMAIL_SMTP_PORT`, `FLUXER_EMAIL_SMTP_USERNAME`, and `FLUXER_EMAIL_SMTP_PASSWORD` are all non-empty. All are optional.
+Email is off by default, and the conditions below turn it on. The switch must be on, and the provider must be `smtp` with a complete SMTP configuration, meaning `FLUXER_EMAIL_FROM_EMAIL`, `FLUXER_EMAIL_SMTP_HOST`, `FLUXER_EMAIL_SMTP_PORT`, `FLUXER_EMAIL_SMTP_USERNAME`, and `FLUXER_EMAIL_SMTP_PASSWORD` are all non-empty. All are optional.
-The same two conditions turn on a DNS check at registration. The check runs when email is on by the rule above. The dashboard switch and the SMTP values decide it along with the variable. The address domain has to publish an `MX` record, or an `A` or `AAAA` record as a fallback, and an address at a domain that publishes neither is answered `That email domain cannot receive mail.` however well formed it is. The first admin account is no exception, so an owner address at a `.lan`, `.internal` or `home.arpa` name needs email left off.
+The same conditions turn on a DNS check at registration. The check runs when email is on by the rule above. The dashboard switch and the SMTP values decide it along with the variable. The address domain has to publish an `MX` record, or an `A` or `AAAA` record as a fallback, and an address at a domain that publishes neither is answered `That email domain cannot receive mail.` however well formed it is. The first admin account is no exception, so an owner address at a `.lan`, `.internal` or `home.arpa` name needs email left off.
#### `FLUXER_EMAIL_ENABLED`
@@ -789,10 +789,10 @@ All are optional.
| Variable | Value in `.env.example` | Controls |
| --- | --- | --- |
-| FLUXER_CAPTCHA_ENABLED | `false` | The [CAPTCHA](/topics/captcha/) switch. Startup fails when it is `true` without a provider and that provider's two keys |
+| FLUXER_CAPTCHA_ENABLED | `false` | The [CAPTCHA](/topics/captcha/) switch. Startup fails when it is `true` without a provider and that provider's keys |
| FLUXER_CAPTCHA_PROVIDER | `none` | The provider. `hcaptcha`, `turnstile`, or `none`. Anything else fails startup |
-`FLUXER_CAPTCHA_HCAPTCHA_SITE_KEY`, `FLUXER_CAPTCHA_HCAPTCHA_SECRET_KEY`, `FLUXER_CAPTCHA_TURNSTILE_SITE_KEY`, and `FLUXER_CAPTCHA_TURNSTILE_SECRET_KEY` ship empty in `.env.example` and the shipped Compose file forwards all four. Set the pair the selected provider needs. The admin dashboard's Runtime Integrations panel sets the same provider and keys, and a value stored there wins over the environment. An instance that configures CAPTCHA only there must leave `FLUXER_CAPTCHA_ENABLED` at `false`, because the boot check reads the environment alone.
+`FLUXER_CAPTCHA_HCAPTCHA_SITE_KEY`, `FLUXER_CAPTCHA_HCAPTCHA_SECRET_KEY`, `FLUXER_CAPTCHA_TURNSTILE_SITE_KEY`, and `FLUXER_CAPTCHA_TURNSTILE_SECRET_KEY` ship empty in `.env.example` and the shipped Compose file forwards them all. Set the pair the selected provider needs. The admin dashboard's Runtime Integrations panel sets the same provider and keys, and a value stored there wins over the environment. An instance that configures CAPTCHA only there must leave `FLUXER_CAPTCHA_ENABLED` at `false`, because the boot check reads the environment alone.
## Single sign-on and passkeys
@@ -830,7 +830,7 @@ Bluesky OAuth login is off by default and is configured through `FLUXER_AUTH_BLU
| FLUXER_VAPID_PRIVATE_KEY | `CHANGE_ME` | The VAPID private key. Base64url of the 32-byte scalar, and the matching half of the pair |
| FLUXER_VAPID_EMAIL | unset | The VAPID contact address. Compose derives `admin@` followed by `FLUXER_DOMAIN` when it is unset |
-The Gateway reads the same three names. A malformed pair, or a private key that does not derive the public point, does not stop it. It records the fault in its log at startup and then drops every web push notification.
+The Gateway reads the same names. A malformed pair, or a private key that does not derive the public point, does not stop it. It records the fault in its log at startup and then drops every web push notification.
`FLUXER_GATEWAY_PUSH_ENABLED` is read by the Gateway alone, defaults to `true`, and turns that check off when it is `false`.
@@ -880,7 +880,7 @@ No default. The service account address. Paired with the project.
#### `FLUXER_PUSH_FCM_PRIVATE_KEY`
-No default. The service account key. Use one of the three key sources.
+No default. The service account key. Use one of the key sources.
#### `FLUXER_PUSH_FCM_PRIVATE_KEY_PATH`
@@ -1026,9 +1026,9 @@ Default `flag_spammer`. What happens when it fires. `flag_spammer` or `suppress_
Defaults to the inverse of `FLUXER_SELF_HOSTED`. External blocklist feeds. 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`, four `FLUXER_ABUSE_THRESHOLD_` names, four `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.
+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.
-Five more 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.
+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.
@@ -1036,9 +1036,9 @@ Five more names in that family limit how often the auto-banner buys an IP classi
- `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.
-The claim key is `abuse:ipclass:claim:` plus the ban key, and replicas send classes to each other on the `abuse_tracker:ipclass` key-value channel. The four numeric names are read through the same helper as the names above, so a non-finite value or one at or below zero falls back to the default.
+The claim key is `abuse:ipclass:claim:` plus the ban key, and replicas send classes to each other on the `abuse_tracker:ipclass` key-value channel. The numeric names are read through the same helper as the names above, so 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 three priorities, each with a share of that ceiling and a token bucket for bursts refilled once a minute.
+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`.
@@ -1046,7 +1046,7 @@ A `FLUXER_IPINFO_BUDGET_` family caps what the instance spends at ipinfo. `FLUXE
The lower ceilings mean background lookups stop first and critical lookups stop last. The counters live in the key-value store under `ipinfo:budget:burst:` and `ipinfo:budget:month:`. A shed lookup returns an unavailable result and raises no error, and any key-value failure admits the lookup at every priority, so a cache outage never stops an admin ban or the auto-banner.
-Two names let the local MaxMind databases answer the registration risk lookup. `FLUXER_RISK_IPINFO_PRESCREEN_ENABLED` is off by default and only `1` or `true`, in any letter case, turns it on. `FLUXER_RISK_IPINFO_PRESCREEN_ALLOW_ASNS` is a comma-separated list of ASN numbers, empty by default, and non-numeric entries are dropped. The pre-screen therefore does nothing until an operator fills the list in. An IP skips ipinfo only when the local city database returns a country, the local ASN database returns a number, that number is in the list, and the ASN organization name is not a commercial privacy provider, an education network or a cellular network. Everything else still goes to ipinfo.
+The names below let the local MaxMind databases answer the registration risk lookup. `FLUXER_RISK_IPINFO_PRESCREEN_ENABLED` is off by default and only `1` or `true`, in any letter case, turns it on. `FLUXER_RISK_IPINFO_PRESCREEN_ALLOW_ASNS` is a comma-separated list of ASN numbers, empty by default, and non-numeric entries are dropped. The pre-screen therefore does nothing until an operator fills the list in. An IP skips ipinfo only when the local city database returns a country, the local ASN database returns a number, that number is in the list, and the ASN organization name is not a commercial privacy provider, an education network or a cellular network. Everything else still goes to ipinfo.
## Limits
@@ -1084,7 +1084,7 @@ Defaults to `debug` in development, `info` otherwise. The Node log level. Read b
#### `RUST_LOG`
-Default `info`. The Rust log filter. Read by `media-proxy`, `app-proxy`, `admin`, and the five internal services. Not in `.env.example` or the Compose file.
+Default `info`. The Rust log filter. Read by `media-proxy`, `app-proxy`, `admin`, and the internal services. Not in `.env.example` or the Compose file.
#### `FLUXER_GATEWAY_LOGGER_LEVEL`
@@ -1126,7 +1126,7 @@ All are optional.
#### `FLUXER_DISCOVERY_ENABLED`
-Default `true`. The public guild [discovery](/http-api/discovery/) surface. With it off, discovery search, discovery join, and the three guild discovery application routes return 400 `DISCOVERY_DISABLED`.
+Default `true`. The public guild [discovery](/http-api/discovery/) surface. With it off, discovery search, discovery join, and the guild discovery application routes return 400 `DISCOVERY_DISABLED`.
#### `FLUXER_DISCOVERY_MIN_MEMBER_COUNT`
@@ -1428,7 +1428,7 @@ Default empty. Extra trusted addresses for that gate. Every entry must parse as
Default `3600`. How often the edge IP list refreshes. Accepts 60 to 86400.
-`media-proxy` range-checks two values at startup and then reads them nowhere: `FLUXER_MEDIA_PROXY_UPLOAD_RELAY_BUFFERED_RETRY_BYTES` and `FLUXER_MEDIA_PROXY_UPLOAD_RELAY_BUFFERED_RETRY_TOTAL_BYTES`. A bad value still fails the boot.
+`media-proxy` range-checks these values at startup and then reads them nowhere: `FLUXER_MEDIA_PROXY_UPLOAD_RELAY_BUFFERED_RETRY_BYTES` and `FLUXER_MEDIA_PROXY_UPLOAD_RELAY_BUFFERED_RETRY_TOTAL_BYTES`. A bad value still fails the boot.
## Gateway settings
@@ -1520,7 +1520,7 @@ Default `16`. The ceiling of the scheduler clamp. Compose forwards it to `gatewa
The Gateway entrypoint derives the scheduler counts before the BEAM starts. It reads the container CPU quota, clamps the result between `FLUXER_ERLANG_SCHEDULERS_MIN` and `FLUXER_ERLANG_SCHEDULERS_MAX`, exports the answer as `FLUXER_ERLANG_SCHEDULERS`, and derives `FLUXER_ERLANG_DIRTY_CPU_SCHEDULERS` from it. `vm.args.src` then substitutes both into `+S` and `+SDcpu`.
-The two clamp bounds reach the Gateway from `.env`. To pin the count, set `FLUXER_ERLANG_SCHEDULERS` on the `gateway` service in `docker-compose.yml`, which skips the clamp.
+The clamp bounds reach the Gateway from `.env`. To pin the count, set `FLUXER_ERLANG_SCHEDULERS` on the `gateway` service in `docker-compose.yml`, which skips the clamp.
The Gateway protocol version is `1`. The Gateway answers any other value in `?v=` with 101, then a close frame reading `Invalid API version`. [Gateway overview](/gateway/overview/) has the close code.
@@ -1660,9 +1660,9 @@ Become `FLUXER_LIVEKIT_API_KEY` and `FLUXER_LIVEKIT_API_SECRET`, and LiveKit's o
## Keys Compose does not forward
-`.env.example` names every variable `docker-compose.yml` reads from `.env`. The Node override table has 278 names, on top of roughly ninety Rust-only and twenty-five Erlang-only names, and Compose forwards a fraction of them. A name below reaches a service only through a Compose override file that adds it to that service's environment block.
+`.env.example` names every variable `docker-compose.yml` reads from `.env`. Compose forwards a fraction of the Node override names, the Rust-only names and the Erlang-only names. A name below reaches a service only through a Compose override file that adds it to that service's environment block.
-#### `FLUXER_AUTH_BLUESKY_` and the six names under it
+#### `FLUXER_AUTH_BLUESKY_` and the names under it
Bluesky login defaults off. The admin dashboard configures it too, under Runtime Integrations.
@@ -1674,7 +1674,7 @@ Mobile push cannot be configured at all from the example.
The only way to change log verbosity.
-#### `FLUXER_APP_PRODUCT_NAME` and the six branding URLs
+#### `FLUXER_APP_PRODUCT_NAME` and the branding URLs
Branding is otherwise admin-dashboard only.
@@ -1732,11 +1732,11 @@ Credentials for the [Admin API](/admin-api/).
Each write publishes a refresh on the key-value pub/sub channel, so other processes drop their cached copy without a restart.
-Two settings exist only in the dashboard: attachment decay, which defaults to on, and the inactivity deletion threshold, which defaults to 365 days.
+These settings exist only in the dashboard: attachment decay, which defaults to on, and the inactivity deletion threshold, which defaults to 365 days.
## Services
-The stack runs 25 containers on one Docker bridge network, which is private to the stack.
+The stack runs its containers on one Docker bridge network, which is private to the stack.
| Service | Image | What it does |
| --- | --- | --- |
@@ -1754,7 +1754,7 @@ The stack runs 25 containers on one Docker bridge network, which is private to t
| gifs, gifs-shard | fluxer-gifs | GIF provider access |
| unfurl, unfurl-shard | fluxer-unfurl | Link unfurling |
| postgres | postgres:16-alpine | The database |
-| valkey | valkey/valkey:8.1-alpine | The key-value store and pub/sub bus, and the two deletion queues |
+| valkey | valkey/valkey:8.1-alpine | The key-value store and pub/sub bus, and the deletion queues |
| nats | nats:2.14-alpine | Core messaging, and the JetStream streams holding queued background jobs |
| meilisearch | getmeili/meilisearch:v1.12 | The search index |
| seaweedfs | chrislusf/seaweedfs:4.34 | S3-compatible object storage |
@@ -1763,9 +1763,9 @@ The stack runs 25 containers on one Docker bridge network, which is private to t
The edge and LiveKit are the only services that publish ports. The edge publishes 80/tcp, 443/tcp, 443/udp, or one plain-HTTP port under the overlay. LiveKit publishes 7881/tcp and 7882/udp. Everything else is reachable only over the bridge network.
-`api` is the one service an operator configures directly, through the shared environment block. `worker`, `gateway`, `app-proxy`, and `media-proxy` are touched rarely, `worker` for lane concurrency, `app-proxy` for CSP extras, and `media-proxy` for transform limits. The edge takes only the three `FLUXER_EDGE_` variables, `postgres` only the password, `meilisearch` only the master key, `valkey` only the two `FLUXER_VALKEY_` tuning values, and `livekit` only the key pair and the two port variables. `static-proxy` reads no environment variables, and the remaining services need none.
+`api` is the one service an operator configures directly, through the shared environment block. `worker`, `gateway`, `app-proxy`, and `media-proxy` are touched rarely, `worker` for lane concurrency, `app-proxy` for CSP extras, and `media-proxy` for transform limits. The edge takes only the `FLUXER_EDGE_` variables, `postgres` only the password, `meilisearch` only the master key, `valkey` only the `FLUXER_VALKEY_` tuning values, and `livekit` only the key pair and the port variables. `static-proxy` reads no environment variables, and the remaining services need none.
-The five internal services each run a router, which takes requests and holds no state, and one shard, which holds the caches and the database connections. The shipped stack fixes `FLUXER_SVC_SHARD_COUNT` at `1`.
+The internal services each run a router, which takes requests and holds no state, and one shard, which holds the caches and the database connections. The shipped stack fixes `FLUXER_SVC_SHARD_COUNT` at `1`.
## Resources
@@ -1885,7 +1885,7 @@ Default `256mb`. The ceiling for `unfurl-shard`. Fetches remote pages, so a slow
Default `256mb`. The ceiling for `admin`. Serves the dashboard and proxies no media.
-A reservation goes to the four services whose loss takes the instance down, so the kernel reclaims from everything else first. All are optional.
+A reservation goes to the services whose loss takes the instance down, so the kernel reclaims from everything else first. All are optional.
#### `FLUXER_POSTGRES_MEMORY_RESERVATION`
@@ -1913,7 +1913,7 @@ No default. The V8 old-space ceiling for `api`, in MB. Appended to `NODE_OPTIONS
No default. The V8 old-space ceiling for `worker`, in MB. Same rule against `FLUXER_WORKER_MEMORY_LIMIT`.
-Six names on the Postgres command line tune the bundled server. Keep them consistent with `FLUXER_POSTGRES_MEMORY_LIMIT`. All are optional.
+The names on the Postgres command line tune the bundled server. Keep them consistent with `FLUXER_POSTGRES_MEMORY_LIMIT`. All are optional.
#### `FLUXER_POSTGRES_SERVER_MAX_CONNECTIONS`
@@ -1949,11 +1949,11 @@ Default `192mb`. The dataset ceiling. Bounds stored keys only. Client buffers, r
#### `FLUXER_VALKEY_MAXMEMORY_POLICY`
-Default `noeviction`. What happens to a write above the ceiling. Under `noeviction` an over-limit write returns an OOM error to the caller. Under any eviction policy Valkey can drop the two deletion queues and the distributed locks. Every lock has a TTL, and the worker rebuilds both queues from the users table within a day. [Volumes and buckets](#volumes-and-buckets) has what dropping a queue costs.
+Default `noeviction`. What happens to a write above the ceiling. Under `noeviction` an over-limit write returns an OOM error to the caller. Under any eviction policy Valkey can drop the deletion queues and the distributed locks. Every lock has a TTL, and the worker rebuilds both queues from the users table within a day. [Volumes and buckets](#volumes-and-buckets) has what dropping a queue costs.
No service sets a CPU limit, a CPU reservation or `cpu_shares`, so every container sees the host's full CPU count. Bound the Gateway's scheduler count with `FLUXER_ERLANG_SCHEDULERS_MIN` and `FLUXER_ERLANG_SCHEDULERS_MAX`, or pin it with `FLUXER_ERLANG_SCHEDULERS` and `FLUXER_ERLANG_DIRTY_CPU_SCHEDULERS` from [Gateway settings](#gateway-settings).
-On a host smaller than the 16 GB the defaults assume, lower the four large ceilings and the Postgres tuning together. The block below is the 8 GB profile.
+On a host smaller than the 16 GB the defaults assume, lower the large ceilings and the Postgres tuning together. The block below is the 8 GB profile.
```bash
FLUXER_POSTGRES_MEMORY_LIMIT=2560mb
@@ -2017,7 +2017,7 @@ CORS origins are exactly the app and marketing endpoints. Serving the client fro
| --- | --- | --- |
| postgres-data | Every account, message, and configuration row | Yes |
| seaweedfs-data | Every uploaded file | Yes |
-| valkey-data | The two deletion queues, held locks, and cached values | Yes |
+| valkey-data | The deletion queues, held locks, and cached values | Yes |
| nats-data | The JetStream `JOBS` and `JOBS_DLQ` streams, on file storage | Yes |
| edge-data | Issued TLS certificates | Optional, a loss only costs a re-issue |
| edge-config | The edge's own state | No |
@@ -2029,7 +2029,7 @@ CORS origins are exactly the app and marketing endpoints. Serving the client fro
Volume names are prefixed with the Compose project name, so `postgres-data` is `fluxer_postgres-data` on the host.
-`seaweedfs-init` creates five buckets and exits.
+`seaweedfs-init` creates these buckets and exits.
| Bucket | Holds |
| --- | --- |
@@ -2039,4 +2039,4 @@ Volume names are prefixed with the Compose project name, so `postgres-data` is `
| fluxer-reports | Abuse report evidence, and NCMEC payloads where that integration is on |
| fluxer-harvests | User and guild data archives |
-An attachment lands in `fluxer-uploads` first. Once processing succeeds, Fluxer copies it into `fluxer` and deletes the original. `FLUXER_S3_BUCKET_STATIC` names a sixth bucket that `seaweedfs-init` skips and that the stack's mode never reads.
+An attachment lands in `fluxer-uploads` first. Once processing succeeds, Fluxer copies it into `fluxer` and deletes the original. `FLUXER_S3_BUCKET_STATIC` names a further bucket that `seaweedfs-init` skips and that the stack's mode never reads.
diff --git a/fluxer_docs/src/content/docs/operator/get-started.mdx b/fluxer_docs/src/content/docs/operator/get-started.mdx
index 2a88ddca7..4590f2c2a 100644
--- a/fluxer_docs/src/content/docs/operator/get-started.mdx
+++ b/fluxer_docs/src/content/docs/operator/get-started.mdx
@@ -102,7 +102,7 @@ curl -fsSLO https://fluxer.dev/install.sh.sha256
sha256sum -c install.sh.sha256
```
-On macOS, the same two downloads and:
+On macOS, the same downloads and:
```bash
shasum -a 256 -c install.sh.sha256
@@ -140,7 +140,7 @@ The run prints one line per phase and ends with the URL to open.
### Serving on another port
-The installer writes an `.env` for `https` on `443` and has no flag for another port. Two lines change it, the port of the public address and the publish that answers on it:
+The installer writes an `.env` for `https` on `443` and has no flag for another port. The lines below change it, the port of the public address and the publish that answers on it:
```ini
FLUXER_PUBLIC_PORT=8443
@@ -236,7 +236,7 @@ The desktop client opens the hosted web app for its release channel, so reach yo
## Backups
-An instance comes back from two artifacts and its `.env`: a dump of the database, which covers `postgres-data`, and a tarball of `seaweedfs-data`, which holds every upload, avatar, report and harvest. [Volumes and buckets](/operator/configuration/#volumes-and-buckets) lists every volume and what it holds.
+An instance comes back from these artifacts and its `.env`: a dump of the database, which covers `postgres-data`, and a tarball of `seaweedfs-data`, which holds every upload, avatar, report and harvest. [Volumes and buckets](/operator/configuration/#volumes-and-buckets) lists every volume and what it holds.
The dump costs no downtime, so run it on a schedule while the stack serves:
diff --git a/fluxer_docs/src/content/docs/operator/reverse-proxy.mdx b/fluxer_docs/src/content/docs/operator/reverse-proxy.mdx
index 7a2c6ad9b..4d90526fb 100644
--- a/fluxer_docs/src/content/docs/operator/reverse-proxy.mdx
+++ b/fluxer_docs/src/content/docs/operator/reverse-proxy.mdx
@@ -93,11 +93,11 @@ Browsers send it, and the admin dashboard refuses a mutating request with a cros
The instance sends its own policy, and its nonce is what lets the web app boot. Browsers enforce every policy they receive.
-Every worked configuration below answers all seven.
+Every worked configuration below answers them all.
## The public address
-Three variables tell the instance the address browsers use. Fluxer reads neither `X-Forwarded-Proto` nor `X-Forwarded-Host`, so you keep these correct by hand:
+The variables below tell the instance the address browsers use. Fluxer reads neither `X-Forwarded-Proto` nor `X-Forwarded-Host`, so you keep these correct by hand:
```ini
FLUXER_DOMAIN=chat.example.com
@@ -107,7 +107,7 @@ FLUXER_PUBLIC_PORT=443
They stay `https` on `443` even though the instance itself speaks plain HTTP on `8080`. Clients read every base URL from the discovery document the API builds out of these values.
-Serving on a port other than `443` means changing one of those three:
+Serving on a port other than `443` means changing one of those variables:
```ini
FLUXER_PUBLIC_PORT=8443
@@ -261,7 +261,7 @@ networks:
external: true
```
-Then list all three files so every command picks them up:
+Then list all the files so every command picks them up:
```ini
COMPOSE_FILE=docker-compose.yml:docker-compose.proxy.yml:traefik.compose.yml
@@ -269,7 +269,7 @@ COMPOSE_FILE=docker-compose.yml:docker-compose.proxy.yml:traefik.compose.yml
## HAProxy
-One frontend, one backend, and four timeouts:
+One frontend, one backend, and the timeouts:
```haproxy
defaults
@@ -296,7 +296,7 @@ backend fluxer_edge
## Apache httpd
-This configuration needs three modules: `mod_proxy`, `mod_proxy_http`, and `mod_headers`. Since 2.4.47 `mod_proxy_http` handles the WebSocket upgrade itself, so `mod_proxy_wstunnel` is not needed.
+This configuration needs these modules: `mod_proxy`, `mod_proxy_http`, and `mod_headers`. Since 2.4.47 `mod_proxy_http` handles the WebSocket upgrade itself, so `mod_proxy_wstunnel` is not needed.
```apache
@@ -440,7 +440,7 @@ Goes to `app-proxy:8080` unchanged.
Pass the query string on `/gateway` through untouched. Clients always send `?v=`, `?encoding=`, `?compress=` and `?stream=`, and `1` is the only version the Gateway accepts.
-Apple and Google fetch the two association files at those fixed paths, for saved-password autofill in the iOS apps and link handling in the Android ones. The last entry already has all four, so a proxy that forwards `/` needs no extra rule. A proxy that forwards a named path allowlist has to list these four paths along with everything else that entry covers.
+Apple and Google fetch the association files at those fixed paths, for saved-password autofill in the iOS apps and link handling in the Android ones. The last entry already has them all, so a proxy that forwards `/` needs no extra rule. A proxy that forwards a named path allowlist has to list these paths along with everything else that entry covers.
`/_metrics` on the API, Media Proxy, and Gateway, plus `/_health/ready`, `/_health/drain`, and `/_health/undrain` on the Gateway, are gated to loopback and are unreachable through any proxy. The probes that work through a proxy are `/_health`, `/api/_health`, `/gateway/_health`, and `/media/_health`.
@@ -491,4 +491,4 @@ Hosting LiveKit on a hostname other than `FLUXER_DOMAIN` means widening the Cont
FLUXER_CSP_EXTRA_CONNECT_SRC=wss://livekit.example.com:7881
```
-`app-proxy` builds the policy and reads its environment at container start, so apply the change with `docker compose up -d app-proxy`. `docker compose restart app-proxy` reuses the existing container with its old environment. [Content Security Policy](/operator/configuration/#content-security-policy) has the other ten variables.
+`app-proxy` builds the policy and reads its environment at container start, so apply the change with `docker compose up -d app-proxy`. `docker compose restart app-proxy` reuses the existing container with its old environment. [Content Security Policy](/operator/configuration/#content-security-policy) has the other variables.
diff --git a/fluxer_docs/src/content/docs/operator/upgrading.mdx b/fluxer_docs/src/content/docs/operator/upgrading.mdx
index d93c19974..ae70b2953 100644
--- a/fluxer_docs/src/content/docs/operator/upgrading.mdx
+++ b/fluxer_docs/src/content/docs/operator/upgrading.mdx
@@ -8,7 +8,7 @@ description: Upgrading and rolling back an instance with the installer, what the
We are grateful to everyone supporting the project through [Fluxer Plutonium](https://fluxer.app/plutonium) or [donations](https://fluxer.app/donate). All of our code is free and open source on [GitHub](https://github.com/fluxerapp/fluxer). The Operator Pass is coming, and adds a direct line to the team for help and feedback.
:::
-An upgrade moves an instance to a newer release. `install.sh --update` does all of it: back up, download the new stack files, pull the new images, recreate the containers that changed. The stack files are the three compose files, the `Caddyfile` and `.env.example`. Allow about twenty minutes, most of it the backup and the pull.
+An upgrade moves an instance to a newer release. `install.sh --update` does all of it: back up, download the new stack files, pull the new images, recreate the containers that changed. The stack files are the compose files, the `Caddyfile` and `.env.example`. Allow about twenty minutes, most of it the backup and the pull.
| Requirement | Value |
| --- | --- |
@@ -33,7 +33,7 @@ The repository holds no ref by the name of a pinned image tag. A release tags ea
| --- | --- |
| Mint | Writes any key the refreshed stack requires that `.env` does not hold, listed under [Run the upgrade](#run-the-upgrade) |
| Record | Writes the image references, the image ID each container has, and the tag `.env` names, into a new record directory |
-| Save | Copies `.env` and the five stack files into that record |
+| Save | Copies `.env` and the stack files into that record |
| Dump | Dumps the database with the stack still serving, and checks the custom-format header on the result |
| Copy | Stops the stack, copies the uploads volume, and starts it again on the images it was already running |
| Fetch | Downloads the stack files at the ref into a staging directory inside the working directory |
@@ -79,7 +79,7 @@ Every step above sits in the script beside a comment holding the command that do
## Keep a local compose change
-The Place step replaces all five stack files with the copies at the ref. An edit made directly in `docker-compose.yml`, `docker-compose.proxy.yml`, `tunnel.compose.yml` or the `Caddyfile` is gone once that step runs. The run prints one line for the whole step and never names the files it replaced. `--dry-run` names every one of them, as `changes`, `unchanged` or `is new`.
+The Place step replaces all the stack files with the copies at the ref. An edit made directly in `docker-compose.yml`, `docker-compose.proxy.yml`, `tunnel.compose.yml` or the `Caddyfile` is gone once that step runs. The run prints one line for the whole step and never names the files it replaced. `--dry-run` names every one of them, as `changes`, `unchanged` or `is new`.
The supported way to hold a local choice is a separate file, listed in `COMPOSE_FILE` in `.env`:
@@ -87,7 +87,7 @@ The supported way to hold a local choice is a separate file, listed in `COMPOSE_
COMPOSE_FILE=docker-compose.yml:docker-compose.proxy.yml:local.compose.yml
```
-Compose merges the files left to right, so `local.compose.yml` wins over the ones before it. An upgrade refreshes the five stack files and nothing else, so a file outside that set survives every upgrade untouched. A record copies `.env` and those five files, and no other file from the working directory. An override file is never backed up with them, so keep it wherever the rest of the configuration lives.
+Compose merges the files left to right, so `local.compose.yml` wins over the ones before it. An upgrade refreshes the stack files and nothing else, so a file outside that set survives every upgrade untouched. A record copies `.env` and those files, and no other file from the working directory. An override file is never backed up with them, so keep it wherever the rest of the configuration lives.
`.env` needs no such file. The only value either upgrade mode replaces there is `FLUXER_IMAGE_TAG`, and the Mint step adds a required key the file does not hold at all.
@@ -180,7 +180,7 @@ wss://chat.example.com/livekit
Only the path is wrong. `docker-compose.yml` derives `https://chat.example.com/livekit` when `.env` sets no `FLUXER_LIVEKIT_URL`, and the client rewrites a leading `http` to `ws` itself, so `https` and `wss` both work.
-The address browsers dial is a column on a voice server row in the database. No upgrade migrates that row, and it is corrected in two places.
+The address browsers dial is a column on a voice server row in the database. No upgrade migrates that row, and it is corrected in the places below.
First `.env`. When it sets `FLUXER_LIVEKIT_URL`, put the new path in that line and run `docker compose up -d api`. `api` writes that value onto the default voice server row at every start, so a row corrected in the dashboard while `.env` still names the old path goes back to the old path on the next restart. Removing the line is also correct, and Compose then derives the URL from `FLUXER_PUBLIC_ORIGIN`, or from the scheme and the domain.
@@ -198,9 +198,9 @@ Each upgrade writes one record directory, named `record-` and a UTC stamp, under
| `seaweedfs-data.tgz` | Every upload, avatar, report and harvest |
| `.env` | Every secret the instance was built with |
-The five stack files sit in the record beside them, so a rollback puts back the exact files the instance was running. The record directory is created `0700` and the `.env` copy inside it is `0600`. Keep records wherever you already keep secrets.
+The stack files sit in the record beside them, so a rollback puts back the exact files the instance was running. The record directory is created `0700` and the `.env` copy inside it is `0600`. Keep records wherever you already keep secrets.
-Nothing else is copied. The dump covers `postgres-data`. The other five volumes either rebuild themselves or hold queued work, and [Volumes and buckets](/operator/configuration/#volumes-and-buckets) lists all seven with what each one holds.
+Nothing else is copied. The dump covers `postgres-data`. The other volumes either rebuild themselves or hold queued work, and [Volumes and buckets](/operator/configuration/#volumes-and-buckets) lists them all with what each one holds.
Put a dump on a timer as well. On Linux and macOS, this crontab line writes one a night and keeps two weeks of them:
@@ -223,9 +223,9 @@ FLUXER_S3_REGION=eu-central-1
FLUXER_S3_FORCE_PATH_STYLE=false
```
-`FLUXER_S3_PUBLIC_ENDPOINT` follows `FLUXER_S3_ENDPOINT` when it is not set on its own, and the credentials stay `FLUXER_S3_ACCESS_KEY` and `FLUXER_S3_SECRET_KEY`. `FLUXER_S3_BUCKET_CDN`, `FLUXER_S3_BUCKET_UPLOADS`, `FLUXER_S3_BUCKET_DOWNLOADS`, `FLUXER_S3_BUCKET_REPORTS` and `FLUXER_S3_BUCKET_HARVESTS` name the five buckets. The bundled object store creates whichever names they hold, and a store outside the stack needs those buckets to exist already.
+`FLUXER_S3_PUBLIC_ENDPOINT` follows `FLUXER_S3_ENDPOINT` when it is not set on its own, and the credentials stay `FLUXER_S3_ACCESS_KEY` and `FLUXER_S3_SECRET_KEY`. `FLUXER_S3_BUCKET_CDN`, `FLUXER_S3_BUCKET_UPLOADS`, `FLUXER_S3_BUCKET_DOWNLOADS`, `FLUXER_S3_BUCKET_REPORTS` and `FLUXER_S3_BUCKET_HARVESTS` name the buckets. The bundled object store creates whichever names they hold, and a store outside the stack needs those buckets to exist already.
-`FLUXER_KV_URL`, `FLUXER_NATS_URL`, `FLUXER_SEARCH_URL` and `FLUXER_LIVEKIT_INTERNAL_URL` move the other four bundled services the same way, and `FLUXER_NATS_JETSTREAM_URL` and `FLUXER_SVC_NATS_URL` follow `FLUXER_NATS_URL` when they are not set on their own.
+`FLUXER_KV_URL`, `FLUXER_NATS_URL`, `FLUXER_SEARCH_URL` and `FLUXER_LIVEKIT_INTERNAL_URL` move the other bundled services the same way, and `FLUXER_NATS_JETSTREAM_URL` and `FLUXER_SVC_NATS_URL` follow `FLUXER_NATS_URL` when they are not set on their own.
Pointing the stack elsewhere leaves the bundled service defined and running with nothing reading it. Take it out with an override file listed in `COMPOSE_FILE`, which [Keep a local compose change](#keep-a-local-compose-change) describes, rather than by editing `docker-compose.yml`, which the Place step replaces on every upgrade.
@@ -319,7 +319,7 @@ Run `sh install.sh --update` afterwards to finish the move on the refreshed file
A pinned instance also pins its stack files. [Match the images to the stack files](#match-the-images-to-the-stack-files) has the `--ref` a pinned tag needs.
-The tag applies only to the eleven Fluxer images. `caddy`, `postgres`, `valkey`, `nats`, `meilisearch`, `seaweedfs` and `livekit` are pinned in `docker-compose.yml`, and they move only when an upgrade refreshes that file.
+The tag applies only to the Fluxer images. `caddy`, `postgres`, `valkey`, `nats`, `meilisearch`, `seaweedfs` and `livekit` are pinned in `docker-compose.yml`, and they move only when an upgrade refreshes that file.
## Change the domain or the passkey relying party
diff --git a/fluxer_docs/src/content/docs/topics/captcha.md b/fluxer_docs/src/content/docs/topics/captcha.md
index 08547fed6..82788ea17 100644
--- a/fluxer_docs/src/content/docs/topics/captcha.md
+++ b/fluxer_docs/src/content/docs/topics/captcha.md
@@ -34,7 +34,7 @@ Create application, redeem gift, create private channel, and add group direct me
## Exemption
-Two exemptions skip the challenge. Fluxer tests both before it reads the token. A request that passes either one proceeds as though the instance had no provider configured.
+The exemptions below skip the challenge. Fluxer tests both before it reads the token. A request that passes either one proceeds as though the instance had no provider configured.
The instance account policy grants the `captcha_exempt` capability to a contact address. A policy rule matches the address itself or the domain it belongs to, so one grant can cover a whole domain. Fluxer tests the capability against the resolved account's email address alone. An unauthenticated request never matches this exemption.
diff --git a/fluxer_docs/src/content/docs/topics/locales.md b/fluxer_docs/src/content/docs/topics/locales.md
index fb314f627..ea041cf76 100644
--- a/fluxer_docs/src/content/docs/topics/locales.md
+++ b/fluxer_docs/src/content/docs/topics/locales.md
@@ -59,7 +59,7 @@ An account created by password registration stores the locale negotiated from it
Fluxer splits the header on commas. It trims each member and then splits it on semicolons. The text before the first semicolon is the language range, and Fluxer reads a `q=` weight from only the first parameter after it. A member with no readable `q=` value has weight 1. Fluxer orders the members by descending weight, and members of equal weight keep their header order.
-Fluxer then runs two passes over that ordered list.
+Fluxer then runs the passes below over that ordered list.
The first pass takes the earliest member whose range names a registry value exactly. Fluxer trims the range, replaces every underscore with a hyphen, and lowercases it before comparing, so `EN-GB` and `en_gb` both name `en-GB`. The bare tags `en` and `sv` are registered aliases for `en-US` and `sv-SE` and match in this pass.
@@ -75,7 +75,7 @@ The second pass runs only when the first selects nothing. It reduces each member
Under those preferences, `en-AU` selects `en-US` and `pt-PT` selects `pt-BR`. A language subtag without a declared preference selects the first registry value whose tag begins with that subtag and a hyphen.
-Every registry tag with a hyphen begins with one of those five subtags, so `de-AT` and `xx-YY` both select nothing. A weight of 0 orders a member last, and that member can still be selected. A range of `*` matches no registry value.
+Every registry tag with a hyphen begins with one of those subtags, so `de-AT` and `xx-YY` both select nothing. A weight of 0 orders a member last, and that member can still be selected. A range of `*` matches no registry value.
The resolved locale selects the localised `message` in an [error response](/http-api/#error-response) and in each element of a validation `errors` array.
diff --git a/fluxer_docs/src/content/docs/topics/rate-limits.md b/fluxer_docs/src/content/docs/topics/rate-limits.md
index 3ba35f99a..7b96e720e 100644
--- a/fluxer_docs/src/content/docs/topics/rate-limits.md
+++ b/fluxer_docs/src/content/docs/topics/rate-limits.md
@@ -4,7 +4,7 @@ title: Rate limits
description: The route and global buckets, the denial body, and the rate limit headers.
---
-Fluxer bounds HTTP API traffic with two allowances, a per-route bucket and one global bucket. A bucket is one allowance counted over one window. Fluxer denies an over-allowance request with 429, `code` set to `RATE_LIMITED`, a [rate limit response object](#rate-limit-response-object), and the [rate limit headers](#rate-limit-headers).
+Fluxer bounds HTTP API traffic with a per-route bucket and one global bucket. A bucket is one allowance counted over one window. Fluxer denies an over-allowance request with 429, `code` set to `RATE_LIMITED`, a [rate limit response object](#rate-limit-response-object), and the [rate limit headers](#rate-limit-headers).
## Buckets and scope
@@ -16,9 +16,9 @@ A request that resolves no account is keyed by the client IP address, exactly fo
Fluxer also evaluates a route bucket against the global bucket unless the route declares that bucket exempt. The global bucket is keyed by the same identity, so a request that resolves no account consumes the global allowance of its client IP address.
-Seven buckets are exempt, and each is the only bucket its route declares: `webhook:execute::webhook_id`, `webhook:message_get::webhook_id`, `webhook:message_edit::webhook_id`, `webhook:message_delete::webhook_id`, `webhook:github::webhook_id`, `webhook:instatus::webhook_id`, and `stripe:webhook`. Those routes draw on no global allowance. The `user:group_dm:create` and `user:group_dm:recipient:add` buckets are exempt as well. Each sits on a route that already consumed a non-exempt bucket, so both routes still draw on the global allowance.
+These buckets are exempt, and each is the only bucket its route declares: `webhook:execute::webhook_id`, `webhook:message_get::webhook_id`, `webhook:message_edit::webhook_id`, `webhook:message_delete::webhook_id`, `webhook:github::webhook_id`, `webhook:instatus::webhook_id`, and `stripe:webhook`. Those routes draw on no global allowance. The `user:group_dm:create` and `user:group_dm:recipient:add` buckets are exempt as well. Each sits on a route that already consumed a non-exempt bucket, so both routes still draw on the global allowance.
-Every HTTP API and Admin API operation declares a bucket, apart from the seven [desktop download](/http-api/downloads/) routes, which declare none. A caller that sends no credential on a [Bluesky client document](/http-api/connections/#get-bluesky-client-metadata) is keyed by the client IP address.
+Every HTTP API and Admin API operation declares a bucket, apart from the [desktop download](/http-api/downloads/) routes, which declare none. A caller that sends no credential on a [Bluesky client document](/http-api/connections/#get-bluesky-client-metadata) is keyed by the client IP address.
The global window is one second. The default allowance is 50 requests per second, and an account holding the [`HIGH_GLOBAL_RATE_LIMIT`](/admin-api/users/#account-flags) flag receives 1,200 requests per second instead. The [`RATE_LIMIT_BYPASS`](/admin-api/users/#account-flags) flag exempts an account from the global bucket and from every route bucket. A successful response to that account has no rate limit header.
@@ -38,7 +38,7 @@ Every bucket is a leaky bucket. It admits at most the declared limit at once and
A route's rate limit middleware normally runs before its authentication policy, so an over-allowance request returns 429 `RATE_LIMITED` where the same request inside its allowance would return 401 or 403. Fluxer resolves the credential before either check, and both buckets are keyed by the authenticated account whenever one resolves.
:::
-Four routes charge a second bucket. [Create private channel](/http-api/users/private-channels/#create-private-channel) and [Add group direct message recipient](/http-api/channels/#add-group-direct-message-recipient) evaluate that second bucket after the authentication policy and the request validation, so an unauthenticated or malformed request is refused before it is consumed. Create private channel consumes `user:group_dm:create` only when the validated body supplies `recipients`. A one-to-one direct message create reaches no second bucket.
+The routes below charge a second bucket. [Create private channel](/http-api/users/private-channels/#create-private-channel) and [Add group direct message recipient](/http-api/channels/#add-group-direct-message-recipient) evaluate that second bucket after the authentication policy and the request validation, so an unauthenticated or malformed request is refused before it is consumed. Create private channel consumes `user:group_dm:create` only when the validated body supplies `recipients`. A one-to-one direct message create reaches no second bucket.
[Delete guild emoji](/http-api/guild-emojis/#delete-guild-emoji) and [Delete guild sticker](/http-api/guild-stickers/#delete-guild-sticker) declare both of their buckets ahead of the authentication policy and the request validation, so an unauthenticated or malformed request consumes the second bucket too. The `guild:emoji:delete:daily::guild_id` and `guild:sticker:delete:daily::guild_id` buckets draw on the global allowance, and one delete request evaluates it twice.
@@ -46,11 +46,11 @@ Four routes charge a second bucket. [Create private channel](/http-api/users/pri
When the global bucket denies a request authenticated by a non-bot account's user session token, Fluxer revokes that token before writing the 429. The client must authenticate again. A bot token, an OAuth2 access token, and an Admin API key are never revoked this way, and a route bucket denial never revokes a credential.
:::
-A deployment can disable both buckets through instance configuration. While they are disabled, no response has an `X-RateLimit-*` header and no request is refused with 429 `RATE_LIMITED`. That switch also turns off the two login allowances. Every other [limit enforced inside a handler](#limits-enforced-inside-a-handler) stays in force.
+A deployment can disable both buckets through instance configuration. While they are disabled, no response has an `X-RateLimit-*` header and no request is refused with 429 `RATE_LIMITED`. That switch also turns off the login allowances. Every other [limit enforced inside a handler](#limits-enforced-inside-a-handler) stays in force.
## Rate limit response object
-The denial body has the two members of the ordinary [error response](/http-api/#error-response) and two further members.
+The denial body has the members of the ordinary [error response](/http-api/#error-response) and two further members.
### Structure
@@ -123,7 +123,7 @@ These headers describe a rate limit decision. An operation that answers 429 retu
6 The leading 16 hexadecimal characters of the SHA-256 digest of the route's declared bucket name before any path parameter is substituted, so it identifies the route and never the caller
-A 429 from a limit enforced inside a handler has the other four route headers and no `X-RateLimit-Bucket`.
+A 429 from a limit enforced inside a handler has the other route headers and no `X-RateLimit-Bucket`.
:::note[A browser client reads none of these headers]
The [cross-origin policy](/http-api/#cross-origin-requests) exposes only `X-Fluxer-Version`, so a script running on an allowed origin observes the 429 status and the response body, and no header.
@@ -133,9 +133,9 @@ The [cross-origin policy](/http-api/#cross-origin-requests) exposes only `X-Flux
An allowance enforced inside a handler is keyed independently of the route bucket and of the global bucket, so exhausting it denies the request while both buckets still have room. The set below is complete.
-The `disable_rate_limits` deployment switch turns off the two login allowances along with both buckets. `relax_registration_rate_limits` turns off the three registration allowances. Every other allowance below is enforced on every deployment.
+The `disable_rate_limits` deployment switch turns off the login allowances along with both buckets. `relax_registration_rate_limits` turns off the registration allowances. Every other allowance below is enforced on every deployment.
-A denial takes one of two shapes. A send or submission allowance answers 429 with the [rate limit response object](#rate-limit-response-object) and the [rate limit headers](#rate-limit-headers) minus `X-RateLimit-Bucket`. A change allowance answers 400 `INVALID_FORM_BODY` with one [validation error](/http-api/#validation-error-object) entry whose `code` names the exhausted allowance.
+A denial takes one of the shapes below. A send or submission allowance answers 429 with the [rate limit response object](#rate-limit-response-object) and the [rate limit headers](#rate-limit-headers) minus `X-RateLimit-Bucket`. A change allowance answers 400 `INVALID_FORM_BODY` with one [validation error](/http-api/#validation-error-object) entry whose `code` names the exhausted allowance.
The 400 shape has no `retry_after` member, no `X-RateLimit-*` header, and no `Retry-After` header. The remaining delay appears only in the entry's localised `message`.
@@ -189,7 +189,7 @@ The Resend IP authorisation cooldown has no `X-RateLimit-*` header. It has a `Re
| [Complete login with TOTP](/http-api/authentication/#complete-login-with-totp) and [Complete login with WebAuthn MFA](/http-api/authentication/#complete-login-with-webauthn-mfa) | 5 per 5 minutes on one multi-factor attempt against one MFA ticket | `INVALID_CODE` |
| [Sudo mode](/http-api/users/mfa/#sudo-mode) with the `totp` method | 10 per 15 minutes on one multi-factor attempt | `INVALID_MFA_CODE` |
-Every Modify current user allowance is keyed by the authenticated account, and the bot tag allowance by the bot account, so an owner changing a bot's tag draws on the bot's allowance. The five guild member allowances are keyed by the guild and the member together, and one account holds a separate allowance in each guild. The login allowances are keyed by the account and by the MFA ticket respectively, and the sudo allowance by the account.
+Every Modify current user allowance is keyed by the authenticated account, and the bot tag allowance by the bot account, so an owner changing a bot's tag draws on the bot's allowance. The guild member allowances are keyed by the guild and the member together, and one account holds a separate allowance in each guild. The login allowances are keyed by the account and by the MFA ticket respectively, and the sudo allowance by the account.
Fluxer consumes every multi-factor allowance before it checks the code, so a correct code drawn against an exhausted allowance is reported exactly like a wrong one. A correct code clears the counter. The ticket allowance also destroys the MFA ticket as it denies, and the client restarts from [Log in with a password](/http-api/authentication/#log-in-with-a-password).
diff --git a/fluxer_docs/src/content/docs/topics/uploads.md b/fluxer_docs/src/content/docs/topics/uploads.md
index 2a63bfa58..f2188ad33 100644
--- a/fluxer_docs/src/content/docs/topics/uploads.md
+++ b/fluxer_docs/src/content/docs/topics/uploads.md
@@ -1,12 +1,12 @@
---
# SPDX-License-Identifier: AGPL-3.0-or-later
title: Attachment uploads
-description: The pre-upload plan, its two modes, and how a client claims the result.
+description: The pre-upload plan, its modes, and how a client claims the result.
---
Fluxer accepts an attachment inline as a `files[n]` part of a [multipart request](/http-api/#request-body-formats), or pre-uploaded before the message exists. The pre-upload operations and objects live on the [Messages resource](/http-api/messages/).
-The flow has two modes and the server chooses between them from the declared byte count. A file of 10485760 bytes or less, the 10 MiB threshold, is planned as a singlepart upload and is finished as soon as its bytes are stored. A larger file is planned as a multipart upload and needs an explicit completion call. Both modes end the same way, by naming the resulting `upload_filename` in an ordinary message operation.
+The server chooses the flow's mode from the declared byte count. A file of 10485760 bytes or less, the 10 MiB threshold, is planned as a singlepart upload and is finished as soon as its bytes are stored. A larger file is planned as a multipart upload and needs an explicit completion call. Both modes end the same way, by naming the resulting `upload_filename` in an ordinary message operation.
:::caution[Pre-uploads can be switched off]
When a deployment disables them, the plan request and the completion request both answer 403 `FEATURE_TEMPORARILY_DISABLED` ahead of the channel, permission, and size checks, and the [instance features object](/http-api/instance/#instance-features-object) reports `presigned_attachment_uploads` false. A client falls back to the inline multipart path.
@@ -43,7 +43,7 @@ A plan is bounded at 10,000 parts. One that would need more returns 400 `FILE_SI
Each `upload_url` is a `PUT` target with its own authorisation in its query string. A direct storage URL has the object store's own signature and a relay URL has the signed relay capability in its `t` parameter, so neither shape reads an `Authorization` header.
-The direct storage capability signs the exact byte count, so a `PUT` of any other length is rejected. A relay capability bounds the length at the same value and answers 413 above it, and the relay applies its own body ceiling, 500 MiB by default, on top of that. Without a declared `Content-Length`, the relay spools the body to the smaller of the two bounds and answers 413 past it. The relay answers 401 when the capability is missing, malformed, or expired. Either way, a client sends exactly the authorised byte count.
+The direct storage capability signs the exact byte count, so a `PUT` of any other length is rejected. A relay capability bounds the length at the same value and answers 413 above it, and the relay applies its own body ceiling, 500 MiB by default, on top of that. Without a declared `Content-Length`, the relay spools the body to the smaller of the bounds and answers 413 past it. The relay answers 401 when the capability is missing, malformed, or expired. Either way, a client sends exactly the authorised byte count.
A singlepart transfer sends the whole file and must send the entry's `content_type` as its `Content-Type` header. The relay takes the media type from its capability and ignores the header. A multipart part transfer sends only that part's bytes and has no signed media type.
@@ -69,7 +69,7 @@ An issued upload URL authorises writing one object or one part. A client treats
The channel, permission, and communication checks of the plan request run again, and the operation answers 403 `FEATURE_TEMPORARILY_DISABLED` when pre-uploads are switched off.
-Two failures are reported as [validation error object](/http-api/#validation-error-object) entries on a 400 `INVALID_FORM_BODY` response.
+The failures below are reported as [validation error object](/http-api/#validation-error-object) entries on a 400 `INVALID_FORM_BODY` response.
| Code | Path | Condition |
| --- | --- | --- |
@@ -108,7 +108,7 @@ A `content_type` containing `jpeg` or `jpg` in any case is accepted without insp
The operation answers 204 once the image is accepted. Fluxer absorbs a transient storage failure, so a 204 confirms acceptance alone.
-[Create stream preview upload URL](/http-api/streams/#create-stream-preview-upload-url) issues a reusable `PUT` capability for the same purpose. Its `content_type` must contain `jpeg` or `jpg` in any case, and every other value returns 400 `PREVIEW_MUST_BE_JPEG`. It answers with `upload_url`, `method` fixed to `PUT`, the `content_type` the client sends, `expires_at`, `expires_in`, and `max_bytes`, which is always 1000000. A direct storage capability lasts one day and a relay capability lasts the relay token lifetime, so `expires_in` differs between the two shapes. The capability writes the same object every time it is used, and a publisher refreshes the thumbnail without asking for a new URL.
+[Create stream preview upload URL](/http-api/streams/#create-stream-preview-upload-url) issues a reusable `PUT` capability for the same purpose. Its `content_type` must contain `jpeg` or `jpg` in any case, and every other value returns 400 `PREVIEW_MUST_BE_JPEG`. It answers with `upload_url`, `method` fixed to `PUT`, the `content_type` the client sends, `expires_at`, `expires_in`, and `max_bytes`, which is always 1000000. A direct storage capability lasts one day and a relay capability lasts the relay token lifetime, so `expires_in` differs between the shapes. The capability writes the same object every time it is used, and a publisher refreshes the thumbnail without asking for a new URL.
Nothing inspects the bytes written through that capability. A relay capability still refuses a declared length above `max_bytes` with 413, and a direct storage capability enforces nothing beyond its signed media type. A publisher encodes a valid JPEG of at most 1000000 bytes itself.
diff --git a/fluxer_docs/src/content/docs/voice/index.md b/fluxer_docs/src/content/docs/voice/index.md
index ece0db22c..7b0832f72 100644
--- a/fluxer_docs/src/content/docs/voice/index.md
+++ b/fluxer_docs/src/content/docs/voice/index.md
@@ -42,7 +42,7 @@ The grant lives for 600 seconds.
One room is one voice channel, and one participant is one voice connection. A member holding several connections in the same channel is several participants in the same room, which is how one account is present from more than one device.
-The grant also names the track sources the connection may publish. SPEAK allows the microphone source, and STREAM allows the camera source together with the two screen share sources. A server-deafened connection may neither publish nor subscribe.
+The grant also names the track sources the connection may publish. SPEAK allows the microphone source, and STREAM allows the camera source together with the screen share sources. A server-deafened connection may neither publish nor subscribe.
## Deployment feature state
@@ -99,7 +99,7 @@ A new voice channel stores a `bitrate` of 64000. The ceiling is 96000, and the `
4 MUTE_MEMBERS covers the `mute` field and DEAFEN_MEMBERS the `deaf` field of the [guild member update object](/http-api/guild-members/#guild-member-update-object). `PRIORITY_SPEAKER` and `USE_VAD` are defined and assignable [permission bits](/http-api/permissions/#permissions) that no HTTP route and no Gateway command evaluates
-[ADMINISTRATOR](/http-api/permissions/) resolves to the complete mask before any channel overwrite is applied, so it satisfies every row of that table. Two states skip the VIEW_CHANNEL and CONNECT check. A member the guild is already moving is admitted. So is a member holding virtual access to the channel. The guild grants virtual access to a connected member that loses VIEW_CHANNEL or that a moderator moves into a channel it cannot see. Virtual access also grants SPEAK and STREAM in that channel on its own.
+[ADMINISTRATOR](/http-api/permissions/) resolves to the complete mask before any channel overwrite is applied, so it satisfies every row of that table. The states below skip the VIEW_CHANNEL and CONNECT check. A member the guild is already moving is admitted. So is a member holding virtual access to the channel. The guild grants virtual access to a connected member that loses VIEW_CHANNEL or that a moderator moves into a channel it cannot see. Virtual access also grants SPEAK and STREAM in that channel on its own.
A grant is evaluated when it is issued, and the guild re-evaluates a connection that is already open. Joining, moving, a region change, a role edit, an overwrite edit, and a member role change each recompute SPEAK and STREAM.
@@ -151,9 +151,9 @@ The [Calls resource](/http-api/calls/) owns its HTTP surface, which reads whethe
A recipient's [incoming call flags](/http-api/users/#incoming-call-flags) decide whether it is rung. The flags can admit nobody, friends only, friends of friends, guild members, or everyone, and they can admit everyone silently.
-[Get call eligibility](/http-api/calls/#get-call-eligibility) applies two conditions of the caller's own before it reads that policy. A caller already connected to the channel's call is reported as not ringable. So is an unclaimed account in a direct message. The operation applies no recipient policy to a group direct message, and reports one as ringable unless the caller is already connected to its call.
+[Get call eligibility](/http-api/calls/#get-call-eligibility) applies conditions of the caller's own before it reads that policy. A caller already connected to the channel's call is reported as not ringable. So is an unclaimed account in a direct message. The operation applies no recipient policy to a group direct message, and reports one as ringable unless the caller is already connected to its call.
-[Ring call recipients](/http-api/calls/#ring-call-recipients) applies neither of those two conditions and evaluates the policy once per targeted recipient, in a group direct message as well as in a direct message. The result selects who is rung, so a recipient the policy excludes and a recipient it admits silently are both left out of the ringing set while the request still answers 204.
+[Ring call recipients](/http-api/calls/#ring-call-recipients) applies neither of those conditions and evaluates the policy once per targeted recipient, in a group direct message as well as in a direct message. The result selects who is rung, so a recipient the policy excludes and a recipient it admits silently are both left out of the ringing set while the request still answers 204.
## Go Live streams
@@ -189,7 +189,7 @@ Every session the account holds receives the Dispatch, including sessions that a
An account chooses whether a friend is told which voice channel it is in. [Modify voice activity sharing](/http-api/users/settings/#modify-voice-activity-sharing) writes the account's default and rewrites the caller's side of every existing friendship to the same value in one operation. It then holds a 24 hour cooldown, and a second attempt inside that window is refused at the `share_voice_activity` path with the validation code `VOICE_ACTIVITY_SHARING_ON_COOLDOWN` and a `retry_after` in seconds.
-The stored result is `share_voice_activity` on the caller's own [relationship object](/http-api/users/relationships/#relationship-object), and `friend_shares_voice_activity` reports the reciprocal record. That reciprocal is resolved by [List relationships](/http-api/users/relationships/#list-relationships) and by the two [Relationship Update](/gateway/events/#relationship-update) Dispatches this operation emits for each rewritten friendship, one to the caller and one to the friend. Every other operation that returns a relationship object reports it as true.
+The stored result is `share_voice_activity` on the caller's own [relationship object](/http-api/users/relationships/#relationship-object), and `friend_shares_voice_activity` reports the reciprocal record. That reciprocal is resolved by [List relationships](/http-api/users/relationships/#list-relationships) and by the [Relationship Update](/gateway/events/#relationship-update) Dispatches this operation emits for each rewritten friendship, one to the caller and one to the friend. Every other operation that returns a relationship object reports it as true.
:::caution[The Gateway does not enforce voice activity sharing]
A [voice state](/gateway/events/#voice-state-object) reaches every session that can view the channel whatever `share_voice_activity` holds, so an account that shares nothing is still visible there. A client MUST NOT present the flag as concealment.